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/.flake8 b/.flake8 new file mode 100644 index 0000000..dda77d8 --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 119 +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 87dae67..22c62df 100644 Binary files a/README.md and b/README.md differ 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..1fae406 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,22 +1,17 @@ -""" -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/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 8c38f3f..69fe72b 100644 --- a/mailing/admin.py +++ b/mailing/admin.py @@ -1,3 +1,72 @@ 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..6329b84 --- /dev/null +++ b/mailing/forms.py @@ -0,0 +1,53 @@ +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__" + exclude = ("can_disable_mailing", "owner") + success_url = reverse_lazy("mailing:mailing_list") + + +class MailingModeratorForm(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__" + exclude = ("can_blocking_client", "owner") + success_url = reverse_lazy("mailing:recipientmailing_list") + + +class RecipientModeratorForm(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..f4ea591 --- /dev/null +++ b/mailing/management/commands/send_mailings.py @@ -0,0 +1,41 @@ +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}" + f"успешно отправлено на {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/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 71a8362..99f16a5 100644 --- a/mailing/models.py +++ b/mailing/models.py @@ -1,3 +1,166 @@ 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"] + permissions = [ + ("can_blocking_client", "Может блокировать получателя"), + ] + + +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"] + permissions = [ + ('can_blocking_sms', 'Может блокировать сообщение'), + ] + + +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..d725cc7 --- /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..7bbc1cf --- /dev/null +++ b/mailing/templates/mailing/includes/main_menu.html @@ -0,0 +1,71 @@ + \ 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..dda23e8 --- /dev/null +++ b/mailing/templates/mailing/mailing_detail.html @@ -0,0 +1,20 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+

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

+
+

Первая отправка: {{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 new file mode 100644 index 0000000..be0213d --- /dev/null +++ b/mailing/templates/mailing/mailing_form.html @@ -0,0 +1,25 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+ {% if object %} +

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

+ {% else %} +

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

+ {% endif %} + + {% 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..b5dafab --- /dev/null +++ b/mailing/templates/mailing/mailingattempt_list.html @@ -0,0 +1,45 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+
+
+
+

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

+
+
+
+
+
+
+ + + + + + + + + {% if user.is_authenticated %} + + {% endif %} + + + + {% for object in object_list %} + + + + + + + {% if user.is_authenticated %} + + {% 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/mailingattemptmy_list.html b/mailing/templates/mailing/mailingattemptmy_list.html new file mode 100644 index 0000000..7451068 --- /dev/null +++ b/mailing/templates/mailing/mailingattemptmy_list.html @@ -0,0 +1,48 @@ +{% extends 'mailing/base.html' %} +{% load my_tags %} +{% block content %} +
+
+
+
+
+

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

+
+
+
+
+
+
+ + + + + + + + + {% if user.is_authenticated %} + + {% endif %} + + + + {% for object in object_list %} + {% if object.mailing.owner == user %} + + + + + + + {% 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/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..993300f --- /dev/null +++ b/mailing/templates/mailing/message_detail.html @@ -0,0 +1,13 @@ +{% 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..e51d49d --- /dev/null +++ b/mailing/templates/mailing/message_form.html @@ -0,0 +1,25 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+ {% if object %} +

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

+ {% else %} +

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

+ {% endif %} + + {% 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..62e3f39 --- /dev/null +++ b/mailing/templates/mailing/message_list.html @@ -0,0 +1,54 @@ +{% 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..4615ef4 --- /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..303eedb --- /dev/null +++ b/mailing/templates/mailing/recipientmailing_form.html @@ -0,0 +1,32 @@ +{% extends 'mailing/base.html' %} +{% block content %} + +
+
+
+
+
+ {% if object %} +

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

+ {% else %} +

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

+ {% endif %} +
+ {% 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..5d795cf --- /dev/null +++ b/mailing/templatetags/my_tags.py @@ -0,0 +1,17 @@ +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() + + 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/mailing/urls.py b/mailing/urls.py new file mode 100644 index 0000000..4170315 --- /dev/null +++ b/mailing/urls.py @@ -0,0 +1,73 @@ +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, + MailingAttemptCreateView, + MailingAttemptMyListView, +) + +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"), + 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 91ea44a..6c3e62e 100644 --- a/mailing/views.py +++ b/mailing/views.py @@ -1,3 +1,288 @@ -from django.shortcuts import render +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, + RecipientModeratorForm, + MailingModeratorForm +) +from mailing.models import ( + Mailing, + MailingAttempt, + Message, + RecipientMailing +) +from mailing.services import ( + get_attempt_from_cache, + get_mailing_from_cache +) -# Create your views here. + +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 + def get_queryset(self, *args, **kwargs): + return get_mailing_from_cache() + + +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") + # 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): + 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 + template_name = 'mailing/recipientmailing_list.html' + + 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 + + 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): + 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): + queryset = super().get_queryset() + return queryset + + +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 + 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() 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 ba2df15..a712837 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" @@ -96,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" @@ -175,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" @@ -463,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" @@ -595,4 +665,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "71538870f4b5e254e94790c04312bf8e89df9da303a394cf5b9e6a236aee6251" +content-hash = "4c6f1d54255b3f5fc02547d98bf2eaf45d19c0627653eca1cc3405fa33dbc2a5" diff --git a/pyproject.toml b/pyproject.toml index cfe53a8..f06944a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,8 @@ psycopg2-binary = "^2.9.10" 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/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/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/forms.py b/users/forms.py new file mode 100644 index 0000000..1597142 --- /dev/null +++ b/users/forms.py @@ -0,0 +1,103 @@ +from django import forms +from django.contrib.auth.forms import 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 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..bc958dd 100644 --- a/users/models.py +++ b/users/models.py @@ -1,3 +1,52 @@ +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..c617563 --- /dev/null +++ b/users/services.py @@ -0,0 +1,22 @@ +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 + +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") +@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] + user.save() + return redirect(reverse("users:users")) diff --git a/users/templates/registration/login.html b/users/templates/registration/login.html new file mode 100644 index 0000000..e5d1f3e --- /dev/null +++ b/users/templates/registration/login.html @@ -0,0 +1,26 @@ +{% extends 'mailing/base.html' %} +{% block title %}Login{% endblock %} + +{% 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..41ef3b9 --- /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..ac6cdcd --- /dev/null +++ b/users/templates/users/user_form.html @@ -0,0 +1,31 @@ +{% extends 'mailing/base.html' %} +{% block content %} +
+
+
+
+
+
+ {% csrf_token %} + {{ form.as_p }} +
+ + {% if user.is_superuser %} + Отмена + {% elif user.is_authenticated %} + Отмена + {% elif user.is_authenticated == False %} + Отмена + {% 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..9b97600 --- /dev/null +++ b/users/templates/users/user_list.html @@ -0,0 +1,61 @@ +{% 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..ebca95b --- /dev/null +++ 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 "#" 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/urls.py b/users/urls.py new file mode 100644 index 0000000..7896df3 --- /dev/null +++ b/users/urls.py @@ -0,0 +1,39 @@ +from django.contrib.auth.views import LoginView, 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, + UserUpdateView, +) + +app_name = UsersConfig.name + +urlpatterns = [ + 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"), + 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..b42ea28 100644 --- a/users/views.py +++ b/users/views.py @@ -1,3 +1,153 @@ -from django.shortcuts import render +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 +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, + UserRegistrationForm, + UserUpdateForm +) +from users.models import User -# Create your views here. + +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 + + users_group = Group.objects.get(name='Пользователи') + user.groups.add(users_group) + + user.save() + send_mail( + subject="Подтверждение почты", + message=f"Здравствуйте, перейдите по ссылке для подтверждения почты: {url} ", + from_email=EMAIL_HOST_USER, + recipient_list=[user.email], + ) + return super().form_valid(form) + + +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 or self.object.email == self.request.user.email: + 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 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): + 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 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): + 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)