Просмотр исходного кода

Personalised products now work with the basket sub-app.

master
David Winterbottom 15 лет назад
Родитель
Сommit
a3c7301dcf

+ 33
- 9
oscar/basket/abstract_models.py Просмотреть файл

@@ -1,4 +1,5 @@
1 1
 from decimal import Decimal
2
+import zlib
2 3
 
3 4
 from django.contrib.auth.models import User
4 5
 from django.db import models
@@ -61,23 +62,39 @@ class AbstractBasket(models.Model):
61 62
     def flush(self):
62 63
         self.lines.all().delete()
63 64
     
64
-    def add_product(self, item, quantity=1):
65
+    def add_product(self, item, quantity=1, options=[]):
65 66
         """
66 67
         Convenience method for adding products to a basket
67 68
         """
69
+        line_ref = self._get_line_reference(item, options)
68 70
         try:
69
-            line = self.lines.get(product=item)
71
+            line = self.lines.get(line_reference=line_ref)
70 72
             line.quantity += quantity
71 73
             line.save()
72 74
         except ObjectDoesNotExist:
73
-            self.lines.create(basket=self, product=item, quantity=quantity)
75
+            line = self.lines.create(basket=self, line_reference=line_ref, product=item, quantity=quantity)
76
+            for option_dict in options:
77
+                o =line.attributes.create(line=line, option=option_dict['option'], value=option_dict['value'])
74 78
     
75 79
     def add_line(self, line):
76 80
         """
77 81
         For adding a line from another basket to this one
78 82
         """
79
-        line.basket = self
80
-        line.save()
83
+        try:
84
+            # Line already exists - bump its quantity and delete the old
85
+            existing_line = self.lines.get(line_reference=line.line_reference)
86
+            existing_line.quantity += line.quantity
87
+            existing_line.save()
88
+            line.delete()
89
+        except ObjectDoesNotExist:
90
+            # Line does not already exist - reassign its basket
91
+            line.basket = self
92
+            line.save()
93
+    
94
+    def _get_line_reference(self, item, options):
95
+        if not options:
96
+            return item.id
97
+        return "%d_%s" % (item.id, zlib.crc32(str(options)))
81 98
     
82 99
     # ==========
83 100
     # Properties
@@ -167,6 +184,16 @@ class AbstractLine(models.Model):
167 184
     def line_price_incl_tax(self):
168 185
         return self.quantity * self.unit_price_incl_tax
169 186
     
187
+    @property
188
+    def description(self):
189
+        d = str(self.product)
190
+        ops = []
191
+        for attribute in self.attributes.all():
192
+            ops.append("%s = '%s'" % (attribute.option.name, attribute.value))
193
+        if ops:
194
+            d = "%s (%s)" % (d, ", ".join(ops))
195
+        return d
196
+    
170 197
     def save(self, *args, **kwargs):
171 198
         if self.quantity == 0:
172 199
             return self.delete(*args, **kwargs)
@@ -187,12 +214,9 @@ class AbstractLineAttribute(models.Model):
187 214
     An attribute of a basket line
