From 2a292cbd411abdbab53469d68eca337bada85c55 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 8 Jul 2025 22:11:58 +0300 Subject: [PATCH 01/10] working on develop_1 --- README.md | Bin 2162 -> 2166 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/README.md b/README.md index 87dae670dcf3542789518ffb1237623082936a44..ad9b6462ef12a093c36b24c1e854cc98ffba048c 100644 GIT binary patch delta 14 Wcmew)@J(RCHs*K+!;M?_vI77xVg?KV delta 10 Scmew+@JV38wvD^?umb=el?8nO From d9f68a5aa38ece667de96c41604868fa195cfbd5 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 8 Jul 2025 23:31:36 +0300 Subject: [PATCH 02/10] finish --- .env.sample | 14 ++ config/settings.py | 49 ++-- config/urls.py | 28 +-- mailing/admin.py | 64 ++++- mailing/forms.py | 37 +++ mailing/management/__init__.py | 0 mailing/management/commands/__init__.py | 0 mailing/management/commands/send_mailings.py | 39 +++ mailing/migrations/0001_initial.py | 73 ++++++ mailing/migrations/0002_initial.py | 53 ++++ mailing/models.py | 112 ++++++++- mailing/services.py | 77 ++++++ mailing/templates/mailing/base.html | 200 +++++++++++++++ .../mailing/includes/carousel_menu.html | 83 +++++++ .../templates/mailing/includes/main_menu.html | 67 +++++ mailing/templates/mailing/index.html | 51 ++++ .../mailing/mailing_confirm_delete.html | 21 ++ mailing/templates/mailing/mailing_detail.html | 19 ++ mailing/templates/mailing/mailing_form.html | 27 ++ mailing/templates/mailing/mailing_list.html | 77 ++++++ .../mailing/mailingattempt_list.html | 48 ++++ .../mailing/message_confirm_delete.html | 21 ++ mailing/templates/mailing/message_detail.html | 15 ++ mailing/templates/mailing/message_form.html | 27 ++ mailing/templates/mailing/message_list.html | 58 +++++ .../recipientmailing_confirm_delete.html | 21 ++ .../mailing/recipientmailing_detail.html | 20 ++ .../mailing/recipientmailing_form.html | 27 ++ .../mailing/recipientmailing_list.html | 71 ++++++ mailing/templatetags/__init__.py | 0 mailing/templatetags/my_tags.py | 15 ++ mailing/urls.py | 76 ++++++ mailing/views.py | 233 +++++++++++++++++- poetry.lock | 23 +- pyproject.toml | 1 + users/forms.py | 95 +++++++ users/management/__init__.py | 0 users/management/commands/__init__.py | 0 users/management/commands/create_manager.py | 25 ++ users/management/commands/create_user.py | 25 ++ users/management/commands/csu.py | 22 ++ users/migrations/0001_initial.py | 48 ++++ users/models.py | 31 ++- users/services.py | 21 ++ users/templates/registration/login.html | 30 +++ users/templates/users/email_confirmation.html | 25 ++ users/templates/users/password_recovery.html | 24 ++ users/templates/users/reset_password.html | 24 ++ .../templates/users/user_confirm_delete.html | 21 ++ users/templates/users/user_detail.html | 35 +++ users/templates/users/user_form.html | 34 +++ users/templates/users/user_list.html | 62 +++++ users/templatetags/__init__.py | 0 users/templatetags/users_tags.py | 0 users/urls.py | 35 +++ users/views.py | 126 +++++++++- 56 files changed, 2375 insertions(+), 55 deletions(-) create mode 100644 mailing/forms.py create mode 100644 mailing/management/__init__.py create mode 100644 mailing/management/commands/__init__.py create mode 100644 mailing/management/commands/send_mailings.py create mode 100644 mailing/migrations/0001_initial.py create mode 100644 mailing/migrations/0002_initial.py create mode 100644 mailing/services.py create mode 100644 mailing/templates/mailing/base.html create mode 100644 mailing/templates/mailing/includes/carousel_menu.html create mode 100644 mailing/templates/mailing/includes/main_menu.html create mode 100644 mailing/templates/mailing/index.html create mode 100644 mailing/templates/mailing/mailing_confirm_delete.html create mode 100644 mailing/templates/mailing/mailing_detail.html create mode 100644 mailing/templates/mailing/mailing_form.html create mode 100644 mailing/templates/mailing/mailing_list.html create mode 100644 mailing/templates/mailing/mailingattempt_list.html create mode 100644 mailing/templates/mailing/message_confirm_delete.html create mode 100644 mailing/templates/mailing/message_detail.html create mode 100644 mailing/templates/mailing/message_form.html create mode 100644 mailing/templates/mailing/message_list.html create mode 100644 mailing/templates/mailing/recipientmailing_confirm_delete.html create mode 100644 mailing/templates/mailing/recipientmailing_detail.html create mode 100644 mailing/templates/mailing/recipientmailing_form.html create mode 100644 mailing/templates/mailing/recipientmailing_list.html create mode 100644 mailing/templatetags/__init__.py create mode 100644 mailing/templatetags/my_tags.py create mode 100644 mailing/urls.py create mode 100644 users/forms.py create mode 100644 users/management/__init__.py create mode 100644 users/management/commands/__init__.py create mode 100644 users/management/commands/create_manager.py create mode 100644 users/management/commands/create_user.py create mode 100644 users/management/commands/csu.py create mode 100644 users/migrations/0001_initial.py create mode 100644 users/services.py create mode 100644 users/templates/registration/login.html create mode 100644 users/templates/users/email_confirmation.html create mode 100644 users/templates/users/password_recovery.html create mode 100644 users/templates/users/reset_password.html create mode 100644 users/templates/users/user_confirm_delete.html create mode 100644 users/templates/users/user_detail.html create mode 100644 users/templates/users/user_form.html create mode 100644 users/templates/users/user_list.html create mode 100644 users/templatetags/__init__.py create mode 100644 users/templatetags/users_tags.py create mode 100644 users/urls.py diff --git a/.env.sample b/.env.sample index ef8e1a3..680c99a 100644 --- a/.env.sample +++ b/.env.sample @@ -2,8 +2,22 @@ SECRET_KEY= DEBUG= +# Настройка БД NAME= USER= PASSWORD= HOST= PORT= + +# Настройка почты +EMAIL_HOST_USER= +EMAIL_HOST_PASSWORD= +EMAIL_PORT= +EMAIL_HOST= +EMAIL_USE_TLS= +EMAIL_USE_SSL= +DEFAULT_FROM_EMAIL= + +# Включение кэширования +CACHE_ENABLE= +LOCATION= \ No newline at end of file diff --git a/config/settings.py b/config/settings.py index ba69c4c..2f0dcc4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -53,7 +53,6 @@ WSGI_APPLICATION = 'config.wsgi.application' - # Database DATABASES = { 'default': { @@ -100,39 +99,31 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # AUTH_USER_MODEL = 'YourAppName.YourClassName' -# AUTH_USER_MODEL = "users.User" +AUTH_USER_MODEL = "users.User" LOGIN_REDIRECT_URL = "/" LOGOUT_REDIRECT_URL = "/" -#Подключение почты Яндекс +# Подключение почты Яндекс # Адрес почтового сервера — smtp.yandex.ru. # Защита соединения — SSL. # Порт — 465. Если почтовый клиент начинает соединение без шифрования — 587. EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' -EMAIL_HOST = 'smtp.yandex.ru' -EMAIL_PORT = 465 -EMAIL_USE_TLS = False -EMAIL_USE_SSL = True -EMAIL_HOST_USER = 'iVasya2033@yandex.ru' -EMAIL_HOST_PASSWORD = 'znwlirhdwkdnopwb' -DEFAULT_FROM_EMAIL = EMAIL_HOST_USER -# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -# EMAIL_HOST = os.getenv("EMAIL_HOST") -# EMAIL_PORT = os.getenv("EMAIL_PORT") -# EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", False) == "True" -# EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", False) == "True" -# EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") -# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") -# DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL") - -#REDIS -# CACHE_ENABLE = os.getenv("CACHE_ENABLE", False) == "True" -# -# if CACHE_ENABLE: -# CACHES = { -# "default": { -# "BACKEND": "django.core.cache.backends.redis.RedisCache", -# "LOCATION": os.getenv("LOCATION"), -# } -# } \ No newline at end of file +EMAIL_HOST = os.getenv("EMAIL_HOST") +EMAIL_PORT = os.getenv("EMAIL_PORT") +EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", False) == "True" +EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", False) == "True" +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") +DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL") + +# REDIS +CACHE_ENABLE = os.getenv("CACHE_ENABLE", False) == "True" + +if CACHE_ENABLE: + CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": os.getenv("LOCATION"), + } + } diff --git a/config/urls.py b/config/urls.py index 35a0802..19c8fc0 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,22 +1,14 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" +from django.conf.urls.static import static from django.contrib import admin -from django.urls import path +from django.urls import include, path + +from config import settings urlpatterns = [ - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), + path("", include("mailing.urls", namespace="mailing")), + path("users/", include("users.urls", namespace="users")), ] + +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/mailing/admin.py b/mailing/admin.py index 8c38f3f..22eff2d 100644 --- a/mailing/admin.py +++ b/mailing/admin.py @@ -1,3 +1,65 @@ from django.contrib import admin -# Register your models here. +from mailing.models import ( + Mailing, + MailingAttempt, + Message, + RecipientMailing) +from users.models import User + + +@admin.register(RecipientMailing) +class RecipientMailingAdmin(admin.ModelAdmin): + list_display = ("id", "fio", "email", "comment", "owner") + list_filter = ("fio",) + search_fields = ( + "fio", + "email", + ) + + +@admin.register(Message) +class MessageAdmin(admin.ModelAdmin): + list_display = ( + "id", + "subject", + "content", + "owner", + ) + search_fields = ("subject",) + list_filter = ("subject",) + + +@admin.register(Mailing) +class MailingAdmin(admin.ModelAdmin): + list_display = ("id", "first_sending", "end_sending", "status", "message", "owner") + search_fields = ("status",) + list_filter = ("status",) + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + list_display = ( + "id", + "avatar", + "email", + "first_name", + "last_name", + "middle_name", + "phone_number", + "country", + ) + search_fields = ("email",) + list_filter = ("email",) + + +@admin.register(MailingAttempt) +class MailingAttemptAdmin(admin.ModelAdmin): + list_display = ( + "id", + "owner", + "date_attempt", + "status", + ) + search_fields = ("owner",) + list_filter = ("owner",) diff --git a/mailing/forms.py b/mailing/forms.py new file mode 100644 index 0000000..2e40a2a --- /dev/null +++ b/mailing/forms.py @@ -0,0 +1,37 @@ +from django.forms import BooleanField, ImageField, ModelForm +from django.urls import reverse_lazy + +from mailing.models import Mailing, Message, RecipientMailing + + +class StyleFormMixin: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for fild_name, fild in self.fields.items(): + if isinstance(fild, BooleanField): + fild.widget.attrs["class"] = "form-check-input" + elif isinstance(fild, ImageField): + fild.widget.attrs["class"] = "form-control-file" + else: + fild.widget.attrs["class"] = "form-control" + + +class MailingForm(StyleFormMixin, ModelForm): + class Meta: + model = Mailing + fields = "__all__" + success_url = reverse_lazy("mailing:mailing_list") + + +class RecipientForm(StyleFormMixin, ModelForm): + class Meta: + model = RecipientMailing + fields = "__all__" + success_url = reverse_lazy("mailing:recipientmailing_list") + + +class MessageForm(StyleFormMixin, ModelForm): + class Meta: + model = Message + fields = "__all__" + success_url = reverse_lazy("mailing:message_list") diff --git a/mailing/management/__init__.py b/mailing/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mailing/management/commands/__init__.py b/mailing/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mailing/management/commands/send_mailings.py b/mailing/management/commands/send_mailings.py new file mode 100644 index 0000000..a7fd040 --- /dev/null +++ b/mailing/management/commands/send_mailings.py @@ -0,0 +1,39 @@ +from django.core.mail import send_mail +from django.core.management import BaseCommand +from django.utils import timezone + +from config.settings import EMAIL_HOST_USER +from mailing.models import Mailing, MailingAttempt + + +class Command(BaseCommand): + help = "Отправка почтовых отправлений получателям" + + def handle(self, *args, **kwargs): + mailings = Mailing.objects.filter(status__in=[Mailing.CREATED, Mailing.LAUNCHED]) + for mailing in mailings: + for recipient in mailing.recipients.all(): + try: + send_mail( + mailing.message.subject, + mailing.message.content, + from_email=EMAIL_HOST_USER, + recipient_list=[recipient.email], + fail_silently=False, + ) + MailingAttempt.objects.create( + date_attempt=timezone.now(), + status=MailingAttempt.STATUS_OK, + server_response="Email отправлен", + mailing=mailing, + ) + print(f"Сообщение {mailing.message.subject} успешно отправлено на {recipient.email}") + except Exception as e: + MailingAttempt.objects.create( + date_attempt=timezone.now(), + status=MailingAttempt.STATUS_NOK, + server_response=str(e), + mailing=mailing, + ) + print(str(e)) + mailing.save() diff --git a/mailing/migrations/0001_initial.py b/mailing/migrations/0001_initial.py new file mode 100644 index 0000000..66f7d24 --- /dev/null +++ b/mailing/migrations/0001_initial.py @@ -0,0 +1,73 @@ +# Generated by Django 5.2.4 on 2025-07-08 20:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Mailing', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_sending', models.DateTimeField(verbose_name='Дата и время первого отправления')), + ('end_sending', models.DateTimeField(verbose_name='Дата и время окончания отправки')), + ('status', models.CharField(choices=[('Создана', 'Создана'), ('Запущена', 'Запущена'), ('Завершена', 'Завершена')], default='Создана', max_length=10, verbose_name='Статус рассылки')), + ('is_active', models.BooleanField(default=True, verbose_name='активна')), + ], + options={ + 'verbose_name': 'Рассылка', + 'verbose_name_plural': 'Рассылки', + 'ordering': ['first_sending'], + 'permissions': [('can_disable_mailing', 'Возможность отключения рассылки')], + }, + ), + migrations.CreateModel( + name='MailingAttempt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_attempt', models.DateTimeField(verbose_name='Дата и время попытки')), + ('status', models.CharField(choices=[('Успешно', 'Успешно'), ('Не успешно', 'Не успешно')], max_length=15, verbose_name='Статус попытки')), + ('server_response', models.TextField(verbose_name='Ответ почтового сервера')), + ], + options={ + 'verbose_name': 'Попытка', + 'verbose_name_plural': 'Попытки', + 'ordering': ['date_attempt', 'status'], + }, + ), + migrations.CreateModel( + name='Message', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('subject', models.CharField(max_length=255, verbose_name='Тема сообщения')), + ('content', models.TextField(verbose_name='Содержание сообщения')), + ], + options={ + 'verbose_name': 'Сообщение', + 'verbose_name_plural': 'Сообщения', + 'ordering': ['subject'], + }, + ), + migrations.CreateModel( + name='RecipientMailing', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('email', models.EmailField(max_length=255, unique=True, verbose_name='E-mail')), + ('fio', models.CharField(max_length=255, verbose_name='ФИО')), + ('comment', models.TextField(blank=True, null=True, verbose_name='Комментарий')), + ('avatar', models.ImageField(blank=True, null=True, upload_to='mailing/avatars', verbose_name='Аватар')), + ('is_active', models.BooleanField(default=True, verbose_name='активность')), + ], + options={ + 'verbose_name': 'Получатель рассылки', + 'verbose_name_plural': 'Получатели рассылки', + 'ordering': ['fio'], + }, + ), + ] diff --git a/mailing/migrations/0002_initial.py b/mailing/migrations/0002_initial.py new file mode 100644 index 0000000..d94749b --- /dev/null +++ b/mailing/migrations/0002_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 5.2.4 on 2025-07-08 20:24 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('mailing', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='mailing', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец'), + ), + migrations.AddField( + model_name='mailingattempt', + name='mailing', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailing', to='mailing.mailing', verbose_name='Рассылка'), + ), + migrations.AddField( + model_name='mailingattempt', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец'), + ), + migrations.AddField( + model_name='message', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец'), + ), + migrations.AddField( + model_name='mailing', + name='message', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mailings', to='mailing.message', verbose_name='Сообщение'), + ), + migrations.AddField( + model_name='recipientmailing', + name='owner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Владелец'), + ), + migrations.AddField( + model_name='mailing', + name='recipients', + field=models.ManyToManyField(help_text='Укажите получателей рассылки (используйте CTRL или COMMAND)', related_name='recipients', to='mailing.recipientmailing', verbose_name='Получатели'), + ), + ] diff --git a/mailing/models.py b/mailing/models.py index 71a8362..17adc32 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -1,3 +1,113 @@ from django.db import models -# Create your models here. +from users.models import User + +NULLABLE = {"blank": True, "null": True} + + +class RecipientMailing(models.Model): + email = models.EmailField(max_length=255, unique=True, verbose_name="E-mail") + fio = models.CharField(max_length=255, verbose_name="ФИО") + comment = models.TextField(verbose_name="Комментарий", **NULLABLE) + avatar = models.ImageField(upload_to="mailing/avatars", **NULLABLE, verbose_name="Аватар") + is_active = models.BooleanField(default=True, verbose_name="активность") + owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + + def __str__(self): + return f"{self.fio} <{self.email}>" + + class Meta: + verbose_name = "Получатель рассылки" + verbose_name_plural = "Получатели рассылки" + ordering = ["fio"] + + +class Message(models.Model): + subject = models.CharField(max_length=255, verbose_name="Тема сообщения") + content = models.TextField(verbose_name="Содержание сообщения") + owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + + def __str__(self): + return self.content + + class Meta: + verbose_name = "Сообщение" + verbose_name_plural = "Сообщения" + ordering = ["subject"] + + +class Mailing(models.Model): + CREATED = "Создана" + LAUNCHED = "Запущена" + COMPLETED = "Завершена" + + STATUS_CHOICES = [ + (CREATED, "Создана"), + (LAUNCHED, "Запущена"), + (COMPLETED, "Завершена"), + ] + + first_sending = models.DateTimeField(verbose_name="Дата и время первого отправления") + end_sending = models.DateTimeField(verbose_name="Дата и время окончания отправки") + status = models.CharField( + max_length=10, + choices=STATUS_CHOICES, + default=CREATED, + verbose_name="Статус рассылки", + ) + message = models.ForeignKey( + Message, + on_delete=models.CASCADE, + verbose_name="Сообщение", + related_name="mailings", + ) + recipients = models.ManyToManyField( + RecipientMailing, + related_name="recipients", + verbose_name="Получатели", + help_text="Укажите получателей рассылки (используйте CTRL или COMMAND)", + ) + is_active = models.BooleanField(default=True, verbose_name="активна") + owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + + def __str__(self): + return f"Рассылка {self.id}" + + class Meta: + verbose_name = "Рассылка" + verbose_name_plural = "Рассылки" + ordering = ["first_sending"] + permissions = [ + ("can_disable_mailing", "Возможность отключения рассылки"), + ] + + +class MailingAttempt(models.Model): + """Попытка рассылки""" + + STATUS_OK = "Успешно" + STATUS_NOK = "Не успешно" + + STATUS_CHOICES = [ + (STATUS_OK, "Успешно"), + (STATUS_NOK, "Не успешно"), + ] + + date_attempt = models.DateTimeField(verbose_name="Дата и время попытки") + status = models.CharField(max_length=15, choices=STATUS_CHOICES, verbose_name="Статус попытки") + server_response = models.TextField(verbose_name="Ответ почтового сервера") + mailing = models.ForeignKey( + Mailing, + on_delete=models.CASCADE, + verbose_name="Рассылка", + related_name="mailing", + ) + owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + + def __str__(self): + return f"{self.date_attempt} <{self.status}>" + + class Meta: + verbose_name = "Попытка" + verbose_name_plural = "Попытки" + ordering = ["date_attempt", "status"] diff --git a/mailing/services.py b/mailing/services.py new file mode 100644 index 0000000..26abb9d --- /dev/null +++ b/mailing/services.py @@ -0,0 +1,77 @@ +from django.contrib.auth.decorators import login_required +from django.core.cache import cache +from django.core.mail import send_mail +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse +from django.utils import timezone + +from config.settings import CACHE_ENABLE, EMAIL_HOST_USER +from mailing.models import Mailing, MailingAttempt + + +def run_mailing(request, pk): + """Функция запуска рассылки по требованию""" + mailing = get_object_or_404(Mailing, id=pk) + for recipient in mailing.recipients.all(): + try: + mailing.status = Mailing.LAUNCHED + send_mail( + subject=mailing.message.subject, + message=mailing.message.content, + from_email=EMAIL_HOST_USER, + recipient_list=[recipient.email], + fail_silently=False, + ) + MailingAttempt.objects.create( + date_attempt=timezone.now(), + status=MailingAttempt.STATUS_OK, + server_response="Email отправлен", + mailing=mailing, + ) + except Exception as e: + print(f"Ошибка при отправке письма для {recipient.email}: {str(e)}") + MailingAttempt.objects.create( + date_attempt=timezone.now(), + status=MailingAttempt.STATUS_NOK, + server_response=str(e), + mailing=mailing, + ) + if mailing.end_sending and mailing.end_sending <= timezone.now(): + # Если время рассылки закончилось, обновляем статус на "завершено" + mailing.status = Mailing.COMPLETED + mailing.save() + return redirect("mailing:mailing_list") + + +def get_mailing_from_cache(): + """Получение данных по рассылкам из кэша, если кэш пуст берем из БД.""" + if not CACHE_ENABLE: + return Mailing.objects.all() + key = "mailing_list" + cache_data = cache.get(key) + if cache_data is not None: + return cache_data + cache_data = Mailing.objects.all() + cache.set(key, cache_data) + return cache_data + + +def get_attempt_from_cache(): + """Получение данных по попыткам из кэша, если кэш пуст берем из БД.""" + if not CACHE_ENABLE: + return MailingAttempt.objects.all() + key = "attempt_list" + cache_data = cache.get(key) + if cache_data is not None: + return cache_data + cache_data = MailingAttempt.objects.all() + cache.set(key, cache_data) + return cache_data + + +@login_required +def block_mailing(request, pk): + mailing = Mailing.objects.get(pk=pk) + mailing.is_active = {mailing.is_active: False, not mailing.is_active: True}[True] + mailing.save() + return redirect(reverse("mailing:mailing_list")) diff --git a/mailing/templates/mailing/base.html b/mailing/templates/mailing/base.html new file mode 100644 index 0000000..8381ca2 --- /dev/null +++ b/mailing/templates/mailing/base.html @@ -0,0 +1,200 @@ +{% load static%} + + + + + + + + + + + Рассылки сообщений + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {% include 'mailing/includes/main_menu.html' %} +
+ +
+ {% if user.is_authenticated %} + {% include 'mailing/includes/carousel_menu.html' %} + {% endif %} + + {% block content %} + {% endblock %}} +
+ + + + + diff --git a/mailing/templates/mailing/includes/carousel_menu.html b/mailing/templates/mailing/includes/carousel_menu.html new file mode 100644 index 0000000..ad5a6d5 --- /dev/null +++ b/mailing/templates/mailing/includes/carousel_menu.html @@ -0,0 +1,83 @@ + \ No newline at end of file diff --git a/mailing/templates/mailing/includes/main_menu.html b/mailing/templates/mailing/includes/main_menu.html new file mode 100644 index 0000000..1b10be8 --- /dev/null +++ b/mailing/templates/mailing/includes/main_menu.html @@ -0,0 +1,67 @@ + \ No newline at end of file diff --git a/mailing/templates/mailing/index.html b/mailing/templates/mailing/index.html new file mode 100644 index 0000000..1e236fc --- /dev/null +++ b/mailing/templates/mailing/index.html @@ -0,0 +1,51 @@ +{% extends 'mailing/base.html' %} +{% load my_tags %} +{% block content %} + +
+
+
+ {% if user.is_authenticated %} +

