diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e7c4d0d --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +SECRET_KEY=django-insecure-change-me-in-production-!@#$%^&*() diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1960fd8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,58 @@ +# ServiceCRM Copilot Instructions + +## Project Overview +ServiceCRM is a simple Service Ticket Management System built with Django. It manages repair tickets ("Inserts") for service customers, tracking details like customer info, defect description, repair status, and notes. + +## Architecture +- **Framework**: Django 4.x (compatible with 5.x/6.x in dependencies). +- **Structure**: Single-app project where the `serviceCRM` package contains both project settings and application logic (views, models, urls). +- **Patterns**: Model-View-Template (MVT). +- **Frontend**: Server-side rendered templates using Bootstrap 5 and `django-crispy-forms`. + +## Key Components + +### Data Model +- **Primary Model**: `Insert` (defined in `serviceCRM/models.py`). + - Represents a service ticket. + - Key fields: `name`, `phone`, `description` (defect), `repair` (resolution), `done` (status), `date`. + +### Views & Controllers +- **Location**: `serviceCRM/views.py`. +- **Pattern**: Mix of Class-Based Views (CBV) and Function-Based Views (FBV). + - List views use `django_tables2.SingleTableView`. + - Create/Update use `generic.View` and `generic.UpdateView`. + - **Note**: Some views use a non-standard static method pattern (e.g., `InsertNew.insert` referenced in URLs). + +### UI/UX +- **Tables**: `django-tables2` is used for listing records (`serviceCRM/tables.py`). +- **Forms**: `django-crispy-forms` with `crispy-bootstrap5` pack (`serviceCRM/forms.py`). +- **Templates**: Located in `templates/` and `templates/serviceCRM/`. + +## Configuration & Environment +- **Settings**: `serviceCRM/settings.py` uses `django-environ`. +- **Environment**: Configuration reads from `.env` file (see `.env.example`). +- **Database**: + - Local/Dev: SQLite (configured via `settings.py` overriding env vars if needed). + - Production: Configurable via `DATABASE_URL` or individual env vars (PostgreSQL supported). + +## Development Workflow + +### Setup +1. Create `.env` from `.env.example`. +2. Install dependencies: `pip install -r requirements.txt`. +3. Run migrations: `python manage.py migrate`. + +### Common Commands +- **Run Server**: `python manage.py runserver` +- **Make Migrations**: `python manage.py makemigrations` (Required when changing `models.py`) +- **Migrate**: `python manage.py migrate` + +## Coding Conventions +- **Imports**: standard library -> third party -> django -> local apps. +- **Urls**: Defined in `serviceCRM/urls.py` (which is the root URLconf). +- **Routing**: Routes are mixed between CBV (`.as_view()`) and function references. Maintain consistency with existing patterns when adding new routes. + +## Critical Patterns to Respect +1. **Forms**: Always use `InputForm` in `serviceCRM/forms.py` for ticket creation/editing to ensure consistent widget rendering with Bootstrap classes. +2. **Tables**: When adding lists, subclass `tables.Table` in `serviceCRM/tables.py` and use `SingleTableView` for display/sorting features. +3. **Template Inheritance**: All templates should extend `base.html`. diff --git a/.gitignore b/.gitignore index 287780c..c322c6a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ .idea/ # Ignore __pycache__ folders and .pyc files -__pycache__/ +*__pycache__/ *.pyc # Ignore .env files @@ -14,7 +14,9 @@ __pycache__/ # Ignore .DS_Store files .DS_Store -.vscode\ +.vscode + +db.sqlite3 pyvenv.cfg diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..6324d40 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14 diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 99cf7ae..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python Debugger: Django", - "type": "debugpy", - "request": "launch", - "program": "${workspaceFolder}\\manage.py", - "args": [ - "runserver" - ], - "django": true, - "autoStartBrowser": false - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index b1eb010..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "cmake.configureOnOpen": false, - "python.pythonPath": ".venv\\Scripts\\python.exe" -} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35d8c3b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# Django Backend Dockerfile +FROM ghcr.io/astral-sh/uv:python3.13-alpine AS builder + +WORKDIR /app + +ENV UV_COMPILE_BYTECODE=1 + +COPY pyproject.toml uv.lock ./ + +RUN uv sync --frozen --no-dev --no-install-project + +COPY serviceCRM/ ./serviceCRM/ +COPY manage.py ./ + +RUN uv sync --frozen --no-dev + + +FROM python:3.13-alpine + +WORKDIR /app + +RUN apk add --no-cache postgresql-client + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/serviceCRM/ /app/serviceCRM/ +COPY --from=builder /app/manage.py /app/ + +RUN adduser -D -u 1000 appuser && \ + chown -R appuser:appuser /app + +USER appuser + +ENV PATH="/app/.venv/bin:$PATH" +ENV PYTHONUNBUFFERED=1 + +EXPOSE 8000 + +CMD ["python", "-m", "uvicorn", "serviceCRM.asgi:application", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b81bec6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3.8' + +services: + web: + build: . + volumes: + - .:/app + ports: + - "8000:8000" + env_file: + - .env + environment: + - DEBUG=False + - ALLOWED_HOSTS=* diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..960dd47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,35 @@ +[project] +name = "servicecrm" +version = "0.1.0" +description = "A CRM application built with Django." +readme = "README.md" +requires-python = ">=3.14" +dependencies = [ + "asgiref>=3.11.0", + "cffi>=2.0.0", + "crispy-bootstrap5>=2025.6", + "crispy-tailwind>=1.0.3", + "cryptography>=46.0.3", + "django>=6.0.1", + "django-crispy-forms>=2.5", + "django-datatable-view>=2.1.6", + "django-environ>=0.12.0", + "django-filter>=25.2", + "django-pipeline>=4.1.0", + "django-tables2>=2.8.0", + "dnspython>=2.8.0", + "feedparser>=6.0.12", + "nanoid>=2.0.0", + "pillow>=12.1.0", + "publicsuffix>=1.1.1", + "pycparser>=2.23", + "python-dateutil>=2.9.0.post0", + "pytz>=2025.2", + "sgmllib3k>=1.0.0", + "six>=1.17.0", + "sqlparse>=0.5.5", + "tablib>=3.9.0", + "typing-extensions>=4.15.0", + "tzdata>=2025.3", + "uvicorn>=0.40.0", +] diff --git a/requirements.txt b/requirements.txt index b51db47..4aa0eb6 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/serviceCRM/filter.py b/serviceCRM/filter.py index 2bd3373..2ea1fe9 100644 --- a/serviceCRM/filter.py +++ b/serviceCRM/filter.py @@ -1,6 +1,27 @@ from .models import Insert import django_filters as filters from django_filters import FilterSet +from django.db.models import Q +from django import forms + +class InsertFilter(FilterSet): + start_date = filters.DateFilter(field_name="date", lookup_expr='gte', label='Од датум', widget=forms.TextInput(attrs={'type': 'date'})) + end_date = filters.DateFilter(field_name="date", lookup_expr='lte', label='До датум', widget=forms.TextInput(attrs={'type': 'date'})) + search = filters.CharFilter(method='filter_search', label='Пребарај (Име, Тел, ID, Опис)') + + class Meta: + model = Insert + fields = ['search', 'done'] + + def filter_search(self, queryset, name, value): + return queryset.filter( + Q(name__icontains=value) | + Q(phone__icontains=value) | + Q(description__icontains=value) | + Q(plateno__icontains=value) | + Q(ticket_id__icontains=value) | + Q(id__icontains=value) + ) class DoneTable(FilterSet): class Meta: @@ -11,4 +32,22 @@ class DoneTable(FilterSet): if value: return queryset.filter(done=True) else: - return queryset.filter(done=False) \ No newline at end of file + return queryset.filter(done=False) +class DoneFilter(FilterSet): + start_date = filters.DateFilter(field_name="date_close", lookup_expr='gte', label='Од датум (Затворено)', widget=forms.TextInput(attrs={'type': 'date'})) + end_date = filters.DateFilter(field_name="date_close", lookup_expr='lte', label='До датум (Затворено)', widget=forms.TextInput(attrs={'type': 'date'})) + search = filters.CharFilter(method='filter_search', label='Пребарај (Име, Тел, ID)') + + class Meta: + model = Insert + fields = ['status'] + + def filter_search(self, queryset, name, value): + return queryset.filter( + Q(name__icontains=value) | + Q(phone__icontains=value) | + Q(description__icontains=value) | + Q(plateno__icontains=value) | + Q(ticket_id__icontains=value) | + Q(id__icontains=value) + ) diff --git a/serviceCRM/forms.py b/serviceCRM/forms.py index 76a8427..ed8c35f 100644 --- a/serviceCRM/forms.py +++ b/serviceCRM/forms.py @@ -1,4 +1,5 @@ from django import forms +from django.utils.translation import gettext_lazy as _ from .models import Insert @@ -9,17 +10,31 @@ class DateInput(forms.DateInput): class InputForm(forms.ModelForm): class Meta: model = Insert - fields = {"name", "phone", "description", "date", "note"} - labels = {'name': "Name", 'phone': "Phone", 'date': "Date", 'description': "Description", 'note': "Note"} + fields = ["name", "phone", "date", "description", "note"] + labels = { + 'name': "Име", + 'phone': "Телефон", + 'date': "Датум на прием", + 'description': "Опис на проблем", + 'note': "Забелешка" + } widgets = { - 'name': forms.TextInput(attrs={'class': 'form-control'}), - 'phone': forms.TextInput(attrs={'class': 'form-control'}), 'date': DateInput(), - 'description': forms.Textarea(attrs={'class': 'form-control'}), - 'note': forms.TextInput(attrs={'class': 'form-control'}) + 'description': forms.Textarea(attrs={'rows': 3}), } - field_order = ["name", "phone", "date", "description", "done"] +class CloseForm(forms.ModelForm): + class Meta: + model = Insert + fields = ["repair", "plateno"] + labels = { + 'repair': "Детали за поправка", + 'plateno': "Наплатено" + } + widgets = { + 'repair': forms.Textarea(attrs={'rows': 4}), + 'plateno': forms.TextInput(attrs={'placeholder': 'пр. 1500 МКД'}) + } # class EditForm(forms.ModelForm): # class Meta: diff --git a/serviceCRM/migrations/0006_insert_date_close.py b/serviceCRM/migrations/0006_insert_date_close.py new file mode 100644 index 0000000..ad850d6 --- /dev/null +++ b/serviceCRM/migrations/0006_insert_date_close.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-01-08 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('serviceCRM', '0005_alter_insert_done'), + ] + + operations = [ + migrations.AddField( + model_name='insert', + name='date_close', + field=models.DateField(blank=True, null=True, verbose_name='date closed'), + ), + ] diff --git a/serviceCRM/migrations/0007_insert_status.py b/serviceCRM/migrations/0007_insert_status.py new file mode 100644 index 0000000..d96ea6f --- /dev/null +++ b/serviceCRM/migrations/0007_insert_status.py @@ -0,0 +1,18 @@ +# Generated by Django 6.0.1 on 2026-01-08 11:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('serviceCRM', '0006_insert_date_close'), + ] + + operations = [ + migrations.AddField( + model_name='insert', + name='status', + field=models.CharField(choices=[('RECEIVED', 'Received'), ('DIAGNOSING', 'Diagnosing'), ('WAITING_PARTS', 'Waiting for Parts'), ('READY', 'Ready for Pickup'), ('COMPLETED', 'Completed')], default='RECEIVED', max_length=20), + ), + ] diff --git a/serviceCRM/migrations/0008_insert_ticket_id.py b/serviceCRM/migrations/0008_insert_ticket_id.py new file mode 100644 index 0000000..0f7adbf --- /dev/null +++ b/serviceCRM/migrations/0008_insert_ticket_id.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-01-08 11:30 + +import serviceCRM.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('serviceCRM', '0007_insert_status'), + ] + + operations = [ + migrations.AddField( + model_name='insert', + name='ticket_id', + field=models.CharField(default=serviceCRM.models.generate_nanoid, max_length=12, null=True), + ), + ] diff --git a/serviceCRM/migrations/0009_alter_insert_ticket_id.py b/serviceCRM/migrations/0009_alter_insert_ticket_id.py new file mode 100644 index 0000000..f0ba1b2 --- /dev/null +++ b/serviceCRM/migrations/0009_alter_insert_ticket_id.py @@ -0,0 +1,19 @@ +# Generated by Django 6.0.1 on 2026-01-08 11:32 + +import serviceCRM.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('serviceCRM', '0008_insert_ticket_id'), + ] + + operations = [ + migrations.AlterField( + model_name='insert', + name='ticket_id', + field=models.CharField(default=serviceCRM.models.generate_nanoid, editable=False, max_length=12, unique=True), + ), + ] diff --git a/serviceCRM/migrations/0010_ticketlog.py b/serviceCRM/migrations/0010_ticketlog.py new file mode 100644 index 0000000..49b1f95 --- /dev/null +++ b/serviceCRM/migrations/0010_ticketlog.py @@ -0,0 +1,27 @@ +# Generated by Django 6.0.1 on 2026-01-08 13:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('serviceCRM', '0009_alter_insert_ticket_id'), + ] + + operations = [ + migrations.CreateModel( + name='TicketLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('action', models.CharField(max_length=50)), + ('details', models.TextField(blank=True, null=True)), + ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='serviceCRM.insert')), + ], + options={ + 'ordering': ['-timestamp'], + }, + ), + ] diff --git a/serviceCRM/migrations/0011_alter_insert_date_alter_insert_date_close_and_more.py b/serviceCRM/migrations/0011_alter_insert_date_alter_insert_date_close_and_more.py new file mode 100644 index 0000000..ae4f021 --- /dev/null +++ b/serviceCRM/migrations/0011_alter_insert_date_alter_insert_date_close_and_more.py @@ -0,0 +1,63 @@ +# Generated by Django 6.0.1 on 2026-01-08 14:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('serviceCRM', '0010_ticketlog'), + ] + + operations = [ + migrations.AlterField( + model_name='insert', + name='date', + field=models.DateField(verbose_name='Датум'), + ), + migrations.AlterField( + model_name='insert', + name='date_close', + field=models.DateField(blank=True, null=True, verbose_name='Датум затворање'), + ), + migrations.AlterField( + model_name='insert', + name='description', + field=models.CharField(max_length=300, verbose_name='Опис'), + ), + migrations.AlterField( + model_name='insert', + name='done', + field=models.BooleanField(default=False, verbose_name='Завршено'), + ), + migrations.AlterField( + model_name='insert', + name='name', + field=models.CharField(max_length=50, verbose_name='Име'), + ), + migrations.AlterField( + model_name='insert', + name='note', + field=models.CharField(blank=True, default=None, max_length=100, null=True, verbose_name='Забелешка'), + ), + migrations.AlterField( + model_name='insert', + name='phone', + field=models.CharField(max_length=20, verbose_name='Телефон'), + ), + migrations.AlterField( + model_name='insert', + name='plateno', + field=models.CharField(blank=True, default=None, max_length=10, null=True, verbose_name='Плаќање/Рег.'), + ), + migrations.AlterField( + model_name='insert', + name='repair', + field=models.CharField(blank=True, default=None, max_length=300, null=True, verbose_name='Поправка'), + ), + migrations.AlterField( + model_name='insert', + name='status', + field=models.CharField(choices=[('RECEIVED', 'Примено'), ('DIAGNOSING', 'Дијагностика'), ('WAITING_PARTS', 'Чека делови'), ('READY', 'Готово за подигање'), ('COMPLETED', 'Завршено')], default='RECEIVED', max_length=20, verbose_name='Статус'), + ), + ] diff --git a/serviceCRM/models.py b/serviceCRM/models.py index d615e72..731ce2c 100644 --- a/serviceCRM/models.py +++ b/serviceCRM/models.py @@ -1,19 +1,63 @@ from django.db import models from django.contrib.auth.models import UserManager +from nanoid import generate +def generate_nanoid(): + return generate(size=12) class Insert(models.Model): - name = models.CharField(max_length=50) - phone = models.CharField(max_length=20) - description = models.CharField(max_length=300) - note= models.CharField(max_length=100, default=None, blank=True, null=True) - date = models.DateField("date submitted") - done = models.BooleanField(default=False) - repair = models.CharField(default=None, blank=True, null=True,max_length=300) - plateno = models.CharField(max_length=10, default=None, blank=True, null=True) + ticket_id = models.CharField(max_length=12, default=generate_nanoid, unique=True, editable=False) + name = models.CharField(max_length=50, verbose_name="Име") + phone = models.CharField(max_length=20, verbose_name="Телефон") + description = models.CharField(max_length=300, verbose_name="Опис") + note= models.CharField(max_length=100, default=None, blank=True, null=True, verbose_name="Забелешка") + date = models.DateField("Датум") + done = models.BooleanField(default=False, verbose_name="Завршено") + + STATUS_CHOICES = [ + ('RECEIVED', 'Примено'), + ('DIAGNOSING', 'Дијагностика'), + ('WAITING_PARTS', 'Чека делови'), + ('READY', 'Готово за подигање'), + ('COMPLETED', 'Завршено'), + ] + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='RECEIVED', verbose_name="Статус") + + date_close = models.DateField("Датум затворање", null=True, blank=True) + repair = models.CharField(default=None, blank=True, null=True,max_length=300, verbose_name="Поправка") + plateno = models.CharField(max_length=10, default=None, blank=True, null=True, verbose_name="Плаќање/Рег.") + + def save(self, *args, **kwargs): + from django.utils import timezone + + # Sync done and status fields + if self.status == 'COMPLETED': + self.done = True + if not self.date_close: + self.date_close = timezone.now().date() + elif self.done: + self.status = 'COMPLETED' + if not self.date_close: + self.date_close = timezone.now().date() + else: + self.done = False + + super(Insert, self).save(*args, **kwargs) def __str__(self): return "Ime: " + self.name + " Telefonski broj: " + self.phone + "\nDefekt: " + self.description + "\nDatum: \n" def isDone(self): return self.done + +class TicketLog(models.Model): + ticket = models.ForeignKey(Insert, on_delete=models.CASCADE, related_name='logs') + timestamp = models.DateTimeField(auto_now_add=True) + action = models.CharField(max_length=50) + details = models.TextField(blank=True, null=True) + + class Meta: + ordering = ['-timestamp'] + + def __str__(self): + return f"{self.ticket.ticket_id} - {self.action} at {self.timestamp}" diff --git a/serviceCRM/settings.py b/serviceCRM/settings.py index 1d319a5..f6c684b 100644 --- a/serviceCRM/settings.py +++ b/serviceCRM/settings.py @@ -25,9 +25,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = env('SECRET_KEY') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = env.bool('DEBUG', default=False) -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['*']) # Application definition @@ -39,22 +39,29 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', + 'whitenoise.runserver_nostatic', # Add whitenoise before staticfiles 'django.contrib.staticfiles', 'crispy_forms', - 'crispy_bootstrap5', + 'crispy_tailwind', 'django_tables2', + 'django_filters', ] -CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" -CRISPY_TEMPLATE_PACK = "bootstrap5" +CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind" +CRISPY_TEMPLATE_PACK = "tailwind" DJANGO_TABLES2_TABLE_ATTRS = { - 'class': 'table table-hover', + 'class': 'min-w-full divide-y divide-gray-200 border', 'thead': { - 'class': 'table-light', + 'class': 'bg-gray-50', + }, + 'tbody': { + 'class': 'bg-white divide-y divide-gray-200', }, } MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', # Add whitenoise middleware 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -88,12 +95,8 @@ WSGI_APPLICATION = 'serviceCRM.wsgi.application' DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': env('DATABASE_NAME'), - 'USER': env('DATABASE_USER'), - 'PASSWORD': env('DATABASE_PASS'), - 'HOST': env('DATABASE_HOST'), - 'PORT': env('DATABASE_PORT'), + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', } } @@ -120,9 +123,20 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = 'mk' -TIME_ZONE = 'UTC' +from django.utils.translation import gettext_lazy as _ + +LANGUAGES = [ + ('mk', _('Macedonian')), + ('en', _('English')), +] + +LOCALE_PATHS = [ + BASE_DIR / 'locale', +] + +TIME_ZONE = 'Europe/Skopje' USE_I18N = True @@ -133,8 +147,13 @@ USE_TZ = True # https://docs.djangoproject.com/en/4.2/howto/static-files/ STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / 'staticfiles' +STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage' # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +LOGIN_REDIRECT_URL = '/' +LOGIN_URL = '/admin/login/' diff --git a/serviceCRM/tables.py b/serviceCRM/tables.py index c5ce725..01f8e2e 100644 --- a/serviceCRM/tables.py +++ b/serviceCRM/tables.py @@ -6,18 +6,24 @@ from .models import Insert class InsertTable(tables.Table): - actions = TemplateColumn(template_code='Edit Details') + actions = TemplateColumn(template_code=''' + Види + Уреди + {% if not record.done %} + Затвори + {% endif %} + ''') class Meta: model = Insert - fields = ("id","name","phone","description","date","done") + fields = ("ticket_id","name","phone","description","date","status","done") per_page = 5 class DoneInsertTable(InsertTable): class Meta: model = Insert - fields = ("id","name","phone","description","date","done") + fields = ("ticket_id","name","phone","description","date", "date_close", "done") per_page = 5 def __init__(self, *args, **kwargs): diff --git a/serviceCRM/templates/base.html b/serviceCRM/templates/base.html index 91e6f22..d29999c 100644 --- a/serviceCRM/templates/base.html +++ b/serviceCRM/templates/base.html @@ -1,43 +1,92 @@ - - - - - - Service - - - - - - - - - - -
- {% comment %}