188 215
     """
189 216
     line = models.ForeignKey('basket.Line', related_name='attributes')
190
-    type = models.CharField(_("Type"), max_length=128)
217
+    option = models.ForeignKey('product.option')
191 218
     value = models.CharField(_("Value"), max_length=255)    
192 219
     
193
-    def get_hash(self):
194
-        return zlib.crc32(self.type + self.value)
195
-    
196 220
     class Meta:
197 221
         abstract = True
198 222
     

+ 7
- 2
oscar/basket/factory.py Просмотреть файл

@@ -39,7 +39,10 @@ def get_saved_basket(request):
39 39
 def _get_basket(request, cookie_key, manager):
40 40
     b = None
41 41
     if request.user.is_authenticated():
42
-        b = manager.get(owner=request.user)
42
+        try:
43
+            b = manager.get(owner=request.user)
44
+        except basket_models.Basket.DoesNotExist, e:
45
+            pass
43 46
     else:
44 47
         b = _get_cookie_basket(request, cookie_key, manager)
45 48
     return b    
@@ -61,7 +64,7 @@ def _get_or_create_cookie_basket(request, response, cookie_key, manager):
61 64
         # new one and store the id and hash in a cookie.
62 65
         basket = manager.create()
63 66
         cookie = "%s_%s" % (basket.id, _get_basket_hash(basket.id))
64
-        response.set_cookie(cookie_key, cookie, max_age=COOKIE_LIFETIME)
67
+        response.set_cookie(cookie_key, cookie, max_age=COOKIE_LIFETIME, httponly=True)
65 68
     else:
66 69
         basket = anon_basket
67 70
     return basket 
@@ -78,6 +81,8 @@ def _get_cookie_basket(request, cookie_key, manager):
78 81
                 b = manager.get(pk=basket_id)
79 82
             except basket_models.Basket.DoesNotExist, e:
80 83
                 b = None
84
+        else:
85
+            response.delete_cookie(cookie_key)
81 86
     return b  
82 87
 
83 88
 def _get_basket_hash(id):

+ 43
- 4
oscar/basket/forms.py Просмотреть файл

@@ -1,6 +1,45 @@
1 1
 from django import forms
2 2
 
3
-class AddToBasketForm(forms.Form):
4
-    action = forms.CharField(widget=forms.HiddenInput(), initial='add')
5
-    product_id = forms.IntegerField(widget=forms.HiddenInput())
6
-    quantity = forms.IntegerField(min_value=1)
3
+
4
+class FormFactory(object):
5
+    
6
+    def create(self, item, values=None):
7
+        """
8
+        For dynamically creating add-to-basket forms for a given product
9
+        """
10
+        self.values = values
11
+        if item.is_group:
12
+            return self._create_group_product_form(item)
13
+        return self._create_product_form(item)
14
+
15
+    def _create_group_product_form(self, item):
16
+        pass
17
+    
18
+    def _create_product_form(self, item):
19
+        # See http://www.b-list.org/weblog/2008/nov/09/dynamic-forms/ for 
20
+        # advice on how this works.
21
+        self.fields = {'action': forms.CharField(widget=forms.HiddenInput(), initial='add'),
22
+                       'product_id': forms.IntegerField(widget=forms.HiddenInput(), min_value=1),
23
+                       'quantity': forms.IntegerField(min_value=1)}
24
+        if not self.values:
25
+            self.values = {'action': 'add', 
26
+                           'product_id': item.id, 
27
+                           'quantity': 1}
28
+            
29
+        for option in item.options.all():
30
+            self._add_option_field(item, option)
31
+            
32
+        form_class = type('AddToBasketForm', (forms.BaseForm,), {'base_fields': self.fields})
33
+        return form_class(self.values)
34
+    
35
+    def _add_option_field(self, item, option):
36
+        """
37
+        Creates the appropriate form field for the product option.
38
+        
39
+        This is designed to be overridden so that specific widgets can be used for 
40
+        certain types of options.
41
+        """
42
+        self.fields[option.code] = forms.CharField()
43
+    
44
+
45
+    

+ 12
- 1
oscar/basket/managers.py Просмотреть файл

@@ -2,9 +2,20 @@ from django.db import models
2 2
 
3 3
 
4 4
 class OpenBasketManager(models.Manager):
5
+    
5 6
     def get_query_set(self):
6 7
         return super(OpenBasketManager, self).get_query_set().filter(status="Open")
7 8
     
9
+    def get_or_create(self, **kwargs):
10
+        return self.get_query_set().get_or_create(status="Open", **kwargs)
11
+    
8 12
 class SavedBasketManager(models.Manager):
13
+    
9 14
     def get_query_set(self):
10
-        return super(SavedBasketManager, self).get_query_set().filter(status="Saved")
15
+        return super(SavedBasketManager, self).get_query_set().filter(status="Saved")
16
+    
17
+    def create(self, **kwargs):
18
+        return self.get_query_set().create(status="Saved", **kwargs)
19
+    
20
+    def get_or_create(self, **kwargs):
21
+        return self.get_query_set().get_or_create(status="Saved", **kwargs)

+ 2
- 2
oscar/basket/templates/basket/summary.html Просмотреть файл

@@ -31,7 +31,7 @@ Your basket is currently empty - go and <a href="/shop/product/">buy something</
31 31
     </tr>
32 32
     {% for line in basket.lines.all %}
33 33
     <tr>
34
-        <td><a href="{{ line.product.get_absolute_url }}">{{ line.product }}</a></td>
34
+        <td><a href="{{ line.product.get_absolute_url }}">{{ line.description }}</a></td>
35 35
         <td>
36 36
             <form action="{% url oscar-basket-line line.line_reference %}" method="post">
37 37
                 {% csrf_token %}
@@ -96,7 +96,7 @@ Your basket is currently empty - go and <a href="/shop/product/">buy something</
96 96
     </tr>
97 97
     {% for line in saved_basket.lines.all %}
98 98
     <tr>
99
-        <td><a href="{{ line.product.get_absolute_url }}">{{ line.product }}</a></td>
99
+        <td><a href="{{ line.product.get_absolute_url }}">{{ line.description }}</a></td>
100 100
         <td>
101 101
             <form action="{% url oscar-saved-basket-line line.line_reference %}" method="post">
102 102
                 {% csrf_token %}

+ 1
- 1
oscar/basket/urls.py Просмотреть файл

@@ -12,7 +12,7 @@ def basket_view(request, *args, **kwargs):
12 12
     return BasketView()(request, *args, **kwargs)
13 13
 
14 14
 urlpatterns = patterns('oscar.basket.views',
15
-    url(r'^line/(?P<line_reference>\w+)/$', line_view, name='oscar-basket-line'),
15
+    url(r'^line/(?P<line_reference>[\w-]+)/$', line_view, name='oscar-basket-line'),
16 16
     url(r'^saved-line/(?P<line_reference>\w+)/$', saved_line_view, name='oscar-saved-basket-line'),
17 17
     url(r'^$', basket_view, name='oscar-basket'),
18 18
 )

+ 13
- 6
oscar/basket/views.py Просмотреть файл

@@ -8,7 +8,7 @@ from oscar.services import import_module
8 8
 
9 9
 # Using dynamic loading
10 10
 basket_models = import_module('basket.models', ['Basket', 'Line', 'InvalidBasketLineError'])
11
-basket_forms = import_module('basket.forms', ['AddToBasketForm'])
11
+basket_forms = import_module('basket.forms', ['FormFactory'])
12 12
 basket_factory = import_module('basket.factory', ['get_or_create_open_basket', 'get_open_basket', 
13 13
                                                   'get_or_create_saved_basket', 'get_saved_basket'])
14 14
 product_models = import_module('product.models', ['Item'])
@@ -55,14 +55,21 @@ class BasketView(object):
55 55
         messages.info(self.request, "Your basket has been emptied")
56 56
         
57 57
     def do_add(self, basket):
58
-        form = basket_forms.AddToBasketForm(self.request.POST)
58
+        item = get_object_or_404(product_models.Item.objects, pk=self.request.POST['product_id'])
59
+        factory = basket_forms.FormFactory()
60
+        form = factory.create(item, self.request.POST)
59 61
         if not form.is_valid():
60
-            messages.error("Unable to add your item to the basket - submission not valid")
62
+            # @todo Put form errors in session and redirect back to product page
63
+            messages.error(self.request, "Unable to add your item to the basket - submission not valid")
61 64
         else:
62
-            product = get_object_or_404(product_models.Item.objects, pk=form.cleaned_data['product_id'])
63
-            basket.add_product(product, form.cleaned_data['quantity'])
65
+            # Extract product options from POST
66
+            options = []
67
+            for option in item.options.all():
68
+                if option.code in form.cleaned_data:
69
+                    options.append({'option': option, 'value': form.cleaned_data[option.code]})
70
+            basket.add_product(item, form.cleaned_data['quantity'], options)
64 71
             messages.info(self.request, "'%s' (quantity %d) has been added to your basket" %
65
-                          (product.get_title(), form.cleaned_data['quantity']))
72
+                          (item.get_title(), form.cleaned_data['quantity']))
66 73
  
67 74
 
68 75
 class LineView(object):

+ 34
- 3
oscar/product/abstract_models.py Просмотреть файл

@@ -7,6 +7,7 @@ from django.db import models
7 7
 from django.utils.translation import ugettext_lazy as _
8 8
 from django.template.defaultfilters import slugify
9 9
 from django.core.urlresolvers import reverse
10
+from django.core.exceptions import ObjectDoesNotExist
10 11
 
11 12
 from oscar.product.managers import BrowsableItemManager
12 13
 
@@ -15,7 +16,7 @@ def _convert_to_underscores(str):
15 16
     For converting a string in CamelCase or normal text with spaces
16 17
     to the normal underscored variety
17 18
     """
