From 3a1dbab9e237927811c93c2f3e4be1a8891edf4c Mon Sep 17 00:00:00 2001 From: apheyhys Date: Wed, 23 Apr 2025 22:23:51 +0300 Subject: [PATCH] Init commit --- .gitignore | 179 +++++ headphones/__init__.py | 0 headphones/admin.py | 555 +++++++++++++++ headphones/apps.py | 6 + headphones/migrations/__init__.py | 0 headphones/models.py | 1050 +++++++++++++++++++++++++++++ headphones/tests.py | 3 + headphones/views.py | 3 + headphones_backend/__init__.py | 0 headphones_backend/asgi.py | 16 + headphones_backend/settings.py | 144 ++++ headphones_backend/urls.py | 26 + headphones_backend/wsgi.py | 16 + manage.py | 22 + poetry.lock | 249 +++++++ pyproject.toml | 21 + users/__init__.py | 0 users/admin.py | 3 + users/apps.py | 6 + users/migrations/__init__.py | 0 users/models.py | 9 + users/tests.py | 3 + users/views.py | 3 + 23 files changed, 2314 insertions(+) create mode 100644 .gitignore create mode 100644 headphones/__init__.py create mode 100644 headphones/admin.py create mode 100644 headphones/apps.py create mode 100644 headphones/migrations/__init__.py create mode 100644 headphones/models.py create mode 100644 headphones/tests.py create mode 100644 headphones/views.py create mode 100644 headphones_backend/__init__.py create mode 100644 headphones_backend/asgi.py create mode 100644 headphones_backend/settings.py create mode 100644 headphones_backend/urls.py create mode 100644 headphones_backend/wsgi.py create mode 100755 manage.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 users/__init__.py create mode 100644 users/admin.py create mode 100644 users/apps.py create mode 100644 users/migrations/__init__.py create mode 100644 users/models.py create mode 100644 users/tests.py create mode 100644 users/views.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50ba39a --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +media/ +static/ + +*/migrations/*.py +!*/migrations/__init__.py \ No newline at end of file diff --git a/headphones/__init__.py b/headphones/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/headphones/admin.py b/headphones/admin.py new file mode 100644 index 0000000..03a94c9 --- /dev/null +++ b/headphones/admin.py @@ -0,0 +1,555 @@ +from django.contrib import admin +from django import forms +from django.urls import reverse +from django.utils.safestring import mark_safe +from django.utils.html import format_html + +from .models import * + + +class HeadphonesAdminForm(forms.ModelForm): + class Meta: + model = Headphones + fields = '__all__' + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance and self.instance.pk: + # Скрываем/показываем поля в зависимости от типа подключения + if self.instance.connection_type == 'wired': + self.fields.pop('wireless_details', None) + self.fields.pop('hybrid_details', None) + elif self.instance.connection_type == 'wireless': + self.fields.pop('wired_details', None) + self.fields.pop('hybrid_details', None) + elif self.instance.connection_type == 'hybrid': + self.fields.pop('wired_details', None) + self.fields.pop('wireless_details', None) + + +class HeadphonesConnectorInline(admin.TabularInline): + model = HeadphonesConnector + extra = 1 + fields = ('connector', 'is_primary', 'notes') + fk_name = 'wired_headphones' + +@admin.register(WiredHeadphones) + +class WiredHeadphonesAdmin(admin.ModelAdmin): + inlines = [HeadphonesConnectorInline] + list_display = ('headphones', 'cable_connection_type', 'cable_length') + list_filter = ('headphones__brand', 'cable_connection_type') + search_fields = ('headphones__model',) + + def get_queryset(self, request): + return super().get_queryset(request).select_related('headphones', 'cable_connection_type') + + +class WiredHeadphonesInline(admin.StackedInline): + model = WiredHeadphones + extra = 0 + fieldsets = [ + (None, { + 'fields': [ + 'cable_connection_type', + 'cable_length', + 'custom_connector_name' + ] + }) + ] + + +class WirelessHeadphonesInline(admin.StackedInline): + model = WirelessHeadphones + extra = 0 + fieldsets = [ + (None, { + 'fields': [ + ('wireless_technology', 'bluetooth_version'), + ('battery_life', 'charging_time'), + 'charging_interface', + ('has_charging_case', 'charging_case_battery_life'), + 'supported_codecs' + ] + }) + ] + + +class HybridHeadphonesInline(admin.StackedInline): + model = HybridHeadphones + extra = 0 + fieldsets = [ + ('Беспроводная часть', { + 'fields': [ + ('wireless_technology', 'bluetooth_version'), + ('battery_life', 'charging_time'), + 'charging_interface', + ('has_charging_case', 'charging_case_battery_life'), + 'supported_codecs' + ] + }), + ('Проводная часть', { + 'fields': [ + ('cable_connection_type', 'connector_to_device'), + 'cable_length' + ] + }), + ('Режимы работы', { + 'fields': [ + 'auto_switch_mode', + 'simultaneous_connection' + ] + }) + ] + + +class HeadphonesReviewInline(admin.StackedInline): + model = HeadphonesReview + extra = 1 + fields = ['resource', 'author', 'language', 'title', 'url', 'date'] + ordering = ['-date'] + autocomplete_fields = ['resource', 'author', 'language'] + + +class HeadphonesImagesInline(admin.TabularInline): + model = HeadphonesImages + extra = 1 + fields = ('photo', 'thumbnail_preview', 'admin_image_info', 'caption', 'is_main') + readonly_fields = ('thumbnail_preview', 'admin_image_info') + + def thumbnail_preview(self, obj): + if obj.photo: + return format_html('', obj.photo.url) + return "-" + + def admin_image_info(self, obj): + return obj.image_info + + admin_image_info.short_description = "Информация о изображении" + admin_image_info.allow_tags = True + + thumbnail_preview.short_description = "Превью" + + +@admin.register(CaseMaterial) +class CaseMaterialAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + list_per_page = 20 + + +class HeadphonesDriverInline(admin.TabularInline): + model = HeadphonesDriver + extra = 1 + fields = ('driver', 'count') + autocomplete_fields = ['driver'] + + +@admin.register(Headphones) +class HeadphonesAdmin(admin.ModelAdmin): + form = HeadphonesAdminForm + list_display = ( + 'model', 'brand', 'connection_type', 'get_reviews_count', 'published') + list_filter = ('brand', 'connection_type', 'published', 'case_type') + search_fields = ('model', 'brand__name', 'description') + prepopulated_fields = {'slug': ('brand', 'model')} + filter_horizontal = ('tags', 'colors') + list_editable = ('published',) + actions = ['make_published', 'make_unpublished'] + readonly_fields = ( + 'get_reviews_count_display', + 'date_added', + 'driver_configuration_display' + ) + autocomplete_fields = ['case_material', 'noise_cancellation'] + + fieldsets = [ + ('Основная информация', { + 'fields': [ + ('brand', 'model'), + 'slug', + 'description', + ('connection_type', 'case_type', 'case_material'), + ('headphone_purpose', 'official_price'), + 'tags', # Добавляем теги в основную секцию + ] + }), + ('Технические характеристики', { + 'fields': [ + ('impedance', 'sensitivity'), + 'frequency_range', + 'frequency_response_chart', + ('microphone', 'noise_cancellation'), + 'ip_rating', + ] + }), + ('Внешний вид', { + 'fields': [ + 'colors', + ('weight', 'release_year') + ] + }), + ('Метаданные', { + 'fields': [ + ('date_added', 'published') + ], + 'classes': ('collapse',) + }), + ] + + inlines = [HeadphonesDriverInline, HeadphonesImagesInline] + + # Методы для отображения + def get_reviews_count(self, obj): + return obj.reviews.count() + + get_reviews_count.short_description = 'Кол-во обзоров' + + def get_reviews_count_display(self, obj): + count = self.get_reviews_count(obj) + url = reverse('admin:headphones_headphonesreview_changelist') + f'?headphones__id__exact={obj.id}' + return format_html('{} обзоров', url, count) + + get_reviews_count_display.short_description = 'Обзоры' + + def driver_configuration_display(self, obj): + drivers = obj.headphonesdriver_set.select_related('driver__driver_type').all() + return format_html("
".join( + f"{d.driver.driver_type.name} {d.driver.size}mm × {d.count}" + for d in drivers + )) if drivers.exists() else "Не указано" + + driver_configuration_display.short_description = 'Конфигурация драйверов' + + def get_inline_instances(self, request, obj=None): + inlines = [ + HeadphonesDriverInline(self.model, self.admin_site), + HeadphonesImagesInline(self.model, self.admin_site) + ] + + if obj: + if obj.connection_type == 'wired': + inlines.append(WiredHeadphonesInline(self.model, self.admin_site)) + elif obj.connection_type == 'wireless': + inlines.append(WirelessHeadphonesInline(self.model, self.admin_site)) + elif obj.connection_type == 'hybrid': + inlines.append(HybridHeadphonesInline(self.model, self.admin_site)) + + inlines.append(HeadphonesReviewInline(self.model, self.admin_site)) + return inlines + + +# Регистрация остальных моделей +@admin.register(Brand) +class BrandAdmin(admin.ModelAdmin): + list_display = ('name', 'country', 'year_founded', 'parent_company') + list_filter = ('country',) + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(Driver) +class DriverAdmin(admin.ModelAdmin): + list_display = ('driver_type', 'size', 'frequency_range') + list_filter = ('driver_type',) + search_fields = ('driver_type__name', 'size', 'frequency_range') # Добавлено + autocomplete_fields = ['driver_type'] # Если нужно автозаполнение для типа + + def get_queryset(self, request): + return super().get_queryset(request).select_related('driver_type') + + +# ====================== +# Регистрация справочников +# ====================== + +@admin.register(BrandCountry) +class BrandCountryAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(CaseColors) +class CaseColorsAdmin(admin.ModelAdmin): + list_display = ('name', 'hex_code') + search_fields = ('name',) + + +@admin.register(CaseType) +class CaseTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(HeadphonePurpose) +class HeadphonePurposeAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(TagCategory) +class TagCategoryAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(Tags) +class TagsAdmin(admin.ModelAdmin): + list_display = ('name', 'category', 'slug') + list_filter = ('category',) + search_fields = ('name', 'category__name') + prepopulated_fields = {'slug': ('name',)} + + +# ====================== +# Регистрация аудио компонентов +# ====================== + +@admin.register(DriverType) +class DriverTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(NoiseCancellationType) +class NoiseCancellationTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(CodecType) +class CodecTypeAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name',) + + +@admin.register(AudioCodec) +class AudioCodecAdmin(admin.ModelAdmin): + list_display = ('name', 'codec_type', 'max_bitrate', 'latency') + list_filter = ('codec_type',) + search_fields = ('name', 'description') + filter_horizontal = () + + +# ====================== +# Регистрация разъемов и подключений +# ====================== + +@admin.register(CableConnectionType) +class CableConnectionTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'is_detachable') + search_fields = ('name', 'description') + readonly_fields = ('image_preview',) + + def image_preview(self, obj): + if obj.image: + return mark_safe(f'') + return "Нет изображения" + + image_preview.short_description = 'Превью' + + +@admin.register(DeviceConnectorType) +class DeviceConnectorTypeAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name', 'description') + + +@admin.register(WirelessTechnology) +class WirelessTechnologyAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name', 'description') + + +@admin.register(ChargingInterface) +class ChargingInterfaceAdmin(admin.ModelAdmin): + list_display = ('name',) + search_fields = ('name', 'description') + + +# ====================== +# Регистрация отзывов и медиа +# ====================== + +@admin.register(HeadphonesReviewType) +class HeadphonesReviewTypeAdmin(admin.ModelAdmin): + list_display = ('name', 'slug') + search_fields = ('name',) + prepopulated_fields = {'slug': ('name',)} + + +@admin.register(HeadphonesReviewResource) +class HeadphonesReviewResourceAdmin(admin.ModelAdmin): + list_display = ('name', 'review_type', 'website_link', 'slug') + list_filter = ('type',) + search_fields = ('name', 'type__name') + + def website_link(self, obj): + return format_html('{}', obj.url, obj.url) + + website_link.short_description = "Ссылка на ресурс" + + def review_type(self, obj): + return obj.type.name if obj.type else '-' + + review_type.short_description = 'Тип обзора' + review_type.admin_order_field = 'type__name' + + +@admin.register(HeadphonesConnector) +class HeadphonesConnectorAdmin(admin.ModelAdmin): + list_display = ('headphones', 'connector_display', 'is_primary', 'notes') + list_filter = ('is_primary', 'connector') + + def connector_display(self, obj): + return str(obj.connector) + + connector_display.short_description = 'Разъем' + connector_display.admin_order_field = 'connector__name' + + +@admin.register(HeadphonesReviewLanguage) +class HeadphonesReviewLanguageAdmin(admin.ModelAdmin): + list_display = ('language',) + search_fields = ('language',) + + +@admin.register(HeadphonesReviewAuthor) +class HeadphonesReviewAuthorAdmin(admin.ModelAdmin): + list_display = ('name', 'nickname', 'reviews_count') + search_fields = ('name', 'nickname') + filter_horizontal = ('resources',) + + def reviews_count(self, obj): + return obj.headphonesreview_set.count() + + reviews_count.short_description = 'Кол-во обзоров' + + # Добавьте инлайн для отображения обзоров автора + class ReviewsInline(admin.TabularInline): + model = HeadphonesReview + extra = 0 + fields = ('title', 'headphones', 'date') + readonly_fields = ('title', 'headphones', 'date') + + inlines = [ReviewsInline] + + +class HeadphonesReviewAuthorInline(admin.StackedInline): + model = HeadphonesReviewAuthor + extra = 1 + fields = ('name', 'nickname', 'resources') + filter_horizontal = ('resources',) + + + + + +@admin.register(HeadphonesReview) +class HeadphonesReviewAdmin(admin.ModelAdmin): + list_display = ( + 'title', + 'headphones_link', + 'resource_link', + 'date', + # 'author_display', + 'review_link' + ) + list_filter = ( + 'resource__type', + 'headphones__brand', + 'date' + ) + search_fields = ( + 'title', + 'headphones__model', + # 'author', + 'resource__name' + ) + raw_id_fields = ('headphones', 'resource') + date_hierarchy = 'date' + list_select_related = ('headphones', 'resource') + + def headphones_link(self, obj): + if obj.headphones: + url = reverse('admin:headphones_headphones_change', args=[obj.headphones.id]) + return format_html('{}', url, obj.headphones.model) + return "-" + + headphones_link.short_description = "Наушники" + headphones_link.admin_order_field = 'headphones__model' + + def resource_link(self, obj): + url = reverse('admin:headphones_headphonesreviewresource_change', args=[obj.resource.id]) + return format_html('{}', url, obj.resource.name) + + resource_link.short_description = "Ресурс" + resource_link.admin_order_field = 'resource__name' + + def review_link(self, obj): + return format_html('Открыть', obj.url) + + review_link.short_description = "Ссылка" + + def author_display(self, obj): + if obj.author: + url = reverse('admin:headphones_headphonesreviewauthor_change', args=[obj.author.id]) + return format_html('{}', url, obj.author.name) + return "-" + + author_display.short_description = "Автор" + + +@admin.register(HeadphonesImages) +class HeadphonesImagesAdmin(admin.ModelAdmin): + list_display = ('product', 'caption') + list_filter = ('product__brand',) + search_fields = ('product__model', 'caption') + readonly_fields = ('image_preview',) + + def image_preview(self, obj): + if obj.photo: + return mark_safe(f'') + return "Нет изображения" + + image_preview.short_description = 'Превью' + + +# ====================== +# Регистрация вспомогательных моделей +# ====================== + +@admin.register(HeadphonesDriver) +class HeadphonesDriverAdmin(admin.ModelAdmin): + list_display = ('headphones', 'driver_type', 'size', 'count') + list_filter = ('driver__driver_type',) + + def driver_type(self, obj): + return obj.driver.driver_type.name + + driver_type.short_description = 'Тип драйвера' + + def size(self, obj): + return f"{obj.driver.size}mm" if obj.driver.size else '-' + + size.short_description = 'Размер' + + +@admin.register(Comment) +class CommentAdmin(admin.ModelAdmin): + list_display = ('user', 'post', 'date', 'short_body') + list_filter = ('date', 'user') + # search_fields = ('body', 'post__model', 'user__username') + readonly_fields = ('date',) + + def short_body(self, obj): + return obj.body[:50] + '...' if len(obj.body) > 50 else obj.body + + short_body.short_description = 'Текст' diff --git a/headphones/apps.py b/headphones/apps.py new file mode 100644 index 0000000..65c088d --- /dev/null +++ b/headphones/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HeadphonesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'headphones' diff --git a/headphones/migrations/__init__.py b/headphones/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/headphones/models.py b/headphones/models.py new file mode 100644 index 0000000..c8d255f --- /dev/null +++ b/headphones/models.py @@ -0,0 +1,1050 @@ +import os +import uuid + +from django.core.files.storage import default_storage +from django.core.validators import MinValueValidator, MaxValueValidator +from django.db import models +from django.db.models.signals import pre_delete +from django.dispatch import receiver +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.utils.text import slugify +from django.conf import settings +from imagekit.models import ProcessedImageField +from imagekit.processors import ResizeToFit +from PIL import Image + + +def upload_to(instance, filename): + return f"headphones/{instance.product.id}/{filename}" + +# ====================== +# Базовые справочники +# ====================== +class BrandCountry(models.Model): + name = models.CharField(max_length=50, verbose_name="Название страны") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Страна производитель" + + def __str__(self): + return self.name + + +class CaseColors(models.Model): + name = models.CharField(max_length=50, verbose_name="Название цвета") + hex_code = models.CharField(max_length=50, verbose_name="HEX-код цвета") + + class Meta: + verbose_name = "Цвет корпуса" + verbose_name_plural = "Цвета корпусов" + + def __str__(self): + return f"{self.name} {self.hex_code}" + + +# ====================== +# Основные модели брендов и категорий +# ====================== +class Brand(models.Model): + name = models.CharField(max_length=50, verbose_name="Название бренда") + country = models.ForeignKey( + BrandCountry, + on_delete=models.CASCADE, + verbose_name="Страна производитель" + ) + year_founded = models.PositiveSmallIntegerField(verbose_name="Год основания") + is_year_approx = models.BooleanField( + verbose_name="Год указан приблизительно", + default=False + ) + + site = models.URLField(verbose_name="Официальный сайт") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + # Рекурсивная связь (может быть null, если это родительская компания) + parent_company = models.ForeignKey( + 'self', + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='subsidiaries', + verbose_name="Родительская компания" + ) + + class Meta: + verbose_name = "Бренд" + verbose_name_plural = "Бренды" + + def __str__(self): + return self.name + + +class CaseType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип корпуса") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Тип корпуса" + verbose_name_plural = "Типы корпусов" + + def __str__(self): + return self.name + + +class CaseMaterial(models.Model): + name = models.CharField(max_length=50, verbose_name="Материал корпуса") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Материал корпуса" + verbose_name_plural = "Материалы корпусов" + + def __str__(self): + return self.name + + +class HeadphonePurpose(models.Model): + name = models.CharField(max_length=50, verbose_name="Назначение") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Назначение наушников" + verbose_name_plural = "Назначения наушников" + + def __str__(self): + return self.name + + +# ====================== +# Теги и категории +# ====================== +class TagCategory(models.Model): + name = models.CharField(max_length=50, verbose_name="Название категории") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Категория тегов" + verbose_name_plural = "Категории тегов" + + def __str__(self): + return self.name + + +class Tags(models.Model): + name = models.CharField(max_length=50, verbose_name="Название тега") + category = models.ForeignKey( + TagCategory, + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="Категория" + ) + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Тег" + verbose_name_plural = "Теги" + ordering = ['category__name', 'name'] + + def __str__(self): + return f"{self.category.name}: {self.name}" if self.category else self.name + + +# ====================== +# Аудио компоненты +# ====================== +class DriverModel(models.Model): + name = models.CharField(max_length=50, verbose_name="Модель драйвера") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Модель драйвера" + verbose_name_plural = "Модели драйверов" + + def __str__(self): + return self.name + + +class DriverType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип драйвера") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Тип драйвера" + verbose_name_plural = "Типы драйверов" + + def __str__(self): + return self.name + + +class Driver(models.Model): + driver_type = models.ForeignKey( + DriverType, + on_delete=models.CASCADE, + verbose_name="Тип драйвера" + ) + driver_model = models.ForeignKey( + DriverModel, + on_delete=models.CASCADE, + verbose_name="Модель драйвера", + blank=True, + null=True + ) + size = models.DecimalField( + verbose_name="Размер (мм)", + max_digits=4, + decimal_places=1, + null=True, + blank=True + ) + + frequency_range = models.CharField( + verbose_name="Частотный диапазон (Гц)", + max_length=50, + null=True, + blank=True + ) + + class Meta: + verbose_name = "Драйвер" + verbose_name_plural = "Драйверы" + + def __str__(self): + return f"{self.driver_type.name} {self.size}mm" + + +class NoiseCancellationType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип шумоподавления") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Тип шумоподавления" + + def __str__(self): + return self.name + + +class CodecType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип кодека") + description = models.TextField(blank=True, verbose_name="Описание") + + class Meta: + verbose_name = "Тип аудиокодека" + verbose_name_plural = "Типы аудиокодеков" + + def __str__(self): + return self.name + + +class AudioCodec(models.Model): + name = models.CharField(max_length=50, verbose_name="Название кодека") + codec_type = models.ForeignKey( + CodecType, + on_delete=models.PROTECT, + verbose_name="Тип кодека" + ) + max_bitrate = models.PositiveIntegerField( + verbose_name="Макс. битрейт (кбит/с)", + null=True, + blank=True + ) + max_sample_rate = models.PositiveIntegerField( + verbose_name="Макс. частота (кГц)", + null=True, + blank=True + ) + bit_depth = models.PositiveSmallIntegerField( + verbose_name="Битовая глубина", + null=True, + blank=True + ) + latency = models.PositiveSmallIntegerField( + verbose_name="Задержка (мс)", + null=True, + blank=True + ) + description = models.TextField(blank=True, verbose_name="Описание") + + class Meta: + verbose_name = "Аудиокодек" + verbose_name_plural = "Аудиокодеки" + + def __str__(self): + return f"{self.name} ({self.codec_type})" + + +# ====================== +# Разъемы и подключения +# ====================== +class CableConnectionType(models.Model): + name = models.CharField(max_length=50, verbose_name="Название разъема") + description = models.CharField( + max_length=100, + blank=True, + verbose_name="Описание" + ) + image = models.ImageField( + upload_to='connectors/', + blank=True, + null=True, + verbose_name="Изображение" + ) + is_detachable = models.BooleanField( + default=True, + verbose_name="Съемный разъем" + ) + + class Meta: + verbose_name = "Тип разъема кабеля" + verbose_name_plural = "Типы разъемов кабелей" + + def __str__(self): + return self.name + + +class CableMaterial(models.Model): + name = models.CharField(max_length=50, verbose_name="Материал кабеля") + description = models.CharField( + max_length=100, + blank=True, + verbose_name="Описание" + ) + + class Meta: + verbose_name = "Материал кабеля" + verbose_name_plural = "Материал кабелей" + + def __str__(self): + return self.name + + +class DeviceConnectorType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип разъема") + description = models.TextField(blank=True, verbose_name="Описание") + + class Meta: + verbose_name = "Тип разъема устройства" + verbose_name_plural = "Типы разъемов устройств" + + def __str__(self): + return self.name + + +class WirelessTechnology(models.Model): + name = models.CharField(max_length=50, verbose_name="Технология") + description = models.TextField(blank=True, verbose_name="Описание") + + class Meta: + verbose_name = "Беспроводная технология" + verbose_name_plural = "Беспроводные технологии" + + def __str__(self): + return self.name + + +class ChargingInterface(models.Model): + name = models.CharField(max_length=50, verbose_name="Интерфейс зарядки") + description = models.TextField(blank=True, verbose_name="Описание") + + class Meta: + verbose_name = "Интерфейс зарядки" + verbose_name_plural = "Интерфейсы зарядки" + + def __str__(self): + return self.name + + +# ====================== +# Отзывы и медиа +# ====================== +class HeadphonesReviewType(models.Model): + name = models.CharField(max_length=50, verbose_name="Тип обзора") + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") # Добавлено slug поле + + class Meta: + verbose_name = "Тип обзора" + verbose_name_plural = "Типы обзоров" + + def __str__(self): + return self.name + + +class HeadphonesReviewAuthorResource(models.Model): + name = models.CharField(max_length=50, verbose_name="Название ресурса") + url = models.URLField(verbose_name="URL ресурса", blank=True) + + class Meta: + verbose_name = "Ресурс автора" + verbose_name_plural = "Ресурсы авторов" + + def __str__(self): + return self.name + + +class HeadphonesReviewAuthor(models.Model): + name = models.CharField(max_length=50, verbose_name="Имя автора") + nickname = models.CharField(max_length=50, verbose_name="Никнейм автора") + resources = models.ManyToManyField(HeadphonesReviewAuthorResource, + related_name='author_resources', + blank=True, + verbose_name="Ресурсы автора") + + class Meta: + verbose_name = "Автор обзора" + verbose_name_plural = "Авторы обзоров" + ordering = ['name'] + + def __str__(self): + return self.nickname if self.nickname else self.name + + +class HeadphonesReviewResource(models.Model): + name = models.CharField(max_length=50, verbose_name="Ресурс обзора") + type = models.ForeignKey( + HeadphonesReviewType, + on_delete=models.CASCADE, + verbose_name="Тип обзора" + ) + url = models.URLField(verbose_name="URL ресурса", blank=True) # Добавлено поле для URL ресурса + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + + class Meta: + verbose_name = "Ресурс обзора" + verbose_name_plural = "Ресурсы обзоров" + ordering = ['name'] # Добавлена сортировка + + def __str__(self): + return f"{self.name} ({self.type.name if self.type else 'без типа'})" + + +class HeadphonesReviewLanguage(models.Model): + language = models.CharField(max_length=50, verbose_name="Язык обзора") + + class Meta: + verbose_name = "Язык обзора" + + def __str__(self): + return self.language + + +class HeadphonesReview(models.Model): + headphones = models.ForeignKey( + 'Headphones', + on_delete=models.CASCADE, + related_name='reviews', + verbose_name="Наушники", + null=True, + blank=True + ) + # Изменено на связь с HeadphonesReviewResource (была ошибка в логике) + resource = models.ForeignKey( + HeadphonesReviewResource, + on_delete=models.CASCADE, + verbose_name="Ресурс обзора" + ) + author = models.ForeignKey( + HeadphonesReviewAuthor, + on_delete=models.CASCADE, + verbose_name="Автор обзора", + null = True, + blank = True, + related_name='reviews' + ) + + language = models.ForeignKey( + HeadphonesReviewLanguage, + on_delete=models.CASCADE, + null=True, + blank=True, + ) + + title = models.CharField(max_length=100, verbose_name="Заголовок обзора") # Изменено name на title + url = models.URLField(verbose_name="Ссылка на обзор", unique=True) # Добавлен unique + date = models.DateField( + verbose_name="Дата публикации", + null=True, + blank=True + ) + + class Meta: + verbose_name = "Обзор наушников" + verbose_name_plural = "Обзоры наушников" + ordering = ['-date'] # Сортировка по дате + indexes = [ + models.Index(fields=['headphones', 'date']), # Добавлен индекс + ] + + def __str__(self): + # Более информативное представление + return f"{self.title} ({self.headphones.model if self.headphones else 'No headphones'})" + + +class HeadphonesImages(models.Model): + product = models.ForeignKey( + 'Headphones', + on_delete=models.CASCADE, + related_name='images', + verbose_name="Наушники" + ) + + photo = ProcessedImageField( + upload_to=upload_to, + processors=[ResizeToFit(1000, 1000)], # Макс. ширина/высота = 1000px + format='WEBP', + options={'quality': 85}, + ) + caption = models.CharField( + max_length=200, + blank=True, + verbose_name="Подпись" + ) + is_main = models.BooleanField( + default=False, + verbose_name="Основное изображение", + help_text="Используется как превью товара" + ) + + def save(self, *args, **kwargs): + # Если это основное изображение, снимаем флаг is_main у других изображений + if self.is_main: + HeadphonesImages.objects.filter(product=self.product).exclude(id=self.id).update(is_main=False) + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + """Удаление файла при удалении записи""" + if self.photo: + try: + if default_storage.exists(self.photo.name): + default_storage.delete(self.photo.name) + except Exception as e: + # Логируем ошибку, но не прерываем удаление + print(f"Ошибка при удалении файла: {e}") + super().delete(*args, **kwargs) + + @property + def image_info(self): + """Вычисляемое поле с информацией о размере изображения""" + if not self.photo: + return mark_safe("—") + + try: + img_path = self.photo.path + if not os.path.exists(img_path): + return mark_safe("Файл не найден") + + with Image.open(img_path) as img: + width, height = img.size + + file_size = os.path.getsize(img_path) + size_kb = file_size / 1024 + file_ext = os.path.splitext(self.photo.name)[1][1:].upper() + + # Создаем отдельные безопасные строки + dimensions = mark_safe(f"{width}×{height}") + size = mark_safe(f"{size_kb:.1f} kb") + ext = mark_safe(file_ext) + + # Комбинируем с помощью format_html + return format_html( + "{}
{}
{}", + dimensions, + size, + ext + ) + except Exception as e: + return mark_safe(f"Ошибка: {str(e)}") + class Meta: + verbose_name = "Изображение наушников" + verbose_name_plural = "Изображения наушников" + + +@receiver(pre_delete, sender=HeadphonesImages) +def delete_image_file(sender, instance, **kwargs): + """Обработчик сигнала для удаления файла при каскадном удалении""" + if instance.photo: + try: + if os.path.isfile(instance.photo.path): + os.remove(instance.photo.path) + except Exception as e: + print(f"Ошибка при удалении файла (сигнал): {e}") + +# ====================== +# Основная модель наушников +# ====================== +class Headphones(models.Model): + # Идентификация и базовые данные + id = models.UUIDField( + primary_key=True, + default=uuid.uuid4, + editable=False, + verbose_name="Идентификатор" + ) + slug = models.SlugField(unique=True, verbose_name="URL-идентификатор") + model = models.CharField(max_length=50, verbose_name="Модель") + description = models.TextField(verbose_name="Описание") + + CONNECTION_TYPE_CHOICES = [ + ('wired', 'Проводные'), + ('wireless', 'Беспроводные'), + ('hybrid', 'Гибридные (проводные + беспроводные)'), + ] + connection_type = models.CharField( + max_length=10, + choices=CONNECTION_TYPE_CHOICES, + default='wired', + verbose_name="Тип подключения" + ) + case_type = models.ForeignKey( + CaseType, + on_delete=models.PROTECT, + verbose_name="Тип корпуса" + ) + case_material = models.ForeignKey( + CaseMaterial, + on_delete=models.PROTECT, + verbose_name="Материал корпуса", + null=True, + blank=True + ) + brand = models.ForeignKey( + Brand, + on_delete=models.PROTECT, + verbose_name="Производитель" + ) + headphone_purpose = models.ForeignKey( + HeadphonePurpose, + on_delete=models.PROTECT, + verbose_name="Назначение" + ) + + # Медиа и ссылки + product_url = models.URLField( + blank=True, + verbose_name="Ссылка на товар" + ) + + # Теги и компоненты + tags = models.ManyToManyField( + Tags, + related_name='headphones', + blank=True, + verbose_name="Теги" + ) + drivers = models.ManyToManyField( + Driver, + through='HeadphonesDriver', + related_name='headphones', + verbose_name="Драйверы", + blank=True, + null=True + ) + + # Технические характеристики + frequency_response_chart = models.JSONField( + verbose_name="АЧХ (JSON)", + null=True, + blank=True, + help_text="Данные в формате [{'frequency': 20, 'amplitude': -2.5}, ...]" + ) + impedance = models.DecimalField( + verbose_name="Импеданс (Ом)", + max_digits=5, + decimal_places=2, + null=True, + blank=True, + validators=[ + MinValueValidator(4), # Минимум 4 Ом + MaxValueValidator(600) # Максимум 600 Ом + ] + ) + sensitivity = models.DecimalField( + verbose_name="Чувствительность (дБ/мВт)", + max_digits=5, + decimal_places=2, + null=True, + blank=True, + help_text="Уровень звукового давления" + ) + frequency_range = models.CharField( + verbose_name="Частотный диапазон (Гц)", + max_length=50, + null=True, + blank=True, + help_text="Пример: 20-20000 Гц" + ) + + # Дополнительные функции + microphone = models.BooleanField( + verbose_name="Встроенный микрофон", + default=False, + ) + noise_cancellation = models.ForeignKey( + NoiseCancellationType, + on_delete=models.PROTECT, + null=True, + blank=True, + verbose_name="Шумоподавление", + ) + ip_rating = models.CharField( + max_length=10, + blank=True, + help_text="Например: IPX4", + verbose_name="Степень защиты (IP)", + ) + official_price = models.DecimalField( + verbose_name="Официальная цена на сайте", + max_digits=6, + decimal_places=2, + null=True, + blank=True + ) + + # Внешний вид и физические параметры + colors = models.ManyToManyField( + CaseColors, + related_name='headphones', + verbose_name="Цвета" + ) + weight = models.PositiveSmallIntegerField( + verbose_name="Вес (г)", + null=True, + blank=True + ) + + # Метаданные + release_year = models.PositiveSmallIntegerField( + verbose_name="Год выхода", + null=True, + blank=True, + help_text="Год выпуска модели" + ) + date_added = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата добавления" + ) + published = models.BooleanField( + default=False, + verbose_name="Опубликовано" + ) + + class Meta: + verbose_name = "Наушники" + verbose_name_plural = "Наушники" + ordering = ['brand', 'model'] + + def __str__(self): + return f"{self.brand.name} {self.model}" + + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(f"{self.brand.name} {self.model}") + super().save(*args, **kwargs) + +class HeadphonesConnector(models.Model): + headphones = models.ForeignKey( + 'Headphones', + on_delete=models.CASCADE, + verbose_name="Наушники" + ) + wired_headphones = models.ForeignKey( + 'WiredHeadphones', + on_delete=models.CASCADE, + null=True, + blank=True, + verbose_name="Проводные наушники" + ) + connector = models.ForeignKey( + DeviceConnectorType, + on_delete=models.PROTECT, + verbose_name="Разъем" + ) + is_primary = models.BooleanField( + default=False, + verbose_name="Основной разъем" + ) + notes = models.CharField( + max_length=100, + blank=True, + verbose_name="Примечания" + ) + + class Meta: + verbose_name = "Разъем наушников" + verbose_name_plural = "Разъемы наушников" + ordering = ['-is_primary', 'connector__name'] + + def __str__(self): + return f"{self.connector.name} ({'основной' if self.is_primary else 'доп.'})" + +# ====================== +# Специфичные модели для типов наушников +# ====================== +class WiredHeadphones(models.Model): + headphones = models.OneToOneField( + Headphones, + on_delete=models.CASCADE, + primary_key=True, + related_name='wired_details', + verbose_name="Наушники" + ) + cable_connection_type = models.ForeignKey( + CableConnectionType, + on_delete=models.PROTECT, + verbose_name="Тип разъема кабеля" + ) + cable_length = models.DecimalField( + max_digits=5, + decimal_places=2, + verbose_name="Длина кабеля (м)", + help_text="Длина кабеля в метрах" + ) + # Удаляем старое поле connector_to_device + custom_connector_name = models.CharField( + max_length=50, + blank=True, + verbose_name="Проприетарный разъем" + ) + + # Добавляем свойство для удобного доступа к разъемам + @property + def connectors(self): + return self.headphones.headphonesconnector_set.all() + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + # Обновляем связь в разъемах + HeadphonesConnector.objects.filter(headphones=self.headphones).update(wired_headphones=self) + + class Meta: + verbose_name = "Характеристики проводных наушников" + verbose_name_plural = "Характеристики проводных наушников" + + def __str__(self): + return f"Проводные характеристики для {self.headphones.model}" + + +class WirelessHeadphones(models.Model): + headphones = models.OneToOneField( + Headphones, + on_delete=models.CASCADE, + primary_key=True, + related_name='wireless_details', + verbose_name="Наушники" + ) + wireless_technology = models.ForeignKey( + WirelessTechnology, + on_delete=models.PROTECT, + verbose_name="Беспроводная технология" + ) + bluetooth_version = models.CharField( + max_length=20, + blank=True, + verbose_name="Версия Bluetooth" + ) + battery_life = models.IntegerField( + verbose_name="Время работы (ч)", + help_text="Время работы от батареи в часах" + ) + charging_time = models.IntegerField( + verbose_name="Время зарядки (мин)", + help_text="Время зарядки в минутах" + ) + has_charging_case = models.BooleanField( + default=False, + verbose_name="Зарядный кейс" + ) + charging_case_battery_life = models.IntegerField( + blank=True, + null=True, + verbose_name="Время работы с кейсом (ч)", + help_text="Общее время работы с кейсом в часах" + ) + charging_interface = models.ForeignKey( + ChargingInterface, + on_delete=models.PROTECT, + verbose_name="Интерфейс зарядки" + ) + supported_codecs = models.ManyToManyField( + AudioCodec, + blank=True, + verbose_name="Поддерживаемые кодеки" + ) + + class Meta: + verbose_name = "Характеристики беспроводных наушников" + verbose_name_plural = "Характеристики беспроводных наушников" + + def __str__(self): + return f"Беспроводные характеристики для {self.headphones.model}" + + +class HybridHeadphones(models.Model): + headphones = models.OneToOneField( + Headphones, + on_delete=models.CASCADE, + primary_key=True, + related_name='hybrid_details', + verbose_name="Наушники" + ) + + # Беспроводная часть + wireless_technology = models.ForeignKey( + WirelessTechnology, + on_delete=models.PROTECT, + verbose_name="Беспроводная технология" + ) + bluetooth_version = models.CharField( + max_length=20, + blank=True, + verbose_name="Версия Bluetooth" + ) + battery_life = models.IntegerField( + verbose_name="Время работы (ч)", + help_text="В беспроводном режиме" + ) + charging_time = models.IntegerField( + verbose_name="Время зарядки (мин)" + ) + supported_codecs = models.ManyToManyField( + AudioCodec, + blank=True, + verbose_name="Поддерживаемые кодеки" + ) + + # Проводная часть + cable_connection_type = models.ForeignKey( + CableConnectionType, + on_delete=models.PROTECT, + verbose_name="Тип разъема кабеля" + ) + cable_length = models.DecimalField( + max_digits=5, + decimal_places=2, + verbose_name="Длина кабеля (м)", + null = True, + blank = True + ) + cable_material = models.ForeignKey( + CableMaterial, + on_delete=models.PROTECT, + verbose_name="Материал кабеля", + null=True, + blank=True + ) + connector_to_device = models.ForeignKey( + DeviceConnectorType, + on_delete=models.PROTECT, + verbose_name="Разъем для устройства" + ) + + # Общие характеристики + auto_switch_mode = models.BooleanField( + default=False, + verbose_name="Автопереключение режимов" + ) + simultaneous_connection = models.BooleanField( + default=False, + verbose_name="Одновременное подключение" + ) + + # Зарядка + charging_interface = models.ForeignKey( + ChargingInterface, + on_delete=models.PROTECT, + verbose_name="Интерфейс зарядки" + ) + has_charging_case = models.BooleanField( + default=False, + verbose_name="Зарядный кейс" + ) + charging_case_battery_life = models.IntegerField( + blank=True, + null=True, + verbose_name="Время работы с кейсом (ч)" + ) + + class Meta: + verbose_name = "Характеристики гибридных наушников" + verbose_name_plural = "Характеристики гибридных наушников" + + def __str__(self): + return f"Гибридные характеристики для {self.headphones.model}" + + +# ====================== +# Вспомогательные модели +# ====================== +class HeadphonesDriver(models.Model): + headphones = models.ForeignKey( + Headphones, + on_delete=models.CASCADE, + verbose_name="Наушники" + ) + driver = models.ForeignKey( + Driver, + on_delete=models.CASCADE, + verbose_name="Драйвер" + ) + count = models.PositiveSmallIntegerField( + default=1, + verbose_name="Количество" + ) + + class Meta: + verbose_name = "Драйвер в наушниках" + verbose_name_plural = "Драйверы в наушниках" + unique_together = [('headphones', 'driver')] + + def __str__(self): + return f"{self.driver.driver_type.name} x{self.count}" + + +class Comment(models.Model): + id = models.CharField( + max_length=100, + blank=True, + unique=True, + default=uuid.uuid4, + primary_key=True, + verbose_name="Идентификатор" + ) + date = models.DateTimeField( + auto_now_add=True, + verbose_name="Дата создания" + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + verbose_name="Пользователь" + ) + body = models.TextField( + blank=True, + null=True, + verbose_name="Текст комментария" + ) + post = models.ForeignKey( + Headphones, + on_delete=models.CASCADE, + blank=True, + related_name='comments', + related_query_name='comment', + verbose_name="Наушники" + ) + parent = models.ForeignKey( + 'self', + related_name='reply', + null=True, + blank=True, + on_delete=models.CASCADE, + verbose_name="Родительский комментарий" + ) + + class Meta: + verbose_name = "Комментарий" + verbose_name_plural = "Комментарии" + ordering = ['-date'] + + def __str__(self): + return f'Комментарий к "{self.post.model}" от {self.user}' diff --git a/headphones/tests.py b/headphones/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/headphones/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/headphones/views.py b/headphones/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/headphones/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/headphones_backend/__init__.py b/headphones_backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/headphones_backend/asgi.py b/headphones_backend/asgi.py new file mode 100644 index 0000000..1d11064 --- /dev/null +++ b/headphones_backend/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for headphones_backend 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/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'headphones_backend.settings') + +application = get_asgi_application() diff --git a/headphones_backend/settings.py b/headphones_backend/settings.py new file mode 100644 index 0000000..04dc854 --- /dev/null +++ b/headphones_backend/settings.py @@ -0,0 +1,144 @@ +""" +Django settings for headphones_backend project. + +Generated by 'django-admin startproject' using Django 5.2. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path +import os +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-8ig5m_im8c%+f_$8px^yas!qh=#j@e)ul(y3cg%6&jzb#cn0ab' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rest_framework', + 'imagekit', + 'headphones', + 'users.apps.UsersConfig', + # 'headphones_main.apps.HeadphonesMainConfig', +] + +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 = 'headphones_backend.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'templates'] + , + '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 = 'headphones_backend.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "OPTIONS": { + "service": "headphones", + "passfile": ".headphones_db", + }, + } +} + + + +# Password validation +# https://docs.djangoproject.com/en/5.2/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', + }, +] + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', + ], + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10 +} + +AUTH_USER_MODEL = 'users.User' + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = 'ru-ru' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = '/static/' +STATIC_ROOT = os.path.join(BASE_DIR, 'static') + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/headphones_backend/urls.py b/headphones_backend/urls.py new file mode 100644 index 0000000..aa67524 --- /dev/null +++ b/headphones_backend/urls.py @@ -0,0 +1,26 @@ +""" +URL configuration for headphones_backend project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/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 django.conf import settings +from django.conf.urls.static import static +urlpatterns = [ + path('admin/', admin.site.urls), +] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/headphones_backend/wsgi.py b/headphones_backend/wsgi.py new file mode 100644 index 0000000..963d947 --- /dev/null +++ b/headphones_backend/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for headphones_backend 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/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'headphones_backend.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..fff9997 --- /dev/null +++ b/manage.py @@ -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', 'headphones_backend.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() diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..d57b66f --- /dev/null +++ b/poetry.lock @@ -0,0 +1,249 @@ +# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. + +[[package]] +name = "asgiref" +version = "3.8.1" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "django" +version = "5.2" +description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "Django-5.2-py3-none-any.whl", hash = "sha256:91ceed4e3a6db5aedced65e3c8f963118ea9ba753fc620831c77074e620e7d83"}, + {file = "Django-5.2.tar.gz", hash = "sha256:1a47f7a7a3d43ce64570d350e008d2949abe8c7e21737b351b6a1611277c6d89"}, +] + +[package.dependencies] +asgiref = ">=3.8.1" +sqlparse = ">=0.3.1" +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +argon2 = ["argon2-cffi (>=19.1.0)"] +bcrypt = ["bcrypt"] + +[[package]] +name = "django-appconf" +version = "1.1.0" +description = "A helper class for handling configuration defaults of packaged apps gracefully." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "django-appconf-1.1.0.tar.gz", hash = "sha256:9fcead372f82a0f21ee189434e7ae9c007cbb29af1118c18251720f3d06243e4"}, + {file = "django_appconf-1.1.0-py3-none-any.whl", hash = "sha256:7abd5a163ff57557f216e84d3ce9dac36c37ffce1ab9a044d3d53b7c943dd10f"}, +] + +[package.dependencies] +django = "*" + +[[package]] +name = "django-imagekit" +version = "5.0.0" +description = "Automated image processing for Django models." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "django-imagekit-5.0.0.tar.gz", hash = "sha256:aae9f74a8e9b6ceb5d15f7d8e266302901e76d9f532c78bd5135cb0fa206a6b0"}, + {file = "django_imagekit-5.0.0-py3-none-any.whl", hash = "sha256:a8e77ed6549751026a51f961bb2cd5fda739be691496da8eecbe68ffb966c261"}, +] + +[package.dependencies] +django-appconf = "*" +pilkit = "*" + +[package.extras] +async = ["django-celery (>=3.0)"] +async-dramatiq = ["django-dramatiq (>=0.4.0)"] +async-rq = ["django-rq (>=0.6.0)"] + +[[package]] +name = "djangorestframework" +version = "3.16.0" +description = "Web APIs for Django, made easy." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "djangorestframework-3.16.0-py3-none-any.whl", hash = "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361"}, + {file = "djangorestframework-3.16.0.tar.gz", hash = "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9"}, +] + +[package.dependencies] +django = ">=4.2" + +[[package]] +name = "pilkit" +version = "3.0" +description = "A collection of utilities and processors for the Python Imaging Library." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pilkit-3.0-py3-none-any.whl", hash = "sha256:fe1707b0411a1d0cbf9ad3986779fa5a346cec4582a188740924aa39f504d117"}, + {file = "pilkit-3.0.tar.gz", hash = "sha256:f6719e8cc0482e5447f5cb94f18b949d8e604ea9673a9b019c74d41b779e4eab"}, +] + +[package.dependencies] +Pillow = ">=7.0" + +[[package]] +name = "pillow" +version = "11.1.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8"}, + {file = "pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07"}, + {file = "pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e"}, + {file = "pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269"}, + {file = "pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49"}, + {file = "pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a"}, + {file = "pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457"}, + {file = "pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6"}, + {file = "pillow-11.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2"}, + {file = "pillow-11.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96"}, + {file = "pillow-11.1.0-cp311-cp311-win32.whl", hash = "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f"}, + {file = "pillow-11.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"}, + {file = "pillow-11.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a"}, + {file = "pillow-11.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1"}, + {file = "pillow-11.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91"}, + {file = "pillow-11.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c"}, + {file = "pillow-11.1.0-cp312-cp312-win32.whl", hash = "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6"}, + {file = "pillow-11.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf"}, + {file = "pillow-11.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc"}, + {file = "pillow-11.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5"}, + {file = "pillow-11.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352"}, + {file = "pillow-11.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3"}, + {file = "pillow-11.1.0-cp313-cp313-win32.whl", hash = "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9"}, + {file = "pillow-11.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c"}, + {file = "pillow-11.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861"}, + {file = "pillow-11.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c"}, + {file = "pillow-11.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547"}, + {file = "pillow-11.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab"}, + {file = "pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9"}, + {file = "pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe"}, + {file = "pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6"}, + {file = "pillow-11.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade"}, + {file = "pillow-11.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196"}, + {file = "pillow-11.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8"}, + {file = "pillow-11.1.0-cp39-cp39-win32.whl", hash = "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5"}, + {file = "pillow-11.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f"}, + {file = "pillow-11.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73"}, + {file = "pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0"}, + {file = "pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "trove-classifiers (>=2024.10.12)"] +typing = ["typing-extensions ; python_version < \"3.10\""] +xmp = ["defusedxml"] + +[[package]] +name = "psycopg" +version = "3.2.6" +description = "PostgreSQL database adapter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "psycopg-3.2.6-py3-none-any.whl", hash = "sha256:f3ff5488525890abb0566c429146add66b329e20d6d4835662b920cbbf90ac58"}, + {file = "psycopg-3.2.6.tar.gz", hash = "sha256:16fa094efa2698f260f2af74f3710f781e4a6f226efe9d1fd0c37f384639ed8a"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "sys_platform == \"win32\""} + +[package.extras] +binary = ["psycopg-binary (==3.2.6) ; implementation_name != \"pypy\""] +c = ["psycopg-c (==3.2.6) ; implementation_name != \"pypy\""] +dev = ["ast-comments (>=1.1.2)", "black (>=24.1.0)", "codespell (>=2.2)", "dnspython (>=2.1)", "flake8 (>=4.0)", "isort-psycopg", "isort[colors] (>=6.0)", "mypy (>=1.14)", "pre-commit (>=4.0.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"] +docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"] +pool = ["psycopg-pool"] +test = ["anyio (>=4.0)", "mypy (>=1.14)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"] + +[[package]] +name = "sqlparse" +version = "0.5.3" +description = "A non-validating SQL parser." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, + {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, +] + +[package.extras] +dev = ["build", "hatch"] +doc = ["sphinx"] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "04a0cf20da59a089ce0256eef5326fcf4b928120ee38f3cbdef93ff118269b0d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..876adb7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "headphones-backend" +version = "0.1.0" +description = "" +authors = [ + {name = "apheyhys",email = "apheyhys@gmail.com"} +] +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "django (>=5.2,<6.0)", + "psycopg (>=3.2.6,<4.0.0)", + "pillow (>=11.1.0,<12.0.0)", + "django-imagekit (>=5.0.0,<6.0.0)", + "djangorestframework (>=3.16.0,<4.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/users/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..2fd8fc3 --- /dev/null +++ b/users/models.py @@ -0,0 +1,9 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models + + +class User(AbstractUser): + photo = models.CharField(max_length=350, blank=True) + + def __str__(self): + return self.username \ No newline at end of file diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/users/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.