Преглед изворни кода

Merge branch 'feature/variants_dashboard'

master
Maik Hoepfel пре 11 година
родитељ
комит
1abd7b25a2

+ 16
- 0
docs/source/releases/v0.8.rst Прегледај датотеку

@@ -82,6 +82,15 @@ has been altered to be:
82 82
 Some properties and method names have also been updated to the new naming. The
83 83
 old ones will throw a deprecation warning.
84 84
 
85
+Better handling of child products in product dashboard
86
+------------------------------------------------------
87
+Together with the changes above, the dashboard experience for child products
88
+has been improved. The difference between a parent product and a stand-alone
89
+product is hidden from the user; a user can now add and remove child products
90
+on any suitable product. When the first child product is added, a stand-alone
91
+product becomes a parent product; and vice versa.
92
+In the front-end, the old name of "product variants" has been kept.
93
+
85 94
 Django 1.7 support
86 95
 ------------------
87 96
 
@@ -296,6 +305,13 @@ the following renaming pattern:
296 305
 * ``variants`` becomes ``children``
297 306
 * ``variant`` becomes ``child``
298 307
 
308
+Product editing
309
+---------------
310
+The dashboard improvements for child products meant slight changes to both
311
+``ProductCreateUpdateView`` and ``ProductForm``. Notably, ``ProductForm`` now
312
+gets a ``parent`` kwarg. Please review your customisations for compatibility
313
+with the updated code.
314
+
299 315
 .. _incompatible_shipping_changes_in_0.8:
300 316
 
301 317
 Shipping

+ 54
- 19
oscar/apps/catalogue/abstract_models.py Прегледај датотеку

@@ -292,6 +292,8 @@ class AbstractProduct(models.Model):
292 292
     #: Determines if a product may be used in an offer. It is illegal to
293 293
     #: discount some types of product (e.g. ebooks) and this field helps
294 294
     #: merchants from avoiding discounting such products
