Example #1
0
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)
Example #2
0
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,
        )
Example #3
0
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
Example #4
0
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()
Example #5
0
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
Example #6
0
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)
Example #7
0
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)
Example #8
0
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,
        )
Example #9
0
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])
Example #10
0
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': 'дд.мм.гггг',
                }),
        )
Example #11
0
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()))
Example #12
0
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
Example #13
0
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()
Example #14
0
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)
Example #15
0
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
Example #16
0
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)
Example #17
0
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)
Example #18
0
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')