Bläddra i källkod

Manage excluded_products in ranges dashboard (#4073)

File Upload for excluded_products

* Add more tests and fix file upload processor by comma.

---------

Co-authored-by: Joey Jurjens <joey@highbiza.nl>
master
Gunther Waidacher 2 år sedan
förälder
incheckning
1d55f8df07
Inget konto är kopplat till bidragsgivarens mejladress

+ 59
- 10
src/oscar/apps/dashboard/ranges/forms.py Visa fil

8
 
8
 
9
 Product = get_model('catalogue', 'Product')
9
 Product = get_model('catalogue', 'Product')
10
 Range = get_model('offer', 'Range')
10
 Range = get_model('offer', 'Range')
11
+RangeProductFileUpload = get_model("offer", "RangeProductFileUpload")
11
 
12
 
12
 UPC_SET_REGEX = re.compile(r'[^,\s]+')
13
 UPC_SET_REGEX = re.compile(r'[^,\s]+')
13
 
14
 
30
     file_upload = forms.FileField(
31
     file_upload = forms.FileField(
31
         label=_("File of SKUs or UPCs"), required=False, max_length=255,
32
         label=_("File of SKUs or UPCs"), required=False, max_length=255,
32
         help_text=_('Either comma-separated, or one identifier per line'))
33
         help_text=_('Either comma-separated, or one identifier per line'))
34
+    upload_type = forms.CharField(widget=forms.HiddenInput(), required=False)
33
 
35
 
34
     def __init__(self, range, *args, **kwargs):
36
     def __init__(self, range, *args, **kwargs):
35
         self.range = range
37
         self.range = range
36
         super().__init__(*args, **kwargs)
38
         super().__init__(*args, **kwargs)
37
 
39
 
40
+    def clean_query_with_upload_type(self, raw, upload_type):
41
+        # Check that the search matches some products
42
+        ids = set(UPC_SET_REGEX.findall(raw))
43
+        # switch for included or excluded products
44
+        if upload_type == RangeProductFileUpload.EXCLUDED_PRODUCTS_TYPE:
45
+            products = self.range.excluded_products.all()
46
+            action = _('excluded from this range')
47
+        else:
48
+            products = self.range.all_products()
49
+            action = _('added to this range')
50
+        existing_skus = set(products.values_list(
51
+            'stockrecords__partner_sku', flat=True))
52
+        existing_upcs = set(products.values_list('upc', flat=True))
53
+        existing_ids = existing_skus.union(existing_upcs)
54
+        new_ids = ids - existing_ids
55
+        if len(new_ids) == 0:
56
+            self.add_error(
57
+                'query',
58
+                _("The products with SKUs or UPCs matching %(skus)s have "
59
+                  "already been %(action)s") % {'skus': ", ".join(ids),
60
+                                                'action': action}
61
+            )
62
+        else:
63
+            self.products = Product._default_manager.filter(
64
+                Q(stockrecords__partner_sku__in=new_ids)
65
+                | Q(upc__in=new_ids))
66
+            if len(self.products) == 0:
67
+                self.add_error(
68
+                    'query',
69
+                    _("No products exist with a SKU or UPC matching %s")
70
+                    % ", ".join(ids))
71
+            found_skus = set(self.products.values_list(
72
+                'stockrecords__partner_sku', flat=True))
73
+            found_upcs = set(self.products.values_list('upc', flat=True))
74
+            found_ids = found_skus.union(found_upcs)
75
+            self.missing_skus = new_ids - found_ids
76
+            self.duplicate_skus = existing_ids.intersection(ids)
77
+
38
     def clean(self):
78
     def clean(self):
39
         clean_data = super().clean()
79
         clean_data = super().clean()
40
         if not clean_data.get('query') and not clean_data.get('file_upload'):
80
         if not clean_data.get('query') and not clean_data.get('file_upload'):
41
             raise forms.ValidationError(
81
             raise forms.ValidationError(
42
                 _("You must submit either a list of SKU/UPCs or a file"))
82
                 _("You must submit either a list of SKU/UPCs or a file"))
83
+        raw = clean_data['query']
84
+        if raw:
85
+            self.clean_query_with_upload_type(raw, clean_data['upload_type'])
43
         return clean_data
86
         return clean_data
44
 
87
 
88
+    def get_products(self):
89
+        return self.products if hasattr(self, 'products') else []
90
+
91
+    def get_missing_skus(self):
92
+        return self.missing_skus
93
+
94
+    def get_duplicate_skus(self):
95
+        return self.duplicate_skus
96
+
97
+
98
+class RangeExcludedProductForm(RangeProductForm):
99
+    """
100
+    Form to add products in range.excluded_products
101
+    """
102
+
45
     def clean_query(self):
103
     def clean_query(self):
46
         raw = self.cleaned_data['query']
104
         raw = self.cleaned_data['query']
47
         if not raw:
105
         if not raw:
49
 
107
 
50
         # Check that the search matches some products
108
         # Check that the search matches some products
51
         ids = set(UPC_SET_REGEX.findall(raw))
109
         ids = set(UPC_SET_REGEX.findall(raw))
52
-        products = self.range.all_products()
110
+        products = self.range.excluded_products.all()
53
         existing_skus = set(products.values_list(
111
         existing_skus = set(products.values_list(
54
             'stockrecords__partner_sku', flat=True))
112
             'stockrecords__partner_sku', flat=True))
55
         existing_upcs = set(products.values_list('upc', flat=True))
113
         existing_upcs = set(products.values_list('upc', flat=True))
77
         self.duplicate_skus = existing_ids.intersection(ids)
135
         self.duplicate_skus = existing_ids.intersection(ids)
78
 
136
 
79
         return raw
137
         return raw
80
-
81
-    def get_products(self):
82
-        return self.products if hasattr(self, 'products') else []
83
-
84
-    def get_missing_skus(self):
85
-        return self.missing_skus
86
-
87
-    def get_duplicate_skus(self):
88
-        return self.duplicate_skus

+ 96
- 19
src/oscar/apps/dashboard/ranges/views.py Visa fil

4
 from django.contrib import messages
4
 from django.contrib import messages
5
 from django.core import exceptions
5
 from django.core import exceptions
6
 from django.db.models import Count
6
 from django.db.models import Count
7
-from django.http import HttpResponseRedirect
8
-from django.shortcuts import HttpResponse, get_object_or_404
7
+from django.shortcuts import HttpResponse, get_object_or_404, redirect
9
 from django.template.loader import render_to_string
8
 from django.template.loader import render_to_string
10
 from django.urls import reverse
9
 from django.urls import reverse
11
 from django.utils.translation import gettext_lazy as _
10
 from django.utils.translation import gettext_lazy as _
20
 RangeProduct = get_model('offer', 'RangeProduct')
19
 RangeProduct = get_model('offer', 'RangeProduct')
21
 RangeProductFileUpload = get_model('offer', 'RangeProductFileUpload')
20
 RangeProductFileUpload = get_model('offer', 'RangeProductFileUpload')
22
 Product = get_model('catalogue', 'Product')
21
 Product = get_model('catalogue', 'Product')
23
-RangeForm, RangeProductForm = get_classes('dashboard.ranges.forms',
24
-                                          ['RangeForm', 'RangeProductForm'])
22
+RangeForm, RangeProductForm = get_classes(
23
+    'dashboard.ranges.forms', ['RangeForm', 'RangeProductForm'])
25
 
24
 
26
 
25
 
27
 class RangeListView(ListView):
26
 class RangeListView(ListView):
96
     model = Product
95
     model = Product
97
     template_name = 'oscar/dashboard/ranges/range_product_list.html'
96
     template_name = 'oscar/dashboard/ranges/range_product_list.html'
98
     context_object_name = 'products'
97
     context_object_name = 'products'
99
-    actions = ('remove_selected_products', 'add_products')
98
+    actions = ('add_products', 'add_excluded_products',
99
+               'remove_selected_products', 'remove_excluded_products')
100
     form_class = RangeProductForm
100
     form_class = RangeProductForm
101
     paginate_by = settings.OSCAR_DASHBOARD_ITEMS_PER_PAGE
101
     paginate_by = settings.OSCAR_DASHBOARD_ITEMS_PER_PAGE
102
 
102
 
103
+    def get(self, request, **kwargs):
104
+        self.upload_type = request.GET.get('upload_type', "")
105
+        return super().get(request, **kwargs)
106
+
103
     def post(self, request, *args, **kwargs):
107
     def post(self, request, *args, **kwargs):
104
         self.object_list = self.get_queryset()
108
         self.object_list = self.get_queryset()
109
+        self.upload_type = request.POST.get('upload_type', "")
105
         if request.POST.get('action', None) == 'add_products':
110
         if request.POST.get('action', None) == 'add_products':
106
             return self.add_products(request)
111
             return self.add_products(request)
112
+        if request.POST.get('action', None) == 'add_excluded_products':
113
+            return self.add_excluded_products(request)
114
+        if request.POST.get('action', None) == 'remove_excluded_products':
115
+            return self.remove_excluded_products(request)
107
         return super().post(request, *args, **kwargs)
116
         return super().post(request, *args, **kwargs)
108
 
117
 
109
     def get_range(self):
118
     def get_range(self):
120
         range = self.get_range()
129
         range = self.get_range()
121
         ctx['range'] = range
130
         ctx['range'] = range
122
         if 'form' not in ctx:
131
         if 'form' not in ctx:
123
-            ctx['form'] = self.form_class(range)
132
+            ctx['form'] = self.form_class(range, initial={
133
+                "upload_type": RangeProductFileUpload.INCLUDED_PRODUCTS_TYPE})
134
+        if 'form_excluded' not in ctx:
135
+            ctx["form_excluded"] = self.form_class(range, initial={
136
+                "upload_type": RangeProductFileUpload.EXCLUDED_PRODUCTS_TYPE})
137
+        ctx['file_uploads_included'] = range.file_uploads.filter(
138
+            upload_type=RangeProductFileUpload.INCLUDED_PRODUCTS_TYPE)
139
+        ctx['file_uploads_excluded'] = range.file_uploads.filter(
140
+            upload_type=RangeProductFileUpload.EXCLUDED_PRODUCTS_TYPE)
141
+        ctx["upload_type"] = self.upload_type
124
         return ctx
142
         return ctx
125
 
143
 
126
     def remove_selected_products(self, request, products):
144
     def remove_selected_products(self, request, products):
130
         num_products = len(products)
148
         num_products = len(products)
131
         messages.success(
149
         messages.success(
132
             request,
150
             request,
133
-            ngettext("Removed %d product from range", "Removed %d products from range", num_products) % num_products
151
+            ngettext("Removed %d product from range",
152
+                     "Removed %d products from range",
153
+                     num_products) % num_products
154
+        )
155
+        return redirect(
156
+            reverse('dashboard:range-products', kwargs={'pk': range.pk})
134
         )
157
         )
135
-        return HttpResponseRedirect(self.get_success_url(request))
136
 
158
 
137
     def add_products(self, request):
159
     def add_products(self, request):
138
         range = self.get_range()
160
         range = self.get_range()
144
 
166
 
145
         self.handle_query_products(request, range, form)
167
         self.handle_query_products(request, range, form)
146
         self.handle_file_products(request, range, form)
168
         self.handle_file_products(request, range, form)
147
-        return HttpResponseRedirect(self.get_success_url(request))
169
+        return redirect(
170
+            reverse('dashboard:range-products', kwargs={'pk': range.pk})
171
+        )
172
+
173
+    def add_excluded_products(self, request):
174
+        range = self.get_range()
175
+        form = self.form_class(
176
+            range, request.POST, request.FILES, initial={
177
+                "upload_type": RangeProductFileUpload.EXCLUDED_PRODUCTS_TYPE}
178
+        )
179
+        if not form.is_valid():
180
+            ctx = self.get_context_data(form_excluded=form,
181
+                                        object_list=self.object_list)
182
+            return self.render_to_response(ctx)
183
+
184
+        self.handle_query_products(request, range, form)
185
+        self.handle_file_products(request, range, form)
186
+        return redirect(
187
+            reverse('dashboard:range-products', kwargs={'pk': range.pk})
188
+            + '?upload_type=excluded'
189
+        )
190
+
191
+    def remove_excluded_products(self, request):
192
+        product_ids = request.POST.getlist('selected_product', None)
193
+        products = self.model.objects.filter(id__in=product_ids)
194
+        range = self.get_range()
195
+        for product in products:
196
+            range.excluded_products.remove(product)
197
+        num_products = len(products)
198
+        messages.success(
199
+            request,
200
+            ngettext(
201
+                "Removed %d product from excluded list",
202
+                "Removed %d products from excluded list",
203
+                num_products) % num_products
204
+        )
205
+        return redirect(
206
+            reverse('dashboard:range-products', kwargs={'pk': range.pk})
207
+            + '?upload_type=excluded'
208
+        )
148
 
209
 
149
     def handle_query_products(self, request, range, form):
210
     def handle_query_products(self, request, range, form):
150
         products = form.get_products()
211
         products = form.get_products()
151
         if not products:
212
         if not products:
152
             return
213
             return
153
-
154
         for product in products:
214
         for product in products:
155
-            range.add_product(product)
156
-
215
+            if form.cleaned_data["upload_type"] == \
216
+                    RangeProductFileUpload.EXCLUDED_PRODUCTS_TYPE:
217
+                range.excluded_products.add(product)
218
+                action = _('excluded from this range')
219
+            else:
220
+                range.add_product(product)
221
+                action = _('added to this range')
157
         num_products = len(products)
222
         num_products = len(products)
158
         messages.success(
223
         messages.success(
159
             request,
224
             request,
160
-            ngettext("%d product added to range", "%d products added to range", num_products) % num_products
225
+            ngettext("%(num_products)d product has been %(action)s",
226
+                     "%(num_products)d products have been %(action)s",
227
+                     num_products) % {'num_products': num_products,
228
+                                      'action': action}
161
         )
229
         )
162
         dupe_skus = form.get_duplicate_skus()
230
         dupe_skus = form.get_duplicate_skus()
163
         if dupe_skus:
231
         if dupe_skus:
164
             messages.warning(
232
             messages.warning(
165
                 request,
233
                 request,
166
-                _("The products with SKUs or UPCs matching %s are already "
167
-                  "in this range") % ", ".join(dupe_skus))
234
+                _("The products with SKUs or UPCs matching %(skus)s have "
235
+                  "already been %(action)s") % {'skus': ", ".join(dupe_skus),
236
+                                                'action': action})
168
 
237
 
169
         missing_skus = form.get_missing_skus()
238
         missing_skus = form.get_missing_skus()
170
         if missing_skus:
239
         if missing_skus:
178
         if 'file_upload' not in request.FILES:
247
         if 'file_upload' not in request.FILES:
179
             return
248
             return
180
         f = request.FILES['file_upload']
249
         f = request.FILES['file_upload']
181
-        upload = self.create_upload_object(request, range, f)
250
+        upload = self.create_upload_object(
251
+            request, range, f, form.cleaned_data["upload_type"])
182
         products = upload.process(TextIOWrapper(f, encoding=request.encoding))
252
         products = upload.process(TextIOWrapper(f, encoding=request.encoding))
183
         if not upload.was_processing_successful():
253
         if not upload.was_processing_successful():
184
             messages.error(request, upload.error_message)
254
             messages.error(request, upload.error_message)
185
         else:
255
         else:
256
+            if form.cleaned_data["upload_type"] == \
257
+                    RangeProductFileUpload.EXCLUDED_PRODUCTS_TYPE:
258
+                action = "excluded from this range"
259
+            else:
260
+                action = "added to this range"
186
             msg = render_to_string(
261
             msg = render_to_string(
187
                 'oscar/dashboard/ranges/messages/range_products_saved.html',
262
                 'oscar/dashboard/ranges/messages/range_products_saved.html',
188
                 {'range': range,
263
                 {'range': range,
189
-                 'upload': upload})
264
+                 'upload': upload,
265
+                 'action': action})
190
             messages.success(request, msg, extra_tags='safe noicon block')
266
             messages.success(request, msg, extra_tags='safe noicon block')
191
         self.check_imported_products_sku_duplicates(request, products)
267
         self.check_imported_products_sku_duplicates(request, products)
192
 
268
 
193
-    def create_upload_object(self, request, range, f):
269
+    def create_upload_object(self, request, range, f, upload_type):
194
         upload = RangeProductFileUpload.objects.create(
270
         upload = RangeProductFileUpload.objects.create(
195
             range=range,
271
             range=range,
196
             uploaded_by=request.user,
272
             uploaded_by=request.user,
197
             filepath=f.name,
273
             filepath=f.name,
198
-            size=f.size
274
+            size=f.size,
275
+            upload_type=upload_type
199
         )
276
         )
200
         return upload
277
         return upload
201
 
278
 

+ 25
- 4
src/oscar/apps/offer/abstract_models.py Visa fil

1050
         AUTH_USER_MODEL,
1050
         AUTH_USER_MODEL,
1051
         on_delete=models.CASCADE,
1051
         on_delete=models.CASCADE,
1052
         verbose_name=_("Uploaded By"))
