class PrettyFilterMixin(object):

    change_list_template = "admin/change_list_filter_sidebar.html"
    change_list_filter_template = "admin/filter_listing.html" class MediaRemovalAdminMixin(object):
    """
    This mixin overrides default delete_selected Admin action and replaces it
    with one that actually constructs a model instance from each object in the
    queryset and calls .delete() upon that instance.

    This way, we ensure that overridden delete method by MediaRemovalMixin
    is called.
    """

    def get_actions(self, request):
        actions = super(MediaRemovalAdminMixin, self).get_actions(request)

        if 'delete_selected' in actions:
            del actions['delete_selected']

        # Add new delete method only for the superuser, as it is quite dangerous
        if request.user.is_superuser:

            # We use self.__class__ here so that self is not binded, otherwise
            # we would be passing 4 arguments instead of 3 (and self twice)
            new_delete = (self.__class__.delete_selected_with_media,
                          "delete_selected_with_media",
                          "Delete objects with associated media files")

            actions['delete_selected_with_media'] = new_delete

        return actions

    def delete_selected_with_media(self, request, queryset):
        for obj in queryset.all():
            obj.delete() class DownloadMediaFilesMixin(object):

    def get_actions(self, request):
        actions = super(DownloadMediaFilesMixin, self).get_actions(request)

        # We use self.__class__ here so that self is not binded, otherwise
        # we would be passing 4 arguments instead of 3 (and self twice)
        export = (self.__class__.download_media_files,
                  "download_media_files",
                  "Download associated media files")

        actions['download_media_files'] = export
        return actions

    def download_media_files(self, request, queryset):
        export_file = 'export.zip'

        # Open StringIO to grab in-memory ZIP contents
        s = StringIO.StringIO()

        # The zip compressor
        zf = zipfile.ZipFile(s, "w")

        for obj in queryset.all():
            for filepath in obj.get_media_files():
                filepath = os.path.join(obj.media_root, filepath)

                if os.path.exists(filepath):
                    fdir, fname = os.path.split(filepath)
                    zf.write(filepath, fname)
                else:
                    self.message_user(request, '%s has no file saved at %s' %
                                                (obj, filepath))

        # Must close zip for all contents to be written
        zf.close()

        # Grab ZIP file from in-memory, make response with correct MIME-type
        response = HttpResponse(s.getvalue(),
                                content_type="application/x-zip-compressed")
        # ..and correct content-disposition
        response['Content-Disposition'] = (
            'attachment; filename=%s' % export_file
            )

        return response class RestrictedCompetitionAdminMixin(object):
    """
    Extends ModelAdmin in the following ways:
        - makes visible only those objects whose competition_field is one of
          the competitions that request.user organizes
        - makes it possible for the user to assign objects to those competitions
          that he organizes

    Following class attributes can be set to modify the behaviour of this mixin:
        - competition_field: Specifies the field that contains the foreign key
                             to the competition that this object belongs to.
                             Note that this can span several objects through
                             foreign keys, that is, it does not need to be
                             a attribute directly on this model.
    """

    competition_field = 'competition'

    def get_queryset(self, request):
        qs = super(RestrictedCompetitionAdminMixin, self).get_queryset(request)
        if request.user.is_superuser:
            return qs

        result = self.model.objects.none()

        for competition in request.user.userprofile.organized_competitions:
            kwargs = {self.competition_field: competition}
            result = result | qs.filter(**kwargs)

        return result

    def formfield_for_foreignkey(self, db_field, request, **kwargs):

        if not request.user.is_superuser:
            if db_field.name == self.competition_field:
                profile = request.user.userprofile
                kwargs["queryset"] = profile.organized_competitions

        return super(RestrictedCompetitionAdminMixin,
                     self).formfield_for_foreignkey(db_field, request, **kwargs) class ImprovedFilteringVersionAdminMixin(object):
    """
    This class overrides default Django's changelist_view. It should be in the
    top of the inheritance hierarchy, above any class/mixin overriding
    the changelist_view.
    """

    @csrf_protect_m
    def changelist_view(self, request, extra_context=None):
        """
        The 'change list' admin view for this model.
        """
        # Additions by the VersionAdmin
        opts = self.model._meta
        recoverlist_view_name = "%s:%s_%s_recoverlist" % (self.admin_site.name, opts.app_label, opts.module_name)
        addurl_view_name = "%s:%s_%s_add" % (self.admin_site.name, opts.app_label, opts.module_name)
        context = {
            "recoverlist_url": reverse(recoverlist_view_name),
            "add_url": reverse(addurl_view_name),
        }
        context.update(extra_context or {})

        # Original Django code
        from django.contrib.admin.views.main import ERROR_FLAG
        app_label = opts.app_label
        if not self.has_change_permission(request, None):
            raise PermissionDenied

        list_display = self.get_list_display(request)
        list_display_links = self.get_list_display_links(request, list_display)
        list_filter = self.get_list_filter(request)

        # Check actions to see if any are available on this changelist
        actions = self.get_actions(request)
        if actions:
            # Add the action checkboxes if there are any actions available.
            list_display = ['action_checkbox'] + list(list_display)

        ChangeList = self.get_changelist(request)
        try:
            self.cl = ChangeList(request, self.model, list_display,
                list_display_links, list_filter, self.date_hierarchy,
                self.search_fields, self.list_select_related,
                self.list_per_page, self.list_max_show_all, self.list_editable,
                self)
        except IncorrectLookupParameters:
            # Wacky lookup parameters were given, so redirect to the main
            # changelist page, without parameters, and pass an 'invalid=1'
            # parameter via the query string. If wacky parameters were given
            # and the 'invalid=1' parameter was already in the query string,
            # something is screwed up with the database, so display an error
            # page.
            if ERROR_FLAG in request.GET.keys():
                return SimpleTemplateResponse('admin/invalid_setup.html', {
                    'title': _('Database error'),
                })
            return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1')

        # If the request was POSTed, this might be a bulk action or a bulk
        # edit. Try to look up an action or confirmation first, but if this
        # isn't an action the POST will fall through to the bulk edit check,
        # below.
        action_failed = False
        selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME)

        # Actions with no confirmation
        if (actions and request.method == 'POST' and
                'index' in request.POST and '_save' not in request.POST):
            if selected:
                response = self.response_action(request, queryset=self.cl.get_queryset(request))
                if response:
                    return response
                else:
                    action_failed = True
            else:
                msg = _("Items must be selected in order to perform "
                        "actions on them. No items have been changed.")
                self.message_user(request, msg, messages.WARNING)
                action_failed = True

        # Actions with confirmation
        if (actions and request.method == 'POST' and
                helpers.ACTION_CHECKBOX_NAME in request.POST and
                'index' not in request.POST and '_save' not in request.POST):
            if selected:
                response = self.response_action(request, queryset=self.cl.get_queryset(request))
                if response:
                    return response
                else:
                    action_failed = True

        # If we're allowing changelist editing, we need to construct a formset
        # for the changelist given all the fields to be edited. Then we'll
        # use the formset to validate/process POSTed data.
        formset = self.cl.formset = None

        # Handle POSTed bulk-edit data.
        if (request.method == "POST" and self.cl.list_editable and
                '_save' in request.POST and not action_failed):
            FormSet = self.get_changelist_formset(request)
            formset = self.cl.formset = FormSet(request.POST, request.FILES, queryset=self.cl.result_list)
            if formset.is_valid():
                changecount = 0
                for form in formset.forms:
                    if form.has_changed():
                        obj = self.save_form(request, form, change=True)
                        self.save_model(request, obj, form, change=True)
                        self.save_related(request, form, formsets=[], change=True)
                        change_msg = self.construct_change_message(request, form, None)
                        self.log_change(request, obj, change_msg)
                        changecount += 1

                if changecount:
                    if changecount == 1:
                        name = force_text(opts.verbose_name)
                    else:
                        name = force_text(opts.verbose_name_plural)
                    msg = ungettext("%(count)s %(name)s was changed successfully.",
                                    "%(count)s %(name)s were changed successfully.",
                                    changecount) % {'count': changecount,
                                                    'name': name,
                                                    'obj': force_text(obj)}
                    self.message_user(request, msg, messages.SUCCESS)

                return HttpResponseRedirect(request.get_full_path())

        # Handle GET -- construct a formset for display.
        elif self.cl.list_editable:
            FormSet = self.get_changelist_formset(request)
            formset = self.cl.formset = FormSet(queryset=self.cl.result_list)

        # Build the list of media to be used by the formset.
        if formset:
            media = self.media + formset.media
        else:
            media = self.media

        # Build the action form and populate it with available actions.
        if actions:
            action_form = self.action_form(auto_id=None)
            action_form.fields['action'].choices = self.get_action_choices(request)
        else:
            action_form = None

        selection_note_all = ungettext('%(total_count)s selected',
            'All %(total_count)s selected', self.cl.result_count)

        context = {
            'module_name': force_text(opts.verbose_name_plural),
            'selection_note': _('0 of %(cnt)s selected') % {'cnt': len(self.cl.result_list)},
            'selection_note_all': selection_note_all % {'total_count': self.cl.result_count},
            'title': self.cl.title,
            'is_popup': self.cl.is_popup,
            'cl': self.cl,
            'media': media,
            'has_add_permission': self.has_add_permission(request),
            'opts': self.cl.opts,
            'app_label': app_label,
            'action_form': action_form,
            'actions_on_top': self.actions_on_top,
            'actions_on_bottom': self.actions_on_bottom,
            'actions_selection_counter': self.actions_selection_counter,
            'preserved_filters': self.get_preserved_filters(request),
        }
        context.update(extra_context or {})

        return TemplateResponse(request, self.change_list_template or [
            'admin/%s/%s/change_list.html' % (app_label, opts.model_name),
            'admin/%s/change_list.html' % app_label,
            'admin/change_list.html'
        ], context, current_app=self.admin_site.name) def foreign_field_filter_factory(field, title=None, sort=None):
    title = title or (field[:1].upper() + field[1:])
    return type(
        "ForeignFieldFilter_{0}".format(field),
        (ForeignFieldFilter,),
        {
            'field': field,
            'title': title,
            'parameter_name': field,
            'sort_lookups': sort,
            }
    ) def admin_commentable(cls):
    """
    Adds a comments section to the change view,
    """

    def change_view(self, request, object_id, form_url='', extra_context=None):
        try:
            extra_context = extra_context or {}
            extra_context['model_object'] = self.model.objects.get(pk=object_id)
        except self.model.DoesNotExist:
            # If we are not able to get the solution, just do not fail here,
            # change view will deal with it just fine
            pass

        return super(cls, self).change_view(request,
            object_id, form_url, extra_context=extra_context)

    cls.change_form_template = 'admin/change_form_commentable.html'
    cls.change_view = change_view

    return cls def editonly_fieldsets(cls):
    """
    Hides edit-only fieldsets when adding a new object.
    """

    def get_fieldsets(self, request, obj=None):
        if obj and hasattr(cls, 'editonly_fieldsets'):
            if cls.fieldsets:
                return cls.fieldsets + cls.editonly_fieldsets
            else:
                return cls.editonly_fieldsets
        else:
            return cls.fieldsets

    cls.get_fieldsets = get_fieldsets

    return cls class Competition(models.Model):
    """
    Represents a competition. One roots portal will usually hold several
    competitions, as there are usually several age categories or several
    subjects categories. Or both.

    Each competition has its own site assigned.
    """

    name = models.CharField(max_length=100,
                            verbose_name=_('competition name'),
                            unique=True)
    organizer_group = models.ForeignKey('auth.Group',
                                        blank=True,
                                        null=True,
                                        verbose_name=_('organizer group'))

    site = models.ForeignKey(Site, blank=True, null=True)

    # Fields added via foreign keys:

    #  competitionorgregistration_set
    #  competitionuserregistration_set
    #  gallery_set
    #  leaflet_set
    #  post_set
    #  problemset_set
    #  season_set
    #  user_set

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("name__icontains",)

    @classmethod
    def get_by_site(cls, site):
        return get_object_or_404(cls, site=site)

    @classmethod
    def get_by_current_site(cls):
        return cls.get_by_site(Site.objects.get_current())

    def __unicode__(self):
        return self.name

    @cached_property
    def active_season(self):
        season_candidates = self.season_set.filter(start__lt=now(),
                                                   end__gt=now())
        if season_candidates.exists():
            return season_candidates[0]
        else:
            return None

    def get_all_user_seasons(self, user):
        """
        Yields the all the seasons the user competed in.
        """

        for season in self.season_set.all():
            if season.competitors.filter(pk=user.pk).exists():
                yield season

    def get_best_user_ranking(self, user):
        """
        Returns the best ranking of this user and the season in which
        it occurred.

        Returns (None, None) if no user ranking found (user would have to never
        submit a solution).
        """

        best_rank = None
        best_season = None

        for season in self.get_all_user_seasons(user):
            rank, _ = season.get_user_ranking(user)
            if rank < best_rank:
                best_rank = rank
                best_season = season

        return best_rank, best_season

    class Meta:
        ordering = ['name']
        verbose_name = _('competition')
        verbose_name_plural = _('competitions') class Season(models.Model, SeasonSeriesBaseMixin):
    """
    Represents an one season of a competition. This is usually autumn or spring
    season. Using this model, however, we are not limited to 2 seasons per year.

    During each Season there might be several ProblemSets published as parts
    of that season.
    """

    competition = models.ForeignKey('competitions.Competition',
                                    verbose_name=_('competition'))
    year = models.IntegerField(verbose_name=_('year'))
    number = models.IntegerField(verbose_name=_('number'))
    name = models.CharField(max_length=50, verbose_name=_('name'))
    join_deadline = models.DateTimeField(blank=True,
                                         null=True,
                                         verbose_name=_('join deadline'))
    start = models.DateTimeField(verbose_name=_('season start'))
    end = models.DateTimeField(verbose_name=_('season end'))
    sum_method = models.CharField(max_length=50,
                                  blank=True,
                                  null=True,
                                  help_text=_('Method that is used to compute the sum of the season'),
                                  choices=settings.ROOTS_SEASON_TOTAL_SUM_METHOD_CHOICES)

    def get_year_segment(self, num_segments=2):
        """
        Returns the year segment this season belongs to.
        """

        season_midpoint = (self.start + (self.end - self.start) / 2).date()

        return YearSegment.by_date(season_midpoint, num_segments)

    @cached_property
    def num_problems(self):
        return sum(s.num_problems for s in self.series)

    @cached_property
    def problems(self):
        # Fetch the problem model, since we cannot import due circular
        # dependencies
        Problem = get_model('problems', 'Problem')

        # Find all problems for all series in this season
        all_problems = [
            series.problems.all()
            for series in self.series_set.all()
        ]

        # Return union of those problemsets
        return reduce(or_, all_problems, Problem.objects.none())

    @cached_property
    def series(self):
        """
        Provides a cached entry for all series.
        """
        return self.series_set.all()

    @cached_property
    def open_series(self):
        """
        Returns all the series that are active (as declared by organizers,
        might be after their submission deadline).
        """

        return self.series_set.filter(is_active=True)

    @cached_property
    def closed_series(self):
        """
        Returns all the series that are considered closed (as declared
        by organizers).
        """

        return self.series_set.filter(is_active=False)

    @cached_property
    def active_series(self):
        """
        Returns all the series that are active (before their submussion
        deadline).
        """

        return self.series_set.filter(submission_deadline__gt=now())

    @cached_property
    def finished_series(self):
        """
        Returns all the series that are after their submission deadline
        """

        return self.series_set.filter(submission_deadline__lt=now())

    @cached_property
    def registrations(self):
        # Keep registrations in a dict for performance
        return {
            registration.user.pk: registration for registration in
            self.userseasonregistration_set.all().select_related('user', 'school')
            }

    @cached_property
    def results(self):
        custom_total_func = getattr(settings,
                                    self.sum_method or '',
                                    simple_series_solution_sum)

        season_results = []

        for competitor in self.competitors:
            user_results = []
            for series in self.series:
                # Find the user's line in the results table
                matching_lines = [line[1:] for line in series.results
                                  if line[0] == competitor]

                # There should be at most one line matching the user's name
                # in the results table. If there is none, make a empty line.
                if matching_lines:
                    user_result = matching_lines[0][:-1]
                else:
                    user_result = ([None] * series.num_problems, 0)

                user_results.append(user_result)

            total = custom_total_func(user_results)
            season_results.append((competitor, user_results, total,
                                   self.registrations[competitor.pk]))

        season_results.sort(key=lambda x: x[2], reverse=True)
        return season_results

    @property
    def solutions(self):
        # Fetch the UserSolution model manually, since importing causes cyclical imports
        UserSolution = get_model('problems', 'UserSolution')
        return reduce(or_, [series.solutions for series in self.series], UserSolution.objects.none())

    @property
    def not_corrected_solutions(self):
        # Fetch the UserSolution model manually, since importing causes cyclical imports
        UserSolution = get_model('problems', 'UserSolution')
        return reduce(or_, [series.not_corrected_solutions for series in self.series], UserSolution.objects.none())

    @cached_property
    def is_active(self):
    	# Season is active if at least one series is active
        return self.series_set.filter(is_active=True).count() > 0

    @cached_property
    def competitors(self):
        """
        Returns the list of the competitors in the given season as everybody
        who submitted at least one problem solution in that season.
        """

        competitors = User.objects.none()

        for series in self.series_set.all():
            competitors = competitors | series.competitors

        return competitors

    def get_user_solutions(self, user):
        """
        Returns the list of lists of solutions of problems in this season
        for a particular user, each series in a separate list.

        If the problem was not submitted, None is used instead of a
        UserSolution object.
        """

        return [series.get_user_solutions(user)
                for series in self.series_set.all()]

    def get_user_solutions_with_total(self, user):
        """
        Returns a list of lists of user solutions with total score computed.

        Also gives a total score per season for this user. A total score may
        be computed using settings.<self.sum_method>. Fallbacks
        to a simple sum of series subtotals if not specified.
        """

        all_solutions = []
        total = 0

        for series in self.series_set.all():
            solutions, subtotal = series.get_user_solutions_with_total(user)
            all_solutions.append(solutions)
            total += subtotal

        custom_total_func = getattr(settings,
                                    self.sum_method or '',
                                    None)

        if custom_total_func is not None:
            assert callable(custom_total_func)
            return custom_total_func(user, all_solutions)
        else:
            # Fallback to simple sum
            return all_solutions, total

    def get_series_nearest_deadline(self):
        """
        Returns the most relevant series for the deadline. That is usually the
        series which is still active, but closest to its deadline.

        If all series are past their deadline, returns the last series.
        If there are no series in this season, returns None.
        """

        if self.active_series.exists():
            # Series are sorted by the submission deadline, so return
            # the first that is not over yet
            return self.active_series[0]
        else:
            # If all the series are over, just return the last one
            if self.series_set.exists():
                return self.series_set.order_by('-submission_deadline')[0]
            else:
                return None

    def get_first_series_after_deadline(self):
        """
        Returns the most relevant series after the deadline. That is usually the
        series which just finished.

        If no series are past their deadline, returns the first series.
        If there are no series in this season, returns None.
        """

        if self.finished_series.exists():
            # Series are ordered by their submission deadline, so we need
            # to reverse the ordering
            return self.finished_series.order_by('-submission_deadline')[0]
        else:
            # If no series finished yet, simply return the first series
            if self.series_set.exists():
                return self.series_set.all()[0]
            else:
                return None

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("name__icontains", "competition__name__icontains",
                "year__iexact", "number__iexact")

    def __unicode__(self):
        template = "{name} ({competition} {year}-{number})"
        return template.format(competition=remove_accents(self.competition),
                               year=self.year,
                               number=self.number,
                               name=remove_accents(self.name),
                               )

    class Meta:
        ordering = ['competition', 'year', 'number']
        verbose_name = _('Season')
        verbose_name_plural = _('Seasons') class Series(models.Model, SeasonSeriesBaseMixin):
    """
    Represents one series of problems in the season of the competition.
    """

    season = models.ForeignKey('competitions.Season',
                               verbose_name=_('season'))
    name = models.CharField(max_length=50,
                            verbose_name=_('name'))
    number = models.PositiveSmallIntegerField(verbose_name=_('number'))
    problemset = models.OneToOneField('problems.ProblemSet',
                                      verbose_name=_('problem set assigned'))
    submission_deadline = models.DateTimeField(verbose_name=_('series submission deadline'))
    is_active = models.BooleanField(default=False,
                                    verbose_name=_('is series active'))
    sum_method = models.CharField(max_length=50,
                                  blank=True,
                                  null=True,
                                  verbose_name=_('total method'),
                                  help_text=_('Method that is used to compute the sum of the series'),
                                  choices=settings.ROOTS_SERIES_TOTAL_SUM_METHOD_CHOICES)

    results_note = models.CharField(max_length=500,
                            blank=True,
                            null=True,
                            verbose_name=_('note in results'))

    @cached_property
    def num_problems(self):
        return self.problems.count()

    @cached_property
    def problems(self):
        return self.problemset.problems.all()

    @cached_property
    def problems_ids(self):
        return list(self.problems.values_list('id', flat=True))

    @property
    def time_to_deadline(self):
        """
        Returns the remaining time to the submission deadline as a datetime.timedelta object. If the deadline is over,
        returns datetime.timedelta(0).
        """
        remaining_time = self.submission_deadline - now()

        if remaining_time.total_seconds() < 0:
            return datetime.timedelta(0)
        else:
            return remaining_time

    def get_elapsed_time_percentage(self, base_timespan=datetime.timedelta(30)):
        """
        Returns the remaining time percentage for the given series. The
        base_timespan parameter defines the time when the countdown starts
        (by default 30 days before the submission deadline).
        """

        remaining_time = self.time_to_deadline

        # If the remaining time is more than base_timespan, return 0 percent
        # to prevent negative values
        if remaining_time > base_timespan:
            return 0
        else:
            elapsed = (
                remaining_time.total_seconds() / base_timespan.total_seconds()
            )

            return (1.0 - elapsed) * 100

    def sort_solutions(self, solutions):
        """
        Sorts the solutions passed as argument according to the order of the
        problems in the problemset.
        """

        sorted_solutions = [None] * self.num_problems

        for solution in solutions:
            try:
                sorted_solutions[self.problems_ids.index(solution.problem.pk)] = solution
            except ValueError:
                raise ValueError("Given solution %s is not for any problem in the problemset %s" %
                                 (solution, self.problemset))

        return sorted_solutions

    @cached_property
    def results(self):
        results = []

        # Fetch the UserSolution model manually, since importing causes cyclical imports
        UserSolution = get_model('problems', 'UserSolution')
        UserSeasonRegistration = get_model('profiles', 'UserSeasonRegistration')

        custom_total_func = getattr(settings,
                                    (self.sum_method or ''),
                                    simple_solution_sum)

        solutions = (UserSolution.objects.only('problem', 'user', 'score', 'classlevel')
                                         .filter(problem__in=self.problems_ids)
                                         .order_by('user', 'problem')
                                         .select_related('user', 'problem'))

        current_user = None

        # Solutions are sorted by the user ids first, then by the problem ids
        for solution in solutions:
            if current_user != solution.user:
                # User has changed, compute the previous line if we are not processing the first solution
                if current_user is not None:
                    # Find the current user's registration to the given season
                    registration = self.season.registrations[current_user.pk]

                    # Process the previous line
                    user_solutions = self.sort_solutions(user_solutions)
                    total = custom_total_func(user=current_user,
                                              registration=registration,
                                              solutions=user_solutions)
                    results.append((current_user, user_solutions, total,
                                    registration))

                # Prepare the new result line
                current_user = solution.user
                user_solutions = [solution]
            else:
                user_solutions.append(solution)

        # Process the last user
        if current_user is not None:
            # Find the current user's registration to the given season
            registration = self.season.registrations[current_user.pk]

            user_solutions = self.sort_solutions(user_solutions)
            total = custom_total_func(user=current_user,
                                      solutions=user_solutions)
            results.append((current_user, user_solutions, total, registration))

        # Sort the results by total_score
        results.sort(key=lambda x: x[2], reverse=True)
        return results


    @cached_property
    def competitors(self):
        """
        Returns the list of the competitors in the given series as everybody
        who submitted at least one problem solution.
        """

        competitors = User.objects.filter(
                usersolution__problem__in=self.problems_ids
            ).distinct()

        return competitors

    @cached_property
    def solutions(self):
        UserSolution = get_model('problems', 'UserSolution')
        return UserSolution.objects.filter(problem__in=self.problems_ids)

    @property
    def not_corrected_solutions(self):
        return self.solutions.filter(score=None)

    def get_user_solutions(self, user):
        """
        Returns the list of solutions of problems in this series for a
        particular user. If the problem was not submitted, None is used
        instead of a UserSolution object.
        """

        solutions = self.solutions.filter(user=user)
        return self.sort_solutions(solutions)

    def get_user_solutions_with_total(self, user):
        """
        Returns a list of user solutions with total score computed.

        The computation of the total score can follow any arbitrary rules,
        it needs to be specified by the settings.<self.sum_method>.

        This defaults to simple sum.
        """

        UserSeasonRegistration = get_model('profiles', 'UserSeasonRegistration')
        solutions = self.get_user_solutions(user)

        custom_total_func = getattr(settings,
                                    self.sum_method or '',
                                    simple_solution_sum)

        # Fint the current user's registration to the given season
        registration = UserSeasonRegistration.objects.get(
            user=user, season=self.season)

        # Process the previous line
        total = custom_total_func(user=current_user,
                                  registration=registration,
                                  solutions=user_solutions)

        return solutions, total

    def is_past_submission_deadline(self):
        return now() > self.submission_deadline

    def is_nearest_deadline(self):
        # Series are returned sorted by the submission deadline
        return self == self.season.get_series_nearest_deadline()

    def is_first_past_deadline(self):
        return self == self.season.get_first_series_after_deadline()

    def clean(self, *args, **kwargs):
        if self.is_active:
            if not self.submission_deadline:
                raise ValidationError("Submission deadline must be set to "
                                      "make the series active")
            if not self.problemset:
                raise ValidationError("Corresponding set of problems must be "
                                      "set to make the series active")

        super(Series, self).clean(*args, **kwargs)

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("name__icontains",)

    def __unicode__(self):
        return self.name

    class Meta:

        ordering = ['submission_deadline']
        unique_together = ('season', 'number')
        verbose_name = _('Series')
        verbose_name_plural = _('Series') class Problem(models.Model):
    '''
    Represents a problem.
    '''

    def get_rating(self):
        return self.rating.get_rating()
    get_rating.short_description = _('Rating')

    def get_usages(self):
        # FIXME: problemsets that have no event will not be displayed
        sets = self.problemset_set.order_by('-event__start_time')\
                                  .filter(event__isnull=False)

        if sets.exists():
            return sets
        else:
            return []

    def last_used_at(self):
        usages = self.get_usages()

        if usages:
            if usages[0].event:
                return usages[0].event

    def last_five_usages(self):
        usages = self.get_usages()[:5]

        if usages:
            return ', '.join([str(problemset.event) for problemset in usages])
        else:
            return ''

    def currently_used_in_season(self):
        # Get the problemsets in decreasing order of their series'
        # submission deadlines. Skip over those not related to any
        # series.
        sets = self.problemset_set.order_by('-series__submission_deadline')
        sets = sets.filter(series__isnull=False)

        if sets:
            return sets[0].series.season

    def current_series_problem(self):
        sets = self.problemset_set.order_by('-series__submission_deadline')
        sets = sets.filter(series__isnull=False)

        if sets:
            problemset = sets[0]
            order = list(problemset.problems.all()).index(self) + 1
            series = problemset.series
            competition = series.season.competition
            return "%s, %s: %d" % (competition, series, order)

    def times_used(self):
        return len(self.get_usages())

    text = models.TextField(verbose_name=_('problem text'),
                            help_text=_('The problem itself. Please insert it '
                                        'in a valid TeX formatting.'))
    result = models.TextField(verbose_name=_('Result / short solution outline'),
                              help_text=_('The result of the problem. For '
                                          'problems that do not have simple '
                                          'results, a hint or short outline of '
                                          'the solution.'),
                              blank=True,
                              null=True)
    rating = RatingField(range=5,
                         verbose_name=_('rating'))
    severity = models.ForeignKey('problems.ProblemSeverity',
                                 verbose_name=_('severity'))
    category = models.ForeignKey('problems.ProblemCategory',
                                 verbose_name=_('category'))
    competition = models.ForeignKey('competitions.Competition',
                                    verbose_name=_('competition'))
    source = models.CharField(max_length=500,
                              blank=True,
                              null=True,
                              verbose_name=_('problem source'),
                              help_text=_('Source where you found the problem'
                                          '(if not original).'))
    image = models.ImageField(storage=OverwriteFileSystemStorage(),
                              blank=True,
                              null=True,
                              upload_to='problems/',
                              verbose_name=_('image'),
                              help_text=_('Image added to the problem text.'))
    additional_files = models.FileField(
                           blank=True,
                           null=True,
                           upload_to='problems/',
                           storage=OverwriteFileSystemStorage(),
                           verbose_name=_('additional files'),
                           help_text=_('Additional files stored with the '
                                       'problem (such as editable images).'))

    # Fields added via foreign keys:

    #  orgsolution_set
    #  problemset_set
    #  user_set
    #  usersolution_set

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("competition__name__icontains","text__icontains")

    def __unicode__(self):
        return remove_uncomplete_latex(truncatewords(self.text, 10))

    class Meta:
        verbose_name = _('problem')
        verbose_name_plural = _('problems') class ProblemSet(models.Model):
    '''
    Represents a collections of problems. This can (optionally) be used at
    event or competition, which organizer should mark here.
    '''

    name = models.CharField(max_length=100,
                            verbose_name=('name'))
    description = models.CharField(max_length=400,
                                   blank=True,
                                   null=True,
                                   verbose_name=('description'))
    competition = models.ForeignKey('competitions.Competition',
                                    verbose_name=_('competition'))
    leaflet = models.ForeignKey('leaflets.Leaflet',
                                blank=True,
                                null=True,
                                verbose_name=_('leaflet'))
    event = models.ForeignKey('events.Event',
                              blank=True,
                              null=True,
                              verbose_name=_('event'))

    problems = SortedManyToManyField(Problem,
                                     through='problems.ProblemInSet',
                                     verbose_name=_('problems'),
                                     sort_value_field_name='position')

    # One to one fields: Series

    def average_severity(self):
        problemset = self.problems.filter(competition=self.competition)
        average = problemset.aggregate(
                      models.Avg('severity__level'))['severity__level__avg']
        return average

    def average_severity_by_competition(self):
        averages = dict()

        for competition in Competition.objects.all():
            problemset = self.problems.filter(competition=competition)
            average = problemset.aggregate(
                          models.Avg('severity__level'))['severity__level__avg']

            if average:
                key = unicode(competition)
                averages[key] = average

        return averages

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("name__icontains", "competition__name__icontains")

    def __unicode__(self):
        return self.name

    def get_problem_count(self):
        return self.problems.count()
    get_problem_count.short_description = _("problems")

    def get_average_severity_by_competition(self):
        averages = self.average_severity_by_competition()
        reports = []
        for competition, average in averages.iteritems():
            reports.append("%s (%s)" % (competition, average))

        return ', '.join(reports)
    get_average_severity_by_competition.short_description = _("average "
                                                              "difficulty")

    class Meta:
        verbose_name = _('Problem set')
        verbose_name_plural = _('Problem sets') class ProblemSeverity(models.Model):
    '''
    Lets you define custom levels of severity for problems.

    Severity level is represented by its name and level, e.g.:
        easy - 1
        medium -2
        hard - 3
        godlike - 4

    This is not hardcoded so that every organization can use their own
    levels of severity to categorize their problems.
    '''

    name = models.CharField(max_length=50,
                            verbose_name=_('name'))
    level = models.IntegerField(verbose_name=_('level'))
    competition = models.ForeignKey(
        'competitions.Competition',
        verbose_name=_('competition'),
        help_text=_('The reference to the competition that uses this severity. '
                    'It makes sense to have severities specific to each '
                    'competition, since organizers might have different '
                    'ways of sorting the problems regarding their severity.'))

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("name__icontains", "level__iexact",
                "competition__name__icontains")

    def __unicode__(self):
        return unicode(self.level) + ' - ' + self.name

    class Meta:
        ordering = ['level']
        verbose_name = _('severity')
        verbose_name_plural = _('severities') class ProblemCategory(models.Model):
    '''
    Represents a category of problems, like geometry or functional equations.
    '''

    name = models.CharField(max_length=50,
                            verbose_name=_('name'))
    competition = models.ForeignKey(
        'competitions.Competition',
        verbose_name=_('competition'),
        help_text=_('The reference to the competition that uses this category. '
                    'It makes sense to have categories specific to each '
                    'competition, since problem types in competitions '
                    'may differ significantly.'))

    # Fields added via foreign keys:
    #     problem_set


    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return ("name__icontains", "competition__name__icontains")

    def __unicode__(self):
        return u'%s (%s)' % (self.name, self.competition)

    class Meta:
        ordering = ['name']
        verbose_name = _('category')
        verbose_name_plural = _('categories') class UserSolution(MediaRemovalMixin,
                   AccessFilePermissionMixin,
                   models.Model):
    '''
    Represents a user submitted solution of a given problem.
    '''

    media_root = settings.SENDFILE_ROOT

    @classmethod
    def get_by_filepath(cls, path):
        try:
            return cls.objects.get(solution=path)
        except ObjectDoesNotExist:
            try:
                return cls.objects.get(corrected_solution=path)
            except ObjectDoesNotExist:
                return None

    def can_access_files(self, user):
        if self.user == user:
            # If you are the author, you can access
            return True
        else:
            # If the problem is in the competition you organize, you can access
            # the solution
            organizer_group = self.problem.competition.organizer_group
            return user.groups.filter(id=organizer_group.pk).exists()

    def get_solution_path(self, *args, **kwargs):
        return 'solutions/' + self.get_id_string()

    def get_corrected_solution_path(self, *args, **kwargs):
        return 'corrected_solutions/' + self.get_id_string()

    def get_media_files(self):
        return [self.get_solution_path()]

    def get_id_string(self):
        return '{user}-{problem}.pdf'.format(
                user=self.user.username,
                problem=self.problem.pk,
            )

    # Keep an explicit reference to an User, since somebody else might
    # be entering the solution on the user's behalf
    user = models.ForeignKey('auth.User',
                             verbose_name=_('user'))

    problem = models.ForeignKey('problems.Problem',
                                verbose_name=_('problem'))

    solution = ContentTypeRestrictedFileField(
                                blank=True,
                                null=True,
                                upload_to=get_solution_path_global,
                                storage=OverwriteFileSystemStorage(location=settings.SENDFILE_ROOT,
                                                                   base_url=settings.SENDFILE_URL),
                                max_size=settings.ROOTS_MAX_SOLUTION_SIZE,
                                verbose_name=_('solution'))

    corrected_solution = ContentTypeRestrictedFileField(
                                blank=True,
                                null=True,
                                upload_to=get_corrected_solution_path_global,
                                storage=OverwriteFileSystemStorage(location=settings.SENDFILE_ROOT,
                                                                   base_url=settings.SENDFILE_URL),
                                max_size=settings.ROOTS_MAX_SOLUTION_SIZE,
                                verbose_name=_('corrected solution'))

    score = models.IntegerField(blank=True,
                                null=True,
                                verbose_name=_('score'))

    classlevel = models.CharField(max_length=2,
                                  blank=True,
                                  null=True,
                                  verbose_name=_('class level at the time '
                                                 'of submission'),
                                  choices=(('Z2', 'Z2'),
                                           ('Z3', 'Z3'),
                                           ('Z4', 'Z4'),
                                           ('Z5', 'Z5'),
                                           ('Z6', 'Z6'),
                                           ('Z7', 'Z7'),
                                           ('Z8', 'Z8'),
                                           ('Z9', 'Z9'),
                                           ('S1', 'S1'),
                                           ('S2', 'S2'),
                                           ('S3', 'S3'),
                                           ('S4', 'S4')))

    school = models.ForeignKey('schools.School',
                               blank=True,
                               null=True,
                               verbose_name=_('school'))

    school_class = models.CharField(max_length=20,
                                    blank=True,
                                    null=True,
                                    verbose_name=_('school class'))

    note = models.CharField(max_length=200, blank=True, null=True)

    corrected_by = models.ManyToManyField('auth.User',
                       blank=True,
                       related_name='usersolutions_corrected_set',
                       verbose_name=_('corrected by'))

    user_modified_at = models.DateTimeField(verbose_name=_('last user modification'),
                                            auto_now=True,
                                            editable=False)

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return (
            'user__username__icontains',
            'user__first_name__icontains',
            'user__last_name__icontains',
            'problem__id__iexact',
        )

    def __unicode__(self):
        return unicode(_("User solution: {user} - {problem_id}")
                       .format(user=unicode(self.user),
                               problem_id=unicode(self.problem.pk))
               )     
        
    class Meta:
        order_with_respect_to = 'problem'
        verbose_name = _('user solution')
        verbose_name_plural = _('user solutions')
        unique_together = ['user', 'problem'] class OrgSolution(models.Model):
    '''
    Represents an ideal solution of a problem. There can be multiple ideal
    solutions (more organizers trying to solve it, more ways of solving).
    '''

    # Keep an explicit reference to an Organizer, since somebody else might
    # be entering the solution on the organizer's behalf
    organizer = models.ForeignKey('auth.User',
                                  verbose_name=_('organizer'))
    problem = models.ForeignKey('problems.Problem',
                                verbose_name=_('problem'))

    # Define autocomplete fields for grapelli search in admin
    @staticmethod
    def autocomplete_search_fields():
        return (
            'organizer__username__icontains',
            'organizer__first_name__icontains',
            'organizer__last_name__icontains',
            'problem__id__iexact',
        )

    def __unicode__(self):
        return unicode(_("organizer solution: {user} - {problem_id}")
                         .format(user=unicode(self.organizer),
                                 problem_id=unicode(self.problem.pk))
               )

    class Meta:
        order_with_respect_to = 'problem'
        verbose_name = _('organizer solution')
        verbose_name_plural = _('organizer solutions') class ProblemInSet(models.Model):

    problem = models.ForeignKey('problems.Problem',
                                verbose_name=_('problem'))
    problemset = models.ForeignKey('problems.ProblemSet',
                                   verbose_name=_('problem set'))
    position = models.PositiveSmallIntegerField(verbose_name=_('position'))

    # This is here as a reference for the SortedManyToManyField
    _sort_field_name = 'position'

    def get_rating(self):
        return self.problem.get_rating()

    def get_usages(self):
        return self.problem.get_usages()

    def last_used_at(self):
        return self.problem.last_used_at()

    def last_five_usages(self):
        return self.problem.last_five_usages()

    def times_used(self):
        return self.problem.times_used()

    def get_category(self):
        return self.problem.category

    def get_severity(self):
        return self.problem.severity

    def get_competition(self):
        return self.problem.competition

    get_rating.short_description = _('Rating')
    get_usages.short_description = _('Usages')
    last_used_at.short_description = _('Last used at')
    last_five_usages.short_description = _('Last five usages')
    times_used.short_description = _('Times used')
    get_category.short_description = _('Category')
    get_severity.short_description = _('Severity')
    get_competition.short_description = _('Competition')

    def __unicode__(self):
        return self.problem.__unicode__()

    class Meta:
        verbose_name = _('problem')
        verbose_name_plural = _('problems')
        ordering = ['position']
        unique_together = ['problem', 'problemset']
