712 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			712 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
import uuid
 | 
						|
 | 
						|
from django.db import models
 | 
						|
from django.urls import reverse
 | 
						|
from django.utils.translation import gettext_lazy as _
 | 
						|
from django.utils import timezone
 | 
						|
from django.dispatch import receiver
 | 
						|
from django.db.models.signals import post_save
 | 
						|
from django.contrib.auth.models import Group
 | 
						|
from django.contrib.auth.models import AbstractUser
 | 
						|
 | 
						|
from .tools import misc, geo
 | 
						|
from notfellchen.settings import MEDIA_URL
 | 
						|
 | 
						|
 | 
						|
class Language(models.Model):
 | 
						|
    """Model representing a Language (e.g. English, French, Japanese, etc.)"""
 | 
						|
    name = models.CharField(max_length=200,
 | 
						|
                            help_text=_("Der Name einer natürliche Sprache wie Deutsch, Englisch oder Arabisch."),
 | 
						|
                            unique=True)
 | 
						|
 | 
						|
    languagecode = models.CharField(max_length=10,
 | 
						|
                                    # Translators: This helptext includes an URL
 | 
						|
                                    help_text=_(
 | 
						|
                                        "Der standartisierte Sprachcode. Mehr Informationen: http://www.i18nguy.com/unicode/language-identifiers.html"),
 | 
						|
                                    verbose_name=_('Sprachcode'))
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        """String for representing the Model object (in Admin site etc.)"""
 | 
						|
        return self.name
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        verbose_name = _('Sprache')
 | 
						|
        verbose_name_plural = _('Sprachen')
 | 
						|
 | 
						|
 | 
						|
class User(AbstractUser):
 | 
						|
    """
 | 
						|
    Model that holds a user's profile, including the django user model
 | 
						|
 | 
						|
    The trust levels act as permission system and can be displayed as a badge for the user
 | 
						|
    """
 | 
						|
 | 
						|
    # Admins can perform all actions and have the highest trust associated with them
 | 
						|
    # Moderators can make moderation decisions regarding the deletion of content
 | 
						|
    # Coordinators can create adoption notices without them being checked
 | 
						|
    # Members can create adoption notices that must be activated
 | 
						|
    ADMIN = "admin"
 | 
						|
    MODERATOR = "Moderator"
 | 
						|
    COORDINATOR = "Koordinator*in"
 | 
						|
    MEMBER = "Mitglied"
 | 
						|
    TRUST_LEVEL = {
 | 
						|
        ADMIN: 4,
 | 
						|
        MODERATOR: 3,
 | 
						|
        COORDINATOR: 2,
 | 
						|
        MEMBER: 1,
 | 
						|
    }
 | 
						|
 | 
						|
    preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
 | 
						|
                                           verbose_name=_('Bevorzugte Sprache'))
 | 
						|
    trust_level = models.IntegerField(choices=TRUST_LEVEL, default=TRUST_LEVEL[MEMBER])
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        verbose_name = _('Nutzer*in')
 | 
						|
        verbose_name_plural = _('Nutzer*innen')
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        return reverse("user-detail", args=[str(self.pk)])
 | 
						|
 | 
						|
    def get_notifications_url(self):
 | 
						|
        return self.get_absolute_url()
 | 
						|
 | 
						|
    def get_num_unread_notifications(self):
 | 
						|
        return BaseNotification.objects.filter(user=self, read=False).count()
 | 
						|
 | 
						|
    @property
 | 
						|
    def owner(self):
 | 
						|
        return self
 | 
						|
 | 
						|
 | 
						|
class Image(models.Model):
 | 
						|
    image = models.ImageField(upload_to='images')
 | 
						|
    alt_text = models.TextField(max_length=2000)
 | 
						|
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.alt_text
 | 
						|
 | 
						|
    @property
 | 
						|
    def as_html(self):
 | 
						|
        return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
 | 
						|
 | 
						|
 | 
						|
class Species(models.Model):
 | 
						|
    """Model representing a species of animal."""
 | 
						|
    name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
 | 
						|
                            verbose_name=_('Name'))
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        """String for representing the Model object."""
 | 
						|
        return self.name
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        verbose_name = _('Tierart')
 | 
						|
        verbose_name_plural = _('Tierarten')
 | 
						|
 | 
						|
 | 
						|