1052
         verbose_name=_("Uploaded By"))
1053
-    date_uploaded = models.DateTimeField(_("Date Uploaded"), auto_now_add=True, db_index=True)
1053
+    date_uploaded = models.DateTimeField(_(
1054
+        "Date Uploaded"), auto_now_add=True, db_index=True)
1055
+
1056
+    INCLUDED_PRODUCTS_TYPE = "included"
1057
+    EXCLUDED_PRODUCTS_TYPE = "excluded"
1058
+    UPLOAD_TYPE_CHOICES = [
1059
+        (INCLUDED_PRODUCTS_TYPE, "Included products upload"),
1060
+        (EXCLUDED_PRODUCTS_TYPE, "Excluded products upload"),
1061
+    ]
1062
+    upload_type = models.CharField(
1063
+        max_length=8, choices=UPLOAD_TYPE_CHOICES,
1064
+        default=INCLUDED_PRODUCTS_TYPE)
1054
 
1065
 
1055
     PENDING, FAILED, PROCESSED = 'Pending', 'Failed', 'Processed'
1066
     PENDING, FAILED, PROCESSED = 'Pending', 'Failed', 'Processed'
1056
     choices = (
1067
     choices = (
1099
     def process(self, file_obj):
1110
     def process(self, file_obj):
1100
         """
1111
         """
1101
         Process the file upload and add products to the range
1112
         Process the file upload and add products to the range
1113
+        or add products to range.excluded_products
1102
         """
1114
         """
1103
         all_ids = set(self.extract_ids(file_obj))
1115
         all_ids = set(self.extract_ids(file_obj))
1104
-        products = self.range.all_products()
1116
+        if self.upload_type == self.INCLUDED_PRODUCTS_TYPE:
1117
+            products = self.range.all_products()
1118
+        elif self.upload_type == self.EXCLUDED_PRODUCTS_TYPE:
1119
+            products = self.range.excluded_products.all()
1120
+        else:
1121
+            raise ValueError(
1122
+                "Unable to process upload type: %s" % self.upload_type)
1105
         existing_skus = products.values_list(
1123
         existing_skus = products.values_list(
1106
             'stockrecords__partner_sku', flat=True)
1124
             'stockrecords__partner_sku', flat=True)
1107
         existing_skus = set(filter(bool, existing_skus))
1125
         existing_skus = set(filter(bool, existing_skus))
1115
             models.Q(stockrecords__partner_sku__in=new_ids)
1133
             models.Q(stockrecords__partner_sku__in=new_ids)
1116
             | models.Q(upc__in=new_ids))
1134
             | models.Q(upc__in=new_ids))