from django.contrib import admin
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from reversion import VersionAdmin
from base.admin import (PrettyFilterMixin, MediaRemovalAdminMixin,
                        DownloadMediaFilesMixin,
                        RestrictedCompetitionAdminMixin,
                        ImprovedFilteringVersionAdminMixin,
                        foreign_field_filter_factory)
from base.util import admin_commentable, editonly_fieldsets
from competitions.models import Competition, Season, Series
from .models import (Problem, ProblemSet, ProblemSeverity, ProblemCategory,
                     UserSolution, OrgSolution, ProblemInSet)
                continue

            for user in competition.active_season.competitors:
                yield (user.pk, "%s: %s" % (competition, user))

    def queryset(self, request, queryset):
        if self.value():
            return queryset.filter(user__pk=self.value())
        else:
            return queryset


class LimitToCurrentSeasonProblemFilter(admin.SimpleListFilter):

    title = _('limit to following current season/series')
    parameter_name = 'current_season_series'

    def lookups(self, request, model_admin):
        for competition in Competition.objects.all():
            # If the competition does not have active season, skip it
            if competition.active_season is None:
                continue

            # First yield the whole season
            yield (
                "season-%d" % competition.active_season.pk,
                "%s: %s" % (competition, competition.active_season)
            )

            # Yield all the finished series
            for series in competition.active_season.finished_series:
                yield (
                    "series-%d" % series.pk,
                    "%s: %s" % (competition, series)
                )

    def queryset(self, request, queryset):
        value = self.value()

        # Check that any value was passed, if not, return unmodified queryset
        if not value:
            return queryset

        # If a season was selected, allow all problems in that season
        if value.startswith("season"):
            # Fetch the list of the problems in this season
            season_pk = value.split("-")[1]
            season = Season.objects.get(pk=season_pk)
            season_problems = season.problems.values_list('pk', flat=True)

            return queryset.filter(problem__pk__in=season_problems)

        elif value.startswith("series"):
            # Fetch the list of the problems in this series
            series_pk = value.split("-")[1]
            series = Series.objects.get(pk=series_pk)
            series_problems = series.problems.values_list('pk', flat=True)

            return queryset.filter(problem__pk__in=series_problems)

        else:
            # Any other value is invalid, return unmodified queryset
            return queryset


class HasUploadedSolutionFilter(admin.SimpleListFilter):

    title = _('users with uploaded solution')
    parameter_name = 'has_uploaded_solution'

    def lookups(self, request, model_admin):
        # Only two options are possible
        values = [
            ('yes', "With solution"),
            ('no', "Without solution")
        ]
        for entry in values:
            yield entry

    def queryset(self, request, queryset):
        value = self.value()

        # Check that any value was passed, if not, return unmodified queryset
        if not value:
            return queryset

        # The solution file field is represented in database as a file path
        # and Django stores non-files as an empty string ''
        if value == "no":
            return queryset.filter(solution='')

        elif value == "yes":
            return queryset.exclude(solution='')

        else:
            return queryset


class LimitToSeasonFilter(admin.SimpleListFilter):

    title = _('limit to season')
    parameter_name = 'season'

    def lookups(self, request, model_admin):
        for competition in Competition.objects.all():
            for season in competition.season_set.all():
                # First yield the whole season
                yield (
                    season.pk,
                    "%s" % season
                )

    def queryset(self, request, queryset):
        value = self.value()

        # Check that any value was passed, if not, return unmodified queryset
        if not value:
            return queryset

        # Fetch the list of the problems in this season

        season = Season.objects.get(pk=value)