2024-03-22 11:45:50 +00:00
|
|
|
import uuid
|
|
|
|
|
2024-03-17 10:26:32 +00:00
|
|
|
from django.db import models
|
2024-03-18 21:50:39 +00:00
|
|
|
from django.urls import reverse
|
2024-03-18 07:26:21 +00:00
|
|
|
from django.utils.translation import gettext_lazy as _
|
2024-05-31 07:58:03 +00:00
|
|
|
from django.utils import timezone
|
2024-03-23 21:20:31 +00:00
|
|
|
from django.dispatch import receiver
|
|
|
|
from django.db.models.signals import post_save
|
2024-04-07 07:06:18 +00:00
|
|
|
from django.contrib.auth.models import Group
|
2024-04-07 09:33:41 +00:00
|
|
|
from django.contrib.auth.models import AbstractUser
|
|
|
|
|
2024-06-08 09:44:21 +00:00
|
|
|
from .tools import misc, geo
|
2024-05-10 11:54:16 +00:00
|
|
|
from notfellchen.settings import MEDIA_URL
|
2024-03-18 07:26:21 +00:00
|
|
|
|
|
|
|
|
2024-04-14 13:56:42 +00:00
|
|
|
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')
|
|
|
|
|
|
|
|
|
2024-06-08 07:31:23 +00:00
|
|
|
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"
|
2024-06-08 10:32:57 +00:00
|
|
|
TRUST_LEVEL = {
|
|
|
|
ADMIN: 4,
|
|
|
|
MODERATOR: 3,
|
|
|
|
COORDINATOR: 2,
|
|
|
|
MEMBER: 1,
|
2024-06-08 07:31:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
|
|
|
|
verbose_name=_('Bevorzugte Sprache'))
|
2024-06-08 10:32:57 +00:00
|
|
|
trust_level = models.IntegerField(choices=TRUST_LEVEL, default=TRUST_LEVEL[MEMBER])
|
2024-06-08 07:31:23 +00:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
verbose_name = _('Nutzer*in')
|
|
|
|
verbose_name_plural = _('Nutzer*innen')
|
|
|
|
|
|
|
|
def get_absolute_url(self):
|
|
|
|
return reverse("user-detail", args=[str(self.pk)])
|
|
|
|
|
2024-08-02 17:11:55 +00:00
|
|
|
def get_notifications_url(self):
|
|
|
|
return self.get_absolute_url()
|
|
|
|
|
2024-08-03 06:24:40 +00:00
|
|
|
def get_num_unread_notifications(self):
|
2024-10-10 21:21:07 +00:00
|
|
|
return BaseNotification.objects.filter(user=self, read=False).count()
|
2024-08-03 06:24:40 +00:00
|
|
|
|
2024-08-06 17:28:03 +00:00
|
|
|
@property
|
|
|
|
def owner(self):
|
|
|
|
return self
|
|
|
|
|
2024-06-08 07:31:23 +00:00
|
|
|
|
2024-03-18 16:10:48 +00:00
|
|
|
class Image(models.Model):
|
|
|
|
image = models.ImageField(upload_to='images')
|
|
|
|
alt_text = models.TextField(max_length=2000)
|
2024-08-06 17:28:03 +00:00
|
|
|
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
2024-03-18 16:10:48 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
2024-05-31 11:51:14 +00:00
|
|
|
return self.alt_text
|
2024-03-18 16:10:48 +00:00
|
|
|
|
2024-05-10 11:54:16 +00:00
|
|
|
@property
|
|
|
|
def as_html(self):
|
2024-05-30 11:58:24 +00:00
|
|
|
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
2024-05-10 11:54:16 +00:00
|
|
|
|
2024-03-18 16:10:48 +00:00
|
|
|
|
2024-03-18 07:26:21 +00:00
|
|
|
class Species(models.Model):
|
|
|
|
"""Model representing a species of animal."""
|
2024-04-13 11:25:09 +00:00
|
|
|
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
|
2024-03-18 07:26:21 +00:00
|
|
|
verbose_name=_('Name'))
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
"""String for representing the Model object."""
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
class Meta:
|
2024-04-13 11:25:09 +00:00
|
|
|
verbose_name = _('Tierart')
|
|
|
|
verbose_name_plural = _('Tierarten')
|
2024-03-18 07:26:21 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Location(models.Model):
|
2024-05-31 08:58:57 +00:00
|
|
|
place_id = models.IntegerField()
|
|
|
|
latitude = models.FloatField()
|
|
|
|
longitude = models.FloatField()
|
|
|
|
name = models.CharField(max_length=2000)
|
2024-03-18 07:26:21 +00:00
|
|
|
|
2024-05-31 10:17:53 +00:00
|
|
|
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()
|
2024-05-31 11:51:14 +00:00
|
|
|
geojson = geo_api.get_geojson_for_query(location_string)
|
|
|
|
if geojson is None:
|
|
|
|
return None
|
|
|
|
result = geojson[0]
|
2024-05-31 10:17:53 +00:00
|
|
|
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
|
|
|
|
|
2024-10-10 05:39:44 +00:00
|
|
|
@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()
|
2024-03-18 07:26:21 +00:00
|
|
|
|
2024-10-10 21:21:07 +00:00
|
|
|
|
2024-03-18 07:26:21 +00:00
|
|
|
class RescueOrganization(models.Model):
|
|
|
|
def __str__(self):
|
|
|
|
return f"{self.name}"
|
|
|
|
|
2024-09-28 21:02:13 +00:00
|
|
|
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"
|
|
|
|
}
|
|
|
|
|
2024-03-18 07:26:21 +00:00
|
|
|
name = models.CharField(max_length=200)
|
2024-04-13 11:25:09 +00:00
|
|
|
trusted = models.BooleanField(default=False, verbose_name=_('Vertrauenswürdig'))
|
2024-10-10 21:21:07 +00:00
|
|
|
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'))
|
2024-05-31 08:58:57 +00:00
|
|
|
location_string = models.CharField(max_length=200, verbose_name=_("Ort der Organisation"))
|
2024-09-28 21:02:13 +00:00
|
|
|
location = models.ForeignKey(Location, on_delete=models.PROTECT, blank=True, null=True)
|
2024-04-13 11:25:09 +00:00
|
|
|
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'))
|
2024-03-18 07:26:21 +00:00
|
|
|
website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
|
|
|
|
|
|
|
|
|
2024-03-19 17:18:55 +00:00
|
|
|
class AdoptionNotice(models.Model):
|
2024-04-12 20:23:49 +00:00
|
|
|
class Meta:
|
|
|
|
permissions = [
|
|
|
|
("create_active_adoption_notice", "Can create an active adoption notice"),
|
|
|
|
]
|
|
|
|
|
2024-03-19 17:18:55 +00:00
|
|
|
def __str__(self):
|
|
|
|
return f"{self.name}"
|
|
|
|
|
2024-10-29 16:50:18 +00:00
|
|
|
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
|
|
|
last_checked = models.DateTimeField(verbose_name=_('Zuletzt überprüft am'), default=timezone.now)
|
2024-04-13 11:25:09 +00:00
|
|
|
searching_since = models.DateField(verbose_name=_('Sucht nach einem Zuhause seit'))
|
2024-03-19 17:18:55 +00:00
|
|
|
name = models.CharField(max_length=200)
|
2024-04-13 11:25:09 +00:00
|
|
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
2024-03-19 17:18:55 +00:00
|
|
|
organization = models.ForeignKey(RescueOrganization, blank=True, null=True, on_delete=models.SET_NULL,
|
2024-04-13 11:25:09 +00:00
|
|
|
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'))
|
2024-03-19 17:18:55 +00:00
|
|
|
photos = models.ManyToManyField(Image, blank=True)
|
2024-05-31 08:58:57 +00:00
|
|
|
location_string = models.CharField(max_length=200, verbose_name=_("Ortsangabe"))
|
2024-05-31 13:57:38 +00:00
|
|
|
location = models.ForeignKey(Location, blank=True, null=True, on_delete=models.SET_NULL, )
|
2024-08-06 17:28:03 +00:00
|
|
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Creator'))
|
2024-03-19 17:18:55 +00:00
|
|
|
|
|
|
|
@property
|
2024-03-20 09:35:40 +00:00
|
|
|
def animals(self):
|
|
|
|
return Animal.objects.filter(adoption_notice=self)
|
2024-03-19 17:18:55 +00:00
|
|
|
|
2024-05-30 11:58:24 +00:00
|
|
|
@property
|
|
|
|
def comments(self):
|
|
|
|
return Comment.objects.filter(adoption_notice=self)
|
|
|
|
|
2024-05-31 10:52:57 +00:00
|
|
|
@property
|
|
|
|
def position(self):
|
|
|
|
if self.location is None:
|
|
|
|
return None
|
|
|
|
else:
|
2024-05-31 11:48:22 +00:00
|
|
|
return self.location.latitude, self.location.longitude
|
2024-05-31 10:52:57 +00:00
|
|
|
|
2024-09-27 14:34:44 +00:00
|
|
|
@property
|
|
|
|
def description_short(self):
|
2024-09-27 22:41:51 +00:00
|
|
|
if self.description is None:
|
|
|
|
return ""
|
2024-09-27 14:34:44 +00:00
|
|
|
if len(self.description) > 200:
|
|
|
|
return self.description[:200] + f" ... [weiterlesen]({self.get_absolute_url()})"
|
|
|
|
|
2024-03-20 09:32:00 +00:00
|
|
|
def get_absolute_url(self):
|
2024-09-30 13:58:51 +00:00
|
|
|
"""Returns the url to access a detailed page for the adoption notice."""
|
2024-03-20 09:32:00 +00:00
|
|
|
return reverse('adoption-notice-detail', args=[str(self.id)])
|
|
|
|
|
2024-03-22 11:53:39 +00:00
|
|
|
def get_report_url(self):
|
2024-09-30 13:58:51 +00:00
|
|
|
"""Returns the url to report an adoption notice."""
|
2024-03-22 11:53:39 +00:00
|
|
|
return reverse('report-adoption-notice', args=[str(self.id)])
|
|
|
|
|
2024-08-02 18:03:25 +00:00
|
|
|
def get_subscriptions(self):
|
2024-09-30 13:58:51 +00:00
|
|
|
# returns all subscriptions to that adoption notice
|
2024-08-02 18:03:25 +00:00
|
|
|
return Subscriptions.objects.filter(adoption_notice=self)
|
|
|
|
|
2024-03-20 14:40:11 +00:00
|
|
|
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]
|
|
|
|
|
2024-05-31 10:53:06 +00:00
|
|
|
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
|
|
|
|
|
2024-05-31 13:47:22 +00:00
|
|
|
@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>"
|
|
|
|
|
2024-05-31 14:00:52 +00:00
|
|
|
@property
|
|
|
|
def is_active(self):
|
2024-05-31 14:25:50 +00:00
|
|
|
if not hasattr(self, 'adoptionnoticestatus'):
|
|
|
|
return False
|
|
|
|
return self.adoptionnoticestatus.is_active
|
2024-10-10 21:21:07 +00:00
|
|
|
|
2024-10-26 06:52:59 +00:00
|
|
|
@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)
|
|
|
|
|
2024-09-30 13:22:19 +00:00
|
|
|
def set_checked(self):
|
2024-10-29 16:50:18 +00:00
|
|
|
self.last_checked = timezone.now()
|
2024-09-30 13:22:19 +00:00
|
|
|
self.save()
|
2024-10-10 21:21:07 +00:00
|
|
|
|
2024-09-30 13:22:19 +00:00
|
|
|
def set_closed(self):
|
2024-10-29 16:50:18 +00:00
|
|
|
self.last_checked = timezone.now()
|
2024-09-30 13:22:19 +00:00
|
|
|
self.adoptionnoticestatus.set_closed()
|
2024-05-31 14:25:50 +00:00
|
|
|
|
2024-10-29 05:52:43 +00:00
|
|
|
def set_active(self):
|
2024-10-29 16:50:18 +00:00
|
|
|
self.last_checked = timezone.now()
|
2024-10-29 05:52:43 +00:00
|
|
|
if not hasattr(self, 'adoptionnoticestatus'):
|
|
|
|
AdoptionNoticeStatus.create_other(self)
|
|
|
|
self.adoptionnoticestatus.set_active()
|
|
|
|
|
|
|
|
def set_to_review(self):
|
2024-10-29 16:50:18 +00:00
|
|
|
self.last_checked = timezone.now()
|
2024-10-29 05:52:43 +00:00
|
|
|
if not hasattr(self, 'adoptionnoticestatus'):
|
|
|
|
AdoptionNoticeStatus.create_other(self)
|
|
|
|
self.adoptionnoticestatus.set_to_review()
|
|
|
|
|
2024-05-31 14:25:50 +00:00
|
|
|
|
|
|
|
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"
|
2024-06-08 10:32:57 +00:00
|
|
|
AWAITING_ACTION = "awaiting_action"
|
2024-05-31 14:25:50 +00:00
|
|
|
CLOSED = "closed"
|
|
|
|
DISABLED = "disabled"
|
|
|
|
MAJOR_STATUS_CHOICES = {
|
|
|
|
ACTIVE: "active",
|
2024-06-08 10:32:57 +00:00
|
|
|
AWAITING_ACTION: "in review",
|
2024-05-31 14:25:50 +00:00
|
|
|
CLOSED: "closed",
|
|
|
|
DISABLED: "disabled",
|
|
|
|
}
|
|
|
|
|
|
|
|
MINOR_STATUS_CHOICES = {
|
|
|
|
ACTIVE: {
|
|
|
|
"searching": "searching",
|
|
|
|
"interested": "interested",
|
|
|
|
},
|
2024-06-08 10:32:57 +00:00
|
|
|
AWAITING_ACTION: {
|
2024-05-31 14:25:50 +00:00
|
|
|
"waiting_for_review": "waiting_for_review",
|
2024-06-08 10:32:57 +00:00
|
|
|
"needs_additional_info": "needs_additional_info",
|
2024-05-31 14:25:50 +00:00
|
|
|
},
|
|
|
|
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",
|
2024-10-10 15:06:29 +00:00
|
|
|
"unchecked": "unchecked",
|
2024-05-31 14:25:50 +00:00
|
|
|
"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
|
|
|
|
|
2024-10-26 06:52:59 +00:00
|
|
|
@property
|
|
|
|
def is_to_be_checked(self):
|
|
|
|
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
|
|
|
|
2024-05-31 14:25:50 +00:00
|
|
|
@staticmethod
|
|
|
|
def get_minor_choices(major_status):
|
|
|
|
return AdoptionNoticeStatus.MINOR_STATUS_CHOICES[major_status]
|
2024-05-31 14:00:52 +00:00
|
|
|
|
2024-10-29 05:52:43 +00:00
|
|
|
@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)
|
|
|
|
|
2024-09-30 13:22:19 +00:00
|
|
|
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()
|
|
|
|
|
2024-10-10 15:06:29 +00:00
|
|
|
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()
|
|
|
|
|
2024-10-29 05:52:43 +00:00
|
|
|
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()
|
|
|
|
|
2024-05-31 10:53:06 +00:00
|
|
|
|
2024-03-18 07:26:21 +00:00
|
|
|
class Animal(models.Model):
|
2024-03-19 05:15:38 +00:00
|
|
|
MALE_NEUTERED = "M_N"
|
|
|
|
MALE = "M"
|
|
|
|
FEMALE_NEUTERED = "F_N"
|
|
|
|
FEMALE = "F"
|
|
|
|
SEX_CHOICES = {
|
2024-03-20 15:40:52 +00:00
|
|
|
MALE_NEUTERED: "neutered male",
|
2024-03-19 05:15:38 +00:00
|
|
|
MALE: "male",
|
2024-03-20 15:40:52 +00:00
|
|
|
FEMALE_NEUTERED: "neutered female",
|
2024-03-19 05:15:38 +00:00
|
|
|
FEMALE: "female",
|
|
|
|
}
|
|
|
|
|
2024-04-13 11:25:09 +00:00
|
|
|
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
2024-03-19 05:15:38 +00:00
|
|
|
name = models.CharField(max_length=200)
|
2024-04-13 11:25:09 +00:00
|
|
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
2024-03-19 05:15:38 +00:00
|
|
|
species = models.ForeignKey(Species, on_delete=models.PROTECT)
|
|
|
|
photos = models.ManyToManyField(Image, blank=True)
|
|
|
|
sex = models.CharField(max_length=20, choices=SEX_CHOICES, )
|
2024-03-19 17:18:55 +00:00
|
|
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE)
|
2024-08-06 17:28:03 +00:00
|
|
|
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
2024-03-19 05:15:38 +00:00
|
|
|
|
2024-03-18 07:26:21 +00:00
|
|
|
def __str__(self):
|
|
|
|
return f"{self.name}"
|
|
|
|
|
2024-03-19 05:15:38 +00:00
|
|
|
@property
|
|
|
|
def age(self):
|
2024-10-29 16:50:18 +00:00
|
|
|
return timezone.now().today().date() - self.date_of_birth
|
2024-03-19 05:15:38 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def hr_age(self):
|
|
|
|
"""Returns a human-readable age based on the date of birth."""
|
|
|
|
return misc.age_as_hr_string(self.age)
|
|
|
|
|
2024-03-20 15:40:52 +00:00
|
|
|
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
|
|
|
|
"""
|
2024-03-20 12:40:00 +00:00
|
|
|
return self.photos.all()
|
|
|
|
|
2024-03-18 21:50:39 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
"""Returns the url to access a detailed page for the animal."""
|
|
|
|
return reverse('animal-detail', args=[str(self.id)])
|
|
|
|
|
2024-03-18 13:26:50 +00:00
|
|
|
|
2024-03-20 10:02:24 +00:00
|
|
|
class Rule(models.Model):
|
|
|
|
"""
|
|
|
|
Class to store rules
|
|
|
|
"""
|
|
|
|
title = models.CharField(max_length=200)
|
|
|
|
|
|
|
|
# Markdown is allowed in rule text
|
|
|
|
rule_text = models.TextField()
|
2024-04-14 13:56:42 +00:00
|
|
|
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)
|
2024-03-20 10:02:24 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.title
|
|
|
|
|
2024-03-22 11:45:50 +00:00
|
|
|
|
|
|
|
class Report(models.Model):
|
2024-04-12 20:23:49 +00:00
|
|
|
class Meta:
|
|
|
|
permissions = []
|
|
|
|
|
2024-03-22 11:45:50 +00:00
|
|
|
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)
|
2024-05-30 13:46:51 +00:00
|
|
|
reported_broken_rules = models.ManyToManyField(Rule)
|
|
|
|
user_comment = models.TextField(blank=True)
|
2024-03-22 11:45:50 +00:00
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
|
|
|
|
def __str__(self):
|
2024-05-30 13:46:51 +00:00
|
|
|
return f"[{self.status}]: {self.user_comment:.20}"
|
2024-03-22 11:45:50 +00:00
|
|
|
|
2024-03-25 09:49:56 +00:00
|
|
|
def get_absolute_url(self):
|
|
|
|
"""Returns the url to access a detailed page for the report."""
|
|
|
|
return reverse('report-detail', args=[str(self.id)])
|
|
|
|
|
2024-03-22 11:45:50 +00:00
|
|
|
def get_reported_rules(self):
|
|
|
|
return self.reported_broken_rules.all()
|
|
|
|
|
|
|
|
def get_moderation_actions(self):
|
|
|
|
return ModerationAction.objects.filter(report=self)
|
|
|
|
|
|
|
|
|
2024-05-30 13:46:51 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2024-03-22 11:45:50 +00:00
|
|
|
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}"
|
2024-03-23 21:20:31 +00:00
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
Membership
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
2024-04-14 11:26:21 +00:00
|
|
|
class Text(models.Model):
|
|
|
|
"""
|
|
|
|
Base class to store markdown content
|
|
|
|
"""
|
|
|
|
title = models.CharField(max_length=100)
|
2024-04-14 11:37:32 +00:00
|
|
|
content = models.TextField(verbose_name="Inhalt")
|
2024-04-14 12:14:35 +00:00
|
|
|
language = models.ForeignKey(Language, verbose_name="Sprache", on_delete=models.PROTECT)
|
2024-04-14 11:37:32 +00:00
|
|
|
text_code = models.CharField(max_length=24, verbose_name="Text code", blank=True)
|
2024-04-14 11:26:21 +00:00
|
|
|
|
|
|
|
class Meta:
|
2024-04-14 11:37:32 +00:00
|
|
|
verbose_name = "Text"
|
|
|
|
verbose_name_plural = "Texte"
|
2024-04-14 11:26:21 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
2024-04-14 12:14:35 +00:00
|
|
|
return f"{self.title} ({self.language})"
|
2024-05-30 11:58:24 +00:00
|
|
|
|
2024-10-05 09:02:26 +00:00
|
|
|
@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
|
|
|
|
|
|
|
|
|
2024-05-31 07:58:03 +00:00
|
|
|
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)
|
2024-10-05 09:02:37 +00:00
|
|
|
publish_start_time = models.DateTimeField(verbose_name="Veröffentlichungszeitpunkt")
|
2024-05-31 07:58:03 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
|
2024-05-30 11:58:24 +00:00
|
|
|
class Comment(models.Model):
|
|
|
|
"""
|
|
|
|
Class to store comments in markdown content
|
|
|
|
"""
|
2024-06-08 07:31:23 +00:00
|
|
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
2024-05-30 11:58:24 +00:00
|
|
|
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}"
|
2024-05-30 13:46:51 +00:00
|
|
|
|
|
|
|
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()
|
2024-08-02 17:11:55 +00:00
|
|
|
|
|
|
|
|
|
|
|
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'))
|
2024-08-02 17:31:32 +00:00
|
|
|
|
2024-08-03 07:02:09 +00:00
|
|
|
@property
|
|
|
|
def url(self):
|
|
|
|
print(f"URL: self.comment.get_absolute_url()")
|
|
|
|
return self.comment.get_absolute_url
|
|
|
|
|
2024-08-02 17:31:32 +00:00
|
|
|
|
|
|
|
class Subscriptions(models.Model):
|
2024-08-06 17:28:03 +00:00
|
|
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
2024-08-02 17:31:32 +00:00
|
|
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
2024-08-02 18:03:25 +00:00
|
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
2024-09-29 21:35:18 +00:00
|
|
|
|
|
|
|
def __str__(self):
|
2024-10-10 21:21:07 +00:00
|
|
|
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):
|
2024-10-19 17:45:42 +00:00
|
|
|
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}"
|