class AbstractParticipant(polymorphic.models.PolymorphicModel, drapo.models.ModelWithTimestamps): contest = models.ForeignKey(Contest, related_name='participants') is_approved = models.BooleanField(default=True) is_disqualified = models.BooleanField(default=False) is_visible_in_scoreboard = models.BooleanField(default=True) region = models.ForeignKey(ContestRegion, null=True, blank=True, related_name='participants') @property def name(self): return self.get_real_instance().name def get_absolute_url(self): return self.get_real_instance().get_absolute_url() def get_current_score(self): correct_attempts = self.attempts.filter(is_correct=True) solved_tasks = correct_attempts.values('task').distinct() return solved_tasks.aggregate(sum=Sum('task__max_score'))['sum'] def __str__(self): return self.name
class EntranceExamTaskSolution(polymorphic.models.PolymorphicModel): task = models.ForeignKey( 'EntranceExamTask', on_delete=models.CASCADE, related_name='solutions', ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='entrance_exam_solutions', ) solution = models.TextField() ip = models.CharField( max_length=50, help_text='IP-адрес, с которого было отправлено решение', default='') created_at = models.DateTimeField( auto_now_add=True, db_index=True, ) def __str__(self): return 'Решение %s по задаче %s' % (self.user, self.task) class Meta: ordering = ['-created_at'] index_together = ('task', 'user')
class EntranceLevelOverride(models.Model): """ If present this level is used instead of dynamically computed one. """ school = models.ForeignKey( 'schools.School', on_delete=models.CASCADE, related_name='+', ) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='entrance_level_overrides', ) entrance_level = models.ForeignKey( 'EntranceLevel', on_delete=models.CASCADE, related_name='overrides', ) class Meta: unique_together = ('school', 'user') def __str__(self): return 'Уровень {} для {}'.format(self.entrance_level, self.user) def save(self, *args, **kwargs): if self.school != self.entrance_level.school: raise IntegrityError( 'Entrance level override should belong to the same school as ' 'its entrance level') super().save(*args, **kwargs)
class UserQuestionnaireStatus(models.Model): class Status(djchoices.DjangoChoices): NOT_FILLED = djchoices.ChoiceItem(1) FILLED = djchoices.ChoiceItem(2) user = models.ForeignKey( users.models.User, on_delete=models.CASCADE, related_name='+', ) questionnaire = models.ForeignKey( Questionnaire, on_delete=models.CASCADE, related_name='statuses', ) status = models.PositiveIntegerField( choices=Status.choices, validators=[Status.validator], ) class Meta: verbose_name_plural = 'user questionnaire statuses' unique_together = ('user', 'questionnaire') def __str__(self): return 'Status {} of {} for {}'.format(self.status, self.questionnaire, self.user)
class QuestionnaireAnswer(models.Model): questionnaire = models.ForeignKey( Questionnaire, on_delete=models.CASCADE, related_name='answers', ) user = models.ForeignKey( users.models.User, on_delete=models.CASCADE, related_name='questionnaire_answers', ) # TODO: may be ForeignKey is better? question_short_name = models.CharField(max_length=100) answer = models.TextField(blank=True) def __str__(self): return 'Ответ «%s» на вопрос %s анкеты %s' % ( self.answer.replace('\n', '\\n'), self.question_short_name, self.questionnaire, ) @property def question(self): return AbstractQuestionnaireQuestion.objects.filter( questionnaire=self.questionnaire, short_name=self.question_short_name).first() class Meta: index_together = ('questionnaire', 'user', 'question_short_name')
class TableCell(models.Model, ProcessedByVisitor): row = models.ForeignKey( TableRow, on_delete=models.CASCADE, related_name='cells', ) block = models.ForeignKey( AbstractDocumentBlock, on_delete=models.CASCADE, related_name='+', ) order = models.PositiveIntegerField( help_text='Ячейки упорядочиваются по возрастанию порядка') class Meta: ordering = ('row', 'order') unique_together = ('row', 'order') def __str__(self): return '%s[%d]: %s' % (self.row, self.order, self.block) def get_reportlab_block(self, visitor): self._process_by_visitor(visitor) return self.block.get_reportlab_block(visitor)
class EntranceLevelUpgrade(models.Model): user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) upgraded_to = models.ForeignKey( 'EntranceLevel', on_delete=models.CASCADE, related_name='+', ) created_at = models.DateTimeField(auto_now_add=True)
class InlineQuestionnaireBlockChild(models.Model): parent = models.ForeignKey( InlineQuestionnaireBlock, related_name='children', on_delete=models.CASCADE, ) block = models.ForeignKey( # TODO (andgein): Maybe it should be foreign key # to AbstractQuestionnaireBlock, not to Question? # In future some blocks may nice fit in InlineQuestionnaireBlock. 'AbstractQuestionnaireQuestion', related_name='+', on_delete=models.CASCADE, help_text='Вопрос, вложенный в InlineBlock') xs_width = models.IntegerField(validators=[ validators.MinValueValidator(1), validators.MaxValueValidator(12) ], help_text='Размер на телефонах. От 1 до 12', default=12) sm_width = models.IntegerField( validators=[ validators.MinValueValidator(1), validators.MaxValueValidator(12) ], help_text= 'Размер на устройствах шириной от 768 до 992 пикселей. От 1 до 12', default=12) md_width = models.IntegerField( validators=[ validators.MinValueValidator(1), validators.MaxValueValidator(12) ], help_text='Размер на остальных устройствах. От 1 до 12', default=6) def __str__(self): return str(self.block) def save(self, *args, **kwargs): if self.parent.questionnaire_id != self.block.questionnaire_id: raise IntegrityError('Selected questionnaire and block ' 'should belong to one school') super().save(*args, **kwargs)
class GroupAccess(polymorphic.models.PolymorphicModel): class Type(djchoices.DjangoChoices): NONE = djchoices.ChoiceItem( value=0, label='Нет доступа, группа не видна', ) LIST_MEMBERS = djchoices.ChoiceItem( value=10, label='Может просматривать участников', ) EDIT_MEMBERS = djchoices.ChoiceItem( value=20, label='Может добавлять и удалять участников', ) ADMIN = djchoices.ChoiceItem( value=30, label='Полный доступ', ) to_group = models.ForeignKey( AbstractGroup, related_name='accesses', on_delete=models.CASCADE, ) access_type = models.PositiveIntegerField( choices=Type.choices, validators=[Type.validator], db_index=True, ) created_by = models.ForeignKey( users.models.User, related_name='+', on_delete=models.CASCADE, null=True, blank=True, help_text='Кем выдан доступ. Если None, то системой') created_at = models.DateTimeField( auto_now_add=True, db_index=True, ) class Meta: verbose_name = 'group access' verbose_name_plural = 'group accesses'
class UserListQuestionnaireQuestion(AbstractQuestionnaireQuestion): block_name = 'user_list_question' group = models.ForeignKey( 'groups.AbstractGroup', null=True, on_delete=models.CASCADE, related_name='+', help_text="Группа, пользователей которой можно выбирать", ) placeholder = models.TextField( blank=True, help_text='Подсказка, показываемая в поле для ввода; пример', ) def get_form_field(self, attrs=None): return forms.ChooseUsersFromGroupField( group=self.group, required=self.is_required, disabled=self.is_disabled, label=self.text, help_text=self.help_text, placeholder=self.placeholder, )
class AbstractTableStyleCommand(polymorphic.models.PolymorphicModel): table = models.ForeignKey( Table, on_delete=models.CASCADE, related_name='style_commands', ) start_column = models.IntegerField( default=0, help_text='Координаты начала: столбец', ) start_row = models.IntegerField( default=0, help_text='Координаты начала: строка', ) stop_column = models.IntegerField( default=-1, help_text='Координаты конца: столбец', ) stop_row = models.IntegerField( help_text='Координаты конца: строка', default=-1, ) class Meta: verbose_name = 'table style command' @property def start(self): return self.start_column, self.start_row @property def stop(self): return self.stop_column, self.stop_row @property def params(self): # param can be dotted path to the nested attribute, i.e. "font.name". # In this case we need get self.font.name, so we split param by "." and apply # functools.reduce with getattr return [ functools.reduce(getattr, [self] + param.split('.')) for param in self.command_params ] def __str__(self): return '%s[%s:%s].%s(%s)' % ( self.table, self.start, self.stop, self.command_name, ', '.join(map(str, self.params)), ) def get_reportlab_style_command(self): return (self.command_name, self.start, self.stop) + tuple(self.params)
class QuestionnaireBlockShowCondition(models.Model): # If there is at least one conditions for `block`, # it will be visible only if one `need_to_be_checked` is checked block = models.ForeignKey( AbstractQuestionnaireBlock, on_delete=models.CASCADE, related_name='show_conditions', ) need_to_be_checked = models.ForeignKey( ChoiceQuestionnaireQuestionVariant, on_delete=models.CASCADE, related_name='+', ) def __str__(self): return 'Show %s only if %s' % (self.block, self.need_to_be_checked)
class FontTableStyleCommand(CellFormattingTableStyleCommand): command_name = 'FONT' command_params = ['font.name', 'size'] font = models.ForeignKey(Font, on_delete=models.CASCADE) size = models.PositiveIntegerField(null=True, default=None, blank=True)
class QuestionnaireTypingDynamics(models.Model): user = models.ForeignKey( users.models.User, on_delete=models.CASCADE, related_name='+', ) questionnaire = models.ForeignKey( Questionnaire, on_delete=models.CASCADE, related_name='+', ) typing_data = sistema.models.CompressedTextField( help_text='JSON с данными о нажатиях клавиш') created_at = models.DateTimeField(auto_now_add=True)
class ParagraphStyle(models.Model): name = models.CharField(max_length=100) leading = models.FloatField() alignment = models.PositiveIntegerField( choices=Alignment.choices, validators=[Alignment.validator], ) font = models.ForeignKey( Font, on_delete=models.CASCADE, related_name='+', ) font_size = models.PositiveIntegerField() bullet_font = models.ForeignKey( Font, on_delete=models.CASCADE, related_name='+', ) space_before = models.PositiveIntegerField() space_after = models.PositiveIntegerField() left_indent = models.PositiveIntegerField() def __str__(self): return self.name def get_reportlab_style(self): return reportlab.lib.styles.ParagraphStyle( name=self.name, leading=self.leading, fontName=self.font.name, fontSize=self.font_size, bulletFontName=self.bullet_font.name, alignment=self.alignment, spaceBefore=self.space_before, spaceAfter=self.space_after, leftIndent=self.left_indent, )
class GroupInGroupMembership(GroupMembership): member = models.ForeignKey( AbstractGroup, related_name='member_in_groups', on_delete=models.CASCADE, ) def __str__(self): return 'Участники %s входят в %s' % (self.member.name, self.group.name)
class GroupAccessForGroup(GroupAccess): group = models.ForeignKey( AbstractGroup, related_name='+', on_delete=models.CASCADE, ) def __str__(self): return 'Доступ участников %s к %s' % (self.group, self.to_group)
class GroupAccessForUser(GroupAccess): user = models.ForeignKey( users.models.User, related_name='+', on_delete=models.CASCADE, ) def __str__(self): return 'Доступ %s к %s' % (self.user.get_full_name(), self.to_group)
class UserInGroupMembership(GroupMembership): member = models.ForeignKey( users.models.User, related_name='member_in_groups', on_delete=models.CASCADE, ) def __str__(self): return '%s в %s' % (self.member, self.group.name)
class News(ModelWithTimestamps): contest = models.ForeignKey(Contest, related_name='news') author = models.ForeignKey(users.models.User, related_name='+') title = models.CharField(max_length=1000, help_text='Title') text = models.TextField(help_text='Supports markdown') is_published = models.BooleanField(default=False) publish_time = models.DateTimeField(auto_now_add=True) class Meta: verbose_name_plural = 'News' def get_absolute_url(self): return urlresolvers.reverse('contests:news', args=[self.contest_id, self.id])
class SolveTaskEntranceLevelUpgradeRequirement(EntranceLevelUpgradeRequirement ): task = models.ForeignKey( 'EntranceExamTask', on_delete=models.CASCADE, related_name='+', ) def is_met_by_user(self, user): return self.task.is_solved_by_user(user)
class AbstractAbsenceReason(polymorphic.models.PolymorphicModel): school = models.ForeignKey('schools.School', on_delete=models.CASCADE, related_name='absence_reasons') user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='absence_reasons', ) private_comment = models.TextField(blank=True, help_text='Не показывается школьнику') public_comment = models.TextField(blank=True, help_text='Показывается школьнику') created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='+', null=True, default=None, blank=True, ) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = 'Absence reason' @classmethod def for_user_in_school(cls, user, school): """ Returns absence reason for specified user or None if user has not declined. """ return cls.objects.filter(user=user, school=school).first() def default_public_comment(self): raise NotImplementedError()
class TeamParticipant(AbstractParticipant): team = models.ForeignKey(teams.models.Team, related_name='participant_in') def __str__(self): return str(self.team) @property def name(self): return self.team.name def get_absolute_url(self): return self.team.get_absolute_url()
class EntranceLevelUpgradeRequirement(polymorphic.models.PolymorphicModel): base_level = models.ForeignKey( 'EntranceLevel', on_delete=models.CASCADE, related_name='+', ) created_at = models.DateTimeField(auto_now_add=True) def is_met_by_user(self, user): # Always met by default. Override when subclassing. return True
class QuestionnaireBlockGroupMemberShowCondition( AbstractQuestionnaireBlockShowCondition): # TODO: Maybe it will be a good idea to extract ServerSideShowCondition as # a polymorphic parent. group = models.ForeignKey( 'groups.AbstractGroup', on_delete=models.CASCADE, related_name='+', help_text='Группа, участником которой должен быть пользователь') def is_satisfied(self, user): return self.group.is_user_in_group(user) def __str__(self): return 'Show %s only if user is a member of %s' % (self.block, self.group) def copy_condition_to_questionnaire(self, to_questionnaire): """ Copy block show condition to the specified questionnaire. Shouldn't be used outside questionnaire.models. Target questionnaire should have a block with the same `short_name`. Otherwise the copy is not created. Also if `group` is school-related group then target questionnaire's school should have a group with the same `short_name`. Otherwise the copy is not created. :param to_questionnaire: The questionnaire to copy the condition to. :return: The new `QuestionnaireBlockGroupMemberShowCondition` on success or `None` on failure. """ if self.group.school is None: target_group = self.group else: if to_questionnaire.school is None: return None target_group = groups.models.AbstractGroup.objects.filter( school=to_questionnaire.school, short_name=self.group.short_name).first() if target_group is None: return None target_block = (to_questionnaire.blocks.filter( short_name=self.block.short_name).first()) if target_block is None: return None return self.__class__.objects.create(block=target_block, group=target_group)
class IndividualParticipant(AbstractParticipant): user = models.ForeignKey(users.models.User, related_name='individual_participant_in') def __str__(self): return str(self.user) @property def name(self): return self.user.get_full_name() def get_absolute_url(self): return self.user.get_absolute_url()
class GroupMembership(models.Model): group = models.ForeignKey( ManuallyFilledGroup, related_name='+', on_delete=models.CASCADE, ) added_by = models.ForeignKey( users.models.User, null=True, blank=True, related_name='+', on_delete=models.CASCADE, help_text='Кем добавлен участник группы. None, если добавлено системой.' ) created_at = models.DateTimeField( auto_now_add=True, db_index=True, ) class Meta: abstract = True
class Paragraph(AbstractDocumentBlock): text = models.TextField() style = models.ForeignKey(ParagraphStyle, on_delete=models.CASCADE) bulletText = models.TextField(default=None, null=True, blank=True) def __str__(self): return self.text[:80] def get_reportlab_block(self, visitor=None): self._process_by_visitor(visitor) return reportlab.platypus.Paragraph(self.text, self.style.get_reportlab_style(), self.bulletText)
class AbstractParticipant(polymorphic.models.PolymorphicModel, drapo.models.ModelWithTimestamps): contest = models.ForeignKey(Contest, related_name='participants') is_approved = models.BooleanField(default=True) is_disqualified = models.BooleanField(default=False) is_visible_in_scoreboard = models.BooleanField(default=True) @property def name(self): return self.get_real_instance().name def get_absolute_url(self): return self.get_real_instance().get_absolute_url()
class ContestRegion(models.Model): contest = models.ForeignKey(Contest, related_name='regions') name = models.TextField(help_text='Region name') start_time = models.DateTimeField( help_text='Contest start time for this region') finish_time = models.DateTimeField( help_text='Contest finish time for this region') timezone = models.TextField(default='UTC', help_text='Timezone for the region') def __str__(self): return self.name