class Location(models.Model):
 | 
						|
    place_id = models.IntegerField()
 | 
						|
    latitude = models.FloatField()
 | 
						|
    longitude = models.FloatField()
 | 
						|
    name = models.CharField(max_length=2000)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_location_from_string(location_string):
 | 
						|
        geo_api = geo.GeoAPI()
 | 
						|
        geojson = geo_api.get_geojson_for_query(location_string)
 | 
						|
        if geojson is None:
 | 
						|
            return None
 | 
						|
        result = geojson[0]
 | 
						|
        if "name" in result:
 | 
						|
            name = result["name"]
 | 
						|
        else:
 | 
						|
            name = result["display_name"]
 | 
						|
        location = Location.objects.create(
 | 
						|
            place_id=result["place_id"],
 | 
						|
            latitude=result["lat"],
 | 
						|
            longitude=result["lon"],
 | 
						|
            name=name,
 | 
						|
        )
 | 
						|
        return location
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def add_location_to_object(instance):
 | 
						|
        """Search the location given in the location string and add it to the object"""
 | 
						|
        location = Location.get_location_from_string(instance.location_string)
 | 
						|
        instance.location = location
 | 
						|
        instance.save()
 | 
						|
 | 
						|
 | 
						|
class RescueOrganization(models.Model):
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.name}"
 | 
						|
 | 
						|
    USE_MATERIALS_ALLOWED = "allowed"
 | 
						|
    USE_MATERIALS_REQUESTED = "requested"
 | 
						|
    USE_MATERIALS_DENIED = "denied"
 | 
						|
    USE_MATERIALS_OTHER = "other"
 | 
						|
    USE_MATERIALS_NOT_ASKED = "not_asked"
 | 
						|
 | 
						|
    ALLOW_USE_MATERIALS_CHOICE = {
 | 
						|
        USE_MATERIALS_ALLOWED: "Usage allowed",
 | 
						|
        USE_MATERIALS_REQUESTED: "Usage requested",
 | 
						|
        USE_MATERIALS_DENIED: "Usage denied",
 | 
						|
        USE_MATERIALS_OTHER: "It's complicated",
 | 
						|
        USE_MATERIALS_NOT_ASKED: "Not asked"
 | 
						|
    }
 | 
						|
 | 
						|
    name = models.CharField(max_length=200)
 | 
						|
    trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
 | 
						|
    allows_using_materials = models.CharField(max_length=200,
 | 
						|
                                              default=ALLOW_USE_MATERIALS_CHOICE[USE_MATERIALS_NOT_ASKED],
 | 
						|
                                              choices=ALLOW_USE_MATERIALS_CHOICE,
 | 
						|
                                              verbose_name=_('Erlaubt Nutzung von Inhalten'))
 | 
						|
    location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
 | 
						|
    location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
 | 
						|
    instagram = models.URLField(null=True, blank=True, verbose_name=_('Instagram Profil'))
 | 
						|
    facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
 | 
						|
    fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil'))
 | 
						|
    website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
 | 
						|
 | 
						|
 | 
						|
