瀏覽代碼

Added file upload handling within range products

master
David Winterbottom 13 年之前
父節點
當前提交
80d9b3e023

+ 20
- 1
oscar/apps/dashboard/ranges/forms.py 查看文件

@@ -4,19 +4,38 @@ from django import forms
4 4
 from django.db.models import get_model
5 5
 
6 6
 Product = get_model('catalogue', 'Product')
7
+Range = get_model('offer', 'Range')
8
+
9
+
10
+class RangeForm(forms.ModelForm):
11
+
12
+    class Meta:
13
+        model = Range
14
+        exclude = ('included_products', 'excluded_products', 'classes')
7 15
 
8 16
 
9 17
 class RangeProductForm(forms.Form):
10 18
     query = forms.CharField(max_length=1024,
19
+                            label="Product SKUs",
11 20
                             widget=forms.Textarea,
21
+                            required=False,
12 22
                             help_text="""You can paste in a selection of SKUs""")
23
+    file_upload = forms.FileField(label="File of SKUs", required=False)
13 24
 
14 25
     def __init__(self, range, *args, **kwargs):
15 26
         self.range = range
16 27
         super(RangeProductForm, self).__init__(*args, **kwargs)
17 28
 
29
+    def clean(self):
30
+        clean_data = super(RangeProductForm, self).clean()
31
+        if not clean_data.get('query') and not clean_data.get('file_upload'):
32
+            raise forms.ValidationError("You must submit either a list of SKUs or a file")
33
+        return clean_data
34
+
18 35
     def clean_query(self):
19 36
         raw = self.cleaned_data['query']
37
+        if not raw:
38
+            return raw
20 39
 
21 40
         # Check that the search matches some products
22 41
         skus = re.compile(r'[\w-]+').findall(raw)
@@ -40,7 +59,7 @@ class RangeProductForm(forms.Form):
40 59
         return raw
41 60
 
42 61
     def get_products(self):
43
-        return self.products
62
+        return self.products if hasattr(self, 'products') else []
44 63
 
45 64
     def get_missing_skus(self):
46 65
         return self.missing_skus

+ 208
- 0
oscar/apps/dashboard/ranges/migrations/0001_initial.py 查看文件

