Compare commits
9 Commits
v0.1.0
...
rework-202
| Author | SHA1 | Date | |
|---|---|---|---|
| be4839ecc2 | |||
| b2b38783cb | |||
| f0502e605f | |||
| 75d359aeb3 | |||
| 5b6c8a8ded | |||
| 5cb05a353f | |||
| 6bf79c460b | |||
| 447d2ab9dd | |||
| 63c52eaeaa |
35
.env.example
Normal file
35
.env.example
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Локальная конфигурация для dev-окружения lpon-site.
|
||||||
|
DJANGO_DEBUG=True
|
||||||
|
DJANGO_SECRET_KEY=insecure-dev-key-change-me-in-prod
|
||||||
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,testserver,lpon.ru
|
||||||
|
DJANGO_ADMINS=YouName:you@email.com
|
||||||
|
|
||||||
|
# Доверенные источники для CSRF (важно для Docker/Nginx)
|
||||||
|
# Укажите здесь URL, по которому вы заходите на сайт (с протоколом и портом gunicorn)
|
||||||
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://127.0.0.1:8000,http://localhost:8000,http://testserver,https://lpon.ru
|
||||||
|
|
||||||
|
# URL для доступа к админке Django (можно сменить для безопасности, чтобы боты не могли её найти)
|
||||||
|
DJANGO_ADMIN_URL=admin/
|
||||||
|
|
||||||
|
# База данных (SQLite)
|
||||||
|
DJANGO_SQLITE_NAME=lpon-db.sqlite3
|
||||||
|
|
||||||
|
# Почта SMTP
|
||||||
|
DJANGO_EMAIL_HOST=smtp.mail.ru
|
||||||
|
DJANGO_EMAIL_PORT=2525
|
||||||
|
DJANGO_EMAIL_HOST_USER=you@email.com
|
||||||
|
DJANGO_EMAIL_HOST_PASSWORD=CHANGE_ME
|
||||||
|
DJANGO_EMAIL_FROM=you@email.com
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Системные пути на хосте (ТОЛЬКО ДЛЯ ПРОДАКШЕНА)
|
||||||
|
# Используется скриптом для генерации корректного Nginx конфига и alias к media-файлам.
|
||||||
|
# На локальной машине разработчика (Dev) эта переменная игнорируется.
|
||||||
|
# В ПРОДАКШЕНЕ: Укажите полный путь к папке проекта на сервере
|
||||||
|
HOST_PROJECT_PATH=/home/user/docker-app/your-site
|
||||||
|
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# Настройки достпа к пакетам в репозитории, чтобы watchtower мог проверять их свежесть и скачивать
|
||||||
|
REPO_USER=xxxxx
|
||||||
|
REPO_PASS=xxxxx
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -363,7 +363,8 @@ db.sqlite3-wal
|
|||||||
public/media/*
|
public/media/*
|
||||||
!public/media/README.md
|
!public/media/README.md
|
||||||
staticfiles/
|
staticfiles/
|
||||||
public/static/static_collected/
|
public/static_collected/
|
||||||
|
.github/
|
||||||
|
|
||||||
# Data Backup
|
# Data Backup
|
||||||
database/data.json
|
database/
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ server {
|
|||||||
|
|
||||||
# Все домены, которые будет обслуживать этот сервер.
|
# Все домены, которые будет обслуживать этот сервер.
|
||||||
# Certbot выпустит сертификаты для всех перечисленных доменов.
|
# Certbot выпустит сертификаты для всех перечисленных доменов.
|
||||||
server_name lpon.ru t.lpon.ru;
|
server_name lpon.ru;
|
||||||
|
|
||||||
# --- ЛОГИ ---
|
# --- ЛОГИ ---
|
||||||
access_log /var/log/nginx/lpon.access.log;
|
access_log /var/log/nginx/lpon.access.log;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
# Пробрасываем свободный порт 8020 на хосте на стандартный порт 80 внутри контейнера
|
# Пробрасываем свободный порт 8020 на хосте на стандартный порт 80 внутри контейнера
|
||||||
- "8020:80"
|
- "127.0.0.1:8020:80"
|
||||||
# Настройка ротации логов
|
# Настройка ротации логов
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
|
|||||||
0
lpon_site/frontend/__init__.py
Normal file
0
lpon_site/frontend/__init__.py
Normal file
3
lpon_site/frontend/admin.py
Normal file
3
lpon_site/frontend/admin.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
5
lpon_site/frontend/apps.py
Normal file
5
lpon_site/frontend/apps.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class FrontendConfig(AppConfig):
|
||||||
|
name = 'frontend'
|
||||||
0
lpon_site/frontend/migrations/__init__.py
Normal file
0
lpon_site/frontend/migrations/__init__.py
Normal file
380
lpon_site/frontend/models.py
Normal file
380
lpon_site/frontend/models.py
Normal file
@@ -0,0 +1,380 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
class TbArtist(models.Model):
|
||||||
|
s_artist = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
db_index=True,
|
||||||
|
verbose_name='Исполнитель',
|
||||||
|
help_text="Исполнитель или группа.",
|
||||||
|
)
|
||||||
|
s_artist_html = models.TextField(
|
||||||
|
blank=True, null=True, default='',
|
||||||
|
verbose_name='Исполнитель (типографированно в HTML)',
|
||||||
|
help_text='Исполнитель или группа, указанные на обложке релиза, с сохранением типографирования и спецсимволов в виде HTML-тегов.',
|
||||||
|
)
|
||||||
|
s_slug = models.SlugField(
|
||||||
|
max_length=64,
|
||||||
|
)
|
||||||
|
j_artist_in_source = models.JSONField(
|
||||||
|
default=list, blank=True, null=True,
|
||||||
|
verbose_name='Исполнитель в источнике',
|
||||||
|
help_text='Список вариантов написания исполнителя, встречающихся в источниках (в разных релизах/каталогах'
|
||||||
|
' или у разных поставщиков). Например: <tt>["The Beatles", "Beatles", "Beatles, The"]</tt>.',
|
||||||
|
)
|
||||||
|
t_created = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Дата Создания записи в БД",
|
||||||
|
)
|
||||||
|
t_updated = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="Дата последнего обновления записи в БД",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return f"product {self.id:0>4}: {self.s_artist}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__unicode__()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Исполнитель'
|
||||||
|
verbose_name_plural = 'Исполнители'
|
||||||
|
ordering = ('s_artist',)
|
||||||
|
|
||||||
|
|
||||||
|
class TbProduct(models.Model):
|
||||||
|
# Абстрактный релиз / сущность / товар, который может быть представлен в виде одного или нескольких предложений
|
||||||
|
# от разных продавцов.
|
||||||
|
k_artists = models.ManyToManyField(
|
||||||
|
TbArtist,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
related_name='+', # ← '+' отключает обратную связь
|
||||||
|
verbose_name='Исполнители',
|
||||||
|
help_text="Выберите исполнителей или группы для этого релиза (можно не указывать)",
|
||||||
|
)
|
||||||
|
s_title = models.CharField(
|
||||||
|
max_length=255, blank=False, null=False, default='',
|
||||||
|
# db_index=True,
|
||||||
|
verbose_name='Название товара (релиза)',
|
||||||
|
help_text='Название товара (релиза), как указано на обложке. Например: <tt>Abbey Road (remastered)</tt>'
|
||||||
|
' или <tt>TDK, CDing I, 90</tt>.',
|
||||||
|
)
|
||||||
|
s_title_html = models.TextField(
|
||||||
|
blank=True, null=True, default='',
|
||||||
|
verbose_name='Название товара (типографированно в HTML)',
|
||||||
|
help_text='Название товара (релиза), как указано на обложке, с сохранением типографирования и спецсимволов'
|
||||||
|
' в виде HTML-тегов.',
|
||||||
|
)
|
||||||
|
s_title_slug = models.SlugField(
|
||||||
|
blank=True, null=True, default='',
|
||||||
|
verbose_name='Слаг',
|
||||||
|
help_text='Слаг для товара, формируемый на основе названия товара (релиза)',
|
||||||
|
)
|
||||||
|
t_title_date = models.DateField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name='Дата релиза',
|
||||||
|
help_text='Дата релиза, если известна. Если известен только год, можно указать 1 января этого года.'
|
||||||
|
' Например: <tt>1969-09-26</tt> или не указывать вовсе.',
|
||||||
|
)
|
||||||
|
i_discogs_master_id = models.IntegerField(
|
||||||
|
blank=True, null=True, default=None,
|
||||||
|
verbose_name='ID на мастер-релиз Discogs',
|
||||||
|
help_text='Уникальный идентификатор мастер-релиза на Discogs, если он там есть. Например: <tt>306323</tt>',
|
||||||
|
)
|
||||||
|
j_product_metadata = models.JSONField(
|
||||||
|
default=dict, blank=True, null=True,
|
||||||
|
verbose_name='Дополнительные данные',
|
||||||
|
help_text='Дополнительные данные о релизе в виде JSON-словаря,',
|
||||||
|
)
|
||||||
|
t_created = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Дата Создания записи в БД",
|
||||||
|
)
|
||||||
|
t_updated = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="Дата последнего обновления записи в БД",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return f"product {self.id:0>4}: {self.s_title}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__unicode__()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Релиз (товар)'
|
||||||
|
verbose_name_plural = 'Релизы (товары)'
|
||||||
|
ordering = ('s_title',)
|
||||||
|
|
||||||
|
|
||||||
|
class TbSeller(models.Model):
|
||||||
|
# Продавец товара
|
||||||
|
class SellerType(models.TextChoices):
|
||||||
|
SELLER = 'seller', 'Продавец'
|
||||||
|
LABEL = 'label', 'Лейбл (издатель)'
|
||||||
|
DIY = 'diy', 'Самиздат группы'
|
||||||
|
CROWD = 'crowdfunding', 'Краудфандинг'
|
||||||
|
OTHER = '++', 'Другое'
|
||||||
|
|
||||||
|
s_seller = models.CharField(
|
||||||
|
max_length=32, blank=False, null=False, default='',
|
||||||
|
verbose_name='Название продавца',
|
||||||
|
help_text='Название продавца или магазина, например: <tt>Клюква Рекодс</tt>',
|
||||||
|
)
|
||||||
|
s_seller_slug = models.SlugField(
|
||||||
|
max_length=32, blank=False, null=False, default='',
|
||||||
|
verbose_name='Слаг',
|
||||||
|
help_text='Слаг для продавца, формируемый на основе названия продавца',
|
||||||
|
)
|
||||||
|
l_seller_type = models.CharField(
|
||||||
|
max_length=9, default=SellerType.SELLER, choices=SellerType.choices,
|
||||||
|
verbose_name='Тип продавца',
|
||||||
|
)
|
||||||
|
j_seller_metadata = models.JSONField(
|
||||||
|
default=dict, blank=True, null=True,
|
||||||
|
verbose_name='Дополнительные данные',
|
||||||
|
help_text='Дополнительные данные о продавце в виде JSON-словаря',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return f"vendor {self.id:0>3}: {self.s_vendor}"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__unicode__()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = 'Продавец'
|
||||||
|
verbose_name_plural = 'Продавцы'
|
||||||
|
ordering = ('s_seller',)
|
||||||
|
|
||||||
|
class TbLabel(models.Model):
|
||||||
|
# Лейблы: производители (Улитка Рекородс, Мелодия, Sony... а так же производители TDK, AXIA, Maxwell и т.д.)
|
||||||
|
s_label = models.CharField(
|
||||||
|
max_length=32, blank=False, null=False, default='',
|
||||||
|
verbose_name='Название лейбла',
|
||||||
|
help_text='Название лейбла, например: <tt>Мелодия</tt> или <tt>TDK</tt>',
|
||||||
|
)
|
||||||
|
s_label_slug = models.SlugField(
|
||||||
|
max_length=32, blank=False, null=False, default='',
|
||||||
|
verbose_name='Слаг',
|
||||||
|
help_text='Слаг для лейбла, формируемый на основе названия лейбла',
|
||||||
|
)
|
||||||
|
j_label_metadata = models.JSONField(
|
||||||
|
default=dict, blank=True, null=True,
|
||||||
|
verbose_name='Дополнительные данные',
|
||||||
|
help_text='Дополнительные данные о лейбле в виде JSON-словаря',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
class TbOffer(models.Model):
|
||||||
|
# Конкретное предложение продавца
|
||||||
|
class Format(models.TextChoices):
|
||||||
|
LP = 'lp', 'vinyl'
|
||||||
|
CD = 'cd', 'CD'
|
||||||
|
BD = 'bd', 'Blu-ray'
|
||||||
|
CS = 'cs', 'Cassette'
|
||||||
|
MD = 'md', 'Minidisc'
|
||||||
|
BX = 'bx', 'Box Set'
|
||||||
|
HI = 'hi', 'Hi-Fi'
|
||||||
|
AC = 'ac', 'Accessory'
|
||||||
|
OTHER = '++', 'Other'
|
||||||
|
|
||||||
|
class Condition(models.TextChoices):
|
||||||
|
S = 's', 'Still Sealed (новое, запечатано)'
|
||||||
|
M = 'm', 'Mint'
|
||||||
|
NM = 'nm', 'Near Mint'
|
||||||
|
VG = 'vg', 'Very Good'
|
||||||
|
G = 'g', 'Good'
|
||||||
|
F = 'f', 'Fair'
|
||||||
|
P = 'p', 'Poor'
|
||||||
|
OTHER = '++', 'Other'
|
||||||
|
|
||||||
|
class Currency(models.TextChoices):
|
||||||
|
RUB = 'rub', 'RUB: российский рубль'
|
||||||
|
TRY = 'try', 'TRY: турецкая лира'
|
||||||
|
AMD = 'amd', 'AMD: армянский драм'
|
||||||
|
USD = 'usd', 'USD: американский доллар'
|
||||||
|
EUR = 'eur', 'EUR: евро'
|
||||||
|
JPY = 'jpy', 'JPY: японская иена'
|
||||||
|
GBP = 'gbp', 'GBP: британский фунт'
|
||||||
|
CNY = 'cny', 'CNY: китайский юань'
|
||||||
|
BYN = 'byn', 'BYN: белорусский рубль'
|
||||||
|
TON = 'ton', 'TON: криптовалюта TON'
|
||||||
|
OTHER = '++', 'Other'
|
||||||
|
|
||||||
|
s_catalog_num = models.TextField(
|
||||||
|
blank=True, default='',
|
||||||
|
verbose_name='Каталожный номер или barcode',
|
||||||
|
help_text='Каталожный номер релиза, если он есть. Например: <tt>SD 16023</tt> для <i>ABBA — Super Trouper'
|
||||||
|
' — 1980 — Atlantic (USA)</i>',
|
||||||
|
)
|
||||||
|
k_product = models.ForeignKey(
|
||||||
|
TbProduct,
|
||||||
|
blank=True, default=None,
|
||||||
|
on_delete=models.SET_NULL, related_name='+',
|
||||||
|
verbose_name='Релиз (товар)',
|
||||||
|
)
|
||||||
|
k_label = models.ForeignKey(
|
||||||
|
TbLabel,
|
||||||
|
null=True, default=None,
|
||||||
|
on_delete=models.SET_NULL, related_name='+',
|
||||||
|
verbose_name='Лейбл',
|
||||||
|
help_text='Лейбл, на котором был выпущен релиз, если он известен. Например: <tt>Atlantic</tt> или <tt>Мелодия</tt>',
|
||||||
|
)
|
||||||
|
k_seller = models.ForeignKey(
|
||||||
|
TbSeller,
|
||||||
|
null=True, default=None,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name='+',
|
||||||
|
verbose_name='Продавец',
|
||||||
|
)
|
||||||
|
l_primary_media = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=Format.choices,
|
||||||
|
default=Format.LP,
|
||||||
|
verbose_name='Формат',
|
||||||
|
help_text='Основной формат носителя (пластинка, CD, кассета и т.п.)'
|
||||||
|
)
|
||||||
|
i_discogs_id = models.IntegerField(
|
||||||
|
blank=True,default=0,
|
||||||
|
verbose_name='ID на релиз Discogs',
|
||||||
|
help_text='Уникальный идентификатор релиза на Discogs, если он там есть. Например: <tt>306323</tt>',
|
||||||
|
)
|
||||||
|
l_condition_media = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=Condition.choices,
|
||||||
|
default=Condition.S,
|
||||||
|
verbose_name="Состояние носителя",
|
||||||
|
help_text='Состояние носителя (пластинки, CD и т.п.) по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).',
|
||||||
|
)
|
||||||
|
l_condition_sleeve = models.CharField(
|
||||||
|
max_length=2,
|
||||||
|
choices=Condition.choices,
|
||||||
|
default=Condition.S,
|
||||||
|
verbose_name="Состояние обложки",
|
||||||
|
help_text='Состояние обложки по шкале от "Still Sealed" (запечатано) до "Poor" (плохое).',
|
||||||
|
)
|
||||||
|
f_price = models.DecimalField(
|
||||||
|
max_digits=10,
|
||||||
|
decimal_places=2,
|
||||||
|
blank=True, default=0.00,
|
||||||
|
verbose_name='Цена',
|
||||||
|
)
|
||||||
|
l_currency = models.CharField(
|
||||||
|
max_length=3,
|
||||||
|
choices=Currency.choices,
|
||||||
|
default=Currency.RUB,
|
||||||
|
verbose_name="Валюта",
|
||||||
|
)
|
||||||
|
i_quantity = models.IntegerField(
|
||||||
|
blank=True, default=0,
|
||||||
|
verbose_name='Количество',
|
||||||
|
)
|
||||||
|
b_is_available = models.BooleanField(
|
||||||
|
default=True,
|
||||||
|
verbose_name='В наличии',
|
||||||
|
)
|
||||||
|
i_discount_to_daily_sale = models.IntegerField(
|
||||||
|
blank=True, default=0,
|
||||||
|
verbose_name='Скидка',
|
||||||
|
help_text='Процент скидки, для ежедневной распродажи',
|
||||||
|
)
|
||||||
|
s_offer_comment = models.TextField(
|
||||||
|
blank=True, default='',
|
||||||
|
verbose_name='Доп.инфо',
|
||||||
|
help_text='Дополнительная информация или комментарий к предложению от продавца, например:'
|
||||||
|
' <tt>"Пластинка запаяна в целлофан, угол обложки замят."</tt>.',
|
||||||
|
)
|
||||||
|
j_offer_metadata = models.JSONField(
|
||||||
|
default=dict, null=True,
|
||||||
|
verbose_name='Дополнительные данные',
|
||||||
|
help_text='Дополнительные данные о предложении в виде JSON-словаря',
|
||||||
|
)
|
||||||
|
t_created = models.DateTimeField(
|
||||||
|
auto_now_add=True,
|
||||||
|
verbose_name="Дата Создания записи в БД",
|
||||||
|
)
|
||||||
|
t_updated = models.DateTimeField(
|
||||||
|
auto_now=True,
|
||||||
|
verbose_name="Дата последнего обновления записи в БД",
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# │ 1
|
||||||
|
# │
|
||||||
|
# │ N
|
||||||
|
# ┌───────▼────────────┐
|
||||||
|
# │ ProductImage │
|
||||||
|
# ├────────────────────┤
|
||||||
|
# │ id │
|
||||||
|
# │ product_id │ FK
|
||||||
|
# │ image_type │ ← cover/back/obi/matrix/etc
|
||||||
|
# │ source │ ← discogs/manual/vendor
|
||||||
|
# │ url │
|
||||||
|
# │ local_path │
|
||||||
|
# │ is_primary │
|
||||||
|
# │ sort_order │
|
||||||
|
# │ metadata_json │
|
||||||
|
# └────────────────────┘
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# ┌────────────────────┐
|
||||||
|
# │ ProductEnrichment │
|
||||||
|
# ├────────────────────┤
|
||||||
|
# │ id │
|
||||||
|
# │ product_id │ FK
|
||||||
|
# │ source │ ← discogs/api/scraper
|
||||||
|
# │ confidence_score │
|
||||||
|
# │ data_json │
|
||||||
|
# │ fetched_at │
|
||||||
|
# └────────────────────┘
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# ┌────────────────────┐
|
||||||
|
# │ PriceHistory │
|
||||||
|
# ├────────────────────┤
|
||||||
|
# │ id │
|
||||||
|
# │ offer_id │ FK
|
||||||
|
# │ old_price │
|
||||||
|
# │ new_price │
|
||||||
|
# │ quantity_snapshot │
|
||||||
|
# │ source │
|
||||||
|
# │ changed_at │
|
||||||
|
# └────────────────────┘
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# ┌────────────────────┐
|
||||||
|
# │ Source │ ← Импорт / источник
|
||||||
|
# ├────────────────────┤
|
||||||
|
# │ id │
|
||||||
|
# │ vendor_id │ FK
|
||||||
|
# │ type │ ← excel/csv/api/html
|
||||||
|
# │ source_url │
|
||||||
|
# │ file_name │
|
||||||
|
# │ file_hash │
|
||||||
|
# │ imported_at │
|
||||||
|
# │ parser_version │
|
||||||
|
# │ meta_json │
|
||||||
|
# └─────────┬──────────┘
|
||||||
|
# │ 1
|
||||||
|
# │
|
||||||
|
# │ N
|
||||||
|
# ┌─────────▼──────────┐
|
||||||
|
# │ SourceItem │ ← Строка из Excel/CSV
|
||||||
|
# ├────────────────────┤
|
||||||
|
# │ id │
|
||||||
|
# │ source_id │ FK
|
||||||
|
# │ row_index │
|
||||||
|
# │ raw_row_json │
|
||||||
|
# │ parsed_data_json │
|
||||||
|
# │ matching_status │
|
||||||
|
# │ confidence_score │
|
||||||
|
# │ matched_product_id │ FK nullable
|
||||||
|
# │ created_offer_id │ FK nullable
|
||||||
|
# │ created_at │
|
||||||
|
# └────────────────────┘
|
||||||
3
lpon_site/frontend/tests.py
Normal file
3
lpon_site/frontend/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
3
lpon_site/frontend/views.py
Normal file
3
lpon_site/frontend/views.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
0
lpon_site/lpon_site/__init__.py
Normal file
0
lpon_site/lpon_site/__init__.py
Normal file
16
lpon_site/lpon_site/asgi.py
Normal file
16
lpon_site/lpon_site/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for lpon_site project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lpon_site.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
149
lpon_site/lpon_site/settings.py
Normal file
149
lpon_site/lpon_site/settings.py
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
"""
|
||||||
|
Django settings for lpon_site project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 6.0.5.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import environ
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
# Читаем переменные окружения
|
||||||
|
env = environ.Env()
|
||||||
|
environ.Env.read_env(os.path.join(BASE_DIR.parent, '.env'))
|
||||||
|
|
||||||
|
def _normalize_admin_url(value: str) -> str:
|
||||||
|
"""Приводит URL админки к виду `segment/` без ведущего слэша."""
|
||||||
|
normalized = value.strip().lstrip('/')
|
||||||
|
if not normalized:
|
||||||
|
return 'admin/'
|
||||||
|
if not normalized.endswith('/'):
|
||||||
|
normalized += '/'
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = env('DJANGO_SECRET_KEY', default='3xym$l+!)erah-k23lf0=t=c_4$e0nr*zls&l%pbz@k6v6qn89')
|
||||||
|
ADMIN_URL = _normalize_admin_url(env('DJANGO_ADMIN_URL', default='admin/'))
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = env.bool('DJANGO_DEBUG', default=False)
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = env.list(
|
||||||
|
'DJANGO_ALLOWED_HOSTS',
|
||||||
|
default=['127.0.0.1', 'localhost', 'testserver', 'lpon.ru'],
|
||||||
|
)
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = env.list('DJANGO_CSRF_TRUSTED_ORIGINS', default=['127.0.0.1', 'localhost', 'testserver'])
|
||||||
|
|
||||||
|
#########################################
|
||||||
|
# Настройки сообщений об ошибках когда все упало и т.п.
|
||||||
|
ADMINS = tuple(
|
||||||
|
tuple(item.split(':', 1))
|
||||||
|
for item in env.list('DJANGO_ADMINS', default=['S.Erjemin:erjemin@gmail.com'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'lpon_site.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'lpon_site.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR.parent.joinpath('database', env('DJANGO_SQLITE_NAME', default='lpon-db.sqlite3')),
|
||||||
|
'OPTIONS': {
|
||||||
|
'timeout': 25,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||||
|
LANGUAGE_CODE = 'ru-RU' #
|
||||||
|
# TIME_ZONE = 'Etc/GMT+3' #
|
||||||
|
TIME_ZONE = 'Europe/Moscow' #
|
||||||
|
USE_I18N = True
|
||||||
|
USE_TZ = True
|
||||||
|
FIRST_DAY_OF_WEEK = 1 # неделя начинается с понедельника
|
||||||
|
DEFAULT_CHARSET = 'utf-8'
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
||||||
|
STATIC_URL = '/static/'
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
# Локальные каталоги проекта: медиа и статика лежат рядом в `public`.
|
||||||
|
PUBLIC_DIR = BASE_DIR.parent.joinpath('public') # Папка `public` находится в корне пректа
|
||||||
|
MEDIA_ROOT = PUBLIC_DIR.joinpath('media')
|
||||||
|
STATICFILES_DIRS = [PUBLIC_DIR.joinpath('static')]
|
||||||
|
STATIC_ROOT = PUBLIC_DIR.joinpath('staticfiles')
|
||||||
23
lpon_site/lpon_site/urls.py
Normal file
23
lpon_site/lpon_site/urls.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""
|
||||||
|
URL configuration for lpon_site project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/6.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
from lpon_site import settings
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(settings.ADMIN_URL, admin.site.urls),
|
||||||
|
]
|
||||||
16
lpon_site/lpon_site/wsgi.py
Normal file
16
lpon_site/lpon_site/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for lpon_site project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lpon_site.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
22
lpon_site/manage.py
Executable file
22
lpon_site/manage.py
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'lpon_site.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
110
poetry.lock
generated
Normal file
110
poetry.lock
generated
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
# This file is automatically @generated by Poetry 1.8.0 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asgiref"
|
||||||
|
version = "3.11.1"
|
||||||
|
description = "ASGI specs, helper code, and adapters"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"},
|
||||||
|
{file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django"
|
||||||
|
version = "6.0.5"
|
||||||
|
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.12"
|
||||||
|
files = [
|
||||||
|
{file = "django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0"},
|
||||||
|
{file = "django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
asgiref = ">=3.9.1"
|
||||||
|
sqlparse = ">=0.5.0"
|
||||||
|
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
argon2 = ["argon2-cffi (>=23.1.0)"]
|
||||||
|
bcrypt = ["bcrypt (>=4.1.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-environ"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application."
|
||||||
|
optional = false
|
||||||
|
python-versions = "<4,>=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "django_environ-0.13.0-py3-none-any.whl", hash = "sha256:37799d14cd78222c6fd8298e48bfe17965ff8e586091ad66a463e52e0e7b799e"},
|
||||||
|
{file = "django_environ-0.13.0.tar.gz", hash = "sha256:6c401e4c219442c2c4588c2116d5292b5484a6f69163ed09cd41f3943bfb645f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
develop = ["coverage[toml] (>=5.0a4)", "furo (>=2024.8.6)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||||
|
docs = ["furo (>=2024.8.6)", "sphinx (>=5.0)", "sphinx-copybutton", "sphinx-notfound-page"]
|
||||||
|
testing = ["coverage[toml] (>=5.0a4)", "pytest (>=4.6.11)", "setuptools (>=71.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "django-extensions"
|
||||||
|
version = "3.2.3"
|
||||||
|
description = "Extensions for Django"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a"},
|
||||||
|
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
Django = ">=3.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dotenv"
|
||||||
|
version = "1.2.2"
|
||||||
|
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
files = [
|
||||||
|
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
|
||||||
|
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cli = ["click (>=5.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.5"
|
||||||
|
description = "A non-validating SQL parser."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba"},
|
||||||
|
{file = "sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["build"]
|
||||||
|
doc = ["sphinx"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.2"
|
||||||
|
description = "Provider of IANA time zone data"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2"
|
||||||
|
files = [
|
||||||
|
{file = "tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7"},
|
||||||
|
{file = "tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.12"
|
||||||
|
content-hash = "7424447119e980f55d2bf47e9e97063dfbb9ae3230680cade5893701992c3e96"
|
||||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "lpon-site"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "LPON - platform for selling vinyl records (and related products)"
|
||||||
|
authors = ["Sergei Erjemin <erjemin@gmail.com>"]
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [
|
||||||
|
{ include = "lpon_site" }
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.12"
|
||||||
|
django = "^6.0"
|
||||||
|
python-dotenv = "^1.0.0"
|
||||||
|
django-environ = "^0.13.0"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
django-extensions = "^3.2"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
Reference in New Issue
Block a user