threads update

feature/fix_generate_pass
Andrey 8 years ago
parent 554c1b8c6c
commit 01b6d3b5fe
  1. 3
      access/admin.py
  2. 14
      access/migrations/0001_initial.py
  3. 35
      access/migrations/0002_auto_20171128_1150.py
  4. 41
      access/models.py
  5. 10
      access/serializers.py
  6. 10
      access/views.py
  7. 58
      courses/migrations/0001_initial.py
  8. 104
      courses/models.py
  9. 3
      courses/views.py
  10. 4
      finance/migrations/0001_initial.py
  11. 22
      finance/migrations/0002_invoice_yandex_pay.py
  12. 6
      journals/migrations/0001_initial.py
  13. 2
      library/migrations/0001_initial.py
  14. 6
      storage/migrations/0001_initial.py

@ -1,8 +1,7 @@
from django.contrib import admin
from access.models import Progress, ExtraPrivilege, User, Account, Invite
from access.models import Progress, User, Account, Invite
admin.site.register(User)
admin.site.register(Account)
admin.site.register(ExtraPrivilege)
admin.site.register(Progress)
admin.site.register(Invite)

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:24
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
import access.models
@ -52,17 +52,6 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Дополнительная информация о пользователе',
},
),
migrations.CreateModel(
name='ExtraPrivilege',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_done', models.BooleanField(default=False, verbose_name='Выполнено?')),
],
options={
'verbose_name': 'Доп право',
'verbose_name_plural': 'Доп права пользователя',
},
),
migrations.CreateModel(
name='Invite',
fields=[
@ -79,7 +68,6 @@ class Migration(migrations.Migration):
name='Progress',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('success', models.BooleanField(default=False, verbose_name='Завершён ли курс')),
],
options={
'verbose_name': 'Прогресс пользователя',

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:24
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
from django.conf import settings
@ -12,46 +12,41 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('courses', '0001_initial'),
('access', '0001_initial'),
('auth', '0008_alter_user_username_max_length'),
('access', '0001_initial'),
('courses', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='progress',
name='active_obj',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.Vertex', verbose_name='Активный объект'),
name='course',
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.Course', verbose_name='Курс'),
),
migrations.AddField(
model_name='progress',
name='course',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Course', verbose_name='Курс'),
name='progress_list',
field=models.ManyToManyField(blank=True, to='courses.Vertex', verbose_name='Лист пройденных объектов'),
),
migrations.AddField(
model_name='progress',
name='template',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.CourseRoute', verbose_name='Шаблон для прохождения если не указан явно смотри функцию get_template()'),
),
migrations.AddField(
model_name='progress',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Студент'),
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Студент'),
),
migrations.AddField(
model_name='invite',
name='owner',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='extraprivilege',
name='subject',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Vertex', verbose_name='Объект'),
),
migrations.AddField(
model_name='extraprivilege',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Правообладатель'),
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='account',
name='owner',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='user',

@ -5,7 +5,7 @@ import string
from django.contrib.contenttypes.models import ContentType
from django_celery_results.models import TaskResult
from courses.models import Vertex, Course
from courses.models import Vertex, Course, CourseMap, CourseRoute
from storage.models import Storage
from django.core.mail import send_mail
@ -22,7 +22,7 @@ from journals.models import Journal, Thread, ACTION_CHOICES
class Invite(models.Model):
owner = models.OneToOneField(to=settings.AUTH_USER_MODEL)
owner = models.OneToOneField(to=settings.AUTH_USER_MODEL, null=True)
hash = models.CharField(max_length=15)
date = models.DateTimeField(null=True, blank=True)
@ -40,7 +40,7 @@ class Account(models.Model):
b_day = models.DateField(blank=True, null=True)
city = models.CharField(max_length=63, null=True, blank=True)
gender = models.SmallIntegerField(choices=GENDER_CHOICES, default=0)
owner = models.OneToOneField(to=settings.AUTH_USER_MODEL)
owner = models.OneToOneField(to=settings.AUTH_USER_MODEL, null=True)
photo = models.ImageField(null=True, blank=True, default='/static/default/access/default.png', upload_to='user/photo/')
phone = models.CharField(max_length=15, null=True, blank=True)
@ -173,32 +173,33 @@ class User(AbstractBaseUser, PermissionsMixin):
class Progress(models.Model):
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Студент')
course = models.ForeignKey(to=Course, verbose_name='Курс')
active_obj = models.ForeignKey(to=Vertex, verbose_name='Активный объект', blank=True, null=True)
success = models.BooleanField(default=False, verbose_name='Завершён ли курс')
user = models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name='Студент', null=True)
course = models.OneToOneField(to=Course, verbose_name='Курс', null=True)
progress_list = models.ManyToManyField(to=Vertex, verbose_name='Лист пройденных объектов', blank=True)
template = models.OneToOneField(to=CourseRoute, blank=True, null=True, verbose_name='Шаблон для прохождения если '
'не указан явно смотри '
'функцию get_template()')
def __str__(self):
return '%s %s %s' % (
self.user.email,
('завершил курс' if self.success else 'в процессе изучения курса'),
('завершил курс' if self.is_finish() else 'в процессе изучения курса'),
self.course.title,
)
def is_access(self, vertex):
return not vertex.is_more(self.active_obj)
def get_template(self):
return self.template if self.template else self.course.route
class Meta:
verbose_name = 'Прогресс пользователя'
verbose_name_plural = 'Прогресс пользователя'
unique_together = ("user", "course")
def is_finish(self):
return self.get_template().is_finish(self.user)
def get_objects_in_progress(self):
return self.get_template().get_active_objects(self.user)
class ExtraPrivilege(models.Model):
user = models.ForeignKey(to=settings.AUTH_USER_MODEL, verbose_name='Правообладатель')
is_done = models.BooleanField(default=False, verbose_name='Выполнено?')
subject = models.ForeignKey(to=Vertex, verbose_name='Объект')
def is_access(self, vertex):
return vertex in self.progress_list
class Meta:
verbose_name = 'Доп право'
verbose_name_plural = 'Доп права пользователя'
verbose_name = 'Прогресс пользователя'
verbose_name_plural = 'Прогресс пользователя'
unique_together = (("user", "course"),)

@ -1,6 +1,6 @@
from django.contrib.auth import get_user_model
from rest_framework import serializers
from access.models import ExtraPrivilege, Account, Progress
from access.models import Account, Progress
from courses.serializers import MiniVertexSerializer
@ -101,10 +101,4 @@ class UserSearchSerializer(serializers.ModelSerializer):
@staticmethod
def get_pay(self):
return sum([i.get_full_price() for i in self.bill_user.all()])
class ExtraPrivilegeSerializer(serializers.ModelSerializer):
class Meta:
model = ExtraPrivilege
exclude = ('user', )
return sum([i.get_full_price() for i in self.bill_user.all()])

@ -7,7 +7,7 @@ from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from django.db.models import Q
from access.models import Invite, Progress, ExtraPrivilege
from access.models import Invite, Progress
from access.serializers import UserInitSerializer, UserSearchSerializer, UserProfileSerializer
from courses.models import Vertex
from journals.models import Thread
@ -232,13 +232,7 @@ class UpdateProgress(APIView):
except Progress.DoesNotExist:
pass
try:
privilege = ExtraPrivilege.objects.get(user=request.user, subject=vertex)
privilege.is_done = True
privilege.save()
return Response({'id': next_vertex.id, 'type': next_vertex.content_type.model}, status=200)
except ExtraPrivilege.DoesNotExist:
return res_403
return Response({'id': next_vertex.id, 'type': next_vertex.content_type.model}, status=200)
class UserGuardView(APIView):

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:24
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
from django.conf import settings
@ -12,9 +12,9 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('storage', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
@ -44,7 +44,6 @@ class Migration(migrations.Migration):
('big_image', models.URLField(blank=True, max_length=255, verbose_name='Большое изображение')),
('big_mobile_image', models.URLField(blank=True, help_text='Большая картинка для мобильной версии', max_length=255, null=True, verbose_name='Под мобилку')),
('mentors', models.ManyToManyField(blank=True, related_name='course_mentors', to=settings.AUTH_USER_MODEL, verbose_name='Кураторы')),
('teachers', models.ManyToManyField(related_name='course_teachers', to=settings.AUTH_USER_MODEL, verbose_name='Преподаватели')),
],
options={
'verbose_name': 'Курс',
@ -55,10 +54,25 @@ class Migration(migrations.Migration):
name='CourseMap',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('dependent_elements', models.TextField(default='[]')),
('independent_elements', models.TextField(default='[]')),
('course', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='courses.Course')),
('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Course', verbose_name='К какому курсу привязан')),
],
options={
'verbose_name': 'Карта линейного прохождения курсов',
'verbose_name_plural': 'Карты линейного прохождения курсов',
},
),
migrations.CreateModel(
name='CourseRoute',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='Имя шаблона')),
('is_template', models.BooleanField(default=True, verbose_name='Может ли быть использован как шаблон')),
('maps', models.ManyToManyField(to='courses.CourseMap', verbose_name='Карта линейного прохождения курсов')),
],
options={
'verbose_name': 'Маршрут прохождения',
'verbose_name_plural': 'Маршруты прохождения',
},
),
migrations.CreateModel(
name='Diploma',
@ -83,6 +97,19 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Генераторы дипловов',
},
),
migrations.CreateModel(
name='PivotVertex',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sort', models.SmallIntegerField(unique=True, verbose_name='Порядок сортировки')),
('map_course', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.CourseMap', verbose_name='К какой сортеровке имеетотношение')),
],
options={
'verbose_name': 'Порядок сортировки узла',
'verbose_name_plural': 'Порядки сортировок узла',
'ordering': ('sort',),
},
),
migrations.CreateModel(
name='SkillJ',
fields=[
@ -154,6 +181,11 @@ class Migration(migrations.Migration):
name='skill',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Skills', verbose_name='Навык'),
),
migrations.AddField(
model_name='pivotvertex',
name='vertex',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='courses.Vertex', verbose_name='К какому узлу'),
),
migrations.AddField(
model_name='diploma',
name='template',
@ -164,6 +196,16 @@ class Migration(migrations.Migration):
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='course',
name='route',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='courses.CourseRoute', verbose_name='Порядок прохождения по умолчанию'),
),
migrations.AddField(
model_name='course',
name='teachers',
field=models.ManyToManyField(related_name='course_teachers', to=settings.AUTH_USER_MODEL, verbose_name='Преподаватели'),
),
migrations.AddField(
model_name='achievements',
name='course',
@ -174,4 +216,8 @@ class Migration(migrations.Migration):
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='pivotvertex',
unique_together=set([('map_course', 'vertex')]),
),
]