@@ -0,0 +1,208 @@
1
+# encoding: utf-8
2
+import datetime
3
+from south.db import db
4
+from south.v2 import SchemaMigration
5
+from django.db import models
6
+
7
+class Migration(SchemaMigration):
8
+
9
+    def forwards(self, orm):
10
+
11
+        # Adding model 'RangeProductFileUpload'
12
+        db.create_table('ranges_rangeproductfileupload', (
13
+            ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14
+            ('range', self.gf('django.db.models.fields.related.ForeignKey')(related_name='file_uploads', to=orm['offer.Range'])),
15
+            ('filepath', self.gf('django.db.models.fields.CharField')(max_length=255)),
16
+            ('size', self.gf('django.db.models.fields.PositiveIntegerField')()),
17
+            ('uploaded_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
18
+            ('date_uploaded', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
19
+            ('status', self.gf('django.db.models.fields.CharField')(default='Pending', max_length=32)),
20
+            ('error_message', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)),
21
+            ('date_processed', self.gf('django.db.models.fields.DateTimeField')(null=True)),
22
+            ('num_new_skus', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)),
23
+            ('num_unknown_skus', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)),
24
+            ('num_duplicate_skus', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)),
25
+        ))
26
+        db.send_create_signal('ranges', ['RangeProductFileUpload'])
27
+
28
+
29
+    def backwards(self, orm):
30
+
31
+        # Deleting model 'RangeProductFileUpload'
32
+        db.delete_table('ranges_rangeproductfileupload')
33
+
34
+
35
+    models = {
36
+        'auth.group': {
37
+            'Meta': {'object_name': 'Group'},
38
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
40
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
41
+        },
42
+        'auth.permission': {
43
+            'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
44
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
45
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
46
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
47
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
48
+        },
49
+        'auth.user': {
50
+            'Meta': {'object_name': 'User'},
51
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
52
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
53
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
54
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
55
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
56
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
57
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
58
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
59
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
60
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
61
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
62
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
63
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
64
+        },
65
+        'catalogue.attributeentity': {
66
+            'Meta': {'object_name': 'AttributeEntity'},
67
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
68
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
69
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
70
+            'type': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'entities'", 'to': "orm['catalogue.AttributeEntityType']"})
71
+        },
72
+        'catalogue.attributeentitytype': {
73
+            'Meta': {'object_name': 'AttributeEntityType'},
74
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
75
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
76
+            'slug': ('django.db.models.fields.SlugField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'})
77
+        },
78
+        'catalogue.attributeoption': {
79
+            'Meta': {'object_name': 'AttributeOption'},
80
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'options'", 'to': "orm['catalogue.AttributeOptionGroup']"}),
81
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
82
+            'option': ('django.db.models.fields.CharField', [], {'max_length': '255'})
83
+        },
84
+        'catalogue.attributeoptiongroup': {
85
+            'Meta': {'object_name': 'AttributeOptionGroup'},
86
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
87
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'})
88
+        },
89
+        'catalogue.category': {
90
+            'Meta': {'ordering': "['name']", 'object_name': 'Category'},
91
+            'depth': ('django.db.models.fields.PositiveIntegerField', [], {}),
92
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
93
+            'full_name': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'db_index': 'True'}),
94
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
95
+            'image': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}),
96
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
97
+            'numchild': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
98
+            'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
99
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '1024', 'db_index': 'True'})
100
+        },
101
+        'catalogue.option': {
102
+            'Meta': {'object_name': 'Option'},
103
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
104
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
105
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
106
+            'type': ('django.db.models.fields.CharField', [], {'default': "'Required'", 'max_length': '128'})
107
+        },
108
+        'catalogue.product': {
109
+            'Meta': {'ordering': "['-date_created']", 'object_name': 'Product'},
110
+            'attributes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.ProductAttribute']", 'through': "orm['catalogue.ProductAttributeValue']", 'symmetrical': 'False'}),
111
+            'categories': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Category']", 'through': "orm['catalogue.ProductCategory']", 'symmetrical': 'False'}),
112
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
113
+            'date_updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
114
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
115
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
116
+            'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'variants'", 'null': 'True', 'to': "orm['catalogue.Product']"}),
117
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductClass']", 'null': 'True'}),
118
+            'product_options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
119
+            'recommended_products': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Product']", 'symmetrical': 'False', 'through': "orm['catalogue.ProductRecommendation']", 'blank': 'True'}),
120
+            'related_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'relations'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
121
+            'score': ('django.db.models.fields.FloatField', [], {'default': '0.0', 'db_index': 'True'}),
122
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'db_index': 'True'}),
123
+            'status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '128', 'null': 'True', 'blank': 'True'}),
124
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
125
+            'upc': ('django.db.models.fields.CharField', [], {'max_length': '64', 'unique': 'True', 'null': 'True', 'blank': 'True'})
126
+        },
127
+        'catalogue.productattribute': {
128
+            'Meta': {'ordering': "['code']", 'object_name': 'ProductAttribute'},
129
+            'code': ('django.db.models.fields.SlugField', [], {'max_length': '128', 'db_index': 'True'}),
130
+            'entity_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntityType']", 'null': 'True', 'blank': 'True'}),
131
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
132
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
133
+            'option_group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOptionGroup']", 'null': 'True', 'blank': 'True'}),
134
+            'product_class': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attributes'", 'null': 'True', 'to': "orm['catalogue.ProductClass']"}),
135
+            'required': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
136
+            'type': ('django.db.models.fields.CharField', [], {'default': "'text'", 'max_length': '20'})
137
+        },
138
+        'catalogue.productattributevalue': {
139
+            'Meta': {'object_name': 'ProductAttributeValue'},
140
+            'attribute': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.ProductAttribute']"}),
141
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
142
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attribute_values'", 'to': "orm['catalogue.Product']"}),
143
+            'value_boolean': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
144
+            'value_date': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
145
+            'value_entity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeEntity']", 'null': 'True', 'blank': 'True'}),
146
+            'value_float': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
147
+            'value_integer': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
148
+            'value_option': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.AttributeOption']", 'null': 'True', 'blank': 'True'}),
149
+            'value_richtext': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
150
+            'value_text': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
151
+        },
152
+        'catalogue.productcategory': {
153
+            'Meta': {'ordering': "['-is_canonical']", 'object_name': 'ProductCategory'},
154
+            'category': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Category']"}),
155
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
156
+            'is_canonical': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
157
+            'product': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
158
+        },
159
+        'catalogue.productclass': {
160
+            'Meta': {'ordering': "['name']", 'object_name': 'ProductClass'},
161
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
162
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
163
+            'options': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['catalogue.Option']", 'symmetrical': 'False', 'blank': 'True'}),
164
+            'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '128', 'db_index': 'True'})
165
+        },
166
+        'catalogue.productrecommendation': {
167
+            'Meta': {'object_name': 'ProductRecommendation'},
168
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
169
+            'primary': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'primary_recommendations'", 'to': "orm['catalogue.Product']"}),
170
+            'ranking': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '0'}),
171
+            'recommendation': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['catalogue.Product']"})
172
+        },
173
+        'contenttypes.contenttype': {
174
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
175
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
176
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
177
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
178
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
179
+        },
180
+        'offer.range': {
181
+            'Meta': {'object_name': 'Range'},
182
+            'classes': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'classes'", 'blank': 'True', 'to': "orm['catalogue.ProductClass']"}),
183
+            'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
184
+            'excluded_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'excludes'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
185
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
186
+            'included_categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': "orm['catalogue.Category']"}),
187
+            'included_products': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'includes'", 'blank': 'True', 'to': "orm['catalogue.Product']"}),
188
+            'includes_all_products': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
189
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'})
190
+        },
191
+        'ranges.rangeproductfileupload': {
192
+            'Meta': {'ordering': "('-date_uploaded',)", 'object_name': 'RangeProductFileUpload'},
193
+            'date_processed': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
194
+            'date_uploaded': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
195
+            'error_message': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}),
196
+            'filepath': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
197
+            'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
198
+            'num_duplicate_skus': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
199
+            'num_new_skus': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
200
+            'num_unknown_skus': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True'}),
201
+            'range': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'file_uploads'", 'to': "orm['offer.Range']"}),
202
+            'size': ('django.db.models.fields.PositiveIntegerField', [], {}),
203
+            'status': ('django.db.models.fields.CharField', [], {'default': "'Pending'", 'max_length': '32'}),
204
+            'uploaded_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
205
+        }
206
+    }
207
+
208
+    complete_apps = ['ranges']