Приветствуем Вас {{user.email}} на нашем сайте

+ {% else %} +

Приветствуем Вас на нашем сайте

+ {% endif %} +
+
+
+
+
+
+ +
+
+
+{% endblock %} \ No newline at end of file diff --git a/mailing/templates/mailing/mailing_confirm_delete.html b/mailing/templates/mailing/mailing_confirm_delete.html new file mode 100644 index 0000000..a9ad7f4 --- /dev/null +++ b/mailing/templates/mailing/mailing_confirm_delete.html @@ -0,0 +1,21 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+

Вы действительно хотите удалить рассылку от:

+

{{ object.first_sending }}

+
+ {% csrf_token %} +
+ + Отмена +
+
+
+
+
+{% endblock %} diff --git a/mailing/templates/mailing/mailing_detail.html b/mailing/templates/mailing/mailing_detail.html new file mode 100644 index 0000000..e4d5944 --- /dev/null +++ b/mailing/templates/mailing/mailing_detail.html @@ -0,0 +1,19 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+ +
+ +

Первая отправка {{object.first_sending }}

+

Окончание отправки {{object.end_sending }}

+

Статус {{object.status}}

+

Статус {{object.is_active}}

+

Статус {{object.owner}}

+

Сообщение {{object.message}}

+

Назад

+
+ +
+
+{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/mailing_form.html b/mailing/templates/mailing/mailing_form.html new file mode 100644 index 0000000..12038c6 --- /dev/null +++ b/mailing/templates/mailing/mailing_form.html @@ -0,0 +1,27 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + Отмена + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/mailing/templates/mailing/mailing_list.html b/mailing/templates/mailing/mailing_list.html new file mode 100644 index 0000000..cff65c1 --- /dev/null +++ b/mailing/templates/mailing/mailing_list.html @@ -0,0 +1,77 @@ +{% extends 'mailing/base.html' %} +{% block content %} +{% load my_tags %} +
+
+
+
+
+

Рассылки

+ {% if user.is_superuser or user|in_group:"Пользователи" %} + Создать рассылку »

+ {% endif %} +
+
+
+ +
+
+
+ + + + + + + + + + {% if user.is_superuser %} + + {% endif %} + + + + + {% for object in object_list %} + + + + + + + + {% if user.is_superuser %} + + {% endif %} + + + + {% endfor%} +
СообщениеВремя началаВремя окончанияТема сообщенияСтатусАктивностьСоздалДействия
{{ object.message|truncatechars:30 }}{{ object.first_sending|date:"d M Y" }}{{ object.end_sending|date:"d M Y" }}{{ object.message.subject }}{{ object.status }} + {% if user.is_superuser or user|in_group:"Менеджеры" %} + {% if object.is_active %} + Заблокировать + {% else %} + Разблокировать + {% endif %} + {% else %} + {{object.is_active|yesno:"Активна, Не активна"}} + {% endif %} + {{ object.owner }} + Посмотреть + {% if user.is_superuser or user|in_group:"Пользователи" %} + Редактировать + Удалить + {% endif %} + {% if user.is_superuser %} + Запустить » + {% endif %} +
+ +
+
+
+{% endblock %}} diff --git a/mailing/templates/mailing/mailingattempt_list.html b/mailing/templates/mailing/mailingattempt_list.html new file mode 100644 index 0000000..1f7779c --- /dev/null +++ b/mailing/templates/mailing/mailingattempt_list.html @@ -0,0 +1,48 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+
+
+
+

Статистика по рассылкам

+
+
+
+ +
+
+
+ + + + + + + + + {% if request.user.is_superuser %} + + {% endif %} + + + + {% for object in object_list %} + + + + + + + {% if request.user.is_superuser %} + + {% endif %} + + + {% endfor%} +
Тема сообщенияОписание рассылкиДатаСтатусОтвет сервераСоздал
{{ object.mailing.message.subject }}{{ object.mailing.message.content|truncatechars:30 }}{{ object.date_attempt}}{{ object.status}}{{ object.server_response}}{{ object.mailing.owner }}
+ +
+
+
+{% endblock %}} diff --git a/mailing/templates/mailing/message_confirm_delete.html b/mailing/templates/mailing/message_confirm_delete.html new file mode 100644 index 0000000..01b42dd --- /dev/null +++ b/mailing/templates/mailing/message_confirm_delete.html @@ -0,0 +1,21 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+

Вы действительно хотите удалить сообщение:

+

{{ object.subject }}

+
+ {% csrf_token %} +
+ + Отмена +
+
+
+
+
+{% endblock %} diff --git a/mailing/templates/mailing/message_detail.html b/mailing/templates/mailing/message_detail.html new file mode 100644 index 0000000..9b933b4 --- /dev/null +++ b/mailing/templates/mailing/message_detail.html @@ -0,0 +1,15 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+ +
+ +

Тема сообщения {{object.subject}}

+

Текст сообщения {{object.content}}

+

Назад

