Source code for oioioi.problems.models

import logging
import os.path
from contextlib import contextmanager
from traceback import format_exception

from django.conf import settings
from django.contrib.auth.models import User
from django.core import validators
from django.core.files.base import ContentFile
from django.core.validators import validate_slug
from django.db import models, transaction
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver
from django.utils import timezone
from django.utils.encoding import force_str
from django.utils.functional import cached_property
from django.utils.module_loading import import_string
from django.utils.text import get_valid_filename
from django.utils.translation import get_language, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from oioioi.base.fields import DottedNameField, EnumField, EnumRegistry
from oioioi.base.utils import split_extension, strip_num_or_hash
from oioioi.contests.models import ProblemInstance
from oioioi.filetracker.fields import FileField
from oioioi.problems.validators import validate_origintag
from unidecode import unidecode

[docs]logger = logging.getLogger(__name__)
[docs]def make_problem_filename(instance, filename): if not isinstance(instance, Problem): try: instance = instance.problem except AttributeError: assert hasattr(instance, 'problem'), ( 'problem_file_generator used ' 'on object %r which does not have \'problem\' attribute' % (instance,) ) return 'problems/%d/%s' % ( instance.id, get_valid_filename(os.path.basename(filename)), )
[docs]class Problem(models.Model): """Represents a problem in the problems database. Instances of :class:`Problem` do not represent problems in contests, see :class:`oioioi.contests.models.ProblemInstance` for those. Each :class:`Problem` has associated main :class:`oioioi.contests.models.ProblemInstance`, called main_problem_instance: 1) It is not assigned to any contest. 2) It allows sending submissions aside from contests. 3) It is a base to create another instances. """
[docs] legacy_name = models.CharField(max_length=255, verbose_name=_("legacy name"))
[docs] short_name = models.CharField( max_length=30, validators=[validate_slug], verbose_name=_("short name") )
[docs] controller_name = DottedNameField( 'oioioi.problems.controllers.ProblemController', verbose_name=_("type") )
[docs] contest = models.ForeignKey( 'contests.Contest', null=True, blank=True, verbose_name=_("contest"), on_delete=models.SET_NULL, )
[docs] author = models.ForeignKey( User, null=True, blank=True, verbose_name=_("author"), on_delete=models.SET_NULL )
# visibility defines read access to all of problem data (this includes # the package, all tests and attachments)
[docs] VISIBILITY_PUBLIC = 'PU'
[docs] VISIBILITY_FRIENDS = 'FR'
[docs] VISIBILITY_PRIVATE = 'PR'
[docs] VISIBILITY_LEVELS_CHOICES = [ (VISIBILITY_PUBLIC, 'Public'), (VISIBILITY_FRIENDS, 'Friends'), (VISIBILITY_PRIVATE, 'Private'), ]
[docs] visibility = models.CharField( max_length=2, verbose_name=_("visibility"), choices=VISIBILITY_LEVELS_CHOICES, default=VISIBILITY_FRIENDS, )
[docs] package_backend_name = DottedNameField( 'oioioi.problems.package.ProblemPackageBackend', null=True, blank=True, verbose_name=_("package type"), )
[docs] ascii_name = models.CharField( max_length=255, null=True ) # autofield, no polish characters
# main_problem_instance: # null=True, because there is a cyclic dependency # and during creation of any Problem, main_problem_instance # must be temporarily set to Null # (ProblemInstance has ForeignKey to Problem # and Problem has ForeignKey to ProblemInstance)
[docs] main_problem_instance = models.ForeignKey( 'contests.ProblemInstance', null=True, blank=False, verbose_name=_("main problem instance"), related_name='main_problem_instance', on_delete=models.CASCADE, )
@cached_property
[docs] def name(self): problem_name = ProblemName.objects.filter( problem=self, language=get_language() ).first() return problem_name.name if problem_name else self.legacy_name
@property
[docs] def controller(self): return import_string(self.controller_name)(self)
@property
[docs] def package_backend(self): return import_string(self.package_backend_name)()
@classmethod
[docs] def create(cls, *args, **kwargs): """Creates a new :class:`Problem` object, with associated main_problem_instance. After the call, the :class:`Problem` and the :class:`ProblemInstance` objects will be saved in the database. """ problem = cls(*args, **kwargs) problem.save() import oioioi.contests.models pi = oioioi.contests.models.ProblemInstance(problem=problem) pi.save() pi.short_name += "_main" pi.save() problem.main_problem_instance = pi problem.save() return problem
[docs] class Meta(object):
[docs] verbose_name = _("problem")
[docs] verbose_name_plural = _("problems")
[docs] permissions = ( ('problems_db_admin', _("Can administer the problems database")), ('problem_admin', _("Can administer the problem")), )
[docs] def __str__(self): return u'%(name)s (%(short_name)s)' % { u'short_name': self.short_name, u'name': self.name, }
[docs] def save(self, *args, **kwargs): self.ascii_name = unidecode(str(self.name)) super(Problem, self).save(*args, **kwargs)
@receiver(post_save, sender=Problem)
[docs]def _call_controller_adjust_problem(sender, instance, raw, **kwargs): if not raw and instance.controller_name: instance.controller.adjust_problem()
@receiver(pre_delete, sender=Problem)
[docs]def _check_problem_instance_integrity(sender, instance, **kwargs): from oioioi.contests.models import ProblemInstance pis = ProblemInstance.objects.filter(problem=instance, contest__isnull=True) if pis.count() > 1: raise RuntimeError( "Multiple main_problem_instance objects for a single problem." )
[docs]class ProblemName(models.Model): """Represents a problem's name translation in a given language. Problem should have its name translated to all available languages. """
[docs] problem = models.ForeignKey(Problem, related_name='names', on_delete=models.CASCADE)
[docs] name = models.CharField( max_length=255, verbose_name=_("name translation"), help_text=_("Human-readable name."), )
[docs] language = models.CharField( max_length=2, choices=settings.LANGUAGES, verbose_name=_("language code") )
[docs] class Meta(object):
[docs] unique_together = ('problem', 'language')
[docs] verbose_name = _("problem name")
[docs] verbose_name_plural = _("problem names")
[docs] def __str__(self): return str("{} - {}".format(self.problem, self.language))
[docs]class ProblemStatement(models.Model): """Represents a file containing problem statement. Problem may have multiple statements, for example in various languages or formats. Formats should be detected according to filename extension of :attr:`content`. """
[docs] problem = models.ForeignKey( Problem, related_name='statements', on_delete=models.CASCADE )
[docs] language = models.CharField( max_length=6, blank=True, null=True, verbose_name=_("language code") )
[docs] content = FileField(upload_to=make_problem_filename, verbose_name=_("content"))
@property
[docs] def filename(self): return os.path.split(self.content.name)[1]
@property
[docs] def download_name(self): return self.problem.short_name + self.extension
@property
[docs] def extension(self): return os.path.splitext(self.content.name)[1].lower()
[docs] class Meta(object):
[docs] verbose_name = _("problem statement")
[docs] verbose_name_plural = _("problem statements")
[docs] def __str__(self): return u'%s / %s' % (self.problem.name, self.filename)
[docs]class ProblemAttachment(models.Model): """Represents an additional file visible to the contestant, linked to a problem. This may be used for things like input data for open data tasks, or for giving users additional libraries etc. """
[docs] problem = models.ForeignKey( Problem, related_name='attachments', on_delete=models.CASCADE )
[docs] description = models.CharField(max_length=255, verbose_name=_("description"))
[docs] content = FileField(upload_to=make_problem_filename, verbose_name=_("content"))
@property
[docs] def filename(self): return os.path.split(self.content.name)[1]
@property
[docs] def download_name(self): return strip_num_or_hash(self.filename)
[docs] class Meta(object):
[docs] verbose_name = _("attachment")
[docs] verbose_name_plural = _("attachments")
[docs] def __str__(self): return u'%s / %s' % (self.problem.name, self.filename)
[docs]def _make_package_filename(instance, filename): if instance.contest: contest_name = instance.contest.id else: contest_name = 'no_contest' return 'package/%s/%s' % ( contest_name, get_valid_filename(os.path.basename(filename)), )
[docs]package_statuses = EnumRegistry()
package_statuses.register('?', pgettext_lazy("Pending", "Pending problem package")) package_statuses.register('OK', _("Uploaded")) package_statuses.register('ERR', _("Error"))
[docs]TRACEBACK_STACK_LIMIT = 100
[docs]def truncate_unicode(string, length, encoding='utf-8'): """ Truncates string to be `length` bytes long. """ encoded = string.encode(encoding)[:length] return encoded.decode(encoding, 'ignore')
[docs]class ProblemPackage(models.Model): """Represents a file with data necessary for creating a :class:`~oioioi.problems.models.Problem` instance. """
[docs] package_file = FileField( upload_to=_make_package_filename, verbose_name=_("package") )
[docs] contest = models.ForeignKey( 'contests.Contest', null=True, blank=True, verbose_name=_("contest"), on_delete=models.SET_NULL, )
[docs] problem = models.ForeignKey( Problem, verbose_name=_("problem"), null=True, blank=True, on_delete=models.CASCADE, )
[docs] created_by = models.ForeignKey( User, verbose_name=_("created by"), null=True, blank=True, on_delete=models.SET_NULL, )
[docs] problem_name = models.CharField( max_length=30, validators=[validate_slug], verbose_name=_("problem name"), null=True, blank=True, )
[docs] celery_task_id = models.CharField(max_length=50, unique=True, null=True, blank=True)
[docs] info = models.CharField( max_length=1000, null=True, blank=True, verbose_name=_("Package information") )
[docs] traceback = FileField( upload_to=_make_package_filename, verbose_name=_("traceback"), null=True, blank=True, )
[docs] status = EnumField(package_statuses, default='?', verbose_name=_("status"))
[docs] creation_date = models.DateTimeField( default=timezone.now, verbose_name=_("creation date") )
@property
[docs] def download_name(self): ext = split_extension(self.package_file.name)[1] if self.problem: return self.problem.short_name + ext else: filename = os.path.split(self.package_file.name)[1] return strip_num_or_hash(filename)
[docs] class Meta(object):
[docs] verbose_name = _("problem package")
[docs] verbose_name_plural = _("problem packages")
[docs] ordering = ['-creation_date']
[docs] class StatusSaver(object): def __init__(self, package): self.package_id = package.id
[docs] def __enter__(self): pass
[docs] def __exit__(self, type, value, traceback): package = ProblemPackage.objects.get(id=self.package_id) if type: package.status = 'ERR' try: # This will work if a PackageProcessingError was thrown info = _( u"Failed operation: %(name)s\n" u"Operation description: %(desc)s\n \n" u"Error description: %(error)r\n \n" % dict( name=value.raiser, desc=value.raiser_desc, error=value.original_exception_info[1], ) ) type, value, _old_traceback = value.original_exception_info except AttributeError: info = _( u"Failed operation unknown.\n" u"Error description: %(error)s\n \n" % dict(error=value) ) # Truncate error so it doesn't take up whole page in list # view (or much space in the database). # Full info is available in package.traceback anyway. package.info = truncate_unicode( info, ProblemPackage._meta.get_field('info').max_length ) package.traceback = ContentFile( force_str(info) + ''.join( format_exception(type, value, traceback, TRACEBACK_STACK_LIMIT) ), 'traceback.txt', ) logger.exception( "Error processing package %s", package.package_file.name, extra={'omit_sentry': True}, ) else: package.status = 'OK' package.celery_task_id = None package.save() return True
[docs] def save_operation_status(self): """Returns a context manager to be used during the unpacking process. The code inside the ``with`` statment is executed in a transaction. If the code inside the ``with`` statement executes successfully, the package ``status`` field is set to ``OK``. If an exception is thrown, it gets logged together with the traceback. Additionally, its value is saved in the package ``info`` field. Lastly, if the package gets deleted from the database before the ``with`` statement ends, a :class:`oioioi.problems.models.ProblemPackage.DoesNotExist` exception is thrown. """ @contextmanager def manager(): with self.StatusSaver(self), transaction.atomic(): yield None return manager()
[docs]class ProblemSite(models.Model): """Represents a global problem site. Contains configuration necessary to view and submit solutions to a :class:`~oioioi.problems.models.Problem`. """
[docs] problem = models.OneToOneField(Problem, on_delete=models.CASCADE)
[docs] url_key = models.CharField(max_length=40, unique=True)
[docs] def __str__(self): return str(self.problem)
[docs] class Meta(object):
[docs] verbose_name = _("problem site")
[docs] verbose_name_plural = _("problem sites")
[docs]class MainProblemInstance(ProblemInstance):
[docs] class Meta(object):
[docs] proxy = True
[docs]class ProblemStatistics(models.Model):
[docs] problem = models.OneToOneField( Problem, on_delete=models.CASCADE, related_name='statistics' )
[docs] user_statistics = models.ManyToManyField(User, through='UserStatistics')
[docs] submitted = models.IntegerField(default=0, verbose_name=_("attempted solutions"))
[docs] solved = models.IntegerField(default=0, verbose_name=_("correct solutions"))
[docs] avg_best_score = models.IntegerField(default=0, verbose_name=_("average result"))
[docs] _best_score_sum = models.IntegerField(default=0)
[docs]class UserStatistics(models.Model):
[docs] problem_statistics = models.ForeignKey(ProblemStatistics, on_delete=models.CASCADE)
[docs] user = models.ForeignKey(User, on_delete=models.CASCADE)
[docs] has_submitted = models.BooleanField(default=False, verbose_name=_("user submitted"))
[docs] has_solved = models.BooleanField(default=False, verbose_name=_("user solved"))
[docs] best_score = models.IntegerField(default=0, verbose_name=_("user's best score"))
[docs] class Meta(object):
[docs] unique_together = ('problem_statistics', 'user')
[docs]def _localized(*localized_fields): """Some models may have fields with language-specific data, which cannot be translated through the normal internalization tools, as it is not defined in the source code (e.g. names of dynamically defined items). Decorate a class with this decorator when there exists a class that: - has a ForeignKey to the decorated class with a related_name of `localizations`. - has a `language` field, and all of `localized_fields`. The `localized_fields` can then be accessed directly through the decorated class, and will be matched to the current language. Be sure to use prefetch_related('localizations') if you will be querying multiple localized model instances! Also see: LocalizationForm """ def decorator(cls): def localize(self, key): language = get_language() # In case prefetch_related('localizations') was done don't want to # use filter to avoid database queries. If it wasn't - querying one # language vs all languages is not too much of a difference anyway. for localization in self.localizations.all(): if localization.language == language: return getattr(localization, key) return None def __getattr__(self, key): if key in self.localized_fields: return self.localize(key) else: raise AttributeError( "'{}' object has no attribute '{}'".format(cls.__name__, key) ) cls.localized_fields = localized_fields cls.localize = localize cls.__getattr__ = __getattr__ return cls return decorator
@_localized('short_name', 'full_name', 'description')
[docs]class OriginTag(models.Model): """OriginTags are used along with OriginInfoCategories and OriginInfoValue to give information about the problem's origin. OriginTags themselves represent general information about a problem's origin, whereas OriginInfoValues grouped under OriginInfoCategories represent more specific information. A Problem should probably not have more than one OriginTag, and should probably have one OriginInfoValue for each category. See also: OriginInfoCategory, OriginInfoValue """
[docs] name = models.CharField( max_length=20, validators=(validate_origintag,), verbose_name=_("name"), help_text=_( "Short, searchable name consisting only of lowercase letters, numbers, " "and hyphens.<br>" "This should refer to general origin, i.e. a particular contest, " "competition, programming camp, etc.<br>" "This will be displayed verbatim in the Problemset." ), )
[docs] problems = models.ManyToManyField( Problem, blank=True, verbose_name=_("problems"), help_text=_("Selected problems will be tagged with this tag.<br>"), )
[docs] class Meta(object):
[docs] verbose_name = _("origin tag")
[docs] verbose_name_plural = _("origin tags")
[docs] def __str__(self): return str(self.name)
[docs]class OriginTagLocalization(models.Model):
[docs] origin_tag = models.ForeignKey( OriginTag, related_name='localizations', on_delete=models.CASCADE )
[docs] language = models.CharField( max_length=2, choices=settings.LANGUAGES, verbose_name=_("language") )
[docs] full_name = models.CharField( max_length=255, verbose_name=_("full name"), help_text=_( "Full, official name of the contest, competition, programming camp, etc. " "which this tag represents." ), )
[docs] short_name = models.CharField( max_length=32, blank=True, verbose_name=_("abbreviation"), help_text=_("(optional) Official abbreviation of the full name."), )
[docs] description = models.TextField( blank=True, verbose_name=_("description"), help_text=_( "(optional) Longer description which Will be displayed in the " "Task Archive next to the name." ), )
[docs] class Meta(object):
[docs] unique_together = ('origin_tag', 'language')
[docs] verbose_name = _("origin tag localization")
[docs] verbose_name_plural = _("origin tag localizations")
[docs] def __str__(self): return str("{} - {}".format(self.origin_tag, self.language))
@_localized('full_name')
[docs]class OriginInfoCategory(models.Model): """This class represents a category of information, which further specifies what its parent_tag is already telling about the origin. It doesn't do much by itself and is instead used to group OriginInfoValues by category See also: OriginTag, OriginInfoValue """
[docs] parent_tag = models.ForeignKey( OriginTag, related_name='info_categories', on_delete=models.CASCADE, verbose_name=_("parent tag"), help_text=_( "This category will be a possible category of information for problems " "tagged with the selected tag." ), )
[docs] name = models.CharField( max_length=20, validators=(validate_origintag,), verbose_name=_("name"), help_text=_( "Type of information within this category. Short, searchable name " "consisting of only lowercase letters, numbers, and hyphens.<br>" "Examples: 'year', 'edition', 'stage', 'day'." ), )
[docs] order = models.IntegerField( blank=True, null=True, verbose_name=_("grouping order"), help_text=_( "Sometimes the parent_tag relationship by itself is not enough to convey " "full information about the information hierarchy.<br>" "Some categories are broader, and others are more specific. More specific " "tags should probably be visually grouped after/under broader tags when " "displayed.<br>The broader the category is the lower grouping order it " "should have - e.g. 'year' should have lower order than 'round'.<br>" "Left blank means 'infinity', which usually means that this category will " "not be used for grouping - some categories could be too specific (e.g. " "when grouping would result in 'groups' of single Problems)." ), )
[docs] class Meta(object):
[docs] verbose_name = _("origin tag - information category")
[docs] verbose_name_plural = _("origin tags - information categories")
[docs] unique_together = ('name', 'parent_tag')
[docs] def __str__(self): return str("{}_{}".format(self.parent_tag, self.name))
[docs]class OriginInfoCategoryLocalization(models.Model):
[docs] origin_info_category = models.ForeignKey( OriginInfoCategory, related_name='localizations', on_delete=models.CASCADE )
[docs] language = models.CharField( max_length=2, choices=settings.LANGUAGES, verbose_name=_("language") )
[docs] full_name = models.CharField( max_length=32, verbose_name=_("name translation"), help_text=_("Human-readable name."), )
[docs] class Meta(object):
[docs] unique_together = ('origin_info_category', 'language')
[docs] verbose_name = _("origin info category localization")
[docs] verbose_name_plural = _("origin info category localizations")
[docs] def __str__(self): return str("{} - {}".format(self.origin_info_category, self.language))
@_localized('full_value')
[docs]class OriginInfoValue(models.Model): """This class represents additional information, further specifying what its parent_tag is already telling about the origin. Each OriginInfoValue has a category, in which it should be unique, and problems should only have one OriginInfoValue within any category. See alse: OriginTag, OriginInfoCategory """
[docs] parent_tag = models.ForeignKey( OriginTag, related_name='info_values', on_delete=models.CASCADE, verbose_name=_("parent tag"), help_text=_( "If an OriginTag T is a parent of OriginInfoValue V, the presence of V " "on a Problem implies the presence of T.<br>" "OriginInfoValues with the same values are also treated as distinct if " "they have different parents.<br>" "You can think of this distinction as prepending an OriginTag.name prefix " "to an OriginInfoValue.value<br>" "e.g. for OriginTag 'pa' and OriginInfoValue '2011', this unique " "OriginInfoValue.name would be 'pa_2011'" ), )
[docs] category = models.ForeignKey( OriginInfoCategory, related_name='values', on_delete=models.CASCADE, verbose_name=_("category"), help_text=_( "This information should be categorized under the selected category." ), )
[docs] value = models.CharField( max_length=32, validators=(validate_origintag,), verbose_name=_("value"), help_text=_( "Short, searchable value consisting of only lowercase letters and " "numbers.<br>" "This will be displayed verbatim in the Problemset - it must be unique " "within its parent tag.<br>" "Examples: for year: '2011', but for round: 'r1' (just '1' for round " "would be ambiguous)." ), )
[docs] order = models.IntegerField( default=0, verbose_name=_("display order"), help_text=_("Order in which this value will be sorted within its category."), )
[docs] problems = models.ManyToManyField( Problem, blank=True, verbose_name=_("problems"), help_text=_( "Select problems described by this value. They will also be tagged with " "the parent tag.<br>" ), )
@property
[docs] def name(self): # Should be unique due to unique constraints on value and parent_tag.name return str('{}_{}'.format(self.parent_tag, self.value))
@property
[docs] def full_name(self): return str( u'{} {}'.format(self.parent_tag.full_name, self.full_value) )
[docs] class Meta(object):
[docs] unique_together = ('parent_tag', 'value')
[docs] verbose_name = _("origin tag - information value")
[docs] verbose_name_plural = _("origin tags - information values")
[docs] def __str__(self): return str(self.name)
[docs]class OriginInfoValueLocalization(models.Model):
[docs] origin_info_value = models.ForeignKey( OriginInfoValue, related_name='localizations', on_delete=models.CASCADE )
[docs] language = models.CharField( max_length=2, choices=settings.LANGUAGES, verbose_name=_("language") )
[docs] full_value = models.CharField( max_length=64, verbose_name=_("translated value"), help_text=_("Human-readable value."), )
[docs] class Meta(object):
[docs] unique_together = ('origin_info_value', 'language')
[docs] verbose_name = _("origin info value localization")
[docs] verbose_name_plural = _("origin info value localizations")
[docs] def __str__(self): return str("{} - {}".format(self.origin_info_value, self.language))
@_localized('full_name')
[docs]class DifficultyTag(models.Model):
[docs] name = models.CharField( max_length=20, unique=True, verbose_name=_("name"), null=False, blank=False, validators=[ validators.MinLengthValidator(3), validators.MaxLengthValidator(20), validators.validate_slug, ], )
[docs] problems = models.ManyToManyField(Problem, through='DifficultyTagThrough')
[docs] class Meta(object):
[docs] verbose_name = _("difficulty tag")
[docs] verbose_name_plural = _("difficulty tags")
[docs] def __str__(self): return str(self.name)
[docs]class DifficultyTagThrough(models.Model):
[docs] problem = models.OneToOneField(Problem, on_delete=models.CASCADE)
[docs] tag = models.ForeignKey(DifficultyTag, on_delete=models.CASCADE)
# This string will be visible in an admin form.
[docs] def __str__(self): return str(self.tag.name)
[docs]class DifficultyTagLocalization(models.Model):
[docs] difficulty_tag = models.ForeignKey( DifficultyTag, related_name='localizations', on_delete=models.CASCADE )
[docs] language = models.CharField( max_length=2, choices=settings.LANGUAGES, verbose_name=_("language") )
[docs] full_name = models.CharField( max_length=32, verbose_name=_("name translation"), help_text=_("Human-readable name."), )
[docs] class Meta(object):
[docs] unique_together = ('difficulty_tag', 'language')
[docs] verbose_name = _("difficulty tag localization")
[docs] verbose_name_plural = _("difficulty tag localizations")
[docs] def __str__(self): return str("{} - {}".format(self.difficulty_tag, self.language))
[docs]class DifficultyTagProposal(models.Model):
[docs] problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
[docs] tag = models.ForeignKey(DifficultyTag, on_delete=models.CASCADE, null=True)
[docs] user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
[docs] def __str__(self): return str(self.problem.name) + u' -- ' + str(self.tag.name)
[docs] class Meta(object):
[docs] verbose_name = _("difficulty proposal")
[docs] verbose_name_plural = _("difficulty proposals")
@_localized('full_name')
[docs]class AlgorithmTag(models.Model):
[docs] name = models.CharField( max_length=20, unique=True, verbose_name=_("name"), null=False, blank=False, validators=[ validators.MinLengthValidator(2), validators.MaxLengthValidator(20), validators.validate_slug, ], )
[docs] problems = models.ManyToManyField(Problem, through='AlgorithmTagThrough')
[docs] class Meta(object):
[docs] verbose_name = _("algorithm tag")
[docs] verbose_name_plural = _("algorithm tags")
[docs] def __str__(self): return str(self.name)
[docs]class AlgorithmTagThrough(models.Model):
[docs] problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
[docs] tag = models.ForeignKey(AlgorithmTag, on_delete=models.CASCADE)
# This string will be visible in an admin form.
[docs] def __str__(self): return str(self.tag.name)
[docs] class Meta(object):
[docs] unique_together = ('problem', 'tag')
[docs]class AlgorithmTagLocalization(models.Model):
[docs] algorithm_tag = models.ForeignKey( AlgorithmTag, related_name='localizations', on_delete=models.CASCADE )
[docs] language = models.CharField( max_length=2, choices=settings.LANGUAGES, verbose_name=_("language") )
[docs] full_name = models.CharField( max_length=50, verbose_name=_("name translation"), help_text=_("Human-readable name."), )
[docs] class Meta(object):
[docs] unique_together = ('algorithm_tag', 'language')
[docs] verbose_name = _("algorithm tag localization")
[docs] verbose_name_plural = _("algorithm tag localizations")
[docs] def __str__(self): return str("{} - {}".format(self.algorithm_tag, self.language))
[docs]class AlgorithmTagProposal(models.Model):
[docs] problem = models.ForeignKey(Problem, on_delete=models.CASCADE)
[docs] tag = models.ForeignKey(AlgorithmTag, on_delete=models.CASCADE)
[docs] user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
[docs] def __str__(self): return str(self.problem.name) + u' -- ' + str(self.tag.name)
[docs] class Meta(object):
[docs] verbose_name = _("algorithm tag proposal")
[docs] verbose_name_plural = _("algorithm tag proposals")