Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
models.py 5.26 KiB
""" Model definitions for the quiz """

from django.core.exceptions import ValidationError
from django.db import models


def validate_rating(value):
    """
    This function acts as a validator for ratings.
    Sadly, it isn't called automatically, so it needs to be used manually.
    """

    if not ((10 >= value >= 1) or value == -1):
        raise ValidationError(u"%s is not a valid rating!" % value)


class CriterionRating(models.Model):
    """
    This is the relation between Sport and Criterion.
    You can use it to add a rating for a specific criterion to a sport.
    To see it's usage check Sport.rate() and Sport.get_rating()
    """

    rating = models.IntegerField(validators=[validate_rating])
    criterion = models.ForeignKey("Criterion", on_delete=models.CASCADE)
    sport = models.ForeignKey("Sport", on_delete=models.CASCADE)

    def __str__(self):
        return str(self.sport) + " - " + str(self.criterion) + ": " + str(self.rating)


class SportManager(models.Manager):
    """
    Manages Creation of Sport Objects
    Since every Criterion Connection needs to be present in the DB at all times,
    the connections are made at creation of the Sport object. For this, Sport objects need to be
    created through this Manager Class.

    Docs: https://docs.djangoproject.com/en/3.2/ref/models/instances/#creating-objects
    """

    def create_sport(self, **kwargs):
        """
        Creates new Sport Object and every CriterionRating for it
        """
        sport = self.create(**kwargs)

        for crit in Criterion.objects.iterator():
            sport.rate(crit, -1)

        return sport


class Sport(models.Model):
    """
    Defines a Sport with name, url that leads to the booking page.
    A sport includes ratings for all criterions.
    (e.g. How much it corresponds to the criterion "Martial Arts")
    """

    name = models.TextField()
    url = models.URLField()
    criteria_ratings = models.ManyToManyField("Criterion", through="CriterionRating")

    objects = SportManager()

    def __str__(self):
        return self.name

    def rate(self, criterion, rating):
        """Defines how much (rating) the sport meets the given criterion"""
        rating_obj, _ = CriterionRating.objects.get_or_create(
            sport=self, criterion=criterion, defaults={"rating": rating}
        )
        validate_rating(rating)
        rating_obj.rating = rating
        rating_obj.save()
        return rating_obj

    def get_rating(self, criterion):
        """Returns how much the sport meets the given criterion"""
        criterion_rating = CriterionRating.objects.get(sport=self, criterion=criterion)
        return criterion_rating.rating

    def is_filled(self):
        """
        Returns a Boolean whether all Criterions are given a valid rating (unequal to -1)
        """

        for crit in self.criteria_ratings.iterator():
            if self.get_rating(crit) == -1:
                return False

        return True


class CriterionManager(models.Manager):
    """
    Manages Creation of Criterion Objects
    Since every Sport Object needs to be rated in every Criterion,
    when a new Criterion is created, every Sport object needs to be rated for that Criterion.
    As a default value, -1 is entered so that it can be recognized that no true value is given.

    Docs: https://docs.djangoproject.com/en/3.2/ref/models/instances/#creating-objects
    """

    def create_criterion(self, **kwargs):
        """
        Creates a Criterion Object and Rates every existing Sport with -1
        """
        crit = Criterion(**kwargs)

        # Criterion needs to be saved before it can be connected to a sport
        crit.save()

        for sport in Sport.objects.iterator():
            sport.rate(crit, -1)

        return crit


class Criterion(models.Model):
    """
    Defines a Sport property that is used a a criterion for our quiz.
    (e.g. Individual or Team sport)
    """

    name = models.TextField()

    objects = CriterionManager()

    def __str__(self):
        return self.name

    def get_active_sum(self):
        """
        Get Number of Sports with Rating larger than 1 and the cumulated sum of all ratings
        TODO: Think about Usefulness of Rating Sums with 1 as Min Value
        """

        num_active = 0
        rating_sum = 0

        for rating_obj in CriterionRating.objects.filter(criterion=self):
            rating_sum += rating_obj.rating
            if rating_obj.rating > 1:
                num_active += 1

        return num_active, rating_sum


class CallToMove(models.Model):
    """Defines text and image that are used to show a call to move between questions"""

    text = models.TextField()
    image = models.ImageField(null=True, max_length=200)

    def __str__(self):
        return self.text


class KnowledgeSnack(models.Model):
    """Defines text and image that are used to show a KnowledgeSnack between questions"""

    text = models.TextField()
    image = models.ImageField(null=True, max_length=200)

    def __str__(self):
        return self.text


class Question(models.Model):
    """Defines a Question that is assigned to exactly one Criterion"""

    text = models.TextField()
    criterion = models.OneToOneField(
        Criterion, on_delete=models.CASCADE, primary_key=True
    )

    def __str__(self):
        return self.text