18
-    without_whitespace = re.sub('\s*', '_', str.strip())
19
+    without_whitespace = re.sub('\s+', '_', str.strip())
19 20
     s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', without_whitespace)
20 21
     return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
21 22
 
@@ -26,7 +27,7 @@ class AbstractItemClass(models.Model):
26 27
     """
27 28
     name = models.CharField(_('name'), max_length=128)
28 29
     slug = models.SlugField(max_length=128, unique=True)
29
-    options = models.ManyToManyField('product.Option')
30
+    options = models.ManyToManyField('product.Option', blank=True)
30 31
 
31 32
     class Meta:
32 33
         abstract = True
@@ -72,13 +73,15 @@ class AbstractItem(models.Model):
72 73
     item_class = models.ForeignKey('product.ItemClass', verbose_name=_('item class'), null=True,
73 74
         help_text="""Choose what type of product this is""")
74 75
     attribute_types = models.ManyToManyField('product.AttributeType', through='ItemAttributeValue')
75
-    options = models.ManyToManyField('product.Option')
76
+    options = models.ManyToManyField('product.Option', blank=True)
76 77
     date_created = models.DateTimeField(auto_now_add=True)
77 78
     date_updated = models.DateTimeField(auto_now=True, null=True, default=None)
78 79
 
79 80
     objects = models.Manager()
80 81
     browsable = BrowsableItemManager()
81 82
 
83
+    # Properties
84
+
82 85
     @property
83 86
     def is_top_level(self):
84 87
         return self.parent == None
@@ -91,6 +94,22 @@ class AbstractItem(models.Model):
91 94
     def is_variant(self):
92 95
         return not self.is_top_level
93 96
 
97
+    @property
98
+    def min_variant_price_incl_tax(self):
99
+        return self._min_variant_price('price_incl_tax')
100
+    
101
+    @property
102
+    def min_variant_price_excl_tax(self):
103
+        return self._min_variant_price('price_excl_tax')
104
+
105
+    @property
106
+    def has_stockrecord(self):
107
+        try:
108
+            sr = self.stockrecord
109
+            return True
110
+        except ObjectDoesNotExist:
111
+            return False
112
+
94 113
     def attribute_summary(self):
95 114
         return ", ".join([attribute.__unicode__() for attribute in self.attributes.all()])
96 115
 
@@ -107,6 +126,18 @@ class AbstractItem(models.Model):
107 126
             return self.parent.item_class
108 127
         return None
109 128
 
129
+    # Helpers
130
+    
131
+    def _min_variant_price(self, property):
132
+        prices = []
133
+        for variant in self.variants.all():
134
+            if variant.has_stockrecord:
135
+                prices.append(getattr(variant.stockrecord, property))
136
+        if not prices:
137
+            return None
138
+        prices.sort()
139
+        return prices[0]
140
+
110 141
     class Meta:
111 142
         abstract = True
112 143
         ordering = ['-date_created']

+ 1
- 4
oscar/product/admin.py Просмотреть файл

@@ -7,13 +7,10 @@ class AttributeInline(admin.TabularInline):
7 7
 class ItemClassAdmin(admin.ModelAdmin):
8 8
     prepopulated_fields = {"slug": ("name",)}
9 9
     
10
-class OptionInline(admin.TabularInline):
11
-    model = Item.options.through    
12
-
13 10
 class ItemAdmin(admin.ModelAdmin):
14 11
     list_display = ('get_title', 'upc', 'get_item_class', 'is_top_level', 'is_group', 'is_variant', 'attribute_summary', 'date_created')
15 12
     prepopulated_fields = {"slug": ("title",)}
16
-    inlines = [AttributeInline, OptionInline]
13
+    inlines = [AttributeInline]
17 14
 
18 15
 admin.site.register(ItemClass, ItemClassAdmin)
19 16
 admin.site.register(Item, ItemAdmin)

+ 23
- 10
oscar/product/templates/product/browse.html Просмотреть файл

@@ -7,15 +7,21 @@
7 7
 
8 8
 {% block content %}
9 9
 
10
+{% if products.count %}
11
+
10 12
 <ol>
11
-{% for product in products.object_list %}
13
+{% for product in products %}
12 14
 <li>
13 15
     <a href="{{ product.get_absolute_url }}">{{ product.get_title }}</a><br/>
14
-    {% if product.stockrecord %} 
15
-        &pound;{{ product.stockrecord.price_incl_tax }}<br/>
16
-        {{ product.stockrecord.availability }}
16
+    {% if product.is_group %}
17
+        From &pound;{{ product.min_variant_price_incl_tax }}
17 18
     {% else %}
18
-        Not available
19
+        {% if product.has_stockrecord %} 
20
+            &pound;{{ product.stockrecord.price_incl_tax }}<br/>
21
+            {{ product.stockrecord.availability }}
22
+        {% else %}
23
+            Not available    
24
+        {% endif %}
19 25
     {% endif %}    
20 26
 </li>
21 27
 {% endfor %}
@@ -23,19 +29,26 @@
23 29
 
24 30
 <div class="pagination">
25 31
     <span class="step-links">
26
-        {% if products.has_previous %}
27
-            <a href="?page={{ products.previous_page_number }}">previous</a>
32
+    
33
+        {% if page_obj.has_previous %}
34
+            <a href="?page={{ page_obj.previous_page_number }}">previous</a>
28 35
         {% endif %}
29 36
 
30 37
         <span class="current">
31
-            Page {{ products.number }} of {{ products.paginator.num_pages }}.
38
+            Page {{ page_obj.number }} of {{ paginator.num_pages }}.
32 39
         </span>
33 40
 
34
-        {% if products.has_next %}
35
-            <a href="?page={{ products.next_page_number }}">next</a>
41
+        {% if page_obj.has_next %}
42
+            <a href="?page={{ page_obj.next_page_number }}">next</a>
36 43
         {% endif %}
37 44
     </span>
38 45
 </div>
39 46
 
47
+{% else %}
48
+
49
+<p>No products found.</p>
50
+
51
+{% endif %}
52
+
40 53
 {% endblock content %}
41 54
 

+ 1
- 1
oscar/product/templates/product/item.html Просмотреть файл

@@ -55,7 +55,7 @@
55 55
     </form>
56 56
 {% else %}
57 57
 
58
-<form action="/shop/basket/" method="post">
58
+<form action="{% url oscar-basket %}" method="post">
59 59
     {% csrf_token %}
60 60
     {{ form.as_p }}
61 61
     <input type="submit" value="Add to basket" />

+ 4
- 3
oscar/product/urls.py Просмотреть файл

@@ -1,7 +1,8 @@
1 1
 from django.conf.urls.defaults import *
2
+from oscar.product.views import ItemDetailView, ProductListView, ItemClassListView
2 3
 
3 4
 urlpatterns = patterns('oscar.product.views',
4
-    url(r'(?P<item_class_slug>[\w-]+)/(?P<item_slug>[\w-]*)-(?P<item_id>\d+)/$', 'item', name='oscar-product-item'),
5
-    url(r'(?P<item_class_slug>[\w-]+)/$', 'item_class', name='oscar-product-item-class'),
6
-    url(r'$', 'all', name='oscar-products'),
5
+    url(r'(?P<item_class_slug>[\w-]+)/(?P<item_slug>[\w-]*)-(?P<item_id>\d+)/$', ItemDetailView.as_view(), name='oscar-product-item'),
6
+    url(r'(?P<item_class_slug>[\w-]+)/$', ItemClassListView.as_view(), name='oscar-product-item-class'),
7
+    url(r'^$', ProductListView.as_view(), name='oscar-products'),
7 8
 )

+ 58
- 47
oscar/product/views.py Просмотреть файл

@@ -1,62 +1,73 @@
1 1
 from django.conf import settings
2 2
 from django.http import HttpResponse, Http404, HttpResponseRedirect
3 3
 from django.template import Context, loader, RequestContext
4
-from django.shortcuts import render_to_response, get_object_or_404
4
+from django.shortcuts import render, get_object_or_404
5 5
 from django.core.urlresolvers import reverse
6 6
 from django.core.paginator import Paginator, InvalidPage, EmptyPage
7
+from django.views.generic import ListView, DetailView
7 8
 
8 9
 from oscar.services import import_module
9 10
 
10 11
 product_models = import_module('product.models', ['Item', 'ItemClass'])
11
-basket_forms = import_module('basket.forms', ['AddToBasketForm'])
12
+basket_forms = import_module('basket.forms', ['FormFactory'])
12 13
 
13 14
 
14
-def item(request, item_class_slug, item_slug, item_id, template_file='product/item.html'):
15
-    """ 
16
-    Single product page
15
+class ItemDetailView(DetailView):
17 16
     """
18
-    item = get_object_or_404(product_models.Item, pk=item_id)
19
-    form = basket_forms.AddToBasketForm({'product_id': item_id, 'quantity': 1, 'action': 'add'})
20
-    return render_to_response(template_file, locals(), context_instance=RequestContext(request))
21
-
22
-
23
-def item_class(request, item_class_slug, template_file='product/browse-all.html', results_per_page=20):
24
-    item_class = get_object_or_404(product_models.ItemClass, slug=item_class_slug)
25
-    product_list = product_models.Item.browsable.filter(item_class=item_class)
26
-    paginator = Paginator(product_list, results_per_page)
27
-    
28
-    # Make sure page request is an int. If not, deliver first page.
29
-    try:
30
-        page = int(request.GET.get('page', '1'))
31
-    except ValueError:
32
-        page = 1
33
-    try:
34
-        products = paginator.page(page)
35
-    except (EmptyPage, InvalidPage):
36
-        products = paginator.page(paginator.num_pages)
37
-    
38
-    return render_to_response(template_file, locals())
39
-
40
-
41
-def all(request, template_file='product/browse.html', results_per_page=20):
42
-    if 'q' in request.GET and request.GET['q']:
43
-        query = request.GET['q'].strip()
44
-        product_list = product_models.Item.browsable.filter(title__icontains=query)
45
-        summary = "Products matching '%s'" % query
46
-    else:
47
-        product_list = product_models.Item.browsable.all()
48
-        summary = "All products"
49
-    paginator = Paginator(product_list, results_per_page)
17
+    View a single product.
18
+    """
19
+    template_name = "product/item.html"
50 20
     