295
+    #: Note that this flag is ignored for child products; they inherit from
296
+    #: the parent product.
295 297
     is_discountable = models.BooleanField(
296 298
         _("Is discountable?"), default=True, help_text=_(
297 299
             "This flag indicates if this product can be used in an offer "
@@ -327,19 +329,23 @@ class AbstractProduct(models.Model):
327 329
         """
328 330
         Validate a product. Those are the rules:
329 331
 
330
-        +---------------+-------------+--------------+-----------+
331
-        |               | stand alone | parent       | child     |
332
-        +---------------+-------------+--------------+-----------+
333
-        | title         | required    | required     | optional  |
334
-        +---------------+-------------+--------------+-----------+
335
-        | product class | required    | must be None | required  |
336
-        +---------------+-------------+--------------+-----------+
337
-        | parent        | forbidden   | forbidden    | required  |
338
-        +---------------+-------------+--------------+-----------+
339
-        | stockrecords  | 0 or more   | forbidden    | required  |
340
-        +---------------+-------------+--------------+-----------+
341
-        | categories    | 1 or more   | 1 or more    | forbidden |
342
-        +---------------+-------------+--------------+-----------+
332
+        +---------------+-------------+--------------+--------------+
333
+        |               | stand alone | parent       | child        |
334
+        +---------------+-------------+--------------+--------------+
335
+        | title         | required    | required     | optional     |
336
+        +---------------+-------------+--------------+--------------+
337
+        | product class | required    | required     | must be None |
338
+        +---------------+-------------+--------------+--------------+
339
+        | parent        | forbidden   | forbidden    | required     |
340
+        +---------------+-------------+--------------+--------------+
341
+        | stockrecords  | 0 or more   | forbidden    | required     |
342
+        +---------------+-------------+--------------+--------------+
343
+        | categories    | 1 or more   | 1 or more    | forbidden    |
344
+        +---------------+-------------+--------------+--------------+
345
+        | attributes    | optional    | optional     | optional     |
346
+        +---------------+-------------+--------------+--------------+
347
+        | rec. products | optional    | optional     | unsupported  |
348
+        +---------------+-------------+--------------+--------------+
343 349
 
344 350
         Because the validation logic is quite complex, validation is delegated
345 351
         to the sub method appropriate for the product's structure.
@@ -368,6 +374,9 @@ class AbstractProduct(models.Model):
368 374
         if self.parent_id and not self.parent.is_parent:
369 375
             raise ValidationError(
370 376
                 _("You can only assign child products to parent products."))
377
+        if self.product_class:
378
+            raise ValidationError(
379
+                _("A child product can't have a product class."))
371 380
 
372 381
     def _clean_parent(self):
373 382
         """
@@ -398,6 +407,23 @@ class AbstractProduct(models.Model):
398 407
     def is_child(self):
399 408
         return self.structure == self.CHILD
400 409
 
410
+    def can_be_parent(self, give_reason=False):
411
+        """
412
+        Helps decide if a the product can be turned into a parent product.
413
+        """
414
+        reason = None
415
+        if self.is_child:
416
+            reason = _('The specified parent product is a child product.')
417
+        if self.has_stockrecords:
418
+            reason = _(
419
+                "One can't add a child product to a product with stock"
420
+                " records.")
421
+        is_valid = reason is None
422
+        if give_reason:
423
+            return is_valid, reason
424
+        else:
425
+            return is_valid
426
+
401 427
     @property
402 428
     def options(self):
403 429
         pclass = self.get_product_class()
@@ -506,7 +532,7 @@ class AbstractProduct(models.Model):
506 532
         """
507 533
         return self._min_child_price('price_excl_tax')
508 534
 
509
-    # Wrappers
535
+    # Wrappers for child products
510 536
 
511 537
     def get_title(self):
512 538
         """
@@ -520,15 +546,24 @@ class AbstractProduct(models.Model):
520 546
 
521 547
     def get_product_class(self):
522 548
         """
523
-        Return a product's item class
549
+        Return a product's item class. Child products inherit their parent's.
524 550
         """
525
-        if self.product_class_id or self.product_class:
526
-            return self.product_class
527
-        if self.parent and self.parent.product_class:
551
+        if self.is_child:
528 552
             return self.parent.product_class
529
-        return None
553
+        else:
554
+            return self.product_class
530 555
     get_product_class.short_description = _("Product class")
531 556
 
557
+    def get_is_discountable(self):
558
+        """
559
+        At the moment, is_discountable can't be set individually for child
560
+        products; they inherit it from their parent.
561
+        """
562
+        if self.is_child:
563
+            return self.parent.is_discountable
564
+        else:
565
+            return self.is_discountable
566
+
532 567
     def get_categories(self):
533 568
         """
534 569
         Return a product's categories or parent's if there is a parent product.

+ 3
- 1
oscar/apps/dashboard/catalogue/app.py Прегледај датотеку

@@ -64,6 +64,9 @@ class CatalogueApplication(Application):
64 64
             url(r'^products/create/(?P<product_class_slug>[\w-]+)/$',
65 65
                 self.product_createupdate_view.as_view(),
66 66
                 name='catalogue-product-create'),
67
+            url(r'^products/(?P<parent_pk>[-\d]+)/create-variant/$',
68
+                self.product_createupdate_view.as_view(),
69
+                name='catalogue-product-create-child'),
67 70
             url(r'^products/(?P<pk>\d+)/delete/$',
68 71
                 self.product_delete_view.as_view(),
69 72
                 name='catalogue-product-delete'),
@@ -101,7 +104,6 @@ class CatalogueApplication(Application):
101 104
             url(r'^product-type/(?P<pk>\d+)/delete/$',
102 105
                 self.product_class_delete_view.as_view(),
103 106
                 name='catalogue-class-delete'),
104
-
105 107
         ]
106 108
         return self.post_process_urls(urls)
107 109
 

+ 62
- 49
oscar/apps/dashboard/catalogue/forms.py Прегледај датотеку

@@ -1,5 +1,5 @@
1 1
 from django import forms
2
-from django.core.exceptions import ValidationError, MultipleObjectsReturned
2
+from django.core import exceptions
3 3
 from django.forms.models import inlineformset_factory
4 4
 from django.utils.translation import ugettext_lazy as _
5 5
 from treebeard.forms import MoveNodeForm, movenodeform_factory
@@ -12,8 +12,6 @@ Product = get_model('catalogue', 'Product')
12 12
 ProductClass = get_model('catalogue', 'ProductClass')
13 13
 Category = get_model('catalogue', 'Category')
14 14
 StockRecord = get_model('partner', 'StockRecord')
15
-Partner = get_model('partner', 'Partner')
16
-ProductAttributeValue = get_model('catalogue', 'ProductAttributeValue')
17 15
 ProductCategory = get_model('catalogue', 'ProductCategory')
18 16
 ProductImage = get_model('catalogue', 'ProductImage')
19 17
 ProductRecommendation = get_model('catalogue', 'ProductRecommendation')
@@ -147,7 +145,8 @@ class StockRecordFormSet(BaseStockRecordFormSet):
147 145
         if self.require_user_stockrecord:
148 146
             try:
149 147
                 user_partner = self.user.partners.get()
150
-            except (Partner.DoesNotExist, MultipleObjectsReturned):
148
+            except (exceptions.ObjectDoesNotExist,
149
+                    exceptions.MultipleObjectsReturned):
151 150
                 pass
152 151
             else:
153 152
                 partner_field = self.forms[0].fields.get('partner', None)
@@ -172,9 +171,9 @@ class StockRecordFormSet(BaseStockRecordFormSet):
172 171
                                         for form in self.forms])
173 172
             user_partners = set(self.user.partners.all())
174 173
             if not user_partners & stockrecord_partners:
175
-                raise ValidationError(_("At least one stock record must be set"
176
-                                        " to a partner that you're associated"
177
-                                        " with."))
174
+                raise exceptions.ValidationError(
175
+                    _("At least one stock record must be set to a partner that"
176
+                      "you're associated with."))
178 177
 
179 178
 
180 179
 def _attr_text_field(attribute):
@@ -265,52 +264,67 @@ class ProductForm(forms.ModelForm):
265 264
     class Meta:
266 265
         model = Product
267 266
         fields = [
268
-            'title', 'upc', 'description',
269
-            'is_discountable',
270
-            'structure', 'parent']
267
+            'title', 'upc', 'description', 'is_discountable', 'structure']
271 268
         widgets = {
272
-            'parent': ProductSelect,
269
+            'structure': forms.HiddenInput()
273 270
         }
274 271
 
275
-    def __init__(self, product_class, data=None, *args, **kwargs):
276
-        self.set_initial_attribute_values(product_class, kwargs)
272
+    def __init__(self, product_class, data=None, parent=None, *args, **kwargs):
273
+        self.set_initial(product_class, parent, kwargs)
277 274
         super(ProductForm, self).__init__(data, *args, **kwargs)
278
-        self.instance.product_class = product_class
279
-
280
-        # This is quite nasty.  We use the raw posted data to determine if the
281
-        # product is a parent product, as this changes the validation rules we
282
-        # want to apply.
283
-        is_parent = data and data.get('structure', '') == 'parent'
284
-        self.add_attribute_fields(is_parent)
285
-
286
-        parent = self.fields.get('parent', None)
287
-        if parent is not None:
288
-            parent.queryset = self.get_parent_products_queryset()
275
+        if parent:
276
+            self.instance.parent = parent
277
+            # We need to set the correct product structures explicitly to pass
278
+            # attribute validation and child product validation. Note that
279
+            # those changes are not persisted.
280
+            self.instance.structure = Product.CHILD
281
+            self.instance.parent.structure = Product.PARENT
282
+
283
+            self.delete_non_child_fields()
284
+        else:
285
+            # Only set product class for non-child products
286
+            self.instance.product_class = product_class
287
+        self.add_attribute_fields(product_class, self.instance.is_parent)
289 288
 
290 289
         if 'title' in self.fields:
291 290
             self.fields['title'].widget = forms.TextInput(
292 291
                 attrs={'autocomplete': 'off'})
293 292
 
293
+    def set_initial(self, product_class, parent, kwargs):
294
+        """
295
+        Set initial data for the form. Sets the correct product structure
296
+        and fetches initial values for the dynamically constructed attribute
297
+        fields.
298
+        """
299
+        if 'initial' not in kwargs:
300
+            kwargs['initial'] = {}
301
+        self.set_initial_attribute_values(product_class, kwargs)
302
+        if parent:
303
+            kwargs['initial']['structure'] = Product.CHILD
304
+
294 305
     def set_initial_attribute_values(self, product_class, kwargs):
295 306
         """
296 307
         Update the kwargs['initial'] value to have the initial values based on
297 308
         the product instance's attributes
298 309
         """
299
-        if kwargs.get('instance', None) is None:
310
+        instance = kwargs.get('instance')
311
+        if instance is None:
300 312
             return
301
-        if 'initial' not in kwargs:
302
-            kwargs['initial'] = {}
303 313
         for attribute in product_class.attributes.all():
304 314
             try:
305
-                value = kwargs['instance'].attribute_values.get(
315
+                value = instance.attribute_values.get(
306 316
                     attribute=attribute).value
307
-            except ProductAttributeValue.DoesNotExist:
317
+            except exceptions.ObjectDoesNotExist:
308 318
                 pass
309 319
             else:
310 320
                 kwargs['initial']['attr_%s' % attribute.code] = value
311 321
 
312
-    def add_attribute_fields(self, is_parent=False):
313
-        for attribute in self.instance.product_class.attributes.all():
322
+    def add_attribute_fields(self, product_class, is_parent=False):
323
+        """
324
+        For each attribute specified by the product class, this method
325
+        dynamically adds form fields to the product form.
326
+        """
327
+        for attribute in product_class.attributes.all():
314 328
             field = self.get_attribute_field(attribute)
315 329
             if field:
316 330
                 self.fields['attr_%s' % attribute.code] = field
@@ -319,31 +333,30 @@ class ProductForm(forms.ModelForm):
319 333
                     self.fields['attr_%s' % attribute.code].required = False
320 334
 
321 335
     def get_attribute_field(self, attribute):
336
+        """
337
+        Gets the correct form field for a given attribute type.
338
+        """
322 339
         return self.FIELD_FACTORIES[attribute.type](attribute)
323 340
 
324
-    def get_parent_products_queryset(self):
341
+    def delete_non_child_fields(self):
325 342
         """
326
-        :return: Parent products, minus this product
343
+        Deletes any fields not needed for child products. Override this if
344
+        you want to e.g. keep the description field.
327 345
         """
328
-        # Not using Product.browsable because a deployment might override
329
-        # that manager to respect a status field or such like
330
-        queryset = Product._default_manager.filter(structure=Product.PARENT)
331
-        if self.instance.pk is not None:
332
-            # Prevent selecting itself as parent
333
-            queryset = queryset.exclude(pk=self.instance.pk)
334
-        return queryset
335
-
336
-    def save(self):
346
+        for field_name in ['description', 'is_discountable']:
347
+            if field_name in self.fields:
348
+                del self.fields[field_name]
349
+
350
+    def _post_clean(self):
337 351
         """
338
-        Set product class and attributes before saving
352
+        Set attributes before ModelForm calls the product's clean method
353
+        (which it does in _post_clean), which in turn validates attributes.
339 354
         """
340
-        product = super(ProductForm, self).save(commit=False)
341
-        for attribute in self.instance.product_class.attributes.all():
355
+        product_class = self.instance.get_product_class()
356
+        for attribute in product_class.attributes.all():
342 357
             value = self.cleaned_data['attr_%s' % attribute.code]
343
-            setattr(product.attr, attribute.code, value)
344
-        product.save()
345
-        self.save_m2m()
346
-        return product
358
+            setattr(self.instance.attr, attribute.code, value)
359
+        super(ProductForm, self)._post_clean()
347 360
 
348 361
 
349 362
 class StockAlertSearchForm(forms.Form):

+ 19
- 11
oscar/apps/dashboard/catalogue/tables.py Прегледај датотеку

@@ -12,28 +12,36 @@ Category = get_model('catalogue', 'Category')
12 12
 
13 13
 class ProductTable(Table):
14 14
     title = TemplateColumn(
15
+        verbose_name=_('Title'),
15 16
         template_name='dashboard/catalogue/product_row_title.html',
16
-        order_by='title', accessor=A('get_title'))
17
+        order_by='title', accessor=A('title'))
17 18
     image = TemplateColumn(
19
+        verbose_name=_('Image'),
18 20
         template_name='dashboard/catalogue/product_row_image.html',
19 21
         orderable=False)
20
-    product_class = Column(verbose_name=_("Type"),
21
-                           accessor=A('get_product_class.name'),
22
-                           order_by=('product_class__name'))
23
-    parent = LinkColumn('dashboard:catalogue-product',
24
-                        verbose_name=_("Parent"), args=[A('parent.pk')],
25
-                        accessor=A('parent.title'))
26
-    children = Column(accessor=A('children.count'), orderable=False)
27
-    stock_records = Column(accessor=A('stockrecords.count'), orderable=False)
22
+    product_class = Column(
23
+        verbose_name=_('Product type'),
24
+        accessor=A('product_class'),
25
+        order_by='product_class__name')
26
+    variants = TemplateColumn(
27
+        verbose_name=_("Variants"),
28
+        template_name='dashboard/catalogue/product_row_variants.html',
29
+        orderable=False
30
+    )
31
+    stock_records = Column(
32
+        verbose_name=_('Stock records'),
33
+        accessor=A('stockrecords.count'),
34
+        orderable=False)
28 35
     actions = TemplateColumn(
36
+        verbose_name=_('Actions'),
29 37
         template_name='dashboard/catalogue/product_row_actions.html',
30 38
         orderable=False)
31 39
 
32 40
     class Meta(DashboardTable.Meta):
33 41
         model = Product
34 42
         fields = ('upc', 'date_created')
35
-        sequence = ('title', 'upc', 'image', 'product_class',
36
-                    'parent', 'children', 'stock_records', '...', 'date_created', 'actions')
43
+        sequence = ('title', 'upc', 'image', 'product_class', 'variants',
44
+                    'stock_records', '...', 'date_created', 'actions')
37 45
         order_by = '-date_created'
38 46
 
39 47
 

+ 161
- 27
oscar/apps/dashboard/catalogue/views.py Прегледај датотеку

@@ -1,13 +1,13 @@
1
-from django.core.exceptions import ObjectDoesNotExist
2 1
 from django.views import generic
3 2
 from django.db.models import Q
4
-from django.http import HttpResponseRedirect, Http404
3
+from django.http import HttpResponseRedirect
5 4
 from django.contrib import messages
6 5
 from django.core.urlresolvers import reverse
7 6
 from django.utils.translation import ugettext_lazy as _
7
+from django.shortcuts import get_object_or_404, redirect
8 8
 from django.template.loader import render_to_string
9 9
 
10
-from oscar.core.loading import get_class, get_classes, get_model
10
+from oscar.core.loading import get_classes, get_model
11 11
 
12 12
 from django_tables2 import SingleTableMixin
13 13
 
@@ -106,7 +106,7 @@ class ProductListView(SingleTableMixin, generic.TemplateView):
106 106
         """
107 107
         Build the queryset for this list
108 108
         """
109
-        queryset = Product.objects.base_queryset()
109
+        queryset = Product.browsable.base_queryset()
110 110
         queryset = self.filter_queryset(queryset)
111 111
         queryset = self.apply_search(queryset)
112 112
         queryset = self.apply_ordering(queryset)
@@ -187,7 +187,17 @@ class ProductCreateRedirectView(generic.RedirectView):
187 187
 
188 188
 class ProductCreateUpdateView(generic.UpdateView):
189 189
     """
190
-    Dashboard view that bundles both creating and updating single products.
190
+    Dashboard view that is can both create and update products of all kinds.
191
+    It can be used in three different ways, each of them with a unique URL
192
+    pattern:
193
+    - When creating a new standalone product, this view is called with the
194
+      desired product class
195
+    - When editing an existing product, this view is called with the product's
196
+      primary key. If the product is a child product, the template considerably
197
+      reduces the available form fields.
198
+    - When creating a new child product, this view is called with the parent's
199
+      primary key.
200
+
191 201
     Supports the permission-based dashboard.
192 202
     """
193 203
 
@@ -219,43 +229,78 @@ class ProductCreateUpdateView(generic.UpdateView):
219 229
         This parts allows generic.UpdateView to handle creating products as
220 230
         well. The only distinction between an UpdateView and a CreateView
221 231
         is that self.object is None. We emulate this behavior.
222
-        Additionally, self.product_class is set.
232
+
233
+        This method is also responsible for setting self.product_class and
234
+        self.parent.
223 235
         """
224 236
         self.creating = 'pk' not in self.kwargs
225 237
         if self.creating:
226
-            try:
227
-                product_class_slug = self.kwargs.get('product_class_slug',
228
-                                                     None)
229
-                self.product_class = ProductClass.objects.get(
230
-                    slug=product_class_slug)
231
-            except ObjectDoesNotExist:
232
-                raise Http404
238
+            # Specifying a parent product is only done when creating a child
239
+            # product.
240
+            parent_pk = self.kwargs.get('parent_pk')
241
+            if parent_pk is None:
242
+                self.parent = None
243
+                # A product class needs to be specified when creating a
244
+                # standalone product.
245
+                product_class_slug = self.kwargs.get('product_class_slug')
246
+                self.product_class = get_object_or_404(
247
+                    ProductClass, slug=product_class_slug)
233 248
             else:
234
-                return None  # success
249
+                self.parent = self.get_and_check_parent(parent_pk)
250
+                self.product_class = self.parent.product_class
251
+
252
+            return None  # success
235 253
         else:
236 254
             product = super(ProductCreateUpdateView, self).get_object(queryset)
237 255
             self.product_class = product.get_product_class()
256
+            self.parent = product.parent
238 257
             return product
239 258
 
259
+    def get_and_check_parent(self, parent_pk):
260
+        """
261
+        Fetches the specified "parent" product and ensures that it can be
262
+        indeed be turned into a parent product if needed.
263
+        """
264
+        parent = get_object_or_404(Product, pk=parent_pk)
265
+        is_valid, reason = parent.can_be_parent(give_reason=True)
266
+        if is_valid:
267
+            return parent
268
+        else:
269
+            messages.error(self.request, reason)
270
+            return redirect('dashboard:catalogue-product-list')
271
+
240 272
     def get_context_data(self, **kwargs):
241 273
         ctx = super(ProductCreateUpdateView, self).get_context_data(**kwargs)
242 274
         ctx['product_class'] = self.product_class
275
+        ctx['parent'] = self.parent
276
+        ctx['title'] = self.get_page_title()
243 277
 
244 278
         for ctx_name, formset_class in self.formsets.items():
245 279
             if ctx_name not in ctx:
246 280
                 ctx[ctx_name] = formset_class(self.product_class,
247 281
                                               self.request.user,
248 282
                                               instance=self.object)
283
+        return ctx
249 284
 
250
-        if self.object is None:
251
-            ctx['title'] = _('Create new %s product') % self.product_class.name
285
+    def get_page_title(self):
286
+        if self.creating:
287
+            if self.parent is None:
288
+                return _('Create new %(product_class)s product') % {
289
+                    'product_class': self.product_class.name}
290
+            else:
291
+                return _('Create new variant of %(parent_product)s') % {
292
+                    'parent_product': self.parent.title}
252 293
         else:
253
-            ctx['title'] = ctx['product'].get_title()
254
-        return ctx
294
+            if self.object.title:
295
+                return self.object.title
296
+            else:
297
+                return _('Editing variant of %(parent_product)s') % {
298
+                    'parent_product': self.parent.title}
255 299
 
256 300
     def get_form_kwargs(self):
257 301
         kwargs = super(ProductCreateUpdateView, self).get_form_kwargs()
258 302
         kwargs['product_class'] = self.product_class
303
+        kwargs['parent'] = self.parent
259 304
         return kwargs
260 305
 
261 306
     def process_all_forms(self, form):
@@ -305,8 +350,12 @@ class ProductCreateUpdateView(generic.UpdateView):
305 350
     def forms_valid(self, form, formsets):
306 351
         """
307 352
         Save all changes and display a success url.
353
+        When creating the first child product, this method also sets the new
354
+        parent's structure accordingly.
308 355
         """
309
-        if not self.creating:
356
+        if self.creating:
357
+            self.handle_adding_child(self.parent)
358
+        else:
310 359
             # a just created product was already saved in process_all_forms()
311 360
             self.object = form.save()
312 361
 
@@ -316,6 +365,19 @@ class ProductCreateUpdateView(generic.UpdateView):
316 365
 
317 366
         return HttpResponseRedirect(self.get_success_url())
318 367
 
368
+    def handle_adding_child(self, parent):
369
+        """
370
+        When creating the first child product, the parent product needs
371
+        to be implicitly converted from a standalone product to a
372
+        parent product.
373
+        """
374
+        # ProductForm eagerly sets the future parent's structure to PARENT to
375
+        # pass validation, but it's not persisted in the database. We ensure
376
+        # it's persisted by calling save()
377
+        if parent is not None:
378
+            parent.structure = Product.PARENT
379
+            parent.save()
380
+
319 381
     def forms_invalid(self, form, formsets):
320 382
         # delete the temporary product again
321 383
         if self.creating and self.object and self.object.pk is not None:
@@ -335,6 +397,13 @@ class ProductCreateUpdateView(generic.UpdateView):
335 397
         return "?".join(url_parts)
336 398
 
337 399
     def get_success_url(self):
400
+        """
401
+        Renders a success message and redirects depending on the button:
402
+        - Standard case is pressing "Save"; redirects to the product list
403
+        - When "Save and continue" is pressed, we stay on the same page
404
+        - When "Create (another) child product" is pressed, it redirects
405
+          to a new product creation page
406
+        """
338 407
         msg = render_to_string(
339 408
             'dashboard/catalogue/messages/product_saved.html',
340 409
             {
@@ -343,16 +412,28 @@ class ProductCreateUpdateView(generic.UpdateView):
343 412
                 'request': self.request
344 413
             })
345 414
         messages.success(self.request, msg, extra_tags="safe noicon")
346
-        url = reverse('dashboard:catalogue-product-list')
347
-        if self.request.POST.get('action') == 'continue':
348
-            url = reverse('dashboard:catalogue-product',
349
-                          kwargs={"pk": self.object.id})
415
+
416
+        action = self.request.POST.get('action')
417
+        if action == 'continue':
418
+            url = reverse(
419
+                'dashboard:catalogue-product', kwargs={"pk": self.object.id})
420
+        elif action == 'create-another-child' and self.parent:
421
+            url = reverse(
422
+                'dashboard:catalogue-product-create-child',
423
+                kwargs={'parent_pk': self.parent.pk})
424
+        elif action == 'create-child':
425
+            url = reverse(
426
+                'dashboard:catalogue-product-create-child',
427
+                kwargs={'parent_pk': self.object.pk})
428
+        else:
429
+            url = reverse('dashboard:catalogue-product-list')
350 430
         return self.get_url_with_querystring(url)
351 431
 
352 432
 
353 433
 class ProductDeleteView(generic.DeleteView):
354 434
     """
355
-    Dashboard view to delete a product.
435
+    Dashboard view to delete a product. Has special logic for deleting the
436
+    last child product.
356 437
     Supports the permission-based dashboard.
357 438
     """
358 439
     template_name = 'dashboard/catalogue/product_delete.html'
@@ -365,10 +446,63 @@ class ProductDeleteView(generic.DeleteView):
365 446
         """
366 447
         return filter_products(Product.objects.all(), self.request.user)
367 448
 
449
+    def get_context_data(self, **kwargs):
450
+        ctx = super(ProductDeleteView, self).get_context_data(**kwargs)
451
+        if self.object.is_child:
452
+            ctx['title'] = _("Delete product variant?")
453
+        else:
454
+            ctx['title'] = _("Delete product?")
455
+        return ctx
456
+
457
+    def delete(self, request, *args, **kwargs):
458
+        # We override the core delete method and don't call super in order to
459
+        # apply more sophisticated logic around handling child products.
460
+        # Calling super makes it difficult to test if the product being deleted
461
+        # is the last child.
462
+
463
+        self.object = self.get_object()
464
+
465
+        # Before performing the delete, record whether this product is the last
466
+        # child.
467
+        is_last_child = False
468
+        if self.object.is_child:
469
+            parent = self.object.parent
470
+            is_last_child = parent.children.count() == 1
471
+
472
+        self.object.delete()
473
+
474
+        # If the product being deleted is the last child, then pass control
475
+        # to a method than can adjust the parent itself.
476
+        if is_last_child:
477
+            self.handle_deleting_last_child(parent)
478
+
479
+        return HttpResponseRedirect(self.get_success_url())
480
+
481
+    def handle_deleting_last_child(self, parent):
482
+        # If the last child product is deleted, this view defaults to turning
483
+        # the parent product into a standalone product. While this is
484
+        # appropriate for many scenarios, it is intentionally easily
485
+        # overridable and not automatically done in e.g. a Product's delete()
486
+        # method as it is more a UX helper than hard business logic.
487
+        parent.structure = parent.STANDALONE
488
+        parent.save()
489
+
368 490
     def get_success_url(self):
369
-        msg = _("Deleted product '%s'") % self.object.title
370
-        messages.success(self.request, msg)
371
-        return reverse('dashboard:catalogue-product-list')
491
+        """
492
+        When deleting child products, this view redirects to editing the
493
+        parent product. When deleting any other product, it redirects to the
494
+        product list view.
495
+        """
496
+        if self.object.is_child:
497
+            msg = _("Deleted product variant '%s'") % self.object.get_title()
498
+            messages.success(self.request, msg)
499
+            return reverse(
500
+                'dashboard:catalogue-product',
501
+                kwargs={'pk': self.object.parent_id})
502
+        else:
503
+            msg = _("Deleted product '%s'") % self.object.title
504
+            messages.success(self.request, msg)
505
+            return reverse('dashboard:catalogue-product-list')
372 506
 
373 507
 
374 508
 class StockAlertListView(generic.ListView):

+ 2
- 1
oscar/apps/offer/models.py Прегледај датотеку

@@ -508,7 +508,8 @@ class Condition(models.Model):
508 508
         if not line.stockrecord_id:
509 509
             return False
510 510
         product = line.product
511
-        return self.range.contains_product(product) and product.is_discountable
511
+        return (self.range.contains_product(product)
512
+                and product.get_is_discountable())
512 513
 
513 514
     def get_applicable_lines(self, offer, basket, most_expensive_first=True):
514 515
         """

+ 40
- 9
oscar/templates/oscar/dashboard/catalogue/messages/product_saved.html Прегледај датотеку

@@ -2,15 +2,46 @@
2 2
 {% load django_tables2 %}
3 3
 
4 4
 <p>
5
-{% if creating %}
6
-    {% blocktrans with name=product.get_title %}
7
-        Created product '{{ name }}'.
8
-    {% endblocktrans %}
9
-{% else %}
10
-    {% blocktrans with name=product.get_title %}
11
-        Updated product '{{ name }}'.
12
-    {% endblocktrans %}
13
-{% endif %}
5
+{% with name=product.title parent_name=product.parent.title %}
6
+    {% if product.is_child %}
7
+
8
+        {% if creating %}
9
+            {% if product.title %}
10
+                {% blocktrans %}
11
+                    Created product variant '{{ name }}'.
12
+                {% endblocktrans %}
13
+            {% else %}
14
+                {% blocktrans %}
15
+                    Created variant of '{{ parent_name }}'.
16
+                {% endblocktrans %}
17
+            {% endif %}
18
+        {% else %}
19
+            {% if product.title %}
20
+                {% blocktrans %}
21
+                    Updated product variant '{{ name }}'.
22
+                {% endblocktrans %}
23
+            {% else %}
24
+                {% blocktrans %}
25
+                    Updated a variant of '{{ parent_name }}'.
26
+                {% endblocktrans %}
27
+            {% endif %}
28
+        {% endif %}
29
+
30
+    {% else %}
31
+
32
+        {% if creating %}
33
+            {% blocktrans %}
34
+                Created product '{{ name }}'.
35
+            {% endblocktrans %}
36
+        {% else %}
37
+            {% blocktrans %}
38
+                Updated product '{{ name }}'.
39
+            {% endblocktrans %}
40
+        {% endif %}
41
+
42
+    {% endif %}
43
+{% endwith %}
44
+
14 45
 </p>
15 46
 
16 47
 <p>

+ 19
- 17
oscar/templates/oscar/dashboard/catalogue/product_delete.html Прегледај датотеку

@@ -3,10 +3,7 @@
3 3
 {% load i18n %}
4 4
 
5 5
 {% block title %}
6
-    {% blocktrans with title=product.get_title %}
7
-        Delete {{ title}}?
8
-    {% endblocktrans %} |
9
-    {{ block.super }}
6
+    {{ title }} | {{ block.super }}
10 7
 {% endblock %}
11 8
 
12 9
 {% block body_class %}{{ block.super }} create-page{% endblock %}
@@ -22,35 +19,40 @@
22 19
             <span class="divider">/</span>
23 20
         </li>
24 21
         <li>
25
-            <a href="{% url 'dashboard:catalogue-product' pk=product.pk %}">{{ product.title }}</a>
22
+            <a href="{% url 'dashboard:catalogue-product' pk=product.pk %}">{{ product.get_title }}</a>
26 23
             <span class="divider">/</span>
27 24
         </li>
28
-        <li class="active">{% trans "Delete?" %}</li>
25
+        <li class="active">{{ title }}</li>
29 26
     </ul>
30 27
 {% endblock %}
31 28
 
32
-{% block header %}
33
-    <div class="page-header">
34
-        <h1>{% trans "Delete product?" %}</h1>
35
-    </div>
36
-{% endblock header%}
29
+{% block headertext %}{{ title }}{% endblock %}
37 30
 
38 31
 {% block dashboard_content %}
39 32
     <div class="table-header">
40
-        <h2>{% trans "Delete product" %}</h2>
33
+        <h2>{{ title }}</h2>
41 34
     </div>
42 35
     <form action="." method="post" class="well">
43 36
         {% csrf_token %}
44 37
 
45
-        {% blocktrans with title=product.title %}
46
-            <p>Delete product <strong>{{ title }}</strong> - are you sure?</p>
47
-        {% endblocktrans %}
38
+        <p>
39
+            {% if product.is_child %}
40
+                {% blocktrans with title=product.get_title %}
41
+                    Delete product variant <strong>{{ title }}</strong> - are you sure?
42
+                {% endblocktrans %}
43
+            {% else %}
44
+                {% blocktrans with title=product.get_title %}
45
+                    Delete product <strong>{{ title }}</strong> - are you sure?
46
+                {% endblocktrans %}
47
+            {% endif %}
48
+        </p>
48 49
 
49 50
         {% if product.is_parent %}
50
-            <p> {% trans "This will also delete the following child products:" %}
51
+            <p>
52
+                {% trans "This will also delete the following child products:" %}
51 53
                 <ul>
52 54
                     {% for child in product.children.all %}
53
-                        <li><strong>{{ child.title }}</strong></li>
55
+                        <li><strong>{{ child.get_title }}</strong></li>
54 56
                     {% endfor %}
55 57
                 </ul>
56 58
             </p>

+ 5
- 0
oscar/templates/oscar/dashboard/catalogue/product_row_variants.html Прегледај датотеку

@@ -0,0 +1,5 @@
1
+{% if record.is_standalone %}
2
+    -
3
+{% else %}
4
+    {{ record.children.count }}
5
+{% endif %}

+ 130
- 71
oscar/templates/oscar/dashboard/catalogue/product_update.html Прегледај датотеку

@@ -21,8 +21,15 @@
21 21
             <a href="{% url 'dashboard:catalogue-product-list' %}">{% trans "Products" %}</a>
22 22
             <span class="divider">/</span>
23 23
         </li>
24
+        {% if parent %}
25
+            <li>
26
+                <a href="{% url 'dashboard:catalogue-product' parent.id %}">
27
+                    {{ parent.title }}
28
+                </a>
29
+                <span class="divider">/</span>
30
+            </li>
31
+        {% endif %}
24 32
         <li class="active">{{ title }}</li>
25
-
26 33
     </ul>
27 34
 {% endblock %}
28 35
 
@@ -31,6 +38,21 @@
31 38
 {% block dashboard_content %}
32 39
     <form action="{% if request.GET.urlencode %}?{{ request.GET.urlencode }}{% endif %}" method="post" class="form-stacked wysiwyg fixed-actions" enctype="multipart/form-data" data-behaviour="affix-nav-errors" autocomplete="off">
33 40
         {% csrf_token %}
41
+
42
+        {% if parent %}
43
+            <div class="row-fluid">
44
+                <div class="span12">
45
+                    <div class="alert alert-info">
46
+                        {% url 'dashboard:catalogue-product' pk=parent.id as parent_url %}
47
+                        {% blocktrans with title=parent.title %}
48
+                            You are currently editing a product variant of
49
+                            <a href="{{ parent_url }}">{{ title }}</a>.
50
+                        {% endblocktrans %}
51
+                    </div>
52
+                </div>
53
+            </div>
54
+        {% endif %}
55
+
34 56
         <div class="row-fluid">
35 57
 
36 58
             {% block tab_nav %}
@@ -40,17 +62,34 @@
40 62
                             <h3>{% trans "Sections" %}</h3>
41 63
                         </div>
42 64
                         <ul class="nav nav-list bs-docs-sidenav" id="product_update_tabs">
43
-                            <li class="active"><a href="#product_details" data-toggle="tab">{% trans 'Product details' %}</a></li>
44
-                            <li><a href="#product_category" data-toggle="tab">{% trans 'Category' %}</a></li>
45
-                            {% if product_class.has_attributes %}
46
-                                <li><a href="#product_attributes" data-toggle="tab">{% trans 'Attributes' %}</a></li>
47
-                            {% endif %}
48
-                            <li><a href="#product_images" data-toggle="tab">{% trans 'Images' %}</a></li>
49
-                            <li><a href="#product_stock" data-toggle="tab">{% trans 'Stock and pricing' %}</a></li>
50
-                            {% if not product.is_child %}
51
-                                <li><a href="#child_products" data-toggle="tab">{% trans 'Variants' %}</a></li>
52
-                            {% endif %}
53
-                            <li><a href="#product_recommended" data-toggle="tab">{% trans 'Upselling' %}</a></li>
65
+                            {% comment %}
66
+                                The navigation below is filtered heavily on the product structure.
67
+                                This intentionally is the only place where filtering is done, as
68
+                                deployments are likely to want to override certain aspects of what
69
+                                data is stored on products of a certain structure. This means that
70
+                                only one template block (instead of all affected) has to be altered.
71
+                            {% endcomment %}
72
+                            {% block tabs %}
73
+                                <li class="active"><a href="#product_details" data-toggle="tab">{% trans 'Product details' %}</a></li>
74
+                                {% if not parent %}
75
+                                    <li><a href="#product_category" data-toggle="tab">{% trans 'Categories' %}</a></li>
76
+                                {% endif %}
77
+                                {% if product_class.has_attributes %}
78
+                                    <li><a href="#product_attributes" data-toggle="tab">{% trans 'Attributes' %}</a></li>
79
+                                {% endif %}
80
+                                {% if not parent %}
81
+                                    <li><a href="#product_images" data-toggle="tab">{% trans 'Images' %}</a></li>
82
+                                {% endif %}
83
+                                {% if not product.is_parent %}
84
+                                    <li><a href="#product_stock" data-toggle="tab">{% trans 'Stock and pricing' %}</a></li>
85
+                                {% endif %}
86
+                                {% if not parent %}
87
+                                    <li><a href="#child_products" data-toggle="tab">{% trans 'Variants' %}</a></li>
88
+                                {% endif %}
89
+                                {% if not parent %}
90
+                                    <li><a href="#product_recommended" data-toggle="tab">{% trans 'Upselling' %}</a></li>
91
+                                {% endif %}
92
+                            {% endblock tabs %}
54 93
                         </ul>
55 94
                     </div>
56 95
                 </div>
@@ -75,17 +114,6 @@
75 114
                                             {% include 'partials/form_field.html' with field=field %}
76 115
                                         {% endif %}
77 116
                                     {% endfor %}
78
-
79
-                                    {% with parent=product.parent %}
80
-                                        {% if parent %}
81
-                                            <div class="control-group">
82
-                                                <label class="control-label">{% trans "Parent" %}</label>
83
-                                                <div class="controls">
84
-                                                    <a href="{% url 'dashboard:catalogue-product' pk=parent.id %}" title="{% blocktrans with title=parent.title %}Edit {{ title }}{% endblocktrans %}">{{ parent.title }}</a>
85
-                                                </div>
86
-                                            </div>
87
-                                        {% endif %}
88
-                                    {% endwith %}
89 117
                                 {% endblock product_details_content %}
90 118
                             </div>
91 119
                         </div>
@@ -111,29 +139,27 @@
111 139
                     {% endblock product_categories %}
112 140
 
113 141
                     {% block product_attributes %}
114
-                        {% if product_class.has_attributes %}
115
-                            <div class="tab-pane" id="product_attributes">
116
-                                {% block product_attributes_content %}
117
-                                    <table class="table table-striped table-bordered">
118
-                                        <caption>
119
-                                            {% trans "Attributes" %}
120
-                                            <span class="label label-success">
121
-                                                {% trans "Product Type:" %} {{ product_class }}
122
-                                            </span>
123
-                                        </caption>
124
-                                        {% for field in form %}
125
-                                            {% if 'attr' in field.id_for_label %}
126
-                                                <tr>
127
-                                                    <td>
128
-                                                        {% include "partials/form_field.html" %}
129
-                                                    </td>
130
-                                                </tr>
131
-                                            {% endif %}
132
-                                        {% endfor %}
133
-                                    </table>
134
-                                {% endblock product_attributes_content %}
135
-                            </div>
136
-                        {% endif %}
142
+                        <div class="tab-pane" id="product_attributes">
143
+                            {% block product_attributes_content %}
144
+                                <table class="table table-striped table-bordered">
145
+                                    <caption>
146
+                                        {% trans "Attributes" %}
147
+                                        <span class="label label-success">
148
+                                            {% trans "Product Type:" %} {{ product_class }}
149
+                                        </span>
150
+                                    </caption>
151
+                                    {% for field in form %}
152
+                                        {% if 'attr' in field.id_for_label %}
153
+                                            <tr>
154
+                                                <td>
155
+                                                    {% include "partials/form_field.html" %}
156
+                                                </td>
157
+                                            </tr>
158
+                                        {% endif %}
159
+                                    {% endfor %}
160
+                                </table>
161
+                            {% endblock product_attributes_content %}
162
+                        </div>
137 163
                     {% endblock product_attributes %}
138 164
 
139 165
                     {% block product_images %}
@@ -214,35 +240,57 @@
214 240
                     {% endblock stockrecords %}
215 241
 
216 242
                     {% block child_products %}
217
-                        {% if not product.is_child %}
218
-                            {% with children=product.children.all %}
219
-                                <div class="tab-pane" id="child_products">
220
-                                    {% block child_products_content %}
221
-                                        <table class='table table-striped table-bordered'>
222
-                                            <caption>{% trans "Variants" %}</caption>
243
+                        {% with children=product.children.all %}
244
+                            <div class="tab-pane" id="child_products">
245
+                                {% block child_products_content %}
246
+                                    <table class='table table-striped table-bordered'>
247
+                                        <caption>
248
+                                            {% trans "Variants" %}
249
+                                            <button class="btn btn-primary pull-right{% if not product.can_be_parent %} disabled{% endif %}" name="action" type="submit" value="create-child">
250
+                                                <i class="icon-plus"></i>
251
+                                                {% trans "Add variant" %}
252
+                                            </button>
253
+                                        </caption>
254
+                                        {% if children %}
255
+                                            <tr>
256
+                                                <th>{% trans "Title" %}</th>
257
+                                                <th>{% trans "Attributes" %}</th>
258
+                                                <th>{% trans "Stock records" %}</th>
259
+                                                <th>&nbsp;</th>
260
+                                            </tr>
223 261
                                             {% for child in children %}
224 262
                                                 <tr>
225 263
                                                     <td>{{ child.get_title }}</td>
226 264
                                                     <td>{{ child.attribute_summary }}</td>
227
-                                                    <td><a href="{% url 'dashboard:catalogue-product' pk=child.id %}" class="btn btn-primary">{% trans "Edit" %}</a></td>
228
-                                                </tr>
229
-                                            {% empty %}
230
-                                                <tr>
231
-                                                    <td colspan="3">
232
-                                                        {% url 'dashboard:catalogue-product-create' product_class.slug as create_url %}
233
-                                                        {% blocktrans %}
234
-                                                            This product does not have any variants. To add variants, please ensure that the product structure is set to
235
-                                                            parent product and finish editing the current product. Then <a href="{{ create_url }}">create</a> one or more
236
-                                                            child products and select the current product as parent.
237
-                                                        {% endblocktrans %}
265
+                                                    <td>{{ child.stockrecords.count }}</td>
266
+                                                    <td>
267
+                                                        <a href="{% url 'dashboard:catalogue-product' pk=child.id %}" class="btn btn-primary">
268
+                                                            {% trans "Edit" %}
269
+                                                        </a>
270
+                                                        <a href="{% url 'dashboard:catalogue-product-delete' pk=child.id %}" class="btn btn-danger">
271
+                                                            {% trans "Delete" %}
272
+                                                        </a>
238 273
                                                     </td>
239 274
                                                 </tr>
240 275
                                             {% endfor %}
241
-                                        </table>
242
-                                    {% endblock child_products_content %}
243
-                                </div>
244
-                            {% endwith %}
245
-                        {% endif %}
276
+                                        {% else %}
277
+                                            <tr>
278
+                                                <td colspan="3">
279
+                                                    {% if product.can_be_parent %}
280
+                                                        {% trans 'This product does not have any variants.' %}
281
+                                                    {% else %}
282
+                                                        {% trans "One can't add variants to this product at this point." %}
283
+                                                        {% if product.has_stockrecords %}
284
+                                                            {% trans 'This is likely because this product still has stock records.' %}
285
+                                                        {% endif %}
286
+                                                    {% endif %}
287
+                                                </td>
288
+                                            </tr>
289
+                                        {% endif %}
290
+                                    </table>
291
+                                {% endblock child_products_content %}
292
+                            </div>
293
+                        {% endwith %}
246 294
                     {% endblock child_products %}
247 295
 
248 296
                     {% block recommended_products %}
@@ -271,10 +319,21 @@
271 319
             <div class="fixed-actions-group">
272 320
                 <div class="form-actions">
273 321
                     <div class="pull-right">
274
-                        <a href="{% url 'dashboard:catalogue-product-list' %}">{% trans "Cancel" %}</a>
322
+                        <a href="{% url 'dashboard:catalogue-product-list' %}">
323
+                            {% trans "Cancel" %}
324
+                        </a>
275 325
                         {% trans "or" %}
276
-                        <button class="btn btn-secondary btn-large" name="action" type="submit" value="continue">{% trans "Save and continue editing" %}</button>
277
-                        <button class="btn btn-primary btn-large" name="action" type="submit" value="save">{% trans "Save" %}</button>
326
+                        {% if parent %}
327
+                            <button class="btn btn-secondary btn-large" name="action" type="submit" value="create-another-child">
328
+                                {% trans "Save and add another variant" %}
329
+                            </button>
330
+                        {% endif %}
331
+                        <button class="btn btn-secondary btn-large" name="action" type="submit" value="continue">
332
+                            {% trans "Save and continue editing" %}
333
+                        </button>
334
+                        <button class="btn btn-primary btn-large" name="action" type="submit" value="save">
335
+                            {% trans "Save" %}
336
+                        </button>
278 337
                     </div>
279 338
                     {% if product %}
280 339
                         <a class="btn btn-success btn-large" href="{{ product.get_absolute_url }}">{% trans "View on site" %}</a>

+ 1
- 1
oscar/templates/oscar/dashboard/ranges/range_product_list.html Прегледај датотеку

@@ -108,7 +108,7 @@
108 108
                                 </td>
109 109
                                 <td>{{ product.upc|default:"-" }}</td>
110 110
                                 <td><a href="{% url 'dashboard:catalogue-product' pk=product.id %}">{{ product.get_title }}</a></td>
111
-                                <td>{% if product.is_discountable %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
111
+                                <td>{% if product.get_is_discountable %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
112 112
                                 <td>
113 113
                                     <a class="btn btn-danger" href="#" data-behaviours="remove">{% trans "Remove" %}</a>
114 114
                                     <a href="#" class="btn btn-info btn-handle"><i class="icon-move icon-large"></i> {% trans "Re-order" %}</a>

+ 3
- 2
oscar/test/newfactories.py Прегледај датотеку

@@ -20,8 +20,9 @@ from oscar.core.compat import get_user_model
20 20
 
21 21
 __all__ = ["UserFactory", "CountryFactory", "UserAddressFactory",
22 22
            "BasketFactory", "VoucherFactory", "ProductFactory",
23
-           "StockRecordFactory", "ProductAttributeFactory",
24
-           "ProductAttributeValueFactory", "AttributeOptionGroupFactory",
23
+           "ProductClassFactory", "StockRecordFactory",
24
+           "ProductAttributeFactory", "ProductAttributeValueFactory",
25
+           "AttributeOptionGroupFactory",
25 26
            "AttributeOptionFactory", "PartnerFactory",
26 27
            "ProductCategoryFactory", "CategoryFactory", "RangeFactory",
27 28
            "ProductClassFactory"]

+ 8
- 8
sites/sandbox/fixtures/child_products.json Прегледај датотеку

@@ -80,7 +80,7 @@
80 80
         "date_created": "2013-12-12T16:34:14.023Z", 
81 81
         "product_options": [], 
82 82
         "slug": "oscar-t-shirt", 
83
-        "product_class": 1,
83
+        "product_class": null,
84 84
         "structure": "child"
85 85
     }
86 86
 },
@@ -98,7 +98,7 @@
98 98
         "date_created": "2013-12-12T16:34:32.170Z", 
99 99
         "product_options": [], 
100 100
         "slug": "oscar-t-shirt", 
101
-        "product_class": 1,
101
+        "product_class": null,
102 102
         "structure": "child"
103 103
     }
104 104
 },