Service CRM

- -
{% endcomment %} - -
-
- {% block content %} - {% endblock %} + +
+
+ {% if messages %} +
+ {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} +
+ {% endif %} + + {% block content %} + {% endblock %}
-
-
- + + \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/close_ticket.html b/serviceCRM/templates/serviceCRM/close_ticket.html new file mode 100644 index 0000000..b0a668f --- /dev/null +++ b/serviceCRM/templates/serviceCRM/close_ticket.html @@ -0,0 +1,34 @@ +{% extends 'base.html' %} +{% load crispy_forms_tags %} + +{% block title %}Затвори налог #{{ object.id }}{% endblock %} + +{% block content %} +
+
+
+

Затвори налог #{{ object.id }}

+

+ Завршување на поправка за {{ object.name }} ({{ object.description|truncatechars:50 }}) +

+ +
+ {% csrf_token %} + {{ form|crispy }} + +
+ + + Откажи + +
+
+
+
+
+{% endblock%} \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/done.html b/serviceCRM/templates/serviceCRM/done.html index 2414505..b14185e 100644 --- a/serviceCRM/templates/serviceCRM/done.html +++ b/serviceCRM/templates/serviceCRM/done.html @@ -1,6 +1,31 @@ {% extends 'base.html' %} {% load render_table from django_tables2 %} +{% load crispy_forms_tags %} {% block content %} - {% render_table table %} +
+
+