51
-    # Make sure page request is an int. If not, deliver first page.
52
-    try:
53
-        page = int(request.GET.get('page', '1'))
54
-    except ValueError:
55
-        page = 1
56
-    try:
57
-        products = paginator.page(page)
58
-    except (EmptyPage, InvalidPage):
59
-        products = paginator.page(paginator.num_pages)
21
+    def get_object(self):
22
+        return get_object_or_404(product_models.Item, pk=self.kwargs['item_id'])
60 23
     
61
-    return render_to_response(template_file, locals())
24
+    def get_context_data(self, **kwargs):
25
+        context = super(ItemDetailView, self).get_context_data(**kwargs)
26
+        
27
+        # Add add-to-basket form for this product
28
+        factory = basket_forms.FormFactory()
29
+        context['form'] = factory.create(self.object)
30
+        
31
+        return context
32
+
33
+
34
+class ItemClassListView(ListView):
35
+    """
36
+    View products filtered by item-class.
37
+    """
38
+    context_object_name = "products"
39
+    template_name = 'product/browse.html'
40
+    paginate_by = 20
41
+
42
+    def get_queryset(self):
43
+        item_class = get_object_or_404(product_models.ItemClass, slug=self.kwargs['item_class_slug'])
44
+        return product_models.Item.browsable.filter(item_class=item_class)
45
+
46
+
47
+class ProductListView(ListView):
48
+
49
+    context_object_name = "products"
50
+    template_name = 'product/browse.html'
51
+    paginate_by = 20
52
+
53
+    def get_search_query(self):
54
+        q = None
55
+        if 'q' in self.request.GET and self.request.GET['q']:
56
+            q = self.request.GET['q'].strip()
57
+        return q
62 58
 
59
+    def get_queryset(self):
60
+        q = self.get_search_query()
61
+        if q:
62
+            return product_models.Item.browsable.filter(title__icontains=q)
63
+        else:
64
+            return product_models.Item.browsable.all()
65
+        
66
+    def get_context_data(self, **kwargs):
67
+        context = super(ProductListView, self).get_context_data(**kwargs)
68
+        q = self.get_search_query()
69
+        if not q:
70
+            context['summary'] = 'All products'
71
+        else:
72
+            context['summary'] = "Products matching '%s'" % q
73
+        return context

Загрузка…
Отмена
Сохранить