django-mp-prom 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ include README.md
2
+ include LICENSE
3
+ include requirements.txt
4
+ recursive-include prom/locale *
5
+ recursive-include prom/static *
6
+ recursive-include prom/templates *
7
+ prune *.pyc
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-mp-prom
3
+ Version: 0.0.1
4
+ Summary: Django Prom.ua export
5
+ Home-page: https://github.com/pmaigutyak/mp-prom
6
+ Download-URL: https://github.com/pmaigutyak/mp-prom/archive/0.0.1.tar.gz
7
+ Author: Paul Maigutyak
8
+ Author-email: pmaigutyak@gmail.com
9
+ License: MIT
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: download-url
13
+ Dynamic: home-page
14
+ Dynamic: license
15
+ Dynamic: summary
@@ -0,0 +1,98 @@
1
+ # MP-prom
2
+
3
+ Django Prom.ua product export.
4
+
5
+ ### Installation
6
+
7
+ Install with pip:
8
+
9
+ ```
10
+ pip install django-mp-prom
11
+ ```
12
+
13
+ Add `prom` to `INSTALLED_APPS`.
14
+
15
+ Set `DOMAIN` in settings. The `host` used for absolute links is `https://<DOMAIN>`.
16
+
17
+ ### Export
18
+
19
+ Subclass `prom.feed.ExportStrategy`. Implement `get_queryset` and every required product field. Override an optional field when the shop has that information.
20
+
21
+ ```python
22
+ from django.urls import path
23
+
24
+ from prom.feed import ExportStrategy
25
+ from products.models import Product
26
+
27
+
28
+ class ProductExport(ExportStrategy):
29
+
30
+ def get_queryset(self):
31
+ return Product.objects.filter(is_visible=True)
32
+
33
+ def get_id(self, product):
34
+ return product.pk
35
+
36
+ def get_name(self, product):
37
+ return product.name
38
+
39
+ def get_description(self, product):
40
+ return product.description
41
+
42
+ def get_price(self, product):
43
+ return product.price
44
+
45
+ def get_currency(self, product):
46
+ return 'UAH'
47
+
48
+ def get_availability(self, product):
49
+ return '+' if product.is_available else '-'
50
+
51
+ def get_code(self, product):
52
+ return product.code
53
+
54
+ def get_images(self, product):
55
+ if not product.logo:
56
+ return None
57
+ return f'{self.host}{product.logo.url}'
58
+
59
+
60
+ urlpatterns = [
61
+ path('prom.xls', ProductExport.as_view(), name='prom'),
62
+ ]
63
+ ```
64
+
65
+ Required product fields: `id`, `name`, `description`, `price`, `currency`, `availability`.
66
+
67
+ `availability` is a Prom value: `+` in stock, `-` out of stock, `@` service, `!` ready to ship, or a number of days.
68
+
69
+ Optional product fields stay out of the file until overridden: `code`, `name_ua`, `keywords`, `keywords_ua`, `description_ua`, `product_type`, `price_from`, `measure_unit`, `minimum_order`, `wholesale_price`, `minimum_wholesale_order`, `quantity`, `images`, `weight`, `width`, `height`, `length`, `discount`, `discount_date_start`, `discount_date_end`, `label`, `vendor`, `country`, `html_title`, `html_title_ua`, `html_description`, `html_description_ua`, `gifts`, `related`, `gift_ids`, `related_ids`, `group_number`, `group_name`, `catalog_url`, `packaging`, `notes`, `product_url`, `gtin`, `mpn`, `unique_id`, `catalog_id`, `group_id`, `variation_group_id`, `location`.
70
+
71
+ A list is written as one cell joined with a comma and a space. `get_images` can return a list of URLs. Empty values are written as blank cells.
72
+
73
+ ### Groups
74
+
75
+ The workbook always has `Export Products Sheet` and `Export Groups Sheet`. Override `get_groups` to add rows. Required group fields are `get_groups_number`, `get_groups_name` and `get_groups_id`.
76
+
77
+ ```python
78
+ def get_groups(self):
79
+ return Category.objects.all()
80
+
81
+ def get_groups_number(self, group):
82
+ return group.pk
83
+
84
+ def get_groups_name(self, group):
85
+ return group.name
86
+
87
+ def get_groups_id(self, group):
88
+ return group.pk
89
+ ```
90
+
91
+ Optional group fields stay out of the sheet until overridden: `name_ua`, `parent_number`, `parent_id`, `image`, `html_title`, `html_title_ua`, `html_description`, `html_description_ua`, `description_before`, `description_before_ua`, `description_after`, `description_after_ua`.
92
+
93
+ The file is cached at `tmp/prom.xls` for one hour.
94
+
95
+ ### Requirements
96
+
97
+ * Django
98
+ * xlwt
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: django-mp-prom
3
+ Version: 0.0.1
4
+ Summary: Django Prom.ua export
5
+ Home-page: https://github.com/pmaigutyak/mp-prom
6
+ Download-URL: https://github.com/pmaigutyak/mp-prom/archive/0.0.1.tar.gz
7
+ Author: Paul Maigutyak
8
+ Author-email: pmaigutyak@gmail.com
9
+ License: MIT
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: download-url
13
+ Dynamic: home-page
14
+ Dynamic: license
15
+ Dynamic: summary
@@ -0,0 +1,10 @@
1
+ MANIFEST.in
2
+ README.md
3
+ setup.py
4
+ django_mp_prom.egg-info/PKG-INFO
5
+ django_mp_prom.egg-info/SOURCES.txt
6
+ django_mp_prom.egg-info/dependency_links.txt
7
+ django_mp_prom.egg-info/top_level.txt
8
+ prom/__init__.py
9
+ prom/apps.py
10
+ prom/feed.py
File without changes
@@ -0,0 +1,7 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class PromConfig(AppConfig):
5
+
6
+ name = 'prom'
7
+ verbose_name = 'Prom'
@@ -0,0 +1,409 @@
1
+ import time
2
+ from io import BytesIO
3
+ from pathlib import Path
4
+
5
+ import xlwt
6
+ from django.conf import settings
7
+ from django.http import HttpResponse, HttpResponseNotFound
8
+ from django.views import View
9
+
10
+
11
+ FEED_FILE_NAME = 'prom.xls'
12
+ FEED_MAX_AGE = 60 * 60
13
+ PRODUCTS_SHEET = 'Export Products Sheet'
14
+ GROUPS_SHEET = 'Export Groups Sheet'
15
+
16
+
17
+ class ExportStrategy(View):
18
+ """Prom.ua Excel export.
19
+
20
+ Subclass this view, implement get_queryset and the required product
21
+ field methods. Override an optional field when the shop has that
22
+ information. Override get_groups and the group field methods to fill
23
+ the groups sheet. The shop domain is read from settings.DOMAIN.
24
+ """
25
+
26
+ file_name = FEED_FILE_NAME
27
+ max_age = FEED_MAX_AGE
28
+ content_type = 'application/ms-excel'
29
+ list_separator = ', '
30
+ product_fields = (
31
+ ('code', 'Код_товару'),
32
+ ('name', 'Назва_позиції'),
33
+ ('name_ua', 'Назва_позиції_укр'),
34
+ ('keywords', 'Пошукові_запити'),
35
+ ('keywords_ua', 'Пошукові_запити_укр'),
36
+ ('description', 'Опис'),
37
+ ('description_ua', 'Опис_укр'),
38
+ ('product_type', 'Тип_товару'),
39
+ ('price', 'Ціна'),
40
+ ('price_from', 'Ціна_від'),
41
+ ('currency', 'Валюта'),
42
+ ('measure_unit', 'Одиниця_виміру'),
43
+ ('minimum_order', 'Мінімальний_обсяг_замовлення'),
44
+ ('wholesale_price', 'Оптова_ціна'),
45
+ ('minimum_wholesale_order', 'Мінімальне_замовлення_опт'),
46
+ ('quantity', 'Кількість'),
47
+ ('images', 'Посилання_зображення'),
48
+ ('weight', 'Вага,кг'),
49
+ ('width', 'Ширина,см'),
50
+ ('height', 'Висота,см'),
51
+ ('length', 'Довжина,см'),
52
+ ('availability', 'Наявність'),
53
+ ('discount', 'Знижка'),
54
+ ('discount_date_start', 'Термін_дії_знижки_від'),
55
+ ('discount_date_end', 'Термін_дії_знижки_до'),
56
+ ('label', 'Ярлик'),
57
+ ('vendor', 'Виробник'),
58
+ ('country', 'Країна_виробник'),
59
+ ('html_title', 'HTML_заголовок'),
60
+ ('html_title_ua', 'HTML_заголовок_укр'),
61
+ ('html_description', 'HTML_опис'),
62
+ ('html_description_ua', 'HTML_опис_укр'),
63
+ ('gifts', 'Подарунки'),
64
+ ('related', 'Супутні'),
65
+ ('gift_ids', 'ID_Подарунків'),
66
+ ('related_ids', 'ID_Супутніх'),
67
+ ('group_number', 'Номер_групи'),
68
+ ('group_name', 'Назва_групи'),
69
+ ('catalog_url', 'Посилання_підрозділу'),
70
+ ('packaging', 'Спосіб_пакування'),
71
+ ('notes', 'Особисті_нотатки'),
72
+ ('product_url', 'Продукт_на_сайті'),
73
+ ('gtin', 'Код_маркування_(GTIN)'),
74
+ ('mpn', 'Номер_пристрою_(MPN)'),
75
+ ('id', 'Ідентифікатор_товару'),
76
+ ('unique_id', 'Унікальний_ідентифікатор'),
77
+ ('catalog_id', 'Ідентифікатор_підрозділу'),
78
+ ('group_id', 'Ідентифікатор_групи'),
79
+ ('variation_group_id', 'ID_групи_різновидів'),
80
+ ('location', 'Де_знаходиться_товар'),
81
+ )
82
+ required_product_fields = (
83
+ 'id',
84
+ 'name',
85
+ 'description',
86
+ 'price',
87
+ 'currency',
88
+ 'availability',
89
+ )
90
+ group_fields = (
91
+ ('number', 'Номер_групи'),
92
+ ('name', 'Назва_групи'),
93
+ ('name_ua', 'Назва_групи_укр'),
94
+ ('id', 'Ідентифікатор_групи'),
95
+ ('parent_number', 'Номер_батьківської_групи'),
96
+ ('parent_id', 'Ідентифікатор_батьківської_групи'),
97
+ ('image', 'Посилання_зображення_групи'),
98
+ ('html_title', 'HTML_заголовок_групи'),
99
+ ('html_title_ua', 'HTML_заголовок_групи_укр'),
100
+ ('html_description', 'HTML_опис_групи'),
101
+ ('html_description_ua', 'HTML_опис_групи_укр'),
102
+ ('description_before', 'Опис_групи_до_списку_товарних_позицій'),
103
+ (
104
+ 'description_before_ua',
105
+ 'Опис_групи_до_списку_товарних_позицій_укр',
106
+ ),
107
+ ('description_after', 'Опис_групи_після_списку_товарних_позицій'),
108
+ (
109
+ 'description_after_ua',
110
+ 'Опис_групи_після_списку_товарних_позицій_укр',
111
+ ),
112
+ )
113
+ required_group_fields = ('number', 'name', 'id')
114
+
115
+ @property
116
+ def host(self):
117
+ return f'https://{settings.DOMAIN}'
118
+
119
+ def get_queryset(self):
120
+ raise NotImplementedError
121
+
122
+ def get_groups(self):
123
+ return ()
124
+
125
+ def get_id(self, product):
126
+ raise NotImplementedError
127
+
128
+ def get_name(self, product):
129
+ raise NotImplementedError
130
+
131
+ def get_description(self, product):
132
+ raise NotImplementedError
133
+
134
+ def get_price(self, product):
135
+ raise NotImplementedError
136
+
137
+ def get_currency(self, product):
138
+ raise NotImplementedError
139
+
140
+ def get_availability(self, product):
141
+ raise NotImplementedError
142
+
143
+ def get_code(self, product):
144
+ return None
145
+
146
+ def get_name_ua(self, product):
147
+ return None
148
+
149
+ def get_keywords(self, product):
150
+ return None
151
+
152
+ def get_keywords_ua(self, product):
153
+ return None
154
+
155
+ def get_description_ua(self, product):
156
+ return None
157
+
158
+ def get_product_type(self, product):
159
+ return None
160
+
161
+ def get_price_from(self, product):
162
+ return None
163
+
164
+ def get_measure_unit(self, product):
165
+ return None
166
+
167
+ def get_minimum_order(self, product):
168
+ return None
169
+
170
+ def get_wholesale_price(self, product):
171
+ return None
172
+
173
+ def get_minimum_wholesale_order(self, product):
174
+ return None
175
+
176
+ def get_quantity(self, product):
177
+ return None
178
+
179
+ def get_images(self, product):
180
+ return None
181
+
182
+ def get_weight(self, product):
183
+ return None
184
+
185
+ def get_width(self, product):
186
+ return None
187
+
188
+ def get_height(self, product):
189
+ return None
190
+
191
+ def get_length(self, product):
192
+ return None
193
+
194
+ def get_discount(self, product):
195
+ return None
196
+
197
+ def get_discount_date_start(self, product):
198
+ return None
199
+
200
+ def get_discount_date_end(self, product):
201
+ return None
202
+
203
+ def get_label(self, product):
204
+ return None
205
+
206
+ def get_vendor(self, product):
207
+ return None
208
+
209
+ def get_country(self, product):
210
+ return None
211
+
212
+ def get_html_title(self, product):
213
+ return None
214
+
215
+ def get_html_title_ua(self, product):
216
+ return None
217
+
218
+ def get_html_description(self, product):
219
+ return None
220
+
221
+ def get_html_description_ua(self, product):
222
+ return None
223
+
224
+ def get_gifts(self, product):
225
+ return None
226
+
227
+ def get_related(self, product):
228
+ return None
229
+
230
+ def get_gift_ids(self, product):
231
+ return None
232
+
233
+ def get_related_ids(self, product):
234
+ return None
235
+
236
+ def get_group_number(self, product):
237
+ return None
238
+
239
+ def get_group_name(self, product):
240
+ return None
241
+
242
+ def get_catalog_url(self, product):
243
+ return None
244
+
245
+ def get_packaging(self, product):
246
+ return None
247
+
248
+ def get_notes(self, product):
249
+ return None
250
+
251
+ def get_product_url(self, product):
252
+ return None
253
+
254
+ def get_gtin(self, product):
255
+ return None
256
+
257
+ def get_mpn(self, product):
258
+ return None
259
+
260
+ def get_unique_id(self, product):
261
+ return None
262
+
263
+ def get_catalog_id(self, product):
264
+ return None
265
+
266
+ def get_group_id(self, product):
267
+ return None
268
+
269
+ def get_variation_group_id(self, product):
270
+ return None
271
+
272
+ def get_location(self, product):
273
+ return None
274
+
275
+ def get_groups_number(self, group):
276
+ raise NotImplementedError
277
+
278
+ def get_groups_name(self, group):
279
+ raise NotImplementedError
280
+
281
+ def get_groups_id(self, group):
282
+ raise NotImplementedError
283
+
284
+ def get_groups_name_ua(self, group):
285
+ return None
286
+
287
+ def get_groups_parent_number(self, group):
288
+ return None
289
+
290
+ def get_groups_parent_id(self, group):
291
+ return None
292
+
293
+ def get_groups_image(self, group):
294
+ return None
295
+
296
+ def get_groups_html_title(self, group):
297
+ return None
298
+
299
+ def get_groups_html_title_ua(self, group):
300
+ return None
301
+
302
+ def get_groups_html_description(self, group):
303
+ return None
304
+
305
+ def get_groups_html_description_ua(self, group):
306
+ return None
307
+
308
+ def get_groups_description_before(self, group):
309
+ return None
310
+
311
+ def get_groups_description_before_ua(self, group):
312
+ return None
313
+
314
+ def get_groups_description_after(self, group):
315
+ return None
316
+
317
+ def get_groups_description_after_ua(self, group):
318
+ return None
319
+
320
+ def get_feed_path(self):
321
+ return Path(settings.BASE_DIR) / 'tmp' / self.file_name
322
+
323
+ def get(self, request, *args, **kwargs):
324
+ path = self.get_feed_path()
325
+ if self._is_stale(path):
326
+ self.write_feed(path)
327
+
328
+ try:
329
+ data = path.read_bytes()
330
+ except OSError:
331
+ return HttpResponseNotFound()
332
+
333
+ response = HttpResponse(data, content_type=self.content_type)
334
+ response['Content-Disposition'] = (
335
+ f'attachment; filename="{self.file_name}"'
336
+ )
337
+ return response
338
+
339
+ def _is_stale(self, path):
340
+ try:
341
+ mtime = path.stat().st_mtime
342
+ except OSError:
343
+ return True
344
+ return time.time() - mtime > self.max_age
345
+
346
+ def write_feed(self, path):
347
+ path.parent.mkdir(parents=True, exist_ok=True)
348
+ path.write_bytes(self.render_feed())
349
+
350
+ def render_feed(self):
351
+ book = xlwt.Workbook(encoding='utf-8')
352
+ self._write_sheet(
353
+ book.add_sheet(PRODUCTS_SHEET),
354
+ self._columns(
355
+ self.product_fields,
356
+ self.required_product_fields,
357
+ 'get_',
358
+ ),
359
+ self.get_queryset(),
360
+ )
361
+ self._write_sheet(
362
+ book.add_sheet(GROUPS_SHEET),
363
+ self._columns(
364
+ self.group_fields,
365
+ self.required_group_fields,
366
+ 'get_groups_',
367
+ ),
368
+ self.get_groups(),
369
+ )
370
+ stream = BytesIO()
371
+ book.save(stream)
372
+ return stream.getvalue()
373
+
374
+ def _columns(self, fields, required, prefix):
375
+ columns = []
376
+ for key, header in fields:
377
+ method = prefix + key
378
+ if key in required or self._overridden(method):
379
+ columns.append((method, header))
380
+ return columns
381
+
382
+ def _overridden(self, method):
383
+ return getattr(type(self), method) is not getattr(
384
+ ExportStrategy, method
385
+ )
386
+
387
+ def _write_sheet(self, sheet, columns, rows):
388
+ for index, (_method, header) in enumerate(columns):
389
+ sheet.write(0, index, header)
390
+
391
+ for row_index, item in enumerate(rows, start=1):
392
+ for index, (method, _header) in enumerate(columns):
393
+ sheet.write(
394
+ row_index,
395
+ index,
396
+ self._cell(getattr(self, method)(item)),
397
+ )
398
+
399
+ def _cell(self, value):
400
+ if isinstance(value, (list, tuple)):
401
+ parts = [
402
+ str(item)
403
+ for item in value
404
+ if item is not None and item != ''
405
+ ]
406
+ value = self.list_separator.join(parts)
407
+ if value is None:
408
+ return ''
409
+ return value
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,19 @@
1
+ from setuptools import setup, find_packages
2
+
3
+
4
+ version = '0.0.1'
5
+ url = 'https://github.com/pmaigutyak/mp-prom'
6
+
7
+
8
+ setup(
9
+ name='django-mp-prom',
10
+ version=version,
11
+ description='Django Prom.ua export',
12
+ author='Paul Maigutyak',
13
+ author_email='pmaigutyak@gmail.com',
14
+ url=url,
15
+ download_url='{}/archive/{}.tar.gz'.format(url, version),
16
+ packages=find_packages(),
17
+ include_package_data=True,
18
+ license='MIT',
19
+ )