@@ -116,7 +116,7 @@
116 116
         "date_created": "2013-12-12T17:32:15.016Z", 
117 117
         "product_options": [], 
118 118
         "slug": "oscar-t-shirt", 
119
-        "product_class": 1,
119
+        "product_class": null,
120 120
         "structure": "child"
121 121
     }
122 122
 },
@@ -152,7 +152,7 @@
152 152
         "date_created": "2013-12-13T11:37:08.138Z", 
153 153
         "product_options": [], 
154 154
         "slug": "commandlinefu-t-shirt", 
155
-        "product_class": 1,
155
+        "product_class": null,
156 156
         "structure": "child"
157 157
     }
158 158
 },
@@ -170,7 +170,7 @@
170 170
         "date_created": "2013-12-13T11:37:45.834Z", 
171 171
         "product_options": [], 
172 172
         "slug": "commandlinefu-t-shirt", 
173
-        "product_class": 1,
173
+        "product_class": null,
174 174
         "structure": "child"
175 175
     }
176 176
 },
@@ -206,7 +206,7 @@
206 206
         "date_created": "2013-12-13T11:38:49.769Z", 
207 207
         "product_options": [], 
208 208
         "slug": "tangent-t-shirt", 
209
-        "product_class": 1,
209
+        "product_class": null,
210 210
         "structure": "child"
