commit 97591a71ea45d7c42d5cb5580116bf917392ad15 Author: Dmitry Date: Sat May 16 16:09:37 2026 +0300 Add initial Django project structure with cleaning app - Created .gitignore to exclude Python-generated files and virtual environments. - Added .python-version to specify Python version 3.13. - Implemented models for Client, Brigade, Service, Order, and CompletedWork. - Created Django admin configurations for managing models. - Added management command to seed the database with test data. - Created initial migrations for the models. - Developed views and templates for listing and detailing orders, clients, and brigades. - Set up URL routing for the cleaning app. - Configured Django settings and WSGI/ASGI entry points. - Added basic test structure in tests.py. - Included project metadata in pyproject.toml and uv.lock. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee67b44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv +cache/ \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/cleaning/__init__.py b/cleaning/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cleaning/admin.py b/cleaning/admin.py new file mode 100644 index 0000000..4017a21 --- /dev/null +++ b/cleaning/admin.py @@ -0,0 +1,36 @@ +from django.contrib import admin +from .models import Client, Brigade, Service, Order, CompletedWork + + +class CompletedWorkInline(admin.TabularInline): + model = CompletedWork + extra = 1 + +@admin.register(Client) +class ClientAdmin(admin.ModelAdmin): + list_display = ['full_name', 'phone', 'address', 'preferred_time'] + search_fields = ['full_name', 'phone', 'address'] + list_filter = ['preferred_time'] + +@admin.register(Brigade) +class BrigadeAdmin(admin.ModelAdmin): + list_display = ['name', 'supervisor', 'employee_count', 'specialization'] + search_fields = ['name', 'supervisor', 'specialization'] + + +@admin.register(Service) +class ServiceAdmin(admin.ModelAdmin): + list_display = ['name', 'price_per_sqm', 'duration_per_sqm'] + search_fields = ['name'] + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ['id', 'client', 'brigade', 'date', 'start_time', 'end_time'] + search_fields = ['client__full_name', 'brigade__name'] + list_filter = ['date', 'brigade'] + inlines = [CompletedWorkInline] + +@admin.register(CompletedWork) +class CompletedWorkAdmin(admin.ModelAdmin): + list_display = ['order', 'service', 'area'] + search_fields = ['order__client__full_name', 'service__name'] diff --git a/cleaning/apps.py b/cleaning/apps.py new file mode 100644 index 0000000..e425e2d --- /dev/null +++ b/cleaning/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class CleaningConfig(AppConfig): + name = 'cleaning' diff --git a/cleaning/management/__init__.py b/cleaning/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cleaning/management/commands/__init__.py b/cleaning/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cleaning/management/commands/seed.py b/cleaning/management/commands/seed.py new file mode 100644 index 0000000..593d09f --- /dev/null +++ b/cleaning/management/commands/seed.py @@ -0,0 +1,95 @@ +import random +from datetime import date, timedelta, time + +from django.core.management.base import BaseCommand +from faker import Faker + +from cleaning.models import Client, Brigade, Service, Order, CompletedWork + +fake = Faker('ru_RU') + + +class Command(BaseCommand): + help = 'Заполнить базу тестовыми данными' + + def handle(self, *args, **options): + CompletedWork.objects.all().delete() + Order.objects.all().delete() + Service.objects.all().delete() + Brigade.objects.all().delete() + Client.objects.all().delete() + + time_choices = ['morning', 'day', 'evening', 'any'] + clients = [] + for _ in range(12): + client = Client.objects.create( + full_name=fake.name(), + address=fake.address().replace('\n', ', '), + phone=fake.phone_number(), + preferred_time=random.choice(time_choices), + ) + clients.append(client) + + specializations = [ + 'Генеральная уборка', + 'Уборка после ремонта', + 'Мытьё окон', + 'Химчистка мебели', + 'Уборка офисов', + ] + brigades = [] + for i, spec in enumerate(specializations): + brigade = Brigade.objects.create( + name=f'Бригада «{fake.last_name()}»', + supervisor=fake.name(), + employee_count=random.randint(2, 8), + specialization=spec, + ) + brigades.append(brigade) + + services_data = [ + ('Мытьё полов', 25.00, 0.5), + ('Мытьё окон', 80.00, 1.2), + ('Уборка ванной комнаты', 120.00, 2.0), + ('Пылесосинг ковров', 30.00, 0.8), + ('Мытьё сантехники', 90.00, 1.5), + ('Протирка мебели', 20.00, 0.3), + ('Чистка кухонной плиты', 150.00, 3.0), + ('Вынос мусора', 10.00, 0.1), + ('Мытьё зеркал', 40.00, 0.4), + ] + services = [] + for name, price, duration in services_data: + service = Service.objects.create( + name=name, + price_per_sqm=price, + duration_per_sqm=duration, + ) + services.append(service) + + today = date.today() + for i in range(25): + order_date = today + timedelta(days=random.randint(-30, 15)) + start_hour = random.randint(8, 16) + duration_hours = random.randint(2, 5) + order = Order.objects.create( + client=random.choice(clients), + brigade=random.choice(brigades), + date=order_date, + start_time=time(start_hour, 0), + end_time=time(start_hour + duration_hours, 0), + ) + chosen_services = random.sample(services, k=random.randint(1, 4)) + for service in chosen_services: + CompletedWork.objects.create( + order=order, + service=service, + area=round(random.uniform(10, 80), 1), + ) + + self.stdout.write(self.style.SUCCESS( + f'Готово: {Client.objects.count()} клиентов, ' + f'{Brigade.objects.count()} бригад, ' + f'{Service.objects.count()} услуг, ' + f'{Order.objects.count()} заказов.' + )) diff --git a/cleaning/migrations/0001_clients_brigades.py b/cleaning/migrations/0001_clients_brigades.py new file mode 100644 index 0000000..8297ea7 --- /dev/null +++ b/cleaning/migrations/0001_clients_brigades.py @@ -0,0 +1,47 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name='Client', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('full_name', models.CharField(max_length=200, verbose_name='ФИО')), + ('address', models.CharField(max_length=300, verbose_name='Адрес')), + ('phone', models.CharField(max_length=20, verbose_name='Телефон')), + ('preferred_time', models.CharField( + choices=[('morning', 'Утро'), ('day', 'День'), ('evening', 'Вечер'), ('any', 'Не важно')], + default='any', + max_length=10, + verbose_name='Предпочтительное время', + )), + ], + options={ + 'verbose_name': 'Клиент', + 'verbose_name_plural': 'Клиенты', + 'ordering': ['full_name'], + }, + ), + migrations.CreateModel( + name='Brigade', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Название')), + ('supervisor', models.CharField(max_length=200, verbose_name='Руководитель')), + ('employee_count', models.PositiveIntegerField(verbose_name='Количество сотрудников')), + ('specialization', models.CharField(max_length=200, verbose_name='Специализация')), + ], + options={ + 'verbose_name': 'Бригада', + 'verbose_name_plural': 'Бригады', + 'ordering': ['name'], + }, + ), + ] diff --git a/cleaning/migrations/0002_services.py b/cleaning/migrations/0002_services.py new file mode 100644 index 0000000..350f03b --- /dev/null +++ b/cleaning/migrations/0002_services.py @@ -0,0 +1,25 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cleaning', '0001_clients_brigades'), + ] + + operations = [ + migrations.CreateModel( + name='Service', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='Наименование')), + ('price_per_sqm', models.DecimalField(decimal_places=2, max_digits=8, verbose_name='Цена за м2')), + ('duration_per_sqm', models.DecimalField(decimal_places=2, max_digits=6, verbose_name='Продолжительность (мин/м2)')), + ], + options={ + 'verbose_name': 'Услуга', + 'verbose_name_plural': 'Услуги', + 'ordering': ['name'], + }, + ), + ] diff --git a/cleaning/migrations/0003_orders.py b/cleaning/migrations/0003_orders.py new file mode 100644 index 0000000..03e45ec --- /dev/null +++ b/cleaning/migrations/0003_orders.py @@ -0,0 +1,36 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cleaning', '0002_services'), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(verbose_name='Дата')), + ('start_time', models.TimeField(verbose_name='Время начала')), + ('end_time', models.TimeField(verbose_name='Время окончания')), + ('client', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='cleaning.client', + verbose_name='Клиент', + )), + ('brigade', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='cleaning.brigade', + verbose_name='Бригада', + )), + ], + options={ + 'verbose_name': 'Заказ', + 'verbose_name_plural': 'Заказы', + 'ordering': ['date'], + }, + ), + ] diff --git a/cleaning/migrations/0004_completed_work.py b/cleaning/migrations/0004_completed_work.py new file mode 100644 index 0000000..2010943 --- /dev/null +++ b/cleaning/migrations/0004_completed_work.py @@ -0,0 +1,42 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cleaning', '0003_orders'), + ] + + operations = [ + migrations.CreateModel( + name='CompletedWork', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('area', models.DecimalField(decimal_places=2, max_digits=8, verbose_name='Фактическая площадь (м2)')), + ('order', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='cleaning.order', + verbose_name='Заказ', + )), + ('service', models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to='cleaning.service', + verbose_name='Услуга', + )), + ], + options={ + 'verbose_name': 'Выполненная работа', + 'verbose_name_plural': 'Выполненные работы', + }, + ), + migrations.AddField( + model_name='order', + name='services', + field=models.ManyToManyField( + through='cleaning.CompletedWork', + to='cleaning.service', + verbose_name='Услуги', + ), + ), + ] diff --git a/cleaning/migrations/__init__.py b/cleaning/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cleaning/models.py b/cleaning/models.py new file mode 100644 index 0000000..bff913e --- /dev/null +++ b/cleaning/models.py @@ -0,0 +1,96 @@ +from django.db import models + + +class Client(models.Model): + TIME_CHOICES = [ + ('morning', 'Утро'), + ('day', 'День'), + ('evening', 'Вечер'), + ('any', 'Не важно'), + ] + + full_name = models.CharField('ФИО', max_length=200) + address = models.CharField('Адрес', max_length=300) + phone = models.CharField('Телефон', max_length=20) + preferred_time = models.CharField( + 'Предпочтительное время', + max_length=10, + choices=TIME_CHOICES, + default='any', + ) + + class Meta: + verbose_name = 'Клиент' + verbose_name_plural = 'Клиенты' + ordering = ['full_name'] + + def __str__(self): + return self.full_name + + +class Brigade(models.Model): + name = models.CharField('Название', max_length=100) + supervisor = models.CharField('Руководитель', max_length=200) + employee_count = models.PositiveIntegerField('Количество сотрудников') + specialization = models.CharField('Специализация', max_length=200) + + class Meta: + verbose_name = 'Бригада' + verbose_name_plural = 'Бригады' + ordering = ['name'] + + def __str__(self): + return self.name + + +class Service(models.Model): + name = models.CharField('Наименование', max_length=200) + price_per_sqm = models.DecimalField('Цена за м2', max_digits=8, decimal_places=2) + duration_per_sqm = models.DecimalField('Продолжительность (мин/м2)', max_digits=6, decimal_places=2) + + class Meta: + verbose_name = 'Услуга' + verbose_name_plural = 'Услуги' + ordering = ['name'] + + def __str__(self): + return self.name + + +class Order(models.Model): + client = models.ForeignKey(Client, on_delete=models.CASCADE, verbose_name='Клиент', related_name='orders') + brigade = models.ForeignKey(Brigade, on_delete=models.CASCADE, verbose_name='Бригада', related_name='orders') + date = models.DateField('Дата') + start_time = models.TimeField('Время начала') + end_time = models.TimeField('Время окончания') + services = models.ManyToManyField(Service, through='CompletedWork', verbose_name='Услуги') + + class Meta: + verbose_name = 'Заказ' + verbose_name_plural = 'Заказы' + ordering = ['date'] + + def __str__(self): + return f'Заказ №{self.pk} — {self.client} ({self.date})' + + def total_cost(self): + total = 0 + for work in self.completedwork_set.select_related('service').all(): # type: ignore + total += work.area * work.service.price_per_sqm + return total + + +class CompletedWork(models.Model): + order = models.ForeignKey(Order, on_delete=models.CASCADE, verbose_name='Заказ') + service = models.ForeignKey(Service, on_delete=models.CASCADE, verbose_name='Услуга') + area = models.DecimalField('Фактическая площадь (м2)', max_digits=8, decimal_places=2) + + class Meta: + verbose_name = 'Выполненная работа' + verbose_name_plural = 'Выполненные работы' + + def __str__(self): + return f'{self.service} — {self.area} м2' + + def cost(self): + return self.area * self.service.price_per_sqm diff --git a/cleaning/templates/cleaning/base.html b/cleaning/templates/cleaning/base.html new file mode 100644 index 0000000..1ec4017 --- /dev/null +++ b/cleaning/templates/cleaning/base.html @@ -0,0 +1,23 @@ + + + + + + {% block title %}Клининг{% endblock %} + + + + +
{% comment %} mt-4 - mergin-top {% endcomment %} + {% block content %}{% endblock %} +
+ + \ No newline at end of file diff --git a/cleaning/templates/cleaning/brigade_detail.html b/cleaning/templates/cleaning/brigade_detail.html new file mode 100644 index 0000000..6b308d3 --- /dev/null +++ b/cleaning/templates/cleaning/brigade_detail.html @@ -0,0 +1,43 @@ +{% extends 'cleaning/base.html' %} + +{% block title %}О бригаде{% endblock %} + +{% block content %} +{# mb-4 — отступ снизу, чтобы карточка не прилипала к таблицам #} +
+

{{ brigade.name }}

+
+

Руководитель: {{ brigade.supervisor }}

+

Сотрудников: {{ brigade.employee_count }}

+

Специализация: {{ brigade.specialization }}

+
+
+ +
Активные заказы
+ + + + {% for order in active %} + + + + + + {% endfor %} + +
ЗаказКлиентДата
Заказ №{{ order.pk }}{{ order.client }}{{ order.date }}
+ +
Выполненные заказы
+ + + + {% for order in completed %} + + + + + + {% endfor %} + +
ЗаказКлиентДата
Заказ №{{ order.pk }}{{ order.client }}{{ order.date }}
+{% endblock %} diff --git a/cleaning/templates/cleaning/client_detail.html b/cleaning/templates/cleaning/client_detail.html new file mode 100644 index 0000000..6d74029 --- /dev/null +++ b/cleaning/templates/cleaning/client_detail.html @@ -0,0 +1,30 @@ +{% extends 'cleaning/base.html' %} + +{% block title %}О клиенте{% endblock %} + +{% block content %} +{# card — карточка с основной информацией о клиенте #} +
+

{{ client.full_name }}

+
+

Адрес: {{ client.address }}

+

Телефон: {{ client.phone }}

+ {# get_preferred_time_display — возвращает читаемое значение choices ('morning' → 'Утро') #} +

Предпочтительное время: {{ client.get_preferred_time_display }}

+
+
+ +
Заказы клиента
+ + + + {% for order in client.order_set.all %} + + + + + + {% endfor %} + +
ЗаказБригадаДата
Заказ №{{ order.pk }}{{ order.brigade }}{{ order.date }}
+{% endblock %} diff --git a/cleaning/templates/cleaning/order_detail.html b/cleaning/templates/cleaning/order_detail.html new file mode 100644 index 0000000..ec7f171 --- /dev/null +++ b/cleaning/templates/cleaning/order_detail.html @@ -0,0 +1,36 @@ +{% extends 'cleaning/base.html' %} + +{% block title %}Детали заказа{% endblock %} + +{% block content %} +{# card — Bootstrap-компонент для группировки информации #} +
+

Заказ №{{ order.pk }}

+
+

Клиент: {{ order.client }}

+

Бригада: {{ order.brigade }}

+

Дата: {{ order.date }}

+

Время: {{ order.start_time }} — {{ order.end_time }}

+
+
+ +
Выполненные работы
{# mt-4 — отступ сверху #} + + + + + + {% for work in order.completedwork_set.all %} + + + + + + {% endfor %} + + {# tfoot с table-group-divider — отделяет итоговую строку чертой #} + + + +
УслугаПлощадь, м²Стоимость, руб.
{{ work.service.name }}{{ work.area }}{{ work.cost }}
Итого{{ order.total_cost }} руб.
+{% endblock %} diff --git a/cleaning/templates/cleaning/order_list.html b/cleaning/templates/cleaning/order_list.html new file mode 100644 index 0000000..c1d997d --- /dev/null +++ b/cleaning/templates/cleaning/order_list.html @@ -0,0 +1,31 @@ +{% extends 'cleaning/base.html' %} + +{% block title %}Заказы{% endblock %} + +{% block content %} +

Список заказов

+{# table-striped — чередование цвета строк, table-hover — подсветка при наведении #} + + + + + + + + + + + + {% for order in orders %} + + + {# ссылки ведут на детали клиента, бригады и заказа #} + + + + + + {% endfor %} + +
КлиентБригадаДеталиДата
{{ forloop.counter }}{{ order.client }}{{ order.brigade }}Заказ №{{ order.pk }}{{ order.date }}
+{% endblock %} diff --git a/cleaning/tests.py b/cleaning/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/cleaning/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/cleaning/urls.py b/cleaning/urls.py new file mode 100644 index 0000000..2223048 --- /dev/null +++ b/cleaning/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from . import views + +app_name = 'cleaning' + +urlpatterns = [ + path('', views.index, name='index'), + path('orders/', views.order_list, name='order_list'), + path('order//', views.order_detail, name='order_detail'), + path('brigades//', views.brigade_detail, name='brigade_detail'), + path('clients//', views.client_detail, name='client_detail'), + +] diff --git a/cleaning/views.py b/cleaning/views.py new file mode 100644 index 0000000..48e459d --- /dev/null +++ b/cleaning/views.py @@ -0,0 +1,34 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.http import HttpResponse +from django.utils import timezone +from .models import Brigade, Order, Client + +def index(request): + return redirect('cleaning:order_list') + +def order_list(request): + orders = Order.objects.order_by('date') + return render(request, 'cleaning/order_list.html', {'orders': orders}) + +def order_detail(request, order_id): + order = get_object_or_404(Order.objects.prefetch_related('completedwork_set__service'), pk=order_id) + return render(request, 'cleaning/order_detail.html', {'order': order}) + +# def brigade_detail(request, pk): +# brigade = get_object_or_404(Brigade, pk=pk) +# return render(request, 'cleaning/brigade_detail.html', {'brigade': brigade}) +def brigade_detail(request, pk): + brigade = get_object_or_404(Brigade, pk=pk) + today = timezone.now().date() + active = brigade.orders.filter(date__gte=today).order_by('date') # type: ignore + completed = brigade.orders.filter(date__lt=today).order_by('-date') # type: ignore + return render(request, 'cleaning/brigade_detail.html', { + 'brigade': brigade, + 'active': active, + 'completed': completed, + }) + +def client_detail(request, pk): + client = get_object_or_404(Client, pk=pk) + orders = client.orders.order_by('date') # type: ignore + return render(request, 'cleaning/client_detail.html', {'client': client}) diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..ffbb5f5 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config 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', 'config.settings') + +application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..77c01cc --- /dev/null +++ b/config/settings.py @@ -0,0 +1,118 @@ +""" +Django settings for config 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 + +# 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/6.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'django-insecure-^*a-kotmr&dhhk))s6vyb2omn24l)llm)e0&0+invb7d@r3b3^' + +# 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', + 'cleaning', +] + +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 = 'config.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 = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/6.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } +} + + +# 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 = 'Europe/Moscow' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/6.0/howto/static-files/ + +STATIC_URL = 'static/' diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..54d134b --- /dev/null +++ b/config/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import include, path + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('cleaning.urls')), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..4ced574 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config 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', 'config.settings') + +application = get_wsgi_application() diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..8e7ac79 --- /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', 'config.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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a47f77f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "project-web" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "django>=6.0.5", + "faker>=37.1.0", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..a91741d --- /dev/null +++ b/uv.lock @@ -0,0 +1,71 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "django" +version = "6.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f1/bf85f0d29ef76abf901f193fe8fef4769d3da7794197832bc30151c071d8/django-6.0.5.tar.gz", hash = "sha256:bc6d6872e98a2864c836e42edd644b362db311147dd5aa8d5b82ba7a032f5269", size = 10924131, upload-time = "2026-05-05T13:54:39.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5b/1328f8b84fce040c404f76822bf8c57d254e368e8cbd8bd67ec2b26d75f5/django-6.0.5-py3-none-any.whl", hash = "sha256:9d58a7cb49244e74c8e161d5e403a46d6209f1009ba40f5a66d6aa0d0786a8f0", size = 8368680, upload-time = "2026-05-05T13:54:33.532Z" }, +] + +[[package]] +name = "faker" +version = "40.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" }, +] + +[[package]] +name = "project-web" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "faker" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=6.0.5" }, + { name = "faker", specifier = ">=37.1.0" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +]