digitalfabrik/lunes-cms

View on GitHub
lunes_cms/cms/admins/training_set_admin.py

Summary

Maintainability
A
1 hr
Test Coverage
F
40%
from __future__ import absolute_import, unicode_literals

from django.conf import settings
from django.contrib import admin, messages
from django.db.models import Count, F, Q
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import ngettext
from django.utils.translation import ugettext_lazy as _
from mptt.admin import DraggableMPTTAdmin

from ..forms import TrainingSetForm
from ..list_filter import DisciplineListFilter
from ..models import Discipline, Document, Static
from ..utils import iter_to_string


class TrainingSetAdmin(DraggableMPTTAdmin):
    """
    Admin Interface to for the TrainigSet module.
    Inheriting from `mptt.admin.DraggableMPTTAdmin`.
    """

    exclude = ("creator_is_admin",)
    fields = [
        "released",
        "title",
        "description",
        "icon",
        "image_tag",
        "documents",
        "discipline",
        "created_by",
    ]
    readonly_fields = ["created_by", "image_tag"]
    search_fields = ["title"]
    form = TrainingSetForm
    list_display = [
        "tree_actions",
        "title",
        "released",
        "words",
        "words_released",
        "words_unreleased",
        "related_disciplines",
        "creator_group",
    ]
    list_display_links = ["title"]
    list_filter = [DisciplineListFilter, "released"]
    actions = ["make_released", "make_unreleased"]
    list_per_page = 25

    def get_list_display(self, request):
        if request.user.is_superuser:
            return [
                display for display in self.list_display if display not in ["words"]
            ]
        return [
            display
            for display in self.list_display
            if display not in ["words_released", "words_unreleased"]
        ]

    def save_model(self, request, obj, form, change):
        """
        Overwrite django built-in function to save
        user group and admin status of model

        :param request: current user request
        :type request: django.http.request
        :param obj: training set object
        :type obj: ~lunes_cms.cms.models.training_set.TrainingSet
        :param form: employed model form
        :type form: ModelForm
        :param change: True if change on existing model
        :type change: bool
        :raises IndexError: Error when user is not superuser and doesn't belong to any group
        """
        if not change:
            if len(request.user.groups.all()) >= 1:
                obj.created_by = request.user.groups.all()[0]
            elif not request.user.is_superuser:
                raise IndexError("No group assigned. Please add the user to a group")
            obj.creator_is_admin = request.user.is_superuser
        obj.save()

    def get_action_choices(self, request, default_choices=""):
        """
        Overwrite django built-in function to modify action choices. The first
        option is dropped since it is a place holder.

        :param request: current user request
        :type request: django.http.request
        :return: modified action choices
        :rtype: dict
        """
        choices = super().get_action_choices(request)
        choices.pop(0)
        return choices

    def get_queryset(self, request):
        """
        Overwrite django built-in function to modify queryset according to user.
        Users that are not superusers only see training set of their groups.

        :param request: current user request
        :type request: django.http.request
        :return: adjusted queryset
        :rtype: QuerySet
        """
        qs = super().get_queryset(request)
        if request.user.is_superuser:
            return qs.filter(creator_is_admin=True).annotate(
                words_released=Count(
                    "documents",
                    filter=Q(
                        documents__creator_is_admin=True,
                        documents__document_image__confirmed=True,
                    ),
                    distinct=True,
                ),
                words_unreleased=Count(
                    "documents",
                    filter=(
                        Q(documents__creator_is_admin=True)
                        & ~Q(documents__document_image__confirmed=True)
                    ),
                    distinct=True,
                ),
            )
        user_groups = request.user.groups.all()
        return qs.filter(created_by__in=user_groups).annotate(
            words=Count(
                "documents",
                filter=Q(documents__created_by__in=user_groups),
                distinct=True,
            ),
        )

    def get_form(self, request, obj=None, change=False, **kwargs):
        """
        Overwrite django built-in function to define custom choices
        in many to many selectors, e.g. users should not see documents
        by superusers. The function modifies the querysets of the
        corresponding base fields dynamically.

        :param request: current user request
        :type request: django.http.request
        :param obj: django model object, defaults to None
        :type obj: django.db.models, optional
        :return: model form with adjusted querysets
        :rtype: ModelForm
        """
        form = super().get_form(request, obj, **kwargs)
        if not request.user.is_superuser:
            form.base_fields["discipline"].queryset = (
                Discipline.objects.filter(
                    created_by__in=request.user.groups.all(), lft=F("rght") - 1
                )
                .order_by("title")
                .order_by("level")
            )
            form.base_fields["documents"].queryset = (
                Document.objects.filter(
                    Q(created_by__in=request.user.groups.all())
                    | (
                        Q(creator_is_admin=True, document_image__confirmed=True)
                        & ~Q(audio="")
                    )
                )
                .distinct()
                .order_by("word")
            )
        else:
            form.base_fields["discipline"].queryset = (
                Discipline.objects.filter(creator_is_admin=True, lft=F("rght") - 1)
                .order_by("title")
                .order_by("level")
            )
            form.base_fields["documents"].queryset = Document.objects.filter(
                creator_is_admin=True
            ).order_by("word")
        return form

    @admin.action(description=_("Release selected training sets"))
    def make_released(self, request, queryset):
        """
        Action to release training set objects. It sets the
        corresponding boolean field to true.

        :param request: current user request
        :type request: django.http.request
        :param queryset: current queryset
        :type queryset: QuerySet
        """
        # Annotate queryset so it can be filtered by the document count later
        annotated_queryset = queryset.annotate(
            document_count=Count(
                "documents",
                filter=Q(documents__document_image__confirmed=True),
                distinct=True,
            )
        )
        # Split training sets by their validity and their "released" status
        invalid_trainingsets = []
        released_trainingsets = []
        unreleased_trainingsets = []
        for trainingset in annotated_queryset:
            if trainingset.document_count < settings.TRAININGSET_MIN_DOCS:
                invalid_trainingsets.append(trainingset)
            elif trainingset.released:
                released_trainingsets.append(trainingset)
            else:
                unreleased_trainingsets.append(trainingset)
        # Show error message for invalid trainingsets
        if invalid_trainingsets:
            messages.error(
                request,
                ngettext(
                    "The training set {} could not be released because it contains less than {} vocabulary words with confirmed images.",
                    "The training sets {} could not be released because they contain less than {} vocabulary words with confirmed images.",
                    len(invalid_trainingsets),
                ).format(
                    iter_to_string(invalid_trainingsets),
                    settings.TRAININGSET_MIN_DOCS,
                ),
            )
        # Show info message for valid trainingsets that are already released
        if released_trainingsets:
            messages.info(
                request,
                ngettext(
                    "The training set {} is already released.",
                    "The training sets {} are already released.",
                    len(released_trainingsets),
                ).format(iter_to_string(released_trainingsets)),
            )
        # Update valid trainingsets and show success message
        if unreleased_trainingsets:
            # Update valid queryset
            annotated_queryset.filter(
                document_count__gte=settings.TRAININGSET_MIN_DOCS
            ).update(released=True)
            messages.success(
                request,
                ngettext(
                    "The training set {} was successfully released.",
                    "The training sets {} were successfully released.",
                    len(unreleased_trainingsets),
                ).format(iter_to_string(unreleased_trainingsets)),
            )

    @admin.action(description=_("Unrelease selected training sets"))
    def make_unreleased(self, request, queryset):
        """
        Action to hide discipline objects. It sets the
        corresponding boolean field to false.

        :param request: current user request
        :type request: django.http.request
        :param queryset: current queryset
        :type queryset: QuerySet
        """
        # Split all training sets by their "released" status
        released_trainingsets = []
        unreleased_trainingsets = []
        for trainingset in queryset:
            if trainingset.released:
                released_trainingsets.append(trainingset)
            else:
                unreleased_trainingsets.append(trainingset)
        # Add info messages for training sets that are already unreleased
        if unreleased_trainingsets:
            messages.info(
                request,
                ngettext(
                    "The training set {} is already unreleased.",
                    "The training sets {} are already unreleased.",
                    len(unreleased_trainingsets),
                ).format(iter_to_string(unreleased_trainingsets)),
            )
        # Get all training sets that are released and need to be updated
        if released_trainingsets:
            queryset.update(released=False)
            messages.success(
                request,
                ngettext(
                    "The training set {} was successfully unreleased.",
                    "The training sets {} were successfully unreleased.",
                    len(released_trainingsets),
                ).format(iter_to_string(released_trainingsets)),
            )

    @admin.display(
        description=_("words"),
        ordering="-words",
    )
    def words(self, obj):
        """
        returns HTML tag of the link to the list of words related to the training set

        :param obj: Training set object
        :type obj: ~lunes_cms.cms.models.training_set.TrainingSet
        :return: HTML tag of the link to the list of words related to the training set
        :rtype: str
        """
        document_list = reverse("admin:cms_document_changelist")
        return mark_safe(
            f"<a href={document_list}?training+set={obj.id}>{obj.words}</a>"
        )

    @admin.display(
        description=_("published words"),
        ordering="-words_released",
    )
    def words_released(self, obj):
        """
        returns HTML tag of the link to the list of released words related to the training set

        :param obj: Training set object
        :type obj: ~lunes_cms.cms.models.training_set.TrainingSet
        :return: HTML tag of the link to the list of released words related to the training set
        :rtype: str
        """
        document_list = reverse("admin:cms_document_changelist")
        return mark_safe(
            f"<a href={document_list}?training+set={obj.id}&images=approved>{obj.words_released}</a>"
        )

    @admin.display(
        description=_("unpublished words"),
        ordering="-words_unreleased",
    )
    def words_unreleased(self, obj):
        """
        returns HTML tag of the Link to the list of unreleased words related to the training set

        :param obj: Training set object
        :type obj: ~lunes_cms.cms.models.training_set.TrainingSet
        :return: HTML tag of the Link to the list of unreleased words related to the training set
        :rtype: str
        """
        document_list = reverse("admin:cms_document_changelist")
        return mark_safe(
            f"<a href={document_list}?training+set={obj.id}&images=no-approved>{obj.words_unreleased}</a>"
        )

    def related_disciplines(self, obj):
        """
        Display related disciplines in list display

        :param obj: Training set object
        :type obj: ~lunes_cms.cms.models.training_set.TrainingSet
        :return: comma separated list of related disciplines
        :rtype: str
        """
        return ", ".join([child.title for child in obj.discipline.all()])

    related_disciplines.short_description = _("disciplines")

    def creator_group(self, obj):
        """
        Include creator group of discipline in list display

        :param obj: Training set object
        :type obj: ~lunes_cms.cms.models.training_set.TrainingSet
        :return: Either static admin group or user group
        :rtype: str
        """
        if obj.creator_is_admin:
            return Static.admin_group
        if obj.created_by:
            return obj.created_by
        return None

    creator_group.short_description = _("creator group")

    class Media:
        """
        Media class of Training Set Admin
        """

        js = ("js/image_preview.js",)