class AdoptionNotice(models.Model):
 | 
						|
    class Meta:
 | 
						|
        permissions = [
 | 
						|
            ("create_active_adoption_notice", "Can create an active adoption notice"),
 | 
						|
        ]
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.name}"
 | 
						|
 | 
						|
    created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
 | 
						|
    last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
 | 
						|
    searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
 | 
						|
    name = models.CharField(max_length=200)
 | 
						|
    description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
 | 
						|
    organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
 | 
						|
                                     verbose_name=_('Organisation'))
 | 
						|
    further_information = models.URLField(null=True, blank=True, verbose_name=_('Link zu mehr Informationen'))
 | 
						|
    group_only = models.BooleanField(default=False, verbose_name=_('Ausschließlich Gruppenadoption'))
 | 
						|
    photos = models.ManyToManyField(Image, blank=True)
 | 
						|
    location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
 | 
						|
    location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, )
 | 
						|
    owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator'))
 | 
						|
 | 
						|
    @property
 | 
						|
    def animals(self):
 | 
						|
        return Animal.objects.filter(adoption_notice=self)
 | 
						|
 | 
						|
    @property
 | 
						|
    def comments(self):
 | 
						|
        return Comment.objects.filter(adoption_notice=self)
 | 
						|
 | 
						|
    @property
 | 
						|
    def position(self):
 | 
						|
        if self.location is None:
 | 
						|
            return None
 | 
						|
        else:
 | 
						|
            return self.location.latitude, self.location.longitude
 | 
						|
 | 
						|
    @property
 | 
						|
    def description_short(self):
 | 
						|
        if self.description is None:
 | 
						|
            return ""
 | 
						|
        if len(self.description) > 200:
 | 
						|
            return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        """Returns the url to access a detailed page for the adoption notice."""
 | 
						|
        return reverse('adoption-notice-detail', args=[str(self.id)])
 | 
						|
 | 
						|
    def get_report_url(self):
 | 
						|
        """Returns the url to report an adoption notice."""
 | 
						|
        return reverse('report-adoption-notice', args=[str(self.id)])
 | 
						|
 | 
						|
    def get_subscriptions(self):
 | 
						|
        # returns all subscriptions to that adoption notice
 | 
						|
        return Subscriptions.objects.filter(adoption_notice=self)
 | 
						|
 | 
						|
    def get_photos(self):
 | 
						|
        """
 | 
						|
        First trys to get group photos that are attached to the adoption notice if there is none it trys to fetch
 | 
						|
        them from the animals
 | 
						|
        """
 | 
						|
        group_photos = self.photos.all()
 | 
						|
        if len(group_photos) > 0:
 | 
						|
            return group_photos
 | 
						|
        else:
 | 
						|
            photos = []
 | 
						|
            for animal in self.animals:
 | 
						|
                photos.extend(animal.photos.all())
 | 
						|
            if len(photos) > 0:
 | 
						|
                return photos
 | 
						|
 | 
						|
    def get_photo(self):
 | 
						|
        """
 | 
						|
        Returns the first photo it finds.
 | 
						|
        First trys to get group photos that are attached to the adoption notice if there is none it trys to fetch
 | 
						|
        them from the animals
 | 
						|
        """
 | 
						|
        group_photos = self.photos.all()
 | 
						|
        if len(group_photos) > 0:
 | 
						|
            return group_photos[0]
 | 
						|
        else:
 | 
						|
            photos = []
 | 
						|
            for animal in self.animals:
 | 
						|
                photos.extend(animal.photos.all())
 | 
						|
            if len(photos) > 0:
 | 
						|
                return photos[0]
 | 
						|
 | 
						|
    def in_distance(self, position, max_distance, unknown_true=True):
 | 
						|
        """
 | 
						|
        Returns a boolean indicating if the Location of the adoption notice is within a given distance to the position
 | 
						|
 | 
						|
        If the location is none, we by default return that the location is within the given distance
 | 
						|
        """
 | 
						|
        if unknown_true and self.position is None:
 | 
						|
            return True
 | 
						|
 | 
						|
        distance = geo.calculate_distance_between_coordinates(self.position, position)
 | 
						|
        return distance < max_distance
 | 
						|
 | 
						|
    @property
 | 
						|
    def link_to_more_information(self):
 | 
						|
        from urllib.parse import urlparse
 | 
						|
 | 
						|
        domain = urlparse(self.further_information).netloc
 | 
						|
        return f"<a href='{self.further_information}'>{domain}</a>"
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_active(self):
 | 
						|
        if not hasattr(self, 'adoptionnoticestatus'):
 | 
						|
            return False
 | 
						|
        return self.adoptionnoticestatus.is_active
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_to_be_checked(self, include_active=False):
 | 
						|
        if not hasattr(self, 'adoptionnoticestatus'):
 | 
						|
            return False
 | 
						|
        return self.adoptionnoticestatus.is_to_be_checked or (include_active and self.adoptionnoticestatus.is_active)
 | 
						|
 | 
						|
    def set_checked(self):
 | 
						|
        self.last_checked = timezone.now()
 | 
						|
        self.save()
 | 
						|
 | 
						|
    def set_closed(self):
 | 
						|
        self.last_checked = timezone.now()
 | 
						|
        self.adoptionnoticestatus.set_closed()
 | 
						|
 | 
						|
    def set_active(self):
 | 
						|
        self.last_checked = timezone.now()
 | 
						|
        if not hasattr(self, 'adoptionnoticestatus'):
 | 
						|
            AdoptionNoticeStatus.create_other(self)
 | 
						|
        self.adoptionnoticestatus.set_active()
 | 
						|
 | 
						|
    def set_to_review(self):
 | 
						|
        self.last_checked = timezone.now()
 | 
						|
        if not hasattr(self, 'adoptionnoticestatus'):
 | 
						|
            AdoptionNoticeStatus.create_other(self)
 | 
						|
        self.adoptionnoticestatus.set_to_review()
 | 
						|
 | 
						|
 | 
						|