+ 0
- 0
oscar/apps/dashboard/ranges/migrations/__init__.py 查看文件


+ 85
- 0
oscar/apps/dashboard/ranges/models.py 查看文件

@@ -0,0 +1,85 @@
1
+import datetime
2
+import os
3
+import csv
4
+
5
+from django.db import models
6
+Product = models.get_model('catalogue', 'Product')
7
+
8
+
9
+class RangeProductFileUpload(models.Model):
10
+    range = models.ForeignKey('offer.Range', related_name='file_uploads')
11
+    filepath = models.CharField(max_length=255)
12
+    size = models.PositiveIntegerField()
13
+    uploaded_by = models.ForeignKey('auth.User')
14
+    date_uploaded = models.DateTimeField(auto_now_add=True)
15
+
16
+    PENDING, FAILED, PROCESSED = 'Pending', 'Failed', 'Processed'
17
+    choices = (
18
+        (PENDING, PENDING),
19
+        (FAILED, FAILED),
20
+        (PROCESSED, PROCESSED),
21
+    )
22
+    status = models.CharField(max_length=32, choices=choices, default=PENDING)
23
+    error_message = models.CharField(max_length=255, null=True)
24
+
25
+    # Post-processing audit fields
26
+    date_processed = models.DateTimeField(null=True)
27
+    num_new_skus = models.PositiveIntegerField(null=True)
28
+    num_unknown_skus = models.PositiveIntegerField(null=True)
29
+    num_duplicate_skus = models.PositiveIntegerField(null=True)
30
+
31
+    class Meta:
32
+        ordering = ('-date_uploaded',)
33
+
34
+    @property
35
+    def filename(self):
36
+        return os.path.basename(self.filepath)
37
+
38
+    def mark_as_failed(self, message=None):
39
+        self.date_processed = datetime.datetime.now()
40
+        self.error_message = message
41
+        self.status = self.FAILED
42
+        self.save()
43
+
44
+    def mark_as_processed(self, num_new, num_unknown, num_duplicate):
45
+        self.status = self.PROCESSED
46
+        self.date_processed = datetime.datetime.now()
47
+        self.num_new_skus = num_new
48
+        self.num_unknown_skus = num_unknown
49
+        self.num_duplicate_skus = num_duplicate
50
+        self.save()
51
+
52
+    def was_processing_successful(self):
53
+        return self.status == self.PROCESSED
54
+
55
+    def process(self):
56
+        """
57
+        Process the file upload and add products to the range
58
+        """
59
+        all_skus = list(self.extract_skus())
60
+        existing_skus = self.range.included_products.all().values_list(
61
+            'stockrecord__partner_sku', flat=True)
62
+        new_skus = list(set(all_skus) - set(existing_skus))
63
+
64
+        products = Product._default_manager.filter(stockrecord__partner_sku__in=new_skus)
65
+        for product in products:
66
+            self.range.included_products.add(product)
67
+
68
+        # Processing stats
69
+        found_skus = products.values_list('stockrecord__partner_sku', flat=True)
70
+        missing_skus = set(new_skus) - set(found_skus)
71
+        dupes = set(all_skus).intersection(set(existing_skus))
72
+
73
+        self.mark_as_processed(len(found_skus), len(missing_skus), len(dupes))
74
+
75
+    def extract_skus(self):
76
+        """
77
+        Extract all SKU-like strings from the file
78
+        """
79
+        reader = csv.reader(open(self.filepath, 'r'))
80
+        for row in reader:
81
+            for field in row:
82
+                yield field
83
+
84
+    def delete_file(self):
85
+        os.unlink(self.filepath)

