Compare commits

..

No commits in common. "3eb7dbe9843dfa6fecf653477d59464e8e557936" and "bf54bc5d51a69017c975c39b75e8e78a761521bb" have entirely different histories.

33 changed files with 165 additions and 518 deletions

View File

@ -35,7 +35,7 @@ dependencies = [
"markdown", "markdown",
"Pillow", "Pillow",
"django-registration", "django-registration",
"psycopg2-binary", "psycopg2",
"django-crispy-forms", "django-crispy-forms",
"crispy-bootstrap4", "crispy-bootstrap4",
"djangorestframework", "djangorestframework",

View File

@ -15,4 +15,3 @@ class FellchensammlungConfig(AppConfig):
except Permission.DoesNotExist: except Permission.DoesNotExist:
pass pass
post_migrate.connect(ensure_languages, sender=self) post_migrate.connect(ensure_languages, sender=self)
import fellchensammlung.receivers

View File

@ -9,14 +9,6 @@ from django.utils.translation import gettext_lazy as _
from notfellchen.settings import MEDIA_URL from notfellchen.settings import MEDIA_URL
def animal_validator(value: str):
value = value.lower()
animal_list = ["ratte", "farbratte", "katze", "hund", "kaninchen", "hase", "kuh", "fuchs", "cow", "rat", "cat",
"dog", "rabbit", "fox", "fancy rat"]
if value not in animal_list:
raise forms.ValidationError(_("Dieses Tier kenne ich nicht. Probier ein anderes"))
class DateInput(forms.DateInput): class DateInput(forms.DateInput):
input_type = 'date' input_type = 'date'
@ -51,7 +43,6 @@ class AdoptionNoticeForm(forms.ModelForm):
'group_only', 'group_only',
'searching_since', 'searching_since',
'location_string', 'location_string',
'organization',
'description', 'description',
'further_information', 'further_information',
), ),
@ -59,19 +50,19 @@ class AdoptionNoticeForm(forms.ModelForm):
class Meta: class Meta:
model = AdoptionNotice model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string", fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
"organization"]
class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm): class AdoptionNoticeFormWithDateWidget(AdoptionNoticeForm):
class Meta: class Meta:
model = AdoptionNotice model = AdoptionNotice
fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string", "organization"] fields = ['name', "group_only", "further_information", "description", "searching_since", "location_string"]
widgets = { widgets = {
'searching_since': DateInput(), 'searching_since': DateInput(),
} }
class AnimalForm(forms.ModelForm): class AnimalForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs: if 'in_adoption_notice_creation_flow' in kwargs:
@ -100,7 +91,6 @@ class AnimalFormWithDateWidget(AnimalForm):
'date_of_birth': DateInput(), 'date_of_birth': DateInput(),
} }
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget): class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs) super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
@ -169,8 +159,6 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta): class Meta(RegistrationForm.Meta):
model = User model = User
captcha = forms.CharField(validators=[animal_validator], label=_("Nenne eine bekannte Tierart"), help_text=_("Bitte nenne hier eine bekannte Tierart (z.B. ein Tier das an der Leine geführt wird). Das Fragen wir dich um sicherzustellen, dass du kein Roboter bist."))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.helper = FormHelper() self.helper = FormHelper()
@ -181,7 +169,7 @@ class CustomRegistrationForm(RegistrationForm):
def _get_distances(): def _get_distances():
return {i: i for i in [20, 50, 100, 200, 500]} return {i: i for i in [10, 20, 50, 100, 200, 500]}
class AdoptionNoticeSearchForm(forms.Form): class AdoptionNoticeSearchForm(forms.Form):

View File

@ -1,10 +1,15 @@
from venv import create
import django.conf.global_settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from django.conf import settings from django.conf import settings
from django.core import mail from django.core import mail
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel from django.db.models import Q, Min
from fellchensammlung.models import User
from notfellchen.settings import host from notfellchen.settings import host
NEWLINE = "\r\n" NEWLINE = "\r\n"
@ -12,7 +17,7 @@ NEWLINE = "\r\n"
def mail_admins_new_report(report): def mail_admins_new_report(report):
subject = _("Neue Meldung") subject = _("Neue Meldung")
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR): for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
greeting = _("Moin,") + "{NEWLINE}" greeting = _("Moin,") + "{NEWLINE}"
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}" new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
if len(report.reported_broken_rules.all()) > 0: if len(report.reported_broken_rules.all()) > 0:
@ -29,15 +34,23 @@ def mail_admins_new_report(report):
link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}" link_text = f"Um alle Details zu sehen, geh bitte auf: {report_url}"
body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text body_text = greeting + new_report_text + reported_rules_text + comment_text + link_text
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email]) message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
print("Sending email to ", moderator.email)
message.send() message.send()
def send_notification_email(notification_pk): @receiver(post_save, sender=User)
try: def mail_admins_new_member(sender, instance: User, created: bool, **kwargs):
notification = CommentNotification.objects.get(pk=notification_pk) if not created:
except CommentNotification.DoesNotExist: return
notification = BaseNotification.objects.get(pk=notification_pk) subject = _("Neuer User") + f": {instance.username}"
subject = f"🔔 {notification.title}" for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
body_text = notification.text greeting = _("Moin,") + "{NEWLINE}"
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email]) new_report_text = _("es hat sich eine neue Person registriert.") + "{NEWLINE}"
user_detail_text = _("Username") + f": {instance.username}{NEWLINE}" + _(
"E-Mail") + f": {instance.email}{NEWLINE}"
user_url = "https://" + host + instance.get_absolute_url()
link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
body_text = greeting + new_report_text + user_detail_text + link_text
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
print("Sending email to ", moderator.email)
message.send() message.send()