@ -91,6 +91,7 @@ class Course(models.Model):
help_text='Большая картинка для мобильной версии', max_length=255)
teachers = models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='Преподаватели',
related_name='course_teachers')
route = models.OneToOneField(to="CourseRoute", verbose_name="Порядок прохождения по умолчанию", blank=True, null=True)
def __str__(self):
return self.title
@ -144,7 +145,7 @@ class Course(models.Model):
if i not in ['topic', 'tutorial', 'task']:
raise ValueError('undefined model: ' + i)
vertex = Vertex.objects.get(id=self.coursemap.get_first())
vertex = Vertex.objects.get(id=self.coursemap_set.get(sort=0).get_first())
if vertex.content_type.model in vertex_model_list:
return vertex
@ -160,7 +161,7 @@ class Course(models.Model):
if i not in ['topic', 'tutorial', 'task']:
raise ValueError('undefined model: ' + i)
vertex = Vertex.objects.get(id=self.coursemap.get_last())
vertex = Vertex.objects.get(id=self.coursemap_set.get(sort=0).get_last())
if vertex.content_type.model in vertex_model_list:
return vertex
@ -334,9 +335,6 @@ class Vertex(models.Model):
if not user.is_authenticated:
return False
if self.extraprivilege_set.filter(user=user).exists():
return True
try:
progress = self.course.progress_set.get(user=user)
except ObjectDoesNotExist:
@ -388,58 +386,80 @@ class Task(models.Model):
Материалы для урока по сути FileField, нужна только для создания лишней связи в таблице
и дублирования метазаголовков файла
"""
materials = models.ManyToManyField(Storage, verbose_name=u'Материалы для домашней работы', blank=True)
is_exam = models.BooleanField(default=False, verbose_name=u'Экзамен или домашка')
materials = models.ManyToManyField(Storage, verbose_name='Материалы для домашней работы', blank=True)
is_exam = models.BooleanField(default=False, verbose_name='Экзамен или домашка')
class Topic(models.Model):
"""
Модель темы, нужно просто для объединения тасков и уроков.
У некоторых тем есть иконка.
Возможно поле icon перекачует в Vertex, а данная модель отвалится за ненадобностью
Модель темы, нужно просто для объединения тасков и уроков.
У некоторых тем есть иконка.
Возможно поле icon перекачует в Vertex, а данная модель отвалится за ненадобностью
"""
icon = models.ImageField(verbose_name='Иконка темы', null=True, blank=True)
class CourseRoute(models.Model):
"""
Объединение нескольких мап курса, одназначно
определяет способ прохождения по курсу.
"""
icon = models.ImageField(verbose_name=u'Иконка темы', null=True, blank=True)
name = models.CharField(max_length=255, verbose_name='Имя шаблона', blank=True, null=True, unique=True)
maps = models.ManyToManyField(to="CourseMap", verbose_name="Карта линейного прохождения курсов")
is_template = models.BooleanField(default=True, verbose_name='Может ли быть использован как шаблон')
def is_finish(self, user):
return bool(sum([int(i.is_finish(user)) for i in self.maps.all()]))
def get_active_objects(self, user):
return [i.getactive_object(user) for i in self.maps.all()]
class Meta:
verbose_name = 'Маршрут прохождения'
verbose_name_plural = 'Маршруты прохождения'
class CourseMap(models.Model):
"""
Так как курс евляется связным графом мы можем отобразить его бесконечным количеством способов,
а нам нужен один самый красивый, мы должный в явном виде указать способ отображения.
Способы отображения курса. Упорядочены в порядке возрастания приоретета.
"""
course = models.OneToOneField(to=Course)
dependent_elements = models.TextField(default='[]')
independent_elements = models.TextField(default='[]')
course = models.ForeignKey(to=Course, verbose_name='К какому курсу привязан')
def map_to_list(self) -> list:
def helper(root_list):
res = []
for i in root_list:
if type(i) == type([]):
res += helper(i)
@transaction_decorator
def add_vertex(self, vertex, sort):
if sort > self.pivotvertex_set.count()+1:
raise ValueError("list index out of range")
for i in self.pivotvertex_set.filter(sort__gte=sort):
i.sort += 1
i.save()
else:
res.append(i)
pivot = PivotVertex.objects.create(vertex=vertex, sort=sort, map_course=self)
pivot.save()
return pivot
return res
def get_difference(self, user) -> list:
return list(set(
[i.vertex for i in self.pivotvertex_set.all()]).difference(set(user.progress.progress_list.all())
))
return helper(json.loads(self.dependent_elements))
def is_finish(self, user) -> bool:
return self.get_difference(user) == []
def get_next(self, vertex_id) -> int:
res_list = self.map_to_list()
if not res_list[-1] == vertex_id:
return res_list[res_list.index(vertex_id) + 1]
error = "vertex_id " + str(vertex_id) + " last object in list\n" + ",".join([str(v) for v in res_list])
raise ValueError(error)
def get_active_object(self, user):
return self.pivotvertex_set.exclude(vertex__in=self.get_difference(user))[0]
class Meta:
verbose_name = 'Карта линейного прохождения курсов'
verbose_name_plural = 'Карты линейного прохождения курсов'
def get_previous(self, vertex_id) -> int:
res_list = self.map_to_list()
if not res_list[0] == vertex_id:
return res_list[res_list.index(vertex_id) - 1]
error = "vertex_id " + str(vertex_id) + " first object in list\n" + ",".join([str(v) for v in res_list])
raise ValueError(error)
def get_first(self):
return self.map_to_list()[0]
class PivotVertex(models.Model):
vertex = models.ForeignKey(to=Vertex, verbose_name="К какому узлу")
sort = models.SmallIntegerField(verbose_name='Порядок сортировки', unique=True)
map_course = models.ForeignKey(to=CourseMap, verbose_name='К какой сортеровке имеетотношение', blank=True, null=True)
def get_last(self):
return self.map_to_list()[-1]
class Meta:
verbose_name = 'Порядок сортировки узла'
verbose_name_plural = 'Порядки сортировок узла'
unique_together = (('map_course', 'vertex'),)
ordering = ('sort', )

@ -2,9 +2,8 @@ from rest_framework.views import APIView
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from access.serializers import ExtraPrivilegeSerializer
from courses.models import Course, Vertex
from access.models import Progress, ExtraPrivilege
from access.models import Progress
from courses.serializers import CourseDetailSerializer, CourseListSerializer, VertexSerializer, CourseTreeSerializer
from journals.models import Thread

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:33
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
from django.conf import settings
@ -12,6 +12,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('yandex_money', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('courses', '0001_initial'),
]
@ -45,6 +46,7 @@ class Migration(migrations.Migration):
('is_open', models.BooleanField(default=True, verbose_name='Открывает ли платёж курс')),
('date', models.DateTimeField(auto_now_add=True)),
('bill', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='finance.Bill', verbose_name='Связный счёт')),
('yandex_pay', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yandex_money.Payment')),
],
options={
'verbose_name': 'Платёж',

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:33
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('yandex_money', '0001_initial'),
('finance', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='invoice',
name='yandex_pay',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='yandex_money.Payment'),
),
]

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:24
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
from django.conf import settings
@ -12,10 +12,10 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
('contenttypes', '0002_remove_content_type_name'),
('storage', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
('auth', '0008_alter_user_username_max_length'),
]
operations = [

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:24
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
import datetime

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-22 11:24
# Generated by Django 1.11.6 on 2017-11-28 11:50
from __future__ import unicode_literals
from django.db import migrations, models
@ -17,8 +17,8 @@ class Migration(migrations.Migration):
name='Storage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('original', models.FileField(upload_to='files', verbose_name='Файл')),
('name', models.CharField(blank=True, max_length=63, null=True, verbose_name='Видимое имя файла')),
('original', models.FileField(max_length=255, upload_to='files', verbose_name='Файл')),
('name', models.CharField(blank=True, max_length=255, null=True, verbose_name='Видимое имя файла')),
],
options={
'verbose_name': 'Файл',

Loading…
Cancel
Save