+ 1
- 1
oscar/apps/dashboard/ranges/tests.py 查看文件

@@ -16,7 +16,7 @@ class RangeProductFormTests(TestCase):
16 16
     def submit_form(self, data):
17 17
         return forms.RangeProductForm(self.range, data)
18 18
 
19
-    def test_validation_of_query(self):
19
+    def test_either_query_or_file_must_be_submitted(self):
20 20
         form = self.submit_form({'query': ''})
21 21
         self.assertFalse(form.is_valid())
22 22
 

+ 48
- 4
oscar/apps/dashboard/ranges/views.py 查看文件

@@ -1,3 +1,5 @@
1
+import os
2
+
1 3
 from django.views.generic import (ListView, DeleteView, CreateView, UpdateView)
2 4
 from django.db.models.loading import get_model
3 5
 from django.core.urlresolvers import reverse
@@ -5,13 +7,17 @@ from django.contrib import messages
5 7
 from django.shortcuts import get_object_or_404
6 8
 from django.http import HttpResponseRedirect
7 9
 from django.template.defaultfilters import pluralize
10
+from django.conf import settings
8 11
 
9 12
 from oscar.views.generic import BulkEditMixin
10
-from oscar.core.loading import get_class
13
+from oscar.core.loading import get_classes
11 14
 
12 15
 Range = get_model('offer', 'Range')
13 16
 Product = get_model('catalogue', 'Product')