+
+ +
+
+{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/message_form.html b/mailing/templates/mailing/message_form.html new file mode 100644 index 0000000..b18139f --- /dev/null +++ b/mailing/templates/mailing/message_form.html @@ -0,0 +1,27 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + Отмена + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/mailing/templates/mailing/message_list.html b/mailing/templates/mailing/message_list.html new file mode 100644 index 0000000..ad93a55 --- /dev/null +++ b/mailing/templates/mailing/message_list.html @@ -0,0 +1,58 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+
+
+ +
+
+

Сообщения

+ Добавить сообщение + »

+
+
+ +
+
+ +
+
+
+ + + + + + {% if request.user.is_superuser %} + + {% endif %} + + + + + {% for object in object_list %} + + + + {% if request.user.is_superuser %} + + {% endif %} + + + + {% endfor%} +
ТемаСообщениеСоздалДействия
{{ object.subject }}{{ object.content|truncatechars:30 }}{{ object.owner }} + Посмотреть + » + Редактировать + » + Удалить +
+ +
+ + +
+
+{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/recipientmailing_confirm_delete.html b/mailing/templates/mailing/recipientmailing_confirm_delete.html new file mode 100644 index 0000000..934e60b --- /dev/null +++ b/mailing/templates/mailing/recipientmailing_confirm_delete.html @@ -0,0 +1,21 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+

Вы действительно хотите удалить получателя:

+

{{ object.fio }}

+
+ {% csrf_token %} +
+ + Отмена +
+
+
+
+
+{% endblock %} diff --git a/mailing/templates/mailing/recipientmailing_detail.html b/mailing/templates/mailing/recipientmailing_detail.html new file mode 100644 index 0000000..e8012fb --- /dev/null +++ b/mailing/templates/mailing/recipientmailing_detail.html @@ -0,0 +1,20 @@ +{% extends 'mailing/base.html' %} +{% block content %} +{% load my_tags %} +
+
+ +
+ Фото +

ФИО получателя: {{object.fio}} +

+

Email получателя: {{object.email }} +

+

Комментарий: {{object.comment}} +

+

Назад

+
+ +
+
+{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/recipientmailing_form.html b/mailing/templates/mailing/recipientmailing_form.html new file mode 100644 index 0000000..f91a219 --- /dev/null +++ b/mailing/templates/mailing/recipientmailing_form.html @@ -0,0 +1,27 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + Отмена + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/mailing/templates/mailing/recipientmailing_list.html b/mailing/templates/mailing/recipientmailing_list.html new file mode 100644 index 0000000..e91e6b2 --- /dev/null +++ b/mailing/templates/mailing/recipientmailing_list.html @@ -0,0 +1,71 @@ +{% extends 'mailing/base.html' %} +{% block content %} +{% load my_tags %} +
+
+
+
+ +
+
+

Получатели

+ {% if user.is_superuser or user|in_group:"Пользователи" %} + Добавить получателя + »

+ {% endif %} +
+
+ +
+
+ +
+
+
+ + + + + + + + {% if request.user.is_superuser %} + + {% endif %} + + + + + {% for object in object_list %} + + + + + + {% if request.user.is_superuser %} + + {% endif %} + + + + {% endfor%} +
ФотоФИО получателяemail получателяКомментарийСоздалДействия
+ Фото + + {{ object.fio }}{{ object.email}}{{ object.comment|truncatechars:30 }}{{ object.owner }} + Посмотреть + » + {% if user.is_superuser or user|in_group:"Пользователи" %} + Редактировать + » + Удалить + {% endif %} +
+ +
+ +
+
+{% endblock %}} \ No newline at end of file diff --git a/mailing/templatetags/__init__.py b/mailing/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mailing/templatetags/my_tags.py b/mailing/templatetags/my_tags.py new file mode 100644 index 0000000..824f7f5 --- /dev/null +++ b/mailing/templatetags/my_tags.py @@ -0,0 +1,15 @@ +from django import template + +register = template.Library() + + +@register.filter() +def media_filter(path): + if path: + return f"/media/{path}" + return "#" + + +@register.filter +def in_group(user, group_name): + return user.groups.filter(name=group_name).exists() \ No newline at end of file diff --git a/mailing/urls.py b/mailing/urls.py new file mode 100644 index 0000000..86d7fc2 --- /dev/null +++ b/mailing/urls.py @@ -0,0 +1,76 @@ +from django.urls import path +from django.views.decorators.cache import cache_page + +from mailing.apps import MailingConfig +from mailing.services import block_mailing, run_mailing +from mailing.views import ( + IndexView, + MailingAttemptListView, + MailingCreateView, + MailingDeleteView, + MailingDetailView, + MailingListView, + MailingUpdateView, + MessageCreateView, + MessageDeleteView, + MessageDetailView, + MessageListView, + MessageUpdateView, + RecipientMailingCreateView, + RecipientMailingDeleteView, + RecipientMailingDetailView, + RecipientMailingListView, + RecipientMailingUpdateView, +) + +app_name = MailingConfig.name + +urlpatterns = [ + path("", IndexView.as_view(), name="index"), + path("mailing/", cache_page(1)(MailingListView.as_view()), name="mailing_list"), + path( + "mailing//detail/", + cache_page(60)(MailingDetailView.as_view()), + name="mailing_detail", + ), + path("mailing/new/", MailingCreateView.as_view(), name="mailing_create"), + path("mailing//edit/", MailingUpdateView.as_view(), name="mailing_update"), + path("mailing//delete/", MailingDeleteView.as_view(), name="mailing_delete"), + path("mailing//run_mailing/", run_mailing, name="run_mailing"), + path("block_mailing/", block_mailing, name="block_mailing"), + path( + "recipientmailing/", + cache_page(1)(RecipientMailingListView.as_view()), + name="recipientmailing_list", + ), + path( + "recipientmailing//detail/", + cache_page(60)(RecipientMailingDetailView.as_view()), + name="recipientmailing_detail", + ), + path( + "recipientmailing/new/", + RecipientMailingCreateView.as_view(), + name="recipientmailing_create", + ), + path( + "recipientmailing//edit/", + RecipientMailingUpdateView.as_view(), + name="recipientmailing_update", + ), + path( + "recipientmailing//delete/", + RecipientMailingDeleteView.as_view(), + name="recipientmailing_delete", + ), + path("message/", cache_page(60)(MessageListView.as_view()), name="message_list"), + path( + "message//detail/", + cache_page(60)(MessageDetailView.as_view()), + name="message_detail", + ), + path("message/new/", MessageCreateView.as_view(), name="message_create"), + path("message//edit/", MessageUpdateView.as_view(), name="message_update"), + path("message//delete/", MessageDeleteView.as_view(), name="message_delete"), + path("attempt/", cache_page(60)(MailingAttemptListView.as_view()), name="attempt"), +] diff --git a/mailing/views.py b/mailing/views.py index 91ea44a..3694737 100644 --- a/mailing/views.py +++ b/mailing/views.py @@ -1,3 +1,232 @@ -from django.shortcuts import render +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.core.exceptions import PermissionDenied -# Create your views here. +from django.urls import reverse_lazy + +from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView + +from mailing.forms import MailingForm, MessageForm, RecipientForm +from mailing.models import Mailing, MailingAttempt, Message, RecipientMailing + + +class IndexView(TemplateView): + template_name = "mailing/index.html" + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data["title"] = "Главная" + context_data["count_mailing"] = len(Mailing.objects.all()) + active_mailings_count = Mailing.objects.filter(status="Запущена").count() + context_data["active_mailings_count"] = active_mailings_count + unique_clients_count = RecipientMailing.objects.distinct().count() + context_data["unique_clients_count"] = unique_clients_count + return context_data + + +class MailingListView(LoginRequiredMixin, ListView): + model = Mailing + + def get_queryset(self, *args, **kwargs): + if self.request.user.is_superuser or self.request.user.groups.filter(name="Менеджеры").exists(): + return super().get_queryset() + elif self.request.user.groups.filter(name="Пользователи").exists(): + return super().get_queryset().filter(owner=self.request.user) + raise PermissionDenied + + +class MailingDetailView(LoginRequiredMixin, DetailView): + model = Mailing + form_class = MailingForm + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.request.user.groups.filter(name="Менеджеры") or self.request.user.is_superuser: + return self.object + if self.object.owner != self.request.user and not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class MailingCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): + model = Mailing + form_class = MailingForm + success_url = reverse_lazy("mailing:mailing_list") + + def form_valid(self, form): + recipient = form.save() + recipient.owner = self.request.user + recipient.save() + return super().form_valid(form) + + def test_func(self): + return self.request.user.groups.filter(name="Пользователи").exists() or self.request.user.is_superuser + + +class MailingUpdateView(LoginRequiredMixin, UpdateView): + model = Mailing + form_class = MailingForm + success_url = reverse_lazy("mailing:mailing_list") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.object.owner != self.request.user and not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class MailingDeleteView(LoginRequiredMixin, DeleteView): + model = Mailing + success_url = reverse_lazy("mailing:mailing_list") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.object.owner != self.request.user and not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class RecipientMailingListView(ListView): + model = RecipientMailing + + def get_context_data(self, **kwargs): + context_data = super().get_context_data(**kwargs) + context_data["title"] = "Получатели" + return context_data + + def get_queryset(self, *args, **kwargs): + if self.request.user.is_superuser or self.request.user.groups.filter(name="Менеджеры"): + return super().get_queryset() + elif self.request.user.groups.filter(name="Пользователи"): + return super().get_queryset().filter(owner=self.request.user) + raise PermissionDenied + + +class RecipientMailingDetailView(LoginRequiredMixin, DetailView): + model = RecipientMailing + form_class = RecipientForm + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.request.user.is_superuser or self.request.user.groups.filter(name="Менеджеры"): + return self.object + if self.object.owner != self.request.user and not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class RecipientMailingCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): + model = RecipientMailing + form_class = RecipientForm + success_url = reverse_lazy("mailing:recipientmailing_list") + + def form_valid(self, form): + recipient = form.save() + recipient.owner = self.request.user + recipient.save() + return super().form_valid(form) + + def test_func(self): + return self.request.user.groups.filter(name="Пользователи").exists() or self.request.user.is_superuser + + +class RecipientMailingUpdateView(LoginRequiredMixin, UpdateView): + model = RecipientMailing + form_class = RecipientForm + success_url = reverse_lazy("mailing:recipientmailing_list") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.object.owner != self.request.user and not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class RecipientMailingDeleteView(LoginRequiredMixin, DeleteView): + model = RecipientMailing + success_url = reverse_lazy("mailing:recipientmailing_list") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.object.owner != self.request.user and not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class MessageListView(ListView): + model = Message + + def get_queryset(self, *args, **kwargs): + if self.request.user.is_superuser: + return super().get_queryset() + else: + raise PermissionDenied + + +class MessageDetailView(LoginRequiredMixin, DetailView): + model = Message + form_class = MessageForm + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class MessageCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): + model = Message + form_class = MessageForm + success_url = reverse_lazy("mailing:message_list") + + def form_valid(self, form): + recipient = form.save() + recipient.owner = self.request.user + recipient.save() + return super().form_valid(form) + + def test_func(self): + return self.request.user.is_superuser + + +class MessageUpdateView(LoginRequiredMixin, UpdateView): + model = Message + form_class = MessageForm + success_url = reverse_lazy("mailing:message_list") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class MessageDeleteView(LoginRequiredMixin, DeleteView): + model = Message + success_url = reverse_lazy("mailing:message_list") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class MailingAttemptCreateView(LoginRequiredMixin, CreateView): + model = MailingAttempt + + def form_valid(self, form): + recipient = form.save() + recipient.owner = self.request.user + recipient.save() + return super().form_valid(form) + + +class MailingAttemptListView(LoginRequiredMixin, ListView): + model = MailingAttempt + + def get_queryset(self, *args, **kwargs): + if self.request.user.is_superuser: + return super().get_queryset() + elif self.request.user.groups.filter(name="Пользователи").exists(): + return super().get_queryset().filter(owner=self.request.user) + raise PermissionDenied diff --git a/poetry.lock b/poetry.lock index ba2df15..e76bcce 100644 --- a/poetry.lock +++ b/poetry.lock @@ -82,6 +82,27 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""} argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-countries" +version = "7.6.1" +description = "Provides a country field for Django models." +optional = false +python-versions = "*" +files = [ + {file = "django-countries-7.6.1.tar.gz", hash = "sha256:c772d4e3e54afcc5f97a018544e96f246c6d9f1db51898ab0c15cd57e19437cf"}, + {file = "django_countries-7.6.1-py3-none-any.whl", hash = "sha256:1ed20842fe0f6194f91faca21076649513846a8787c9eb5aeec3cbe1656b8acc"}, +] + +[package.dependencies] +asgiref = "*" +typing-extensions = "*" + +[package.extras] +dev = ["black", "django", "djangorestframework", "graphene-django", "pytest", "pytest-django", "tox (==4.*)"] +maintainer = ["django", "zest.releaser[recommended]"] +pyuca = ["pyuca"] +test = ["djangorestframework", "graphene-django", "pytest", "pytest-cov", "pytest-django"] + [[package]] name = "executing" version = "2.2.0" @@ -595,4 +616,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "71538870f4b5e254e94790c04312bf8e89df9da303a394cf5b9e6a236aee6251" +content-hash = "3644693c682e70c01ed88ace18501aeee83dfe38fdd56c318babbff6c0b325b9" diff --git a/pyproject.toml b/pyproject.toml index cfe53a8..efb645b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ psycopg2-binary = "^2.9.10" pillow = "^11.3.0" ipython = "^9.4.0" redis = "^6.2.0" +django-countries = "^7.6.1" [build-system] diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 0000000..938f8ce --- /dev/null +++ b/users/forms.py @@ -0,0 +1,95 @@ +from django import forms +from django.contrib.auth.forms import AuthenticationForm, UserChangeForm, UserCreationForm +from django.forms import ModelForm +from django.urls import reverse_lazy + +from mailing.forms import StyleFormMixin +from users.models import User + + +class UserRegistrationForm(StyleFormMixin, UserCreationForm): + class Meta: + model = User + template_name = "users/user_form.html" + fields = ("email", "password1", "password2") + + def clean_email(self): + """ + Проверка email на уникальность + """ + email = self.cleaned_data.get("email") + if User.objects.filter(email=email).exists(): + raise forms.ValidationError("Такой email уже используется в системе") + return email + + +class UserForm(StyleFormMixin, UserChangeForm): + class Meta: + model = User + fields = ( + "first_name", + "last_name", + "email", + "password", + "phone_number", + "avatar", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + phone_number = self.fields["phone_number"].widget + + self.fields["password"].widget = forms.HiddenInput() + phone_number.attrs["class"] = "form-control bfh-phone" + phone_number.attrs["data-format"] = "+7 (ddd) ddd-dd-dd" + + +class UserUpdateForm(StyleFormMixin, ModelForm): + class Meta: + model = User + fields = ( + "first_name", + "last_name", + "email", + "password", + "phone_number", + "country", + "is_active", + "is_superuser", + "is_staff", + "avatar", + ) + success_url = reverse_lazy("users:users") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + phone_number = self.fields["phone_number"].widget + + self.fields["password"].widget = forms.HiddenInput() + phone_number.attrs["class"] = "form-control bfh-phone" + phone_number.attrs["data-format"] = "+7 (ddd) ddd-dd-dd" + + def clean_email(self): + email = self.cleaned_data.get("email") + + if User.objects.filter(email=email).exclude(pk=self.instance.pk).exists(): + raise forms.ValidationError("Пользователь с таким Email уже существует.") + + return email + + +class PasswordRecoveryForm(StyleFormMixin, forms.Form): + email = forms.EmailField(label="Укажите Email") + + def clean_email(self): + """ + Проверка email на уникальность + """ + email = self.cleaned_data.get("email") + if not User.objects.filter(email=email).exists(): + raise forms.ValidationError("Такого email нет в системе") + return email + + +class UserLoginForm(StyleFormMixin, AuthenticationForm): + model = User diff --git a/users/management/__init__.py b/users/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/management/commands/__init__.py b/users/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/management/commands/create_manager.py b/users/management/commands/create_manager.py new file mode 100644 index 0000000..10c5b66 --- /dev/null +++ b/users/management/commands/create_manager.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import Group +from django.core.management import BaseCommand + +from users.models import User + + +class Command(BaseCommand): + def handle(self, *args, **options): + email = "manager1@example.com" + password = "123" + user = User.objects.create(email=email) + user.set_password(password) + user.is_active = True + user.is_superuser = False + user.is_staff = False + users_group, created = Group.objects.get_or_create(name="Менеджеры") + user.groups.add(users_group) + user.save() + self.stdout.write( + self.style.SUCCESS( + f'Пользователь добавлен в группу "Менеджеры"\n' + f'email для входа: {email}\n' + f'пароль: {password}' + ) + ) diff --git a/users/management/commands/create_user.py b/users/management/commands/create_user.py new file mode 100644 index 0000000..900a19d --- /dev/null +++ b/users/management/commands/create_user.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import Group +from django.core.management import BaseCommand + +from users.models import User + + +class Command(BaseCommand): + def handle(self, *args, **options): + email = "user1@example.com" + password = "123" + user = User.objects.create(email=email) + user.set_password(password) + user.is_active = True + user.is_superuser = False + user.is_staff = False + users_group, created = Group.objects.get_or_create(name="Пользователи") + user.groups.add(users_group) + user.save() + self.stdout.write( + self.style.SUCCESS( + f'Пользователь добавлен в группу "Пользователи"\n' + f'email для входа: {email}\n' + f'пароль: {password}' + ) + ) diff --git a/users/management/commands/csu.py b/users/management/commands/csu.py new file mode 100644 index 0000000..e8afdc7 --- /dev/null +++ b/users/management/commands/csu.py @@ -0,0 +1,22 @@ +from django.core.management import BaseCommand + +from users.models import User + + +class Command(BaseCommand): + def handle(self, *args, **options): + email = "admin@example.com" + password = "123" + user = User.objects.create(email=email) + user.set_password(password) + user.is_active = True + user.is_superuser = True + user.is_staff = True + user.save() + self.stdout.write( + self.style.SUCCESS( + f"Создан администратор\n" + f"email для входа: {email}\n" + f"пароль: {password}" + ) + ) diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..23ba2c5 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.4 on 2025-07-08 20:24 + +import django.contrib.auth.models +import django.utils.timezone +import django_countries.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), + ('first_name', models.CharField(max_length=50, verbose_name='Имя')), + ('last_name', models.CharField(max_length=50, verbose_name='Фамилия')), + ('middle_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Отчество')), + ('phone_number', models.CharField(blank=True, help_text='Введите номер телефона', max_length=20, null=True, verbose_name='Номер телефона')), + ('country', django_countries.fields.CountryField(blank=True, max_length=50, null=True)), + ('avatar', models.ImageField(blank=True, null=True, upload_to='users/avatars', verbose_name='Аватар')), + ('token', models.CharField(blank=True, max_length=100, null=True, verbose_name='Токен')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'Пользователь', + 'verbose_name_plural': 'Пользователи', + 'permissions': [('can_block_user', 'Возможность блокировки пользователя')], + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/users/models.py b/users/models.py index 71a8362..b8f54e9 100644 --- a/users/models.py +++ b/users/models.py @@ -1,3 +1,32 @@ +from django.contrib.auth.models import AbstractUser from django.db import models +from django_countries.fields import CountryField -# Create your models here. +NULLABLE = {"blank": True, "null": True} + + +class User(AbstractUser): + username = None + email = models.EmailField(unique=True, verbose_name="Email") + first_name = models.CharField(max_length=50, verbose_name="Имя") + last_name = models.CharField(max_length=50, verbose_name="Фамилия") + middle_name = models.CharField(max_length=50, verbose_name="Отчество", **NULLABLE) + phone_number = models.CharField( + max_length=20, verbose_name="Номер телефона", **NULLABLE, help_text="Введите номер телефона" + ) + country = CountryField(max_length=50, blank_label="(выберите страну)", **NULLABLE) + avatar = models.ImageField(upload_to="users/avatars", **NULLABLE, verbose_name="Аватар") + token = models.CharField(max_length=100, verbose_name="Токен", **NULLABLE) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = [] + + def __str__(self): + return self.email + + class Meta: + verbose_name = "Пользователь" + verbose_name_plural = "Пользователи" + permissions = [ + ("can_block_user", "Возможность блокировки пользователя"), + ] diff --git a/users/services.py b/users/services.py new file mode 100644 index 0000000..650fdf4 --- /dev/null +++ b/users/services.py @@ -0,0 +1,21 @@ +from django.contrib.auth.decorators import permission_required +from django.http import HttpResponseRedirect +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse + +from users.models import User + + +def email_verification(request, token): + user = get_object_or_404(User, token=token) + user.is_active = True + user.save() + return HttpResponseRedirect(reverse("users:login")) + + +@permission_required("users.view_user") +def block_user(self, pk): + user = User.objects.get(pk=pk) + user.is_active = {user.is_active: False, not user.is_active: True}[True] + user.save() + return redirect(reverse("users:users")) \ No newline at end of file diff --git a/users/templates/registration/login.html b/users/templates/registration/login.html new file mode 100644 index 0000000..8487c06 --- /dev/null +++ b/users/templates/registration/login.html @@ -0,0 +1,30 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + + Отмена + + + +
+ +
+
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/users/templates/users/email_confirmation.html b/users/templates/users/email_confirmation.html new file mode 100644 index 0000000..7b8d752 --- /dev/null +++ b/users/templates/users/email_confirmation.html @@ -0,0 +1,25 @@ +{% extends 'mailing/base.html' %} + +{% block content %} + +
+

+
+ +{% endblock %} \ No newline at end of file diff --git a/users/templates/users/password_recovery.html b/users/templates/users/password_recovery.html new file mode 100644 index 0000000..f7e80b6 --- /dev/null +++ b/users/templates/users/password_recovery.html @@ -0,0 +1,24 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+

Восстановление пароля

+ +
+
+
+
+
+
+

На почту будет отправлено письмо с новым паролем

+ {% csrf_token %} + {{ form.as_p }} +
+
+ +
+ +
+
+ +
+{% endblock %} \ No newline at end of file diff --git a/users/templates/users/reset_password.html b/users/templates/users/reset_password.html new file mode 100644 index 0000000..30eca87 --- /dev/null +++ b/users/templates/users/reset_password.html @@ -0,0 +1,24 @@ +{% extends 'mailing/base.html' %} + +{% block content %} + +
+

+
+ +{% endblock %} \ No newline at end of file diff --git a/users/templates/users/user_confirm_delete.html b/users/templates/users/user_confirm_delete.html new file mode 100644 index 0000000..0fa3a98 --- /dev/null +++ b/users/templates/users/user_confirm_delete.html @@ -0,0 +1,21 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+

Вы действительно хотите удалить {{object.email}}:

+

{{ object.first_sending }}

+
+ {% csrf_token %} +
+ + Отмена +
+
+
+
+
+{% endblock %} diff --git a/users/templates/users/user_detail.html b/users/templates/users/user_detail.html new file mode 100644 index 0000000..7981c2e --- /dev/null +++ b/users/templates/users/user_detail.html @@ -0,0 +1,35 @@ +{% extends 'mailing/base.html' %} +{% load users_tags %} +{% block content %} +
+
+
+
+
+
+ Фото +

{{ object.email }}

+

Имя: {{ object.first_name }}

+

Фамилия: {{ object.last_name }}

+

Телефон: {{ object.phone_number }}

+ +
+
+ Редактировать + + Назад + {% if user.is_superuser %} + Удалить + {% endif %} +
+ +
+
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/users/templates/users/user_form.html b/users/templates/users/user_form.html new file mode 100644 index 0000000..e6efbc5 --- /dev/null +++ b/users/templates/users/user_form.html @@ -0,0 +1,34 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + + + {% if user.is_superuser%} + Отмена + {% else %} + Отмена + {% endif %} + + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/users/templates/users/user_list.html b/users/templates/users/user_list.html new file mode 100644 index 0000000..1b478e2 --- /dev/null +++ b/users/templates/users/user_list.html @@ -0,0 +1,62 @@ +{% extends 'mailing/base.html' %} +{% block content %} +{% load my_tags %} +
+ +
+
+

Список пользователей

+ + + + + + + + + + + {% if user.is_superuser %} + + {% endif %} + + + + + {% for object in object_list %} + + + + + + + {% if user.is_superuser or user|in_group:"Менеджеры" %} + + {% endif %} + + {% if user.is_superuser %} + + {% endif %} + + {% endfor%} +
emailИмяФамилияТелефонАдминистраторАктивностьПоследнее подключениеДействия
{{ object.email }}{{ object.first_name }}{{ object.last_name }}{{ object.phone_number }}{{ object.is_superuser|yesno:"Да, Нет"}} + {% if object.is_active %} + Заблокировать + {% else %} + Разблокировать + {% endif %} + {% else %} + {{object.is_active|yesno:"Активный, Не активный"}} + {{ object.last_login }} + Редактировать + Удалить +
+ +
+
+
+{% endblock %}} diff --git a/users/templatetags/__init__.py b/users/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/templatetags/users_tags.py b/users/templatetags/users_tags.py new file mode 100644 index 0000000..e69de29 diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..ddf0667 --- /dev/null +++ b/users/urls.py @@ -0,0 +1,35 @@ +from django.contrib.auth.views import LogoutView +from django.urls import path + +from users.apps import UsersConfig +from users.services import block_user, email_verification +from users.views import ( + EmailConfirmationView, + PasswordRecoveryView, + UserCreateView, + UserDeleteView, + UserDetailView, + UserListView, + UserLoginView, + UserUpdateView, +) + +app_name = UsersConfig.name + +urlpatterns = [ + path("login/", UserLoginView.as_view(), name="login"), + path("logout/", LogoutView.as_view(), name="logout"), + path("register/", UserCreateView.as_view(), name="register"), + path("users/", UserListView.as_view(), name="users"), + path("detail//", UserDetailView.as_view(), name="detail"), + path("update//", UserUpdateView.as_view(), name="update"), + path("delete//", UserDeleteView.as_view(), name="delete"), + path("email-confirm//", email_verification, name="email-confirm"), + path( + "email-confirmation/", + EmailConfirmationView.as_view(), + name="email_confirmation", + ), + path("password-recovery/", PasswordRecoveryView.as_view(), name="password_recovery"), + path("block_user/", block_user, name="block_user"), +] diff --git a/users/views.py b/users/views.py index 91ea44a..045cca6 100644 --- a/users/views.py +++ b/users/views.py @@ -1,3 +1,125 @@ -from django.shortcuts import render +import secrets -# Create your views here. +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.contrib.auth.views import LoginView +from django.core.exceptions import PermissionDenied +from django.core.mail import send_mail +from django.urls import reverse_lazy +from django.utils.crypto import get_random_string +from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, TemplateView, UpdateView + +from config.settings import EMAIL_HOST_USER +from users.forms import PasswordRecoveryForm, UserLoginForm, UserRegistrationForm, UserUpdateForm +from users.models import User + + +class UserCreateView(CreateView): + model = User + form_class = UserRegistrationForm + success_url = reverse_lazy("users:email_confirmation") + + def form_valid(self, form): + user = form.save() + user.is_active = False + token = secrets.token_hex(16) + host = self.request.get_host() + url = f"http://{host}/users/email-confirm/{token}/" + user.token = token + user.save() + send_mail( + subject="Подтверждение почты", + message=f"Здравствуйте, перейдите по ссылке для подтверждения почты: {url} ", + from_email=EMAIL_HOST_USER, + recipient_list=[user.email], + ) + return super().form_valid(form) + + +class UserLoginView(LoginView): + model = User + form_class = UserLoginForm + + +class UserListView(LoginRequiredMixin, UserPassesTestMixin, ListView): + model = User + template_name = "users/user_list.html" + + def test_func(self): + return self.request.user.groups.filter(name="Менеджеры").exists() or self.request.user.is_superuser + + +class UserDetailView(LoginRequiredMixin, DetailView): + model = User + form_class = UserUpdateForm + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if self.request.user.is_superuser: + return self.object + raise PermissionDenied + + +class UserUpdateView(LoginRequiredMixin, UpdateView): + model = User + form_class = UserUpdateForm + + def get_success_url(self): + if self.request.user.is_superuser: + return reverse_lazy("users:users") + else: + return reverse_lazy("mailing:index") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class UserDeleteView(LoginRequiredMixin, DeleteView): + model = User + + def get_success_url(self): + if self.request.user.is_superuser: + return reverse_lazy("users:users") + else: + return reverse_lazy("mailing:index") + + def get_object(self, queryset=None): + self.object = super().get_object(queryset) + if not self.request.user.is_superuser: + raise PermissionDenied + return self.object + + +class EmailConfirmationView(TemplateView): + model = User + template_name = "users/email_confirmation.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Письмо активации отправлено" + return context + + +class PasswordRecoveryView(FormView): + template_name = "users/password_recovery.html" + form_class = PasswordRecoveryForm + success_url = reverse_lazy("users:login") + + def form_valid(self, form): + email = form.cleaned_data["email"] + user = User.objects.get(email=email) + length = 12 + alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + password = get_random_string(length, alphabet) + user.set_password(password) + user.save() + send_mail( + subject="Восстановление пароля", + message=f"Ваш новый пароль: {password}", + from_email=EMAIL_HOST_USER, + recipient_list=[user.email], + fail_silently=False, + ) + return super().form_valid(form) From 302fa270879c09c5120bc2698778eaba6f593a94 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 8 Jul 2025 23:56:05 +0300 Subject: [PATCH 03/10] working on finish --- mailing/templates/mailing/base.html | 2 +- static/{icons => image}/bootstrap.svg | 0 users/templatetags/users_tags.py | 10 ++++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) rename static/{icons => image}/bootstrap.svg (100%) diff --git a/mailing/templates/mailing/base.html b/mailing/templates/mailing/base.html index 8381ca2..97ed507 100644 --- a/mailing/templates/mailing/base.html +++ b/mailing/templates/mailing/base.html @@ -1,4 +1,4 @@ -{% load static%} +{% load static %} diff --git a/static/icons/bootstrap.svg b/static/image/bootstrap.svg similarity index 100% rename from static/icons/bootstrap.svg rename to static/image/bootstrap.svg diff --git a/users/templatetags/users_tags.py b/users/templatetags/users_tags.py index e69de29..ebca95b 100644 --- a/users/templatetags/users_tags.py +++ b/users/templatetags/users_tags.py @@ -0,0 +1,10 @@ +from django import template + +register = template.Library() + + +@register.filter() +def user_media_filter(path): + if path: + return f"/media/{path}" + return "#" From c739079ec87be123e096e7e1eaff631215147233 Mon Sep 17 00:00:00 2001 From: Admin Date: Wed, 9 Jul 2025 00:52:30 +0300 Subject: [PATCH 04/10] working on flace8 and readme --- .flake8 | 4 + README.md | Bin 2166 -> 9644 bytes config/urls.py | 5 +- groups.json | 1 + mailing/admin.py | 9 +- mailing/management/commands/send_mailings.py | 4 +- mailing/models.py | 86 +++++++++++++------ mailing/templatetags/my_tags.py | 2 +- mailing/tests.py | 3 - output_flake8.txt | 5 ++ poetry.lock | 51 ++++++++++- pyproject.toml | 1 + users/admin.py | 3 - users/models.py | 38 ++++++-- users/services.py | 2 +- users/tests.py | 3 - users/views.py | 15 +++- 17 files changed, 181 insertions(+), 51 deletions(-) create mode 100644 .flake8 create mode 100644 groups.json create mode 100644 output_flake8.txt diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..5e1fd16 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 100 +ignore = E203,W503 +exclude = .git,__pycache__,env,.env,venv,.venv,migrations \ No newline at end of file diff --git a/README.md b/README.md index ad9b6462ef12a093c36b24c1e854cc98ffba048c..250d9b765ecfeea83491c718e1fa4fb2914fd8a5 100644 GIT binary patch literal 9644 zcmb`N+io1k5r+F>t^(vDhJXl+B@N90p^T7PQe1Jw!;(uR=>LoG z_wXcahF^zYg@x*{+ONjds9LWE)uXBo`}$nd_XB-0Vnr)l)0j~;thUrzhmEkRwncs4 z*Q`dfwMVt5Zv%bbR{O46eXX(jCu-eN?~dAi%}+G$Ahy=wJ4yDFUf)Z)bIG9`y8FkL%W26|I;(tnO&mNb-%Un_9c6 zeD-WlclKpzwio9fSDX5D35M+&Ak*L9U8$Drn-M=l9;{3bFzxo(Y}4S1`tRw5{JHHW z?fkrdCA00M>dLSrX|XwIthaXA)OWLRBioJQh+%cRUHfom#!8qsqq`f(vtJ{vXOTOY z)0=C4H;luV!BF~ZcozN<^4_n-BzSe!j(DU0R^s=3)PnZji8^oSy$*2LdF~e2D#omI z#^kHPhXaki7kRif)*U$KH;9hd*w{oe!$x_pXhUCe=~+n=Eu1 zUdRf6luqaXB8E1&v*x7`hOpoW(;jKIoM&**s z`gv2m3tO7&h~5c5h*7UpNgyA%5o5@wV1bV|@tb?+a4Iv*{C$i7m*e}@_+GBz`Z3mW z$ByJBdJH1&;Iu4{p;BURAK$MZ{7Nx*(9z^lv>~1gZtKwtcVbnwr}e44EK1M%^0Iv5 znSAR+uUEq3M48d-g52}h1t($`^@Qh!U%L8Y#Nt3!o6RahUi({xfCl#?c4nD8Dq0KU z_9A~kc+kciG#~@zc?myT7DBsP?|zgVJ9vcl9E3OJ?3!KWU4hdq`;7B%!;bc_YSBEL zjF8%PWJkC*mW3Y4i{UI-i9$(dd)HPl!+|z%=AS{DLJWj(j@aXeP*wppH z|A$N6HA*WI<$f3Cve;v>)GGZ+v{b2g7e9}ZNc$nI<==H3zKB?uuenvp`gp}aIfZHl zAITB_d36Dm2eZ2QVA#e?tM~9`KeIK_LXCXpNUi&kk5ozc0PA42ofw_h;yDppkrClP zC$(Hg`iLw^!TFNV_xt3ZJ9=udQW4)M9KH4in`c6CNg z;@F%|WtO|FSxv0`2Jsz?x^Z(g=0nd?AGRV0T9pXH$1(1^?Mxn6vN@x@M@`NNgXkSL z*U-4D7BbGPeE3TlRl&z=R?7=p!Ut&#_bvmPHbSE#jJ+x=Sp{n~1Q@|F>{(`Y>WdPe zMa?C8k&!Ht=T*8}+)fY^v8P*~!z8P4aywXEX0f5vbr^BP3zNil4l(p#5jl9G>I>X= z^*7PY!K`cvGuX&DV_MgzO9m0k*qh1@yUkkTk#c7q2QCRic;=k&s8`mTFdoVA81S^U z(Zj5;t2Gk8a;$8m#}3OV@P>J0vIoj6cyNC1Nj}R>)2XVEC(~+dxjmiZ(I!(~>l+&M zeG;X0e!!QmzxQ#sE9b25m|s{%#FBYFPI8xi`J^~Yk;yzJ!Awx4?z374RLJS2wC~!w zt6#H`Vpt^hb6gjNAbBO(11p09xF%*T2um`b z-Ey*r*BX{Jat|~FjV;FgqqyE!@B7l)(IOHV%BD^C4g;Hx<*7sMoK9sWrzh$KH?wDs zsP*KM8Y_^uJk8r3jopjFHR3fn)NYJG7QUvHh^r|A*-ew5c*l{I#6gePxsT7@ZJ5Q0 zO||B5hYI?{GP1niR1Aa~jNq))s1mSB$VVXA>|GCeA&+^i-*;NZD(p$+dni4Ty(7-WtwD*Ny{-% zyP`cfv)OpNO-2j$-iUF8tYUXf@5c-csbTN{B3}8#VNb2pePqYPa9Fe$XTUz@dL@n? z?2D%yNS~Z<{W22~*EAo7o20%5?X>co!h7aby|W(L&2?aVBgXhLCQx;ho?KR9Rx|Z` zlzF2^=zbYM!)p<%n7r0O^ZeXR)y7)9!0oEM4*iK6;OX%r&ryQ{;cL-S-8)|Jgbg=i z%ph7OdHj}&+7B;82j@MkVlFb|_bS#(Pgxz&p1kAJC^2m0j&xsKD5XFKm`6S+Z`tq! za%*>IcI@s7U-4gssx&O1E%}(k|U`d&F}5%+EogUt8x*u4`hG#kfY^ zQQ8w^4e#9kDbrc(E@kMi8O*RP$n@i+N+(}4Wo3h5VcCLIZ>GQh2-{;M~ zUi%UK^1HO(hyh^adB^Mid3g|2@D6(_n6GqIeDOQCWUYQ%c*I!HTGYT;B|SZLwprf5 zm+cmpn*_T}GYNRdkKl6G&(d?Ut=>J-IStgvWOf^A6^r>g=M=IH*_FKOcgB`KlKt&D z-4)^5|0EV|{b}A()W$QHp-mOg_mq`+Yw=n8LbMWoLG@`>fO!wnZ$|m3fgAOik3N;3 zzSeUWzP->b@EdvC^Z0!IRA;lgZQE}@U+VV(-)g7ldM@*W?$gH_c@q8^Ci?zwt-`ar zGd-cdG55CwtL;i@cm7+C(w90LmVOTmAFUcNRMUKy{#-kAlYOBlNwqXNjZcG~Ny6`= zc4%t;b~EmlVvpl=Bz$iR!CL*COMlWjb_OMT;6bp*M^WAQxK-`-Oo*IEJd9<;dTNh# zaS>La3H6d^JaVkv&{L1%2`PWVCeKnSQv0tWOcU1|TbxIo=Sk!32{ZolUuSp(HlN{H zG5f$F^Cz+xoUvbd*xuhtTi>6({mES&tI1RKz7T?|tz6GF_Q#G5h8-K&?){O;YRWoL zHIQjQP0zdM%UEZggsO3JfCdnWQkGIKw1k*x-9w?2a8-8?Y)XfpFu%kl3@o_;4_ zK2%0ibh8omi-|hw?eXZhEpxfOAPQQh%b$zd$-wWF?Y5mA?AJhhakW20E^(r;bH#R> z9c|zATMT3&sy|k2`9`0dNbGs0$9C=*IVoCH|0>*3EIJBb=rhf&7`M>b>4Q}N9iXVPAdUYP$@jylZGGS0X4H}Ad3OB<|D=dA1@BE diff --git a/config/urls.py b/config/urls.py index 19c8fc0..1fae406 100644 --- a/config/urls.py +++ b/config/urls.py @@ -11,4 +11,7 @@ ] if settings.DEBUG: - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static( + settings.MEDIA_URL, + document_root=settings.MEDIA_ROOT + ) diff --git a/groups.json b/groups.json new file mode 100644 index 0000000..4fa4ce5 --- /dev/null +++ b/groups.json @@ -0,0 +1 @@ +[{"model": "auth.permission", "pk": 1, "fields": {"name": "Can add log entry", "content_type": 1, "codename": "add_logentry"}}, {"model": "auth.permission", "pk": 2, "fields": {"name": "Can change log entry", "content_type": 1, "codename": "change_logentry"}}, {"model": "auth.permission", "pk": 3, "fields": {"name": "Can delete log entry", "content_type": 1, "codename": "delete_logentry"}}, {"model": "auth.permission", "pk": 4, "fields": {"name": "Can view log entry", "content_type": 1, "codename": "view_logentry"}}, {"model": "auth.permission", "pk": 5, "fields": {"name": "Can add permission", "content_type": 2, "codename": "add_permission"}}, {"model": "auth.permission", "pk": 6, "fields": {"name": "Can change permission", "content_type": 2, "codename": "change_permission"}}, {"model": "auth.permission", "pk": 7, "fields": {"name": "Can delete permission", "content_type": 2, "codename": "delete_permission"}}, {"model": "auth.permission", "pk": 8, "fields": {"name": "Can view permission", "content_type": 2, "codename": "view_permission"}}, {"model": "auth.permission", "pk": 9, "fields": {"name": "Can add group", "content_type": 3, "codename": "add_group"}}, {"model": "auth.permission", "pk": 10, "fields": {"name": "Can change group", "content_type": 3, "codename": "change_group"}}, {"model": "auth.permission", "pk": 11, "fields": {"name": "Can delete group", "content_type": 3, "codename": "delete_group"}}, {"model": "auth.permission", "pk": 12, "fields": {"name": "Can view group", "content_type": 3, "codename": "view_group"}}, {"model": "auth.permission", "pk": 13, "fields": {"name": "Can add content type", "content_type": 5, "codename": "add_contenttype"}}, {"model": "auth.permission", "pk": 14, "fields": {"name": "Can change content type", "content_type": 5, "codename": "change_contenttype"}}, {"model": "auth.permission", "pk": 15, "fields": {"name": "Can delete content type", "content_type": 5, "codename": "delete_contenttype"}}, {"model": "auth.permission", "pk": 16, "fields": {"name": "Can view content type", "content_type": 5, "codename": "view_contenttype"}}, {"model": "auth.permission", "pk": 17, "fields": {"name": "Can add session", "content_type": 6, "codename": "add_session"}}, {"model": "auth.permission", "pk": 18, "fields": {"name": "Can change session", "content_type": 6, "codename": "change_session"}}, {"model": "auth.permission", "pk": 19, "fields": {"name": "Can delete session", "content_type": 6, "codename": "delete_session"}}, {"model": "auth.permission", "pk": 20, "fields": {"name": "Can view session", "content_type": 6, "codename": "view_session"}}, {"model": "auth.permission", "pk": 21, "fields": {"name": "Can add ╤ююс∙хэшх", "content_type": 7, "codename": "add_message"}}, {"model": "auth.permission", "pk": 22, "fields": {"name": "Can change ╤ююс∙хэшх", "content_type": 7, "codename": "change_message"}}, {"model": "auth.permission", "pk": 23, "fields": {"name": "Can delete ╤ююс∙хэшх", "content_type": 7, "codename": "delete_message"}}, {"model": "auth.permission", "pk": 24, "fields": {"name": "Can view ╤ююс∙хэшх", "content_type": 7, "codename": "view_message"}}, {"model": "auth.permission", "pk": 25, "fields": {"name": "Can add ╧юыєўрЄхы№ Ёрёё√ыъш", "content_type": 8, "codename": "add_recipientmailing"}}, {"model": "auth.permission", "pk": 26, "fields": {"name": "Can change ╧юыєўрЄхы№ Ёрёё√ыъш", "content_type": 8, "codename": "change_recipientmailing"}}, {"model": "auth.permission", "pk": 27, "fields": {"name": "Can delete ╧юыєўрЄхы№ Ёрёё√ыъш", "content_type": 8, "codename": "delete_recipientmailing"}}, {"model": "auth.permission", "pk": 28, "fields": {"name": "Can view ╧юыєўрЄхы№ Ёрёё√ыъш", "content_type": 8, "codename": "view_recipientmailing"}}, {"model": "auth.permission", "pk": 29, "fields": {"name": "Can add ╨рёё√ыър", "content_type": 9, "codename": "add_mailing"}}, {"model": "auth.permission", "pk": 30, "fields": {"name": "Can change ╨рёё√ыър", "content_type": 9, "codename": "change_mailing"}}, {"model": "auth.permission", "pk": 31, "fields": {"name": "Can delete ╨рёё√ыър", "content_type": 9, "codename": "delete_mailing"}}, {"model": "auth.permission", "pk": 32, "fields": {"name": "Can view ╨рёё√ыър", "content_type": 9, "codename": "view_mailing"}}, {"model": "auth.permission", "pk": 33, "fields": {"name": "Can add ╧юя√Єър", "content_type": 10, "codename": "add_mailingattempt"}}, {"model": "auth.permission", "pk": 34, "fields": {"name": "Can change ╧юя√Єър", "content_type": 10, "codename": "change_mailingattempt"}}, {"model": "auth.permission", "pk": 35, "fields": {"name": "Can delete ╧юя√Єър", "content_type": 10, "codename": "delete_mailingattempt"}}, {"model": "auth.permission", "pk": 36, "fields": {"name": "Can view ╧юя√Єър", "content_type": 10, "codename": "view_mailingattempt"}}, {"model": "auth.permission", "pk": 37, "fields": {"name": "Can add ╧юы№чютрЄхы№", "content_type": 11, "codename": "add_user"}}, {"model": "auth.permission", "pk": 38, "fields": {"name": "Can change ╧юы№чютрЄхы№", "content_type": 11, "codename": "change_user"}}, {"model": "auth.permission", "pk": 39, "fields": {"name": "Can delete ╧юы№чютрЄхы№", "content_type": 11, "codename": "delete_user"}}, {"model": "auth.permission", "pk": 40, "fields": {"name": "Can view ╧юы№чютрЄхы№", "content_type": 11, "codename": "view_user"}}, {"model": "auth.permission", "pk": 41, "fields": {"name": "┬ючьюцэюёЄ№ юЄъы■ўхэш  Ёрёё√ыъш", "content_type": 9, "codename": "can_disable_mailing"}}, {"model": "auth.permission", "pk": 42, "fields": {"name": "┬ючьюцэюёЄ№ сыюъшЁютъш яюы№чютрЄхы ", "content_type": 11, "codename": "can_block_user"}}, {"model": "auth.group", "pk": 1, "fields": {"name": "╧юы№чютрЄхыш", "permissions": [29, 30, 31, 32, 36, 25, 26, 27, 28]}}, {"model": "auth.group", "pk": 2, "fields": {"name": "╠хэхфцхЁ√", "permissions": [41, 30, 32, 28, 42, 38, 40]}}] \ No newline at end of file diff --git a/mailing/admin.py b/mailing/admin.py index 22eff2d..69fe72b 100644 --- a/mailing/admin.py +++ b/mailing/admin.py @@ -32,7 +32,14 @@ class MessageAdmin(admin.ModelAdmin): @admin.register(Mailing) class MailingAdmin(admin.ModelAdmin): - list_display = ("id", "first_sending", "end_sending", "status", "message", "owner") + list_display = ( + "id", + "first_sending", + "end_sending", + "status", + "message", + "owner" + ) search_fields = ("status",) list_filter = ("status",) diff --git a/mailing/management/commands/send_mailings.py b/mailing/management/commands/send_mailings.py index a7fd040..f4ea591 100644 --- a/mailing/management/commands/send_mailings.py +++ b/mailing/management/commands/send_mailings.py @@ -27,7 +27,9 @@ def handle(self, *args, **kwargs): server_response="Email отправлен", mailing=mailing, ) - print(f"Сообщение {mailing.message.subject} успешно отправлено на {recipient.email}") + print( + f"Сообщение {mailing.message.subject}" + f"успешно отправлено на {recipient.email}") except Exception as e: MailingAttempt.objects.create( date_attempt=timezone.now(), diff --git a/mailing/models.py b/mailing/models.py index 17adc32..c5873ce 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -6,12 +6,28 @@ class RecipientMailing(models.Model): - email = models.EmailField(max_length=255, unique=True, verbose_name="E-mail") - fio = models.CharField(max_length=255, verbose_name="ФИО") - comment = models.TextField(verbose_name="Комментарий", **NULLABLE) - avatar = models.ImageField(upload_to="mailing/avatars", **NULLABLE, verbose_name="Аватар") - is_active = models.BooleanField(default=True, verbose_name="активность") - owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + email = models.EmailField( + max_length=255, + unique=True, + verbose_name="E-mail") + fio = models.CharField( + max_length=255, + verbose_name="ФИО") + comment = models.TextField( + verbose_name="Комментарий", + **NULLABLE) + avatar = models.ImageField( + upload_to="mailing/avatars", + **NULLABLE, + verbose_name="Аватар") + is_active = models.BooleanField( + default=True, + verbose_name="активность") + owner = models.ForeignKey( + User, + on_delete=models.SET_NULL, + **NULLABLE, + verbose_name="Владелец") def __str__(self): return f"{self.fio} <{self.email}>" @@ -23,9 +39,16 @@ class Meta: class Message(models.Model): - subject = models.CharField(max_length=255, verbose_name="Тема сообщения") - content = models.TextField(verbose_name="Содержание сообщения") - owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + subject = models.CharField( + max_length=255, + verbose_name="Тема сообщения") + content = models.TextField( + verbose_name="Содержание сообщения") + owner = models.ForeignKey( + User, + on_delete=models.SET_NULL, + **NULLABLE, + verbose_name="Владелец") def __str__(self): return self.content @@ -47,28 +70,33 @@ class Mailing(models.Model): (COMPLETED, "Завершена"), ] - first_sending = models.DateTimeField(verbose_name="Дата и время первого отправления") - end_sending = models.DateTimeField(verbose_name="Дата и время окончания отправки") + first_sending = models.DateTimeField( + verbose_name="Дата и время первого отправления") + end_sending = models.DateTimeField( + verbose_name="Дата и время окончания отправки") status = models.CharField( max_length=10, choices=STATUS_CHOICES, default=CREATED, - verbose_name="Статус рассылки", - ) + verbose_name="Статус рассылки", ) message = models.ForeignKey( Message, on_delete=models.CASCADE, verbose_name="Сообщение", - related_name="mailings", - ) + related_name="mailings", ) recipients = models.ManyToManyField( RecipientMailing, related_name="recipients", verbose_name="Получатели", - help_text="Укажите получателей рассылки (используйте CTRL или COMMAND)", - ) - is_active = models.BooleanField(default=True, verbose_name="активна") - owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + help_text="Укажите получателей рассылки (используйте CTRL или COMMAND)", ) + is_active = models.BooleanField( + default=True, + verbose_name="активна") + owner = models.ForeignKey( + User, + on_delete=models.SET_NULL, + **NULLABLE, + verbose_name="Владелец") def __str__(self): return f"Рассылка {self.id}" @@ -93,16 +121,24 @@ class MailingAttempt(models.Model): (STATUS_NOK, "Не успешно"), ] - date_attempt = models.DateTimeField(verbose_name="Дата и время попытки") - status = models.CharField(max_length=15, choices=STATUS_CHOICES, verbose_name="Статус попытки") - server_response = models.TextField(verbose_name="Ответ почтового сервера") + date_attempt = models.DateTimeField( + verbose_name="Дата и время попытки") + status = models.CharField( + max_length=15, + choices=STATUS_CHOICES, + verbose_name="Статус попытки") + server_response = models.TextField( + verbose_name="Ответ почтового сервера") mailing = models.ForeignKey( Mailing, on_delete=models.CASCADE, verbose_name="Рассылка", - related_name="mailing", - ) - owner = models.ForeignKey(User, on_delete=models.SET_NULL, **NULLABLE, verbose_name="Владелец") + related_name="mailing",) + owner = models.ForeignKey( + User, + on_delete=models.SET_NULL, + **NULLABLE, + verbose_name="Владелец") def __str__(self): return f"{self.date_attempt} <{self.status}>" diff --git a/mailing/templatetags/my_tags.py b/mailing/templatetags/my_tags.py index 824f7f5..2d5c77f 100644 --- a/mailing/templatetags/my_tags.py +++ b/mailing/templatetags/my_tags.py @@ -12,4 +12,4 @@ def media_filter(path): @register.filter def in_group(user, group_name): - return user.groups.filter(name=group_name).exists() \ No newline at end of file + return user.groups.filter(name=group_name).exists() diff --git a/mailing/tests.py b/mailing/tests.py index 7ce503c..e69de29 100644 --- a/mailing/tests.py +++ b/mailing/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/output_flake8.txt b/output_flake8.txt new file mode 100644 index 0000000..836f002 --- /dev/null +++ b/output_flake8.txt @@ -0,0 +1,5 @@ +.\mailing\views.py:6:101: E501 line too long (103 > 100 characters) +.\mailing\views.py:30:101: E501 line too long (104 > 100 characters) +.\mailing\views.py:62:101: E501 line too long (110 > 100 characters) +.\mailing\views.py:129:101: E501 line too long (110 > 100 characters) +.\users\views.py:59:101: E501 line too long (107 > 100 characters) diff --git a/poetry.lock b/poetry.lock index e76bcce..a712837 100644 --- a/poetry.lock +++ b/poetry.lock @@ -117,6 +117,22 @@ files = [ [package.extras] tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] +[[package]] +name = "flake8" +version = "7.3.0" +description = "the modular source code checker: pep8 pyflakes and co" +optional = false +python-versions = ">=3.9" +files = [ + {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, + {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, +] + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.14.0,<2.15.0" +pyflakes = ">=3.4.0,<3.5.0" + [[package]] name = "ipython" version = "9.4.0" @@ -196,6 +212,17 @@ files = [ [package.dependencies] traitlets = "*" +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "parso" version = "0.8.4" @@ -484,6 +511,28 @@ files = [ [package.extras] tests = ["pytest"] +[[package]] +name = "pycodestyle" +version = "2.14.0" +description = "Python style guide checker" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +description = "passive checker of Python programs" +optional = false +python-versions = ">=3.9" +files = [ + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, +] + [[package]] name = "pygments" version = "2.19.2" @@ -616,4 +665,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3644693c682e70c01ed88ace18501aeee83dfe38fdd56c318babbff6c0b325b9" +content-hash = "4c6f1d54255b3f5fc02547d98bf2eaf45d19c0627653eca1cc3405fa33dbc2a5" diff --git a/pyproject.toml b/pyproject.toml index efb645b..f06944a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ pillow = "^11.3.0" ipython = "^9.4.0" redis = "^6.2.0" django-countries = "^7.6.1" +flake8 = "^7.3.0" [build-system] diff --git a/users/admin.py b/users/admin.py index 8c38f3f..e69de29 100644 --- a/users/admin.py +++ b/users/admin.py @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/users/models.py b/users/models.py index b8f54e9..bc958dd 100644 --- a/users/models.py +++ b/users/models.py @@ -7,16 +7,36 @@ class User(AbstractUser): username = None - email = models.EmailField(unique=True, verbose_name="Email") - first_name = models.CharField(max_length=50, verbose_name="Имя") - last_name = models.CharField(max_length=50, verbose_name="Фамилия") - middle_name = models.CharField(max_length=50, verbose_name="Отчество", **NULLABLE) + email = models.EmailField( + unique=True, + verbose_name="Email") + first_name = models.CharField( + max_length=50, + verbose_name="Имя") + last_name = models.CharField( + max_length=50, + verbose_name="Фамилия") + middle_name = models.CharField( + max_length=50, + verbose_name="Отчество", + **NULLABLE) phone_number = models.CharField( - max_length=20, verbose_name="Номер телефона", **NULLABLE, help_text="Введите номер телефона" - ) - country = CountryField(max_length=50, blank_label="(выберите страну)", **NULLABLE) - avatar = models.ImageField(upload_to="users/avatars", **NULLABLE, verbose_name="Аватар") - token = models.CharField(max_length=100, verbose_name="Токен", **NULLABLE) + max_length=20, + verbose_name="Номер телефона", + **NULLABLE, + help_text="Введите номер телефона") + country = CountryField( + max_length=50, + blank_label="(выберите страну)", + **NULLABLE) + avatar = models.ImageField( + upload_to="users/avatars", + **NULLABLE, + verbose_name="Аватар") + token = models.CharField( + max_length=100, + verbose_name="Токен", + **NULLABLE) USERNAME_FIELD = "email" REQUIRED_FIELDS = [] diff --git a/users/services.py b/users/services.py index 650fdf4..f96843c 100644 --- a/users/services.py +++ b/users/services.py @@ -18,4 +18,4 @@ def block_user(self, pk): user = User.objects.get(pk=pk) user.is_active = {user.is_active: False, not user.is_active: True}[True] user.save() - return redirect(reverse("users:users")) \ No newline at end of file + return redirect(reverse("users:users")) diff --git a/users/tests.py b/users/tests.py index 7ce503c..e69de29 100644 --- a/users/tests.py +++ b/users/tests.py @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/users/views.py b/users/views.py index 045cca6..b98c6c3 100644 --- a/users/views.py +++ b/users/views.py @@ -6,10 +6,21 @@ from django.core.mail import send_mail from django.urls import reverse_lazy from django.utils.crypto import get_random_string -from django.views.generic import CreateView, DeleteView, DetailView, FormView, ListView, TemplateView, UpdateView +from django.views.generic import ( + CreateView, + DeleteView, + DetailView, + FormView, + ListView, + TemplateView, + UpdateView) from config.settings import EMAIL_HOST_USER -from users.forms import PasswordRecoveryForm, UserLoginForm, UserRegistrationForm, UserUpdateForm +from users.forms import ( + PasswordRecoveryForm, + UserLoginForm, + UserRegistrationForm, + UserUpdateForm) from users.models import User From ee2236edced493bc04716cd4b400066552565e64 Mon Sep 17 00:00:00 2001 From: Admin Date: Sat, 12 Jul 2025 22:12:18 +0300 Subject: [PATCH 05/10] working on develop_1 fix --- mailing/models.py | 13 +++++++- mailing/templates/mailing/base.html | 8 ++--- .../templates/mailing/includes/main_menu.html | 29 ++++++++--------- mailing/templates/mailing/mailing_form.html | 31 +++++++++++++------ mailing/templates/mailing/message_form.html | 31 +++++++++++++------ mailing/views.py | 30 +++++++++++++----- users/forms.py | 16 +++++++--- users/templates/registration/login.html | 8 ++--- users/templates/users/user_detail.html | 4 +-- users/templates/users/user_form.html | 3 -- users/urls.py | 12 ++++--- users/views.py | 23 ++++++++------ 12 files changed, 133 insertions(+), 75 deletions(-) diff --git a/mailing/models.py b/mailing/models.py index c5873ce..82f358b 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -6,6 +6,9 @@ class RecipientMailing(models.Model): + """ + Модель получателя рассылки. + """ email = models.EmailField( max_length=255, unique=True, @@ -39,6 +42,9 @@ class Meta: class Message(models.Model): + """ + Модель сообщения. + """ subject = models.CharField( max_length=255, verbose_name="Тема сообщения") @@ -60,6 +66,9 @@ class Meta: class Mailing(models.Model): + """ + Модель рассылки. + """ CREATED = "Создана" LAUNCHED = "Запущена" COMPLETED = "Завершена" @@ -111,7 +120,9 @@ class Meta: class MailingAttempt(models.Model): - """Попытка рассылки""" + """ + Модель попытки рассылки. + """ STATUS_OK = "Успешно" STATUS_NOK = "Не успешно" diff --git a/mailing/templates/mailing/base.html b/mailing/templates/mailing/base.html index 97ed507..6075896 100644 --- a/mailing/templates/mailing/base.html +++ b/mailing/templates/mailing/base.html @@ -7,7 +7,7 @@ - + Рассылки сообщений @@ -19,9 +19,9 @@ - - - + + + diff --git a/mailing/templates/mailing/includes/main_menu.html b/mailing/templates/mailing/includes/main_menu.html index 1b10be8..37f167a 100644 --- a/mailing/templates/mailing/includes/main_menu.html +++ b/mailing/templates/mailing/includes/main_menu.html @@ -34,34 +34,35 @@ - \ No newline at end of file diff --git a/mailing/templates/mailing/mailing_form.html b/mailing/templates/mailing/mailing_form.html index 12038c6..a819e9f 100644 --- a/mailing/templates/mailing/mailing_form.html +++ b/mailing/templates/mailing/mailing_form.html @@ -6,18 +6,29 @@
-
+ {% if object %} +

Редактирование Сообщения

+ {% else %} +

Создание Сообщения

+ {% endif %} + {% csrf_token %} {{ form.as_p }} -
- - Отмена + + Отмена + + + + + + + + + + + + +
diff --git a/mailing/templates/mailing/message_form.html b/mailing/templates/mailing/message_form.html index b18139f..76f1ae1 100644 --- a/mailing/templates/mailing/message_form.html +++ b/mailing/templates/mailing/message_form.html @@ -6,18 +6,29 @@
-
+ {% if object %} +

Редактирование Сообщения

+ {% else %} +

Создание Сообщения

+ {% endif %} + {% csrf_token %} {{ form.as_p }} -
- - Отмена + + Отмена + + + + + + + + + + + + +
diff --git a/mailing/views.py b/mailing/views.py index 3694737..fe14b03 100644 --- a/mailing/views.py +++ b/mailing/views.py @@ -1,12 +1,28 @@ -from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.contrib.auth.mixins import ( + LoginRequiredMixin, + UserPassesTestMixin +) from django.core.exceptions import PermissionDenied - from django.urls import reverse_lazy - -from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView - -from mailing.forms import MailingForm, MessageForm, RecipientForm -from mailing.models import Mailing, MailingAttempt, Message, RecipientMailing +from django.views.generic import ( + CreateView, + DeleteView, + DetailView, + ListView, + TemplateView, + UpdateView +) +from mailing.forms import ( + MailingForm, + MessageForm, + RecipientForm +) +from mailing.models import ( + Mailing, + MailingAttempt, + Message, + RecipientMailing +) class IndexView(TemplateView): diff --git a/users/forms.py b/users/forms.py index 938f8ce..32e3b18 100644 --- a/users/forms.py +++ b/users/forms.py @@ -8,6 +8,9 @@ class UserRegistrationForm(StyleFormMixin, UserCreationForm): + """ + Модель регистрация пользователя. + """ class Meta: model = User template_name = "users/user_form.html" @@ -24,6 +27,9 @@ def clean_email(self): class UserForm(StyleFormMixin, UserChangeForm): + """ + Модель форма пользователя. + """ class Meta: model = User fields = ( @@ -45,6 +51,9 @@ def __init__(self, *args, **kwargs): class UserUpdateForm(StyleFormMixin, ModelForm): + """ + Модель изменение пользователя. + """ class Meta: model = User fields = ( @@ -79,6 +88,9 @@ def clean_email(self): class PasswordRecoveryForm(StyleFormMixin, forms.Form): + """ + Модель форма восстановления пароля. + """ email = forms.EmailField(label="Укажите Email") def clean_email(self): @@ -89,7 +101,3 @@ def clean_email(self): if not User.objects.filter(email=email).exists(): raise forms.ValidationError("Такого email нет в системе") return email - - -class UserLoginForm(StyleFormMixin, AuthenticationForm): - model = User diff --git a/users/templates/registration/login.html b/users/templates/registration/login.html index 8487c06..e5d1f3e 100644 --- a/users/templates/registration/login.html +++ b/users/templates/registration/login.html @@ -1,6 +1,7 @@ {% extends 'mailing/base.html' %} -{% block content %} +{% block title %}Login{% endblock %} +{% block content %}
@@ -13,18 +14,13 @@ - Отмена - -
-
-
{% endblock %} \ No newline at end of file diff --git a/users/templates/users/user_detail.html b/users/templates/users/user_detail.html index 7981c2e..41ef3b9 100644 --- a/users/templates/users/user_detail.html +++ b/users/templates/users/user_detail.html @@ -15,8 +15,8 @@

{{ object.email }}

- Редактировать - + Редактировать Назад {% if user.is_superuser %} diff --git a/users/templates/users/user_form.html b/users/templates/users/user_form.html index e6efbc5..884e46e 100644 --- a/users/templates/users/user_form.html +++ b/users/templates/users/user_form.html @@ -1,6 +1,5 @@ {% extends 'mailing/base.html' %} {% block content %} -
@@ -24,8 +23,6 @@ {% else %} Отмена {% endif %} - -
diff --git a/users/urls.py b/users/urls.py index ddf0667..7896df3 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,4 +1,4 @@ -from django.contrib.auth.views import LogoutView +from django.contrib.auth.views import LoginView, LogoutView from django.urls import path from users.apps import UsersConfig @@ -10,16 +10,20 @@ UserDeleteView, UserDetailView, UserListView, - UserLoginView, UserUpdateView, ) app_name = UsersConfig.name urlpatterns = [ - path("login/", UserLoginView.as_view(), name="login"), - path("logout/", LogoutView.as_view(), name="logout"), + path("login/", + LoginView.as_view(template_name='registration/login.html'), + name="login"), + path("logout/", + LogoutView.as_view(template_name='registration/login.html', next_page='../../'), + name="logout"), path("register/", UserCreateView.as_view(), name="register"), + path("users/", UserListView.as_view(), name="users"), path("detail//", UserDetailView.as_view(), name="detail"), path("update//", UserUpdateView.as_view(), name="update"), diff --git a/users/views.py b/users/views.py index b98c6c3..a7ff2b1 100644 --- a/users/views.py +++ b/users/views.py @@ -1,7 +1,6 @@ import secrets from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin -from django.contrib.auth.views import LoginView from django.core.exceptions import PermissionDenied from django.core.mail import send_mail from django.urls import reverse_lazy @@ -13,18 +12,21 @@ FormView, ListView, TemplateView, - UpdateView) - + UpdateView +) from config.settings import EMAIL_HOST_USER from users.forms import ( PasswordRecoveryForm, - UserLoginForm, UserRegistrationForm, - UserUpdateForm) + UserUpdateForm +) from users.models import User class UserCreateView(CreateView): + """ + Модель создания пользователя. + """ model = User form_class = UserRegistrationForm success_url = reverse_lazy("users:email_confirmation") @@ -46,12 +48,10 @@ def form_valid(self, form): return super().form_valid(form) -class UserLoginView(LoginView): - model = User - form_class = UserLoginForm - - class UserListView(LoginRequiredMixin, UserPassesTestMixin, ListView): + """ + Модель просмотра пользователя. + """ model = User template_name = "users/user_list.html" @@ -60,6 +60,9 @@ def test_func(self): class UserDetailView(LoginRequiredMixin, DetailView): + """ + Модель удаления пользователя. + """ model = User form_class = UserUpdateForm From fe447fd913b6dfb72ecfa1da55ecea43ce87830e Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 13 Jul 2025 15:41:04 +0300 Subject: [PATCH 06/10] working on develop_1 fixend --- mailing/templates/mailing/base.html | 2 +- mailing/templates/mailing/mailing_detail.html | 19 ++++++----- mailing/templates/mailing/mailing_form.html | 21 +++--------- .../mailing/mailingattempt_list.html | 2 -- mailing/templates/mailing/message_detail.html | 8 ++--- mailing/templates/mailing/message_form.html | 21 +++--------- mailing/templates/mailing/message_list.html | 14 +++----- .../mailing/recipientmailing_detail.html | 14 ++++---- .../mailing/recipientmailing_form.html | 5 +++ mailing/urls.py | 27 +++++++-------- mailing/views.py | 34 +++++++++++-------- users/templates/users/user_form.html | 8 +---- 12 files changed, 71 insertions(+), 104 deletions(-) diff --git a/mailing/templates/mailing/base.html b/mailing/templates/mailing/base.html index 6075896..d725cc7 100644 --- a/mailing/templates/mailing/base.html +++ b/mailing/templates/mailing/base.html @@ -191,7 +191,7 @@ {% endif %} {% block content %} - {% endblock %}} + {% endblock %} diff --git a/mailing/templates/mailing/mailing_detail.html b/mailing/templates/mailing/mailing_detail.html index e4d5944..dda23e8 100644 --- a/mailing/templates/mailing/mailing_detail.html +++ b/mailing/templates/mailing/mailing_detail.html @@ -2,18 +2,19 @@ {% block content %}
- +

