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 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 ScoreByPlaceAdditionalScorer(AbstractAdditionalScorer): """ Additional scores to first solved task teams """ place = models.PositiveIntegerField( help_text='I.e. 1 for team who first solved the task') points = models.IntegerField() def __str__(self): return '%d additional points for %d team' % (self.points, self.place)
class Document(models.Model): name = models.CharField( max_length=255, help_text='Не показывается школьникам. Например, «Договор 2016 с ' 'Берендеевыми Полянами»', ) page_size = models.CharField(max_length=20, choices=PageSize.choices, validators=[PageSize.validator]) left_margin = models.IntegerField(default=0) right_margin = models.IntegerField(default=0) top_margin = models.IntegerField(default=0) bottom_margin = models.IntegerField(default=0) def __str__(self): return self.name
class EntranceLevel(models.Model): """ Уровень вступительной работы. Для каждой задачи могут быть указаны уровни, для которых она предназначена. Уровень школьника определяется с помощью EntranceLevelLimiter'ов (например, на основе тематической анкеты из модуля topics, класса в школе или учёбы в других параллелях в прошлые годы) """ school = models.ForeignKey('schools.School', on_delete=models.CASCADE) short_name = models.CharField( max_length=100, help_text='Используется в урлах. ' 'Лучше обойтись латинскими буквами, цифрами и подчёркиванием') name = models.CharField(max_length=100) order = models.IntegerField(default=0) tasks = models.ManyToManyField( 'EntranceExamTask', blank=True, related_name='entrance_levels', ) def __str__(self): return 'Уровень «%s» для %s' % (self.name, self.school) def __gt__(self, other): return self.order > other.order def __lt__(self, other): return self.order < other.order def __ge__(self, other): return self.order >= other.order def __le__(self, other): return self.order <= other.order class Meta: ordering = ('school_id', 'order')
class AbstractQuestionnaireBlock(polymorphic.models.PolymorphicModel): questionnaire = models.ForeignKey('Questionnaire', on_delete=models.CASCADE) short_name = models.CharField( max_length=100, help_text='Используется в урлах. Лучше обойтись латинскими буквами, ' 'цифрами и подчёркиванием') order = models.IntegerField( default=0, help_text='Блоки выстраиваются по возрастанию порядка') is_question = False def __str__(self): return '%s. %s' % (self.questionnaire, self.short_name) class Meta: verbose_name = 'questionnaire block' unique_together = [('short_name', 'questionnaire'), ('questionnaire', 'order')] ordering = ('questionnaire_id', 'order')
class AbstractQuestionnaireBlock(polymorphic.models.PolymorphicModel): questionnaire = models.ForeignKey( 'Questionnaire', on_delete=models.CASCADE, related_name='blocks', ) short_name = models.CharField( max_length=100, help_text='Используется в урлах. Лучше обойтись латинскими буквами, ' 'цифрами и подчёркиванием') order = models.IntegerField( default=0, help_text='Блоки выстраиваются по возрастанию порядка') # TODO (andgein): it may be better to make another model with storing # top-level blocks of questionnaire (and orders of them) is_top_level = models.BooleanField( help_text='True, если блок находится на верхнем уровне вложенности', default=True, ) is_question = False class Meta: verbose_name = 'questionnaire block' unique_together = [('short_name', 'questionnaire'), ('questionnaire', 'order')] ordering = ('questionnaire_id', 'order') def __str__(self): obj = self.get_real_instance() if hasattr(obj, 'text'): description = '%s (%s)' % (obj.text, self.short_name) else: description = self.short_name return '%s. %s' % (self.questionnaire, description) def copy_to_questionnaire(self, to_questionnaire): """ Make a copy of this block in the specified questionnaire. In order for this method to work correctly for each particular subclass the subclass should override _copy_fields_to_instance. :param to_questionnaire: A questionnaire to copy the block to. """ fields_diff = (set(self.__class__._meta.get_fields()) - set(AbstractQuestionnaireBlock._meta.get_fields())) fields_to_copy = list( filter( lambda f: not f.auto_created and not f.is_relation, fields_diff, )) field_kwargs = {f.name: getattr(self, f.name) for f in fields_to_copy} new_instance = self.__class__( questionnaire=to_questionnaire, short_name=self.short_name, order=self.order, **field_kwargs, ) self._copy_fields_to_instance(new_instance) new_instance.save() return new_instance def is_visible_to_user(self, user): """ Return true if block is visible to the user. Includes only server-side conditions, which don't change on client side during editing. For now there is only one server-side show condition - `GroupMemberShowCondition` :param user: User :return: True if block is visible, False otherwise """ group_member_conditions = ( self.show_conditions_questionnaireblockgroupmembershowcondition. all()) if not group_member_conditions.exists(): return True for condition in group_member_conditions: if condition.is_satisfied(user): return True return False def copy_dependencies_to_instance(self, other_block): """ Copies dependencies between blocks. This method is called when all blocks are copied :param other_block: Block from other questionnaire where dependencies should be copied Override this method in a subclass to copy any objects having a reference to this block and other blocks. The override must: - call super().copy_dependencies_to_instance(other), - make copies of the dependencies for the passed instance. """ pass def _copy_fields_to_instance(self, other): """ Subclasses must override this method if they define new relation fields or if some of their plain fields require non-trivial copying. The implementation should: - call super()._copy_fields_to_instance(other), - copy its field values to the passed instance. :param other: The instance to copy field values to. """ pass @property def block_name(self): """ Name of template file in `templates/questionnaire/blocks/`. Also part of css class for this type's blocks (i.e. `questionnaire__block__markdown` for MarkdownQuestionnaireBlock) """ raise NotImplementedError("%s doesn't implement block_name property " % self.__class__.__name__) @cached_property def show_conditions(self): return (list( self.show_conditions_questionnaireblockgroupmembershowcondition. all()) + list( self. show_conditions_questionnaireblockvariantcheckedshowcondition. all()))
class 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')
class EntranceExamTask(polymorphic.models.PolymorphicModel): title = models.CharField(max_length=100, help_text='Название') text = models.TextField(help_text='Формулировка задания') exam = models.ForeignKey( 'EntranceExam', on_delete=models.CASCADE, related_name='%(class)s', ) category = models.ForeignKey( 'EntranceExamTaskCategory', on_delete=models.CASCADE, verbose_name='категория', related_name='tasks', ) help_text = models.CharField( max_length=100, help_text='Дополнительная информация, например, сведения о формате ' 'ответа', blank=True) order = models.IntegerField( help_text='Задачи выстраиваются по возрастанию порядка', default=0) max_score = models.PositiveIntegerField() custom_description = models.TextField( help_text='Текст с описанием типа задачи. Оставьте пустым, тогда будет ' 'использован текст по умолчанию для данного вида задач. ' 'В этом тексте можно указать, например, ' 'для кого эта задача предназначена.\n' 'Поддерживается Markdown', blank=True, ) def __str__(self): return "{}: {}".format(self.exam.school.name, self.title) def save(self, *args, **kwargs): if self.category.exam_id != self.exam_id: raise IntegrityError( "{}.{}: task's category should belong to the same exam as the " "task itself".format(self.__module__, self.__class__.__name__)) super().save(*args, **kwargs) def is_accepted_for_user(self, user): # Always not accepted by default. Override when subclassing. return False def is_solved_by_user(self, user): # Always not solved by default. Override when subclassing. return False @property def template_file(self): """ Return template file name in folder templates/entrance/exam/ """ raise NotImplementedError('Child should define property template_file') @property def type_title(self): """ Return title of blocks with these tasks """ raise NotImplementedError('Child should define property type_title') def get_form_for_user(self, user, *args, **kwargs): """ Return form for this task for the specified user """ raise NotImplementedError('Child should define get_form_for_user()') @property def solution_class(self): raise NotImplementedError( 'Child should define property solution_class')
class EntranceExamTaskCategory(models.Model): """ Tasks are displayed in these categories on the exam page. """ exam = models.ForeignKey( 'EntranceExam', on_delete=models.CASCADE, verbose_name="экзамен", related_name='task_categories', ) short_name = models.SlugField( help_text="Может состоять только из букв, цифр, знака подчеркивания и " "дефиса.", ) title = models.CharField( verbose_name="заголовок", max_length=100, help_text="Заголовок категории, например «Практические задачи:»", ) order = models.IntegerField( verbose_name="порядок", help_text="Категории задач отображаются в заданном порядке", ) is_mandatory = models.BooleanField( verbose_name="обязательная категория", default=True, help_text="Обязательно ли решать задачи в этой категории, чтобы " "вступительная считалась выполненной?", ) available_from_time = models.ForeignKey( 'dates.KeyDate', on_delete=models.CASCADE, verbose_name="доступна c", related_name='+', null=True, blank=True, default=None, help_text="Момент времени, начиная с которого задачи этой категории " "показываются пользователям. Оставьте пустым, если задачи " "должны быть доступны с начала вступительной работы.", ) available_to_time = models.ForeignKey( 'dates.KeyDate', on_delete=models.CASCADE, verbose_name="доступна до", related_name='+', null=True, blank=True, default=None, help_text="Момент времени, после которого возможность послать решения " "по задачам этой категории будет закрыта. Оставьте пустым, " "если задачи должны быть доступны до конца вступительной " "работы.", ) text_after_closing = models.TextField( blank=True, verbose_name="текст после закрытия", help_text="Текст, который показывается на странице задачи после " "закрытия задач этой категории, но до конца вступительной " "работы.\n" "Поддерживается Markdown.", ) class Meta: unique_together = [('exam', 'short_name'), ('exam', 'order')] verbose_name = _('task category') verbose_name_plural = _('task categories') def __str__(self): return 'Категория задач «{}» для «{}»'.format(self.title, self.exam) def is_started_for_user(self, user): if self.available_from_time is None: return True return self.available_from_time.passed_for_user(user) def is_finished_for_user(self, user): if self.available_to_time is None: return False return self.available_to_time.passed_for_user(user)