class Paragraph(AbstractDocumentBlock): text = models.TextField() style = models.ForeignKey(ParagraphStyle, on_delete=models.CASCADE) bulletText = models.TextField(default=None, null=True, blank=True) def __str__(self): return self.text[:80] def get_reportlab_block(self, visitor=None): self._process_by_visitor(visitor) return reportlab.platypus.Paragraph(self.text, self.style.get_reportlab_style(), self.bulletText)
class QuestionnaireAnswer(models.Model): questionnaire = models.ForeignKey( Questionnaire, on_delete=models.CASCADE, related_name='answers', ) user = models.ForeignKey( users.models.User, on_delete=models.CASCADE, related_name='questionnaire_answers', ) # TODO: may be ForeignKey is better? question_short_name = models.CharField(max_length=100) answer = models.TextField(blank=True) def __str__(self): return 'Ответ «%s» на вопрос %s анкеты %s' % ( self.answer.replace('\n', '\\n'), self.question_short_name, self.questionnaire, ) @property def question(self): return AbstractQuestionnaireQuestion.objects.filter( questionnaire=self.questionnaire, short_name=self.question_short_name).first() class Meta: index_together = ('questionnaire', 'user', 'question_short_name')
class EntranceExamTaskSolution(polymorphic.models.PolymorphicModel): task = models.ForeignKey( 'EntranceExamTask', on_delete=models.CASCADE, related_name='solutions', ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='entrance_exam_solutions', ) solution = models.TextField() ip = models.CharField( max_length=50, help_text='IP-адрес, с которого было отправлено решение', default='') created_at = models.DateTimeField( auto_now_add=True, db_index=True, ) def __str__(self): return 'Решение %s по задаче %s' % (self.user, self.task) class Meta: ordering = ['-created_at'] index_together = ('task', 'user')
class ContestRegion(models.Model): contest = models.ForeignKey(Contest, related_name='regions') name = models.TextField(help_text='Region name') start_time = models.DateTimeField( help_text='Contest start time for this region') finish_time = models.DateTimeField( help_text='Contest finish time for this region') timezone = models.TextField(default='UTC', help_text='Timezone for the region') def __str__(self): return self.name
class InlineQuestionnaireBlock(AbstractQuestionnaireBlock): block_name = 'inline' text = models.TextField(help_text='Общий текст для вопросов в блоке', blank=True) help_text = models.CharField( max_length=400, blank=True, help_text='Подсказка, помогающая ответить на вопросы в блоке', ) def __str__(self): return '%s: %s' % (self.text, ', '.join( [c.block.short_name for c in self.children.all()])) def copy_dependencies_to_instance(self, other_block): super().copy_dependencies_to_instance(other_block) for child in self.children.all(): child.pk = None child.parent = other_block child.block = other_block.questionnaire.blocks.get( short_name=child.block.short_name) child.save() def ordered_children(self): return self.children.order_by('block__order')
class AbstractQuestionnaireQuestion(AbstractQuestionnaireBlock): is_question = True text = models.TextField(help_text='Вопрос') is_required = models.BooleanField( help_text='Является ли вопрос обязательным', ) help_text = models.CharField( max_length=400, blank=True, help_text='Подсказка, помогающая ответить на вопрос', ) is_disabled = models.BooleanField( default=False, help_text='Выключена ли возможность ответить на вопрос. Не может быть ' 'отмечено одновременно с is_required', ) def get_form_field(self, attrs=None): raise NotImplementedError( 'Child should implement its own method get_form_field()') def save(self, *args, **kwargs): if self.is_disabled and self.is_required: raise IntegrityError( 'questionnaire.AbstractQuestionnaireBlock: is_disabled can not ' 'be set with is_required') super().save(*args, **kwargs) def __str__(self): return '%s: %s' % (self.questionnaire, self.text)
class UserListQuestionnaireQuestion(AbstractQuestionnaireQuestion): block_name = 'user_list_question' group = models.ForeignKey( 'groups.AbstractGroup', null=True, on_delete=models.CASCADE, related_name='+', help_text="Группа, пользователей которой можно выбирать", ) placeholder = models.TextField( blank=True, help_text='Подсказка, показываемая в поле для ввода; пример', ) def get_form_field(self, attrs=None): return forms.ChooseUsersFromGroupField( group=self.group, required=self.is_required, disabled=self.is_disabled, label=self.text, help_text=self.help_text, placeholder=self.placeholder, )
class MarkdownQuestionnaireBlock(AbstractQuestionnaireBlock): block_name = 'markdown' markdown = models.TextField() def __str__(self): return self.markdown[:40]
class AbstractAbsenceReason(polymorphic.models.PolymorphicModel): school = models.ForeignKey('schools.School', on_delete=models.CASCADE, related_name='absence_reasons') user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='absence_reasons', ) private_comment = models.TextField(blank=True, help_text='Не показывается школьнику') public_comment = models.TextField(blank=True, help_text='Показывается школьнику') created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='+', null=True, default=None, blank=True, ) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = 'Absence reason' @classmethod def for_user_in_school(cls, user, school): """ Returns absence reason for specified user or None if user has not declined. """ return cls.objects.filter(user=user, school=school).first() def default_public_comment(self): raise NotImplementedError()
class ProgramEntranceExamTask(EjudgeEntranceExamTask): template_file = 'program.html' solutions_template_file = '_program_solutions.html' input_file_name = models.CharField(max_length=100, blank=True) output_file_name = models.CharField(max_length=100, blank=True) time_limit = models.PositiveIntegerField(help_text='В миллисекундах') # Use FileSizeField to be able to define memory limit with units (i.e. 256M) memory_limit = sizefield.models.FileSizeField() input_format = models.TextField(blank=True) output_format = models.TextField(blank=True) def get_form_for_user(self, user, *args, **kwargs): return forms.ProgramEntranceTaskForm(self, *args, **kwargs) @property def solution_class(self): return ProgramEntranceExamTaskSolution
class TextQuestionnaireQuestion(AbstractQuestionnaireQuestion): block_name = 'text_question' is_multiline = models.BooleanField() placeholder = models.TextField( blank=True, help_text='Подсказка, показываемая в поле для ввода; пример', ) fa = models.CharField( max_length=20, blank=True, help_text='Имя иконки FontAwesome, которую нужно показать в поле', ) def get_form_field(self, attrs=None): if attrs is None: attrs = {} if 'placeholder' not in attrs: attrs['placeholder'] = self.placeholder if self.is_multiline: attrs['class'] = 'gui-textarea ' + attrs.pop('class', '') else: attrs['class'] = 'gui-input ' + attrs.pop('class', '') if self.fa != '': if 'fa' not in attrs: attrs['fa'] = self.fa if self.is_multiline: widget = frontend.forms.TextareaWithFaIcon(attrs) else: widget = frontend.forms.TextInputWithFaIcon(attrs) else: if self.is_multiline: widget = django.forms.Textarea(attrs) else: widget = django.forms.TextInput(attrs) return django.forms.CharField( required=self.is_required, disabled=self.is_disabled, help_text=self.help_text, label=self.text, widget=widget, )
class ChoiceQuestionnaireQuestionVariant(models.Model): text = models.TextField() question = models.ForeignKey('ChoiceQuestionnaireQuestion', on_delete=models.CASCADE, related_name='variants') order = models.PositiveIntegerField(default=0) # If variant is disabled it shows as gray and can't be selected is_disabled = models.BooleanField(default=False) # If this one is selected all options are disabled disable_question_if_chosen = models.BooleanField(default=False) def __str__(self): return '{}: {} {}'.format(self.question, self.id, self.text)
class News(ModelWithTimestamps): contest = models.ForeignKey(Contest, related_name='news') author = models.ForeignKey(users.models.User, related_name='+') title = models.CharField(max_length=1000, help_text='Title') text = models.TextField(help_text='Supports markdown') is_published = models.BooleanField(default=False) publish_time = models.DateTimeField(auto_now_add=True) class Meta: verbose_name_plural = 'News' def get_absolute_url(self): return urlresolvers.reverse('contests:news', args=[self.contest_id, self.id])
class FileEntranceExamTask(EntranceExamTask): template_file = 'file.html' type_title = 'Теоретические задачи' checking_criteria = models.TextField( default='', blank=True, help_text='Критерии выставления баллов для проверяющих. ' 'Поддерживается Markdown', ) def is_accepted_for_user(self, user): return self.solutions.filter(user=user).exists() def get_form_for_user(self, user, *args, **kwargs): return forms.FileEntranceTaskForm(self, *args, **kwargs) @property def solution_class(self): return FileEntranceExamTaskSolution
class FileEntranceExamTaskSolution(EntranceExamTaskSolution): original_filename = models.TextField()
class AbstractGroup(polymorphic.models.PolymorphicModel): school = models.ForeignKey( schools.models.School, null=True, blank=True, related_name='groups', on_delete=models.CASCADE, ) created_by = models.ForeignKey( users.models.User, null=True, blank=True, related_name='created_groups', on_delete=models.CASCADE, help_text='Создатель группы. Не может никогда измениться и ' 'всегда имеет полные права на группу.' 'None, если владелец группы — система') short_name = models.CharField( max_length=100, help_text='Используется в урлах. ' 'Лучше обойтись латинскими буквами, цифрами и подчёркиванием', db_index=True, ) name = models.CharField(max_length=60, help_text='Покороче, используется на метках') description = models.TextField(help_text='Подлинее, подробное описание') can_be_deleted = models.BooleanField( default=True, help_text='Системные группы не могут быть удалены', ) list_members_to_everyone = models.BooleanField( default=False, help_text='Видно ли всем участие других в этой группе', ) class Meta: unique_together = ('short_name', 'school') verbose_name = 'group' def is_user_in_group(self, user): """ You can override this method in subclass. By default it calls overridden self.user_ids. Be careful: this approach can be slow on large groups. :return: True if user is in group and False otherwise. """ return user.id in self.user_ids @property def users(self): """ You can override this method in subclass. By default it calls overridden self.user_ids :return: QuerySet for users.models.User model with users from this group """ return users.models.User.objects.filter(id__in=self.user_ids) @property def user_ids(self): """ :return: QuerySet or list of ids of users which are members of this group """ raise NotImplementedError( 'Each group should implement user_ids(), but %s doesn\'t' % self.__class__.__name__) @property def default_access_type(self): if self.list_members_to_everyone: return GroupAccess.Type.LIST_MEMBERS return GroupAccess.Type.NONE def get_access_type_for_user(self, user): user_access = GroupAccessForUser.objects.filter(to_group=self, user=user).first() if user_access is None: user_access = self.default_access_type else: user_access = user_access.access_type # We need to cast queryset to list because following call # group_access.group.is_user_in_group() produces another query to database # and this query should be finished at this time group_accesses = list( GroupAccessForGroup.objects.filter(to_group=self).select_related( 'group').order_by('-access_type')) for group_access in group_accesses: # Access levels are sorted in decreasing order, # so we use the first one granted to the user if user_access > group_access.access_type: break if group_access.group.is_user_in_group(user): return group_access.access_type return user_access def __str__(self): result = 'Группа «%s»' % self.name if self.school is not None: result += ' для ' + str(self.school) return result
class EntranceExamTaskCategory(models.Model): """ Tasks are displayed in these categories on the exam page. """ exam = models.ForeignKey( 'EntranceExam', on_delete=models.CASCADE, verbose_name="экзамен", related_name='task_categories', ) short_name = models.SlugField( help_text="Может состоять только из букв, цифр, знака подчеркивания и " "дефиса.", ) title = models.CharField( verbose_name="заголовок", max_length=100, help_text="Заголовок категории, например «Практические задачи:»", ) order = models.IntegerField( verbose_name="порядок", help_text="Категории задач отображаются в заданном порядке", ) is_mandatory = models.BooleanField( verbose_name="обязательная категория", default=True, help_text="Обязательно ли решать задачи в этой категории, чтобы " "вступительная считалась выполненной?", ) available_from_time = models.ForeignKey( 'dates.KeyDate', on_delete=models.CASCADE, verbose_name="доступна c", related_name='+', null=True, blank=True, default=None, help_text="Момент времени, начиная с которого задачи этой категории " "показываются пользователям. Оставьте пустым, если задачи " "должны быть доступны с начала вступительной работы.", ) available_to_time = models.ForeignKey( 'dates.KeyDate', on_delete=models.CASCADE, verbose_name="доступна до", related_name='+', null=True, blank=True, default=None, help_text="Момент времени, после которого возможность послать решения " "по задачам этой категории будет закрыта. Оставьте пустым, " "если задачи должны быть доступны до конца вступительной " "работы.", ) text_after_closing = models.TextField( blank=True, verbose_name="текст после закрытия", help_text="Текст, который показывается на странице задачи после " "закрытия задач этой категории, но до конца вступительной " "работы.\n" "Поддерживается Markdown.", ) class Meta: unique_together = [('exam', 'short_name'), ('exam', 'order')] verbose_name = _('task category') verbose_name_plural = _('task categories') def __str__(self): return 'Категория задач «{}» для «{}»'.format(self.title, self.exam) def is_started_for_user(self, user): if self.available_from_time is None: return True return self.available_from_time.passed_for_user(user) def is_finished_for_user(self, user): if self.available_to_time is None: return False return self.available_to_time.passed_for_user(user)
class EntranceExamTask(polymorphic.models.PolymorphicModel): title = models.CharField(max_length=100, help_text='Название') text = models.TextField(help_text='Формулировка задания') exam = models.ForeignKey( 'EntranceExam', on_delete=models.CASCADE, related_name='%(class)s', ) category = models.ForeignKey( 'EntranceExamTaskCategory', on_delete=models.CASCADE, verbose_name='категория', related_name='tasks', ) help_text = models.CharField( max_length=100, help_text='Дополнительная информация, например, сведения о формате ' 'ответа', blank=True) order = models.IntegerField( help_text='Задачи выстраиваются по возрастанию порядка', default=0) max_score = models.PositiveIntegerField() custom_description = models.TextField( help_text='Текст с описанием типа задачи. Оставьте пустым, тогда будет ' 'использован текст по умолчанию для данного вида задач. ' 'В этом тексте можно указать, например, ' 'для кого эта задача предназначена.\n' 'Поддерживается Markdown', blank=True, ) def __str__(self): return "{}: {}".format(self.exam.school.name, self.title) def save(self, *args, **kwargs): if self.category.exam_id != self.exam_id: raise IntegrityError( "{}.{}: task's category should belong to the same exam as the " "task itself".format(self.__module__, self.__class__.__name__)) super().save(*args, **kwargs) def is_accepted_for_user(self, user): # Always not accepted by default. Override when subclassing. return False def is_solved_by_user(self, user): # Always not solved by default. Override when subclassing. return False @property def template_file(self): """ Return template file name in folder templates/entrance/exam/ """ raise NotImplementedError('Child should define property template_file') @property def type_title(self): """ Return title of blocks with these tasks """ raise NotImplementedError('Child should define property type_title') def get_form_for_user(self, user, *args, **kwargs): """ Return form for this task for the specified user """ raise NotImplementedError('Child should define get_form_for_user()') @property def solution_class(self): raise NotImplementedError( 'Child should define property solution_class')
class EntranceStatus(models.Model): class Status(djchoices.DjangoChoices): NOT_PARTICIPATED = djchoices.ChoiceItem(1, 'Не участвовал в конкурсе') AUTO_REJECTED = djchoices.ChoiceItem(2, 'Автоматический отказ') NOT_ENROLLED = djchoices.ChoiceItem(3, 'Не прошёл по конкурсу') ENROLLED = djchoices.ChoiceItem(4, 'Поступил') PARTICIPATING = djchoices.ChoiceItem(5, 'Подал заявку') IN_RESERVE_LIST = djchoices.ChoiceItem(6, 'В резервном списке') school = models.ForeignKey( 'schools.School', on_delete=models.CASCADE, related_name='entrance_statuses', ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='entrance_statuses', ) # created_by=None means system's auto creating created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='+', blank=True, null=True, default=None, ) public_comment = models.TextField( help_text='Публичный комментарий. Может быть виден поступающему', blank=True, ) private_comment = models.TextField( help_text='Приватный комментарий. Виден только админам вступительной', blank=True, ) is_status_visible = models.BooleanField(default=False) status = models.IntegerField(choices=Status.choices, validators=[Status.validator]) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) is_approved = models.BooleanField( help_text= 'Подтверждено ли участие пользователем. Имеет смысл, только если статус = «Поступил»', default=False) approved_at = models.DateTimeField(null=True, blank=True, default=None) @property def is_enrolled(self): return self.status == self.Status.ENROLLED @property def is_in_reserve_list(self): return self.status == self.Status.IN_RESERVE_LIST def approve(self): self.is_approved = True self.approved_at = django.utils.timezone.now() self.save() def remove_approving(self): self.is_approved = False self.approved_at = None self.save() @classmethod def create_or_update(cls, school, user, status, **kwargs): with transaction.atomic(): current = cls.objects.filter(school=school, user=user).first() if current is None: current = cls(school=school, user=user, status=status, **kwargs) else: current.status = status for key, value in current: setattr(current, key, value) current.save() @classmethod def get_visible_status(cls, school, user): return cls.objects.filter(school=school, user=user, is_status_visible=True).first() def __str__(self): return '%s %s' % (self.user, self.Status.values[self.status]) class Meta: verbose_name_plural = 'User entrance statuses' unique_together = ('school', 'user')
class Contest(polymorphic.models.PolymorphicModel): name = models.TextField(help_text='Contest name') is_visible_in_list = models.BooleanField(default=False) registration_type = models.CharField( max_length=20, choices=ContestRegistrationType.choices, validators=[ContestRegistrationType.validator]) participation_mode = models.CharField( max_length=20, choices=ContestParticipationMode.choices, validators=[ContestParticipationMode.validator]) start_time = models.DateTimeField(help_text='Contest start time') finish_time = models.DateTimeField(help_text='Contest finish time') registration_start_time = models.DateTimeField( help_text= 'Contest registration start time, only for open and moderated registrations', blank=True, null=True) registration_finish_time = models.DateTimeField( help_text= 'Contest registration finish time, only for open and moderated registration', blank=True, null=True) short_description = models.TextField(help_text='Shows on main page') description = models.TextField( help_text='Full description. Supports MarkDown') def __str__(self): return self.name def get_absolute_url(self): return urls.reverse('contests:contest', args=[self.id]) def is_user_participating(self, user): if self.participation_mode == ContestParticipationMode.Individual: return self.participants.filter( individualparticipant__user=user).exists() elif self.participation_mode == ContestParticipationMode.Team: return self.participants.filter( teamparticipant__team__members=user).exists() else: raise ValueError('Unknown participation mode: %s' % (self.participation_mode, )) def get_user_team(self, user): """ Return Team object for user if contest is team-based and user is a participant Otherwise return None """ if self.participation_mode != ContestParticipationMode.Team: return None team_participant = self.get_participant_for_user(user) if team_participant is None: return None return team_participant.team def get_participant_for_user(self, user): """ Returns IndividualParticipant or TeamParticipant """ participant = None if user.is_anonymous: return None if self.participation_mode == ContestParticipationMode.Team: participant = self.participants.filter( teamparticipant__team__members=user).first() if self.participation_mode == ContestParticipationMode.Individual: participant = self.participants.filter( individualparticipant__user=user).first() return participant def can_register_now(self): return (self.registration_type in [ ContestRegistrationType.Open, ContestRegistrationType.Moderated ] and self.registration_start_time <= timezone.now() < self.registration_finish_time) def can_register_in_future(self): return (self.registration_type in [ ContestRegistrationType.Open, ContestRegistrationType.Moderated ] and timezone.now() < self.registration_start_time) def is_running(self): return self.start_time <= timezone.now() < self.finish_time def is_finished(self): return self.finish_time <= timezone.now() def is_started(self): return self.start_time <= timezone.now() def show_menu_on_top(self): return self.is_started() def is_team(self): return self.participation_mode == ContestParticipationMode.Team def is_individual(self): return self.participation_mode == ContestParticipationMode.Individual