211 211
     }
212 212
 },
@@ -224,7 +224,7 @@
224 224
         "date_created": "2013-12-13T11:39:12.859Z", 
225 225
         "product_options": [], 
226 226
         "slug": "tangent-t-shirt", 
227
-        "product_class": 1,
227
+        "product_class": null,
228 228
         "structure": "child"
229 229
     }
230 230
 },
@@ -242,7 +242,7 @@
242 242
         "date_created": "2013-12-13T11:39:53.073Z", 
243 243
         "product_options": [], 
244 244
         "slug": "tangent-t-shirt", 
245
-        "product_class": 1,
245
+        "product_class": null,
246 246
         "structure": "child"
247 247
     }
248 248
 },

+ 1
- 1
tests/functional/dashboard/catalogue_tests.py Прегледај датотеку

@@ -58,7 +58,7 @@ class TestAStaffUser(WebTestCase):
58 58
         form['stockrecords-0-partner_sku'] = '14'
59 59
         form['stockrecords-0-num_in_stock'] = '555'
60 60
         form['stockrecords-0-price_excl_tax'] = '13.99'
61
-        page = form.submit('action', index=0)
61
+        page = form.submit(name='action', value='continue')
62 62
 
63 63
         self.assertEqual(Product.objects.count(), 1)
