Beispiel #1
0
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
Beispiel #2
0
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')
Beispiel #3
0
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()
Beispiel #4
0
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')
Beispiel #5
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)
Beispiel #6
0
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()
Beispiel #7
0
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')
Beispiel #8
0
class ValignTableStyleCommand(CellFormattingTableStyleCommand):
    command_name = 'VALIGN'

    command_params = ['direction']

    direction = models.CharField(max_length=6,
                                 choices=[('TOP', 'Top'), ('MIDDLE', 'Middle'),
                                          ('BOTTOM', 'Bottom')])
Beispiel #9
0
class AlignmentTableStyleCommand(CellFormattingTableStyleCommand):
    command_name = 'ALIGNMENT'

    command_params = 'align'

    align = models.CharField(max_length=20,
                             choices=TableCellAlignment.choices,
                             validators=[TableCellAlignment.validator])
Beispiel #10
0
class TextColorTableStyleCommand(CellFormattingTableStyleCommand):
    command_name = 'TEXTCOLOR'

    command_params = ['color']

    color = models.CharField(max_length=20,
                             choices=Color.choices,
                             validators=[Color.validator])
Beispiel #11
0
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))
Beispiel #12
0
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')
Beispiel #13
0
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
Beispiel #14
0
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, )
Beispiel #15
0
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
Beispiel #16
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,
        )
Beispiel #17
0
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,
        )
Beispiel #18
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])
Beispiel #19
0
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')
Beispiel #20
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
Beispiel #21
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()))
Beispiel #22
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
Beispiel #23
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)
Beispiel #24
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)
Beispiel #25
0
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')
Beispiel #26
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)
Beispiel #27
0
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()