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 ChoiceQuestionnaireQuestion(AbstractQuestionnaireQuestion): block_name = 'choice_question' is_multiple = models.BooleanField() is_inline = models.BooleanField() def get_form_field(self, attrs=None): if attrs is None: attrs = {} choices = ((v.id, { 'label': v.text, 'disabled': v.is_disabled }) for v in self.variants.order_by('order', 'id')) attrs['inline'] = self.is_inline if self.is_multiple: field_class = forms.TypedMultipleChoiceFieldForChoiceQuestion widget_class = frontend.forms.SistemaCheckboxSelect else: field_class = forms.TypedChoiceFieldForChoiceQuestion widget_class = frontend.forms.SistemaRadioSelect return field_class( question=self, required=self.is_required, disabled=self.is_disabled, coerce=int, choices=choices, widget=widget_class(attrs=attrs), label=self.text, help_text=self.help_text, )
class AbstractParticipant(polymorphic.models.PolymorphicModel, drapo.models.ModelWithTimestamps): contest = models.ForeignKey(Contest, related_name='participants') is_approved = models.BooleanField(default=True) is_disqualified = models.BooleanField(default=False) is_visible_in_scoreboard = models.BooleanField(default=True) region = models.ForeignKey(ContestRegion, null=True, blank=True, related_name='participants') @property def name(self): return self.get_real_instance().name def get_absolute_url(self): return self.get_real_instance().get_absolute_url() def get_current_score(self): correct_attempts = self.attempts.filter(is_correct=True) solved_tasks = correct_attempts.values('task').distinct() return solved_tasks.aggregate(sum=Sum('task__max_score'))['sum'] def __str__(self): return self.name
class AbstractParticipant(polymorphic.models.PolymorphicModel, drapo.models.ModelWithTimestamps): contest = models.ForeignKey(Contest, related_name='participants') is_approved = models.BooleanField(default=True) is_disqualified = models.BooleanField(default=False) is_visible_in_scoreboard = models.BooleanField(default=True) @property def name(self): return self.get_real_instance().name def get_absolute_url(self): return self.get_real_instance().get_absolute_url()
class ChoiceQuestionnaireQuestion(AbstractQuestionnaireQuestion): block_name = 'choice_question' is_multiple = models.BooleanField() is_inline = models.BooleanField() def copy_dependencies_to_instance(self, other_block): super().copy_dependencies_to_instance(other_block) for variant in self.variants.all(): variant.pk = None variant.question = other_block variant.save() def get_form_field(self, attrs=None): if attrs is None: attrs = {} choices = ((v.id, { 'label': v.text, 'disabled': v.is_disabled }) for v in self.variants.order_by('order', 'id')) attrs['inline'] = self.is_inline if self.is_multiple: field_class = forms.TypedMultipleChoiceFieldForChoiceQuestion widget_class = frontend.forms.SistemaCheckboxSelect else: field_class = forms.TypedChoiceFieldForChoiceQuestion widget_class = frontend.forms.SistemaRadioSelect return field_class( question=self, required=self.is_required, disabled=self.is_disabled, coerce=int, choices=choices, widget=widget_class(attrs=attrs), label=self.text, help_text=self.help_text, ) def get_variant_text(self, variant_id): variant = self.variants.filter(id=variant_id).first() if variant is None: return None return variant.text
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 Spacer(AbstractDocumentBlock): width = models.PositiveIntegerField() height = models.PositiveIntegerField() is_glue = models.BooleanField(default=False) def __str__(self): return 'Spacer %dx%d' % (self.width, self.height) def get_reportlab_block(self, visitor=None): self._process_by_visitor(visitor) return reportlab.platypus.Spacer(self.width, self.height, self.is_glue)
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 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 DateQuestionnaireQuestion(AbstractQuestionnaireQuestion): block_name = 'date_question' with_year = models.BooleanField(default=True) min_year = models.PositiveIntegerField(null=True) max_year = models.PositiveIntegerField(null=True) def get_form_field(self, attrs=None): return django.forms.DateField( required=self.is_required, disabled=self.is_disabled, label=self.text, help_text=self.help_text, widget=django.forms.DateInput( attrs={ 'class': 'gui-input datetimepicker', 'data-format': 'DD.MM.YYYY', 'data-view-mode': 'years', 'data-pick-time': 'false', 'placeholder': 'дд.мм.гггг', }), )
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 EnrolledToSessionAndParallel(models.Model): entrance_status = models.ForeignKey( EntranceStatus, on_delete=models.CASCADE, related_name='sessions_and_parallels', ) session = models.ForeignKey( 'schools.Session', on_delete=models.CASCADE, related_name='+', blank=True, null=True, default=None, ) parallel = models.ForeignKey( 'schools.Parallel', on_delete=models.CASCADE, related_name='+', blank=True, null=True, default=None, ) selected_by_user = models.BooleanField( help_text= 'Пользователь может выбрать одну из предложенных ему параллелей и смен', default=False, db_index=True) def save(self, *args, **kwargs): if self.session is None and self.parallel is None: raise IntegrityError( '%s: session and parallel can not be None at the same time' % self.__class__.__name__) if (self.session is not None and self.session.school_id != self.entrance_status.school_id): raise IntegrityError( '%s: session should belong to the same school as entrance status (%s != %s)' % ( self.__class__.__name__, self.session.school, self.entrance_status.school, )) if (self.parallel is not None and self.parallel.school_id != self.entrance_status.school_id): raise IntegrityError( '%s: parallel should belong to the same school as entrance status (%s != %s)' % ( self.__class__.__name__, self.parallel.school, self.entrance_status.school, )) if (self.session is not None and self.parallel is not None and not self.parallel.sessions.filter( id=self.session_id).exists()): raise IntegrityError( '%s: parallel %s doesn\'t belong to session %s' % ( self.__class__.__name__, self.parallel, self.session, )) super().save(*args, **kwargs) class Meta: unique_together = ('entrance_status', 'session', 'parallel') def __str__(self): return '%s, параллель %s' % (self.session.get_full_name(), self.parallel.name) @transaction.atomic def select_this_option(self): self.entrance_status.sessions_and_parallels.update( selected_by_user=False) self.selected_by_user = True self.save()
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 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 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 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')