Описание Рассылки

- -

Первая отправка {{object.first_sending }}

-

Окончание отправки {{object.end_sending }}

-

Статус {{object.status}}

-

Статус {{object.is_active}}

-

Статус {{object.owner}}

-

Сообщение {{object.message}}

+

Первая отправка: {{object.first_sending }}

+

Окончание отправки: {{object.end_sending }}

+

Статус рассылки: {{object.status}}

+

Статус активности: {{object.is_active}}

+

Получатели: +

{% for recipients in object.recipients.all %}{{ recipients }}

{% endfor %} +

+

Сообщение: {{object.message}}

+

Владелец: {{object.owner}}

Назад

-
{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/mailing_form.html b/mailing/templates/mailing/mailing_form.html index a819e9f..be0213d 100644 --- a/mailing/templates/mailing/mailing_form.html +++ b/mailing/templates/mailing/mailing_form.html @@ -7,29 +7,16 @@
{% if object %} -

Редактирование Сообщения

+

Редактирование Рассылки

{% else %} -

Создание Сообщения

+

Создание Рассылки

{% endif %} {% csrf_token %} {{ form.as_p }} - - Отмена + + Отмена - - - - - - - - - - - - -
diff --git a/mailing/templates/mailing/mailingattempt_list.html b/mailing/templates/mailing/mailingattempt_list.html index 1f7779c..84ad81d 100644 --- a/mailing/templates/mailing/mailingattempt_list.html +++ b/mailing/templates/mailing/mailingattempt_list.html @@ -37,11 +37,9 @@

Статистика по рассылкам

{% if request.user.is_superuser %} {{ object.mailing.owner }} {% endif %} - {% endfor%} -
diff --git a/mailing/templates/mailing/message_detail.html b/mailing/templates/mailing/message_detail.html index 9b933b4..993300f 100644 --- a/mailing/templates/mailing/message_detail.html +++ b/mailing/templates/mailing/message_detail.html @@ -2,14 +2,12 @@ {% block content %}
- +

Описание Сообщения

- -

Тема сообщения {{object.subject}}

-

Текст сообщения {{object.content}}

+

Тема сообщения: {{object.subject}}

+

Текст сообщения: {{object.content}}

Назад

-
{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/message_form.html b/mailing/templates/mailing/message_form.html index 76f1ae1..e51d49d 100644 --- a/mailing/templates/mailing/message_form.html +++ b/mailing/templates/mailing/message_form.html @@ -7,29 +7,16 @@
{% if object %} -

Редактирование Сообщения

+

Редактирование Сообщения

{% else %} -

Создание Сообщения

+

Создание Сообщения

{% endif %} {% csrf_token %} {{ form.as_p }} - - Отмена + + Отмена - - - - - - - - - - - - -
diff --git a/mailing/templates/mailing/message_list.html b/mailing/templates/mailing/message_list.html index ad93a55..62e3f39 100644 --- a/mailing/templates/mailing/message_list.html +++ b/mailing/templates/mailing/message_list.html @@ -3,20 +3,20 @@
-
- -
-
-
+
+
+
Действия над Сообщениями доступны для супер-юзера
+
+
@@ -45,14 +45,10 @@

Сообщения

»Удалить - {% endfor%}
-
- -
{% endblock %}} \ No newline at end of file diff --git a/mailing/templates/mailing/recipientmailing_detail.html b/mailing/templates/mailing/recipientmailing_detail.html index e8012fb..4615ef4 100644 --- a/mailing/templates/mailing/recipientmailing_detail.html +++ b/mailing/templates/mailing/recipientmailing_detail.html @@ -3,15 +3,15 @@ {% load my_tags %}
- +

Описание Получателя

Фото -

ФИО получателя: {{object.fio}} -

-

Email получателя: {{object.email }} -

-

Комментарий: {{object.comment}} -

+

ФИО получателя: {{ object.fio }}

+

+

Email получателя: {{ object.email }}

+

+

Комментарий: {{ object.comment }}

+

Назад

diff --git a/mailing/templates/mailing/recipientmailing_form.html b/mailing/templates/mailing/recipientmailing_form.html index f91a219..303eedb 100644 --- a/mailing/templates/mailing/recipientmailing_form.html +++ b/mailing/templates/mailing/recipientmailing_form.html @@ -6,6 +6,11 @@
+ {% if object %} +

Редактирование Получателя

+ {% else %} +

Создание Получателя

+ {% endif %}
{% csrf_token %} {{ form.as_p }} diff --git a/mailing/urls.py b/mailing/urls.py index 86d7fc2..6caecd9 100644 --- a/mailing/urls.py +++ b/mailing/urls.py @@ -28,26 +28,21 @@ urlpatterns = [ path("", IndexView.as_view(), name="index"), path("mailing/", cache_page(1)(MailingListView.as_view()), name="mailing_list"), - path( - "mailing//detail/", - cache_page(60)(MailingDetailView.as_view()), - name="mailing_detail", - ), + path("mailing//detail/",cache_page(60)(MailingDetailView.as_view()),name="mailing_detail",), path("mailing/new/", MailingCreateView.as_view(), name="mailing_create"), path("mailing//edit/", MailingUpdateView.as_view(), name="mailing_update"), path("mailing//delete/", MailingDeleteView.as_view(), name="mailing_delete"), path("mailing//run_mailing/", run_mailing, name="run_mailing"), path("block_mailing/", block_mailing, name="block_mailing"), - path( - "recipientmailing/", - cache_page(1)(RecipientMailingListView.as_view()), - name="recipientmailing_list", - ), - path( - "recipientmailing//detail/", - cache_page(60)(RecipientMailingDetailView.as_view()), - name="recipientmailing_detail", - ), + + path("recipientmailing/", + cache_page(1)(RecipientMailingListView.as_view()), + name="recipientmailing_list", +), + path("recipientmailing//detail/", + cache_page(60)(RecipientMailingDetailView.as_view()), + name="recipientmailing_detail", +), path( "recipientmailing/new/", RecipientMailingCreateView.as_view(), @@ -63,6 +58,7 @@ RecipientMailingDeleteView.as_view(), name="recipientmailing_delete", ), + path("message/", cache_page(60)(MessageListView.as_view()), name="message_list"), path( "message//detail/", @@ -72,5 +68,6 @@ path("message/new/", MessageCreateView.as_view(), name="message_create"), path("message//edit/", MessageUpdateView.as_view(), name="message_update"), path("message//delete/", MessageDeleteView.as_view(), name="message_delete"), + path("attempt/", cache_page(60)(MailingAttemptListView.as_view()), name="attempt"), ] diff --git a/mailing/views.py b/mailing/views.py index fe14b03..865681e 100644 --- a/mailing/views.py +++ b/mailing/views.py @@ -23,7 +23,10 @@ Message, RecipientMailing ) - +from mailing.services import ( + get_attempt_from_cache, + get_mailing_from_cache +) class IndexView(TemplateView): template_name = "mailing/index.html" @@ -42,12 +45,14 @@ def get_context_data(self, **kwargs): class MailingListView(LoginRequiredMixin, ListView): model = Mailing + # def get_queryset(self, *args, **kwargs): + # if self.request.user.is_superuser or self.request.user.groups.filter(name="Менеджеры").exists(): + # return super().get_queryset() + # elif self.request.user.groups.filter(name="Пользователи").exists(): + # return super().get_queryset().filter(owner=self.request.user) + # raise PermissionDenied def get_queryset(self, *args, **kwargs): - if self.request.user.is_superuser or self.request.user.groups.filter(name="Менеджеры").exists(): - return super().get_queryset() - elif self.request.user.groups.filter(name="Пользователи").exists(): - return super().get_queryset().filter(owner=self.request.user) - raise PermissionDenied + return get_mailing_from_cache() class MailingDetailView(LoginRequiredMixin, DetailView): @@ -172,10 +177,8 @@ class MessageListView(ListView): model = Message def get_queryset(self, *args, **kwargs): - if self.request.user.is_superuser: - return super().get_queryset() - else: - raise PermissionDenied + queryset = super().get_queryset() + return queryset class MessageDetailView(LoginRequiredMixin, DetailView): @@ -241,8 +244,9 @@ class MailingAttemptListView(LoginRequiredMixin, ListView): model = MailingAttempt def get_queryset(self, *args, **kwargs): - if self.request.user.is_superuser: - return super().get_queryset() - elif self.request.user.groups.filter(name="Пользователи").exists(): - return super().get_queryset().filter(owner=self.request.user) - raise PermissionDenied + # if self.request.user.is_superuser: + # return super().get_queryset() + # elif self.request.user.groups.filter(name="Пользователи").exists(): + # return super().get_queryset().filter(owner=self.request.user) + # raise PermissionDenied + return get_attempt_from_cache() diff --git a/users/templates/users/user_form.html b/users/templates/users/user_form.html index 884e46e..e53df7a 100644 --- a/users/templates/users/user_form.html +++ b/users/templates/users/user_form.html @@ -9,7 +9,6 @@ {% csrf_token %} {{ form.as_p }}
- - - {% if user.is_superuser%} - Отмена - {% else %} - Отмена - {% endif %} + Отмена
From 34e0cc12f31bf394d3bf305f2ac660a2ca8d9a3f Mon Sep 17 00:00:00 2001 From: Admin Date: Sun, 13 Jul 2025 15:49:01 +0300 Subject: [PATCH 07/10] working on develop_1 readme --- README.md | Bin 9644 -> 10086 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/README.md b/README.md index 250d9b765ecfeea83491c718e1fa4fb2914fd8a5..22c62df4a8ce103a593e30a78200145ee94f1347 100644 GIT binary patch delta 326 zcmZ4E{mgH}FU4qC7E=}l26Gkz7DE=q*HwKCru(+{UOkS@bK6#F^0&gxuCPO+y5kn$F z2}9~+S!Maj{}ebTb17vonooYOAkGca;ml$WgwB&UD#`Cr?z?b}eEkWyoVFW=I9P dsSHfQ<-sZq84PfmTLe^}0`yPu=7-9s83De&I|={* delta 70 zcmaFnx5j(JFGXHO7CRPm784c&7AF>;$$OQug-sds7!(*##dDRHPhO!UzzkB8t86&A MMWt->0@Y)T0D+Ja#{d8T From feae268dfa2619abf4a1fa709fb3821cf0f5c424 Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 17 Jul 2025 01:42:08 +0300 Subject: [PATCH 08/10] working on develop_1 --- .flake8 | 2 +- mailing/forms.py | 16 +++++++ ..._options_alter_recipientmailing_options.py | 21 ++++++++ mailing/models.py | 6 +++ .../templates/mailing/includes/main_menu.html | 6 +-- .../mailing/mailingattempt_list.html | 5 +- mailing/urls.py | 31 ++++++------ mailing/views.py | 48 ++++++++++++++----- users/forms.py | 8 ++-- users/services.py | 5 +- users/templates/users/user_form.html | 6 +++ users/templates/users/user_list.html | 1 - users/views.py | 32 +++++++++---- 13 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 mailing/migrations/0003_alter_message_options_alter_recipientmailing_options.py diff --git a/.flake8 b/.flake8 index 5e1fd16..dda77d8 100644 --- a/.flake8 +++ b/.flake8 @@ -1,4 +1,4 @@ [flake8] -max-line-length = 100 +max-line-length = 119 ignore = E203,W503 exclude = .git,__pycache__,env,.env,venv,.venv,migrations \ No newline at end of file diff --git a/mailing/forms.py b/mailing/forms.py index 2e40a2a..6329b84 100644 --- a/mailing/forms.py +++ b/mailing/forms.py @@ -17,6 +17,14 @@ def __init__(self, *args, **kwargs): class MailingForm(StyleFormMixin, ModelForm): + class Meta: + model = Mailing + fields = "__all__" + exclude = ("can_disable_mailing", "owner") + success_url = reverse_lazy("mailing:mailing_list") + + +class MailingModeratorForm(StyleFormMixin, ModelForm): class Meta: model = Mailing fields = "__all__" @@ -24,6 +32,14 @@ class Meta: class RecipientForm(StyleFormMixin, ModelForm): + class Meta: + model = RecipientMailing + fields = "__all__" + exclude = ("can_blocking_client", "owner") + success_url = reverse_lazy("mailing:recipientmailing_list") + + +class RecipientModeratorForm(StyleFormMixin, ModelForm): class Meta: model = RecipientMailing fields = "__all__" diff --git a/mailing/migrations/0003_alter_message_options_alter_recipientmailing_options.py b/mailing/migrations/0003_alter_message_options_alter_recipientmailing_options.py new file mode 100644 index 0000000..3147d2e --- /dev/null +++ b/mailing/migrations/0003_alter_message_options_alter_recipientmailing_options.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.4 on 2025-07-16 20:24 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('mailing', '0002_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='message', + options={'ordering': ['subject'], 'permissions': [('can_blocking_sms', 'Может блокировать сообщение')], 'verbose_name': 'Сообщение', 'verbose_name_plural': 'Сообщения'}, + ), + migrations.AlterModelOptions( + name='recipientmailing', + options={'ordering': ['fio'], 'permissions': [('can_blocking_client', 'Может блокировать получателя')], 'verbose_name': 'Получатель рассылки', 'verbose_name_plural': 'Получатели рассылки'}, + ), + ] diff --git a/mailing/models.py b/mailing/models.py index 82f358b..99f16a5 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -39,6 +39,9 @@ class Meta: verbose_name = "Получатель рассылки" verbose_name_plural = "Получатели рассылки" ordering = ["fio"] + permissions = [ + ("can_blocking_client", "Может блокировать получателя"), + ] class Message(models.Model): @@ -63,6 +66,9 @@ class Meta: verbose_name = "Сообщение" verbose_name_plural = "Сообщения" ordering = ["subject"] + permissions = [ + ('can_blocking_sms', 'Может блокировать сообщение'), + ] class Mailing(models.Model): diff --git a/mailing/templates/mailing/includes/main_menu.html b/mailing/templates/mailing/includes/main_menu.html index 37f167a..0d80f24 100644 --- a/mailing/templates/mailing/includes/main_menu.html +++ b/mailing/templates/mailing/includes/main_menu.html @@ -36,6 +36,9 @@ aria-expanded="false">Пользователь
-
@@ -21,7 +20,7 @@

Статистика по рассылкам

Дата Статус Ответ сервера - {% if request.user.is_superuser %} + {% if user.is_authenticated %} Создал {% endif %} @@ -34,7 +33,7 @@

Статистика по рассылкам

{{ object.date_attempt}} {{ object.status}} {{ object.server_response}} - {% if request.user.is_superuser %} + {% if user.is_authenticated %} {{ object.mailing.owner }} {% endif %} diff --git a/mailing/urls.py b/mailing/urls.py index 6caecd9..77e8d1a 100644 --- a/mailing/urls.py +++ b/mailing/urls.py @@ -20,15 +20,17 @@ RecipientMailingDeleteView, RecipientMailingDetailView, RecipientMailingListView, - RecipientMailingUpdateView, + RecipientMailingUpdateView, MailingAttemptCreateView, ) app_name = MailingConfig.name urlpatterns = [ path("", IndexView.as_view(), name="index"), - path("mailing/", cache_page(1)(MailingListView.as_view()), name="mailing_list"), - path("mailing//detail/",cache_page(60)(MailingDetailView.as_view()),name="mailing_detail",), + path("mailing/", + cache_page(1)(MailingListView.as_view()), name="mailing_list"), + path("mailing//detail/", + cache_page(60)(MailingDetailView.as_view()), name="mailing_detail"), path("mailing/new/", MailingCreateView.as_view(), name="mailing_create"), path("mailing//edit/", MailingUpdateView.as_view(), name="mailing_update"), path("mailing//delete/", MailingDeleteView.as_view(), name="mailing_delete"), @@ -37,37 +39,32 @@ path("recipientmailing/", cache_page(1)(RecipientMailingListView.as_view()), - name="recipientmailing_list", -), + name="recipientmailing_list",), path("recipientmailing//detail/", cache_page(60)(RecipientMailingDetailView.as_view()), - name="recipientmailing_detail", -), + name="recipientmailing_detail",), path( "recipientmailing/new/", RecipientMailingCreateView.as_view(), - name="recipientmailing_create", - ), + name="recipientmailing_create",), path( "recipientmailing//edit/", RecipientMailingUpdateView.as_view(), - name="recipientmailing_update", - ), + name="recipientmailing_update",), path( "recipientmailing//delete/", RecipientMailingDeleteView.as_view(), - name="recipientmailing_delete", - ), + name="recipientmailing_delete",), - path("message/", cache_page(60)(MessageListView.as_view()), name="message_list"), + path("message/", + cache_page(60)(MessageListView.as_view()), name="message_list"), path( "message//detail/", - cache_page(60)(MessageDetailView.as_view()), - name="message_detail", - ), + cache_page(60)(MessageDetailView.as_view()), name="message_detail",), path("message/new/", MessageCreateView.as_view(), name="message_create"), path("message//edit/", MessageUpdateView.as_view(), name="message_update"), path("message//delete/", MessageDeleteView.as_view(), name="message_delete"), path("attempt/", cache_page(60)(MailingAttemptListView.as_view()), name="attempt"), + path("attempt/create/", MailingAttemptCreateView.as_view(), name="attempt_create"), ] diff --git a/mailing/views.py b/mailing/views.py index 865681e..cbbecea 100644 --- a/mailing/views.py +++ b/mailing/views.py @@ -15,7 +15,9 @@ from mailing.forms import ( MailingForm, MessageForm, - RecipientForm + RecipientForm, + RecipientModeratorForm, + MailingModeratorForm ) from mailing.models import ( Mailing, @@ -28,6 +30,7 @@ get_mailing_from_cache ) + class IndexView(TemplateView): template_name = "mailing/index.html" @@ -46,7 +49,8 @@ class MailingListView(LoginRequiredMixin, ListView): model = Mailing # def get_queryset(self, *args, **kwargs): - # if self.request.user.is_superuser or self.request.user.groups.filter(name="Менеджеры").exists(): + # if (self.request.user.is_superuser or + # self.request.user.groups.filter(name="Менеджеры").exists()): # return super().get_queryset() # elif self.request.user.groups.filter(name="Пользователи").exists(): # return super().get_queryset().filter(owner=self.request.user) @@ -87,12 +91,24 @@ class MailingUpdateView(LoginRequiredMixin, UpdateView): model = Mailing form_class = MailingForm success_url = reverse_lazy("mailing:mailing_list") - - def get_object(self, queryset=None): - self.object = super().get_object(queryset) - if self.object.owner != self.request.user and not self.request.user.is_superuser: - raise PermissionDenied - return self.object + # permission_required = 'mailing.can_disable_mailing' + + # def get_object(self, queryset=None): + # self.object = super().get_object(queryset) + # if self.object.owner != self.request.user and not self.request.user.is_superuser: + # raise PermissionDenied + # return self.object + + def get_form_class(self): + user = self.request.user + if user.has_perm('mailing.can_blocking_client'): + return MailingModeratorForm + return MailingForm + # if user == self.object.owner: + # return MailingForm + # if user.has_perm('mailing.can_disable_mailing'): + # return MailingModeratorForm + # raise PermissionDenied class MailingDeleteView(LoginRequiredMixin, DeleteView): @@ -155,11 +171,17 @@ class RecipientMailingUpdateView(LoginRequiredMixin, UpdateView): form_class = RecipientForm success_url = reverse_lazy("mailing:recipientmailing_list") - def get_object(self, queryset=None): - self.object = super().get_object(queryset) - if self.object.owner != self.request.user and not self.request.user.is_superuser: - raise PermissionDenied - return self.object + # def get_object(self, queryset=None): + # self.object = super().get_object(queryset) + # if self.object.owner != self.request.user and not self.request.user.is_superuser: + # raise PermissionDenied + # return self.object + + def get_form_class(self): + user = self.request.user + if user.has_perm('mailing.can_blocking_client'): + return RecipientModeratorForm + return RecipientForm class RecipientMailingDeleteView(LoginRequiredMixin, DeleteView): diff --git a/users/forms.py b/users/forms.py index 32e3b18..1597142 100644 --- a/users/forms.py +++ b/users/forms.py @@ -1,5 +1,5 @@ from django import forms -from django.contrib.auth.forms import AuthenticationForm, UserChangeForm, UserCreationForm +from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.forms import ModelForm from django.urls import reverse_lazy @@ -63,9 +63,9 @@ class Meta: "password", "phone_number", "country", - "is_active", - "is_superuser", - "is_staff", + # "is_active", + # "is_superuser", + # "is_staff", "avatar", ) success_url = reverse_lazy("users:users") diff --git a/users/services.py b/users/services.py index f96843c..c617563 100644 --- a/users/services.py +++ b/users/services.py @@ -1,4 +1,4 @@ -from django.contrib.auth.decorators import permission_required +from django.contrib.auth.decorators import login_required from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect from django.urls import reverse @@ -13,7 +13,8 @@ def email_verification(request, token): return HttpResponseRedirect(reverse("users:login")) -@permission_required("users.view_user") +# @permission_required("users.view_user") +@login_required def block_user(self, pk): user = User.objects.get(pk=pk) user.is_active = {user.is_active: False, not user.is_active: True}[True] diff --git a/users/templates/users/user_form.html b/users/templates/users/user_form.html index e53df7a..ac6cdcd 100644 --- a/users/templates/users/user_form.html +++ b/users/templates/users/user_form.html @@ -16,7 +16,13 @@ Сохранить {% endif %} + {% if user.is_superuser %} Отмена + {% elif user.is_authenticated %} + Отмена + {% elif user.is_authenticated == False %} + Отмена + {% endif %}
diff --git a/users/templates/users/user_list.html b/users/templates/users/user_list.html index 1b478e2..9b97600 100644 --- a/users/templates/users/user_list.html +++ b/users/templates/users/user_list.html @@ -55,7 +55,6 @@