64 64
         product = Product.objects.all()[0]

+ 2
- 4
tests/functional/dashboard/product_tests.py Прегледај датотеку

@@ -113,14 +113,12 @@ class TestCreateChildProduct(ProductWebTest):
113 113
         super(TestCreateChildProduct, self).setUp()
114 114
 
115 115
     def test_categories_are_not_required(self):
116
-        url = reverse('dashboard:catalogue-product-create',
117
-                      kwargs={'product_class_slug': self.pclass.slug})
116
+        url = reverse('dashboard:catalogue-product-create-child',
117
+                      kwargs={'parent_pk': self.parent.pk})
118 118
         page = self.get(url)
119 119
 
120 120
         product_form = page.form
121
-        product_form['structure'] = 'child'
122 121
         product_form['title'] = expected_title = 'Nice T-Shirt'
123
-        product_form['parent'] = str(self.parent.id)
124 122
         product_form.submit()
125 123
 
126 124
         try:

+ 12
- 13
tests/integration/catalogue/product_tests.py Прегледај датотеку

@@ -70,13 +70,15 @@ class TopLevelProductTests(ProductTests):
70 70
         self.assertEqual(set([product]), set(Product.browsable.all()))
71 71
 
72 72
 
73
-class VariantProductTests(ProductTests):
73
+class ChildProductTests(ProductTests):
74 74
 