14
-RangeProductForm = get_class('dashboard.ranges.forms', 'RangeProductForm')
17
+RangeForm, RangeProductForm = get_classes('dashboard.ranges.forms',
18
+                                          ['RangeForm', 'RangeProductForm'])
19
+
20
+RangeProductFileUpload = get_model('ranges', 'RangeProductFileUpload')
15 21
 
16 22
 
17 23
 class RangeListView(ListView):
@@ -23,6 +29,7 @@ class RangeListView(ListView):
23 29
 class RangeCreateView(CreateView):
24 30
     model = Range
25 31
     template_name = 'dashboard/ranges/range_form.html'
32
+    form_class = RangeForm
26 33
 
27 34
     def get_success_url(self):
28 35
         messages.success(self.request, "Range created")
@@ -32,6 +39,7 @@ class RangeCreateView(CreateView):
32 39
 class RangeUpdateView(UpdateView):
33 40
     model = Range
34 41
     template_name = 'dashboard/ranges/range_form.html'
42
+    form_class = RangeForm
35 43
 
36 44
     def get_success_url(self):
37 45
         messages.success(self.request, "Range updated")
@@ -87,12 +95,20 @@ class RangeProductListView(ListView, BulkEditMixin):
87 95
 
88 96
     def add_products(self, request):
89 97
         range = self.get_range()
90
-        form = self.form_class(range, request.POST)
98
+        form = self.form_class(range, request.POST, request.FILES)
91 99
         if not form.is_valid():
92 100
             ctx = self.get_context_data(form=form, object_list=self.object_list)
93 101
             return self.render_to_response(ctx)
94 102
 
103
+        self.handle_query_products(request, form)
104
+        self.handle_file_products(request, range, form)
105
+        return HttpResponseRedirect(self.get_success_url(request))
106
+
107
+    def handle_query_products(self, request, form):
95 108
         products = form.get_products()
109
+        if not products:
110
+            return
111
+
96 112
         for product in products:
97 113
             range.included_products.add(product)
98 114
 
@@ -111,4 +127,32 @@ class RangeProductListView(ListView, BulkEditMixin):
111 127
             messages.warning(request,
112 128
                              "No product was found with SKU matching %s" % ', '.join(missing_skus))
113 129
 
114
-        return HttpResponseRedirect(self.get_success_url(request))
130
+    def handle_file_products(self, request, range, form):
131
+        upload = self.create_upload_object(request, range)
132
+        upload.process()
133
+        if not upload.was_processing_successful():
134
+            messages.error(request, upload.error_message)
135
+        else:
136
+            msg = "File processed: %d products added, %d duplicate SKUs, %d " \
137
+                  "SKUS were not found"
138
+            msg = msg % (upload.num_new_skus, upload.num_duplicate_skus,
139
+                         upload.num_unknown_skus)
140
+            if upload.num_new_skus:
141
+                messages.success(request, msg)
142
+            else:
143
+                messages.warning(request, msg)
144
+        upload.delete_file()
145
+
146
+    def create_upload_object(self, request, range):
147
+        f = request.FILES['file_upload']
148
+        destination_path = os.path.join(settings.OSCAR_UPLOAD_ROOT, f.name)
149
+        with open(destination_path, 'wb+') as dest:
150
+            for chunk in f.chunks():
151
+                dest.write(chunk)
152
+        upload = RangeProductFileUpload.objects.create(
153
+            range=range,
154
+            uploaded_by=request.user,
155
+            filepath=destination_path,
156
+            size=f.size
157
+        )
158
+        return upload

+ 2
- 1
oscar/defaults.py 查看文件

@@ -12,10 +12,11 @@ OSCAR_DEFAULT_CURRENCY = 'GBP'
12 12
 # Max number of products to keep on the user's history
13 13
 OSCAR_RECENTLY_VIEWED_PRODUCTS = 4
14 14
 
15
-# Image paths
15
+# Paths
16 16
 OSCAR_IMAGE_FOLDER = 'images/products/%Y/%m/'
17 17
 OSCAR_PROMOTION_FOLDER = 'images/promotions/'
18 18
 OSCAR_MISSING_IMAGE_URL = '/static/oscar/img/image_not_found.jpg'
