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 %}
+
+
+
+
+
+
+
+
+
+
+ Рассылки сообщений
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Toggle theme
+
+
+
+
+
+
+
+ Light
+
+
+
+
+
+
+
+
+
+
+ Dark
+
+
+
+
+
+
+
+
+
+
+ Auto
+
+
+
+
+
+
+
+
+
+
+ {% 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 @@
+
+
+
+
+
+
+
+
+
+
+ {% load my_tags %}
+ {% if user|in_group:"Пользователи" or user.is_superuser%}
+
+ {% endif %}
+ {% if user.is_superuser %}
+
+ {% endif %}
+
+
+
+
+
+ Previous
+
+
+
+ Next
+
+
\ 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 @@
+
+
+
+
+
+
+
+
+
+
+ Главная
+
+ {% if user.is_authenticated %}
+
+ Рассылки
+
+
+ Получатели
+
+ {% if user.is_superuser %}
+
+ Сообщения
+
+ {% endif %}
+ {% load my_tags %}
+ {% if user|in_group:"Пользователи" or user.is_superuser%}
+
+ Статистика
+
+ {% endif %}
+ {% endif %}
+
+ Пользователь
+
+
+
+
+
+
+
+
\ 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_superuser %}
+
+ Создано рассылок:
+ {{ count_mailing }}
+
+
+ Количество активных рассылок:
+ {{ active_mailings_count }}
+
+
+ Количество уникальных получателей:
+ {{ unique_clients_count }}
+
+ {% else %}
+
+ Создано рассылок:
+ {{ count_mailing }}
+
+
+ Количество активных рассылок:
+ {{ active_mailings_count }}
+
+
+ Количество уникальных получателей:
+ {{ unique_clients_count }}
+
+ {% 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 %}
+
+
+{% 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 %}
+
+
+{% 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 %}
+
+ {{ 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 %}
+
+ {% if user.is_superuser %}
+ {{ object.owner }}
+ {% endif %}
+
+ Посмотреть
+ {% if user.is_superuser or user|in_group:"Пользователи" %}
+ Редактировать
+ Удалить
+ {% endif %}
+ {% if user.is_superuser %}
+ Запустить »
+ {% endif %}
+
+
+
+ {% endfor%}
+
+
+
+
+
+{% 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 %}
+
+ {{ object.mailing.message.subject }}
+ {{ object.mailing.message.content|truncatechars:30 }}
+ {{ object.date_attempt}}
+ {{ object.status}}
+ {{ object.server_response}}
+ {% if user.is_authenticated %}
+ {{ object.mailing.owner }}
+ {% endif %}
+
+ {% endfor%}
+
+
+
+
+{% 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 %}
+
+ {{ object.mailing.message.subject }}
+ {{ object.mailing.message.content|truncatechars:30 }}
+ {{ object.date_attempt}}
+ {{ object.status}}
+ {{ object.server_response}}
+ {% if user.is_authenticated %}
+ {{ object.mailing.owner }}
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+{% 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 %}
+
+ {{ object.subject }}
+ {{ object.content|truncatechars:30 }}
+ {% if request.user.is_superuser %}
+ {{ object.owner }}
+ {% endif %}
+
+ Посмотреть
+ »
+ Редактировать
+ »
+ Удалить
+
+
+ {% endfor%}
+
+
+
+
+{% 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 }}
+
+
+ {% if object %}
+ Сохранить
+ {% else %}
+ Создать
+ {% endif %}
+
+
Отмена
+
+
+
+
+
+
+{% 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 %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Фото
+ ФИО получателя
+ email получателя
+ Комментарий
+ {% if request.user.is_superuser %}
+ Создал
+ {% endif %}
+ Действия
+
+
+
+ {% for object in object_list %}
+
+
+
+
+
+ {{ object.fio }}
+ {{ object.email}}
+ {{ object.comment|truncatechars:30 }}
+ {% if request.user.is_superuser %}
+ {{ object.owner }}
+ {% endif %}
+
+ Посмотреть
+ »
+ {% if user.is_superuser or user|in_group:"Пользователи" %}
+ Редактировать
+ »
+ Удалить
+ {% endif %}
+
+
+
+ {% endfor%}
+
+
+
+
+
+
+{% 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 @@
+[1m.\mailing\views.py[m[36m:[m6[36m:[m101[36m:[m [1m[31mE501[m line too long (103 > 100 characters)
+[1m.\mailing\views.py[m[36m:[m30[36m:[m101[36m:[m [1m[31mE501[m line too long (104 > 100 characters)
+[1m.\mailing\views.py[m[36m:[m62[36m:[m101[36m:[m [1m[31mE501[m line too long (110 > 100 characters)
+[1m.\mailing\views.py[m[36m:[m129[36m:[m101[36m:[m [1m[31mE501[m line too long (110 > 100 characters)
+[1m.\users\views.py[m[36m:[m59[36m:[m101[36m:[m [1m[31mE501[m 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 }}
+
+
+
+
+
+
+
+
+{% 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 not object %}
+ Зарегистрироваться
+ {% else %}
+ Сохранить
+ {% endif %}
+
+ {% 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 %}
+
+
+
+
+
Список пользователей
+
+
+
+ email
+ Имя
+ Фамилия
+ Телефон
+ Администратор
+ Активность
+ Последнее подключение
+ {% if user.is_superuser %}
+ Действия
+ {% endif %}
+
+
+
+
+ {% for object in object_list %}
+
+ {{ object.email }}
+ {{ object.first_name }}
+ {{ object.last_name }}
+ {{ object.phone_number }}
+ {{ object.is_superuser|yesno:"Да, Нет"}}
+ {% if user.is_superuser or user|in_group:"Менеджеры" %}
+
+ {% if object.is_active %}
+ Заблокировать
+ {% else %}
+ Разблокировать
+ {% endif %}
+ {% else %}
+ {{object.is_active|yesno:"Активный, Не активный"}}
+
+ {% endif %}
+ {{ object.last_login }}
+ {% if user.is_superuser %}
+
+ Редактировать
+ Удалить
+
+ {% endif %}
+
+ {% endfor%}
+
+
+
+
+{% 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)