Список пользователей

{% endfor%} - diff --git a/users/views.py b/users/views.py index a7ff2b1..b42ea28 100644 --- a/users/views.py +++ b/users/views.py @@ -1,5 +1,5 @@ import secrets - +from django.contrib.auth.models import Group from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin from django.core.exceptions import PermissionDenied from django.core.mail import send_mail @@ -38,6 +38,10 @@ def form_valid(self, form): host = self.request.get_host() url = f"http://{host}/users/email-confirm/{token}/" user.token = token + + users_group = Group.objects.get(name='Пользователи') + user.groups.add(users_group) + user.save() send_mail( subject="Подтверждение почты", @@ -61,14 +65,14 @@ def test_func(self): class UserDetailView(LoginRequiredMixin, DetailView): """ - Модель удаления пользователя. + Модель Детального просмотра пользователя. """ model = User form_class = UserUpdateForm def get_object(self, queryset=None): self.object = super().get_object(queryset) - if self.request.user.is_superuser: + if self.request.user.is_superuser or self.object.email == self.request.user.email: return self.object raise PermissionDenied @@ -85,9 +89,15 @@ def get_success_url(self): def get_object(self, queryset=None): self.object = super().get_object(queryset) - if not self.request.user.is_superuser: - raise PermissionDenied - return self.object + if self.request.user.is_superuser or self.object.email == self.request.user.email: + return self.object + raise PermissionDenied + + # if not self.request.user.is_superuser: + # raise PermissionDenied + # elif self.object.email == self.request.user.email: + # return self.object + # return self.object class UserDeleteView(LoginRequiredMixin, DeleteView): @@ -101,9 +111,13 @@ def get_success_url(self): def get_object(self, queryset=None): self.object = super().get_object(queryset) - if not self.request.user.is_superuser: - raise PermissionDenied - return self.object + if self.request.user.is_superuser or self.object.email == self.request.user.email: + return self.object + raise PermissionDenied + + # if not self.request.user.is_superuser: + # raise PermissionDenied + # return self.object class EmailConfirmationView(TemplateView): From 4804124f21768c635dd9350cca632d4cf926e6ba Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 17 Jul 2025 23:46:39 +0300 Subject: [PATCH 09/10] working on develop_1 --- .../templates/mailing/includes/main_menu.html | 5 +- .../mailing/mailingattemptmy_list.html | 49 +++++++++++++++++++ mailing/templatetags/my_tags.py | 2 + mailing/urls.py | 5 +- mailing/views.py | 16 +++++- 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 mailing/templates/mailing/mailingattemptmy_list.html diff --git a/mailing/templates/mailing/includes/main_menu.html b/mailing/templates/mailing/includes/main_menu.html index 0d80f24..7bbc1cf 100644 --- a/mailing/templates/mailing/includes/main_menu.html +++ b/mailing/templates/mailing/includes/main_menu.html @@ -39,6 +39,9 @@ + diff --git a/mailing/templates/mailing/mailingattemptmy_list.html b/mailing/templates/mailing/mailingattemptmy_list.html new file mode 100644 index 0000000..bfd7149 --- /dev/null +++ b/mailing/templates/mailing/mailingattemptmy_list.html @@ -0,0 +1,49 @@ +{% extends 'mailing/base.html' %} +{% load my_tags %} +{% block content %} +
+
+
+
+
+

Статистика по личным рассылкам {{user.email}}

+
{{user}}
+
+
+
+
+
+
+ + + + + + + + + {% if user.is_authenticated %} + + {% endif %} + + + + {% for object in object_list %} + {% if object.mailing.owner == user.email %} + + + + + + + {% if user.is_authenticated %} + + {% endif %} + + {% endif %} + {% endfor %} +
Тема сообщенияОписание рассылкиДатаСтатусОтвет сервераСоздал
{{ object.mailing.message.subject }}{{ object.mailing.message.content|truncatechars:30 }}{{ object.date_attempt}}{{ object.status}}{{ object.server_response}}{{ object.mailing.owner }}
+
+
+
+{% endblock %}} diff --git a/mailing/templatetags/my_tags.py b/mailing/templatetags/my_tags.py index 2d5c77f..5d795cf 100644 --- a/mailing/templatetags/my_tags.py +++ b/mailing/templatetags/my_tags.py @@ -13,3 +13,5 @@ def media_filter(path): @register.filter def in_group(user, group_name): return user.groups.filter(name=group_name).exists() + + diff --git a/mailing/urls.py b/mailing/urls.py index 77e8d1a..4170315 100644 --- a/mailing/urls.py +++ b/mailing/urls.py @@ -20,7 +20,9 @@ RecipientMailingDeleteView, RecipientMailingDetailView, RecipientMailingListView, - RecipientMailingUpdateView, MailingAttemptCreateView, + RecipientMailingUpdateView, + MailingAttemptCreateView, + MailingAttemptMyListView, ) app_name = MailingConfig.name @@ -66,5 +68,6 @@ path("message//delete/", MessageDeleteView.as_view(), name="message_delete"), path("attempt/", cache_page(60)(MailingAttemptListView.as_view()), name="attempt"), + path("attempt/my/", MailingAttemptMyListView.as_view(), name="attemptmy"), path("attempt/create/", MailingAttemptCreateView.as_view(), name="attempt_create"), ] diff --git a/mailing/views.py b/mailing/views.py index cbbecea..6c3e62e 100644 --- a/mailing/views.py +++ b/mailing/views.py @@ -124,6 +124,7 @@ def get_object(self, queryset=None): class RecipientMailingListView(ListView): model = RecipientMailing + template_name = 'mailing/recipientmailing_list.html' def get_context_data(self, **kwargs): context_data = super().get_context_data(**kwargs) @@ -135,7 +136,7 @@ def get_queryset(self, *args, **kwargs): return super().get_queryset() elif self.request.user.groups.filter(name="Пользователи"): return super().get_queryset().filter(owner=self.request.user) - raise PermissionDenied + # raise PermissionDenied class RecipientMailingDetailView(LoginRequiredMixin, DetailView): @@ -272,3 +273,16 @@ def get_queryset(self, *args, **kwargs): # return super().get_queryset().filter(owner=self.request.user) # raise PermissionDenied return get_attempt_from_cache() + + +class MailingAttemptMyListView(LoginRequiredMixin, ListView): + model = MailingAttempt + template_name = 'mailing/mailingattemptmy_list.html' + + def get_queryset(self, *args, **kwargs): + # if self.request.user.is_superuser: + # return super().get_queryset() + # elif self.request.user.groups.filter(name="Пользователи").exists(): + # return super().get_queryset().filter(owner=self.request.user) + # raise PermissionDenied + return get_attempt_from_cache() From 84f93a566cc93068e2d79fbf0ad93fd4057cc76a Mon Sep 17 00:00:00 2001 From: Admin Date: Thu, 17 Jul 2025 23:53:07 +0300 Subject: [PATCH 10/10] working on develop_1 --- mailing/templates/mailing/mailingattemptmy_list.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/mailing/templates/mailing/mailingattemptmy_list.html b/mailing/templates/mailing/mailingattemptmy_list.html index bfd7149..7451068 100644 --- a/mailing/templates/mailing/mailingattemptmy_list.html +++ b/mailing/templates/mailing/mailingattemptmy_list.html @@ -6,8 +6,7 @@
-

Статистика по личным рассылкам {{user.email}}

-
{{user}}
+

Статистика по личным рассылкам пользователя {{user.email}}

@@ -29,7 +28,7 @@
{{user}}
{% for object in object_list %} - {% if object.mailing.owner == user.email %} + {% if object.mailing.owner == user %} {{ object.mailing.message.subject }} {{ object.mailing.message.content|truncatechars:30 }}