19
+OSCAR_UPLOAD_ROOT = '/tmp'
19 20
 
20 21
 # Search settings
21 22
 OSCAR_SEARCH_SUGGEST_LIMIT = 10

+ 3
- 1
oscar/templates/dashboard/ranges/range_list.html 查看文件

@@ -43,7 +43,9 @@ Range management | {{ block.super }}
43 43
 				<td>{{ range.date_created }}</td>
44 44
 				<td>
45 45
 					<a class="btn btn-primary" href="{% url dashboard:range-update range.id %}">Edit</a>
46
-					<a class="btn btn-primary" href="{% url dashboard:range-products range.id %}">Edit products</a>
46
+					{% if not range.includes_all_products %}
47
+					<a class="btn btn-info" href="{% url dashboard:range-products range.id %}">Edit products</a>
48
+					{% endif %}
47 49
 					<a class="btn btn-danger" href="{% url dashboard:range-delete range.id %}">Delete</a>
48 50
 				</td>
49 51
 			</tr>

+ 36
- 3
oscar/templates/dashboard/ranges/range_product_list.html 查看文件

@@ -12,7 +12,11 @@ Products in range '{{ range.name }}' | {{ block.super }}
12 12
         <span class="divider">/</span>
13 13
     </li>
14 14
     <li>
15
-	<a href="{% url dashboard:range-update range.id %}">Range '{{ range.name }}'</a>
15
+	<a href="{% url dashboard:range-list %}">Ranges</a>
16
+        <span class="divider">/</span>
17
+    </li>
18
+    <li>
19
+	<a href="{% url dashboard:range-update range.id %}">'{{ range.name }}'</a>
16 20
         <span class="divider">/</span>
17 21
     </li>
18 22
 	<li class="active"><a href=".">Products</a></li>
@@ -31,12 +35,41 @@ Products in range '{{ range.name }}' | {{ block.super }}
31 35
 		<div class="sub-header">
32 36
 			<h3>Add products</h3>
33 37
 		</div>
34
-		<form action="." method="post" class="">
38
+		<form action="." method="post" class="" enctype="multipart/form-data">
35 39
 			{% csrf_token %}
36 40
 			<input type="hidden" name="action" value="add_products"/>
37 41
 			{% include 'partials/form_fields.html' with form=form %}
38 42
 			<button type="submit" class="btn btn-primary">Go!</button>
39 43
 		</form>
44
+		{% with uploads=range.file_uploads.all %}
45
+			{% if uploads %}
46
+			<div class="sub-header">
47
+				<h3>Upload history</h3>
48
+			</div>
49
+			<table class="table">
50
+				<thead>
51
+				<tr>
52
+					<th>Filename</th>
53
+					<th>New SKUs</th>
54
+					<th>Dupliate SKUs</th>
55
+					<th>Unknown SKUs</th>
56
+					<th>Date uploaded</th>
57
+				</tr>
58
+				</thead>
59
+				<tbody>
60
+				{% for upload in uploads %}
61
+				<tr>
62
+					<td>{{ upload.filename }}</td>
63
+					<td>{{ upload.num_new_skus }}</td>
64
+					<td>{{ upload.num_unknown_skus }}</td>
65
+					<td>{{ upload.num_duplicate_skus }}</td>
66
+					<td>{{ upload.date_uploaded }}</td>
67
+				</tr>
68
+				{% endfor %}
69
+				</tbody>
70
+			</table>
71
+			{% endif %}
72
+		{% endwith %}
40 73
 	</div>
41 74
 
42 75
 	{% if products.count %}
@@ -45,7 +78,7 @@ Products in range '{{ range.name }}' | {{ block.super }}
45 78
 			<table class="table table-striped table-bordered">
46 79
 				<tr>
47 80
 					<th></th>
48
-					<th>SKU<th>
81
+					<th>SKU</th>
49 82
 					<th>Title</th>
50 83
 					<th>Price</th>
51 84
 					<th></th>

Loading…
取消
儲存