Пребарување Архива

+
+
+ {{ filter.form.start_date|as_crispy_field }} +
+
+ {{ filter.form.end_date|as_crispy_field }} +
+
+ {{ filter.form.search|as_crispy_field }} +
+
+ +
+
+
+
+ +
+ {% render_table table %} +
{%endblock%} \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/edit.html b/serviceCRM/templates/serviceCRM/edit.html index f7b2e22..e583fa6 100644 --- a/serviceCRM/templates/serviceCRM/edit.html +++ b/serviceCRM/templates/serviceCRM/edit.html @@ -1,13 +1,17 @@ {% extends 'base.html' %} -{% block title %}Edit{% endblock %} +{% load crispy_forms_tags %} +{% block title %}Уреди{% endblock %} {% block content %} -{% csrf_token %} +
+
-
{% csrf_token %} - {{form.as_p}} - - + {{ form|crispy }} +
+ +
+
+
{% endblock%} \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/form.html b/serviceCRM/templates/serviceCRM/form.html index af443c0..69b0dd9 100644 --- a/serviceCRM/templates/serviceCRM/form.html +++ b/serviceCRM/templates/serviceCRM/form.html @@ -1,11 +1,16 @@ {% extends 'base.html' %} +{% load crispy_forms_tags %} + {% block content %} -{% csrf_token %} +
+
-
{% csrf_token %} - {{form.as_p}} - + {{ form|crispy }} +
+
+
+
{% endblock%} \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/list.html b/serviceCRM/templates/serviceCRM/list.html index bfdcac4..3b4ec4e 100644 --- a/serviceCRM/templates/serviceCRM/list.html +++ b/serviceCRM/templates/serviceCRM/list.html @@ -1,5 +1,57 @@ {% extends 'base.html' %} {% load render_table from django_tables2 %} +{% load crispy_forms_tags %} + {% block content %} - {% render_table table %} + +
+
+
+
Активни налози
+
{{ stats_active }}
+
+
+
+
+
Завршени денес
+
{{ stats_today_closed }}
+
+
+
+
+
Спремни за подигање
+
{{ stats_ready }}
+
+
+
+ +
+
+