75 75
     def setUp(self):
76
-        super(VariantProductTests, self).setUp()
76
+        super(ChildProductTests, self).setUp()
77 77
         self.parent = Product.objects.create(
78
-            title="Parent product", product_class=self.product_class,
79
-            structure=Product.PARENT)
78
+            title="Parent product",
79
+            product_class=self.product_class,
80
+            structure=Product.PARENT,
81
+            is_discountable=False)
80 82
 
81 83
     def test_child_products_dont_need_titles(self):
82 84
         Product.objects.create(
@@ -84,19 +86,16 @@ class VariantProductTests(ProductTests):
84 86
             structure=Product.CHILD)
85 87
 
86 88
     def test_child_products_dont_need_a_product_class(self):
87
-        Product.objects.create(
88
-            parent=self.parent, structure=Product.CHILD)
89
+        Product.objects.create(parent=self.parent, structure=Product.CHILD)
89 90
 
90
-    def test_child_products_inherit_parent_titles(self):
91
+    def test_child_products_inherit_fields(self):
91 92
         p = Product.objects.create(
92
-            parent=self.parent, product_class=self.product_class,
93
-            structure=Product.CHILD)
93
+            parent=self.parent,
94
+            structure=Product.CHILD,
95
+            is_discountable=True)
94 96
         self.assertEqual("Parent product", p.get_title())