View File

@ -7,7 +7,7 @@ from fellchensammlung import baker_recipes
from model_bakery import baker from model_bakery import baker
from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \ from fellchensammlung.models import AdoptionNotice, Species, Animal, Image, ModerationAction, User, Rule, \
Report, Comment, ReportAdoptionNotice, TrustLevel Report, Comment, ReportAdoptionNotice
class Command(BaseCommand): class Command(BaseCommand):
@ -101,10 +101,10 @@ class Command(BaseCommand):
User.objects.create_user('test', password='foobar') User.objects.create_user('test', password='foobar')
admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org", admin1 = User.objects.create_superuser(username="admin", password="admin", email="admin1@example.org",
trust_level=TrustLevel.ADMIN) trust_level=User.TRUST_LEVEL[User.ADMIN])
mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org", mod1 = User.objects.create_user(username="mod1", password="mod", email="mod1@example.org",
trust_level=TrustLevel.MODERATOR) trust_level=User.TRUST_LEVEL[User.MODERATOR])
comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1) comment1 = baker.make(Comment, user=admin1, text="This is a comment", adoption_notice=adoption1)
comment2 = baker.make(Comment, comment2 = baker.make(Comment,

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-13 15:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0015_rescueorganization_comment'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='phone_number',
field=models.CharField(blank=True, max_length=15, null=True, verbose_name='Telefonnummer'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-14 06:42
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0016_rescueorganization_phone_number'),
]
operations = [
migrations.AddField(
model_name='user',
name='organization_affiliation',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='fellchensammlung.rescueorganization', verbose_name='Organisation'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-14 17:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0017_user_organization_affiliation'),
]
operations = [
migrations.AddField(
model_name='rescueorganization',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='Beschreibung'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-14 18:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0018_rescueorganization_description'),
]
operations = [
migrations.RenameField(
model_name='rescueorganization',
old_name='comment',
new_name='internal_comment',
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-14 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0019_rename_comment_rescueorganization_internal_comment'),
]
operations = [
migrations.AlterField(
model_name='rescueorganization',
name='internal_comment',
field=models.TextField(blank=True, null=True, verbose_name='Interner Kommentar'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-14 20:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0020_alter_rescueorganization_internal_comment'),
]
operations = [
migrations.AddField(
model_name='user',
name='reason_for_signup',
field=models.TextField(default='-', verbose_name='Grund für die Registrierung'),
preserve_default=False,
),
]

View File

@ -1,23 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-20 18:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0021_user_reason_for_signup'),
]
operations = [
migrations.AlterField(
model_name='user',
name='reason_for_signup',
field=models.TextField(help_text="Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben.", verbose_name='Grund für die Registrierung'),
),
migrations.AlterField(
model_name='user',
name='trust_level',
field=models.IntegerField(choices=[(1, 'Member'), (2, 'Coordinator'), (3, 'Moderator'), (4, 'Admin')], default=1),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 5.1.1 on 2024-11-20 19:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fellchensammlung', '0022_alter_user_reason_for_signup_alter_user_trust_level'),
]
operations = [
migrations.AddField(
model_name='user',
name='email_notifications',
field=models.BooleanField(default=True, verbose_name='Benachrichtigung per E-Mail'),
),
]

View File

@ -34,6 +34,86 @@ class Language(models.Model):
verbose_name_plural = _('Sprachen') verbose_name_plural = _('Sprachen')
class User(AbstractUser):
"""
Model that holds a user's profile, including the django user model
The trust levels act as permission system and can be displayed as a badge for the user
"""
# Admins can perform all actions and have the highest trust associated with them
# Moderators can make moderation decisions regarding the deletion of content
# Coordinators can create adoption notices without them being checked
# Members can create adoption notices that must be activated
ADMIN = "admin"
MODERATOR = "Moderator"
COORDINATOR = "Koordinator*in"
MEMBER = "Mitglied"
TRUST_LEVEL = {
ADMIN: 4,
MODERATOR: 3,
COORDINATOR: 2,
MEMBER: 1,
}
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Bevorzugte Sprache'))
trust_level = models.IntegerField(choices=TRUST_LEVEL, default=TRUST_LEVEL[MEMBER])
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = _('Nutzer*in')
verbose_name_plural = _('Nutzer*innen')
def get_absolute_url(self):
return reverse("user-detail", args=[str(self.pk)])
def get_notifications_url(self):
return self.get_absolute_url()
def get_num_unread_notifications(self):
return BaseNotification.objects.filter(user=self, read=False).count()
@property
def adoption_notices(self):
return AdoptionNotice.objects.filter(owner=self)
@property
def owner(self):
return self
class Image(models.Model):
image = models.ImageField(upload_to='images')
alt_text = models.TextField(max_length=2000)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.alt_text
@property
def as_html(self):
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
class Species(models.Model):
"""Model representing a species of animal."""
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class Location(models.Model): class Location(models.Model):
place_id = models.IntegerField() place_id = models.IntegerField()
latitude = models.FloatField() latitude = models.FloatField()
@ -106,104 +186,10 @@ class RescueOrganization(models.Model):
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil')) facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil')) fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil'))
email = models.EmailField(null=True, blank=True, verbose_name=_('E-Mail')) email = models.EmailField(null=True, blank=True, verbose_name=_('E-Mail'))
phone_number = models.CharField(max_length=15, null=True, blank=True, verbose_name=_('Telefonnummer'))
website = models.URLField(null=True, blank=True, verbose_name=_('Website')) website = models.URLField(null=True, blank=True, verbose_name=_('Website'))
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)
internal_comment = models.TextField(verbose_name=_("Interner Kommentar"), null=True, blank=True, ) comment = models.TextField(verbose_name=_("Kommentar"), null=True, blank=True,)
description = models.TextField(null=True, blank=True, verbose_name=_('Beschreibung')) # Markdown allowed
def get_absolute_url(self):
return reverse("rescue-organization-detail", args=[str(self.pk)])
@property
def adoption_notices(self):
return AdoptionNotice.objects.filter(organization=self)
# Admins can perform all actions and have the highest trust associated with them
# Moderators can make moderation decisions regarding the deletion of content
# Coordinators can create adoption notices without them being checked
# Members can create adoption notices that must be activated
class TrustLevel(models.IntegerChoices):
MEMBER = 1, 'Member'
COORDINATOR = 2, 'Coordinator'
MODERATOR = 3, 'Moderator'
ADMIN = 4, 'Admin'
class User(AbstractUser):
"""
Model that holds a user's profile, including the django user model
The trust levels act as permission system and can be displayed as a badge for the user
"""
trust_level = models.IntegerField(
choices=TrustLevel.choices,
default=TrustLevel.MEMBER, # Default to the lowest trust level
)
preferred_language = models.ForeignKey(Language, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Bevorzugte Sprache'))
updated_at = models.DateTimeField(auto_now=True)
organization_affiliation = models.ForeignKey(RescueOrganization, on_delete=models.PROTECT, null=True, blank=True,
verbose_name=_('Organisation'))
reason_for_signup = models.TextField(verbose_name=_("Grund für die Registrierung"), help_text=_(
"Wir würden gerne wissen warum du dich registriertst, ob du dich z.B. Tiere eines bestimmten Tierheim einstellen willst 'nur mal gucken' willst. Beides ist toll! Wenn du für ein Tierheim/eine Pflegestelle arbeitest kontaktieren wir dich ggf. um dir erweiterte Rechte zu geben."))
email_notifications = models.BooleanField(verbose_name=_("Benachrichtigung per E-Mail"), default=True)
REQUIRED_FIELDS = ["reason_for_signup", "email"]
class Meta:
verbose_name = _('Nutzer*in')
verbose_name_plural = _('Nutzer*innen')
def get_absolute_url(self):
return reverse("user-detail", args=[str(self.pk)])
def get_notifications_url(self):
return self.get_absolute_url()
def get_num_unread_notifications(self):
return BaseNotification.objects.filter(user=self, read=False).count()
@property
def adoption_notices(self):
return AdoptionNotice.objects.filter(owner=self)
@property
def owner(self):
return self
class Image(models.Model):
image = models.ImageField(upload_to='images')
alt_text = models.TextField(max_length=2000)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.alt_text
@property
def as_html(self):
return f'<img src="{MEDIA_URL}/{self.image}" alt="{self.alt_text}">'
class Species(models.Model):
"""Model representing a species of animal."""
name = models.CharField(max_length=200, help_text=_('Name der Tierart'),
verbose_name=_('Name'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String for representing the Model object."""
return self.name
class Meta:
verbose_name = _('Tierart')
verbose_name_plural = _('Tierarten')
class AdoptionNotice(models.Model): class AdoptionNotice(models.Model):
@ -253,6 +239,7 @@ class AdoptionNotice(models.Model):
else: else:
return "mixed" return "mixed"
@property @property
def comments(self): def comments(self):
return Comment.objects.filter(adoption_notice=self) return Comment.objects.filter(adoption_notice=self)
@ -728,6 +715,7 @@ class CommentNotification(BaseNotification):
@property @property
def url(self): def url(self):
print(f"URL: self.comment.get_absolute_url()")
return self.comment.get_absolute_url return self.comment.get_absolute_url

View File

@ -1,37 +0,0 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from fellchensammlung.models import BaseNotification, CommentNotification, User, TrustLevel
from .tasks import task_send_notification_email
from notfellchen.settings import host
from django.utils.translation import gettext_lazy as _
@receiver(post_save, sender=CommentNotification)
def comment_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
base_notification_receiver(sender, instance, created, **kwargs)
@receiver(post_save, sender=BaseNotification)
def base_notification_receiver(sender, instance: BaseNotification, created: bool, **kwargs):
if not created or not instance.user.email_notifications:
return
else:
task_send_notification_email.delay(instance.pk)
@receiver(post_save, sender=User)
def notification_new_user(sender, instance: User, created: bool, **kwargs):
NEWLINE = "\r\n"
if not created:
return
# Create Notification text
subject = _("Neuer User") + f": {instance.username}"
new_user_text = _("Es hat sich eine neue Person registriert.") + f"{NEWLINE}"
user_detail_text = _("Username") + f": {instance.username}{NEWLINE}" + _(
"E-Mail") + f": {instance.email}{NEWLINE}"
user_url = "https://" + host + instance.get_absolute_url()
link_text = f"Um alle Details zu sehen, geh bitte auf: {user_url}"
body_text = new_user_text + user_detail_text + link_text
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
notification = BaseNotification.objects.create(title=subject, text=body_text, user=moderator)
notification.save()

View File

@ -95,7 +95,6 @@ textarea {
.container-cards { .container-cards {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
width: 100%;
} }
.card { .card {
@ -453,11 +452,13 @@ select, .button {
.card h1 { .card h1 {
color: var(--text-three); color: var(--text-three);
text-shadow: 1px 1px var(--shadow-three); text-shadow: 1px 1px var(--shadow-three);
width: 85%;
} }
.card h2 { .card h2 {
color: var(--text-three); color: var(--text-three);
text-shadow: 1px 1px var(--shadow-three); text-shadow: 1px 1px var(--shadow-three);
width: 85%;
} }
.card img { .card img {

View File

@ -1,7 +1,5 @@
from celery.app import shared_task
from django.utils import timezone from django.utils import timezone
from notfellchen.celery import app as celery_app from notfellchen.celery import app as celery_app
from .mail import send_notification_email
from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices from .tools.admin import clean_locations, deactivate_unchecked_adoption_notices, deactivate_404_adoption_notices
from .tools.misc import healthcheck_ok from .tools.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp from .models import Location, AdoptionNotice, Timestamp
@ -45,8 +43,3 @@ def add_adoption_notice_location(pk):
def task_healthcheck(): def task_healthcheck():
healthcheck_ok() healthcheck_ok()
set_timestamp("task_healthcheck") set_timestamp("task_healthcheck")
@shared_task
def task_send_notification_email(notification_pk):
send_notification_email(notification_pk)

View File

@ -1,29 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load custom_tags %}
{% load i18n %}
{% block title %}<title>{{ org.name }}</title>{% endblock %}
{% block content %}
<div class="card">
<h1>{{ org.name }}</h1>
<b><i class="fa-solid fa-location-dot"></i></b>
{% if org.location %}
{{ org.location.str_hr }}
{% else %}
{{ org.location_string }}
{% endif %}
<p>{{ org.description | render_markdown }}</p>
</div>
<h2>{% translate 'Vermittlungen der Organisation' %}</h2>
<div class="container-cards">
{% if org.adoption_notices %}
{% for adoption_notice in org.adoption_notices %}
{% include "fellchensammlung/partials/partial-adoption-notice-minimal.html" %}
{% endfor %}
{% else %}
<p>{% translate "Keine Vermittlungen gefunden." %}</p>
{% endif %}
</div>
{% endblock %}

View File

@ -13,34 +13,8 @@
<p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p> <p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p>
{% endif %} {% endif %}
<div class="container-cards">
{% if user.id is request.user.id %} {% if user.id is request.user.id %}
<div class="card">
{% if token %}
<form action="" method="POST">
{% csrf_token %}
<p class="text-muted"><strong>{% translate "API token:" %}</strong> {{ token }}</p>
<input class="btn" type="submit" name="delete_token"
value={% translate "Delete API token" %}>
</form>
{% else %}
<p>{% translate "Kein API-Token vorhanden." %}</p>
<form action="" method="POST">
{% csrf_token %}
<input class="btn" type="submit" name="create_token"
value={% translate "Create API token" %}>
</form>
{% endif %}
</div>
</div><p>
<div class="container-comment-form">
<h2>{% trans 'Profil verwalten' %}</h2>
<p>
<a class="btn2" href="{% url 'password_change' %}">{% translate "Change password" %}</a>
<a class="btn2" href="{% url 'user-me-export' %}">{% translate "Daten exportieren" %}</a>
</p>
</div>
</p>
<h2>{% translate 'Benachrichtigungen' %}</h2> <h2>{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %} {% include "fellchensammlung/lists/list-notifications.html" %}
<h2>{% translate 'Meine Vermittlungen' %}</h2> <h2>{% translate 'Meine Vermittlungen' %}</h2>

View File

@ -2,7 +2,7 @@
{% load custom_tags %} {% load custom_tags %}
{% load i18n %} {% load i18n %}
{% block title %}<title>{{ adoption_notice.name }}</title>{% endblock %} {% block title %}<title>{{adoption_notice.name }}</title>{% endblock %}
{% block content %} {% block content %}
<div class="detail-adoption-notice-header"> <div class="detail-adoption-notice-header">
@ -34,9 +34,6 @@
<table> <table>
<tr> <tr>
<th>{% translate "Ort" %}</th> <th>{% translate "Ort" %}</th>
{% if adoption_notice.organization %}
<th>{% translate "Organisation" %}</th>
{% endif %}
<th>{% translate "Suchen seit" %}</th> <th>{% translate "Suchen seit" %}</th>
<th>{% translate "Zuletzt aktualisiert" %}</th> <th>{% translate "Zuletzt aktualisiert" %}</th>
<th>{% translate "Weitere Informationen" %}</th> <th>{% translate "Weitere Informationen" %}</th>
@ -49,9 +46,6 @@
{{ adoption_notice.location_string }} {{ adoption_notice.location_string }}
{% endif %} {% endif %}
</td> </td>
{% if adoption_notice.organization %}
<td><a href="{{adoption_notice.organization.get_absolute_url }}">{{ adoption_notice.organization }}</a></td>
{% endif %}
<td>{{ adoption_notice.searching_since }}</td> <td>{{ adoption_notice.searching_since }}</td>
<td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td> <td>{{ adoption_notice.last_checked | date:'d. F Y' }}</td>
@ -60,8 +54,7 @@
<form method="get" action="{% url 'external-site' %}"> <form method="get" action="{% url 'external-site' %}">
<input type="hidden" name="url" value="{{ adoption_notice.further_information }}"> <input type="hidden" name="url" value="{{ adoption_notice.further_information }}">
<button class="btn" type="submit" id="submit"> <button class="btn" type="submit" id="submit">
{{ adoption_notice.further_information | domain }} <i {{ adoption_notice.further_information | domain }} <i class="fa-solid fa-arrow-up-right-from-square"></i>
class="fa-solid fa-arrow-up-right-from-square"></i>
</button> </button>
</form> </form>
</td> </td>

View File

@ -1,15 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load custom_tags %}
{% block title %}<title>{% translate "403 Forbidden" %}</title>{% endblock %}
{% block content %}
<h1>403 Forbidden</h1>
<p>
{% blocktranslate %}
Diese Aktion ist dir nicht erlaubt. Logge dich ein oder nutze einen anderen Account. Wenn du denkst, dass hier
ein Fehler vorliegt, kontaktiere das Team!
{% endblocktranslate %}
</p>
{% endblock %}

View File

@ -8,6 +8,9 @@
<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE_CURRENT %} selected{% endif %}> <option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE_CURRENT %} selected{% endif %}>
{{ language.0|language_name_local }} {{ language.0|language_name_local }}
</option> </option>
<!--<option value="{{ language.0 }}" {% if language.0 == LANGUAGE_CODE %} selected{% endif %}>
{{ language.0|language_name_local }} ({{ language.0 }})
</option>-->
{% endfor %} {% endfor %}
</select> </select>
<!--<input type="submit" value={% translate "change" %}>--> <!--<input type="submit" value={% translate "change" %}>-->

View File

@ -26,7 +26,7 @@
</div> </div>
<a class="btn2" href="{% url 'user-me' %}"><i aria-hidden="true" class="fas fa-user"></i></a> <a class="btn2" href="{{ user.get_absolute_url }}"><i aria-hidden="true" class="fas fa-user"></i></a>
<form class="btn2 button_darken" action="{% url 'logout' %}" method="post"> <form class="btn2 button_darken" action="{% url 'logout' %}" method="post">
{% csrf_token %} {% csrf_token %}
<button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button> <button class="button" type="submit"><i aria-hidden="true" class="fas fa-sign-out"></i></button>

View File

@ -24,8 +24,6 @@ urlpatterns = [
path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, name="adoption-notice-add-photo"), path("vermittlung/<int:adoption_notice_id>/add-photo", views.add_photo_to_adoption_notice, name="adoption-notice-add-photo"),
# ex: /adoption_notice/2/add-animal # ex: /adoption_notice/2/add-animal
path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, name="adoption-notice-add-animal"), path("vermittlung/<int:adoption_notice_id>/add-animal", views.adoption_notice_add_animal, name="adoption-notice-add-animal"),
path("organisation/<int:rescue_organization_id>/", views.detail_view_rescue_organization,
name="rescue-organization-detail"),
# ex: /search/ # ex: /search/
path("suchen/", views.search, name="search"), path("suchen/", views.search, name="search"),
@ -52,9 +50,7 @@ urlpatterns = [
## USERS ## ## USERS ##
########### ###########
# ex: user/1 # ex: user/1
path("user/<int:user_id>/", views.user_by_id, name="user-detail"), path("user/<int:user_id>/", views.user_detail, name="user-detail"),
path("user/me/", views.my_profile, name="user-me"),
path('user/me/export/', views.export_own_profile, name='user-me-export'),
path('accounts/register/', path('accounts/register/',
RegistrationView.as_view( RegistrationView.as_view(

View File

@ -1,14 +1,12 @@
import logging import logging
from django.http import HttpResponseRedirect, JsonResponse, HttpResponse from django.http import HttpResponseRedirect, JsonResponse
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.utils import translation from django.utils import translation
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.core.serializers import serialize
import json
from .mail import mail_admins_new_report from .mail import mail_admins_new_report
from notfellchen import settings from notfellchen import settings
@ -16,7 +14,7 @@ from notfellchen import settings
from fellchensammlung import logger from fellchensammlung import logger
from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \ from .models import AdoptionNotice, Text, Animal, Rule, Image, Report, ModerationAction, \
User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \ User, Location, AdoptionNoticeStatus, Subscriptions, CommentNotification, BaseNotification, RescueOrganization, \
Species, Log, Timestamp, TrustLevel Species, Log, Timestamp
from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \ from .forms import AdoptionNoticeForm, AdoptionNoticeFormWithDateWidget, ImageForm, ReportAdoptionNoticeForm, \
CommentForm, ReportCommentForm, AnimalForm, \ CommentForm, ReportCommentForm, AnimalForm, \
AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal AdoptionNoticeSearchForm, AnimalFormWithDateWidget, AdoptionNoticeFormWithDateWidgetAutoAnimal
@ -25,19 +23,18 @@ from .tools.geo import GeoAPI
from .tools.metrics import gather_metrics_data from .tools.metrics import gather_metrics_data
from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices from .tools.admin import clean_locations, get_unchecked_adoption_notices, deactivate_unchecked_adoption_notices
from .tasks import add_adoption_notice_location from .tasks import add_adoption_notice_location
from rest_framework.authtoken.models import Token
def user_is_trust_level_or_above(user, trust_level=TrustLevel.MODERATOR): def user_is_trust_level_or_above(user, trust_level=User.MODERATOR):
return user.is_authenticated and user.trust_level >= trust_level return user.is_authenticated and user.trust_level >= User.TRUST_LEVEL[trust_level]
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=User.MODERATOR):
return user.is_authenticated and ( return user.is_authenticated and (
user.trust_level == trust_level or django_object.owner == user) user.trust_level == 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=User.MODERATOR):
if not user_is_owner_or_trust_level(user, django_object, trust_level): if not user_is_owner_or_trust_level(user, django_object, trust_level):
raise PermissionDenied raise PermissionDenied
@ -73,8 +70,6 @@ def change_language(request):
response = HttpResponseRedirect(redirect_path) response = HttpResponseRedirect(redirect_path)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language_code) response.set_cookie(settings.LANGUAGE_COOKIE_NAME, language_code)
return response return response
else:
return render(request, 'fellchensammlung/index.html')
def adoption_notice_detail(request, adoption_notice_id): def adoption_notice_detail(request, adoption_notice_id):
@ -153,8 +148,7 @@ def adoption_notice_edit(request, adoption_notice_id):
adoption_notice_instance.save() adoption_notice_instance.save()
"""Log""" """Log"""
Log.objects.create(user=request.user, action="adoption_notice_edit", Log.objects.create(user=request.user, action="adoption_notice_edit", text=f"{request.user} hat Vermittlung {adoption_notice.pk} geändert")
text=f"{request.user} hat Vermittlung {adoption_notice.pk} geändert")
return redirect(reverse("adoption-notice-detail", args=[adoption_notice_instance.pk], )) return redirect(reverse("adoption-notice-detail", args=[adoption_notice_instance.pk], ))
else: else:
form = AdoptionNoticeForm(instance=adoption_notice) form = AdoptionNoticeForm(instance=adoption_notice)
@ -210,7 +204,7 @@ def add_adoption_notice(request):
add_adoption_notice_location.delay_on_commit(instance.pk) add_adoption_notice_location.delay_on_commit(instance.pk)
# Set correct status # Set correct status
if request.user.trust_level >= TrustLevel.MODERATOR: if request.user.trust_level >= User.TRUST_LEVEL[User.COORDINATOR]:
instance.set_active() instance.set_active()
else: else:
instance.set_unchecked() instance.set_unchecked()
@ -420,41 +414,16 @@ def report_detail_success(request, report_id):
return report_detail(request, report_id, form_complete=True) return report_detail(request, report_id, form_complete=True)
def user_detail(request, user, token=None):
context = {"user": user,
"adoption_notices": AdoptionNotice.objects.filter(owner=user),
"notifications": BaseNotification.objects.filter(user=user, read=False)}
if token is not None:
context["token"] = token
return render(request, 'fellchensammlung/details/detail-user.html', context=context)
@login_required @login_required
def user_by_id(request, user_id): def user_detail(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(request.user, user)
if user == request.user: if request.method == "POST":
return my_profile(request)
else:
return user_detail(request, user)
@login_required()
def my_profile(request):
if request.method == 'POST':
if "create_token" in request.POST:
Token.objects.create(user=request.user)
elif "delete_token" in request.POST:
Token.objects.get(user=request.user).delete()
action = request.POST.get("action") action = request.POST.get("action")
if action == "notification_mark_read": if action == "notification_mark_read":
notification_id = request.POST.get("notification_id") notification_id = request.POST.get("notification_id")
try:
notification = CommentNotification.objects.get(pk=notification_id) notification = CommentNotification.objects.get(pk=notification_id)
except CommentNotification.DoesNotExist:
notification = BaseNotification.objects.get(pk=notification_id)
notification.read = True notification.read = True
notification.save() notification.save()
elif action == "notification_mark_all_read": elif action == "notification_mark_all_read":
@ -462,11 +431,11 @@ def my_profile(request):
for notification in notifications: for notification in notifications:
notification.read = True notification.read = True
notification.save() notification.save()
try:
token = Token.objects.get(user=request.user) context = {"user": user,
except Token.DoesNotExist: "adoption_notices": AdoptionNotice.objects.filter(owner=user),
token = None "notifications": CommentNotification.objects.filter(user=user, read=False)}
return user_detail(request, request.user, token) return render(request, 'fellchensammlung/details/detail-user.html', context=context)
@user_passes_test(user_is_trust_level_or_above) @user_passes_test(user_is_trust_level_or_above)
@ -478,19 +447,16 @@ def modqueue(request):
@login_required @login_required
def updatequeue(request): def updatequeue(request):
#TODO: Make sure update can only be done for instances with permission
if request.method == "POST": if request.method == "POST":
adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id")) adoption_notice = AdoptionNotice.objects.get(id=request.POST.get("adoption_notice_id"))
edit_permission = request.user == adoption_notice.owner or user_is_trust_level_or_above(request.user,
TrustLevel.MODERATOR)
if not edit_permission:
return render(request, "fellchensammlung/errors/403.html", status=403)
action = request.POST.get("action") action = request.POST.get("action")
if action == "checked_inactive": if action == "checked_inactive":
adoption_notice.set_closed() adoption_notice.set_closed()
if action == "checked_active": if action == "checked_active":
adoption_notice.set_active() adoption_notice.set_active()
if user_is_trust_level_or_above(request.user, TrustLevel.MODERATOR): if user_is_trust_level_or_above(request.user, User.MODERATOR):
last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked") last_checked_adoption_list = AdoptionNotice.objects.order_by("last_checked")
else: else:
last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked") last_checked_adoption_list = AdoptionNotice.objects.filter(owner=request.user).order_by("last_checked")
@ -575,20 +541,3 @@ def external_site_warning(request):
context.update(texts) context.update(texts)
return render(request, 'fellchensammlung/external_site_warning.html', context=context) return render(request, 'fellchensammlung/external_site_warning.html', context=context)
def detail_view_rescue_organization(request, rescue_organization_id):
org = RescueOrganization.objects.get(pk=rescue_organization_id)
return render(request, 'fellchensammlung/details/detail-rescue-organization.html', context={"org": org})
def export_own_profile(request):
user = request.user
ANs = AdoptionNotice.objects.filter(owner=user)
user_as_json = serialize('json', [user])
user_editable = json.loads(user_as_json)
user_editable[0]["fields"]["password"] = "Password hash redacted for security reasons"
user_as_json = json.dumps(user_editable)
ANs_as_json = serialize('json', ANs)
full_json = f"{user_as_json}, {ANs_as_json}"
return HttpResponse(full_json, content_type="application/json")

View File

@ -169,7 +169,6 @@ INSTALLED_APPS = [
'crispy_forms', 'crispy_forms',
"crispy_bootstrap4", "crispy_bootstrap4",
"rest_framework", "rest_framework",
'rest_framework.authtoken'
] ]
MIDDLEWARE = [ MIDDLEWARE = [

View File

@ -25,9 +25,9 @@ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
] ]
urlpatterns += i18n_patterns( urlpatterns += i18n_patterns (
path("", include("fellchensammlung.urls")), path("", include("fellchensammlung.urls")),
prefix_default_language=False prefix_default_language = False
) )
if settings.DEBUG: if settings.DEBUG:

View File

@ -1,10 +1,8 @@
{% load i18n %} {% load i18n %}
{{ site.name }}: {% trans "Account aktivieren" %} {% trans "Account aktivieren" %} {{ site.name }}:
{% trans 'Hier ist dein Aktivierungs-Key. Mit diesem kannst du deinen Account freischalten.' %} <a href="{{ site.domain }}{% url 'django_registration_activate' activation_key%}">{% trans "Activate by clicking this link" %}</a>
{{ activation_key }} {% trans "oder öffne den folgenden link im Browser" %}:
{{ site.domain }}{% url 'django_registration_activate' activation_key%}
{% trans "Öffne den folgenden link im Browser und gib den Aktivierungs-Key dort ein" %}: {% blocktrans %}Der link ist gültig für {{ expiration_days }} tage.{% endblocktrans %}
https://{{ site.domain }}{% url 'django_registration_activate' %}
{% blocktrans %}Der Link ist für {{ expiration_days }} Tage gültig.{% endblocktrans %}

View File

@ -1 +1 @@
{% load i18n %}{{ site.name }}: {% translate "Account aktivieren" %} {% load i18n %}{% translate "Account aktivieren" %} {{ site.name }}

View File

@ -1,15 +0,0 @@
{% extends "fellchensammlung/base_generic.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block content %}
{% if not user.is_authenticated %}
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" class="btn2" value={% translate 'Absenden' %}>
</form>
{% else %}
<p>{% translate "Du bist bereits eingeloggt." %}</p>
{% endif %}
{% endblock %}

View File

@ -2,5 +2,5 @@
{% load i18n %} {% load i18n %}
{% block content %} {% block content %}
<p>{% translate "Du bist nun registriert. Du hast eine E-Mail mit einem Link zur Aktivierung deines Kontos bekommen." %}</p> <p>{% translate "Du bist nun registriert. Du hast eine E-Mail mit einem Link zur aktivierung bekommen." %}</p>
{% endblock %} {% endblock %}

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from django.test import TestCase from django.test import TestCase
from model_bakery import baker from model_bakery import baker
from fellchensammlung.models import Announcement, Language, User, TrustLevel from fellchensammlung.models import Announcement, Language, User
class UserTest(TestCase): class UserTest(TestCase):
@ -12,7 +12,7 @@ class UserTest(TestCase):
test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org") test_user_1 = User.objects.create(username="Testuser1", password="SUPERSECRET", email="test@example.org")
self.assertTrue(test_user_1.trust_level == 1) self.assertTrue(test_user_1.trust_level == 1)
self.assertTrue(test_user_1.trust_level == TrustLevel.MEMBER) self.assertTrue(test_user_1.trust_level == User.TRUST_LEVEL[User.MEMBER])
class AnnouncementTest(TestCase): class AnnouncementTest(TestCase):

View File

@ -4,7 +4,7 @@ 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
from fellchensammlung.views import add_adoption_notice from fellchensammlung.views import add_adoption_notice
@ -20,7 +20,7 @@ class AnimalAndAdoptionTest(TestCase):
first_name="Max", first_name="Max",
last_name="Müller", last_name="Müller",
password='12345') password='12345')
test_user0.trust_level = TrustLevel.ADMIN test_user0.trust_level = User.TRUST_LEVEL[User.ADMIN]
test_user0.save() test_user0.save()
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1") adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
@ -133,7 +133,7 @@ class UpdateQueueTest(TestCase):
first_name="Admin", first_name="Admin",
last_name="BOFH", last_name="BOFH",
password='12345', password='12345',
trust_level=TrustLevel.MODERATOR) trust_level=User.TRUST_LEVEL[User.MODERATOR])
test_user0.is_superuser = True test_user0.is_superuser = True
test_user0.save() test_user0.save()