class AdoptionNoticeStatus(models.Model):
 | 
						|
    """
 | 
						|
    The major status indicates a general state of an adoption notice
 | 
						|
    whereas the minor status is used for reporting
 | 
						|
    """
 | 
						|
 | 
						|
    ACTIVE = "active"
 | 
						|
    AWAITING_ACTION = "awaiting_action"
 | 
						|
    CLOSED = "closed"
 | 
						|
    DISABLED = "disabled"
 | 
						|
    MAJOR_STATUS_CHOICES = {
 | 
						|
        ACTIVE: "active",
 | 
						|
        AWAITING_ACTION: "in review",
 | 
						|
        CLOSED: "closed",
 | 
						|
        DISABLED: "disabled",
 | 
						|
    }
 | 
						|
 | 
						|
    MINOR_STATUS_CHOICES = {
 | 
						|
        ACTIVE: {
 | 
						|
            "searching": "searching",
 | 
						|
            "interested": "interested",
 | 
						|
        },
 | 
						|
        AWAITING_ACTION: {
 | 
						|
            "waiting_for_review": "waiting_for_review",
 | 
						|
            "needs_additional_info": "needs_additional_info",
 | 
						|
        },
 | 
						|
        CLOSED: {
 | 
						|
            "successful_with_notfellchen": "successful_with_notfellchen",
 | 
						|
            "successful_without_notfellchen": "successful_without_notfellchen",
 | 
						|
            "animal_died": "animal_died",
 | 
						|
            "closed_for_other_adoption_notice": "closed_for_other_adoption_notice",
 | 
						|
            "not_open_for_adoption_anymore": "not_open_for_adoption_anymore",
 | 
						|
            "other": "other"
 | 
						|
        },
 | 
						|
        DISABLED: {
 | 
						|
            "against_the_rules": "against_the_rules",
 | 
						|
            "missing_information": "missing_information",
 | 
						|
            "technical_error": "technical_error",
 | 
						|
            "unchecked": "unchecked",
 | 
						|
            "other": "other"
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    major_status = models.CharField(choices=MAJOR_STATUS_CHOICES, max_length=200)
 | 
						|
    minor_choices = {}
 | 
						|
    for key in MINOR_STATUS_CHOICES:
 | 
						|
        minor_choices.update(MINOR_STATUS_CHOICES[key])
 | 
						|
    minor_status = models.CharField(choices=minor_choices, max_length=200)
 | 
						|
    adoption_notice = models.OneToOneField(AdoptionNotice, on_delete=models.CASCADE)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.adoption_notice}: {self.major_status}, {self.minor_status}"
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_active(self):
 | 
						|
        return self.major_status == self.ACTIVE
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_to_be_checked(self):
 | 
						|
        return self.major_status == self.DISABLED and self.minor_status == "unchecked"
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_minor_choices(major_status):
 | 
						|
        return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def create_other(an_instance):
 | 
						|
        # Used as empty status to be changed immediately
 | 
						|
        major_status = AdoptionNoticeStatus.DISABLED
 | 
						|
        minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.DISABLED]["other"]
 | 
						|
        AdoptionNoticeStatus.objects.create(major_status=major_status,
 | 
						|
                                            minor_status=minor_status,
 | 
						|
                                            adoption_notice=an_instance)
 | 
						|
 | 
						|
    def set_closed(self):
 | 
						|
        self.major_status = self.MAJOR_STATUS_CHOICES[self.CLOSED]
 | 
						|
        self.minor_status = self.MINOR_STATUS_CHOICES[self.CLOSED]["other"]
 | 
						|
        self.save()
 | 
						|
 | 
						|
    def deactivate_unchecked(self):
 | 
						|
        self.major_status = self.MAJOR_STATUS_CHOICES[self.DISABLED]
 | 
						|
        self.minor_status = self.MINOR_STATUS_CHOICES[self.DISABLED]["unchecked"]
 | 
						|
        self.save()
 | 
						|
 | 
						|
    def set_active(self):
 | 
						|
        self.major_status = self.MAJOR_STATUS_CHOICES[self.ACTIVE]
 | 
						|
        self.minor_status = self.MINOR_STATUS_CHOICES[self.ACTIVE]["searching"]
 | 
						|
        self.save()
 | 
						|
 | 
						|
    def set_to_review(self):
 | 
						|
        self.major_status = AdoptionNoticeStatus.AWAITING_ACTION
 | 
						|
        self.minor_status = AdoptionNoticeStatus.MINOR_STATUS_CHOICES[AdoptionNoticeStatus.AWAITING_ACTION][
 | 
						|
            "waiting_for_review"]
 | 
						|
        self.save()
 | 
						|
 | 
						|
 | 
						|