1117
         for product in products:
1135
         for product in products:
1118
-            self.range.add_product(product)
1136
+            if self.upload_type == self.INCLUDED_PRODUCTS_TYPE:
1137
+                self.range.add_product(product)
1138
+            else:
1139
+                self.range.excluded_products.add(product)
1119
 
1140
 
1120
         # Processing stats
1141
         # Processing stats
1121
         found_skus = products.values_list(
1142
         found_skus = products.values_list(
1133
         reader = csv.reader(file_obj)
1154
         reader = csv.reader(file_obj)
1134
         for line in reader:
1155
         for line in reader:
1135
             if line:
1156
             if line:
1136
-                yield line[0]
1157
+                yield from line

+ 18
- 0
src/oscar/apps/offer/migrations/0011_rangeproductfileupload_included.py Visa fil

1
+# Generated by Django 3.2.18 on 2023-03-14 11:11
2
+
3
+from django.db import migrations, models
4
+
5
+
6
+class Migration(migrations.Migration):
7
+
8
+    dependencies = [
9
+        ('offer', '0010_conditionaloffer_combinations'),
10
+    ]
11
+
12
+    operations = [
13
+        migrations.AddField(
14
+            model_name='rangeproductfileupload',
15
+            name='upload_type',
16
+            field=models.CharField(choices=[('included', 'Included products upload'), ('excluded', 'Excluded products upload')], default='included', max_length=8),
17
+        ),
18
+    ]

+ 0
- 1
src/oscar/apps/offer/models.py Visa fil

53
 
53
 
54
     __all__.append('RangeProductFileUpload')
54
     __all__.append('RangeProductFileUpload')
55
 
55
 
56
-
57
 # Import the benefits and the conditions. Required after initializing the
56
 # Import the benefits and the conditions. Required after initializing the
58
 # parent models to allow overriding them
57
 # parent models to allow overriding them
59
 
58
 

+ 3
- 3
src/oscar/templates/oscar/dashboard/ranges/messages/range_products_saved.html Visa fil

6
 <ul>
6
 <ul>
7
     {% if upload.num_new_skus %}
7
     {% if upload.num_new_skus %}
8
         <li>
8
         <li>
9
-            {% blocktrans count n=upload.num_new_skus %}
10
-                <strong>{{ n }}</strong> product added
9
+            {% blocktrans with action=action count n=upload.num_new_skus %}
10
+                <strong>{{ n }}</strong> product has been {{ action }}
11
             {% plural %}
11
             {% plural %}
12
-                <strong>{{ n }}</strong> products added
12
+                <strong>{{ n }}</strong> products have been {{ action }}
13
             {% endblocktrans %}
13
             {% endblocktrans %}
14
         </li>
14
         </li>
15
     {% endif %}
15
     {% endif %}

+ 230
- 96
src/oscar/templates/oscar/dashboard/ranges/range_product_list.html Visa fil

30
 
30
 
31
 {% block dashboard_content %}
31
 {% block dashboard_content %}
32
 
32
 
33
-    {% if range.includes_all_products %}
33
+<div class="tabbable dashboard">
34
+
35
+  <ul class="nav nav-tabs mb-0" role="tablist">
36
+    {% block nav_tabs %}
37
+      <li class="nav-item">
38
+        <a class="nav-link {% if not upload_type == "excluded" %}active{% endif %}" href="#included" data-toggle="tab">
39
+          {% trans "Products in range" %}
40
+        </a>
41
+      </li>
42
+      <li class="nav-item">
43
+        <a class="nav-link {% if upload_type == "excluded" %}active{% endif %}" href="#excluded" data-toggle="tab">
44
+          {% trans "Products excluded from range" %}
45
+        </a>
46
+      </li>
47
+      {% endblock nav_tabs %}
48
+  </ul>
49
+
50
+  <div class="tab-content">
51
+
52
+    <div class="tab-pane fade {% if not upload_type == "excluded" %}show active{% endif %}" id="included" role="tabpanel">
53
+
54
+      {% if range.includes_all_products %}
34
         {% trans "This range contains all products. To add products manually, please unselect the 'Includes All Products' option on the edit range page." %}
55
         {% trans "This range contains all products. To add products manually, please unselect the 'Includes All Products' option on the edit range page." %}
35
-    {% else %}
56
+      {% else %}
36
 
57
 
37
-    <div class="table-header">
38
-        <h3>{% trans "Add products" %}</h3>
39
-    </div>
58
+        <div class="table-header">
59
+          <h3>{% trans "Add products" %}</h3>
60
+        </div>
40
 
61
 
41
         <div class="card card-body bg-light">
62
         <div class="card card-body bg-light">
42
 
63
 
43
-            <form method="post" class="form-stacked" enctype="multipart/form-data">
44
-                {% csrf_token %}
45
-                <input type="hidden" name="action" value="add_products"/>
46
-                {% include 'oscar/dashboard/partials/form_fields.html' with form=form %}
47
-                <div class="controls">
48
-                    <button type="submit" class="btn btn-primary" data-loading-text="{% trans 'Running...' %}">{% trans "Go!" %}</button>
49
-                </div>
50
-            </form>
64
+          <form method="post" class="form-stacked" enctype="multipart/form-data">
65
+            {% csrf_token %}
66
+            <input type="hidden" name="action" value="add_products"/>
67
+            {% include 'oscar/dashboard/partials/form_fields.html' with form=form %}
68
+            <div class="controls">
69
+              <button type="submit" class="btn btn-primary" data-loading-text="{% trans 'Running...' %}">
70
+                {% trans "Go!" %}
71
+              </button>
72
+            </div>
73
+          </form>
51
 
74
 
52
-            {% with uploads=range.file_uploads.all %}
53
-                {% if uploads %}
54
-                    <div class="table-header">
55
-                        <h3>{% trans "Upload history" %}</h3>
56
-                    </div>
57
-                    <table class="table table-striped table-bordered table-hover">
58
-                        <thead>
59
-                            <tr>
60
-                                <th>{% trans "Filename" %}</th>
61
-                                <th>{% trans "New products" %}</th>
62
-                                <th>{% trans "Duplicate products" %}</th>
63
-                                <th>{% trans "Unknown products" %}</th>
64
-                                <th>{% trans "Date uploaded" %}</th>
65
-                            </tr>
66
-                        </thead>
67
-                        <tbody>
68
-                            {% for upload in uploads %}
69
-                                <tr>
70
-                                    <td>{{ upload.filepath }}</td>
71
-                                    <td>{{ upload.num_new_skus }}</td>
72
-                                    <td>{{ upload.num_duplicate_skus }}</td>
73
-                                    <td>{{ upload.num_unknown_skus }}</td>
74
-                                    <td>{{ upload.date_uploaded }}</td>
75
-                                </tr>
76
-                            {% endfor %}
77
-                        </tbody>
78
-                    </table>
79
-                {% endif %}
80
-            {% endwith %}
81
-
82
-            {% if products %}
83
-                <form method="post">
84
-                    {% csrf_token %}
85
-                    <table class="table table-striped table-bordered table-hover">
86
-                        <caption>
87
-                            <h3 class="float-left">{% trans "Products in range" %}</h3>
88
-                            <div class="float-right">
89
-                                <input type="hidden" name="action" value="remove_selected_products" />
90
-                                <button type="submit" class="btn btn-secondary" data-loading-text="{% trans 'Removing...' %}">{% trans "Remove selected products" %}</button>
91
-                            </div>
92
-                        </caption>
93
-                        <thead>
94
-                            <tr>
95
-                                <th></th>
96
-                                <th>{% trans "UPC" %}</th>
97
-                                <th>{% trans "Title" context "Product title" %}</th>
98
-                                <th>{% trans "Is product discountable?" %}</th>
99
-                                <th></th>
100
-                            </tr>
101
-                        </thead>
102
-                        <tbody class="product_list">
103
-                        {% for product in products %}
104
-                            <tr id="product_{{ product.pk }}">
105
-                                <td>
106
-                                    <input type="checkbox" name="selected_product" value="{{ product.id }}" />
107
-                                </td>
108
-                                <td>{{ product.upc|default:"-" }}</td>
109
-                                <td><a href="{% url 'dashboard:catalogue-product' pk=product.id %}">{{ product.get_title }}</a></td>
110
-                                <td>{% if product.get_is_discountable %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
111
-                                <td>
112
-                                    <a class="btn btn-danger" href="#" data-behaviours="remove">{% trans "Remove" %}</a>
113
-                                    {% if range.is_reorderable %}
114
-                                        <a href="#" class="btn btn-info btn-handle"><i class="fas fa-arrows-alt"></i> {% trans "Re-order" context "Change the sequence order" %}</a>
115
-                                    {% endif %}
116
-                                </td>
117
-                            </tr>
118
-                        {% endfor %}
119
-                        </tbody>
120
-                    </table>
121
-                    {% include "oscar/dashboard/partials/pagination.html" %}
122
-                </form>
123
-
124
-            {% else %}
125
-                <table class="table table-striped table-bordered table-hover">
126
-                    <caption>
127
-                        {% trans "Products in Range" %}
128
-                    </caption>
129
-                    <tr><td>{% trans "No products found." %}</td></tr>
130
-                </table>
75
+          {% with uploads=file_uploads_included %}
76
+            {% if uploads %}
77
+            <div class="table-header">
78
+              <h3>{% trans "Upload history" %}</h3>
79
+            </div>
80
+            <table class="table table-striped table-bordered table-hover">
81
+              <thead>
82
+                <tr>
83
+                  <th>{% trans "Filename" %}</th>
84
+                  <th>{% trans "New products" %}</th>
85
+                  <th>{% trans "Duplicate products" %}</th>
86
+                  <th>{% trans "Unknown products" %}</th>
87
+                  <th>{% trans "Date uploaded" %}</th>
88
+                </tr>
89
+              </thead>
90
+              <tbody>
91
+                {% for upload in uploads %}
92
+                  <tr>
93
+                    <td>{{ upload.filepath }}</td>
94
+                    <td>{{ upload.num_new_skus }}</td>
95
+                    <td>{{ upload.num_duplicate_skus }}</td>
96
+                    <td>{{ upload.num_unknown_skus }}</td>
97
+                    <td>{{ upload.date_uploaded }}</td>
98
+                  </tr>
99
+                {% endfor %}
100
+              </tbody>
101
+            </table>
131
             {% endif %}
102
             {% endif %}
103
+          {% endwith %}
132
 
104
 
105
+          {% if products %}
106
+          <form method="post">
107
+            {% csrf_token %}
108
+            <table class="table table-striped table-bordered table-hover">
109
+             <caption>
110
+                <h3 class="float-left">{% trans "Products in range" %}</h3>
111
+                <div class="float-right">
112
+                  <input type="hidden" name="action" value="remove_selected_products" />
113
+                  <button type="submit" class="btn btn-secondary" data-loading-text="{% trans 'Removing...' %}">
114
+                    {% trans "Remove selected products" %}
115
+                  </button>
116
+                </div>
117
+              </caption>
118
+              <thead>
119
+                <tr>
120
+                  <th></th>
121
+                  <th>{% trans "UPC" %}</th>
122
+                  <th>{% trans "Title" context "Product title" %}</th>
123
+                  <th>{% trans "Is product discountable?" %}</th>
124
+                  <th></th>
125
+                </tr>
126
+              </thead>
127
+              <tbody class="product_list">
128
+                {% for product in products %}
129
+                  <tr id="product_{{ product.pk }}">
130
+                    <td>
131
+                      <input type="checkbox" name="selected_product" value="{{ product.id }}" />
132
+                    </td>
133
+                    <td>{{ product.upc|default:"-" }}</td>
134
+                    <td><a href="{% url 'dashboard:catalogue-product' pk=product.id %}">{{ product.get_title }}</a></td>
135
+                    <td>{% if product.get_is_discountable %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
136
+                    <td>
137
+                      <a class="btn btn-danger" href="#" data-behaviours="remove">{% trans "Remove" %}</a>
138
+                      {% if range.is_reorderable %}
139
+                        <a href="#" class="btn btn-info btn-handle"><i class="fas fa-arrows-alt"></i>
140
+                           {% trans "Re-order" context "Change the sequence order" %}
141
+                        </a>
142
+                      {% endif %}
143
+                    </td>
144
+                  </tr>
145
+                {% endfor %}
146
+              </tbody>
147
+            </table>
148
+            {% include "oscar/dashboard/partials/pagination.html" %}
149
+          </form>
150
+
151
+          {% else %}
152
+          <table class="table table-striped table-bordered table-hover">
153
+            <caption>
154
+              {% trans "Products in Range" %}
155
+            </caption>
156
+            <tr><td>{% trans "No products found." %}</td></tr>
157
+          </table>
158
+          {% endif %}
159
+
160
+        </div>
161
+
162
+      {% endif %}
133
 
163
 
134
     </div>
164
     </div>
135
-        {% endif %}
136
-        <div class="form-actions">
137
-            <a href="{% url 'dashboard:range-update' pk=range.id %}" class="btn btn-primary">{% trans "Edit range" %}</a> {% trans "or" %}
138
-            <a href="{% url 'dashboard:range-list' %}" class="">{% trans "return to range list" %}</a>
165
+
166
+    <div class="tab-pane fade {% if upload_type == "excluded" %}show active{% endif %}" id="excluded" role="tabpanel">
167
+
168
+        <div class="table-header">
169
+          <h3>{% trans "Exclude products" %}</h3>
139
         </div>
170
         </div>
140
 
171
 
172
+        <div class="card card-body bg-light">
173
+
174
+          <form method="post" class="form-stacked" enctype="multipart/form-data">
175
+            {% csrf_token %}
176
+            <input type="hidden" name="action" value="add_excluded_products"/>
177
+            {% include 'oscar/dashboard/partials/form_fields.html' with form=form_excluded %}
178
+            <div class="controls">
179
+              <button type="submit" class="btn btn-primary" data-loading-text="{% trans 'Running...' %}">
180
+                {% trans "Go!" %}
181
+              </button>
182
+            </div>
183
+          </form>
184
+
185
+          {% with uploads=file_uploads_excluded %}
186
+          {% if uploads %}
187
+            <div class="table-header">
188
+              <h3>{% trans "Upload history" %}</h3>
189
+            </div>
190
+            <table class="table table-striped table-bordered table-hover">
191
+              <thead>
192
+                <tr>
193
+                  <th>{% trans "Filename" %}</th>
194
+                  <th>{% trans "New products" %}</th>
195
+                  <th>{% trans "Duplicate products" %}</th>
196
+                  <th>{% trans "Unknown products" %}</th>
197
+                  <th>{% trans "Date uploaded" %}</th>
198
+                </tr>
199
+              </thead>
200
+              <tbody>
201
+                {% for upload in uploads %}
202
+                  <tr>
203
+                    <td>{{ upload.filepath }}</td>
204
+                    <td>{{ upload.num_new_skus }}</td>
205
+                    <td>{{ upload.num_duplicate_skus }}</td>
206
+                    <td>{{ upload.num_unknown_skus }}</td>
207
+                    <td>{{ upload.date_uploaded }}</td>
208
+                  </tr>
209
+                {% endfor %}
210
+              </tbody>
211
+            </table>
212
+          {% endif %}
213
+          {% endwith %}
214
+
215
+          {% if range.excluded_products.count %}
216
+            <form method="post">
217
+              {% csrf_token %}
218
+              <table class="table table-striped table-bordered table-hover">
219
+               <caption>
220
+                  <h3 class="float-left">{% trans "Products excluded from range" %}</h3>
221
+                  <div class="float-right">
222
+                    <input type="hidden" name="action" value="remove_excluded_products" />
223
+                    <button type="submit" class="btn btn-secondary" data-loading-text="{% trans 'Removing...' %}">
224
+                      {% trans "Remove selected excluded products" %}
225
+                    </button>
226
+                  </div>
227
+                </caption>
228
+                <thead>
229
+                  <tr>
230
+                    <th></th>
231
+                    <th>{% trans "UPC" %}</th>
232
+                    <th>{% trans "Title" context "Product title" %}</th>
233
+                    <th>{% trans "Is product discountable?" %}</th>
234
+                    <th></th>
235
+                  </tr>
236
+                </thead>
237
+                <tbody class="product_list">
238
+                  {% for product in range.excluded_products.all %}
239
+                    <tr id="product_{{ product.pk }}">
240
+                      <td>
241
+                        <input type="checkbox" name="selected_product" value="{{ product.id }}" />
242
+                      </td>
243
+                      <td>{{ product.upc|default:"-" }}</td>
244
+                      <td><a href="{% url 'dashboard:catalogue-product' pk=product.id %}">{{ product.get_title }}</a></td>
245
+                      <td>{% if product.get_is_discountable %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
246
+                      <td><a class="btn btn-danger" href="#" data-behaviours="remove">{% trans "Remove" %}</a></td>
247
+                    </tr>
248
+                  {% endfor %}
249
+                </tbody>
250
+              </table>
251
+            </form>
252
+
253
+          {% else %}
254
+            <table class="table table-striped table-bordered table-hover">
255
+              <caption>
256
+                {% trans "Products excluded from Range" %}
257
+              </caption>
258
+              <tr><td>{% trans "No products found." %}</td></tr>
259
+            </table>
260
+          {% endif %}
261
+
262
+        </div>
263
+
264
+    </div>
265
+
266
+  </div>
267
+
268
+</div>
269
+
270
+<div class="form-actions">
271
+  <a href="{% url 'dashboard:range-update' pk=range.id %}" class="btn btn-primary">{% trans "Edit range" %}</a> {% trans "or" %}
272
+  <a href="{% url 'dashboard:range-list' %}" class="">{% trans "return to range list" %}</a>
273
+</div>
274
+
141
 {% endblock dashboard_content %}
275
 {% endblock dashboard_content %}
142
 
276
 
143
 {% block onbodyload %}
277
 {% block onbodyload %}

+ 161
- 10
tests/functional/dashboard/test_range.py Visa fil

85
 
85
 
86
     def test_upload_file_with_skus(self):
86
     def test_upload_file_with_skus(self):
87
         range_products_page = self.get(self.url)
87
         range_products_page = self.get(self.url)
88
-        form = range_products_page.form
88
+        form = range_products_page.forms[0]
89
         form['file_upload'] = Upload('new_skus.txt', b'456')
89
         form['file_upload'] = Upload('new_skus.txt', b'456')
90
         form.submit().follow()
90
         form.submit().follow()
91
         all_products = self.range.all_products()
91
         all_products = self.range.all_products()
94
         range_product_file_upload = RangeProductFileUpload.objects.get()
94
         range_product_file_upload = RangeProductFileUpload.objects.get()
95
         self.assertEqual(range_product_file_upload.range, self.range)
95
         self.assertEqual(range_product_file_upload.range, self.range)
96
         self.assertEqual(range_product_file_upload.num_new_skus, 1)
96
         self.assertEqual(range_product_file_upload.num_new_skus, 1)
97
-        self.assertEqual(range_product_file_upload.status, RangeProductFileUpload.PROCESSED)
97
+        self.assertEqual(range_product_file_upload.status,
98
+                         RangeProductFileUpload.PROCESSED)
98
         self.assertEqual(range_product_file_upload.size, 3)
99
         self.assertEqual(range_product_file_upload.size, 3)
99
 
100
 
101
+    def test_upload_excluded_file_with_skus(self):
102
+        excluded_products = self.range.excluded_products.all()
103
+        self.assertEqual(len(excluded_products), 0)
104
+        self.assertFalse(self.product3 in excluded_products)
105
+
106
+        # Upload the product
107
+        range_products_page = self.get(self.url)
108
+        form = range_products_page.forms[1]
109
+        form['file_upload'] = Upload('new_skus.txt', b'456')
110
+        form.submit().follow()
111
+
112
+        excluded_products = self.range.excluded_products.all()
113
+        self.assertEqual(len(excluded_products), 1)
114
+        self.assertTrue(self.product3 in excluded_products)
115
+
116
+        range_product_file_upload = RangeProductFileUpload.objects.get()
117
+        self.assertEqual(range_product_file_upload.range, self.range)
118
+        self.assertEqual(range_product_file_upload.num_new_skus, 1)
119
+        self.assertEqual(range_product_file_upload.status,
120
+                         RangeProductFileUpload.PROCESSED)
121
+        self.assertEqual(range_product_file_upload.size, 3)
122
+
123
+    def test_upload_multiple_excluded_file_with_skus(self):
124
+        excluded_products = self.range.excluded_products.all()
125
+        self.assertEqual(len(excluded_products), 0)
126
+        self.assertFalse(self.product3 in excluded_products)
127
+        self.assertFalse(self.product4 in excluded_products)
128
+
129
+        # Upload the products
130
+        range_products_page = self.get(self.url)
131
+        form = range_products_page.forms[1]
132
+        form['file_upload'] = Upload('new_skus.txt', b'456,789')
133
+        form.submit().follow()
134
+
135
+        excluded_products = self.range.excluded_products.all()
136
+        self.assertEqual(len(excluded_products), 2)
137
+        self.assertTrue(self.product3 in excluded_products)
138
+        self.assertTrue(self.product4 in excluded_products)
139
+
140
+        range_product_file_upload = RangeProductFileUpload.objects.get()
141
+        self.assertEqual(range_product_file_upload.range, self.range)
142
+        self.assertEqual(range_product_file_upload.num_new_skus, 2)
143
+        self.assertEqual(range_product_file_upload.status,
144
+                         RangeProductFileUpload.PROCESSED)
145
+        self.assertEqual(range_product_file_upload.size, 7)
146
+
147
+    def test_exclude_skus_textarea_form_field(self):
148
+        excluded_products = self.range.excluded_products.all()
149
+        self.assertEqual(len(excluded_products), 0)
150
+        self.assertFalse(self.product3 in excluded_products)
151
+
152
+        range_products_page = self.get(self.url)
153
+        form = range_products_page.forms[1]
154
+        form['query'] = '456'
155
+        form.submit().follow()
156
+
157
+        excluded_products = self.range.excluded_products.all()
158
+        self.assertEqual(len(excluded_products), 1)
159
+        self.assertTrue(self.product3 in excluded_products)
160
+
161
+    def test_exclude_multiple_skus_textarea_form_field(self):
162
+        excluded_products = self.range.excluded_products.all()
163
+        self.assertEqual(len(excluded_products), 0)
164
+        self.assertFalse(self.product3 in excluded_products)
165
+        self.assertFalse(self.product4 in excluded_products)
166
+
167
+        range_products_page = self.get(self.url)
168
+        form = range_products_page.forms[1]
169
+        form['query'] = '456,789'
170
+        form.submit().follow()
171
+
172
+        excluded_products = self.range.excluded_products.all()
173
+        self.assertEqual(len(excluded_products), 2)
174
+        self.assertTrue(self.product3 in excluded_products)
175
+        self.assertTrue(self.product4 in excluded_products)
176
+
100
     def test_dupe_skus_warning(self):
177
     def test_dupe_skus_warning(self):
101
         self.range.add_product(self.product3)
178
         self.range.add_product(self.product3)
102
         range_products_page = self.get(self.url)
179
         range_products_page = self.get(self.url)
106
         self.assertEqual(list(response.context['messages']), [])
183
         self.assertEqual(list(response.context['messages']), [])
107
         self.assertEqual(
184
         self.assertEqual(
108
             response.context['form'].errors['query'],
185
             response.context['form'].errors['query'],
109
-            ['The products with SKUs or UPCs matching 456 are already in this range']
186
+            ['The products with SKUs or UPCs matching 456 have already been added to this range']
110
         )
187
         )
111
 
188
 
112
         form = response.forms[0]
189
         form = response.forms[0]
115
         messages = list(response.context['messages'])
192
         messages = list(response.context['messages'])
116
         self.assertEqual(len(messages), 2)
193
         self.assertEqual(len(messages), 2)
117
         self.assertEqual(messages[0].level, SUCCESS)
194
         self.assertEqual(messages[0].level, SUCCESS)
118
-        self.assertEqual(messages[0].message, '1 product added to range')
195
+        self.assertEqual(messages[0].message, '1 product has been added to this range')
196
+        self.assertEqual(messages[1].level, WARNING)
197
+        self.assertEqual(
198
+            messages[1].message,
199
+            'The products with SKUs or UPCs matching 456 have already been added to this range'
200
+        )
201
+
202
+    def test_dupe_excluded_skus_warning(self):
203
+        self.range.add_product(self.product3)
204
+        self.range.add_product(self.product4)
205
+        self.range.excluded_products.add(self.product3)
206
+        range_products_page = self.get(self.url)
207
+        form = range_products_page.forms[2]
208
+        form['query'] = '456'
209
+        response = form.submit()
210
+        self.assertEqual(list(response.context['messages']), [])
211
+        self.assertEqual(
212
+            response.context['form_excluded'].errors['query'],
213
+            ['The products with SKUs or UPCs matching 456 have already been excluded from this range']
214
+        )
215
+
216
+        form = response.forms[2]
217
+        form['query'] = '456, 789'
218
+        response = form.submit().follow()
219
+        messages = list(response.context['messages'])
220
+        self.assertEqual(len(messages), 2)
221
+        self.assertEqual(messages[0].level, SUCCESS)
222
+        self.assertEqual(messages[0].message, '1 product has been excluded from this range')
119
         self.assertEqual(messages[1].level, WARNING)
223
         self.assertEqual(messages[1].level, WARNING)
120
         self.assertEqual(
224
         self.assertEqual(
121
             messages[1].message,
225
             messages[1].message,
122
-            'The products with SKUs or UPCs matching 456 are already in this range'
226
+            'The products with SKUs or UPCs matching 456 have already been excluded from this range'
123
         )
227
         )
124
 
228
 
125
     def test_missing_skus_warning(self):
229
     def test_missing_skus_warning(self):
126
         range_products_page = self.get(self.url)
230
         range_products_page = self.get(self.url)
127
-        form = range_products_page.form
231
+        form = range_products_page.forms[0]
128
         form['query'] = '321'
232
         form['query'] = '321'
129
         response = form.submit()
233
         response = form.submit()
130
         self.assertEqual(list(response.context['messages']), [])
234
         self.assertEqual(list(response.context['messages']), [])
132
             response.context['form'].errors['query'],
236
             response.context['form'].errors['query'],
133
             ['No products exist with a SKU or UPC matching 321']
237
             ['No products exist with a SKU or UPC matching 321']
134
         )
238
         )
135
-        form = range_products_page.form
239
+        form = range_products_page.forms[0]
136
         form['query'] = '456, 321'
240
         form['query'] = '456, 321'
137
         response = form.submit().follow()
241
         response = form.submit().follow()
138
         messages = list(response.context['messages'])
242
         messages = list(response.context['messages'])
139
         self.assertEqual(len(messages), 2)
243
         self.assertEqual(len(messages), 2)
140
         self.assertEqual(messages[0].level, SUCCESS)
244
         self.assertEqual(messages[0].level, SUCCESS)
141
-        self.assertEqual(messages[0].message, '1 product added to range')
245
+        self.assertEqual(messages[0].message, '1 product has been added to this range')
142
         self.assertEqual(messages[1].level, WARNING)
246
         self.assertEqual(messages[1].level, WARNING)
143
         self.assertEqual(
247
         self.assertEqual(
144
             messages[1].message, 'No product(s) were found with SKU or UPC matching 321'
248
             messages[1].message, 'No product(s) were found with SKU or UPC matching 321'
146
 
250
 
147
     def test_same_skus_within_different_products_warning_query(self):
251
     def test_same_skus_within_different_products_warning_query(self):
148
         range_products_page = self.get(self.url)
252
         range_products_page = self.get(self.url)
149
-        form = range_products_page.form
253
+        form = range_products_page.forms[0]
150
         form['query'] = '123123'
254
         form['query'] = '123123'
151
         response = form.submit().follow()
255
         response = form.submit().follow()
152
         messages = list(response.context['messages'])
256
         messages = list(response.context['messages'])
158
 
262
 
159
     def test_same_skus_within_different_products_warning_file_upload(self):
263
     def test_same_skus_within_different_products_warning_file_upload(self):
160
         range_products_page = self.get(self.url)
264
         range_products_page = self.get(self.url)
161
-        form = range_products_page.form
265
+        form = range_products_page.forms[0]
162
         form['file_upload'] = Upload('skus.txt', b'123123')
266
         form['file_upload'] = Upload('skus.txt', b'123123')
163
         response = form.submit().follow()
267
         response = form.submit().follow()
164
         messages = list(response.context['messages'])
268
         messages = list(response.context['messages'])
210
         self.assertTrue(self.range.contains_product(self.child2))
314
         self.assertTrue(self.range.contains_product(self.child2))
211
         self.assertFalse(self.range.contains_product(self.parent))
315
         self.assertFalse(self.range.contains_product(self.parent))
212
 
316
 
317
+    def test_remove_selected_product(self):
318
+        self.range.add_product(self.product3)
319
+        range_products_page = self.get(self.url)
320
+        form = range_products_page.forms[1]
321
+        form['selected_product'] = '456'
322
+        response = form.submit().follow()
323
+        messages = list(response.context['messages'])
324
+        self.assertEqual(len(messages), 1)
325
+        self.assertEqual(messages[0].level, SUCCESS)
326
+        self.assertEqual(messages[0].message, 'Removed 1 product from range')
327
+        self.assertFalse(self.range.contains_product(self.product3))
328
+        self.assertTrue(self.product3 in self.range.excluded_products.all())
329
+
330
+    def test_remove_excluded_product(self):
331
+        self.range.add_product(self.product3)
332
+        self.range.excluded_products.add(self.product3)
333
+        self.assertFalse(self.range.contains_product(self.product3))
334
+
335
+        # Remove the product from exclusion form
336
+        range_products_page = self.get(self.url)
337
+        form = range_products_page.forms[2]
338
+        form['selected_product'] = '456'
339
+        response = form.submit().follow()
340
+
341
+        messages = list(response.context['messages'])
342
+        self.assertEqual(len(messages), 1)
343
+        self.assertEqual(messages[0].level, SUCCESS)
344
+        self.assertEqual(messages[0].message, 'Removed 1 product from excluded list')
345
+        self.assertTrue(self.range.contains_product(self.product3))
346
+
347
+    def test_remove_multiple_excluded_products(self):
348
+        self.test_upload_multiple_excluded_file_with_skus()
349
+        self.assertIn(self.product3, self.range.excluded_products.all())
350
+        self.assertIn(self.product4, self.range.excluded_products.all())
351
+
352
+        range_products_page = self.get(self.url)
353
+        form = range_products_page.forms[2]
354
+        form['selected_product'] = [self.product3.pk, self.product4.pk]
355
+        response = form.submit().follow()
356
+
357
+        messages = list(response.context['messages'])
358
+        self.assertEqual(len(messages), 1)
359
+        self.assertEqual(messages[0].level, SUCCESS)
360
+        self.assertEqual(messages[0].message, 'Removed 2 products from excluded list')
361
+        self.assertNotIn(self.product3, self.range.excluded_products.all())
362
+        self.assertNotIn(self.product4, self.range.excluded_products.all())
363
+
213
 
364
 
214
 class RangeReorderViewTest(WebTestCase):
365
 class RangeReorderViewTest(WebTestCase):
215
     is_staff = True
366
     is_staff = True

Laddar…
Avbryt
Spara