diff --git a/.python-version b/.python-version
index 75fb1c5..6f16a1b 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-django_project
+dz_django
diff --git a/authapp/__init__.py b/authapp/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/authapp/admin.py b/authapp/admin.py
new file mode 100644
index 0000000..d7a2f0f
--- /dev/null
+++ b/authapp/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+from authapp import models
+
+@admin.register(models.CustomUser)
+class CustomUserAdmin(admin.ModelAdmin):
+ list_display = ["id", "username", "email", "is_active", "date_joined"]
+ ordering = ["-date_joined"]
\ No newline at end of file
diff --git a/authapp/apps.py b/authapp/apps.py
new file mode 100644
index 0000000..ef802c9
--- /dev/null
+++ b/authapp/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class AuthappConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'authapp'
diff --git a/authapp/forms.py b/authapp/forms.py
new file mode 100644
index 0000000..834a10b
--- /dev/null
+++ b/authapp/forms.py
@@ -0,0 +1,56 @@
+import os
+from django import forms
+from django.contrib.auth import get_user_model
+from django.contrib.auth.forms import UserCreationForm, UsernameField
+from django.core.exceptions import ValidationError
+from django.utils.translation import gettext_lazy as _
+
+class CustomUserCreationForm(UserCreationForm):
+ field_order = [
+ "username",
+ "password1",
+ "password2",
+ "email",
+ "first_name",
+ "last_name",
+ "age",
+ "avatar",
+ ]
+
+ class Meta:
+ model = get_user_model()
+ fields = (
+ "username",
+ "email",
+ "first_name",
+ "last_name",
+ "age",
+ "avatar",
+ )
+ field_classes = {"username": UsernameField}
+
+class CustomUserChangeForm(forms.ModelForm):
+ class Meta:
+ model = get_user_model()
+ fields = (
+ "username",
+ "email",
+ "first_name",
+ "last_name",
+ "age",
+ "avatar",
+ )
+ field_classes = {"username": UsernameField}
+
+ def clean_avatar(self):
+ arg_as_str = "avatar"
+ if arg_as_str in self.changed_data and self.instance.avatar:
+ if os.path.exists(self.instance.avatar.path):
+ os.remove(self.instance.avatar.path)
+ return self.cleaned_data.get(arg_as_str)
+
+ def clean_age(self):
+ data = self.cleaned_data.get("age")
+ if data < 10 or data > 100:
+ raise ValidationError(_("Please, enter a valid age!"))
+ return data
\ No newline at end of file
diff --git a/authapp/migrations/0001_initial.py b/authapp/migrations/0001_initial.py
new file mode 100644
index 0000000..6738181
--- /dev/null
+++ b/authapp/migrations/0001_initial.py
@@ -0,0 +1,44 @@
+# Generated by Django 4.0.4 on 2022-06-02 13:27
+
+import authapp.models
+import django.contrib.auth.models
+import django.contrib.auth.validators
+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='CustomUser',
+ 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')),
+ ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username')),
+ ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
+ ('age', models.PositiveIntegerField(blank=True, null=True)),
+ ('avatar', models.ImageField(blank=True, null=True, upload_to=authapp.models.users_avatars_path)),
+ ('email', models.CharField(error_messages={'unique': 'A user with that email address already exists.'}, max_length=256, unique=True, verbose_name='email address')),
+ ('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(auto_now_add=True, verbose_name='date joined')),
+ ('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={
+ 'abstract': False,
+ },
+ managers=[
+ ('objects', django.contrib.auth.models.UserManager()),
+ ],
+ ),
+ ]
diff --git a/authapp/migrations/__init__.py b/authapp/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/authapp/models.py b/authapp/models.py
new file mode 100644
index 0000000..8758ae8
--- /dev/null
+++ b/authapp/models.py
@@ -0,0 +1,79 @@
+from pathlib import Path
+from time import time
+from django.contrib.auth.base_user import AbstractBaseUser
+from django.contrib.auth.models import PermissionsMixin, UserManager
+from django.contrib.auth.validators import ASCIIUsernameValidator
+from django.core.mail import send_mail
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+def users_avatars_path(instance, filename):
+ num = int(time() * 1000)
+ suff = Path(filename).suffix
+ return "user_{0}/avatars/{1}".format(instance.username, f"pic_{num}{suff}")
+
+class CustomUser(AbstractBaseUser, PermissionsMixin):
+ username_validator = ASCIIUsernameValidator()
+ username = models.CharField(
+ _("username"),
+ max_length=150,
+ unique=True,
+ help_text=_(
+ "Required. 150 characters or fewer. Letters, digits and @/./+/-/_only."),
+ validators=[username_validator],
+ error_messages={
+ "unique": _("A user with that username already exists."),
+ },
+ )
+ first_name = models.CharField(_("first name"), max_length=150, blank=True)
+ last_name = models.CharField(_("last name"), max_length=150, blank=True)
+ age = models.PositiveIntegerField(blank=True, null=True)
+ avatar = models.ImageField(
+ upload_to=users_avatars_path, blank=True, null=True
+ )
+ email = models.CharField(
+ _("email address"),
+ max_length=256,
+ unique=True,
+ error_messages={
+ "unique": _("A user with that email address already exists."),
+ },
+ )
+ is_staff = models.BooleanField(
+ _("staff status"),
+ default=False,
+ help_text=_(
+ "Designates whether the user can log into this admin site."
+ ),
+ )
+ is_active = models.BooleanField(
+ _("active"),
+ default=True,
+ help_text=_(
+ "Designates whether this user should be treated as active. \
+ Unselect this instead of deleting accounts."
+ ),
+ )
+ date_joined = models.DateTimeField(_("date joined"), auto_now_add=True)
+ objects = UserManager()
+ EMAIL_FIELD = "email"
+ USERNAME_FIELD = "username"
+ REQUIRED_FIELDS = ["email"]
+
+ class Meta:
+ verbose_name = _("user")
+ verbose_name_plural = _("users")
+
+ def clean(self):
+ super().clean()
+ self.email = self.__class__.objects.normalize_email(self.email)
+
+ def get_full_name(self):
+ full_name = "%s %s" % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def get_short_name(self):
+ return self.first_name
+
+ def email_user(self, subject, message, from_email=None, **kwargs):
+ send_mail(subject, message, from_email, [self.email], **kwargs)
\ No newline at end of file
diff --git a/authapp/templates/admin/authapp/customers_form.html b/authapp/templates/admin/authapp/customers_form.html
new file mode 100644
index 0000000..e356b7d
--- /dev/null
+++ b/authapp/templates/admin/authapp/customers_form.html
@@ -0,0 +1,33 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+ {% if user.is_anonymous %}
+
Регистрация нового пользователя
+ {% else %}
+
Редактировать профиль
+
+
+ {% if user.avatar %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/authapp/templates/admin/base_site.html b/authapp/templates/admin/base_site.html
new file mode 100644
index 0000000..1b2aa42
--- /dev/null
+++ b/authapp/templates/admin/base_site.html
@@ -0,0 +1,12 @@
+{% extends "includes/base.html" %}
+{% load static %}
+{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{
+site_title|default:_('Django site admin') }}{% endblock %}
+{% block branding %}
+
+{% endblock %}
+{% block nav-global %}{% endblock %}
\ No newline at end of file
diff --git a/authapp/templates/authapp/customuser_form.html b/authapp/templates/authapp/customuser_form.html
new file mode 100644
index 0000000..ef68c7d
--- /dev/null
+++ b/authapp/templates/authapp/customuser_form.html
@@ -0,0 +1,33 @@
+{% extends 'includes/base.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+ {% if user.is_anonymous %}
+
Регистрация нового пользователя
+ {% else %}
+
Редактировать профиль
+
+
+ {% if user.avatar %}
+
+ {% else %}
+
+ {% endif %}
+
+
+ {% endif %}
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/authapp/templates/includes/messages.html b/authapp/templates/includes/messages.html
new file mode 100644
index 0000000..7c5b1ad
--- /dev/null
+++ b/authapp/templates/includes/messages.html
@@ -0,0 +1,34 @@
+
+
+
+
+ {% for message in messages %}
+
+
+ {% if message.level > 20 %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/authapp/templates/registration/login.html b/authapp/templates/registration/login.html
new file mode 100644
index 0000000..2005d94
--- /dev/null
+++ b/authapp/templates/registration/login.html
@@ -0,0 +1,81 @@
+{% load static %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Welcome to Braniac!
+
+
+
+ {% include 'includes/messages.html' %}
+
+
+
+
+
+
Вход пользователя
+
+
+
+
+
Вход через социальные сети
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/authapp/templates/registration/profile_edit.html b/authapp/templates/registration/profile_edit.html
new file mode 100644
index 0000000..58bdf11
--- /dev/null
+++ b/authapp/templates/registration/profile_edit.html
@@ -0,0 +1,60 @@
+{% block content %}
+
+
+
Редактирование профиля
+
+
+ {% if user.avatar %}
+
+ {% else %}
+
+ {% endif %}
+
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/authapp/templates/registration/register.html b/authapp/templates/registration/register.html
new file mode 100644
index 0000000..57219f8
--- /dev/null
+++ b/authapp/templates/registration/register.html
@@ -0,0 +1,16 @@
+{% extends 'includes/base.html' %}
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+
+
Регистрация нового пользователя
+
+
Зарегистрироваться
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/authapp/tests.py b/authapp/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/authapp/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/authapp/urls.py b/authapp/urls.py
new file mode 100644
index 0000000..3372245
--- /dev/null
+++ b/authapp/urls.py
@@ -0,0 +1,18 @@
+from django.urls import path
+from authapp import views
+from authapp.apps import AuthappConfig
+from django.urls import include, path
+from django.views.generic import RedirectView
+from django.conf import settings
+from django.conf.urls.static import static
+from django.contrib import admin
+
+
+app_name = AuthappConfig.name
+
+urlpatterns = [
+ path("login/", views.CustomLoginView.as_view(), name="login"),
+ path("logout/", views.CustomLogoutView.as_view(), name="logout"),
+ path("register/", views.RegisterView.as_view(), name="register"),
+ path("profile_edit//", views.ProfileEditView.as_view(), name="profile_edit"),
+]
\ No newline at end of file
diff --git a/authapp/views.py b/authapp/views.py
new file mode 100644
index 0000000..0e2caca
--- /dev/null
+++ b/authapp/views.py
@@ -0,0 +1,49 @@
+from django.contrib import messages
+from django.contrib.auth import get_user_model
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.contrib.auth.views import LoginView, LogoutView
+from django.urls import reverse_lazy
+from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
+from django.views.generic import CreateView, UpdateView
+from requests import request
+from authapp import forms
+from django.contrib.auth.forms import AuthenticationForm
+# import .emodels as authapp_models
+
+class CustomLoginView(LoginView):
+ def form_valid(self, form):
+ ret = super().form_valid(form)
+ name = self.request.user.get_full_name()
+ message = _(f"Вход совершен Привет, {name if name else self.request.user.get_username()}")
+ messages.add_message(self.request, messages.INFO, mark_safe(message))
+ return ret
+
+ def form_invalid(self, form):
+ for _unused, msg in form.error_messages.items():
+ msg = 'Введены неправильные логин и/или пароль'
+ messages.add_message(self.request, messages.WARNING,mark_safe(f"Что-то пошло не так: {msg}"),)
+ return self.render_to_response(self.get_context_data(form=form))
+
+class CustomLogoutView(LogoutView):
+ def dispatch(self, request, *args, **kwargs):
+ messages.add_message(self.request, messages.INFO, _("Досвидания!"))
+ return super().dispatch(request, *args, **kwargs)
+
+class RegisterView(CreateView):
+ model = get_user_model()
+ form_class = forms.CustomUserCreationForm
+ success_url = reverse_lazy("mainapp:main_page")
+
+
+
+class ProfileEditView(UserPassesTestMixin, UpdateView):
+ model = get_user_model()
+ form_class = forms.CustomUserChangeForm
+
+ def test_func(self):
+ return True if self.request.user.pk == self.kwargs.get("pk") else False
+
+ def get_success_url(self):
+ return reverse_lazy("authapp:profile_edit", args=[self.request.user.pk])
+
diff --git a/config/__init__.py b/config/__init__.py
index e69de29..a55740a 100644
--- a/config/__init__.py
+++ b/config/__init__.py
@@ -0,0 +1,3 @@
+from .celery import celery_app
+
+__all__ = ("celery_app",)
\ No newline at end of file
diff --git a/config/celery.py b/config/celery.py
new file mode 100644
index 0000000..9b3b422
--- /dev/null
+++ b/config/celery.py
@@ -0,0 +1,7 @@
+import os
+from celery import Celery
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")
+celery_app = Celery("braniac")
+celery_app.config_from_object("django.conf:settings", namespace="CELERY")
+celery_app.autodiscover_tasks()
\ No newline at end of file
diff --git a/config/settings.py b/config/settings.py
index cca7b92..48a6d7a 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -10,12 +10,15 @@
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = 'django-insecure-)8%fdb3%51xymravhh$zl#u^3#5dmipoui(^f@v##8x#8hmf%^'
+SECRET_KEY = os.environ.get('django_key')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
+if DEBUG:
+ INTERNAL_IPS = ["127.0.0.1",]
-ALLOWED_HOSTS = []
+
+ALLOWED_HOSTS = ['*']
# Application definition
@@ -28,6 +31,13 @@
'django.contrib.messages',
'django.contrib.staticfiles',
'mainapp',
+ 'markdownify.apps.MarkdownifyConfig',
+ 'authapp.apps.AuthappConfig',
+ 'social_django',
+ 'crispy_forms',
+ "debug_toolbar",
+
+
]
MIDDLEWARE = [
@@ -38,6 +48,7 @@
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ "debug_toolbar.middleware.DebugToolbarMiddleware",
]
ROOT_URLCONF = 'config.urls'
@@ -49,11 +60,14 @@
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- # "mainapp.context_processors.example.simple_context_processor",
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.template.context_processors.media",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ "mainapp.context_processors.example.simple_context_processor",
+ "social_django.context_processors.backends",
+ "social_django.context_processors.login_redirect",
],
'libraries':{'email_to_link':'mainapp.context_processors.example'}
@@ -61,6 +75,14 @@
},
]
+CACHES = {
+ "default": {
+ "BACKEND": "django_redis.cache.RedisCache",
+ "LOCATION": "redis://127.0.0.1:6379",
+ "OPTIONS": {
+ "CLIENT_CLASS": "django_redis.client.DefaultClient",
+ },}}
+
WSGI_APPLICATION = 'config.wsgi.application'
@@ -76,8 +98,6 @@
# Password validation
-# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
-
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
@@ -121,3 +141,59 @@
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+MEDIA_URL = "/media/"
+MEDIA_ROOT = BASE_DIR / "media"
+AUTH_USER_MODEL = "authapp.CustomUser"
+LOGIN_REDIRECT_URL = "mainapp:main_page"
+LOGOUT_REDIRECT_URL = "mainapp:main_page"
+MESSAGE_STORAGE = "django.contrib.messages.storage.session.SessionStorage"
+AUTHENTICATION_BACKENDS = (
+ "social_core.backends.github.GithubOAuth2",
+ "django.contrib.auth.backends.ModelBackend",
+ )
+
+SOCIAL_AUTH_GITHUB_KEY = os.environ.get('SOCIAL_AUTH_GITHUB_KEY')
+SOCIAL_AUTH_GITHUB_SECRET = os.environ.get('django_git_password')
+AUTH_USER_MODEL = 'authapp.CustomUser'
+CRISPY_TEMPLATE_PACK = "bootstrap4"
+LOG_FILE = BASE_DIR / "var" / "log" / "main_log.log"
+CELERY_BROKER_URL = "redis://localhost:6379"
+CELERY_RESULT_BACKEND = "redis://localhost:6379"
+
+LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "formatters": {
+ "console": {
+ "format": "[%(asctime)s] %(levelname)s %(name)s (%(lineno)d)%(message)s"},
+ },
+ "handlers": {
+ "console": {"class": "logging.StreamHandler", "formatter": "console"}, },
+ "loggers": {"django": {"level": "INFO", "handlers": ["console"]},
+ },
+}
+
+CELERY_BROKER_URL = "redis://localhost:6379"
+CELERY_RESULT_BACKEND = "redis://localhost:6379"
+
+
+# глобально
+EMAIL_HOST = 'smtp.mail.ru'#"localhost"
+EMAIL_PORT = "465" # 465- mail, yandex
+EMAIL_HOST_USER = os.environ.get('email') #"django@geekshop.local" # myname@yandex.ru
+EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') #
+EMAIL_USE_SSL = True # yandex True # google False
+EMAIL_USE_TLS = False # google True , yandex - False
+EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
+
+# локально
+# EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
+# EMAIL_FILE_PATH = os.path.join(BASE_DIR, 'var', 'email_messages')
+# EMAIL_FILE_PATH = "var/email-messages/"
+
+# консоль
+# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
+
+
+# SESSION_COOKIE_SECURE = False
+# CSRF_COOKIE_SECURE = False
\ No newline at end of file
diff --git a/config/urls.py b/config/urls.py
index 8755559..8d3fc9b 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -1,10 +1,20 @@
from django.contrib import admin
+
+from django.conf import settings
from django.urls import path, include
from django.views.generic import RedirectView
+from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("", RedirectView.as_view(url="mainapp/")),
path("mainapp/", include("mainapp.urls", namespace='mainapp')),
-]
+ path("authapp/", include("authapp.urls", namespace="authapp")),
+ path("social_auth/", include("social_django.urls", namespace='social'))
+ ]
+
+if settings.DEBUG:
+ import debug_toolbar
+ urlpatterns.append(path("__debug__/", include(debug_toolbar.urls)))
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/db.sqlite3 b/db.sqlite3
new file mode 100644
index 0000000..f6e15d2
Binary files /dev/null and b/db.sqlite3 differ
diff --git a/mainapp/admin.py b/mainapp/admin.py
index 8c38f3f..be834ec 100644
--- a/mainapp/admin.py
+++ b/mainapp/admin.py
@@ -1,3 +1,40 @@
from django.contrib import admin
+from mainapp import models as mainapp_models
+from django.utils.translation import gettext_lazy as _
-# Register your models here.
+
+@admin.register(mainapp_models.News)
+class NewsAdmin(admin.ModelAdmin):
+ list_display = ['id', 'title', 'preambule', 'deleted', 'created']
+ ordering = ['-id', 'title']
+ list_per_page = 4
+ list_filter = ['deleted', 'body_as_markdown']
+ search_fields = ["title", "preambule", "body"]
+ actions = ['mark_deleted']
+
+@admin.register(mainapp_models.Courses)
+class CourseAdmin(admin.ModelAdmin):
+ list_display = ['name', 'description', 'cost', 'deleted']
+ ordering = ['-name', 'description']
+ list_per_page = 4
+ list_filter = ['deleted','description_as_markdown']
+ search_fields = ['description', 'name']
+ actions = ['mark_deleted']
+
+@admin.register(mainapp_models.Lesson)
+class LessonAdmin(admin.ModelAdmin):
+ list_display = ["id", "get_course_name", "num", "title", "deleted"]
+ ordering = ["-course__name", "-num"]
+ list_per_page = 5
+ list_filter = ["course", "created", "deleted"]
+ actions = ["mark_deleted"]
+
+ def get_course_name(self, obj):
+ return obj.course.name
+
+ get_course_name.short_description = _("Course")
+
+ def mark_deleted(self, request, queryset):
+ queryset.update(deleted=True)
+
+ mark_deleted.short_description = _("Mark deleted")
\ No newline at end of file
diff --git a/mainapp/apps.py b/mainapp/apps.py
index 8aa89a8..ac8c3dd 100644
--- a/mainapp/apps.py
+++ b/mainapp/apps.py
@@ -4,3 +4,4 @@
class MainappConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'mainapp'
+ verbose_name='Основное приложение'
diff --git a/mainapp/context_processors/example.py b/mainapp/context_processors/example.py
index d6969ed..06cfbe9 100644
--- a/mainapp/context_processors/example.py
+++ b/mainapp/context_processors/example.py
@@ -6,3 +6,7 @@
@register.filter
def email_to_link(value):
return mark_safe(f"{value} ")
+
+
+def simple_context_processor(request):
+ return {"foo": "bar"}
\ No newline at end of file
diff --git a/mainapp/fixtures/001_news.json b/mainapp/fixtures/001_news.json
new file mode 100644
index 0000000..a7f5ce9
--- /dev/null
+++ b/mainapp/fixtures/001_news.json
@@ -0,0 +1,74 @@
+[
+ {
+ "model": "mainapp.news",
+ "pk": 1,
+ "fields": {
+ "title": "Запуск нового курса по Python",
+ "preambule": "Мы рады Вам сообщить о запуске нового курса по Python для начинающих!",
+ "body": "Python предназначен для разработки и изучения новых приложений. Мы предлагаем не просто изучить новый язык программирования, но и научиться применять его на практике. Данный курс будет полезен не только тем, кто хочет научиться программировать, но и всем тем, кому не хватает времени на выполнение домашних заданий, а также тем, у кого нет возможности посещать курсы. Курс состоит из 7 встреч (одно занятие в неделю длительностью 3 часа).\r\nВстречи проводятся по воскресеньям с 14:00.",
+ "body_as_markdown": false,
+ "created": "2021-07-07T18:32:47.331Z",
+ "updated": "2021-07-07T18:32:47.331Z"
+ }
+ },
+ {
+ "model": "mainapp.news",
+ "pk": 2,
+ "fields": {
+ "title": "Урока по PHP в среду не состоится",
+ "preambule": "Всем, кто не успел купить курс в понедельник, настоятельно рекомендую это сделать по ссылке в конце публикации.",
+ "body": "Сегодня в Саратове не будет проходить открытый урок по PHP, который должен был состояться в среду на факультете компьютерных наук СГУ. Преподавательница, за которой закреплена аудитория, сказала что заболела и не придет. На следующий урок, запланированный на 28 февраля, перенесли все, кроме курса «Веб-программирование». Об этом сообщили в группе факультета в социальной сети «Вконтакте». Курс «Веб-программист» состоит из двух частей. Первая — общий курс в течение 72 часов.",
+ "body_as_markdown": false,
+ "created": "2021-07-06T18:32:47.331Z",
+ "updated": "2021-07-06T18:32:47.331Z"
+ }
+ },
+ {
+ "model": "mainapp.news",
+ "pk": 3,
+ "fields": {
+ "title": "Всем руководителям подразделений подключится к Zoom",
+ "preambule": "Все сотрудники подключены к Zoom, поэтому мы видим, что они делают и что говорят.",
+ "body": "Каждый из 5 руководителей будет в одном из своих офисов с помощью этого приложения, мы хотим, чтобы они могли видеть все, что происходит на сайте, просматривать отчеты, которые мы им предоставляем. После того, как сотрудники подключили ZOOM, нам необходимо, чтобы каждый секретарь, менеджер и т. Д. Были подключены к каждому из этих 5 компьютеров.",
+ "body_as_markdown": false,
+ "created": "2021-07-05T18:32:47.331Z",
+ "updated": "2021-07-05T18:32:47.331Z"
+ }
+ },
+ {
+ "model": "mainapp.news",
+ "pk": 4,
+ "fields": {
+ "title": "Сегодня студенты всего мира отмечают праздник",
+ "preambule": "Подробности внутри...",
+ "body": "Сегодня студенты всего мира отмечают праздник, который официально является международным, но более распространен в азиатских странах, таких как Китай или Япония. Он называется Фестиваль студенческого пирога (Holly Fest) и празднуется каждый год в начале сентября. Это праздник, объединяющий студентов во всем мире, которые отмечают начало нового учебного года и дарят друг другу подарки. Во многих азиатских странах этот день также знаменует начало нового года по китайскому календарю День Рождения: 9 июля, 23 года",
+ "body_as_markdown": false,
+ "created": "2021-07-04T18:32:47.331Z",
+ "updated": "2021-07-04T18:32:47.331Z"
+ }
+ },
+ {
+ "model": "mainapp.news",
+ "pk": 5,
+ "fields": {
+ "title": "Встречайте нового преподавателя направления DevOps",
+ "preambule": "Дмитрий Шишмарев работает в IT-бизнесе с 2001 года.",
+ "body": "До прихода в компанию 1С-Битрикс начинал карьеру в качестве системного администратора. В 2009 перешел на должность DevOps-инженера, где и трудится по сей день, развиваясь в своей сфере. Дмитрий является сертифицированным специалистом компании Bitrix. На его счету более 30 успешно реализованных проектов, среди которых: разработка корпоративной системы управления веб-проектами на базе 1C-Bitrix",
+ "body_as_markdown": false,
+ "created": "2021-07-03T18:32:47.331Z",
+ "updated": "2021-07-03T18:32:47.331Z"
+ }
+ },
+ {
+ "model": "mainapp.news",
+ "pk": 6,
+ "fields": {
+ "title": "JavaScript снова возглавил рейтинг самых отвратительных языков",
+ "preambule": "И опять, и снова.",
+ "body": "Рейтинг самых отвратительных языков программирования, составленный британским изданием, возглавляет JavaScript. В опросе приняли участие более 1000 специалистов по разработке ПО, работающих в различных компаниях. При составлении списка учитывались такие свойства языка, как сложность и простота обучения, а также производительность. Специалисты оценивали язык по 10-балльной шкале, где один балл означал «ужасающий», а 10 баллов – «отвратительный».",
+ "body_as_markdown": false,
+ "created": "2021-07-02T18:32:47.331Z",
+ "updated": "2021-07-02T18:32:47.331Z"
+ }
+ }
+]
\ No newline at end of file
diff --git a/mainapp/fixtures/002_courses.json b/mainapp/fixtures/002_courses.json
new file mode 100644
index 0000000..425b530
--- /dev/null
+++ b/mainapp/fixtures/002_courses.json
@@ -0,0 +1,106 @@
+[
+ {
+ "model": "mainapp.courses",
+ "pk": 1,
+ "fields": {
+ "name": "Web Python",
+ "description": "Курс Web Python поможет Вам осовоить язык программирования Python, а также разобраться с его основными аспектами, такими как: модули, парсеры, регулярные выражения. Курс поможет Вам научиться составлять эффективные программы для решения Ваших задач и проблем. Он научит Вас создавать собственные веб-приложения для работы с базами данных и сайтами. Вы сможете использовать Python для изучения Java, Ruby, PHP, C, C ++, C # или любого другого языка программирования.\r\nНа курсе Web Python проходит изучение веб-фреймворков Django, Bootstrap и Flask.\r\nВо втором семестре вы изучите такие инструменты и методы, как:\r\n\r\n* PHP и MySql;\r\n* Работа с файлами DOC, EXCEL, PATH;\r\n* Реляционная и объектная модель базы данных;\r\n* Создание и управление пакетами;\r\n* Как работать с FTP сервером;\r\n* Настройка среды и создание сайтов;\r\n* Веб-серверы;\r\n* Конфигурационное управление в Unix и Windows;\r\n* Инструменты для создания сайта;\r\n* Интеграция внешних приложений;\r\n* Расширения;",
+ "description_as_markdown": true,
+ "cost": "10000.50",
+ "cover": "img/course001.jpg",
+ "created": "2021-07-11T08:24:53.084Z",
+ "updated": "2021-07-11T09:06:35.322Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 2,
+ "fields": {
+ "name": "Web Golang",
+ "description": "На курсе Web Golang вы изучите основы Go, получите практические навыки реализации проектов на Golang и научитесь работать в распределенных средах разработки. Вы узнаете: что такое Go как компилировать код Go как писать тесты как работает веб-сервер. Курс ведет Виктор Иванов\r\nСертифицированный специалист Google Developer Expert (Certified Technical Professional).\r\nАвтор учебника “Web-разработка на Go”. На курсе вы будете изучать основы языка программирования Go и получите практический опыт реализации проектов на Go.",
+ "description_as_markdown": false,
+ "cost": "9009.99",
+ "cover": "img/course002.jpg",
+ "created": "2021-07-11T08:28:07.397Z",
+ "updated": "2021-07-11T09:06:40.127Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 3,
+ "fields": {
+ "name": "Web JavaScript",
+ "description": "Курс JavaScript ведёт преподаватель Дмитрий Кокшаров. Он преподает в разных вузах, в том числе и в МФТИ. Дмитрий считает, что курс — это база знаний, которая пригодится в жизни. \r\nКурс JavaScript состоит из модулей, один из которых — «Основы функционального программирования». Этот модуль рассчитан на тех, кто не знает основы JavaScript или только начал заниматься этим языком. Вы узнаете, как устроены функции, какие бывают типы данных, как применять модификаторы.\r\nНа курсе JavaScript Вы изучите:\r\n\r\n* Основные паттерны проектирования (design patterns) \r\n* Разработку на чистом JavaScript\r\n* ReactJS и AngularJS\r\n* ООП на JavaScript \r\n* Основы HTML 5 и CSS3\r\n* Javascript ES6 и ES7\r\n* Основы кроссбраузерные\r\n\r\nВы погрузитесь в динамичный мир современного JS-разработки, который станет интересным и увлекательным. Курс JavaScript будет полезен как новичкам, так и опытным разработчикам, желающим изучить новые технологии и получить новые знания.",
+ "description_as_markdown": true,
+ "cost": "2500.15",
+ "cover": "img/course003.jpg",
+ "created": "2021-07-11T08:31:20.301Z",
+ "updated": "2021-07-11T09:06:46.902Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 4,
+ "fields": {
+ "name": "Web Java",
+ "description": "На курсе Java Вы изучите язык и фреймворк Java с самых азов до продвинутого уровня.\r\nА главное — Вы получите теоретические и практические навыки для создания, отладки, тестирования своих программ. В итоге Вы сможете выполнить проекты любой степени сложности, используя различные среды разработки и инструменты: от Eclipse IDE до IntelliJ IDEA, не говоря уже о современных облачных решениях от Google и GitHub.\r\nКурс Java ведёт сильный преподаватель Дмитрий. Дмитрий - это живой пример профессионального, увлеченного и целеустремленного преподавателя. С первых занятий, уже на первом уроке можно понять, что преподаватель очень заинтересован в том чтобы его ученики не просто получили теоретические знания, а приобрели практические навыки.",
+ "description_as_markdown": false,
+ "cost": "10000.00",
+ "cover": "img/course004.jpg",
+ "created": "2021-07-11T08:34:10.771Z",
+ "updated": "2021-07-11T09:06:52.015Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 5,
+ "fields": {
+ "name": "Web PHP",
+ "description": "На курсе PHP Вы изучите основы PHP и получите навыки работы с PHP, такими как работа с базами данных, работа с графикой, создание сайтов, работа в PHPEdit, обработка исключений, управление памятью, работа со строками, работа с файлами. Курс PHP позволяет получить навыки программирования на языке PHP. После прохождения курса Вы сможете создавать собственный веб-сайт с системой управления, а так же применять свои знания для создания полноценных информационных систем.\r\nКурс PHP ведёт разработчик из «Сбербанка», который умеет делать сложные вещи из простых. Он рассказывает, как сделать сайт, не имея ничего, кроме базовых знаний — и делится секретами, за которые в других сферах с радостью уволили бы. У меня было достаточно времени, чтобы посмотреть на PHP с нуля и понять, что он крутой. Как-то раз меня спросили, почему я не пишу код сам, а только консультирую других. Я ответил: «Потому что мне нравится помогать людям». Но для этого нужно уметь всё.\r\nPHP — это сложная штука.",
+ "description_as_markdown": false,
+ "cost": "9500.78",
+ "cover": "img/course005.jpg",
+ "created": "2021-07-11T08:36:21.087Z",
+ "updated": "2021-07-11T09:06:57.580Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 6,
+ "fields": {
+ "name": "Python AI",
+ "description": "На курсе Python AI Вы изучите создание простых нейросетей для задач машинного зрения.\r\nА также научитесь создавать и развивать более сложные нейросети.\r\nКурс рассчитан на:\r\n\r\n* Для тех, кому интересен мир машинного обучения, создания ИИ и робототехники.\r\n* Для специалистов в области физики, математики, программирования на Python.\r\n* Для всех, кто хочет прокачать свои навыки в Python, а также понять алгоритм работы нейронных сетей и научиться применять их на практике.\r\n\r\nКурс Python AI ведёт преподаватель школы \"Фабрика Открытий\" Денис Мигачёв.\r\nОн изучает математику и IT, много программирует и делает игры. Денис расскажет вам про инструменты, которые пригодятся для создания ИИ, и даст практические задания — напишите свою игру на Python.",
+ "description_as_markdown": true,
+ "cost": "6660.66",
+ "cover": "img/course006.jpg",
+ "created": "2021-07-11T08:39:15.369Z",
+ "updated": "2021-07-11T09:07:04.253Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 7,
+ "fields": {
+ "name": "DevOps",
+ "description": "На курсе DevOps Вы изучите основы автоматизации с Python, Git, Docker и узнаете про GitHub, Jenkins и Kubernetes. Узнаете, как все это работает, и поймете, зачем вам все это нужно.\r\nКурс состоит из 12 онлайн-семинаров, длительностью 90 минут. Он подходит опытным программистам, которые хотят усовершенствовать свои навыки в области управления проектами.\r\nНа курсе Вы изучите:\r\n\r\n* Основные возможности и функции Git\r\n* Как создавать, добавлять и удалять ветки, коммиты и тэги\r\n* Как использовать ветку и как ее можно назвать",
+ "description_as_markdown": true,
+ "cost": "7770.77",
+ "cover": "img/course007.jpg",
+ "created": "2021-07-11T08:40:33.347Z",
+ "updated": "2021-07-11T09:07:11.184Z"
+ }
+ },
+ {
+ "model": "mainapp.courses",
+ "pk": 8,
+ "fields": {
+ "name": "3D-моделирование",
+ "description": "Курс 3D-моделирования ведёт практикующий архитектор с высшим архитектурным образованием, со стажем преподавания более 7 лет.\r\nНа курсе 3D-моделирования Вы изучите программы:\r\n\r\n* 3D-Max\r\n* Blender\r\n* Rhino\r\n* Maya\r\n* AutoCAD (V-Ray)\r\n\r\nНа курсе Вы научитесь:\r\n\r\n1. Создавать модели для разработки приложений.\r\n2. Создавать дизайн мебели.\r\n3. Создавать реалистичные текстуры и освещение.\r\n4. Создавать анимации.\r\n5. Настраивать рабочий процесс в программах.\r\n6. Уметь работать с 3D Studio Max, Blender, Rhinoceros.\r\n7. Уметь использовать инструменты программы AutoCAD.\r\n8. Уметь создавать свои собственные проекты или адаптировать модели 3D программ под свои нужды.",
+ "description_as_markdown": true,
+ "cost": "5900.00",
+ "cover": "img/no_image.svg",
+ "created": "2021-07-11T08:58:28.830Z",
+ "updated": "2021-07-11T09:07:16.651Z"
+ }
+ }
+]
\ No newline at end of file
diff --git a/mainapp/fixtures/003_lessons.json b/mainapp/fixtures/003_lessons.json
new file mode 100644
index 0000000..212a407
--- /dev/null
+++ b/mainapp/fixtures/003_lessons.json
@@ -0,0 +1,834 @@
+[
+ {
+ "model": "mainapp.lesson",
+ "pk": 11,
+ "fields": {
+ "course": 1,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 12,
+ "fields": {
+ "course": 1,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 13,
+ "fields": {
+ "course": 1,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 14,
+ "fields": {
+ "course": 1,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 15,
+ "fields": {
+ "course": 1,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 16,
+ "fields": {
+ "course": 1,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 17,
+ "fields": {
+ "course": 1,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 18,
+ "fields": {
+ "course": 1,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 21,
+ "fields": {
+ "course": 2,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 22,
+ "fields": {
+ "course": 2,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 23,
+ "fields": {
+ "course": 2,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 24,
+ "fields": {
+ "course": 2,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 25,
+ "fields": {
+ "course": 2,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 26,
+ "fields": {
+ "course": 2,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 27,
+ "fields": {
+ "course": 2,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 28,
+ "fields": {
+ "course": 2,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 31,
+ "fields": {
+ "course": 3,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 32,
+ "fields": {
+ "course": 3,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 33,
+ "fields": {
+ "course": 3,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 34,
+ "fields": {
+ "course": 3,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 35,
+ "fields": {
+ "course": 3,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 36,
+ "fields": {
+ "course": 3,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 37,
+ "fields": {
+ "course": 3,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 38,
+ "fields": {
+ "course": 3,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 41,
+ "fields": {
+ "course": 4,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 42,
+ "fields": {
+ "course": 4,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 43,
+ "fields": {
+ "course": 4,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 44,
+ "fields": {
+ "course": 4,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 45,
+ "fields": {
+ "course": 4,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 46,
+ "fields": {
+ "course": 4,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 47,
+ "fields": {
+ "course": 4,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 48,
+ "fields": {
+ "course": 4,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 51,
+ "fields": {
+ "course": 5,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 52,
+ "fields": {
+ "course": 5,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 53,
+ "fields": {
+ "course": 5,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 54,
+ "fields": {
+ "course": 5,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 55,
+ "fields": {
+ "course": 5,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 56,
+ "fields": {
+ "course": 5,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 57,
+ "fields": {
+ "course": 5,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 58,
+ "fields": {
+ "course": 5,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 61,
+ "fields": {
+ "course": 6,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 62,
+ "fields": {
+ "course": 6,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 63,
+ "fields": {
+ "course": 6,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 64,
+ "fields": {
+ "course": 6,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 65,
+ "fields": {
+ "course": 6,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 66,
+ "fields": {
+ "course": 6,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 67,
+ "fields": {
+ "course": 6,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 68,
+ "fields": {
+ "course": 6,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 71,
+ "fields": {
+ "course": 7,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 72,
+ "fields": {
+ "course": 7,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 73,
+ "fields": {
+ "course": 7,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 74,
+ "fields": {
+ "course": 7,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 75,
+ "fields": {
+ "course": 7,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 76,
+ "fields": {
+ "course": 7,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 77,
+ "fields": {
+ "course": 7,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 78,
+ "fields": {
+ "course": 7,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 81,
+ "fields": {
+ "course": 8,
+ "num": 1,
+ "title": "Lesson 1",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 82,
+ "fields": {
+ "course": 8,
+ "num": 2,
+ "title": "Lesson 2",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 83,
+ "fields": {
+ "course": 8,
+ "num": 3,
+ "title": "Lesson 3",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 84,
+ "fields": {
+ "course": 8,
+ "num": 4,
+ "title": "Lesson 4",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 85,
+ "fields": {
+ "course": 8,
+ "num": 5,
+ "title": "Lesson 5",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 86,
+ "fields": {
+ "course": 8,
+ "num": 6,
+ "title": "Lesson 6",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 87,
+ "fields": {
+ "course": 8,
+ "num": 7,
+ "title": "Lesson 7",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ },
+ {
+ "model": "mainapp.lesson",
+ "pk": 88,
+ "fields": {
+ "course": 8,
+ "num": 8,
+ "title": "Lesson 8",
+ "description": "Описание урока",
+ "description_as_markdown": false,
+ "created": "2021-07-11T12:53:26.832Z",
+ "updated": "2021-07-11T12:53:26.833Z"
+ }
+ }
+]
\ No newline at end of file
diff --git a/mainapp/fixtures/004_teachers.json b/mainapp/fixtures/004_teachers.json
new file mode 100644
index 0000000..5d5ec3c
--- /dev/null
+++ b/mainapp/fixtures/004_teachers.json
@@ -0,0 +1,93 @@
+[
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 1,
+ "fields": {
+ "name_first": "Альфред",
+ "name_second": "Нуцубидзе",
+ "day_birth": "1990-07-10",
+ "course": [
+ 1,
+ 3
+ ]
+ }
+ },
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 2,
+ "fields": {
+ "name_first": "Роман",
+ "name_second": "Доржинов",
+ "day_birth": "1988-02-04",
+ "course": [
+ 2,
+ 4
+ ]
+ }
+ },
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 3,
+ "fields": {
+ "name_first": "Ярослав",
+ "name_second": "Конягин",
+ "day_birth": "1981-12-08",
+ "course": [
+ 3,
+ 5
+ ]
+ }
+ },
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 4,
+ "fields": {
+ "name_first": "Автандил",
+ "name_second": "Наварский",
+ "day_birth": "1983-05-16",
+ "course": [
+ 4,
+ 6
+ ]
+ }
+ },
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 5,
+ "fields": {
+ "name_first": "Роза",
+ "name_second": "Уланова",
+ "day_birth": "1986-05-09",
+ "course": [
+ 5,
+ 7
+ ]
+ }
+ },
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 6,
+ "fields": {
+ "name_first": "Бронислава",
+ "name_second": "Алиева",
+ "day_birth": "1971-01-07",
+ "course": [
+ 6,
+ 8
+ ]
+ }
+ },
+ {
+ "model": "mainapp.courseteachers",
+ "pk": 7,
+ "fields": {
+ "name_first": "Диана",
+ "name_second": "Попова",
+ "day_birth": "1990-08-25",
+ "course": [
+ 1,
+ 8
+ ]
+ }
+ }
+]
\ No newline at end of file
diff --git a/mainapp/forms.py b/mainapp/forms.py
new file mode 100644
index 0000000..410fdee
--- /dev/null
+++ b/mainapp/forms.py
@@ -0,0 +1,30 @@
+from django import forms
+from django.utils.translation import gettext_lazy as _
+from mainapp import models as mainapp_models
+
+class CourseFeedbackForm(forms.ModelForm):
+
+ def __init__(self, *args, course=None, user=None, **kwargs):
+ ret = super().__init__(*args, **kwargs)
+ if course and user:
+ self.fields["course"].initial = course.pk
+ self.fields["user"].initial = user.pk
+ return ret
+
+ class Meta:
+ model = mainapp_models.CourseFeedback
+ fields = ("course", "user", "feedback", "rating")
+ widgets = {
+ "course": forms.HiddenInput(),
+ "user": forms.HiddenInput(),
+ "rating": forms.RadioSelect(),
+ }
+
+class MailFeedbackForm(forms.Form):
+ user_id = forms.IntegerField(widget=forms.HiddenInput)
+ message = forms.CharField(widget=forms.Textarea, help_text=_("Enter your message"), label=_("Message"),)
+
+ def __init__(self, *args, user=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ if user:
+ self.fields["user_id"].initial = user.pk
\ No newline at end of file
diff --git a/mainapp/migrations/0001_initial.py b/mainapp/migrations/0001_initial.py
new file mode 100644
index 0000000..c094f9e
--- /dev/null
+++ b/mainapp/migrations/0001_initial.py
@@ -0,0 +1,93 @@
+# Generated by Django 4.0.4 on 2022-06-02 10:53
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Courses',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=256, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
+ ('description_as_markdown', models.BooleanField(default=False, verbose_name='As markdown')),
+ ('cost', models.DecimalField(decimal_places=2, default=0, max_digits=8, verbose_name='Cost')),
+ ('cover', models.CharField(default='no_image.svg', max_length=25, verbose_name='Cover')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Edited')),
+ ('deleted', models.BooleanField(default=False)),
+ ],
+ options={
+ 'verbose_name': 'Курс',
+ 'verbose_name_plural': 'Курсы',
+ },
+ ),
+ migrations.CreateModel(
+ name='News',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=256, verbose_name='Заголовок')),
+ ('preambule', models.CharField(max_length=1024, verbose_name='Вступление')),
+ ('body', models.TextField(blank=True, null=True, verbose_name='Основной текст')),
+ ('body_as_markdown', models.BooleanField(default=False, verbose_name='As markdown')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Edited')),
+ ('deleted', models.BooleanField(default=False)),
+ ],
+ options={
+ 'verbose_name': 'Новости',
+ 'verbose_name_plural': 'Новости',
+ 'ordering': ('-created',),
+ },
+ ),
+ migrations.CreateModel(
+ name='Lesson',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('num', models.PositiveIntegerField(verbose_name='Lesson number')),
+ ('title', models.CharField(max_length=256, verbose_name='Name')),
+ ('description', models.TextField(blank=True, null=True, verbose_name='Description')),
+ ('description_as_markdown', models.BooleanField(default=False, verbose_name='As markdown')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
+ ('updated', models.DateTimeField(auto_now=True, verbose_name='Edited')),
+ ('deleted', models.BooleanField(default=False)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mainapp.courses')),
+ ],
+ options={
+ 'ordering': ('course', 'num'),
+ },
+ ),
+ migrations.CreateModel(
+ name='CourseTeachers',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name_first', models.CharField(max_length=128, verbose_name='Name')),
+ ('name_second', models.CharField(max_length=128, verbose_name='Surname')),
+ ('day_birth', models.DateField(verbose_name='Birth date')),
+ ('deleted', models.BooleanField(default=False)),
+ ('course', models.ManyToManyField(to='mainapp.courses')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='CourseFeedback',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('feedback', models.TextField(default='No feedback', verbose_name='Feedback')),
+ ('rating', models.SmallIntegerField(choices=[(5, '⭐⭐⭐⭐⭐'), (4, '⭐⭐⭐⭐'), (3, '⭐⭐⭐'), (2, '⭐⭐'), (1, '⭐')], default=5, verbose_name='Rating')),
+ ('created', models.DateTimeField(auto_now_add=True, verbose_name='Created')),
+ ('deleted', models.BooleanField(default=False)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='mainapp.courses', verbose_name='Course')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
+ ],
+ ),
+ ]
diff --git a/mainapp/models.py b/mainapp/models.py
index 71a8362..458d2fb 100644
--- a/mainapp/models.py
+++ b/mainapp/models.py
@@ -1,3 +1,103 @@
+from tabnanny import verbose
from django.db import models
+from django.utils.translation import gettext_lazy as _
+from django.contrib.auth import get_user_model
-# Create your models here.
+class News(models.Model):
+ title = models.CharField(max_length=256, verbose_name='Заголовок')
+ preambule = models.CharField(max_length=1024, verbose_name="Вступление")
+ body = models.TextField(blank=True, null=True, verbose_name="Основной текст")
+ body_as_markdown = models.BooleanField(default=False, verbose_name="As markdown")
+ created = models.DateTimeField(auto_now_add=True, verbose_name="Created", editable=False)
+ updated = models.DateTimeField(auto_now=True, verbose_name="Edited", editable=False)
+ deleted = models.BooleanField(default=False)
+
+ class Meta:
+ verbose_name = _("Новости")
+ verbose_name_plural = _("Новости")
+ ordering = ("-created",)
+
+ def __str__(self) -> str:
+ return f"{self.pk} {self.title}" #pk- primary key
+
+ def delete(self, *args):
+ self.deleted = True
+ self.save()
+
+
+
+
+class CoursesManager(models.Manager):
+ def get_queryset(self):
+ return super().get_queryset().filter(deleted=False)
+
+
+class Courses(models.Model):
+ objects = CoursesManager()
+ name = models.CharField(max_length=256, verbose_name="Name")
+ description = models.TextField(verbose_name="Description", blank=True, null=True)
+ description_as_markdown = models.BooleanField(verbose_name="As markdown", default=False)
+ cost = models.DecimalField(max_digits=8, decimal_places=2, verbose_name="Cost", default=0)
+ cover = models.CharField(max_length=25, default="no_image.svg", verbose_name="Cover")
+ created = models.DateTimeField(auto_now_add=True, verbose_name="Created")
+ updated = models.DateTimeField(auto_now=True, verbose_name="Edited")
+ deleted = models.BooleanField(default=False)
+
+ def __str__(self) -> str:
+ return f"{self.pk} {self.name}"
+
+ def delete(self, *args):
+ self.deleted = True
+ self.save()
+
+ class Meta:
+ verbose_name = ('Курс')
+ verbose_name_plural = ('Курсы')
+
+class Lesson(models.Model):
+ course = models.ForeignKey(Courses, on_delete=models.CASCADE)
+ num = models.PositiveIntegerField(verbose_name="Lesson number")
+ title = models.CharField(max_length=256, verbose_name="Name")
+ description = models.TextField(verbose_name="Description", blank=True, null=True)
+ description_as_markdown = models.BooleanField(verbose_name="As markdown", default=False)
+ created = models.DateTimeField(auto_now_add=True, verbose_name="Created", editable=False)
+ updated = models.DateTimeField(auto_now=True, verbose_name="Edited", editable=False)
+ deleted = models.BooleanField(default=False)
+
+ def __str__(self):
+ return f"{self.course.name} | {self.num} | {self.title}"
+
+ def delete(self, *args):
+ self.deleted = True
+ self.save()
+
+ class Meta:
+ ordering = ("course", "num")
+
+class CourseTeachers(models.Model):
+ course = models.ManyToManyField(Courses)
+ name_first = models.CharField(max_length=128, verbose_name="Name")
+ name_second = models.CharField(max_length=128, verbose_name="Surname")
+ day_birth = models.DateField(verbose_name="Birth date")
+ deleted = models.BooleanField(default=False)
+
+ def __str__(self):
+ return "{0:0>3} {1} {2}".format(self.pk, self.name_second, self.name_first)
+
+ def delete(self, *args):
+ self.deleted = True
+ self.save()
+
+class CourseFeedback(models.Model):
+ user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, verbose_name=_("User"))
+ course = models.ForeignKey(Courses, on_delete=models.CASCADE, verbose_name=_("Course"))
+ feedback = models.TextField(default=_("No feedback"), verbose_name=_("Feedback"))
+
+ RATING = ((5, "⭐⭐⭐⭐⭐"), (4, "⭐⭐⭐⭐"), (3, "⭐⭐⭐"), (2, "⭐⭐"),(1, "⭐"))
+ rating = models.SmallIntegerField(choices=RATING, default=5, verbose_name=_("Rating"))
+
+ created = models.DateTimeField(auto_now_add=True, verbose_name="Created")
+ deleted = models.BooleanField(default=False)
+
+ def __str__(self):
+ return f"{self.course} ({self.user})"
diff --git a/mainapp/tasks.py b/mainapp/tasks.py
new file mode 100644
index 0000000..ced565e
--- /dev/null
+++ b/mainapp/tasks.py
@@ -0,0 +1,38 @@
+from email import message_from_string
+from django.core.mail import send_mail
+from celery import shared_task
+
+from django.contrib.auth import get_user_model
+
+# @shared_task
+# def send_feedback_to_email(message_from, body):
+# model_user = get_user_model()
+# if not model_user:
+# model_user = "Аноним"
+
+# send_mail(f'Feedback from {message_from}', #subject
+# message=body,
+# recipient_list=['gas53@bk.ru'],
+# from_email='gas53@bk.ru',
+# fail_silently=False)
+
+
+
+from django.core import mail
+
+@shared_task
+def send_feedback_mail(mes_from, body):
+ connection = mail.get_connection()
+ model_user = get_user_model()
+ with connection as conn:
+ email1 = mail.EmailMessage(
+ f'Hello from {mes_from}',
+ f'{body}',
+ 'gas53@bk.ru',
+ ['gas53@bk.ru'],
+ connection=conn,
+ )
+ email1.send()
+
+# send_feedback_mail()
+
diff --git a/mainapp/tasks_1.py b/mainapp/tasks_1.py
new file mode 100644
index 0000000..9a25ec5
--- /dev/null
+++ b/mainapp/tasks_1.py
@@ -0,0 +1,16 @@
+from django.core import mail
+
+
+def send_feedback_mail(*args):
+ connection = mail.get_connection()
+ with connection as conn:
+ email1 = mail.EmailMessage(
+ 'Hello',
+ 'Body goes here',
+ 'gas53@bk.ru',
+ ['gas53@bk.ru'],
+ connection=conn,
+ )
+ email1.send()
+
+send_feedback_mail()
\ No newline at end of file
diff --git a/mainapp/templates/admin/base_site.html b/mainapp/templates/admin/base_site.html
new file mode 100644
index 0000000..738f981
--- /dev/null
+++ b/mainapp/templates/admin/base_site.html
@@ -0,0 +1,12 @@
+{% extends "admin/base.html" %}
+{% load static %}
+{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{
+site_title|default:_('Django site admin') }}{% endblock %}
+{% block branding %}
+
+{% endblock %}
+{% block nav-global %}{% endblock %}
\ No newline at end of file
diff --git a/mainapp/templates/includes/base.html b/mainapp/templates/includes/base.html
index 983bef1..8ef3310 100644
--- a/mainapp/templates/includes/base.html
+++ b/mainapp/templates/includes/base.html
@@ -13,10 +13,11 @@
+
{% include 'includes/head_menu.html' %}
{% block content %}
-
+ Нет контента страници
{% endblock content %}
{% include 'includes/foot_menu.html' %}
diff --git a/mainapp/templates/includes/feedback_card.html b/mainapp/templates/includes/feedback_card.html
new file mode 100644
index 0000000..e7a1c00
--- /dev/null
+++ b/mainapp/templates/includes/feedback_card.html
@@ -0,0 +1,9 @@
+
+
+
{{ item.get_rating_display }}
+
{{ item.user }}
+
+ {{ item.feedback }}
+
+
+
\ No newline at end of file
diff --git a/mainapp/templates/includes/head_menu.html b/mainapp/templates/includes/head_menu.html
index a921130..39c5a53 100644
--- a/mainapp/templates/includes/head_menu.html
+++ b/mainapp/templates/includes/head_menu.html
@@ -1,6 +1,6 @@
diff --git a/mainapp/templates/mainapp/contacts.html b/mainapp/templates/mainapp/contacts.html
index abd1b4c..3f3c0b4 100644
--- a/mainapp/templates/mainapp/contacts.html
+++ b/mainapp/templates/mainapp/contacts.html
@@ -1,7 +1,7 @@
{% extends 'includes/base.html' %}
{% load static %}
{% load email_to_link %}
-
+{% load email_to_link crispy_forms_tags %}
{% block content %}
@@ -118,5 +118,14 @@ Москва
-
+{% if form %}
+
+
Отправить сообщение в техподдержку
+
+
+{% endif %}
{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/courses_detail.html b/mainapp/templates/mainapp/courses_detail.html
new file mode 100644
index 0000000..acfc555
--- /dev/null
+++ b/mainapp/templates/mainapp/courses_detail.html
@@ -0,0 +1,137 @@
+{% extends 'base.html' %}
+{% load static markdownify crispy_forms_tags cache %}
+{% block title %}
+{{ course_object.name }}
+{% endblock title %}
+{% block content %}
+
+ {{ course_object.name }}
+
+
+
+
+
+ {% if course_object.description_as_markdown %}
+ {{ course_object.description|markdownify }}
+ {% else %}
+ {{ course_object.description }}
+ {% endif %}
+
+
+
+ Преподаватели
+ {% for item in teachers %}
+ {{ item.name_second }} {{ item.name_first }}{% if forloop.last %}{%
+ else %},{% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+ цена
+
+ {{ course_object.cost}}
+
+ Купить
+
+
+
+
+
+
+ цена
+
+ {{ course_object.cost}}
+
+ Купить
+
+
+
+
+ {% cache 300 lessons %}
+ {% for item in lessons %}
+
+
+
+
+
+
+
+
+
+ {% if item.description_as_markdown %}
+ {{ item.description|markdownify }}
+ {% else %}
+ {{ item.description }}
+ {% endif %}
+
+
+
+
+
+ {% endfor %}
+ {% endcache %}
+
+Отзывы
+
+ {% if feedback_list %}
+ {% for item in feedback_list %}
+ {% include 'mainapp/includes/feedback_card.html' %}
+ {% endfor %}
+ {% else %}
+
+ No feedback yet. Be first!
+
+ {% endif %}
+
+{% if feedback_form %}
+
+{% endif %}
+{% endblock content %}
+{% block js %}
+
+{% endblock js %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/log_view.html b/mainapp/templates/mainapp/log_view.html
new file mode 100644
index 0000000..f3ad0cc
--- /dev/null
+++ b/mainapp/templates/mainapp/log_view.html
@@ -0,0 +1,11 @@
+{% block content %}
+
+
+
Первые 1000 строк файла
+{{ log }}
+
+{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/news.html b/mainapp/templates/mainapp/news.html
index 6be1d0d..f723a66 100644
--- a/mainapp/templates/mainapp/news.html
+++ b/mainapp/templates/mainapp/news.html
@@ -1,70 +1,88 @@
{% extends 'includes/base.html' %}
+{% block title %}
+Новости
+{% endblock title %}
+
{% load static %}
{% block content %}
-
-
-
-
-
-
-
- {% for i in range %}
-
-
-
{{ news_title }}
-
{{ datetime_obj|date:"Y-m-d h-i-s" }}
-
{{i}}
-
Подробнее
+{% if page_num %}
+
+
+
Текущая страница: {{ page_num }}
+
+
+{% endif %}
+
+
+ {% for item in object_list %}
+
+
+
{{ item.title }}
+
+ {{ item.created|date:"Y-m-d h-i-s" }}
+
+
{{ item.preambule }}
+
+
+ {% if perms.mainapp.change_news %}
+
+ {% endif %}
+ {% if perms.mainapp.delete_news %}
+
+ {% endif %}
- {% endfor %}
-
-
-
-
-
-
-
-
+
+
+
+
+
{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/news_confirm_delete.html b/mainapp/templates/mainapp/news_confirm_delete.html
new file mode 100644
index 0000000..c36ca47
--- /dev/null
+++ b/mainapp/templates/mainapp/news_confirm_delete.html
@@ -0,0 +1,26 @@
+{% extends 'base.html' %}
+mainapp/templates/mainapp/news_confirm_delete.html
+{% block content %}
+
+
+
Пометить новость удалённой?
+
+ Поставить отметку удаления для новости:
+ "{{ object.title }}" от {{ object.created }}
+
+
+
+
+
+ {% csrf_token %}
+ Подтвердить
+
+
+
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/news_detail.html b/mainapp/templates/mainapp/news_detail.html
new file mode 100644
index 0000000..8148106
--- /dev/null
+++ b/mainapp/templates/mainapp/news_detail.html
@@ -0,0 +1,19 @@
+{% extends 'base.html' %}
+{% block title %}
+{{ object.title|default:"Новости" }}
+{% endblock title %}
+{% block content %}
+
+
+
+
+
{{ object.title }}
+
+ {{ object.body }}
+
+
Назад
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/news_form.html b/mainapp/templates/mainapp/news_form.html
new file mode 100644
index 0000000..890ec25
--- /dev/null
+++ b/mainapp/templates/mainapp/news_form.html
@@ -0,0 +1,12 @@
+{% load crispy_forms_tags %}
+{% block content %}
+
+
+
+ {% csrf_token %}
+ {{ form|crispy }}
+ Опубликовать
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/templates/mainapp/news_list.html b/mainapp/templates/mainapp/news_list.html
new file mode 100644
index 0000000..4650757
--- /dev/null
+++ b/mainapp/templates/mainapp/news_list.html
@@ -0,0 +1,85 @@
+{% extends 'base.html' %}
+{% block title %}
+Новости
+{% endblock title %}
+{% block content %}
+{% if page_num %}
+
+
+
Текущая страница: {{ page_num }}
+
+
+{% endif %}
+
+
+ {% for item in object_list %}
+
+
+
{{ item.title }}
+
+ {{ item.created|date:"Y-m-d h-i-s" }}
+
+
{{ item.preambule }}
+
+
+ {% if perms.mainapp.change_news %}
+
+ {% endif %}
+ {% if perms.mainapp.delete_news %}
+
+ {% endif %}
+
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+{% endblock content %}
\ No newline at end of file
diff --git a/mainapp/urls.py b/mainapp/urls.py
index b20583d..f5495e5 100644
--- a/mainapp/urls.py
+++ b/mainapp/urls.py
@@ -1,19 +1,24 @@
-from argparse import Namespace
from django.urls import path
from mainapp import views
-from django.views.generic import RedirectView
from mainapp.apps import MainappConfig
+from django.views.decorators.cache import cache_page
app_name = MainappConfig.name
-
urlpatterns = [
- path("", views.Index.as_view(), name="main_page"),
- path("news/", views.News.as_view(), name="news"),
- path("news/
/", views.NewsWithPaginatorView.as_view(), name="news_paginator"),
+ path("", views.MainPageView.as_view(), name="main_page"),
+ path("news/", views.NewsListView.as_view(), name="news"),
+ path("news/create/", views.NewsCreateView.as_view(), name="news_create"),
+ path("news//detail",views.NewsDetailView.as_view(),name="news_detail",),
+ path("news//update",views.NewsUpdateView.as_view(),name="news_update",),
+ path("news//delete",views.NewsDeleteView.as_view(),name="news_delete",),
+ path("courses/",cache_page(60 * 5)(views.CoursesListView.as_view()),name="courses",),
+ path("courses//",views.CoursesDetailView.as_view(),name="courses_detail",),
+ path("course_feedback/",views.CourseFeedbackFormProcessView.as_view(),name="course_feedback",),
+ path("contacts/", views.ContactsPageView.as_view(), name="contacts"),
+ path("doc_site/", views.DocSitePageView.as_view(), name="doc_site"),
+ path("log_view/", views.LogView.as_view(), name="log_view"),
+ path("log_download/", views.LogDownloadView.as_view(), name="log_download"),
+ ]
- path("courses/", views.Courses.as_view(), name="courses"),
- path("contacts/", views.Contacts.as_view(), name="contacts"),
- path("doc_site/", views.Doc.as_view(), name="doc_site"),
- path("login/", views.Login.as_view(), name="login"),
-]
\ No newline at end of file
+
\ No newline at end of file
diff --git a/mainapp/views.py b/mainapp/views.py
index 6348efc..1b79df4 100644
--- a/mainapp/views.py
+++ b/mainapp/views.py
@@ -1,58 +1,164 @@
-from multiprocessing import get_context
-from django.views.generic import View, TemplateView
-from datetime import datetime
+from mainapp import models as mainapp_models
+from django.shortcuts import get_object_or_404
+from django.views.generic import TemplateView, View, ListView, CreateView, DetailView, DeleteView
+from django.contrib.auth.mixins import PermissionRequiredMixin, LoginRequiredMixin
+from django.template.loader import render_to_string
+from django.urls import reverse_lazy
+from mainapp import forms as mainapp_forms
+from django.http import JsonResponse
+from django.views.generic import CreateView, UpdateView
+from django.contrib.auth.mixins import UserPassesTestMixin
+from django.http import FileResponse, JsonResponse
+from config import settings
+from mainapp import tasks as mainapp_tasks
+from django.http.response import HttpResponseRedirect
+import logging
+from django.conf import settings
+from django.contrib import messages
+from django.core.cache import cache
+from django.utils.translation import gettext_lazy as _
+from mainapp import forms as mainapp_forms
+from mainapp import models as mainapp_models
+
+
+
+logger = logging.getLogger(__name__)
+
+class MainPageView(TemplateView):
+ template_name = "mainapp/index.html"
+
+
+class NewsPageView(TemplateView):
+ template_name = "mainapp/news.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["news_qs"] = mainapp_models.News.objects.all()[:5]
+ return context
-class Index(TemplateView):
- template_name = 'mainapp/index.html'
+class NewsPageDetailView(TemplateView):
+ template_name = "mainapp/news_detail.html"
+
+ def get_context_data(self, pk=None, **kwargs):
+ context = super().get_context_data(pk=pk, **kwargs)
+ context["news_object"] = get_object_or_404(mainapp_models.News, pk=pk)
+ return context
-class News(TemplateView):
+
+class NewsListView(ListView):
template_name = 'mainapp/news.html'
+ model = mainapp_models.News
+ paginate_by = 5
- def get_news(self):
- news = [
- 'Их было около дюжины человек, солдат, матросов, недавних вчерашних крестьян, голодных, оборванных, у многих измятые гимнастерки, у некоторых простреленная и обгоревшая одежда. У каждого — револьвер в руке, у кого обрез, у других винтовки',
- 'Солдаты всем сразу дали винтовки и стали учить стрелять.',
- 'К берегам реки приткнула баржу, и около баржи плавало несколько солдат с винтовками.',
- 'RuGPT3: Быстрый как ветер», — кричит с берега Михаил, а ему кричат из толпы ребята, рабочие:',
- 'Небашев пошел к порту и постоял около портовых огней. На берегу видны были факелы, освещающие вход в порт и дебаркадеры. Н'
- ]
- return news
+ def get_queryset(self):
+ return super().get_queryset().filter(deleted=False)
- def get_context_data(self, **kwargs):
- # Get all previous data
- context = super().get_context_data(**kwargs)
- # Create your own data
- context["news_title"] = "Громкий новостной заголовок"
- context[ "news_preview"] = "Предварительное описание, которое заинтересует каждого"
- context["datetime_obj"] = datetime.now()
- context["range"] = self.get_news()
- return context
+class NewsCreateView(PermissionRequiredMixin, CreateView):
+ model = mainapp_models.News
+ fields = "__all__" # использование всех полей
+ success_url = reverse_lazy("mainapp:news")
+ permission_required = ("mainapp.add_news",) # PermissionRequiredMixin проверять, предоставляются ли пользователю права для создания новостей
+class NewsDetailView(DetailView):
+ model = mainapp_models.News
+class NewsUpdateView(PermissionRequiredMixin, UpdateView):
+ model = mainapp_models.News
+ fields = "__all__"
+ success_url = reverse_lazy("mainapp:news")
+ permission_required = ("mainapp.change_news",)
-class NewsWithPaginatorView(News):
- def get_context_data(self, page, **kwargs):
- context = super().get_context_data(page=page, **kwargs)
- context["page_num"] = page
- return context
+class NewsDeleteView(PermissionRequiredMixin, DeleteView):
+ model = mainapp_models.News
+ success_url = reverse_lazy("mainapp:news")
+ permission_required = ("mainapp.delete_news",)
+class CoursesListView(TemplateView):
+ template_name = "mainapp/courses_list.html"
-class Contacts(TemplateView):
- template_name = 'mainapp/contacts.html'
+ def get_context_data(self, **kwargs):
+ context = super(CoursesListView, self).get_context_data(**kwargs)
+ context["objects"] = mainapp_models.Courses.objects.all()[:7]
+ return context
-class Courses(TemplateView):
- template_name = 'mainapp/courses_list.html'
+class CoursesDetailView(TemplateView):
+ template_name = "mainapp/courses_detail.html"
+
+ def get_context_data(self, pk=None, **kwargs):
+ context = super(CoursesDetailView, self).get_context_data(**kwargs)
+ context["course_object"] = get_object_or_404(mainapp_models.Courses, pk=pk)
+ context["lessons"] = mainapp_models.Lesson.objects.filter(course=context["course_object"])
+ context["teachers"] = mainapp_models.CourseTeachers.objects.filter(
+ course=context["course_object"])
+ if not self.request.user.is_anonymous:
+ if not mainapp_models.CourseFeedback.objects.filter(
+ course=context["course_object"], user=self.request.user).count():
+ context["feedback_form"] = mainapp_forms.CourseFeedbackForm(course=context["course_object"], user=self.request.user)
+ context["feedback_list"] = mainapp_models.CourseFeedback.objects.filter(
+ course=context["course_object"]).order_by("-created", "-rating")[:5]
+ return context
-class Doc(TemplateView):
- template_name = 'mainapp/doc_site.html'
+class CourseFeedbackFormProcessView(LoginRequiredMixin, CreateView):
+ model = mainapp_models.CourseFeedback
+ form_class = mainapp_forms.CourseFeedbackForm
-class Login(TemplateView):
- template_name = 'mainapp/login.html'
+ def form_valid(self, form):
+ self.object = form.save()
+ rendered_card = render_to_string("mainapp/includes/feedback_card.html", context={"item": self.object})
+ return JsonResponse({"card": rendered_card})
+class ContactsPageView(TemplateView):
+ template_name = "mainapp/contacts.html"
+ def get_context_data(self, **kwargs):
+ context = super(ContactsPageView, self).get_context_data(**kwargs)
+ if self.request.user.is_authenticated:
+ context["form"] = mainapp_forms.MailFeedbackForm(user=self.request.user)
+ return context
+ def post(self, *args, **kwargs):
+ print(self.request.POST)
+ if self.request.user.is_authenticated:
+ cache_lock_flag = cache.get(f"mail_feedback_lock_{self.request.user.pk}")
+ if not cache_lock_flag:
+ cache.set(f"mail_feedback_lock_{self.request.user.pk}", "lock",timeout=300,)
+
+ messages.add_message(self.request, messages.INFO, _("Message sended"))
+
+ mainapp_tasks.send_feedback_mail.delay(
+ {
+ "user_id": self.request.POST.get("user_id"),
+ "message": self.request.POST.get("message"),
+ }
+ )
+ else:
+ messages.add_message(self.request, messages.WARNING,
+ _("You can send only one message per 5 minutes"),
+ )
+ return HttpResponseRedirect(reverse_lazy("mainapp:contacts"))
+
+class DocSitePageView(TemplateView):
+ template_name = "mainapp/doc_site.html"
+
+class LogView(TemplateView):
+ template_name = "mainapp/log_view.html"
+ def get_context_data(self, **kwargs):
+ context = super(LogView, self).get_context_data(**kwargs)
+ log_slice = []
+ with open(settings.LOG_FILE, "r") as log_file:
+ for i, line in enumerate(log_file):
+ if i == 1000:
+ break
+ log_slice.insert(0, line)
+ context["log"] = "".join(log_slice)
+ return context
+class LogDownloadView(UserPassesTestMixin, View):
+ def test_func(self):
+ return self.request.user.is_superuser
+ def get(self, *args, **kwargs):
+ return FileResponse(open(settings.LOG_FILE, "rb"))
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..8f0e660
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,49 @@
+amqp==5.1.1
+asgiref==3.5.2
+async-timeout==4.0.2
+beautifulsoup4==4.11.1
+billiard==3.6.4.0
+bleach==4.1.0
+celery==5.2.7
+certifi==2022.5.18.1
+cffi==1.15.0
+charset-normalizer==2.0.12
+click==8.1.3
+click-didyoumean==0.3.0
+click-plugins==1.1.1
+click-repl==0.2.0
+cryptography==37.0.2
+defusedxml==0.7.1
+Deprecated==1.2.13
+Django==4.0.4
+django-crispy-forms==1.14.0
+django-debug-toolbar==3.4.0
+django-markdownify==0.9.1
+django-redis==5.2.0
+idna==3.3
+importlib-metadata==4.11.4
+kombu==5.2.4
+Markdown==3.3.7
+markdownify==0.11.2
+oauthlib==3.2.0
+packaging==21.3
+prompt-toolkit==3.0.29
+pycparser==2.21
+PyJWT==2.4.0
+pyparsing==3.0.9
+python3-openid==3.2.0
+pytz==2022.1
+redis==4.3.1
+requests==2.27.1
+requests-oauthlib==1.3.1
+six==1.16.0
+social-auth-app-django==5.0.0
+social-auth-core==4.2.0
+soupsieve==2.3.2.post1
+sqlparse==0.4.2
+urllib3==1.26.9
+vine==5.0.0
+wcwidth==0.2.5
+webencodings==0.5.1
+wrapt==1.14.1
+zipp==3.8.0