class Animal(models.Model):
 | 
						|
    MALE_NEUTERED = "M_N"
 | 
						|
    MALE = "M"
 | 
						|
    FEMALE_NEUTERED = "F_N"
 | 
						|
    FEMALE = "F"
 | 
						|
    SEX_CHOICES = {
 | 
						|
        MALE_NEUTERED: "neutered male",
 | 
						|
        MALE: "male",
 | 
						|
        FEMALE_NEUTERED: "neutered female",
 | 
						|
        FEMALE: "female",
 | 
						|
    }
 | 
						|
 | 
						|
    date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
 | 
						|
    name = models.CharField(max_length=200)
 | 
						|
    description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
 | 
						|
    species = models.ForeignKey(Species, on_delete=models.PROTECT)
 | 
						|
    photos = models.ManyToManyField(Image, blank=True)
 | 
						|
    sex = models.CharField(max_length=20, choices=SEX_CHOICES, )
 | 
						|
    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
 | 
						|
    owner = models.ForeignKey(User, on_delete=models.CASCADE)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.name}"
 | 
						|
 | 
						|
    @property
 | 
						|
    def age(self):
 | 
						|
        return timezone.now().today().date() - self.date_of_birth
 | 
						|
 | 
						|
    @property
 | 
						|
    def hr_age(self):
 | 
						|
        """Returns a human-readable age based on the date of birth."""
 | 
						|
        return misc.age_as_hr_string(self.age)
 | 
						|
 | 
						|
    def get_photo(self):
 | 
						|
        """
 | 
						|
        Selects a random photo from the animal
 | 
						|
        """
 | 
						|
        photos = self.photos.all()
 | 
						|
        if len(photos) > 0:
 | 
						|
            return photos[0]
 | 
						|
 | 
						|
    def get_photos(self):
 | 
						|
        """
 | 
						|
        Selects all photos from the animal
 | 
						|
        """
 | 
						|
        return self.photos.all()
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        """Returns the url to access a detailed page for the animal."""
 | 
						|
        return reverse('animal-detail', args=[str(self.id)])
 | 
						|
 | 
						|
 | 
						|
class Rule(models.Model):
 | 
						|
    """
 | 
						|
    Class to store rules
 | 
						|
    """
 | 
						|
    title = models.CharField(max_length=200)
 | 
						|
 | 
						|
    # Markdown is allowed in rule text
 | 
						|
    rule_text = models.TextField()
 | 
						|
    language = models.ForeignKey(Language, on_delete=models.PROTECT)
 | 
						|
    # Rule identifier allows to translate rules with the same identifier
 | 
						|
    rule_identifier = models.CharField(max_length=24)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return self.title
 | 
						|
 | 
						|
 | 
						|
class Report(models.Model):
 | 
						|
    class Meta:
 | 
						|
        permissions = []
 | 
						|
 | 
						|
    ACTION_TAKEN = "action taken"
 | 
						|
    NO_ACTION_TAKEN = "no action taken"
 | 
						|
    WAITING = "waiting"
 | 
						|
    STATES = {
 | 
						|
        ACTION_TAKEN: "Action was taken",
 | 
						|
        NO_ACTION_TAKEN: "No action was taken",
 | 
						|
        WAITING: "Waiting for moderator action",
 | 
						|
    }
 | 
						|
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text=_('ID dieses reports'),
 | 
						|
                          verbose_name=_('ID'))
 | 
						|
    status = models.CharField(max_length=30, choices=STATES)
 | 
						|
    reported_broken_rules = models.ManyToManyField(Rule)
 | 
						|
    user_comment = models.TextField(blank=True)
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"[{self.status}]: {self.user_comment:.20}"
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        """Returns the url to access a detailed page for the report."""
 | 
						|
        return reverse('report-detail', args=[str(self.id)])
 | 
						|
 | 
						|
    def get_reported_rules(self):
 | 
						|
        return self.reported_broken_rules.all()
 | 
						|
 | 
						|
    def get_moderation_actions(self):
 | 
						|
        return ModerationAction.objects.filter(report=self)
 | 
						|
 | 
						|
 | 
						|
