Compare commits

...

37 Commits

Author SHA1 Message Date
3eb7dbe984 fix: Allow all notifications to be marked as read
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
2024-11-20 23:37:55 +01:00
202dfe46c2 fix: For some reason this needs width now? 2024-11-20 23:34:25 +01:00
01da0f1e29 feat: add 403 page 2024-11-20 23:23:06 +01:00
8ccdf50bc5 refactor: Use new trust level class 2024-11-20 23:08:02 +01:00
d46ab8da6b feat: Show all notifications on profile 2024-11-20 23:03:02 +01:00
1dd53a87e9 feat: Notify admins of new user via notification framework #10 2024-11-20 23:02:41 +01:00
40bb2e54bd fix: Readjust trust level 2024-11-20 23:01:19 +01:00
433ad9d4b9 refactor: remove unnecessary print 2024-11-20 22:53:48 +01:00
231c27819d feat: Send e-mail notifications to user 2024-11-20 20:26:44 +01:00
890309564f feat: Add field for user to opt-out of e-mail notifications 2024-11-20 20:26:05 +01:00
e1e1f822c8 fix: Use correct view 2024-11-20 20:00:36 +01:00
7a788f4c90 fix: Allow users to perform actions on own profile 2024-11-20 20:00:27 +01:00
7efa626b8b feat: Migrate to new Integer choice field to allow nicer handling 2024-11-20 19:55:16 +01:00
08e20e1875 feat: Add basic user data export 2024-11-18 23:01:27 +01:00
f1c79a5f94 feat: UI improvements for user profile 2024-11-18 22:58:32 +01:00
5dd1991af8 feat: Restructure view of own profile, add token authorization for API 2024-11-18 22:41:12 +01:00
c0edef51bd feat: add explanation for signup reason 2024-11-18 18:34:12 +01:00
cb703e79ae feat: add fancy rat 2024-11-18 18:33:53 +01:00
87066b0cea feat: add signup-reason 2024-11-14 21:54:32 +01:00
c4976c4b34 feat: Upgrade django-registration to 5.1 2024-11-14 21:54:17 +01:00
ee46ff9cda fix: typo 2024-11-14 21:53:24 +01:00
d4f27e8f2f feat: Allow to set organization when creating adoption notice 2024-11-14 21:11:34 +01:00
4a6584370e feat: re-add understrike to buttons 2024-11-14 21:11:12 +01:00
82d3f95c99 feat: add understrike to improve accessibility 2024-11-14 21:00:52 +01:00
dce3d89c7e feat: rename comment to internal comment 2024-11-14 19:31:42 +01:00
5520590145 feat: Add link to rescue org 2024-11-14 19:29:41 +01:00
efabebfdbf feat: Add ANs to rescue organization 2024-11-14 19:27:32 +01:00
6c52246bb7 feat: Add detail view for organizations 2024-11-14 19:16:47 +01:00
2c11f7c385 feat: Add description for organizations 2024-11-14 19:01:24 +01:00
9ee0bd8e30 feat: Add rescue to detail view 2024-11-14 18:49:28 +01:00
1955476d24 feat: Improve activation e-mail format 2024-11-14 18:32:08 +01:00
05178da029 feat: Add captcha to registration 2024-11-14 18:30:51 +01:00
7a80cf8df1 refactor: remove unused 2024-11-14 18:29:56 +01:00
db94ec41ed feat: Add organization affiliation to user 2024-11-14 18:28:55 +01:00
5582538a70 fix: Pin django registration version, otherwise causes reverse error 2024-11-14 18:28:02 +01:00
7aa364fc38 refactor: remove unnecessary print 2024-11-14 07:10:49 +01:00
96ce5963fe feat: add phone number 2024-11-14 07:10:27 +01:00
33 changed files with 518 additions and 165 deletions

View File

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

View File

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

View File

