Compare commits
31 Commits
2589f1c703
...
develop
Author | SHA1 | Date | |
---|---|---|---|
a372be4af2 | |||
5d333b28ab | |||
84ad047c01 | |||
c93b2631cb | |||
15dd06a91f | |||
30ff26c7ef | |||
1434e7502a | |||
93b21fb7d0 | |||
e5c82f392c | |||
0626964461 | |||
23a724e390 | |||
2a9c7cf854 | |||
335630e16d | |||
6051f7c294 | |||
c1ea6cd211 | |||
6c43b46007 | |||
dc9e68c4b9 | |||
4b03f99971 | |||
426f4b3d8b | |||
3604233507 | |||
8c5099f14a | |||
d5bc348453 | |||
bce98cb439 | |||
1ed3d27533 | |||
39a098af8e | |||
62491b84c1 | |||
81f7f5bb5d | |||
8ce4122160 | |||
370ad2ce66 | |||
f25c425d85 | |||
d921623f31 |
@@ -147,3 +147,11 @@ class AdoptionNoticeSearchForm(forms.Form):
|
|||||||
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
||||||
label=_("Suchradius"))
|
label=_("Suchradius"))
|
||||||
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||||
|
|
||||||
|
|
||||||
|
class RescueOrgSearchForm(forms.Form):
|
||||||
|
template_name = "fellchensammlung/forms/form_snippets.html"
|
||||||
|
|
||||||
|
location_string = forms.CharField(max_length=100, label=_("Stadt"), required=False)
|
||||||
|
max_distance = forms.ChoiceField(choices=DistanceChoices, initial=DistanceChoices.ONE_HUNDRED,
|
||||||
|
label=_("Suchradius"))
|
||||||
|
@@ -17,7 +17,7 @@ def notify_mods_new_report(report, notification_type):
|
|||||||
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
|
||||||
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
if notification_type == NotificationTypeChoices.NEW_REPORT_AN:
|
||||||
title = _("Vermittlung gemeldet")
|
title = _("Vermittlung gemeldet")
|
||||||
elif notification_type == NotificationTypeChoices.NEW_COMMENT:
|
elif notification_type == NotificationTypeChoices.NEW_REPORT_COMMENT:
|
||||||
title = _("Kommentar gemeldet")
|
title = _("Kommentar gemeldet")
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@@ -0,0 +1,13 @@
|
|||||||
|
from django.core.management import BaseCommand
|
||||||
|
from fellchensammlung.tools.admin import mask_organization_contact_data
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Mask e-mail addresses and phone numbers of organizations for testing purposes.'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("domain", type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
domain = options["domain"]
|
||||||
|
mask_organization_contact_data(domain)
|
19
src/fellchensammlung/management/commands/sync_to_twenty.py
Normal file
19
src/fellchensammlung/management/commands/sync_to_twenty.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from django.core.management import BaseCommand
|
||||||
|
from tqdm import tqdm
|
||||||
|
|
||||||
|
from fellchensammlung.models import RescueOrganization
|
||||||
|
from fellchensammlung.tools.twenty import sync_rescue_org_to_twenty
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Send rescue organizations as companies to twenty'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("base_url", type=str)
|
||||||
|
parser.add_argument("token", type=str)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
base_url = options["base_url"]
|
||||||
|
token = options["token"]
|
||||||
|
for rescue_org in tqdm(RescueOrganization.objects.all()):
|
||||||
|
sync_rescue_org_to_twenty(rescue_org, base_url, token)
|
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.2.1 on 2025-08-02 09:33
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('fellchensammlung', '0058_socialmediapost'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rescueorganization',
|
||||||
|
name='twenty_id',
|
||||||
|
field=models.UUIDField(blank=True, help_text='ID der der Organisation in Twenty', null=True, verbose_name='Twenty-ID'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rescueorganization',
|
||||||
|
name='specializations',
|
||||||
|
field=models.ManyToManyField(blank=True, to='fellchensammlung.species'),
|
||||||
|
),
|
||||||
|
]
|
@@ -60,6 +60,10 @@ class Location(models.Model):
|
|||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Standort")
|
||||||
|
verbose_name_plural = _("Standorte")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.city and self.postcode:
|
if self.city and self.postcode:
|
||||||
return f"{self.city} ({self.postcode})"
|
return f"{self.city} ({self.postcode})"
|
||||||
@@ -103,6 +107,10 @@ class Location(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ImportantLocation(models.Model):
|
class ImportantLocation(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Wichtiger Standort")
|
||||||
|
verbose_name_plural = _("Wichtige Standorte")
|
||||||
|
|
||||||
location = models.OneToOneField(Location, on_delete=models.CASCADE)
|
location = models.OneToOneField(Location, on_delete=models.CASCADE)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
@@ -173,10 +181,14 @@ class RescueOrganization(models.Model):
|
|||||||
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
|
parent_org = models.ForeignKey("RescueOrganization", on_delete=models.PROTECT, blank=True, null=True)
|
||||||
# allows to specify if a rescue organization has a specialization for dedicated species
|
# allows to specify if a rescue organization has a specialization for dedicated species
|
||||||
specializations = models.ManyToManyField(Species, blank=True)
|
specializations = models.ManyToManyField(Species, blank=True)
|
||||||
|
twenty_id = models.UUIDField(verbose_name=_("Twenty-ID"), null=True, blank=True,
|
||||||
|
help_text=_("ID der der Organisation in Twenty"))
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ('external_object_identifier', 'external_source_identifier',)
|
unique_together = ('external_object_identifier', 'external_source_identifier',)
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
verbose_name = _("Tierschutzorganisation")
|
||||||
|
verbose_name_plural = _("Tierschutzorganisationen")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
@@ -204,6 +216,18 @@ class RescueOrganization(models.Model):
|
|||||||
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
|
adoption_notices_discovered.extend(child.adoption_notices_in_hierarchy)
|
||||||
return adoption_notices_discovered
|
return adoption_notices_discovered
|
||||||
|
|
||||||
|
@property
|
||||||
|
def adoption_notices_in_hierarchy_divided_by_status(self):
|
||||||
|
"""Returns two lists of adoption notices, the first active, the other inactive."""
|
||||||
|
active_adoption_notices = []
|
||||||
|
inactive_adoption_notices = []
|
||||||
|
for an in self.adoption_notices_in_hierarchy:
|
||||||
|
if an.is_active:
|
||||||
|
active_adoption_notices.append(an)
|
||||||
|
else:
|
||||||
|
inactive_adoption_notices.append(an)
|
||||||
|
return active_adoption_notices, inactive_adoption_notices
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def position(self):
|
def position(self):
|
||||||
if self.location:
|
if self.location:
|
||||||
@@ -248,6 +272,14 @@ class RescueOrganization(models.Model):
|
|||||||
def child_organizations(self):
|
def child_organizations(self):
|
||||||
return RescueOrganization.objects.filter(parent_org=self)
|
return RescueOrganization.objects.filter(parent_org=self)
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
return geo.object_in_distance(self, position, max_distance, unknown_true)
|
||||||
|
|
||||||
|
|
||||||
# Admins can perform all actions and have the highest trust associated with them
|
# Admins can perform all actions and have the highest trust associated with them
|
||||||
# Moderators can make moderation decisions regarding the deletion of content
|
# Moderators can make moderation decisions regarding the deletion of content
|
||||||
@@ -325,6 +357,10 @@ class Image(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.alt_text
|
return self.alt_text
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Bild")
|
||||||
|
verbose_name_plural = _("Bilder")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_html(self):
|
def as_html(self):
|
||||||
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
|
||||||
@@ -335,11 +371,11 @@ class AdoptionNotice(models.Model):
|
|||||||
permissions = [
|
permissions = [
|
||||||
("create_active_adoption_notice", "Can create an active adoption notice"),
|
("create_active_adoption_notice", "Can create an active adoption notice"),
|
||||||
]
|
]
|
||||||
|
verbose_name = _("Vermittlung")
|
||||||
|
verbose_name_plural = _("Vermittlungen")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
|
||||||
return self.name
|
return self.name
|
||||||
return f"[{self.adoptionnoticestatus.as_string()}] {self.name}"
|
|
||||||
|
|
||||||
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
created_at = models.DateField(verbose_name=_('Erstellt am'), default=timezone.now)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -472,11 +508,7 @@ class AdoptionNotice(models.Model):
|
|||||||
|
|
||||||
If the location is none, we by default return that the location is within the given distance
|
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 geo.object_in_distance(self, position, max_distance, unknown_true)
|
||||||
return True
|
|
||||||
|
|
||||||
distance = geo.calculate_distance_between_coordinates(self.position, position)
|
|
||||||
return distance < max_distance
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_active(self):
|
def is_active(self):
|
||||||
@@ -496,6 +528,18 @@ class AdoptionNotice(models.Model):
|
|||||||
return False
|
return False
|
||||||
return self.adoptionnoticestatus.is_closed
|
return self.adoptionnoticestatus.is_closed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_interested(self):
|
||||||
|
if not hasattr(self, 'adoptionnoticestatus'):
|
||||||
|
return False
|
||||||
|
return self.adoptionnoticestatus.is_interested
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_awaiting_action(self):
|
||||||
|
if not hasattr(self, 'adoptionnoticestatus'):
|
||||||
|
return False
|
||||||
|
return self.adoptionnoticestatus.is_awaiting_action
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_disabled_unchecked(self):
|
def is_disabled_unchecked(self):
|
||||||
if not hasattr(self, 'adoptionnoticestatus'):
|
if not hasattr(self, 'adoptionnoticestatus'):
|
||||||
@@ -545,6 +589,10 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
whereas the minor status is used for reporting
|
whereas the minor status is used for reporting
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Vermittlungsstatus')
|
||||||
|
verbose_name_plural = _('Vermittlungsstati')
|
||||||
|
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
AWAITING_ACTION = "awaiting_action"
|
AWAITING_ACTION = "awaiting_action"
|
||||||
CLOSED = "closed"
|
CLOSED = "closed"
|
||||||
@@ -609,6 +657,14 @@ class AdoptionNoticeStatus(models.Model):
|
|||||||
def is_closed(self):
|
def is_closed(self):
|
||||||
return self.major_status == self.CLOSED
|
return self.major_status == self.CLOSED
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_awaiting_action(self):
|
||||||
|
return self.major_status == self.AWAITING_ACTION
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_interested(self):
|
||||||
|
return self.major_status == self.ACTIVE and self.minor_status == "interested"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_disabled_unchecked(self):
|
def is_disabled_unchecked(self):
|
||||||
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
return self.major_status == self.DISABLED and self.minor_status == "unchecked"
|
||||||
@@ -663,6 +719,10 @@ class SexChoicesWithAll(models.TextChoices):
|
|||||||
|
|
||||||
|
|
||||||
class Animal(models.Model):
|
class Animal(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Tier')
|
||||||
|
verbose_name_plural = _('Tiere')
|
||||||
|
|
||||||
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
date_of_birth = models.DateField(verbose_name=_('Geburtsdatum'))
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung'))
|
||||||
@@ -728,6 +788,11 @@ class SearchSubscription(models.Model):
|
|||||||
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
|
- On new AdoptionNotice: Check all existing SearchSubscriptions for matches
|
||||||
- For matches: Send notification to user of the SearchSubscription
|
- For matches: Send notification to user of the SearchSubscription
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Abonnierte Suche")
|
||||||
|
verbose_name_plural = _("Abonnierte Suchen")
|
||||||
|
|
||||||
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
|
location = models.ForeignKey(Location, on_delete=models.PROTECT, null=True)
|
||||||
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
|
sex = models.CharField(max_length=20, choices=SexChoicesWithAll.choices)
|
||||||
@@ -746,6 +811,11 @@ class Rule(models.Model):
|
|||||||
"""
|
"""
|
||||||
Class to store rules
|
Class to store rules
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Regel")
|
||||||
|
verbose_name_plural = _("Regeln")
|
||||||
|
|
||||||
title = models.CharField(max_length=200)
|
title = models.CharField(max_length=200)
|
||||||
|
|
||||||
# Markdown is allowed in rule text
|
# Markdown is allowed in rule text
|
||||||
@@ -762,7 +832,8 @@ class Rule(models.Model):
|
|||||||
|
|
||||||
class Report(models.Model):
|
class Report(models.Model):
|
||||||
class Meta:
|
class Meta:
|
||||||
permissions = []
|
verbose_name = _("Meldung")
|
||||||
|
verbose_name_plural = _("Meldungen")
|
||||||
|
|
||||||
ACTION_TAKEN = "action taken"
|
ACTION_TAKEN = "action taken"
|
||||||
NO_ACTION_TAKEN = "no action taken"
|
NO_ACTION_TAKEN = "no action taken"
|
||||||
@@ -842,6 +913,10 @@ class ReportComment(Report):
|
|||||||
|
|
||||||
|
|
||||||
class ModerationAction(models.Model):
|
class ModerationAction(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Moderationsaktion")
|
||||||
|
verbose_name_plural = _("Moderationsaktionen")
|
||||||
|
|
||||||
BAN = "user_banned"
|
BAN = "user_banned"
|
||||||
DELETE = "content_deleted"
|
DELETE = "content_deleted"
|
||||||
COMMENT = "comment"
|
COMMENT = "comment"
|
||||||
@@ -906,6 +981,11 @@ class Announcement(Text):
|
|||||||
"""
|
"""
|
||||||
Class to store announcements that should be displayed for all users
|
Class to store announcements that should be displayed for all users
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Banner")
|
||||||
|
verbose_name_plural = _("Banner")
|
||||||
|
|
||||||
logged_in_only = models.BooleanField(default=False)
|
logged_in_only = models.BooleanField(default=False)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -955,6 +1035,11 @@ class Comment(models.Model):
|
|||||||
"""
|
"""
|
||||||
Class to store comments in markdown content
|
Class to store comments in markdown content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Kommentar")
|
||||||
|
verbose_name_plural = _("Kommentare")
|
||||||
|
|
||||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
@@ -974,6 +1059,10 @@ class Comment(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Notification(models.Model):
|
class Notification(models.Model):
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Benachrichtigung")
|
||||||
|
verbose_name_plural = _("Benachrichtigungen")
|
||||||
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
notification_type = models.CharField(max_length=200,
|
notification_type = models.CharField(max_length=200,
|
||||||
@@ -1017,6 +1106,12 @@ class Notification(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class Subscriptions(models.Model):
|
class Subscriptions(models.Model):
|
||||||
|
"""Subscription to a AdoptionNotice"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Abonnement")
|
||||||
|
verbose_name_plural = _("Abonnements")
|
||||||
|
|
||||||
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
owner = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name=_('Nutzer*in'))
|
||||||
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
adoption_notice = models.ForeignKey(AdoptionNotice, on_delete=models.CASCADE, verbose_name=_('AdoptionNotice'))
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
@@ -1044,6 +1139,11 @@ class Timestamp(models.Model):
|
|||||||
"""
|
"""
|
||||||
Class to store timestamps based on keys
|
Class to store timestamps based on keys
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Zeitstempel")
|
||||||
|
verbose_name_plural = _("Zeitstempel")
|
||||||
|
|
||||||
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
|
key = models.CharField(max_length=255, verbose_name=_("Schlüssel"), primary_key=True)
|
||||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
|
timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("Zeitstempel"))
|
||||||
data = models.CharField(max_length=2000, blank=True, null=True)
|
data = models.CharField(max_length=2000, blank=True, null=True)
|
||||||
@@ -1056,6 +1156,11 @@ class SpeciesSpecificURL(models.Model):
|
|||||||
"""
|
"""
|
||||||
Model that allows to specify a URL for a rescue organization where a certain species can be found
|
Model that allows to specify a URL for a rescue organization where a certain species can be found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("Tierartspezifische URL")
|
||||||
|
verbose_name_plural = _("Tierartspezifische URLs")
|
||||||
|
|
||||||
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
species = models.ForeignKey(Species, on_delete=models.CASCADE, verbose_name=_("Tierart"))
|
||||||
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
rescue_organization = models.ForeignKey(RescueOrganization, on_delete=models.CASCADE,
|
||||||
verbose_name=_("Tierschutzorganisation"))
|
verbose_name=_("Tierschutzorganisation"))
|
||||||
|
11
src/fellchensammlung/static/fellchensammlung/js/mousetrap.min.js
vendored
Normal file
11
src/fellchensammlung/static/fellchensammlung/js/mousetrap.min.js
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
/* mousetrap v1.6.5 craig.is/killing/mice */
|
||||||
|
(function(q,u,c){function v(a,b,g){a.addEventListener?a.addEventListener(b,g,!1):a.attachEvent("on"+b,g)}function z(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return n[a.which]?n[a.which]:r[a.which]?r[a.which]:String.fromCharCode(a.which).toLowerCase()}function F(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function w(a){return"shift"==a||"ctrl"==a||"alt"==a||
|
||||||
|
"meta"==a}function A(a,b){var g,d=[];var e=a;"+"===e?e=["+"]:(e=e.replace(/\+{2}/g,"+plus"),e=e.split("+"));for(g=0;g<e.length;++g){var m=e[g];B[m]&&(m=B[m]);b&&"keypress"!=b&&C[m]&&(m=C[m],d.push("shift"));w(m)&&d.push(m)}e=m;g=b;if(!g){if(!p){p={};for(var c in n)95<c&&112>c||n.hasOwnProperty(c)&&(p[n[c]]=c)}g=p[e]?"keydown":"keypress"}"keypress"==g&&d.length&&(g="keydown");return{key:m,modifiers:d,action:g}}function D(a,b){return null===a||a===u?!1:a===b?!0:D(a.parentNode,b)}function d(a){function b(a){a=
|
||||||
|
a||{};var b=!1,l;for(l in p)a[l]?b=!0:p[l]=0;b||(x=!1)}function g(a,b,t,f,g,d){var l,E=[],h=t.type;if(!k._callbacks[a])return[];"keyup"==h&&w(a)&&(b=[a]);for(l=0;l<k._callbacks[a].length;++l){var c=k._callbacks[a][l];if((f||!c.seq||p[c.seq]==c.level)&&h==c.action){var e;(e="keypress"==h&&!t.metaKey&&!t.ctrlKey)||(e=c.modifiers,e=b.sort().join(",")===e.sort().join(","));e&&(e=f&&c.seq==f&&c.level==d,(!f&&c.combo==g||e)&&k._callbacks[a].splice(l,1),E.push(c))}}return E}function c(a,b,c,f){k.stopCallback(b,
|
||||||
|
b.target||b.srcElement,c,f)||!1!==a(b,c)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=z(a);b&&("keyup"==a.type&&y===b?y=!1:k.handleKey(b,F(a),a))}function m(a,g,t,f){function h(c){return function(){x=c;++p[a];clearTimeout(q);q=setTimeout(b,1E3)}}function l(g){c(t,g,a);"keyup"!==f&&(y=z(g));setTimeout(b,10)}for(var d=p[a]=0;d<g.length;++d){var e=d+1===g.length?l:h(f||
|
||||||
|
A(g[d+1]).action);n(g[d],e,f,a,d)}}function n(a,b,c,f,d){k._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var e=a.split(" ");1<e.length?m(a,e,b,c):(c=A(a,c),k._callbacks[c.key]=k._callbacks[c.key]||[],g(c.key,c.modifiers,{type:c.action},f,a,d),k._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:d,combo:a}))}var k=this;a=a||u;if(!(k instanceof d))return new d(a);k.target=a;k._callbacks={};k._directMap={};var p={},q,y=!1,r=!1,x=!1;k._handleKey=function(a,
|
||||||
|
d,e){var f=g(a,d,e),h;d={};var k=0,l=!1;for(h=0;h<f.length;++h)f[h].seq&&(k=Math.max(k,f[h].level));for(h=0;h<f.length;++h)f[h].seq?f[h].level==k&&(l=!0,d[f[h].seq]=1,c(f[h].callback,e,f[h].combo,f[h].seq)):l||c(f[h].callback,e,f[h].combo);f="keypress"==e.type&&r;e.type!=x||w(a)||f||b(d);r=l&&"keydown"==e.type};k._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)n(a[d],b,c)};v(a,"keypress",e);v(a,"keydown",e);v(a,"keyup",e)}if(q){var n={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",
|
||||||
|
18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",224:"meta"},r={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},C={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},B={option:"alt",command:"meta","return":"enter",
|
||||||
|
escape:"esc",plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},p;for(c=1;20>c;++c)n[111+c]="f"+c;for(c=0;9>=c;++c)n[c+96]=c.toString();d.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};d.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};d.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};d.prototype.reset=function(){this._callbacks={};
|
||||||
|
this._directMap={};return this};d.prototype.stopCallback=function(a,b){if(-1<(" "+b.className+" ").indexOf(" mousetrap ")||D(b,this.target))return!1;if("composedPath"in a&&"function"===typeof a.composedPath){var c=a.composedPath()[0];c!==a.target&&(b=c)}return"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};d.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};d.addKeycodes=function(a){for(var b in a)a.hasOwnProperty(b)&&(n[b]=a[b]);p=null};
|
||||||
|
d.init=function(){var a=d(u),b;for(b in a)"_"!==b.charAt(0)&&(d[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};d.init();q.Mousetrap=d;"undefined"!==typeof module&&module.exports&&(module.exports=d);"function"===typeof define&&define.amd&&define(function(){return d})}})("undefined"!==typeof window?window:null,"undefined"!==typeof window?document:null);
|
@@ -0,0 +1,15 @@
|
|||||||
|
function mark_checked(index) {
|
||||||
|
document.getElementById('mark_checked_'+index).submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function open_information(index) {
|
||||||
|
let link = document.getElementById('species_url_'+index+'_1');
|
||||||
|
if (!link) {
|
||||||
|
link = document.getElementById('rescue_org_website_'+index);
|
||||||
|
}
|
||||||
|
window.open(link.href);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mousetrap.bind('c', function() { mark_checked(1); });
|
||||||
|
|
||||||
|
Mousetrap.bind('o', function() { open_information(1); });
|
@@ -22,7 +22,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Looks for all notifications with a delete and allows closing them when pressing delete
|
// Looks for all notifications with a delete and allows closing them when pressing delete
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
(document.querySelectorAll('.notification .delete') || []).forEach(($delete) => {
|
(document.querySelectorAll('.notification .delete:not(.js-delete-excluded)') || []).forEach(($delete) => {
|
||||||
const $notification = $delete.parentNode;
|
const $notification = $delete.parentNode;
|
||||||
|
|
||||||
$delete.addEventListener('click', () => {
|
$delete.addEventListener('click', () => {
|
||||||
|
@@ -30,7 +30,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-header-title">{{ faq.title }}</h2>
|
<h2 class="card-header-title">{{ faq.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
{{ faq.content | render_markdown }}
|
{{ faq.content | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,11 +16,27 @@
|
|||||||
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
|
{% block canonical_url %}{% host %}{% url 'rescue-organizations' %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="block">
|
<div class="columns block">
|
||||||
|
<div class=" column {% if show_search %}is-two-thirds {% endif %}">
|
||||||
<div style="height: 70vh">
|
<div style="height: 70vh">
|
||||||
{% include "fellchensammlung/partials/partial-map.html" %}
|
{% include "fellchensammlung/partials/partial-map.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if show_search %}
|
||||||
|
<div class="column is-one-third">
|
||||||
|
<form method="GET" autocomplete="off">
|
||||||
|
<input type="hidden" name="longitude" maxlength="200" id="longitude">
|
||||||
|
<input type="hidden" name="latitude" maxlength="200" id="latitude">
|
||||||
|
<!--- https://docs.djangoproject.com/en/5.2/topics/forms/#reusable-form-templates -->
|
||||||
|
{{ search_form }}
|
||||||
|
<button class="button is-primary is-fullwidth" type="submit" value="search" name="action">
|
||||||
|
<i class="fas fa-search fa-fw"></i> {% trans 'Suchen' %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="block">
|
<div class="block">
|
||||||
{% with rescue_organizations=rescue_organizations_to_list %}
|
{% with rescue_organizations=rescue_organizations_to_list %}
|
||||||
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
{% include "fellchensammlung/lists/list-animal-shelters.html" %}
|
||||||
@@ -29,16 +45,17 @@
|
|||||||
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
|
<nav class="pagination" role="navigation" aria-label="{% trans 'Paginierung' %}">
|
||||||
{% if rescue_organizations_to_list.has_previous %}
|
{% if rescue_organizations_to_list.has_previous %}
|
||||||
<a class="pagination-previous"
|
<a class="pagination-previous"
|
||||||
href="?page={{ rescue_organizations_to_list.previous_page_number }}">{% trans 'Vorherige' %}</a>
|
href="?page={% url_replace request 'page' rescue_organizations_to_list.previous_page_number %}">{% trans 'Vorherige' %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if rescue_organizations_to_list.has_next %}
|
{% if rescue_organizations_to_list.has_next %}
|
||||||
<a class="pagination-next" href="?page={{ rescue_organizations_to_list.next_page_number }}">{% trans 'Nächste' %}</a>
|
<a class="pagination-next"
|
||||||
|
href="?{% url_replace request 'page' rescue_organizations_to_list.next_page_number %}">{% trans 'Nächste' %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<ul class="pagination-list">
|
<ul class="pagination-list">
|
||||||
{% for page in elided_page_range %}
|
{% for page in elided_page_range %}
|
||||||
{% if page != "…" %}
|
{% if page != "…" %}
|
||||||
<li>
|
<li>
|
||||||
<a href="?page={{ page }}"
|
<a href="?{% url_replace request 'page' page %}"
|
||||||
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
|
class="pagination-link {% if page == rescue_organizations_to_list.number %} is-current{% endif %}"
|
||||||
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
|
aria-label="{% blocktranslate %}Gehe zu Seite {{ page }}.{% endblocktranslate %}">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
|
@@ -26,20 +26,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if adoption_notice.is_closed %}
|
{% include "fellchensammlung/partials/partial-adoption-notice-status.html" %}
|
||||||
<article class="message is-warning">
|
|
||||||
<div class="message-header">
|
|
||||||
<p>{% translate 'Vermittlung deaktiviert' %}</p>
|
|
||||||
</div>
|
|
||||||
<div class="message-body content">
|
|
||||||
{% blocktranslate %}
|
|
||||||
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
|
|
||||||
vermittelt wurden.
|
|
||||||
In den Kommentaren findest du ggf. mehr Informationen.
|
|
||||||
{% endblocktranslate %}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
{% endif %}
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<!--- Title level (including action dropdown) -->
|
<!--- Title level (including action dropdown) -->
|
||||||
@@ -211,9 +198,9 @@
|
|||||||
<div class="column block">
|
<div class="column block">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h1 class="card-header-title title is-4">{% translate "Beschreibung" %}</h1>
|
<h4 class="card-header-title title is-4">{% translate "Beschreibung" %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
<p class="expandable">{% if adoption_notice.description %}
|
<p class="expandable">{% if adoption_notice.description %}
|
||||||
{{ adoption_notice.description | render_markdown }}
|
{{ adoption_notice.description | render_markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@@ -32,7 +32,9 @@
|
|||||||
{{ org.location_string }}
|
{{ org.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if org.description %}
|
{% if org.description %}
|
||||||
|
<div class="block content">
|
||||||
<p>{{ org.description | render_markdown }}</p>
|
<p>{{ org.description | render_markdown }}</p>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if org.specializations %}
|
{% if org.specializations %}
|
||||||
@@ -52,7 +54,8 @@
|
|||||||
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
|
<h3 class="title is-5">{% translate 'Übergeordnete Organisation' %}</h3>
|
||||||
<p>
|
<p>
|
||||||
<span>
|
<span>
|
||||||
<i class="fa-solid fa-building fa-fw" aria-label="{% trans 'Tierschutzorganisation' %}"></i>
|
<i class="fa-solid fa-building fa-fw"
|
||||||
|
aria-label="{% trans 'Tierschutzorganisation' %}"></i>
|
||||||
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
|
<a href="{{ org.parent_org.get_absolute_url }}"> {{ org.parent_org }}</a>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -86,13 +89,32 @@
|
|||||||
|
|
||||||
|
|
||||||
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
|
<h2 class="title is-2">{% translate 'Vermittlungen der Organisation' %}</h2>
|
||||||
|
{% with ans_by_status=org.adoption_notices_in_hierarchy_divided_by_status %}
|
||||||
|
{% with active_ans=ans_by_status.0 inactive_ans=ans_by_status.1 %}
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-3">{% translate 'Aktive Vermittlungen' %}</h3>
|
||||||
<div class="container-cards">
|
<div class="container-cards">
|
||||||
{% if org.adoption_notices_in_hierarchy %}
|
{% if active_ans %}
|
||||||
{% for adoption_notice in org.adoption_notices_in_hierarchy %}
|
{% for adoption_notice in active_ans %}
|
||||||
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="block">
|
||||||
|
<h3 class="title is-3">{% translate 'Inaktive Vermittlungen' %}</h3>
|
||||||
|
<div class="container-cards">
|
||||||
|
{% if inactive_ans %}
|
||||||
|
{% for adoption_notice in inactive_ans %}
|
||||||
|
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
<h1 class="message-header">
|
<h1 class="message-header">
|
||||||
{{ external_site_warning.title }}
|
{{ external_site_warning.title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="message-body">
|
<div class="message-body content">
|
||||||
{{ external_site_warning.content | render_markdown }}
|
{{ external_site_warning.content | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@@ -92,6 +92,7 @@
|
|||||||
{% translate 'Tierheime in der Nähe' %}
|
{% translate 'Tierheime in der Nähe' %}
|
||||||
</a>
|
</a>
|
||||||
<br/>
|
<br/>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
{% trust_level "MODERATOR" as coordinator_trust_level %}
|
{% trust_level "MODERATOR" as coordinator_trust_level %}
|
||||||
{% if request.user.trust_level >= coordinator_trust_level %}
|
{% if request.user.trust_level >= coordinator_trust_level %}
|
||||||
<a class="nav-link " href="{% url "modtools" %}">
|
<a class="nav-link " href="{% url "modtools" %}">
|
||||||
@@ -104,6 +105,7 @@
|
|||||||
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
|
<i class="fa-solid fa-screwdriver-wrench fa-fw"></i>{% translate 'Admin-Bereich' %}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
@@ -1,6 +1,9 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
<meta name="description" content="{% trans 'Inhalt melden' %}">
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="title is-1">{% translate "Melden" %}</h1>
|
<h1 class="title is-1">{% translate "Melden" %}</h1>
|
||||||
Wenn dieser Inhalt nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.
|
Wenn dieser Inhalt nicht unseren <a href='{% url "about" %}'>Regeln</a> entspricht, wähle bitte eine der folgenden Regeln aus und hinterlasse einen Kommentar der es detaillierter erklärt, insbesondere wenn der Regelverstoß nicht offensichtlich ist.
|
||||||
|
@@ -23,7 +23,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if introduction %}
|
{% if introduction %}
|
||||||
<h1>{{ introduction.title }}</h1>
|
<h1>{{ introduction.title }}</h1>
|
||||||
|
<div class="content">
|
||||||
{{ introduction.content | render_markdown }}
|
{{ introduction.content | render_markdown }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
|
<h2 class="title is-2">{% translate "Aktuelle Vermittlungen" %}</h2>
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
<h2 class="title is-1">{{ how_to.title }}</h2>
|
<h2 class="title is-1">{{ how_to.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
{{ how_to.content | render_markdown }}
|
{{ how_to.content | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -71,7 +71,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if adoption_notice.description_short %}
|
{% if adoption_notice.description_short %}
|
||||||
|
<div class="content">
|
||||||
{{ adoption_notice.description_short | render_markdown }}
|
{{ adoption_notice.description_short | render_markdown }}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
{% load custom_tags %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% if adoption_notice.is_closed %}
|
||||||
|
<article class="message is-warning">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{% translate 'Vermittlung deaktiviert' %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body content">
|
||||||
|
{% blocktranslate %}
|
||||||
|
Diese Vermittlung wurde deaktiviert. Typischerweise passiert das, wenn die Tiere erfolgreich
|
||||||
|
vermittelt wurden.
|
||||||
|
In den Kommentaren findest du ggf. mehr Informationen.
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% elif adoption_notice.is_interested %}
|
||||||
|
<article class="message is-info">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{% translate 'Tiere sind reserviert' %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body content">
|
||||||
|
{% blocktranslate %}
|
||||||
|
Diese Tiere sind bereits reserviert.
|
||||||
|
In den Kommentaren findest du ggf. mehr Informationen.
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% elif adoption_notice.is_awaiting_action %}
|
||||||
|
<article class="message is-warning">
|
||||||
|
<div class="message-header">
|
||||||
|
<p>{% translate 'Warten auf Aktivierung' %}</p>
|
||||||
|
</div>
|
||||||
|
<div class="message-body content">
|
||||||
|
{% blocktranslate %}
|
||||||
|
Diese Vermittlung muss noch durch Moderator*innen aktiviert werden und taucht daher nicht auf der
|
||||||
|
Startseite auf.
|
||||||
|
Ggf. fehlen noch relevante Informationen.
|
||||||
|
{% endblocktranslate %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
|
@@ -19,13 +19,15 @@
|
|||||||
{{ adoption_notice.location_string }}
|
{{ adoption_notice.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<div class="content">
|
||||||
{% if adoption_notice.description %}
|
{% if adoption_notice.description %}
|
||||||
{{ adoption_notice.description | render_markdown }}
|
{{ adoption_notice.description | render_markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<p>
|
||||||
{% translate "Keine Beschreibung" %}
|
{% translate "Keine Beschreibung" %}
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% if adoption_notice.get_photo %}
|
{% if adoption_notice.get_photo %}
|
||||||
<div class="adoption-notice-img">
|
<div class="adoption-notice-img">
|
||||||
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
|
<img src="{{ MEDIA_URL }}/{{ adoption_notice.get_photo.image }}"
|
||||||
|
@@ -15,18 +15,27 @@
|
|||||||
<strong>{% translate 'Zuletzt geprüft:' %}</strong> {{ rescue_org.last_checked_hr }}
|
<strong>{% translate 'Zuletzt geprüft:' %}</strong> {{ rescue_org.last_checked_hr }}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<i class="fas fa-images" aria-label="{% translate "Material verwenden?" %}"></i> {{ rescue_org.get_allows_using_materials_display }}
|
<i class="fas fa-images"
|
||||||
|
aria-label="{% translate "Material verwenden?" %}"></i> {{ rescue_org.get_allows_using_materials_display }}
|
||||||
</p>
|
</p>
|
||||||
{% if rescue_org.website %}
|
{% if rescue_org.website %}
|
||||||
<a href="{{ rescue_org.website }}" target="_blank">
|
<a href="{{ rescue_org.website }}" id="rescue_org_website_{{ forloop.counter }}" target="_blank">
|
||||||
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
|
<i class="fas fa-globe" aria-label="{% translate "Website" %}"></i>
|
||||||
{{ rescue_org.website|domain }}
|
{{ rescue_org.website|domain }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% with rescue_org_counter=forloop.counter %}
|
||||||
{% for species_url in rescue_org.species_urls %}
|
{% for species_url in rescue_org.species_urls %}
|
||||||
<p>{{ species_url.species }}: <a href="{{ species_url.url }}" target="_blank">{{ species_url.url }}</a>
|
<p>{{ species_url.species }}:
|
||||||
|
<a href="{{ species_url.url }}"
|
||||||
|
id="species_url_{{ rescue_org_counter }}_{{ forloop.counter }}"
|
||||||
|
target="_blank">
|
||||||
|
{{ species_url.url }}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% if set_internal_comment_available %}
|
{% if set_internal_comment_available %}
|
||||||
<div class="block">
|
<div class="block">
|
||||||
@@ -55,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<div class="card-footer-item is-confirm">
|
<div class="card-footer-item is-confirm">
|
||||||
<form method="post">
|
<form method="post" id="mark_checked_{{ forloop.counter }}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden"
|
<input type="hidden"
|
||||||
name="rescue_organization_id"
|
name="rescue_organization_id"
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load custom_tags %}
|
{% load custom_tags %}
|
||||||
<div class="notification">
|
<div class="notification {% if not notification.read %}is-info is-light{% endif %}">
|
||||||
<form class="delete" method="POST">
|
{% if not notification.read %}
|
||||||
|
<form class="delete js-delete-excluded" method="POST">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="action" value="notification_mark_read">
|
<input type="hidden" name="action" value="notification_mark_read">
|
||||||
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
|
<input type="hidden" name="notification_id" value="{{ notification.pk }}">
|
||||||
<button class="" type="submit" id="submit"></button>
|
<button class="delete js-delete-excluded" type="submit" id="submit"></button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
<div class="notification-header">
|
<div class="notification-header">
|
||||||
<a href="{{ notification.url }}"><b>{{ notification.title }}</b></a>
|
<a href="{{ notification.url }}"><b>{{ notification.title }}</b></a>
|
||||||
<i class="card-timestamp">{{ notification.created_at|time_since_hr }}</i>
|
<i class="card-timestamp">{{ notification.created_at|time_since_hr }}</i>
|
||||||
|
@@ -8,18 +8,18 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<p>
|
<div class="block">
|
||||||
<b><i class="fa-solid fa-location-dot"></i></b>
|
<b><i class="fa-solid fa-location-dot"></i></b>
|
||||||
{% if rescue_organization.location %}
|
{% if rescue_organization.location %}
|
||||||
{{ rescue_organization.location }}
|
{{ rescue_organization.location }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ rescue_organization.location_string }}
|
{{ rescue_organization.location_string }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
<p>
|
<div class="block content">
|
||||||
{% if rescue_organization.description_short %}
|
{% if rescue_organization.description_short %}
|
||||||
{{ rescue_organization.description_short | render_markdown }}
|
{{ rescue_organization.description_short | render_markdown }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -3,7 +3,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2 class="card-header-title">{{ rule.title }}</h2>
|
<h2 class="card-header-title">{{ rule.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content content">
|
||||||
<p class="content">{{ rule.rule_text | render_markdown }}</p>
|
{{ rule.rule_text | render_markdown }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@@ -1,5 +1,11 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load static %}
|
||||||
|
{% block additional_scrips %}
|
||||||
|
<script src="{% static 'fellchensammlung/js/mousetrap.min.js' %}"></script>
|
||||||
|
<script src="{% static 'fellchensammlung/js/rescue-org-check-shortcuts.js' %}"></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1>
|
<h1 class="title is-1">{% translate "Aktualitätscheck" %}</h1>
|
||||||
<p class="subtitle is-3">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p>
|
<p class="subtitle is-3">{% translate "Überprüfe ob es in Tierheimen neue Tiere gibt die ein Zuhause suchen" %}</p>
|
||||||
|
@@ -122,3 +122,12 @@ def host():
|
|||||||
def time_since_hr(timestamp):
|
def time_since_hr(timestamp):
|
||||||
t_delta = timezone.now() - timestamp
|
t_delta = timezone.now() - timestamp
|
||||||
return time_since_as_hr_string(t_delta)
|
return time_since_as_hr_string(t_delta)
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def url_replace(request, field, value):
|
||||||
|
dict_ = request.GET.copy()
|
||||||
|
|
||||||
|
dict_[field] = value
|
||||||
|
|
||||||
|
return dict_.urlencode()
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from random import randint
|
||||||
from notfellchen import settings
|
from notfellchen import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -139,3 +140,18 @@ def send_test_email(email):
|
|||||||
to = email
|
to = email
|
||||||
|
|
||||||
mail.send_mail(subject, plain_message, from_email, [to], html_message=html_message)
|
mail.send_mail(subject, plain_message, from_email, [to], html_message=html_message)
|
||||||
|
|
||||||
|
|
||||||
|
def mask_organization_contact_data(catchall_domain="example.org"):
|
||||||
|
"""
|
||||||
|
Masks e-mails, so they are all sent to one domain, preferably a catchall domain.
|
||||||
|
"""
|
||||||
|
rescue_orgs_with_phone_number = RescueOrganization.objects.filter(phone_number__isnull=False)
|
||||||
|
for rescue_org_with_phone_number in rescue_orgs_with_phone_number:
|
||||||
|
rescue_org_with_phone_number.phone_number = randint(100000000000, 1000000000000)
|
||||||
|
rescue_org_with_phone_number.save()
|
||||||
|
rescue_orgs_with_email = RescueOrganization.objects.filter(email__isnull=False)
|
||||||
|
for rescue_org_with_email in rescue_orgs_with_email:
|
||||||
|
rescue_org_with_email.email = f"{rescue_org_with_email.email.replace('@', '-')}@{catchall_domain}"
|
||||||
|
rescue_org_with_email.save()
|
||||||
|
|
||||||
|
@@ -58,6 +58,9 @@ class FediClient:
|
|||||||
response = requests.post(status_endpoint, headers=self.headers, data=payload)
|
response = requests.post(status_endpoint, headers=self.headers, data=payload)
|
||||||
|
|
||||||
# Raise exception if posting fails
|
# Raise exception if posting fails
|
||||||
|
if response.status_code >= 300:
|
||||||
|
logging.error(f"Request= {response.request.body}")
|
||||||
|
logging.error(f"Response= {response.json()}")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
return response.json()
|
return response.json()
|
||||||
@@ -70,8 +73,11 @@ class FediClient:
|
|||||||
:param alt_text: The alt text for the image.
|
:param alt_text: The alt text for the image.
|
||||||
:return: The response from the Mastodon API.
|
:return: The response from the Mastodon API.
|
||||||
"""
|
"""
|
||||||
|
MAX_NUM_OF_IMAGES = 6
|
||||||
|
if len(images) > MAX_NUM_OF_IMAGES:
|
||||||
|
logging.warning(f"Too many images ({len(images)}) to post. Selecting the first {MAX_NUM_OF_IMAGES} images.")
|
||||||
media_ids = []
|
media_ids = []
|
||||||
for image in images:
|
for image in images[:MAX_NUM_OF_IMAGES]:
|
||||||
# Upload the image and get the media ID
|
# Upload the image and get the media ID
|
||||||
media_ids.append(self.upload_media(f"{settings.MEDIA_ROOT}/{image.image}", image.alt_text))
|
media_ids.append(self.upload_media(f"{settings.MEDIA_ROOT}/{image.image}", image.alt_text))
|
||||||
|
|
||||||
|
@@ -53,6 +53,19 @@ def calculate_distance_between_coordinates(position1, position2):
|
|||||||
return distance_in_km
|
return distance_in_km
|
||||||
|
|
||||||
|
|
||||||
|
def object_in_distance(obj, position, max_distance, unknown_true=True):
|
||||||
|
"""
|
||||||
|
Returns a boolean indicating if the Location of the object 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 obj.position is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
distance = calculate_distance_between_coordinates(obj.position, position)
|
||||||
|
return distance < max_distance
|
||||||
|
|
||||||
|
|
||||||
class ResponseMock:
|
class ResponseMock:
|
||||||
content = b'[{"place_id":138181499,"licence":"Data \xc2\xa9 OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1247237,"lat":"48.4949904","lon":"9.040330235970146","category":"boundary","type":"postal_code","place_rank":21, "importance":0.12006895017929346,"addresstype":"postcode","name":"72072","display_name":"72072, Derendingen, T\xc3\xbcbingen, Landkreis T\xc3\xbcbingen, Baden-W\xc3\xbcrttemberg, Deutschland", "boundingbox":["48.4949404","48.4950404","9.0402802","9.0403802"]}]'
|
content = b'[{"place_id":138181499,"licence":"Data \xc2\xa9 OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright","osm_type":"relation","osm_id":1247237,"lat":"48.4949904","lon":"9.040330235970146","category":"boundary","type":"postal_code","place_rank":21, "importance":0.12006895017929346,"addresstype":"postcode","name":"72072","display_name":"72072, Derendingen, T\xc3\xbcbingen, Landkreis T\xc3\xbcbingen, Baden-W\xc3\xbcrttemberg, Deutschland", "boundingbox":["48.4949404","48.4950404","9.0402802","9.0403802"]}]'
|
||||||
status_code = 200
|
status_code = 200
|
||||||
|
@@ -2,9 +2,9 @@ import logging
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from .geo import LocationProxy, Position
|
from .geo import LocationProxy, Position
|
||||||
from ..forms import AdoptionNoticeSearchForm
|
from ..forms import AdoptionNoticeSearchForm, RescueOrgSearchForm
|
||||||
from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \
|
from ..models import SearchSubscription, AdoptionNotice, SexChoicesWithAll, Location, \
|
||||||
Notification, NotificationTypeChoices
|
Notification, NotificationTypeChoices, RescueOrganization
|
||||||
|
|
||||||
|
|
||||||
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
|
def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: bool = True):
|
||||||
@@ -18,7 +18,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b
|
|||||||
return
|
return
|
||||||
for search_subscription in SearchSubscription.objects.all():
|
for search_subscription in SearchSubscription.objects.all():
|
||||||
logging.debug(f"Search subscription {search_subscription} found.")
|
logging.debug(f"Search subscription {search_subscription} found.")
|
||||||
search = Search(search_subscription=search_subscription)
|
search = AdoptionNoticeSearch(search_subscription=search_subscription)
|
||||||
if search.adoption_notice_fits_search(adoption_notice):
|
if search.adoption_notice_fits_search(adoption_notice):
|
||||||
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
notification_text = f"{_('Zu deiner Suche')} {search_subscription} wurde eine neue Vermittlung gefunden"
|
||||||
Notification.objects.create(user_to_notify=search_subscription.owner,
|
Notification.objects.create(user_to_notify=search_subscription.owner,
|
||||||
@@ -33,7 +33,7 @@ def notify_search_subscribers(adoption_notice: AdoptionNotice, only_if_active: b
|
|||||||
logging.info(f"Subscribers for AN {adoption_notice.pk} have been notified\n")
|
logging.info(f"Subscribers for AN {adoption_notice.pk} have been notified\n")
|
||||||
|
|
||||||
|
|
||||||
class Search:
|
class AdoptionNoticeSearch:
|
||||||
def __init__(self, request=None, search_subscription=None):
|
def __init__(self, request=None, search_subscription=None):
|
||||||
self.sex = None
|
self.sex = None
|
||||||
self.area_search = None
|
self.area_search = None
|
||||||
@@ -45,7 +45,7 @@ class Search:
|
|||||||
self.location_string = None
|
self.location_string = None
|
||||||
|
|
||||||
if request:
|
if request:
|
||||||
self.search_from_request(request)
|
self.adoption_notice_search_from_request(request)
|
||||||
elif search_subscription:
|
elif search_subscription:
|
||||||
self.search_from_search_subscription(search_subscription)
|
self.search_from_search_subscription(search_subscription)
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class Search:
|
|||||||
|
|
||||||
return adoptions
|
return adoptions
|
||||||
|
|
||||||
def search_from_request(self, request):
|
def adoption_notice_search_from_request(self, request):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
self.search_form = AdoptionNoticeSearchForm(request.POST)
|
self.search_form = AdoptionNoticeSearchForm(request.POST)
|
||||||
self.search_form.is_valid()
|
self.search_form.is_valid()
|
||||||
@@ -157,3 +157,75 @@ class Search:
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class RescueOrgSearch:
|
||||||
|
def __init__(self, request):
|
||||||
|
self.area_search = None
|
||||||
|
self.max_distance = None
|
||||||
|
self.location = None # Can either be Location (DjangoModel) or LocationProxy
|
||||||
|
self.place_not_found = False # Indicates that a location was given but could not be geocoded
|
||||||
|
self.search_form = None
|
||||||
|
# Either place_id or location string must be set for area search
|
||||||
|
self.location_string = None
|
||||||
|
|
||||||
|
self.rescue_org_search_from_request(request)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{_('Suche')}: {self.location=}, {self.area_search=}, {self.max_distance=}"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""
|
||||||
|
Custom equals that also supports SearchSubscriptions
|
||||||
|
|
||||||
|
Only allowed to be called for located subscriptions
|
||||||
|
"""
|
||||||
|
# If both locations are empty check only the max distance
|
||||||
|
if self.location is None and other.location is None:
|
||||||
|
return self.max_distance == other.max_distance
|
||||||
|
# If one location is empty and the other is not, they are not equal
|
||||||
|
elif self.location is not None and other.location is None or self.location is None and other.location is not None:
|
||||||
|
return False
|
||||||
|
return self.location == other.location and self.max_distance == other.max_distance
|
||||||
|
|
||||||
|
def _locate(self):
|
||||||
|
try:
|
||||||
|
self.location = LocationProxy(self.location_string)
|
||||||
|
except ValueError:
|
||||||
|
self.place_not_found = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def position(self):
|
||||||
|
if self.area_search and not self.place_not_found:
|
||||||
|
return Position(latitude=self.location.latitude, longitude=self.location.longitude)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def rescue_org_fits_search(self, rescue_org: RescueOrganization):
|
||||||
|
# make sure it's an area search and the place is found to check location
|
||||||
|
if self.area_search and not self.place_not_found:
|
||||||
|
# If adoption notice is in not in search distance, return false
|
||||||
|
if not rescue_org.in_distance(self.location.position, self.max_distance):
|
||||||
|
logging.debug("Area mismatch")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_rescue_orgs(self):
|
||||||
|
rescue_orgs = RescueOrganization.objects.all()
|
||||||
|
fitting_rescue_orgs = [rescue_org for rescue_org in rescue_orgs if self.rescue_org_fits_search(rescue_org)]
|
||||||
|
|
||||||
|
return fitting_rescue_orgs
|
||||||
|
|
||||||
|
def rescue_org_search_from_request(self, request):
|
||||||
|
if request.method == 'GET' and request.GET.get("action", False) == "search":
|
||||||
|
self.search_form = RescueOrgSearchForm(request.GET)
|
||||||
|
self.search_form.is_valid()
|
||||||
|
|
||||||
|
if self.search_form.cleaned_data["location_string"] != "" and self.search_form.cleaned_data[
|
||||||
|
"max_distance"] != "":
|
||||||
|
self.area_search = True
|
||||||
|
self.location_string = self.search_form.cleaned_data["location_string"]
|
||||||
|
self.max_distance = int(self.search_form.cleaned_data["max_distance"])
|
||||||
|
self._locate()
|
||||||
|
else:
|
||||||
|
self.search_form = RescueOrgSearchForm()
|
||||||
|
52
src/fellchensammlung/tools/twenty.py
Normal file
52
src/fellchensammlung/tools/twenty.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
from fellchensammlung.models import RescueOrganization
|
||||||
|
|
||||||
|
|
||||||
|
def sync_rescue_org_to_twenty(rescue_org: RescueOrganization, base_url, token: str):
|
||||||
|
if rescue_org.twenty_id:
|
||||||
|
update = True
|
||||||
|
else:
|
||||||
|
update = False
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"eMails": {
|
||||||
|
"primaryEmail": rescue_org.email,
|
||||||
|
"additionalEmails": None
|
||||||
|
},
|
||||||
|
"domainName": {
|
||||||
|
"primaryLinkLabel": rescue_org.website,
|
||||||
|
"primaryLinkUrl": rescue_org.website,
|
||||||
|
"additionalLinks": []
|
||||||
|
},
|
||||||
|
"name": rescue_org.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if rescue_org.location:
|
||||||
|
payload["address"] = {
|
||||||
|
"addressStreet1": f"{rescue_org.location.street} {rescue_org.location.housenumber}",
|
||||||
|
"addressCity": rescue_org.location.city,
|
||||||
|
"addressPostcode": rescue_org.location.postcode,
|
||||||
|
"addressCountry": rescue_org.location.countrycode,
|
||||||
|
"addressLat": rescue_org.location.latitude,
|
||||||
|
"addressLng": rescue_org.location.longitude,
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f"Bearer {token}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if update:
|
||||||
|
url = f"{base_url}/rest/companies/{rescue_org.twenty_id}"
|
||||||
|
response = requests.patch(url, json=payload, headers=headers)
|
||||||
|
assert response.status_code == 200
|
||||||
|
else:
|
||||||
|
url = f"{base_url}/rest/companies"
|
||||||
|
response = requests.post(url, json=payload, headers=headers)
|
||||||
|
try:
|
||||||
|
assert response.status_code == 201
|
||||||
|
except AssertionError:
|
||||||
|
print(response.request.body)
|
||||||
|
return
|
||||||
|
rescue_org.twenty_id = response.json()["data"]["createCompany"]["id"]
|
||||||
|
rescue_org.save()
|
@@ -36,7 +36,7 @@ from .tools.admin import clean_locations, get_unchecked_adoption_notices, deacti
|
|||||||
from .tasks import post_adoption_notice_save
|
from .tasks import post_adoption_notice_save
|
||||||
from rest_framework.authtoken.models import Token
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
from .tools.search import Search
|
from .tools.search import AdoptionNoticeSearch, RescueOrgSearch
|
||||||
|
|
||||||
|
|
||||||
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
|
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
|
||||||
@@ -44,8 +44,11 @@ def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR):
|
|||||||
|
|
||||||
|
|
||||||
def user_is_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
def user_is_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
||||||
|
"""
|
||||||
|
Checks if a user is either the owner of a record or has a trust level equal or higher than the given one
|
||||||
|
"""
|
||||||
return user.is_authenticated and (
|
return user.is_authenticated and (
|
||||||
user.trust_level == trust_level or django_object.owner == user)
|
user.trust_level >= trust_level or django_object.owner == user)
|
||||||
|
|
||||||
|
|
||||||
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
def fail_if_user_not_owner_or_trust_level(user, django_object, trust_level=TrustLevel.MODERATOR):
|
||||||
@@ -100,13 +103,12 @@ def handle_an_check_actions(request, action, adoption_notice=None):
|
|||||||
if action == "checked_inactive":
|
if action == "checked_inactive":
|
||||||
adoption_notice.set_closed()
|
adoption_notice.set_closed()
|
||||||
elif action == "checked_active":
|
elif action == "checked_active":
|
||||||
print("dads")
|
|
||||||
adoption_notice.set_active()
|
adoption_notice.set_active()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def adoption_notice_detail(request, adoption_notice_id):
|
def adoption_notice_detail(request, adoption_notice_id):
|
||||||
adoption_notice = AdoptionNotice.objects.get(id=adoption_notice_id)
|
adoption_notice = get_object_or_404(AdoptionNotice, id=adoption_notice_id)
|
||||||
adoption_notice_meta = adoption_notice._meta
|
adoption_notice_meta = adoption_notice._meta
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
try:
|
try:
|
||||||
@@ -198,7 +200,7 @@ def adoption_notice_edit(request, adoption_notice_id):
|
|||||||
|
|
||||||
def search_important_locations(request, important_location_slug):
|
def search_important_locations(request, important_location_slug):
|
||||||
i_location = get_object_or_404(ImportantLocation, slug=important_location_slug)
|
i_location = get_object_or_404(ImportantLocation, slug=important_location_slug)
|
||||||
search = Search()
|
search = AdoptionNoticeSearch()
|
||||||
search.search_from_predefined_i_location(i_location)
|
search.search_from_predefined_i_location(i_location)
|
||||||
|
|
||||||
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
|
site_title = _("Ratten in %(location_name)s") % {"location_name": i_location.name}
|
||||||
@@ -229,8 +231,8 @@ def search(request, templatename="fellchensammlung/search.html"):
|
|||||||
# A user just visiting the search site did not search, only upon completing the search form a user has really
|
# A user just visiting the search site did not search, only upon completing the search form a user has really
|
||||||
# searched. This will toggle the "subscribe" button
|
# searched. This will toggle the "subscribe" button
|
||||||
searched = False
|
searched = False
|
||||||
search = Search()
|
search = AdoptionNoticeSearch()
|
||||||
search.search_from_request(request)
|
search.adoption_notice_search_from_request(request)
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
searched = True
|
searched = True
|
||||||
if "subscribe_to_search" in request.POST:
|
if "subscribe_to_search" in request.POST:
|
||||||
@@ -569,7 +571,7 @@ def user_detail(request, user, token=None):
|
|||||||
def user_by_id(request, user_id):
|
def user_by_id(request, user_id):
|
||||||
user = User.objects.get(id=user_id)
|
user = User.objects.get(id=user_id)
|
||||||
# Only users that are mods or owners of the user are allowed to view
|
# Only users that are mods or owners of the user are allowed to view
|
||||||
fail_if_user_not_owner_or_trust_level(request.user, user)
|
fail_if_user_not_owner_or_trust_level(user=request.user, django_object=user, trust_level=TrustLevel.MODERATOR)
|
||||||
if user == request.user:
|
if user == request.user:
|
||||||
return my_profile(request)
|
return my_profile(request)
|
||||||
else:
|
else:
|
||||||
@@ -747,8 +749,12 @@ def external_site_warning(request, template_name='fellchensammlung/external-site
|
|||||||
|
|
||||||
def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'):
|
def list_rescue_organizations(request, species=None, template='fellchensammlung/animal-shelters.html'):
|
||||||
if species is None:
|
if species is None:
|
||||||
rescue_organizations = RescueOrganization.objects.all()
|
# rescue_organizations = RescueOrganization.objects.all()
|
||||||
|
|
||||||
|
org_search = RescueOrgSearch(request)
|
||||||
|
rescue_organizations = org_search.get_rescue_orgs()
|
||||||
else:
|
else:
|
||||||
|
org_search = None
|
||||||
rescue_organizations = RescueOrganization.objects.filter(specializations=species)
|
rescue_organizations = RescueOrganization.objects.filter(specializations=species)
|
||||||
|
|
||||||
paginator = Paginator(rescue_organizations, 10)
|
paginator = Paginator(rescue_organizations, 10)
|
||||||
@@ -765,7 +771,21 @@ def list_rescue_organizations(request, species=None, template='fellchensammlung/
|
|||||||
rescue_organizations_to_list = paginator.get_page(page_number)
|
rescue_organizations_to_list = paginator.get_page(page_number)
|
||||||
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
|
context = {"rescue_organizations_to_list": rescue_organizations_to_list,
|
||||||
"show_rescue_orgs": True,
|
"show_rescue_orgs": True,
|
||||||
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1)}
|
"elided_page_range": paginator.get_elided_page_range(page_number, on_each_side=2, on_ends=1),
|
||||||
|
}
|
||||||
|
if org_search:
|
||||||
|
additional_context = {
|
||||||
|
"show_search": True,
|
||||||
|
"search_form": org_search.search_form,
|
||||||
|
"place_not_found": org_search.place_not_found,
|
||||||
|
"map_center": org_search.position,
|
||||||
|
"search_center": org_search.position,
|
||||||
|
"map_pins": [org_search],
|
||||||
|
"location": org_search.location,
|
||||||
|
"search_radius": org_search.max_distance,
|
||||||
|
"zoom_level": zoom_level_for_radius(org_search.max_distance),
|
||||||
|
}
|
||||||
|
context.update(additional_context)
|
||||||
return render(request, template, context=context)
|
return render(request, template, context=context)
|
||||||
|
|
||||||
|
|
||||||
@@ -875,6 +895,7 @@ def moderation_tools_overview(request):
|
|||||||
if action == "post_to_fedi":
|
if action == "post_to_fedi":
|
||||||
adoption_notice = SocialMediaPost.get_an_to_post()
|
adoption_notice = SocialMediaPost.get_an_to_post()
|
||||||
if adoption_notice is not None:
|
if adoption_notice is not None:
|
||||||
|
logging.info(f"Posting adoption notice: {adoption_notice} ({adoption_notice.id})")
|
||||||
try:
|
try:
|
||||||
post = post_an_to_fedi(adoption_notice)
|
post = post_an_to_fedi(adoption_notice)
|
||||||
context = {"action_was_posting": True, "post": post, "posted_successfully": True}
|
context = {"action_was_posting": True, "post": post, "posted_successfully": True}
|
||||||
|
@@ -30,6 +30,6 @@ urlpatterns += i18n_patterns(
|
|||||||
prefix_default_language=False
|
prefix_default_language=False
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG: # pragma: no cover
|
||||||
urlpatterns += static(settings.MEDIA_URL,
|
urlpatterns += static(settings.MEDIA_URL,
|
||||||
document_root=settings.MEDIA_ROOT)
|
document_root=settings.MEDIA_ROOT)
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
{% extends "fellchensammlung/base.html" %}
|
{% extends "fellchensammlung/base.html" %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block description %}
|
||||||
|
{% if next %}
|
||||||
|
<meta name="description" content="{% trans 'Bei Notfellchen.org einloggen' %}">
|
||||||
|
{% else %}
|
||||||
|
<meta name="description" content="{% translate "Bitte log dich ein um diese Seite sehen zu können." %}">
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% if form.errors %}
|
{% if form.errors %}
|
||||||
@@ -14,8 +22,8 @@
|
|||||||
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p class="is-warning">{% translate "Du bist bereits eingeloggt." %}</p>
|
<p class="is-warning">{% translate "Du bist bereits eingeloggt." %}</p>
|
||||||
{% else %} {% if next %}
|
{% else %}
|
||||||
|
{% if next %}
|
||||||
<div class="notification is-warning">
|
<div class="notification is-warning">
|
||||||
<button class="delete"></button>
|
<button class="delete"></button>
|
||||||
<p>
|
<p>
|
||||||
|
@@ -2,12 +2,12 @@ from datetime import timedelta
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
from fellchensammlung.tools.admin import get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices, \
|
||||||
deactivate_404_adoption_notices
|
deactivate_404_adoption_notices, mask_organization_contact_data
|
||||||
from fellchensammlung.tools.misc import is_404
|
from fellchensammlung.tools.misc import is_404
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
from fellchensammlung.models import AdoptionNotice
|
from fellchensammlung.models import AdoptionNotice, RescueOrganization
|
||||||
|
|
||||||
|
|
||||||
class DeactivationTest(TestCase):
|
class DeactivationTest(TestCase):
|
||||||
@@ -96,3 +96,21 @@ class PingTest(TestCase):
|
|||||||
self.adoption2.refresh_from_db()
|
self.adoption2.refresh_from_db()
|
||||||
self.assertTrue(self.adoption1.is_active)
|
self.assertTrue(self.adoption1.is_active)
|
||||||
self.assertFalse(self.adoption2.is_active)
|
self.assertFalse(self.adoption2.is_active)
|
||||||
|
|
||||||
|
|
||||||
|
class MaskingTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.rescue1 = baker.make(RescueOrganization, email="test1@example.com", )
|
||||||
|
cls.rescue2 = baker.make(RescueOrganization, email="test-2@example.com", phone_number="0123456789", )
|
||||||
|
|
||||||
|
def test_masking(self):
|
||||||
|
mask_organization_contact_data()
|
||||||
|
self.assertEqual(RescueOrganization.objects.count(), 2)
|
||||||
|
|
||||||
|
# Ensure that the rescues are pulled from the database again, otherwise this test will fail
|
||||||
|
self.rescue1.refresh_from_db()
|
||||||
|
self.rescue2.refresh_from_db()
|
||||||
|
self.assertNotEqual(self.rescue1.phone_number, "0123456789")
|
||||||
|
self.assertEqual(self.rescue1.email, "test1-example.com@example.org")
|
||||||
|
self.assertEqual(self.rescue2.email, "test-2-example.com@example.org")
|
||||||
|
@@ -85,8 +85,8 @@ class TestNotifications(TestCase):
|
|||||||
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
|
cls.test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
|
||||||
|
|
||||||
def test_mark_read(self):
|
def test_mark_read(self):
|
||||||
not1 = Notification.objects.create(user=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
|
not1 = Notification.objects.create(user_to_notify=self.test_user_1, text="New rats to adopt", title="🔔 New Rat alert")
|
||||||
not2 = Notification.objects.create(user=self.test_user_1,
|
not2 = Notification.objects.create(user_to_notify=self.test_user_1,
|
||||||
text="New wombat to adopt", title="🔔 New Wombat alert")
|
text="New wombat to adopt", title="🔔 New Wombat alert")
|
||||||
not1.mark_read()
|
not1.mark_read()
|
||||||
|
|
||||||
|
@@ -3,11 +3,12 @@ from time import sleep
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from fellchensammlung.models import SearchSubscription, User, TrustLevel, AdoptionNotice, Location, SexChoicesWithAll, \
|
from fellchensammlung.models import SearchSubscription, User, TrustLevel, AdoptionNotice, Location, SexChoicesWithAll, \
|
||||||
Animal, Species, AdoptionNoticeNotification, SexChoices
|
Animal, Species, SexChoices, Notification
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from fellchensammlung.tools.geo import LocationProxy
|
from fellchensammlung.tools.geo import LocationProxy
|
||||||
from fellchensammlung.tools.search import Search, notify_search_subscribers
|
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
|
||||||
|
from fellchensammlung.tools.search import AdoptionNoticeSearch, notify_search_subscribers
|
||||||
|
|
||||||
|
|
||||||
class TestSearch(TestCase):
|
class TestSearch(TestCase):
|
||||||
@@ -71,7 +72,7 @@ class TestSearch(TestCase):
|
|||||||
sex=SexChoicesWithAll.ALL,
|
sex=SexChoicesWithAll.ALL,
|
||||||
max_distance=100
|
max_distance=100
|
||||||
)
|
)
|
||||||
search1 = Search()
|
search1 = AdoptionNoticeSearch()
|
||||||
search1.search_position = LocationProxy("Stuttgart").position
|
search1.search_position = LocationProxy("Stuttgart").position
|
||||||
search1.max_distance = 100
|
search1.max_distance = 100
|
||||||
search1.area_search = True
|
search1.area_search = True
|
||||||
@@ -82,11 +83,11 @@ class TestSearch(TestCase):
|
|||||||
self.assertEqual(search_subscription1, search1)
|
self.assertEqual(search_subscription1, search1)
|
||||||
|
|
||||||
def test_adoption_notice_fits_search(self):
|
def test_adoption_notice_fits_search(self):
|
||||||
search1 = Search(search_subscription=self.subscription1)
|
search1 = AdoptionNoticeSearch(search_subscription=self.subscription1)
|
||||||
self.assertTrue(search1.adoption_notice_fits_search(self.adoption1))
|
self.assertTrue(search1.adoption_notice_fits_search(self.adoption1))
|
||||||
self.assertFalse(search1.adoption_notice_fits_search(self.adoption2))
|
self.assertFalse(search1.adoption_notice_fits_search(self.adoption2))
|
||||||
|
|
||||||
search2 = Search(search_subscription=self.subscription2)
|
search2 = AdoptionNoticeSearch(search_subscription=self.subscription2)
|
||||||
self.assertFalse(search2.adoption_notice_fits_search(self.adoption1))
|
self.assertFalse(search2.adoption_notice_fits_search(self.adoption1))
|
||||||
self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))
|
self.assertTrue(search2.adoption_notice_fits_search(self.adoption2))
|
||||||
|
|
||||||
@@ -100,5 +101,7 @@ class TestSearch(TestCase):
|
|||||||
"""
|
"""
|
||||||
notify_search_subscribers(self.adoption1)
|
notify_search_subscribers(self.adoption1)
|
||||||
|
|
||||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1, adoption_notice=self.adoption1).exists())
|
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
|
||||||
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
|
adoption_notice=self.adoption1,
|
||||||
|
notification_type=NotificationTypeChoices.AN_FOR_SEARCH_FOUND).exists())
|
||||||
|
self.assertFalse(Notification.objects.filter(user_to_notify=self.test_user2,).exists())
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, AdoptionNoticeNotification
|
from fellchensammlung.models import User, TrustLevel, Species, Location, AdoptionNotice, Notification
|
||||||
|
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
|
||||||
from fellchensammlung.tools.notifications import notify_of_AN_to_be_checked
|
from fellchensammlung.tools.notifications import notify_of_AN_to_be_checked
|
||||||
|
|
||||||
|
|
||||||
@@ -24,11 +25,17 @@ class TestNotifications(TestCase):
|
|||||||
cls.test_user0.trust_level = TrustLevel.ADMIN
|
cls.test_user0.trust_level = TrustLevel.ADMIN
|
||||||
cls.test_user0.save()
|
cls.test_user0.save()
|
||||||
|
|
||||||
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1,)
|
cls.adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user1, )
|
||||||
cls.adoption1.set_unchecked() # Could also emit notification
|
cls.adoption1.set_unchecked() # Could also emit notification
|
||||||
|
|
||||||
def test_notify_of_AN_to_be_checked(self):
|
def test_notify_of_AN_to_be_checked(self):
|
||||||
notify_of_AN_to_be_checked(self.adoption1)
|
notify_of_AN_to_be_checked(self.adoption1)
|
||||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user0).exists())
|
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user0,
|
||||||
self.assertTrue(AdoptionNoticeNotification.objects.filter(user=self.test_user1).exists())
|
adoption_notice=self.adoption1,
|
||||||
self.assertFalse(AdoptionNoticeNotification.objects.filter(user=self.test_user2).exists())
|
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
|
||||||
|
self.assertTrue(Notification.objects.filter(user_to_notify=self.test_user1,
|
||||||
|
adoption_notice=self.adoption1,
|
||||||
|
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
|
||||||
|
self.assertFalse(Notification.objects.filter(user_to_notify=self.test_user2,
|
||||||
|
adoption_notice=self.adoption1,
|
||||||
|
notification_type=NotificationTypeChoices.AN_IS_TO_BE_CHECKED).exists())
|
||||||
|
@@ -5,8 +5,9 @@ from django.urls import reverse
|
|||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
|
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
|
||||||
Animal, Subscriptions, Comment, CommentNotification, SearchSubscription
|
Animal, Subscriptions, Comment, Notification, SearchSubscription
|
||||||
from fellchensammlung.tools.geo import LocationProxy
|
from fellchensammlung.tools.geo import LocationProxy
|
||||||
|
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
|
||||||
from fellchensammlung.views import add_adoption_notice
|
from fellchensammlung.views import add_adoption_notice
|
||||||
|
|
||||||
|
|
||||||
@@ -34,16 +35,7 @@ class AnimalAndAdoptionTest(TestCase):
|
|||||||
species=rat,
|
species=rat,
|
||||||
description="Eine unglaublich süße Ratte")
|
description="Eine unglaublich süße Ratte")
|
||||||
|
|
||||||
def test_detail_animal(self):
|
def test_detail_adoption_notice(self):
|
||||||
self.client.login(username='testuser0', password='12345')
|
|
||||||
|
|
||||||
response = self.client.post(reverse('animal-detail', args="1"))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
# Check our user is logged in
|
|
||||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
|
||||||
self.assertContains(response, "Rat1")
|
|
||||||
|
|
||||||
def test_detail_animal_notice(self):
|
|
||||||
self.client.login(username='testuser0', password='12345')
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
|
||||||
response = self.client.post(reverse('adoption-notice-detail', args="1"))
|
response = self.client.post(reverse('adoption-notice-detail', args="1"))
|
||||||
@@ -101,91 +93,6 @@ class AnimalAndAdoptionTest(TestCase):
|
|||||||
self.assertTrue(an.sexes == set("M", ))
|
self.assertTrue(an.sexes == set("M", ))
|
||||||
|
|
||||||
|
|
||||||
class SearchTest(TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
test_user0 = User.objects.create_user(username='testuser0',
|
|
||||||
first_name="Admin",
|
|
||||||
last_name="BOFH",
|
|
||||||
password='12345')
|
|
||||||
test_user0.save()
|
|
||||||
|
|
||||||
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
|
||||||
|
|
||||||
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
|
||||||
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
|
||||||
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
|
||||||
|
|
||||||
berlin = Location.get_location_from_string("Berlin")
|
|
||||||
adoption1.location = berlin
|
|
||||||
adoption1.save()
|
|
||||||
|
|
||||||
stuttgart = Location.get_location_from_string("Stuttgart")
|
|
||||||
adoption3.location = stuttgart
|
|
||||||
adoption3.save()
|
|
||||||
|
|
||||||
adoption1.set_active()
|
|
||||||
adoption3.set_active()
|
|
||||||
adoption2.set_unchecked()
|
|
||||||
|
|
||||||
def test_basic_view(self):
|
|
||||||
response = self.client.get(reverse('search'))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
self.assertContains(response, "TestAdoption1")
|
|
||||||
self.assertNotContains(response, "TestAdoption2")
|
|
||||||
self.assertContains(response, "TestAdoption3")
|
|
||||||
|
|
||||||
def test_basic_view_logged_in(self):
|
|
||||||
self.client.login(username='testuser0', password='12345')
|
|
||||||
response = self.client.get(reverse('search'))
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
# Check our user is logged in
|
|
||||||
self.assertEqual(str(response.context['user']), 'testuser0')
|
|
||||||
|
|
||||||
self.assertContains(response, "TestAdoption1")
|
|
||||||
self.assertContains(response, "TestAdoption3")
|
|
||||||
self.assertNotContains(response, "TestAdoption2")
|
|
||||||
|
|
||||||
def test_unauthenticated_subscribe(self):
|
|
||||||
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
|
|
||||||
|
|
||||||
def test_unauthenticated_unsubscribe(self):
|
|
||||||
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
|
|
||||||
self.assertEqual(response.status_code, 302)
|
|
||||||
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
|
|
||||||
|
|
||||||
def test_subscribe(self):
|
|
||||||
self.client.login(username='testuser0', password='12345')
|
|
||||||
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
|
|
||||||
"subscribe_to_search": ""})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
|
|
||||||
max_distance=50).exists())
|
|
||||||
|
|
||||||
def test_unsubscribe(self):
|
|
||||||
user0 = User.objects.get(username='testuser0')
|
|
||||||
self.client.login(username='testuser0', password='12345')
|
|
||||||
location = Location.get_location_from_string("München")
|
|
||||||
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
|
|
||||||
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
|
|
||||||
"unsubscribe_to_search": subscription.pk})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
|
|
||||||
max_distance=200).exists())
|
|
||||||
|
|
||||||
def test_location_search(self):
|
|
||||||
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
# We can't use assertContains because TestAdoption3 will always be in response at is included in map
|
|
||||||
# In order to test properly, we need to only care for the context that influences the list display
|
|
||||||
an_names = [a.name for a in response.context["adoption_notices"]]
|
|
||||||
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
|
|
||||||
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateQueueTest(TestCase):
|
class UpdateQueueTest(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@@ -339,8 +246,10 @@ class AdoptionDetailTest(TestCase):
|
|||||||
reverse('adoption-notice-detail', args=str(an1.pk)),
|
reverse('adoption-notice-detail', args=str(an1.pk)),
|
||||||
data={"action": "comment", "text": "Test"})
|
data={"action": "comment", "text": "Test"})
|
||||||
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
|
self.assertTrue(Comment.objects.filter(user__username="testuser0").exists())
|
||||||
self.assertFalse(CommentNotification.objects.filter(user__username="testuser0").exists())
|
self.assertFalse(Notification.objects.filter(user_to_notify__username="testuser0",
|
||||||
self.assertTrue(CommentNotification.objects.filter(user__username="testuser1").exists())
|
notification_type=NotificationTypeChoices.NEW_COMMENT).exists())
|
||||||
|
self.assertTrue(Notification.objects.filter(user_to_notify__username="testuser1",
|
||||||
|
notification_type=NotificationTypeChoices.NEW_COMMENT).exists())
|
||||||
|
|
||||||
|
|
||||||
class AdoptionEditTest(TestCase):
|
class AdoptionEditTest(TestCase):
|
||||||
|
@@ -2,7 +2,8 @@ from django.test import TestCase
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from docs.conf import language
|
from docs.conf import language
|
||||||
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment
|
from fellchensammlung.models import User, TrustLevel, AdoptionNotice, Species, Rule, Language, Comment, ReportComment, \
|
||||||
|
Location, ImportantLocation
|
||||||
from model_bakery import baker
|
from model_bakery import baker
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +39,11 @@ class BasicViewTest(TestCase):
|
|||||||
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
|
report_comment1 = ReportComment.objects.create(reported_comment=comment1,
|
||||||
user_comment="ReportComment1")
|
user_comment="ReportComment1")
|
||||||
report_comment1.save()
|
report_comment1.save()
|
||||||
report_comment1.reported_broken_rules.set({rule1,})
|
report_comment1.reported_broken_rules.set({rule1, })
|
||||||
|
|
||||||
|
berlin = Location.get_location_from_string("Berlin")
|
||||||
|
cls.important_berlin = ImportantLocation(location=berlin, slug="berlin", name="Berlin")
|
||||||
|
cls.important_berlin.save()
|
||||||
|
|
||||||
def test_index_logged_in(self):
|
def test_index_logged_in(self):
|
||||||
self.client.login(username='testuser0', password='12345')
|
self.client.login(username='testuser0', password='12345')
|
||||||
@@ -60,11 +65,19 @@ class BasicViewTest(TestCase):
|
|||||||
self.client.login(username='testuser0', password='12345')
|
self.client.login(username='testuser0', password='12345')
|
||||||
response = self.client.get(reverse('about'))
|
response = self.client.get(reverse('about'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Rule 1")
|
|
||||||
|
|
||||||
def test_about_anonymous(self):
|
def test_about_anonymous(self):
|
||||||
response = self.client.get(reverse('about'))
|
response = self.client.get(reverse('about'))
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def terms_of_service_logged_in(self):
|
||||||
|
response = self.client.get(reverse('terms-of-service'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Rule 1")
|
||||||
|
|
||||||
|
def terms_of_service_anonymous(self):
|
||||||
|
response = self.client.get(reverse('terms-of-service'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Rule 1")
|
self.assertContains(response, "Rule 1")
|
||||||
|
|
||||||
def test_report_adoption_logged_in(self):
|
def test_report_adoption_logged_in(self):
|
||||||
@@ -133,4 +146,55 @@ class BasicViewTest(TestCase):
|
|||||||
self.assertContains(response, "ReportComment1")
|
self.assertContains(response, "ReportComment1")
|
||||||
self.assertContains(response, '<form action="allow" class="">')
|
self.assertContains(response, '<form action="allow" class="">')
|
||||||
|
|
||||||
|
def test_rss_logged_in(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
|
||||||
|
response = self.client.get(reverse('rss'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "TestAdoption0")
|
||||||
|
self.assertNotContains(response, "TestAdoption5") # Should not be active, therefore not shown
|
||||||
|
|
||||||
|
def test_rss_anonymous(self):
|
||||||
|
response = self.client.get(reverse('rss'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "TestAdoption1")
|
||||||
|
self.assertNotContains(response, "TestAdoption5")
|
||||||
|
|
||||||
|
def test_an_form_logged_in(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
|
||||||
|
response = self.client.get(reverse('add-adoption'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_an_form_anonymous(self):
|
||||||
|
response = self.client.get(reverse('add-adoption'))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/accounts/login/?next=/vermitteln/")
|
||||||
|
|
||||||
|
def test_important_location_logged_in(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
|
||||||
|
response = self.client.get(reverse('search-by-location', args=(self.important_berlin.slug,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_important_location_anonymous(self):
|
||||||
|
response = self.client.get(reverse('search-by-location', args=(self.important_berlin.slug,)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_map_logged_in(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
response = self.client.get(reverse('map'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_map_anonymous(self):
|
||||||
|
response = self.client.get(reverse('map'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_metrics_logged_in(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
response = self.client.get(reverse('metrics'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
def test_metrics_anonymous(self):
|
||||||
|
response = self.client.get(reverse('metrics'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
113
src/tests/test_views/test_search.py
Normal file
113
src/tests/test_views/test_search.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth.models import Permission
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel, \
|
||||||
|
Animal, Subscriptions, Comment, Notification, SearchSubscription
|
||||||
|
from fellchensammlung.tools.geo import LocationProxy
|
||||||
|
from fellchensammlung.tools.model_helpers import NotificationTypeChoices
|
||||||
|
from fellchensammlung.views import add_adoption_notice
|
||||||
|
|
||||||
|
|
||||||
|
class SearchTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
test_user0 = User.objects.create_user(username='testuser0',
|
||||||
|
first_name="Max",
|
||||||
|
last_name="BOFH",
|
||||||
|
password='12345')
|
||||||
|
test_user0.save()
|
||||||
|
|
||||||
|
test_user1 = User.objects.create_user(username='testuser1',
|
||||||
|
first_name="Moritz",
|
||||||
|
last_name="BOFH",
|
||||||
|
password='12345')
|
||||||
|
test_user1.save()
|
||||||
|
|
||||||
|
# Location of Berlin: lat 52.5170365 lon 13.3888599 PLZ 10115 (Mitte)
|
||||||
|
|
||||||
|
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
|
||||||
|
adoption2 = baker.make(AdoptionNotice, name="TestAdoption2")
|
||||||
|
adoption3 = baker.make(AdoptionNotice, name="TestAdoption3")
|
||||||
|
|
||||||
|
berlin = Location.get_location_from_string("Berlin")
|
||||||
|
adoption1.location = berlin
|
||||||
|
adoption1.save()
|
||||||
|
|
||||||
|
stuttgart = Location.get_location_from_string("Stuttgart")
|
||||||
|
adoption3.location = stuttgart
|
||||||
|
adoption3.save()
|
||||||
|
|
||||||
|
adoption1.set_active()
|
||||||
|
adoption3.set_active()
|
||||||
|
adoption2.set_unchecked()
|
||||||
|
|
||||||
|
cls.subscription1 = SearchSubscription.objects.create(owner=test_user1,
|
||||||
|
max_distance=200,
|
||||||
|
location=stuttgart,
|
||||||
|
sex="A")
|
||||||
|
|
||||||
|
def test_basic_view(self):
|
||||||
|
response = self.client.get(reverse('search'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
self.assertContains(response, "TestAdoption1")
|
||||||
|
self.assertNotContains(response, "TestAdoption2")
|
||||||
|
self.assertContains(response, "TestAdoption3")
|
||||||
|
|
||||||
|
def test_basic_view_logged_in(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
response = self.client.get(reverse('search'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
# Check our user is logged in
|
||||||
|
self.assertEqual(str(response.context['user']), 'testuser0')
|
||||||
|
|
||||||
|
self.assertContains(response, "TestAdoption1")
|
||||||
|
self.assertContains(response, "TestAdoption3")
|
||||||
|
self.assertNotContains(response, "TestAdoption2")
|
||||||
|
|
||||||
|
def test_unauthenticated_subscribe(self):
|
||||||
|
response = self.client.post(reverse('search'), {"subscribe_to_search": ""})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
|
||||||
|
|
||||||
|
def test_unauthenticated_unsubscribe(self):
|
||||||
|
response = self.client.post(reverse('search'), {"unsubscribe_to_search": 1})
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/accounts/login/?next=/suchen/")
|
||||||
|
|
||||||
|
def test_unauthorized_unsubscribe(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
# This should not be allowed as the subscription owner is different than the request user
|
||||||
|
response = self.client.post(reverse('search'), {"unsubscribe_to_search": self.subscription1.id})
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_subscribe(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A",
|
||||||
|
"subscribe_to_search": ""})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertTrue(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
|
||||||
|
max_distance=50).exists())
|
||||||
|
|
||||||
|
def test_unsubscribe(self):
|
||||||
|
user0 = User.objects.get(username='testuser0')
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
location = Location.get_location_from_string("München")
|
||||||
|
subscription = SearchSubscription.objects.create(owner=user0, max_distance=200, location=location, sex="A")
|
||||||
|
response = self.client.post(reverse('search'), {"max_distance": 200, "location_string": "München", "sex": "A",
|
||||||
|
"unsubscribe_to_search": subscription.pk})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertFalse(SearchSubscription.objects.filter(owner=User.objects.get(username='testuser0'),
|
||||||
|
max_distance=200).exists())
|
||||||
|
|
||||||
|
def test_location_search(self):
|
||||||
|
response = self.client.post(reverse('search'), {"max_distance": 50, "location_string": "Berlin", "sex": "A"})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
# We can't use assertContains because TestAdoption3 will always be in response at is included in map
|
||||||
|
# In order to test properly, we need to only care for the context that influences the list display
|
||||||
|
an_names = [a.name for a in response.context["adoption_notices"]]
|
||||||
|
self.assertTrue("TestAdoption1" in an_names) # Adoption in Berlin
|
||||||
|
self.assertFalse("TestAdoption3" in an_names) # Adoption in Stuttgart
|
73
src/tests/test_views/test_user_views.py
Normal file
73
src/tests/test_views/test_user_views.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from rest_framework.authtoken.models import Token
|
||||||
|
|
||||||
|
from model_bakery import baker
|
||||||
|
|
||||||
|
from fellchensammlung.models import AdoptionNotice, User, TrustLevel, Notification
|
||||||
|
|
||||||
|
|
||||||
|
class UserTest(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
cls.test_user0 = User.objects.create_user(username='testuser0',
|
||||||
|
first_name="Admin",
|
||||||
|
last_name="BOFH",
|
||||||
|
password='12345')
|
||||||
|
cls.test_user0.trust_level = TrustLevel.ADMIN
|
||||||
|
cls.test_user0.save()
|
||||||
|
|
||||||
|
cls.test_user1 = User.objects.create_user(username='testuser1',
|
||||||
|
first_name="Max",
|
||||||
|
last_name="Müller",
|
||||||
|
password='12345')
|
||||||
|
|
||||||
|
cls.test_user2 = User.objects.create_user(username='testuser2',
|
||||||
|
first_name="Mira",
|
||||||
|
last_name="Müller",
|
||||||
|
password='12345')
|
||||||
|
|
||||||
|
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1", owner=cls.test_user0)
|
||||||
|
notification1 = baker.make(Notification,
|
||||||
|
title="TestNotification1",
|
||||||
|
user_to_notify=cls.test_user0,
|
||||||
|
adoption_notice=adoption1)
|
||||||
|
notification2 = baker.make(Notification, title="TestNotification1", user_to_notify=cls.test_user1)
|
||||||
|
notification3 = baker.make(Notification, title="TestNotification1", user_to_notify=cls.test_user2)
|
||||||
|
token = baker.make(Token, user=cls.test_user0)
|
||||||
|
|
||||||
|
def test_detail_self(self):
|
||||||
|
self.client.login(username='testuser1', password='12345')
|
||||||
|
|
||||||
|
response = self.client.post(reverse('user-me'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Max")
|
||||||
|
|
||||||
|
def test_detail_self_via_id(self):
|
||||||
|
self.client.login(username='testuser1', password='12345')
|
||||||
|
|
||||||
|
response = self.client.post(reverse('user-detail', args=str(self.test_user1.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Max")
|
||||||
|
|
||||||
|
def test_detail_admin_with_token(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
|
||||||
|
response = self.client.post(reverse('user-me'))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, Token.objects.get(user=self.test_user0).key)
|
||||||
|
|
||||||
|
def test_detail_unauthenticated(self):
|
||||||
|
response = self.client.get(reverse('user-me'))
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/accounts/login/?next=/user/me/")
|
||||||
|
|
||||||
|
def test_detail_unauthorized(self):
|
||||||
|
self.client.login(username='testuser2', password='12345')
|
||||||
|
response = self.client.get(reverse('user-detail', args=str(self.test_user1.pk)))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_detail_authorized(self):
|
||||||
|
self.client.login(username='testuser0', password='12345')
|
||||||
|
response = self.client.get(reverse('user-detail', args=str(self.test_user1.pk)))
|
||||||
|
self.assertEqual(response.status_code, 200)
|
Reference in New Issue
Block a user