class ReportAdoptionNotice(Report):
 | 
						|
    adoption_notice = models.ForeignKey("AdoptionNotice", on_delete=models.CASCADE)
 | 
						|
 | 
						|
    @property
 | 
						|
    def reported_content(self):
 | 
						|
        return self.adoption_notice
 | 
						|
 | 
						|
 | 
						|
class ReportComment(Report):
 | 
						|
    reported_comment = models.ForeignKey("Comment", on_delete=models.CASCADE)
 | 
						|
 | 
						|
    @property
 | 
						|
    def reported_content(self):
 | 
						|
        return self.reported_comment
 | 
						|
 | 
						|
 | 
						|
class ModerationAction(models.Model):
 | 
						|
    BAN = "user_banned"
 | 
						|
    DELETE = "content_deleted"
 | 
						|
    COMMENT = "comment"
 | 
						|
    OTHER = "other_action_taken"
 | 
						|
    NONE = "no_action_taken"
 | 
						|
    ACTIONS = {
 | 
						|
        BAN: "User was banned",
 | 
						|
        DELETE: "Content was deleted",
 | 
						|
        COMMENT: "Comment was added",
 | 
						|
        OTHER: "Other action was taken",
 | 
						|
        NONE: "No action was taken"
 | 
						|
    }
 | 
						|
    action = models.CharField(max_length=30, choices=ACTIONS.items())
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
    public_comment = models.TextField(blank=True)
 | 
						|
    # Only visible to moderator
 | 
						|
    private_comment = models.TextField(blank=True)
 | 
						|
    report = models.ForeignKey(Report, on_delete=models.CASCADE)
 | 
						|
 | 
						|
    # TODO: Needs field for moderator that performed the action
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"[{self.action}]: {self.public_comment}"
 | 
						|
 | 
						|
 | 
						|
"""
 | 
						|
Membership
 | 
						|
"""
 | 
						|
 | 
						|
 | 
						|
class Text(models.Model):
 | 
						|
    """
 | 
						|
    Base class to store markdown content
 | 
						|
    """
 | 
						|
    title = models.CharField(max_length=100)
 | 
						|
    content = models.TextField(verbose_name="Inhalt")
 | 
						|
    language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT)
 | 
						|
    text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True)
 | 
						|
 | 
						|
    class Meta:
 | 
						|
        verbose_name = "Text"
 | 
						|
        verbose_name_plural = "Texte"
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.title} ({self.language})"
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_texts(text_codes, language, expandable_dict=None):
 | 
						|
        if expandable_dict is None:
 | 
						|
            expandable_dict = {}
 | 
						|
        for text_code in text_codes:
 | 
						|
            try:
 | 
						|
                expandable_dict[text_code] = Text.objects.get(text_code=text_code, language=language, )
 | 
						|
            except Text.DoesNotExist:
 | 
						|
                expandable_dict[text_code] = None
 | 
						|
        return expandable_dict
 | 
						|
 | 
						|
 | 
						|