@ -9,6 +9,14 @@ from django.utils.translation import gettext_lazy as _
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):
input_type = 'date'
@ -43,6 +51,7 @@ class AdoptionNoticeForm(forms.ModelForm):
'group_only',
'searching_since',
'location_string',
'organization',
'description',
'further_information',
),
@ -50,19 +59,19 @@ class AdoptionNoticeForm(forms.ModelForm):
class Meta:
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 Meta:
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"]
widgets = {
'searching_since': DateInput(),
}
class AnimalForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
if 'in_adoption_notice_creation_flow' in kwargs:
@ -91,6 +100,7 @@ class AnimalFormWithDateWidget(AnimalForm):
'date_of_birth': DateInput(),
}
class AdoptionNoticeFormWithDateWidgetAutoAnimal(AdoptionNoticeFormWithDateWidget):
def __init__(self, *args, **kwargs):
super(AdoptionNoticeFormWithDateWidgetAutoAnimal, self).__init__(*args, **kwargs)
@ -159,6 +169,8 @@ class CustomRegistrationForm(RegistrationForm):
class Meta(RegistrationForm.Meta):
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):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
@ -169,7 +181,7 @@ class CustomRegistrationForm(RegistrationForm):
def _get_distances():
return {i: i for i in [10, 20, 50, 100, 200, 500]}
return {i: i for i in [20, 50, 100, 200, 500]}
class AdoptionNoticeSearchForm(forms.Form):

View File

@ -1,15 +1,10 @@
from venv import create
import django.conf.global_settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.utils.translation import gettext
from django.conf import settings
from django.core import mail
from django.db.models import Q, Min
from fellchensammlung.models import User
from fellchensammlung.models import User, CommentNotification, BaseNotification, TrustLevel
from notfellchen.settings import host
NEWLINE = "\r\n"
@ -17,7 +12,7 @@ NEWLINE = "\r\n"
def mail_admins_new_report(report):
subject = _("Neue Meldung")
for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
for moderator in User.objects.filter(trust_level__gt=TrustLevel.MODERATOR):
greeting = _("Moin,") + "{NEWLINE}"
new_report_text = _("es wurde ein Regelverstoß gemeldet.") + "{NEWLINE}"
if len(report.reported_broken_rules.all()) > 0:
@ -34,23 +29,15 @@ def mail_admins_new_report(report):
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
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [moderator.email])
print("Sending email to ", moderator.email)
message.send()
@receiver(post_save, sender=User)
def mail_admins_new_member(sender, instance: User, created: bool, **kwargs):
if not created:
return
subject = _("Neuer User") + f": {instance.username}"
for moderator in User.objects.filter(trust_level__gt=User.TRUST_LEVEL[User.MODERATOR]):
greeting = _("Moin,") + "{NEWLINE}"
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()
def send_notification_email(notification_pk):
try:
notification = CommentNotification.objects.get(pk=notification_pk)
except CommentNotification.DoesNotExist:
notification = BaseNotification.objects.get(pk=notification_pk)
subject = f"🔔 {notification.title}"
body_text = notification.text
message = mail.EmailMessage(subject, body_text, settings.DEFAULT_FROM_EMAIL, [notification.user.email])
message.send()

View File

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

View File

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,19 @@
# 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

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,18 @@
# 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

@ -0,0 +1,19 @@
# 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

@ -0,0 +1,23 @@
# 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