95
-
96
-    def test_child_products_inherit_product_class(self):
97
-        p = Product.objects.create(
98
-            parent=self.parent, structure=Product.CHILD)
99 97
         self.assertEqual("Clothing", p.get_product_class().name)
98
+        self.assertEqual(False, p.get_is_discountable())
100 99
 
101 100
     def test_child_products_are_not_part_of_browsable_set(self):
102 101
         Product.objects.create(

+ 9
- 8
tests/unit/dashboard/catalogue_form_tests.py Прегледај датотеку

@@ -1,24 +1,25 @@
1 1
 from django.test import TestCase
2
-from django_dynamic_fixture import G
3 2
 
4
-from oscar.apps.catalogue import models
5 3
 from oscar.apps.dashboard.catalogue import forms
4
+from oscar.test import factories
6 5
 
7 6
 
8 7
 class TestCreateProductForm(TestCase):
9 8
 
10 9
     def setUp(self):
11
-        self.pclass = G(models.ProductClass)
10
+        self.product_class = factories.ProductClassFactory()
12 11
 
13
-    def submit(self, data):
14
-        return forms.ProductForm(self.pclass, data=data)
12
+    def submit(self, data, parent=None):
13
+        return forms.ProductForm(self.product_class, parent=parent, data=data)
15 14
 
16 15
     def test_validates_that_parent_products_must_have_title(self):
17 16
         form = self.submit({'structure': 'parent'})
18 17
         self.assertFalse(form.is_valid())
18
+        form = self.submit({'structure': 'parent', 'title': 'foo'})
19
+        self.assertTrue(form.is_valid())
19 20
 
20 21
     def test_validates_that_child_products_dont_need_a_title(self):
21
-        parent = G(
22
-            models.Product, product_class=self.pclass, structure='parent')
23
-        form = self.submit({'structure': 'child', 'parent': parent.id})
22
+        parent = factories.ProductFactory(
23
+            product_class=self.product_class, structure='parent')
24
+        form = self.submit({'structure': 'child'}, parent=parent)
24 25
         self.assertTrue(form.is_valid())

Loading…
Откажи
Сачувај