class Announcement(Text):
 | 
						|
    """
 | 
						|
    Class to store announcements that should be displayed for all users
 | 
						|
    """
 | 
						|
    logged_in_only = models.BooleanField(default=False)
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
    publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
 | 
						|
    publish_end_time = models.DateTimeField(verbose_name="Veröffentlichungsende")
 | 
						|
    IMPORTANT = "important"
 | 
						|
    WARNING = "warning"
 | 
						|
    INFO = "info"
 | 
						|
    TYPES = {
 | 
						|
        IMPORTANT: "important",
 | 
						|
        WARNING: "warning",
 | 
						|
        INFO: "info",
 | 
						|
    }
 | 
						|
    type = models.CharField(choices=TYPES, max_length=100, default=INFO)
 | 
						|
 | 
						|
    @property
 | 
						|
    def is_active(self):
 | 
						|
        return self.publish_start_time < timezone.now() < self.publish_end_time
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"[{'🟢' if self.is_active else '🔴'}]{self.title} ({self.language})"
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def get_active_announcements(logged_in=False, language=None):
 | 
						|
        if logged_in:
 | 
						|
            all_active_announcements = [a for a in Announcement.objects.all() if a.is_active]
 | 
						|
        else:
 | 
						|
            all_active_announcements = [a for a in Announcement.objects.filter(logged_in_only=False) if a.is_active]
 | 
						|
        if language is None:
 | 
						|
            return all_active_announcements
 | 
						|
        else:
 | 
						|
            if logged_in:
 | 
						|
                announcements_in_language = Announcement.objects.filter(language=language)
 | 
						|
            else:
 | 
						|
                announcements_in_language = Announcement.objects.filter(language=language, logged_in_only=False)
 | 
						|
            active_announcements_in_language = [a for a in announcements_in_language if a.is_active]
 | 
						|
 | 
						|
            untranslated_announcements = []
 | 
						|
            text_codes = [announcement.text_code for announcement in active_announcements_in_language]
 | 
						|
            for announcement in all_active_announcements:
 | 
						|
                if announcement.language != language and announcement.text_code not in text_codes:
 | 
						|
                    untranslated_announcements.append(announcement)
 | 
						|
            return active_announcements_in_language + untranslated_announcements
 | 
						|
 | 
						|
 | 
						|
class Comment(models.Model):
 | 
						|
    """
 | 
						|
    Class to store comments in markdown content
 | 
						|
    """
 | 
						|
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
 | 
						|
    text = models.TextField(verbose_name="Inhalt")
 | 
						|
    reply_to = models.ForeignKey("self", verbose_name="Antwort auf", blank=True, null=True, on_delete=models.CASCADE)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.user} at {self.created_at.strftime('%H:%M %d.%m.%y')}: {self.text:.10}"
 | 
						|
 | 
						|
    def get_report_url(self):
 | 
						|
        return reverse('report-comment', args=[str(self.id)])
 | 
						|
 | 
						|
    @property
 | 
						|
    def get_absolute_url(self):
 | 
						|
        return self.adoption_notice.get_absolute_url()
 | 
						|
 | 
						|
 | 
						|
class BaseNotification(models.Model):
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
    title = models.CharField(max_length=100)
 | 
						|
    text = models.TextField(verbose_name="Inhalt")
 | 
						|
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
 | 
						|
    read = models.BooleanField(default=False)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"[{self.user}] {self.title} ({self.created_at})"
 | 
						|
 | 
						|
    def get_absolute_url(self):
 | 
						|
        self.user.get_notifications_url()
 | 
						|
 | 
						|
 | 
						|
class CommentNotification(BaseNotification):
 | 
						|
    comment = models.ForeignKey(Comment, on_delete=models.CASCADE, verbose_name=_('Antwort'))
 | 
						|
 | 
						|
    @property
 | 
						|
    def url(self):
 | 
						|
        print(f"URL: self.comment.get_absolute_url()")
 | 
						|
        return self.comment.get_absolute_url
 | 
						|
 | 
						|
 | 
						|
class Subscriptions(models.Model):
 | 
						|
    owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
 | 
						|
    adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"{self.owner} - {self.adoption_notice}"
 | 
						|
 | 
						|
 | 
						|
class Log(models.Model):
 | 
						|
    """
 | 
						|
    Basic class that allows logging random entries for later inspection
 | 
						|
    """
 | 
						|
    user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_("Nutzer*in"))
 | 
						|
    action = models.CharField(max_length=255, verbose_name=_("Aktion"))
 | 
						|
    text = models.CharField(max_length=1000, verbose_name=_("Log text"))
 | 
						|
    created_at = models.DateTimeField(auto_now_add=True)
 | 
						|
 | 
						|
    def __str__(self):
 | 
						|
        return f"[{self.action}] - {self.user} - {self.created_at.strftime('%H:%M:%S %d-%m-%Y ')}"
 | 
						|
 | 
						|
 | 
						|
class Timestamp(models.Model):
 | 
						|
    """
 | 
						|
    Class to store timestamps based on keys
 | 
						|
    """
 | 
						|
    key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
 | 
						|
    timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
 | 
						|
    data = models.CharField(max_length=2000, blank=True, null=True)
 | 
						|
 | 
						|
    def ___str__(self):
 | 
						|
        return f"[{self.key}] - {self.timestamp.strftime('%H:%M:%S %d-%m-%Y ')} - {self.data}"
 |