@ -0,0 +1,18 @@
# 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,86 +34,6 @@ class Language(models.Model):
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):
place_id = models.IntegerField()
latitude = models.FloatField()
@ -186,10 +106,104 @@ class RescueOrganization(models.Model):
facebook = models.URLField(null=True, blank=True, verbose_name=_('Facebook Profil'))
fediverse_profile = models.URLField(null=True, blank=True, verbose_name=_('Fediverse Profil'))
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'))
updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True)
comment = models.TextField(verbose_name=_("Kommentar"), null=True, blank=True,)
internal_comment = models.TextField(verbose_name=_("Interner 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):
@ -239,7 +253,6 @@ class AdoptionNotice(models.Model):
else:
return "mixed"
@property
def comments(self):
return Comment.objects.filter(adoption_notice=self)
@ -715,7 +728,6 @@ class CommentNotification(BaseNotification):
@property
def url(self):
print(f"URL: self.comment.get_absolute_url()")
return self.comment.get_absolute_url

View File

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

View File

@ -1,5 +1,7 @@
from celery.app import shared_task
from django.utils import timezone
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.misc import healthcheck_ok
from .models import Location, AdoptionNotice, Timestamp
@ -43,3 +45,8 @@ def add_adoption_notice_location(pk):
def task_healthcheck():
healthcheck_ok()
set_timestamp("task_healthcheck")
@shared_task
def task_send_notification_email(notification_pk):
send_notification_email(notification_pk)

View File

@ -0,0 +1,29 @@
{% 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,12 +13,38 @@
<p>{% translate "Keine bevorzugte Sprache gesetzt." %}</p>
{% endif %}
{% if user.id is request.user.id %}
<div class="container-cards">
{% 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>
{% include "fellchensammlung/lists/list-notifications.html" %}
<h2>{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
<h2>{% translate 'Benachrichtigungen' %}</h2>
{% include "fellchensammlung/lists/list-notifications.html" %}
<h2>{% translate 'Meine Vermittlungen' %}</h2>
{% include "fellchensammlung/lists/list-adoption-notices.html" %}
{% endif %}
{% endif %}
{% endblock %}

View File

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

View File

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

View File

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

View File

@ -24,6 +24,8 @@ urlpatterns = [
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
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/
path("suchen/", views.search, name="search"),
@ -50,7 +52,9 @@ urlpatterns = [
## USERS ##
###########
# ex: user/1
path("user/<int:user_id>/", views.user_detail, name="user-detail"),
path("user/<int:user_id>/", views.user_by_id, 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/',
RegistrationView.as_view(

View File

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

View File

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

View File

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

View File

@ -0,0 +1,15 @@
{% 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 %}
{% block content %}
<p>{% translate "Du bist nun registriert. Du hast eine E-Mail mit einem Link zur aktivierung bekommen." %}</p>
<p>{% translate "Du bist nun registriert. Du hast eine E-Mail mit einem Link zur Aktivierung deines Kontos bekommen." %}</p>
{% endblock %}

View File

@ -4,7 +4,7 @@ from django.utils import timezone
from django.test import TestCase
from model_bakery import baker
from fellchensammlung.models import Announcement, Language, User
from fellchensammlung.models import Announcement, Language, User, TrustLevel
class UserTest(TestCase):
@ -12,7 +12,7 @@ class UserTest(TestCase):
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 == User.TRUST_LEVEL[User.MEMBER])
self.assertTrue(test_user_1.trust_level == TrustLevel.MEMBER)
class AnnouncementTest(TestCase):

View File

@ -4,7 +4,7 @@ from django.urls import reverse
from model_bakery import baker
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus
from fellchensammlung.models import Animal, Species, AdoptionNotice, User, Location, AdoptionNoticeStatus, TrustLevel
from fellchensammlung.views import add_adoption_notice
@ -20,7 +20,7 @@ class AnimalAndAdoptionTest(TestCase):
first_name="Max",
last_name="Müller",
password='12345')
test_user0.trust_level = User.TRUST_LEVEL[User.ADMIN]
test_user0.trust_level = TrustLevel.ADMIN
test_user0.save()
adoption1 = baker.make(AdoptionNotice, name="TestAdoption1")
@ -133,7 +133,7 @@ class UpdateQueueTest(TestCase):
first_name="Admin",
last_name="BOFH",
password='12345',
trust_level=User.TRUST_LEVEL[User.MODERATOR])
trust_level=TrustLevel.MODERATOR)
test_user0.is_superuser = True
test_user0.save()