class TestEntranceExamTask(EntranceExamTask): template_file = 'test.html' type_title = 'Тестовые задания' correct_answer_re = models.CharField( max_length=100, help_text='Правильный ответ (регулярное выражение)', ) validation_re = models.CharField( max_length=100, help_text='Регулярное выражение для валидации ввода', blank=True, ) def is_solution_valid(self, solution): return re.fullmatch(self.validation_re, solution) is not None def is_solution_correct(self, solution): return re.fullmatch(self.correct_answer_re, solution) is not None def is_accepted_for_user(self, user): last_solution = self.solutions.filter(user=user).last() return (last_solution is not None and self.is_solution_valid(last_solution.solution)) def is_solved_by_user(self, user): last_solution = self.solutions.filter(user=user).last() return (last_solution is not None and self.is_solution_correct(last_solution.solution)) def get_form_for_user(self, user, *args, **kwargs): initial = {} last_solution = (self.solutions.filter( user=user).order_by('-created_at').first()) if last_solution is not None: initial['solution'] = last_solution.solution form = forms.TestEntranceTaskForm(self, initial=initial, *args, **kwargs) if self.exam.is_closed() or self.category.is_finished_for_user(user): form['solution'].field.widget.attrs['readonly'] = True return form # Define it as property because TestEntranceExamTaskSolution # is not defined yet @property def solution_class(self): return TestEntranceExamTaskSolution
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 TaskBasedContest(Contest): tasks_grouping = models.CharField(max_length=20, choices=TasksGroping.choices, validators=[TasksGroping.validator]) @cached_property def categories(self): if self.tasks_grouping != TasksGroping.ByCategories: return [] return list(self.categories_list.categories.all()) @cached_property def tasks(self): if self.tasks_grouping != TasksGroping.OneByOne: return [] return list(self.tasks_list.tasks.all()) def get_tasks_solved_by_participant(self, participant): """ Returns task ids for solved by participant tasks """ return set( self.attempts.filter(participant=participant, is_checked=True, is_correct=True).values_list('task_id', flat=True)) def has_task(self, task): if self.tasks_grouping == TasksGroping.OneByOne: return task in self.tasks if self.tasks_grouping == TasksGroping.ByCategories: return self.categories_list.categories.filter(tasks=task).exists()
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 Font(models.Model): name = models.CharField(max_length=100, unique=True) filename = RelativeFilePathField( path=django.db.migrations.writer.SettingsReference( settings.SISTEMA_GENERATOR_FONTS_DIR, 'SISTEMA_GENERATOR_FONTS_DIR'), match='.*\.ttf', recursive=True, max_length=1000) def __str__(self): return self.name def get_reportlab_font(self): return reportlab.pdfbase.ttfonts.TTFont(self.name, self.get_filename_abspath()) def register_in_reportlab(self): reportlab.pdfbase.pdfmetrics.registerFont(self.get_reportlab_font()) _registered = False @classmethod def register_all_in_reportlab(cls): if cls._registered: return cls._registered = True for font in cls.objects.all(): font.register_in_reportlab()
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 ValignTableStyleCommand(CellFormattingTableStyleCommand): command_name = 'VALIGN' command_params = ['direction'] direction = models.CharField(max_length=6, choices=[('TOP', 'Top'), ('MIDDLE', 'Middle'), ('BOTTOM', 'Bottom')])
class AlignmentTableStyleCommand(CellFormattingTableStyleCommand): command_name = 'ALIGNMENT' command_params = 'align' align = models.CharField(max_length=20, choices=TableCellAlignment.choices, validators=[TableCellAlignment.validator])
class TextColorTableStyleCommand(CellFormattingTableStyleCommand): command_name = 'TEXTCOLOR' command_params = ['color'] color = models.CharField(max_length=20, choices=Color.choices, validators=[Color.validator])
class LineTableStyleCommand(AbstractTableStyleCommand): command_name = models.CharField( max_length=100, choices=[(c, c.title()) for c in sorted(reportlab.platypus.tables.LINECOMMANDS)], ) command_params = ['thickness', 'color'] thickness = models.FloatField(help_text='В пунктах') color = models.CharField(max_length=20, choices=Color.choices, validators=[Color.validator]) def get_reportlab_command(self): return (self.command_name, self.start, self.stop, self.thickness, reportlab.lib.colors.HexColor(self.color))
class EntranceLevel(models.Model): """ Уровень вступительной работы. Для каждой задачи могут быть указаны уровни, для которых она предназначена. Уровень школьника определяется с помощью EntranceLevelLimiter'ов (например, на основе тематической анкеты из модуля topics, класса в школе или учёбы в других параллелях в прошлые годы) """ school = models.ForeignKey('schools.School', on_delete=models.CASCADE) short_name = models.CharField( max_length=100, help_text='Используется в урлах. ' 'Лучше обойтись латинскими буквами, цифрами и подчёркиванием') name = models.CharField(max_length=100) order = models.IntegerField(default=0) tasks = models.ManyToManyField( 'EntranceExamTask', blank=True, related_name='entrance_levels', ) def __str__(self): return 'Уровень «%s» для %s' % (self.name, self.school) def __gt__(self, other): return self.order > other.order def __lt__(self, other): return self.order < other.order def __ge__(self, other): return self.order >= other.order def __le__(self, other): return self.order <= other.order class Meta: ordering = ('school_id', 'order')
class Document(models.Model): name = models.CharField( max_length=255, help_text='Не показывается школьникам. Например, «Договор 2016 с ' 'Берендеевыми Полянами»', ) page_size = models.CharField(max_length=20, choices=PageSize.choices, validators=[PageSize.validator]) left_margin = models.IntegerField(default=0) right_margin = models.IntegerField(default=0) top_margin = models.IntegerField(default=0) bottom_margin = models.IntegerField(default=0) def __str__(self): return self.name
class PaddingTableStyleCommand(CellFormattingTableStyleCommand): command_params = ['padding'] direction = models.CharField(max_length=6, choices=[('LEFT', 'Left'), ('RIGHT', 'Right'), ('BOTTOM', 'Bottom'), ('TOP', 'Top')]) padding = models.PositiveIntegerField(help_text='В пунктах') @property def command_name(self): return '%sPADDING' % (self.direction, )
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 ParagraphStyle(models.Model): name = models.CharField(max_length=100) leading = models.FloatField() alignment = models.PositiveIntegerField( choices=Alignment.choices, validators=[Alignment.validator], ) font = models.ForeignKey( Font, on_delete=models.CASCADE, related_name='+', ) font_size = models.PositiveIntegerField() bullet_font = models.ForeignKey( Font, on_delete=models.CASCADE, related_name='+', ) space_before = models.PositiveIntegerField() space_after = models.PositiveIntegerField() left_indent = models.PositiveIntegerField() def __str__(self): return self.name def get_reportlab_style(self): return reportlab.lib.styles.ParagraphStyle( name=self.name, leading=self.leading, fontName=self.font.name, fontSize=self.font_size, bulletFontName=self.bullet_font.name, alignment=self.alignment, spaceBefore=self.space_before, spaceAfter=self.space_after, leftIndent=self.left_indent, )
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 AbstractQuestionnaireBlock(polymorphic.models.PolymorphicModel): questionnaire = models.ForeignKey('Questionnaire', on_delete=models.CASCADE) short_name = models.CharField( max_length=100, help_text='Используется в урлах. Лучше обойтись латинскими буквами, ' 'цифрами и подчёркиванием') order = models.IntegerField( default=0, help_text='Блоки выстраиваются по возрастанию порядка') is_question = False def __str__(self): return '%s. %s' % (self.questionnaire, self.short_name) class Meta: verbose_name = 'questionnaire block' unique_together = [('short_name', 'questionnaire'), ('questionnaire', 'order')] ordering = ('questionnaire_id', 'order')
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 AbstractQuestionnaireBlock(polymorphic.models.PolymorphicModel): questionnaire = models.ForeignKey( 'Questionnaire', on_delete=models.CASCADE, related_name='blocks', ) short_name = models.CharField( max_length=100, help_text='Используется в урлах. Лучше обойтись латинскими буквами, ' 'цифрами и подчёркиванием') order = models.IntegerField( default=0, help_text='Блоки выстраиваются по возрастанию порядка') # TODO (andgein): it may be better to make another model with storing # top-level blocks of questionnaire (and orders of them) is_top_level = models.BooleanField( help_text='True, если блок находится на верхнем уровне вложенности', default=True, ) is_question = False class Meta: verbose_name = 'questionnaire block' unique_together = [('short_name', 'questionnaire'), ('questionnaire', 'order')] ordering = ('questionnaire_id', 'order') def __str__(self): obj = self.get_real_instance() if hasattr(obj, 'text'): description = '%s (%s)' % (obj.text, self.short_name) else: description = self.short_name return '%s. %s' % (self.questionnaire, description) def copy_to_questionnaire(self, to_questionnaire): """ Make a copy of this block in the specified questionnaire. In order for this method to work correctly for each particular subclass the subclass should override _copy_fields_to_instance. :param to_questionnaire: A questionnaire to copy the block to. """ fields_diff = (set(self.__class__._meta.get_fields()) - set(AbstractQuestionnaireBlock._meta.get_fields())) fields_to_copy = list( filter( lambda f: not f.auto_created and not f.is_relation, fields_diff, )) field_kwargs = {f.name: getattr(self, f.name) for f in fields_to_copy} new_instance = self.__class__( questionnaire=to_questionnaire, short_name=self.short_name, order=self.order, **field_kwargs, ) self._copy_fields_to_instance(new_instance) new_instance.save() return new_instance def is_visible_to_user(self, user): """ Return true if block is visible to the user. Includes only server-side conditions, which don't change on client side during editing. For now there is only one server-side show condition - `GroupMemberShowCondition` :param user: User :return: True if block is visible, False otherwise """ group_member_conditions = ( self.show_conditions_questionnaireblockgroupmembershowcondition. all()) if not group_member_conditions.exists(): return True for condition in group_member_conditions: if condition.is_satisfied(user): return True return False def copy_dependencies_to_instance(self, other_block): """ Copies dependencies between blocks. This method is called when all blocks are copied :param other_block: Block from other questionnaire where dependencies should be copied Override this method in a subclass to copy any objects having a reference to this block and other blocks. The override must: - call super().copy_dependencies_to_instance(other), - make copies of the dependencies for the passed instance. """ pass def _copy_fields_to_instance(self, other): """ Subclasses must override this method if they define new relation fields or if some of their plain fields require non-trivial copying. The implementation should: - call super()._copy_fields_to_instance(other), - copy its field values to the passed instance. :param other: The instance to copy field values to. """ pass @property def block_name(self): """ Name of template file in `templates/questionnaire/blocks/`. Also part of css class for this type's blocks (i.e. `questionnaire__block__markdown` for MarkdownQuestionnaireBlock) """ raise NotImplementedError("%s doesn't implement block_name property " % self.__class__.__name__) @cached_property def show_conditions(self): return (list( self.show_conditions_questionnaireblockgroupmembershowcondition. all()) + list( self. show_conditions_questionnaireblockvariantcheckedshowcondition. all()))
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
class Questionnaire(models.Model): title = models.CharField(max_length=100, help_text='Название анкеты') short_name = models.CharField( max_length=100, help_text='Используется в урлах. Лучше обойтись латинскими буквами, ' 'цифрами и подчёркиванием', ) school = models.ForeignKey( schools.models.School, on_delete=models.CASCADE, blank=True, null=True, ) session = models.ForeignKey( schools.models.Session, on_delete=models.CASCADE, blank=True, null=True, ) close_time = models.ForeignKey( 'dates.KeyDate', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, verbose_name='Время закрытия', help_text='Начиная с этого момента пользователи видят анкету в режиме ' 'только для чтения', ) enable_autofocus = models.BooleanField( default=True, help_text='Будет ли курсор автоматически фокусироваться на первом ' 'вопросе при загрузке страницы', ) should_record_typing_dynamics = models.BooleanField(default=False) must_fill = models.ForeignKey( groups.models.AbstractGroup, blank=True, null=True, default=None, on_delete=models.SET_NULL, related_name='+', help_text='Группа пользователей, которые должны заполнить эту анкету.' 'Если не указано, то считается, что никто не должен') class Meta: unique_together = ('school', 'short_name') def __str__(self): if self.school is not None: return '%s. %s' % (self.school, self.title) return self.title # TODO: Extract to ModelWithCloseTime? def is_closed_for_user(self, user): return (self.close_time is not None and self.close_time.passed_for_user(user)) @cached_property def blocks(self): return sorted(self.abstractquestionnaireblock_set.all(), key=operator.attrgetter('order')) @cached_property def questions(self): questions = self.abstractquestionnaireblock_set.instance_of( AbstractQuestionnaireQuestion) return sorted(questions, key=operator.attrgetter('order')) @cached_property def show_conditions(self): return group_by( QuestionnaireBlockShowCondition.objects.filter( block__questionnaire=self), operator.attrgetter('block_id')) def get_form_class(self, attrs=None): if attrs is None: attrs = {} fields = { 'prefix': self.get_prefix(), } is_first = True for question in self.questions: question_attrs = copy.copy(attrs) if is_first: if self.enable_autofocus: question_attrs['autofocus'] = 'autofocus' is_first = False fields[question.short_name] = ( question.get_form_field(question_attrs)) form_class = type('%sForm' % self.short_name.title(), (forms.QuestionnaireForm, ), fields) return form_class def get_prefix(self): return 'questionnaire_' + self.short_name def get_absolute_url(self): if self.school is None: return reverse( 'questionnaire', kwargs={'questionnaire_name': self.short_name}, ) else: return reverse('school:questionnaire', kwargs={ 'questionnaire_name': self.short_name, 'school_name': self.school.short_name }) def is_filled_by(self, user): user_status = self.statuses.filter(user=user).first() if user_status is None: return False return user_status.status == UserQuestionnaireStatus.Status.FILLED def get_filled_user_ids(self, only_who_must_fill=False): if only_who_must_fill and self.must_fill is None: return [] qs = self.statuses.filter(status=UserQuestionnaireStatus.Status.FILLED) if only_who_must_fill: must_fill_user_ids = list( self.must_fill.users.values_list('id', flat=True)) qs = qs.filter(user_id__in=must_fill_user_ids) return qs.values_list('user_id', flat=True).distinct() def get_filled_users(self, only_who_must_fill=False): filled_user_ids = self.get_filled_user_ids(only_who_must_fill) return users.models.User.objects.filter(id__in=filled_user_ids)
class Questionnaire(models.Model): title = models.CharField(max_length=100, help_text='Название анкеты') short_name = models.CharField( max_length=100, help_text='Используется в урлах. Лучше обойтись латинскими буквами, ' 'цифрами и подчёркиванием', ) school = models.ForeignKey( schools.models.School, on_delete=models.CASCADE, blank=True, null=True, ) session = models.ForeignKey( schools.models.Session, on_delete=models.CASCADE, blank=True, null=True, ) close_time = models.ForeignKey( 'dates.KeyDate', on_delete=models.SET_NULL, related_name='+', blank=True, null=True, verbose_name='Время закрытия', help_text='Начиная с этого момента пользователи видят анкету в режиме ' 'только для чтения', ) enable_autofocus = models.BooleanField( default=True, help_text='Будет ли курсор автоматически фокусироваться на первом ' 'вопросе при загрузке страницы', ) should_record_typing_dynamics = models.BooleanField(default=False) must_fill = models.ForeignKey( groups.models.AbstractGroup, blank=True, null=True, default=None, on_delete=models.SET_NULL, related_name='+', help_text='Группа пользователей, которые должны заполнить эту анкету.' 'Если не указано, то считается, что никто не должен') class Meta: unique_together = ('school', 'short_name') def __str__(self): if self.school is not None: return '%s. %s' % (self.school, self.title) return self.title def save(self, *args, **kwargs): if (self.school is not None and self.session is not None and self.session.school != self.school): raise IntegrityError( "Questionnaire's session should belong to the questionnaire's " "school") super().save(*args, **kwargs) # TODO: Extract to ModelWithCloseTime? def is_closed_for_user(self, user): return (self.close_time is not None and self.close_time.passed_for_user(user)) @cached_property def _blocks_with_server_side_show_conditions(self): return (self.blocks.prefetch_related( 'show_conditions_questionnaireblockgroupmembershowcondition')) @cached_property def ordered_blocks(self): return self._blocks_with_server_side_show_conditions.order_by('order') @cached_property def ordered_top_level_blocks(self): return (self._blocks_with_server_side_show_conditions.filter( is_top_level=True).order_by('order')) @cached_property def questions(self): return (self._blocks_with_server_side_show_conditions.instance_of( AbstractQuestionnaireQuestion)) @cached_property def ordered_questions(self): return self.questions.order_by('order') @cached_property def variant_checked_show_conditions(self): conditions = ( QuestionnaireBlockVariantCheckedShowCondition.objects.filter( block__questionnaire=self)) return group_by(conditions, operator.attrgetter('block_id')) def get_form_class(self, user, attrs=None): if attrs is None: attrs = {} fields = { 'prefix': self.get_fields_common_prefix(), } is_first = True for question in self.ordered_questions: if not question.is_visible_to_user(user): continue question_attrs = copy.copy(attrs) if is_first: if self.enable_autofocus: question_attrs['autofocus'] = 'autofocus' is_first = False fields[question.short_name] = ( question.get_form_field(question_attrs)) form_class = type('%sForm' % self.short_name.title(), (forms.QuestionnaireForm, ), fields) return form_class def get_fields_common_prefix(self): return 'questionnaire_' + self.short_name def get_absolute_url(self): if self.school is None: return reverse( 'questionnaire', kwargs={'questionnaire_name': self.short_name}, ) else: return reverse('school:questionnaire', kwargs={ 'questionnaire_name': self.short_name, 'school_name': self.school.short_name }) def is_filled_by(self, user): user_status = self.statuses.filter(user=user).first() if user_status is None: return False return user_status.status == UserQuestionnaireStatus.Status.FILLED def get_filled_user_ids(self, only_who_must_fill=False): if only_who_must_fill and self.must_fill is None: return [] qs = self.statuses.filter(status=UserQuestionnaireStatus.Status.FILLED) if only_who_must_fill: must_fill_user_ids = list( self.must_fill.users.values_list('id', flat=True)) qs = qs.filter(user_id__in=must_fill_user_ids) return qs.values_list('user_id', flat=True).distinct() def get_filled_users(self, only_who_must_fill=False): filled_user_ids = self.get_filled_user_ids(only_who_must_fill) return users.models.User.objects.filter(id__in=filled_user_ids) def get_user_answers(self, user, substitute_choice_variants=False): """ Returns dict with user answers :param substitute_choice_variants: if True, questions of type ChoiceQuestionnaireQuestion will return ['value 1', 'value 2'] instead of [1, 2] :return: dict of {question.short_name: answer} """ questions = {q.short_name: q for q in self.questions} answers = self.answers.filter(user=user) variants = list( ChoiceQuestionnaireQuestionVariant.objects.filter( question__questionnaire=self)) variants = {v.id: v for v in variants} result = {} for answer in answers: # Question could be deleted from the time when user answered it if answer.question_short_name not in questions: continue question = questions[answer.question_short_name] answer_value = answer.answer answer_type = str if isinstance(question, ChoiceQuestionnaireQuestion): if substitute_choice_variants and answer_value: answer_value = variants[int(answer_value)].text if question.is_multiple: answer_type = list elif isinstance(question, DateQuestionnaireQuestion): answer_value = datetime.datetime.strptime( answer_value, settings.SISTEMA_QUESTIONNAIRE_STORING_DATE_FORMAT).date() answer_type = datetime.date elif isinstance(question, UserListQuestionnaireQuestion): answer_type = list if answer_type is list: if question.short_name not in result: result[question.short_name] = [] result[question.short_name].append(answer_value) else: result[question.short_name] = answer_value return result # A unique object used as the default argument value in the clone method. # Needed, because we want to handle None. KEEP_VALUE = object() @transaction.atomic def clone(self, new_school=KEEP_VALUE, new_short_name=KEEP_VALUE, new_session=KEEP_VALUE, new_close_time=KEEP_VALUE): """ Make and return a full copy of the questionnaire. The copy should have a unique `(school, short_name)` combination. You can change either of them by setting the corresponding method argument. :param new_school: The school for the new questionnaire. By default is equal to the source questionnaire's school. :param new_short_name: The short name for the new questionnaire. By default is equal to the source questionnaire's school. :param new_session: The session for the new questionnaire. By default keeps its value if the school is unchanged, or is set to None otherwise. :param new_close_time: The `dates.KeyDate` object for the closing time. By default keeps its value if the school is unchanged, or is set to `None` otherwise. :return: The fresh copy of the questionnaire. """ if self.pk is None: raise ValueError( "The questionnaire should be in database to be cloned") if new_school == self.KEEP_VALUE: new_school = self.school if new_short_name == self.KEEP_VALUE: new_short_name = self.short_name school_unchanged = (new_school == self.school) if new_session == self.KEEP_VALUE: new_session = self.session if school_unchanged else None if new_close_time == self.KEEP_VALUE: new_close_time = self.close_time if school_unchanged else None # Check if questionnaire with pair (school, short_name) already exists new_questionnaire = Questionnaire.objects.filter( school=new_school, short_name=new_short_name, ).first() if new_questionnaire is None: # Questionnaire doesn't exist, create a copy new_questionnaire = Questionnaire.objects.get(pk=self.pk) new_questionnaire.pk = None new_questionnaire.school = new_school new_questionnaire.short_name = new_short_name new_questionnaire.session = new_session new_questionnaire.close_time = new_close_time new_questionnaire.save() elif new_questionnaire.id == self.id: raise IntegrityError('Can\'t copy questionnaire to itself') self._copy_blocks_to_questionnaire(new_questionnaire) self._copy_block_dependencies(new_questionnaire) self._copy_block_show_conditions_to_questionnaire(new_questionnaire) return new_questionnaire def _copy_blocks_to_questionnaire(self, to_questionnaire): for block in self.blocks.all(): block.copy_to_questionnaire(to_questionnaire) def _copy_block_show_conditions_to_questionnaire(self, to_questionnaire): """ Copy all inheritors of the `AbstractQuestionnaireBlockShowCondition` objects to the specified questionnaire. Conditions are skipped if the target questionnaire doesn't have a block with the same `short_name` or in some other cases (see documentation for copy_condition_to_questionnaire() methods in inherited classes). :param to_questionnaire: The questionnaire to copy the conditions to. :return: (<number of copied conditions>, <number of skipped conditions>) """ copied_count = 0 skipped_count = 0 for block in self.blocks.all(): for condition in block.show_conditions: new_condition = (condition.copy_condition_to_questionnaire( to_questionnaire)) if new_condition is None: skipped_count += 1 else: copied_count += 1 return copied_count, skipped_count def _copy_block_dependencies(self, to_questionnaire): """ Copy block's dependencies when all blocks are already created """ for block in self.blocks.all(): other_block = to_questionnaire.blocks.get( short_name=block.short_name) block.copy_dependencies_to_instance(other_block)
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 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 FontFamily(models.Model): name = models.CharField(max_length=100, unique=True) normal = models.ForeignKey( Font, on_delete=models.CASCADE, null=True, blank=True, default=None, help_text='Обычное начертание', related_name='+', ) bold = models.ForeignKey( Font, on_delete=models.CASCADE, null=True, blank=True, default=None, help_text='Полужирное начертание', related_name='+', ) italic = models.ForeignKey( Font, on_delete=models.CASCADE, null=True, blank=True, default=None, help_text='Курсивное начертание', related_name='+', ) bold_italic = models.ForeignKey( Font, on_delete=models.CASCADE, null=True, blank=True, default=None, help_text='Полужирное курсивное начертание', related_name='+', ) def __str__(self): return self.name class Meta: verbose_name_plural = 'Font families' def register_in_reportlab(self): # Ensure that all fonts are registered Font.register_all_in_reportlab() reportlab.pdfbase.pdfmetrics.registerFontFamily( self.name, self.normal.name, self.bold.name, self.italic.name, self.bold_italic.name) _registered = False @classmethod def register_all_in_reportlab(cls): if cls._registered: return cls._registered = True for family in cls.objects.all(): family.register_in_reportlab()