Пребарај налози

+
+
+ {{ filter.form.start_date|as_crispy_field }} +
+
+ {{ filter.form.end_date|as_crispy_field }} +
+
+ {{ filter.form.search|as_crispy_field }} +
+ +
+ +
+
+
+
+ +
+ {% render_table table %} +
{% endblock%} \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/nalog.html b/serviceCRM/templates/serviceCRM/nalog.html index 2a88587..bd18cda 100644 --- a/serviceCRM/templates/serviceCRM/nalog.html +++ b/serviceCRM/templates/serviceCRM/nalog.html @@ -1,83 +1,107 @@ -{% block content %} +{% extends 'base.html' %} {% load static %} - - - - - Servicing Ticket - - - -
-
- Company Logo +{% block content %} +
+
+
+

Сервисен Налог #{{ data.ticket_id }}

+

Креиран на {{ data.date }} (Интерен ID: {{ data.id }})

-
-
- {{ name }} -
-
- {{phone}} -
-
- {{date}} -
-
- {{ desc }} -
-
-
-
-
- {{ name }} -
-
- {{ phone }} -
-
- {{ date }} -
-
- {{ desc }} -
+
- - +
+
+
+
Име на клиент
+
{{ data.name }}
+
+
+
Телефонски број
+
{{ data.phone }}
+
+
+
Опис на дефект
+
{{ data.description }}
+
+
+
Статус
+
{{ data.get_status_display }}
+
+ {% if data.note %} +
+
Интерна забелешка
+
{{ data.note }}
+
+ {% endif %} + {% if data.done %} +
+
Детали за поправка
+
{{ data.repair }}
+
+
+
Плаќање/Регистарски број
+
{{ data.plateno }}
+
+
+
Датум на затворање
+
{{ data.date_close }}
+
+ {% endif %} +
+
+ + +
+

Историја на активности

+
+
    + {% for log in data.logs.all %} +
  • +
    + {% if not forloop.last %} + + {% endif %} +
    +
    + + + + + +
    +
    +
    +

    {{ log.details }} ({{ log.action }})

    +
    +
    + +
    +
    +
    +
    +
  • + {% empty %} +
  • Нема историја.
  • + {% endfor %} +
+
+
+ + +
{% endblock %} diff --git a/serviceCRM/templates/serviceCRM/print_label.html b/serviceCRM/templates/serviceCRM/print_label.html new file mode 100644 index 0000000..5e0dac0 --- /dev/null +++ b/serviceCRM/templates/serviceCRM/print_label.html @@ -0,0 +1,41 @@ +{% load static %} + + + + + Налепница #{{ ticket.id }} + + + +
+ Logo +
+
{{ ticket.date }}
+ +
#{{ ticket.id }}
+
{{ ticket.ticket_id }}
+ +
{{ ticket.name }}
+
{{ ticket.phone }}
+ +
+ Дефект:
+ {{ ticket.description|truncatechars:50 }} +
+ + diff --git a/serviceCRM/templates/serviceCRM/print_receipt.html b/serviceCRM/templates/serviceCRM/print_receipt.html new file mode 100644 index 0000000..c7fb4c0 --- /dev/null +++ b/serviceCRM/templates/serviceCRM/print_receipt.html @@ -0,0 +1,184 @@ +{% load static %} + + + + + Потврда #{{ ticket.id }} + + + + +
+ + +
+ +
+
+ Logo +

СЕРВИСНА ПОТВРДА

+ Примерок за Клиент +
+
+
Налог #{{ ticket.id }}
+
Реф: {{ ticket.ticket_id }}
+
{{ ticket.date }}
+
{{ ticket.get_status_display }}
+
+
+ + +
+
+

Клиент

+

{{ ticket.name }}

+

{{ ticket.phone }}

+
+
+

Детали за сервис

+
+

Пријавен дефект:

+

{{ ticket.description }}

+ {% if ticket.plateno %} +

Проценка/Наплатено:

+

{{ ticket.plateno }}

+ {% endif %} +
+
+
+ + +
+

УСЛОВИ: Со потпис потврдувате дека сте согласни со условите. Сервисот не одговара за загуба на податоци. Дијагностика се наплаќа ако поправката е одбиена. Уреди неподигнати 30 дена по известувањето се отстрануваат.

+ + +
+
+

Потпис на клиент

+
+
+

Сервисер

+
+
+
+ +
+ + +
+ +
+
+ Logo +

СЕРВИСНА ПОТВРДА

+ Примерок за Сервис +
+
+
Налог #{{ ticket.id }}
+
Реф: {{ ticket.ticket_id }}
+
{{ ticket.date }}
+
{{ ticket.get_status_display }}
+
+
+ + +
+
+

Клиент

+

{{ ticket.name }}

+

{{ ticket.phone }}

+
+
+

Детали за сервис

+
+

Пријавен дефект:

+

{{ ticket.description }}

+ {% if ticket.plateno %} +

Проценка/Наплатено:

+

{{ ticket.plateno }}

+ {% endif %} +
+
+
+ + +
+

УСЛОВИ: Со потпис потврдувате дека сте согласни со условите. Сервисот не одговара за загуба на податоци. Дијагностика се наплаќа ако поправката е одбиена. Уреди неподигнати 30 дена по известувањето се отстрануваат.

+ + +
+
+

Потпис на клиент

+
+
+

Сервисер

+
+
+
+
+ +
+ + +
+ + +
+ + diff --git a/serviceCRM/templates/serviceCRM/public_track_form.html b/serviceCRM/templates/serviceCRM/public_track_form.html new file mode 100644 index 0000000..d57c6b0 --- /dev/null +++ b/serviceCRM/templates/serviceCRM/public_track_form.html @@ -0,0 +1,35 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

+ Проверка на статус +

+

+ Внесете го бројот на вашиот налог +

+
+ +
+
+
+
+ +
+ +
+
+ +
+ +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/serviceCRM/templates/serviceCRM/public_track_result.html b/serviceCRM/templates/serviceCRM/public_track_result.html new file mode 100644 index 0000000..324658c --- /dev/null +++ b/serviceCRM/templates/serviceCRM/public_track_result.html @@ -0,0 +1,89 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+

+ Статус на поправка +

+

+ Рефернца: {{ ticket.ticket_id }} +

+
+
+ + {% if ticket.done %} + ЗАВРШЕНО + {% else %} + ВО ИЗРАБОТКА + {% endif %} + +
+
+
+
+
+
+ Уред +
+
+ {{ ticket.name }} +
+
+
+
+ Дефект / Проблем +
+
+ {{ ticket.description }} +
+
+
+
+ Моментален статус +
+
+ {{ ticket.get_status_display }} +
+
+
+
+ Примен на +
+
+ {{ ticket.date }} +
+
+ {% if ticket.done %} +
+
+ Забелешки за сервисот +
+
+ {{ ticket.repair|default:"Нема забелешки." }} +
+
+
+
+ Завршено на +
+
+ {{ ticket.date_close }} +
+
+ {% endif %} +
+
+ +
+
+{% endblock %} \ No newline at end of file diff --git a/serviceCRM/urls.py b/serviceCRM/urls.py index ac3fe81..df379e1 100644 --- a/serviceCRM/urls.py +++ b/serviceCRM/urls.py @@ -19,13 +19,24 @@ from django.urls import path import serviceCRM.views as view urlpatterns = [ - path("", view.InsertListView.as_view(), name="index"), + path("", view.home, name="home"), + path("dashboard/", view.InsertListView.as_view(), name="dashboard"), path('admin/', admin.site.urls), path("insert/", view.InsertNew.insert, name="insert"), - path("edit//", view.Update.as_view(), name="update"), - path("nalog//", view.Nalog, name="nalog"), - path("delete//", view.Delete.delete, name="delete"), + path("logout/", view.logout_view, name="logout"), + path("track/", view.track_ticket, name="track_ticket"), + path("ticket//", view.Nalog, name="nalog"), + + # Secure public links + path("print/label//", view.print_label, name="print_label"), + path("print/receipt//", view.print_receipt, name="print_receipt"), + + # Secure staff actions + path("edit//", view.Update.as_view(), name="update"), + path("close//", view.CloseTicketView.as_view(), name="close_ticket"), + path("delete//", view.Delete.delete, name="delete"), + path("done/", view.Done.as_view(), name="done"), - path("done//", view.Done.done_by_id, name="done"), + path("done//", view.Done.done_by_id, name="done_detail"), path("datatable/", view.DatatableView.as_view(), name="datatable"), ] diff --git a/serviceCRM/views.py b/serviceCRM/views.py index 8dfa634..501932c 100644 --- a/serviceCRM/views.py +++ b/serviceCRM/views.py @@ -1,74 +1,144 @@ from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import get_object_or_404, render +from django.shortcuts import get_object_or_404, render, redirect from django.views import generic +from django.utils import timezone +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.decorators import login_required +from django.contrib.auth import logout -from .forms import InputForm -from .models import Insert +from .forms import InputForm, CloseForm +from .models import Insert, TicketLog from .tables import DoneInsertTable, InsertTable +from .filter import InsertFilter, DoneFilter -from django_tables2 import SingleTableView +from django_tables2.views import SingleTableMixin,SingleTableView +from django_filters.views import FilterView from datatableview.views import DatatableView -class InsertListView(SingleTableView): +class InsertListView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Insert table_class = InsertTable template_name = 'serviceCRM/list.html' + filterset_class = InsertFilter + + def get_queryset(self): + return Insert.objects.filter(done=False).order_by('-date', '-id') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + # Dashboard Widgets Data + context['stats_active'] = Insert.objects.filter(done=False).count() + context['stats_today_closed'] = Insert.objects.filter(done=True, date_close=timezone.now().date()).count() + context['stats_ready'] = Insert.objects.filter(status='READY', done=False).count() + return context class InsertNew(generic.View): model = Insert template_name = "serviceCRM/form.html" + @login_required def insert(request): if request.method == 'POST': form = InputForm(request.POST) if form.is_valid(): ticket = form.save() - print("Raboti") - return HttpResponseRedirect(f"/nalog/{ticket.id}/") + TicketLog.objects.create(ticket=ticket, action="Креирано", details="Налогот е креиран во системот.") + return HttpResponseRedirect(f"/ticket/{ticket.ticket_id}/") else: form = InputForm() return render(request, InsertNew.template_name, {'form': form}) -class Update(generic.UpdateView): +class Update(LoginRequiredMixin, generic.UpdateView): model = Insert template_name = "serviceCRM/edit.html" - fields = ["name", "phone", "description","note", "done", "repair", "plateno"] + fields = ["status", "name", "phone", "description","note", "repair", "plateno", "done"] success_url = '/' + slug_field = 'ticket_id' + slug_url_kwarg = 'ticket_id' + def form_valid(self, form): + response = super().form_valid(form) + if 'status' in form.changed_data: + TicketLog.objects.create(ticket=self.object, action="Промена на статус", details=f"Статусот е променет во {self.object.get_status_display()}") + else: + TicketLog.objects.create(ticket=self.object, action="Ажурирано", details="Деталите за налогот се ажурирани.") + return response -def Nalog(request, id): - try: - data = Insert.objects.get(id=id) - except: - return HttpResponseRedirect("/") - template = "serviceCRM/nalog.html" - context = {"name": data.name, "phone": data.phone, "desc": data.description, "date": data.date} - return render(request, template, context) - -class Done(SingleTableView): +class CloseTicketView(LoginRequiredMixin, generic.UpdateView): + model = Insert + form_class = CloseForm + template_name = "serviceCRM/close_ticket.html" + success_url = '/done/' + slug_field = 'ticket_id' + slug_url_kwarg = 'ticket_id' + + def form_valid(self, form): + form.instance.done = True + form.instance.date_close = timezone.now().date() + response = super().form_valid(form) + TicketLog.objects.create(ticket=self.object, action="Затворено", details="Налогот и поправката се завршени.") + return response + + +@login_required +def Nalog(request, ticket_id): + data = get_object_or_404(Insert, ticket_id=ticket_id) + template = "serviceCRM/nalog.html" + context = {"data": data} + return render(request, template, context) + +@login_required +def print_label(request, ticket_id): + ticket = get_object_or_404(Insert, ticket_id=ticket_id) + return render(request, 'serviceCRM/print_label.html', {'ticket': ticket}) + +@login_required +def print_receipt(request, ticket_id): + ticket = get_object_or_404(Insert, ticket_id=ticket_id) + return render(request, 'serviceCRM/print_receipt.html', {'ticket': ticket}) + +class Done(LoginRequiredMixin, SingleTableMixin, FilterView): model = Insert - table_data = Insert.objects.filter(done=True) table_class = DoneInsertTable template_name = 'serviceCRM/done.html' + filterset_class = DoneFilter - def done_by_id(request, id): - try: - req = get_object_or_404(Insert, id=id) - except: - return HttpResponseRedirect("/done/") - context = {"name": req.name, "phone": req.phone, "desc": req.description, "date": req.date} - return HttpResponse(f"Report ID: {id} \nName: {req.name} \nPhone: {req.phone} \nDescription: {req.description} \n Note:{req.note} \nDate: {req.date} \nDone: {req.done} \nRepair: {req.repair} \n Plateno: {req.plateno} \n") + def get_queryset(self): + return Insert.objects.filter(done=True) + + @login_required + def done_by_id(request, ticket_id): + req = get_object_or_404(Insert, ticket_id=ticket_id) + return HttpResponse(f"Report ID: {ticket_id} \nName: {req.name} \nPhone: {req.phone} \nDescription: {req.description} \n Note:{req.note} \nDate: {req.date} \nDone: {req.done} \nRepair: {req.repair} \n Plateno: {req.plateno} \n") class Delete(generic.View): model = Insert - def delete(request, id): - req = get_object_or_404(Insert, id=id) + @login_required + def delete(request, ticket_id): + req = get_object_or_404(Insert, ticket_id=ticket_id) req.delete() return HttpResponseRedirect("/") -class DatatableView(DatatableView): +class DatatableView(LoginRequiredMixin, DatatableView): model = Insert template_name = 'serviceCRM/Insert_list.html' - \ No newline at end of file +def track_ticket(request): + if request.method == 'GET' and 'ticket_id' in request.GET: + ticket_id = request.GET.get('ticket_id').strip() + try: + ticket = Insert.objects.get(ticket_id=ticket_id) + return render(request, 'serviceCRM/public_track_result.html', {'ticket': ticket}) + except Insert.DoesNotExist: + return render(request, 'serviceCRM/public_track_form.html', {'error': 'Налогот не е пронајден. Проверете го бројот на налогот.'}) + + return render(request, 'serviceCRM/public_track_form.html') +def logout_view(request): + logout(request) + return redirect('/') + +def home(request): + if request.user.is_authenticated: + return redirect('dashboard') + return track_ticket(request) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..837b1e6 --- /dev/null +++ b/uv.lock @@ -0,0 +1,488 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "asgiref" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/4db2509eabd14b4a8c71d1b24c8d5734c52b8560a7b1e1a8b56c8d25568b/asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4", size = 37969, upload-time = "2025-11-19T15:32:20.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/be/317c2c55b8bbec407257d45f5c8d1b6867abc76d12043f2d3d58c538a4ea/asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d", size = 24096, upload-time = "2025-11-19T15:32:19.004Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "crispy-bootstrap5" +version = "2025.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-crispy-forms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/30/36cc4144b6dff91bb54490a3b474897b7469bcda9517bf9f54681ea91011/crispy_bootstrap5-2025.6.tar.gz", hash = "sha256:f1bde7cac074c650fc82f31777d4a4cfd0df2512c68bc4128f259c75d3daada4", size = 23950, upload-time = "2025-06-08T07:43:35.461Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/d4/8cf1ba773a91fc17bab1fd46b75bbdef780c4cccbbb8230e617980a0362c/crispy_bootstrap5-2025.6-py3-none-any.whl", hash = "sha256:a343aa128b4383f35f00295b94de2b10862f2a4f24eda21fa6ead45234c07050", size = 24794, upload-time = "2025-06-08T07:43:34.206Z" }, +] + +[[package]] +name = "crispy-tailwind" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "django-crispy-forms" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/ac/a307ae5ce869d7151b90d4b8b042a48eb454a936dacc695f6418486e5bd8/crispy-tailwind-1.0.3.tar.gz", hash = "sha256:2bc9f616d406e4b003f25d46fcb0079f1c2522719d97adb107667271d849459a", size = 19172, upload-time = "2024-02-13T09:52:37.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/ca/11f65e24f3c182dfaf90fd3710d2dcca0fbc3026923e47b43f52a4a2349b/crispy_tailwind-1.0.3-py3-none-any.whl", hash = "sha256:31427f66b1c4fd0d6fb040f4197cfb97d104cdbe7641ea2dea940c0057c4db4b", size = 25700, upload-time = "2024-02-13T09:52:35.928Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + +[[package]] +name = "django" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/9b/016f7e55e855ee738a352b05139d4f8b278d0b451bd01ebef07456ef3b0e/django-6.0.1.tar.gz", hash = "sha256:ed76a7af4da21551573b3d9dfc1f53e20dd2e6c7d70a3adc93eedb6338130a5f", size = 11069565, upload-time = "2026-01-06T18:55:53.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/b5/814ed98bd21235c116fd3436a7ed44d47560329a6d694ec8aac2982dbb93/django-6.0.1-py3-none-any.whl", hash = "sha256:a92a4ff14f664a896f9849009cb8afaca7abe0d6fc53325f3d1895a15253433d", size = 8338791, upload-time = "2026-01-06T18:55:46.175Z" }, +] + +[[package]] +name = "django-crispy-forms" +version = "2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/a1/6a638d13717e4d4f8df169dade0fa51bdc65d9825df39d98ce709a776b49/django_crispy_forms-2.5.tar.gz", hash = "sha256:066e72a8f152a1334f1c811cc37740868efe3265e5a218f79079ef89f848c3d8", size = 1097999, upload-time = "2025-11-06T20:44:01.921Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ac3a11950baaf75c1f3242e3af9dfe45201f6ee10c113dd37a9c000876d2/django_crispy_forms-2.5-py3-none-any.whl", hash = "sha256:adc99d5901baca09479c53bf536b3909e80a9f2bb299438a223de4c106ebf1f9", size = 31464, upload-time = "2025-11-06T20:44:00.795Z" }, +] + +[[package]] +name = "django-datatable-view" +version = "2.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/cc/f26f6bf8cf70110fe40d86039ac01a6f407260eca1ea67ad56e529a39071/django-datatable-view-2.1.6.tar.gz", hash = "sha256:b84ba6809b35b1d018eb502b3fed5b9dec219ed87120ee54f04a601551a341af", size = 50474, upload-time = "2021-05-28T21:21:45.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/ae/026ee8aee1ad6b3d06407cccb2c3036d47a6b319b5b03986d221059e48bf/django_datatable_view-2.1.6-py2.py3-none-any.whl", hash = "sha256:74b4ac9e99ebba50f5558b62434a21fa28c681b0c9f40b875c96ff497aeba11b", size = 59862, upload-time = "2021-05-28T21:21:43.278Z" }, +] + +[[package]] +name = "django-environ" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/04/65d2521842c42f4716225f20d8443a50804920606aec018188bbee30a6b0/django_environ-0.12.0.tar.gz", hash = "sha256:227dc891453dd5bde769c3449cf4a74b6f2ee8f7ab2361c93a07068f4179041a", size = 56804, upload-time = "2025-01-13T17:03:37.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl", hash = "sha256:92fb346a158abda07ffe6eb23135ce92843af06ecf8753f43adf9d2366dcc0ca", size = 19957, upload-time = "2025-01-13T17:03:32.918Z" }, +] + +[[package]] +name = "django-filter" +version = "25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/e4/465d2699cd388c0005fb8d6ae6709f239917c6d8790ac35719676fffdcf3/django_filter-25.2.tar.gz", hash = "sha256:760e984a931f4468d096f5541787efb8998c61217b73006163bf2f9523fe8f23", size = 143818, upload-time = "2025-10-05T09:51:31.521Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl", hash = "sha256:9c0f8609057309bba611062fe1b720b4a873652541192d232dd28970383633e3", size = 94145, upload-time = "2025-10-05T09:51:29.728Z" }, +] + +[[package]] +name = "django-pipeline" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, + { name = "wheel" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/7b/2f78ccdad9f45a3d4f709950160f9d7e5009b2b8bec8ef636025ec89b62e/django_pipeline-4.1.0.tar.gz", hash = "sha256:aa1d79df6f215b78396cdd50ed162f8741dc4993e9fba2c78483d9b6f1e722b4", size = 72180, upload-time = "2025-09-13T11:47:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/89/fd40adbf5cc16550007ad67bffd7493c9976f7e576bf431d1bc537cfa976/django_pipeline-4.1.0-py3-none-any.whl", hash = "sha256:bdb84feb8db73b9fe8298fd9d0f6e50f30d78eb28a8ed28f73ca5d154080c3d5", size = 75523, upload-time = "2025-09-13T11:42:55.754Z" }, +] + +[[package]] +name = "django-tables2" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ab/3533314e818b04a9e1f04ae167845fec509f4e78ebaa6cf507d12eabcce3/django_tables2-2.8.0.tar.gz", hash = "sha256:0dea3401bb99a0164ba09e20d59a7d90856fdc05e5ae2da9a14d0fa14d99257c", size = 129204, upload-time = "2025-11-21T10:17:43.813Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/8b/047bb29359953b1c76f8886377845538144033d87da622c6b743f597a50b/django_tables2-2.8.0-py3-none-any.whl", hash = "sha256:860633b0f448216af73fca6005c7e38dc9b46931dc36c08a9281a71ee250b1ee", size = 95968, upload-time = "2025-11-21T10:17:42.363Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "feedparser" +version = "6.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sgmllib3k" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/79/db7edb5e77d6dfbc54d7d9df72828be4318275b2e580549ff45a962f6461/feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228", size = 286579, upload-time = "2025-09-10T13:33:59.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/eb/c96d64137e29ae17d83ad2552470bafe3a7a915e85434d9942077d7fd011/feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324", size = 81480, upload-time = "2025-09-10T13:33:58.022Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "nanoid" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/0250bf5935d88e214df469d35eccc0f6ff7e9db046fc8a9aeb4b2a192775/nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68", size = 3290, upload-time = "2018-11-20T14:45:51.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/0d/8630f13998638dc01e187fadd2e5c6d42d127d08aeb4943d231664d6e539/nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb", size = 5844, upload-time = "2018-11-20T14:45:50.165Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/02/d52c733a2452ef1ffcc123b68e6606d07276b0e358db70eabad7e40042b7/pillow-12.1.0.tar.gz", hash = "sha256:5c5ae0a06e9ea030ab786b0251b32c7e4ce10e58d983c0d5c56029455180b5b9", size = 46977283, upload-time = "2026-01-02T09:13:29.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/87/bdf971d8bbcf80a348cc3bacfcb239f5882100fe80534b0ce67a784181d8/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:5cb7bc1966d031aec37ddb9dcf15c2da5b2e9f7cc3ca7c54473a20a927e1eb91", size = 4062533, upload-time = "2026-01-02T09:12:20.791Z" }, + { url = "https://files.pythonhosted.org/packages/ff/4f/5eb37a681c68d605eb7034c004875c81f86ec9ef51f5be4a63eadd58859a/pillow-12.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:97e9993d5ed946aba26baf9c1e8cf18adbab584b99f452ee72f7ee8acb882796", size = 4138546, upload-time = "2026-01-02T09:12:23.664Z" }, + { url = "https://files.pythonhosted.org/packages/11/6d/19a95acb2edbace40dcd582d077b991646b7083c41b98da4ed7555b59733/pillow-12.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:414b9a78e14ffeb98128863314e62c3f24b8a86081066625700b7985b3f529bd", size = 3601163, upload-time = "2026-01-02T09:12:26.338Z" }, + { url = "https://files.pythonhosted.org/packages/fc/36/2b8138e51cb42e4cc39c3297713455548be855a50558c3ac2beebdc251dd/pillow-12.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6bdb408f7c9dd2a5ff2b14a3b0bb6d4deb29fb9961e6eb3ae2031ae9a5cec13", size = 5266086, upload-time = "2026-01-02T09:12:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/649056e4d22e1caa90816bf99cef0884aed607ed38075bd75f091a607a38/pillow-12.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3413c2ae377550f5487991d444428f1a8ae92784aac79caa8b1e3b89b175f77e", size = 4657344, upload-time = "2026-01-02T09:12:31.117Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6b/c5742cea0f1ade0cd61485dc3d81f05261fc2276f537fbdc00802de56779/pillow-12.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e5dcbe95016e88437ecf33544ba5db21ef1b8dd6e1b434a2cb2a3d605299e643", size = 6232114, upload-time = "2026-01-02T09:12:32.936Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/9f521268ce22d63991601aafd3d48d5ff7280a246a1ef62d626d67b44064/pillow-12.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d0a7735df32ccbcc98b98a1ac785cc4b19b580be1bdf0aeb5c03223220ea09d5", size = 8042708, upload-time = "2026-01-02T09:12:34.78Z" }, + { url = "https://files.pythonhosted.org/packages/1a/eb/257f38542893f021502a1bbe0c2e883c90b5cff26cc33b1584a841a06d30/pillow-12.1.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c27407a2d1b96774cbc4a7594129cc027339fd800cd081e44497722ea1179de", size = 6347762, upload-time = "2026-01-02T09:12:36.748Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/8ba375025701c09b309e8d5163c5a4ce0102fa86bbf8800eb0d7ac87bc51/pillow-12.1.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15c794d74303828eaa957ff8070846d0efe8c630901a1c753fdc63850e19ecd9", size = 7039265, upload-time = "2026-01-02T09:12:39.082Z" }, + { url = "https://files.pythonhosted.org/packages/cf/dc/cf5e4cdb3db533f539e88a7bbf9f190c64ab8a08a9bc7a4ccf55067872e4/pillow-12.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c990547452ee2800d8506c4150280757f88532f3de2a58e3022e9b179107862a", size = 6462341, upload-time = "2026-01-02T09:12:40.946Z" }, + { url = "https://files.pythonhosted.org/packages/d0/47/0291a25ac9550677e22eda48510cfc4fa4b2ef0396448b7fbdc0a6946309/pillow-12.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b63e13dd27da389ed9475b3d28510f0f954bca0041e8e551b2a4eb1eab56a39a", size = 7165395, upload-time = "2026-01-02T09:12:42.706Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4c/e005a59393ec4d9416be06e6b45820403bb946a778e39ecec62f5b2b991e/pillow-12.1.0-cp314-cp314-win32.whl", hash = "sha256:1a949604f73eb07a8adab38c4fe50791f9919344398bdc8ac6b307f755fc7030", size = 6431413, upload-time = "2026-01-02T09:12:44.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/af/f23697f587ac5f9095d67e31b81c95c0249cd461a9798a061ed6709b09b5/pillow-12.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f9f6a650743f0ddee5593ac9e954ba1bdbc5e150bc066586d4f26127853ab94", size = 7176779, upload-time = "2026-01-02T09:12:46.727Z" }, + { url = "https://files.pythonhosted.org/packages/b3/36/6a51abf8599232f3e9afbd16d52829376a68909fe14efe29084445db4b73/pillow-12.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:808b99604f7873c800c4840f55ff389936ef1948e4e87645eaf3fccbc8477ac4", size = 2543105, upload-time = "2026-01-02T09:12:49.243Z" }, + { url = "https://files.pythonhosted.org/packages/82/54/2e1dd20c8749ff225080d6ba465a0cab4387f5db0d1c5fb1439e2d99923f/pillow-12.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc11908616c8a283cf7d664f77411a5ed2a02009b0097ff8abbba5e79128ccf2", size = 5268571, upload-time = "2026-01-02T09:12:51.11Z" }, + { url = "https://files.pythonhosted.org/packages/57/61/571163a5ef86ec0cf30d265ac2a70ae6fc9e28413d1dc94fa37fae6bda89/pillow-12.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:896866d2d436563fa2a43a9d72f417874f16b5545955c54a64941e87c1376c61", size = 4660426, upload-time = "2026-01-02T09:12:52.865Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e1/53ee5163f794aef1bf84243f755ee6897a92c708505350dd1923f4afec48/pillow-12.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8e178e3e99d3c0ea8fc64b88447f7cac8ccf058af422a6cedc690d0eadd98c51", size = 6269908, upload-time = "2026-01-02T09:12:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/bc/0b/b4b4106ff0ee1afa1dc599fde6ab230417f800279745124f6c50bcffed8e/pillow-12.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:079af2fb0c599c2ec144ba2c02766d1b55498e373b3ac64687e43849fbbef5bc", size = 8074733, upload-time = "2026-01-02T09:12:56.802Z" }, + { url = "https://files.pythonhosted.org/packages/19/9f/80b411cbac4a732439e629a26ad3ef11907a8c7fc5377b7602f04f6fe4e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdec5e43377761c5dbca620efb69a77f6855c5a379e32ac5b158f54c84212b14", size = 6381431, upload-time = "2026-01-02T09:12:58.823Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b7/d65c45db463b66ecb6abc17c6ba6917a911202a07662247e1355ce1789e7/pillow-12.1.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:565c986f4b45c020f5421a4cea13ef294dde9509a8577f29b2fc5edc7587fff8", size = 7068529, upload-time = "2026-01-02T09:13:00.885Z" }, + { url = "https://files.pythonhosted.org/packages/50/96/dfd4cd726b4a45ae6e3c669fc9e49deb2241312605d33aba50499e9d9bd1/pillow-12.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43aca0a55ce1eefc0aefa6253661cb54571857b1a7b2964bd8a1e3ef4b729924", size = 6492981, upload-time = "2026-01-02T09:13:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1c/b5dc52cf713ae46033359c5ca920444f18a6359ce1020dd3e9c553ea5bc6/pillow-12.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0deedf2ea233722476b3a81e8cdfbad786f7adbed5d848469fa59fe52396e4ef", size = 7191878, upload-time = "2026-01-02T09:13:05.276Z" }, + { url = "https://files.pythonhosted.org/packages/53/26/c4188248bd5edaf543864fe4834aebe9c9cb4968b6f573ce014cc42d0720/pillow-12.1.0-cp314-cp314t-win32.whl", hash = "sha256:b17fbdbe01c196e7e159aacb889e091f28e61020a8abeac07b68079b6e626988", size = 6438703, upload-time = "2026-01-02T09:13:07.491Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0e/69ed296de8ea05cb03ee139cee600f424ca166e632567b2d66727f08c7ed/pillow-12.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27b9baecb428899db6c0de572d6d305cfaf38ca1596b5c0542a5182e3e74e8c6", size = 7182927, upload-time = "2026-01-02T09:13:09.841Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f5/68334c015eed9b5cff77814258717dec591ded209ab5b6fb70e2ae873d1d/pillow-12.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f61333d817698bdcdd0f9d7793e365ac3d2a21c1f1eb02b32ad6aefb8d8ea831", size = 2545104, upload-time = "2026-01-02T09:13:12.068Z" }, +] + +[[package]] +name = "publicsuffix" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/70/8124bab47b4b83c5846e124e91e8958696200acabc7404d3765f44212f8d/publicsuffix-1.1.1.tar.gz", hash = "sha256:22ce1d65ab6af5e9b2122e2443facdb93fb5c4abf24138099cb10fe7989f43b6", size = 66870, upload-time = "2019-12-01T13:44:51.867Z" } + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "servicecrm" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "asgiref" }, + { name = "cffi" }, + { name = "crispy-bootstrap5" }, + { name = "crispy-tailwind" }, + { name = "cryptography" }, + { name = "django" }, + { name = "django-crispy-forms" }, + { name = "django-datatable-view" }, + { name = "django-environ" }, + { name = "django-filter" }, + { name = "django-pipeline" }, + { name = "django-tables2" }, + { name = "dnspython" }, + { name = "feedparser" }, + { name = "nanoid" }, + { name = "pillow" }, + { name = "publicsuffix" }, + { name = "pycparser" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "sgmllib3k" }, + { name = "six" }, + { name = "sqlparse" }, + { name = "tablib" }, + { name = "typing-extensions" }, + { name = "tzdata" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "asgiref", specifier = ">=3.11.0" }, + { name = "cffi", specifier = ">=2.0.0" }, + { name = "crispy-bootstrap5", specifier = ">=2025.6" }, + { name = "crispy-tailwind", specifier = ">=1.0.3" }, + { name = "cryptography", specifier = ">=46.0.3" }, + { name = "django", specifier = ">=6.0.1" }, + { name = "django-crispy-forms", specifier = ">=2.5" }, + { name = "django-datatable-view", specifier = ">=2.1.6" }, + { name = "django-environ", specifier = ">=0.12.0" }, + { name = "django-filter", specifier = ">=25.2" }, + { name = "django-pipeline", specifier = ">=4.1.0" }, + { name = "django-tables2", specifier = ">=2.8.0" }, + { name = "dnspython", specifier = ">=2.8.0" }, + { name = "feedparser", specifier = ">=6.0.12" }, + { name = "nanoid", specifier = ">=2.0.0" }, + { name = "pillow", specifier = ">=12.1.0" }, + { name = "publicsuffix", specifier = ">=1.1.1" }, + { name = "pycparser", specifier = ">=2.23" }, + { name = "python-dateutil", specifier = ">=2.9.0.post0" }, + { name = "pytz", specifier = ">=2025.2" }, + { name = "sgmllib3k", specifier = ">=1.0.0" }, + { name = "six", specifier = ">=1.17.0" }, + { name = "sqlparse", specifier = ">=0.5.5" }, + { name = "tablib", specifier = ">=3.9.0" }, + { name = "typing-extensions", specifier = ">=4.15.0" }, + { name = "tzdata", specifier = ">=2025.3" }, + { name = "uvicorn", specifier = ">=0.40.0" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "sgmllib3k" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/bd/3704a8c3e0942d711c1299ebf7b9091930adae6675d7c8f476a7ce48653c/sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9", size = 5750, upload-time = "2010-08-24T14:33:52.445Z" } + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[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 = "tablib" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/00/416d2ba54d7d58a7f7c61bf62dfeb48fd553cf49614daf83312f2d2c156e/tablib-3.9.0.tar.gz", hash = "sha256:1b6abd8edb0f35601e04c6161d79660fdcde4abb4a54f66cc9f9054bd55d5fe2", size = 125565, upload-time = "2025-10-15T18:21:56.263Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/6b/32e51d847148b299088fc42d3d896845fd09c5247190133ea69dbe71ba51/tablib-3.9.0-py3-none-any.whl", hash = "sha256:eda17cd0d4dda614efc0e710227654c60ddbeb1ca92cdcfc5c3bd1fc5f5a6e4a", size = 49580, upload-time = "2025-10-15T18:21:44.185Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[[package]] +name = "wheel" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545, upload-time = "2024-11-23T00:18:23.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494, upload-time = "2024-11-23T00:18:21.207Z" }, +]