]> git.menne-pb.de Git - pinpoint.git/commitdiff
Reintroduce sending mails
authorJörn Menne <jmenne@fedora.de>
Mon, 10 Feb 2025 20:21:26 +0000 (21:21 +0100)
committerJörn Menne <jmenne@fedora.de>
Mon, 10 Feb 2025 20:21:26 +0000 (21:21 +0100)
georeport/admin.py
georeport/migrations/0003_category_close_with_link.py [new file with mode: 0644]
georeport/migrations/0004_rename_user_category_users_alter_report_title.py [new file with mode: 0644]
georeport/models.py
georeport/tests.py
georeport/urls.py
georeport/views.py
pinpoint_report/settings.py

index f22a8a135d136d0b4ef74ba8f7871509c11ffcdf..a23b035363a2f34764d5ac8c4bd317b2621b5b24 100644 (file)
@@ -1,10 +1,15 @@
 # Copyright: (c) 2025, Jörn Menne <jmenne@posteo.de>
 # GNU General Public License v3.0 (see LICSENE or https://www.gnu.org/license/gpl-3.0.md)
-from django.contrib import admin
+from django.conf.global_settings import DEFAULT_FROM_EMAIL
+from django.contrib import admin, messages
 from typing import override
-
+from django.utils.translation import ngettext
 from georeport.models import Category, Report
-
+from django.conf import settings
+from Crypto.Cipher import ChaCha20
+from base64 import urlsafe_b64encode
+from django.shortcuts import reverse
+from django.core.mail import send_mail
 
 # TODO: CategoryAdmin
 
@@ -27,15 +32,162 @@ class CategoryAdmin(admin.ModelAdmin):
     the admin site, such that the model can be edited on there.
     """
 
-    exclude = ["user", "groups"]
+    # exclude = ["users", "groups"]
     inlines = [CategoryInline]
     search_fields = ["name"]
     # TODO: Prevent circles while creating groups
 
+    @override
+    def has_change_permission(self, request, obj=None):
+        """
+        Override has change_permission, in order to only
+        allow user associated to a category to modify the
+        category.
+        The association can be directly (user to category) or
+        indirectly over groups.
+        """
+        user = request.user
+        basepermission = super().has_change_permission(request, obj)
+        # Always allow superusers to modify
+        if user.is_superuser:
+            return True
+
+        if obj:
+            allowed = getAllowedUsers(obj)
+        else:
+            allowed = []
+
+        if basepermission and (request.user in allowed):
+            return True
+        return False
+
+
+def getAllowedUsers(category):
+    """
+    Function to get a queryset with all users, which should
+    have access to the category.
+
+    Arguments:
+        category: Category
+            A category, to which allowed users shall be found
+
+    Returns:
+        Queryset containing all uses associated with the category.
+    """
+    # TODO: Find better location for this function
+    qs = category.users.all()
+    for group in category.groups.all():
+        qs = qs | group.user_set.all()
+
+    if category.parent:
+        qs = qs | getAllowedUsers(category.parent)
+    return qs
+
 
 # TODO: ReportAdmin
 @admin.register(Report)
 class ReportAdmin(admin.ModelAdmin):
     exclude = [
-        "_oldstate",
+        "_oldState",
     ]
+    readonly_fields = [
+        "created_at",
+        "updated_at",
+    ]
+
+    list_display = ["title", "category__name", "state", "published"]
+    list_filter = ["state"]
+
+    @admin.action(description="Publish selected reports.")
+    def make_public(self, request, queryset):
+        """
+        Admin-action to bulk-publish reports.
+
+        Arguments:
+            request: The current http-request. The request is created by performing the action.
+            queryset: A queryset containing every Report, which was selected by the user.
+
+        """
+        updated = queryset.update(published=True)
+        self.message_user(
+            request,
+            ngettext(
+                "%d report was published",
+                "%d reports were published",
+                updated,
+            )
+            % updated,
+            messages.SUCCESS,
+        )
+
+    @override
+    def save_model(self, request, obj, form, change):
+        """
+        Override of the default save-function of a ModelAdmin to provide
+        custom actions before the object is saved.
+
+        For information about the arguments please refer to admin.ModelAdmin.save_model.
+        """
+        # send an update-mail if the state was chagned
+        if not obj.state == obj._oldState:
+            send_update(obj)
+
+        # TODO: close__link shall also work on descendants
+        if obj.state == 1 and obj.category.close_with_link:
+            send_close_link(obj)
+
+        obj._oldstate = obj.state
+        super().save_model(request, obj, form, change)
+
+
+def send_update(report):
+    # TODO: Tests
+    if not settings.SEND_MAIL:
+        return
+    recipient_list = [report.email]
+    subject = f"Report with title {report.title} was updated."
+    message = f"The state of the report {report.id}: {report.title} was changed to {report.state}."
+    send_mail(
+        subject=subject,
+        message=message,
+        from_email=settings.DEFAULT_FROM_EMAIL,
+        recipient_list=recipient_list,
+        fail_silently=True,
+    )
+
+
+def send_close_link(report):
+    """
+    If the category allows it, a link is send to the owners of the category,
+    over which the category can be closed
+    """
+    # TODO: Tests
+
+    if not settings.SEND_MAIL:
+        return
+    # Create a encrypted version of the id to send as a close_link
+    message = ""
+    padded_id = str(report.id).zfill(6)
+    cipher = ChaCha20.new(key=settings.KEY)
+    byte_text = bytes(padded_id, "utf-8")
+    ciphertext = cipher.encrypt(byte_text)
+    nonce = cipher.nonce
+
+    b64nonce = urlsafe_b64encode(nonce).decode("utf-8")
+    b64ct = urlsafe_b64encode(ciphertext).decode("utf-8")
+    url = reverse("georeport:index")
+    message = f"localhost:8000{url}{b64nonce}/{b64ct}"
+    print(message)
+
+    subject = f"Close link for report {report.id}"
+    user = getAllowedUsers(report.category)
+    recipient_list = []
+    for u in user:
+        recipient_list.append(u.email)
+    send_mail(
+        subject=subject,
+        message=message,
+        from_email=settings.DEFAULT_FROM_EMAIL,
+        recipient_list=recipient_list,
+        fail_silently=True,
+    )
diff --git a/georeport/migrations/0003_category_close_with_link.py b/georeport/migrations/0003_category_close_with_link.py
new file mode 100644 (file)
index 0000000..21d9160
--- /dev/null
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.5 on 2025-02-10 18:50
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('georeport', '0002_rename_descrption_report_description_report_latitude_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='category',
+            name='close_with_link',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/georeport/migrations/0004_rename_user_category_users_alter_report_title.py b/georeport/migrations/0004_rename_user_category_users_alter_report_title.py
new file mode 100644 (file)
index 0000000..ffc5382
--- /dev/null
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.5 on 2025-02-10 19:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('georeport', '0003_category_close_with_link'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='category',
+            old_name='user',
+            new_name='users',
+        ),
+        migrations.AlterField(
+            model_name='report',
+            name='title',
+            field=models.CharField(max_length=100),
+        ),
+    ]
index bf6d52c9e70b4be5f8d4f35a459a8f525addab1f..c52b70eed9837789c2500aea823d9111c7f54a21 100644 (file)
@@ -29,9 +29,12 @@ class Category(models.Model):
         blank=True,
     )
 
-    user = models.ManyToManyField(User, related_name="owner", blank=True)
+    users = models.ManyToManyField(User, related_name="owner", blank=True)
     groups = models.ManyToManyField(Group, related_name="group_owner", blank=True)
 
+    # TODO: Make it so, that descendens are also affected by the field
+    close_with_link = models.BooleanField(default=False)  # type:ignore
+
     class Meta:
         verbose_name_plural = "Categories"
 
@@ -63,7 +66,7 @@ class Report(models.Model):
     category = models.ForeignKey(
         Category, on_delete=models.RESTRICT, related_name="reports"
     )
-    title = models.CharField(max_length=100, unique=True)
+    title = models.CharField(max_length=100)
     description = models.TextField(blank=True, null=True)
     email = models.EmailField()
     # TODO: Images
index 0380e4c2ea663fc0dc33aae1518afc3616aebc13..0fe21d7950954234ebd52143a714772fbb36f468 100644 (file)
@@ -174,7 +174,7 @@ class GetCategoryViewTests(TestCase):
 
         user = User.objects.create_user(username="test", password="1234")
 
-        self.root1.user.add(user)
+        self.root1.users.add(user)
         self.root1.save()
         self.client.login(username="test", password="1234")
         response = self.client.get(url)
index 05bcf358b5054bc0b439a9aa6cbc8a5667180d96..0f4de3bf66d468904672b7901b0d5397e196f8eb 100644 (file)
@@ -18,5 +18,9 @@ urlpatterns = [
     path("<int:id>", views.report_detail_view, name="report"),
     path("requests/<int:id>", views.report_detail_view, name="request-id"),
     path("create", views.create_report_view, name="create"),
-    #  path("<str:b64nonce>/<str:b64ct>", views.finish_link, name="finish"),
+    path(
+        "<str:b64nonce>/<str:b64ct>",
+        views.close_with_link_view,
+        name="finish",
+    ),
 ]
index 8ff38e31df39f51bb714094b7e58e18ae9b8b24c..ee2047adf65dfd3cfe2f54b09762c5908060d085 100644 (file)
@@ -23,6 +23,8 @@ from django.core.mail import send_mail
 from Crypto.Cipher import ChaCha20
 from base64 import urlsafe_b64decode
 
+from .admin import send_update
+
 
 # TODO: test
 @require_safe
@@ -84,7 +86,7 @@ def category_detail_view(request, id) -> HttpResponse:
 
     # Check if the user is allowed to view the category
     user = request.user
-    allowed_user = cat.user.all()
+    allowed_user = cat.users.all()
     allowed_groups = cat.groups.all()
     allowed = False
     if user.is_superuser:
@@ -156,24 +158,31 @@ def report_detail_view(request, id):
 
 # TODO: Finish Link
 # TODO: Tests
-def set_report_to_finish_view(request, b64nonce, b64ciphertext):
-    nonce = urlsafe_base64_decode(b64nonce)
-    ciphertext = urlsafe_base64_decode(b64ciphertext)
+def close_with_link_view(request, b64nonce, b64ct):
+    nonce = urlsafe_b64decode(b64nonce)
+    ct = urlsafe_b64decode(b64ct)
     cipher = ChaCha20.new(key=settings.KEY, nonce=nonce)
-    id = int(cipher.decrypt(ciphertext))
+    pk = cipher.decrypt(ct)
+    id = int(pk)
     report = get_object_or_404(Report, pk=id)
 
     if report.state == 1:
         report.state = 2
         report.save()
-
-    return redirect("georeport:detail", id)
+        send_update(report)
+    return redirect("georeport:report", id)
 
 
 # TODO:Tests
-def send_creation_confirmation(report):
-    if not settings.email:
+def send_creation_confirmation(report_dict):
+    if not settings.SEND_MAIL:
         return
+    report = (
+        Report.objects.filter(title=report_dict["title"])  # type:ignore
+        .filter(latitude=report_dict["latitude"])
+        .filter(longitude=report_dict["longitude"])
+        .first()
+    )
     recipient_list = [report.email]
     subject = "Report created"
     message = f'The report with title "{report.title}" was created with id {report.id}'
@@ -182,14 +191,21 @@ def send_creation_confirmation(report):
         message=message,
         recipient_list=recipient_list,
         from_email=DEFAULT_FROM_EMAIL,
+        fail_silently=True,
     )
 
 
 # TODO: Tests
 # TODO: Recruse groupmembers mail addresses
-def send_creation_mail(report):
-    if not settings.send_mail:
+def send_creation_mail(report_dict):
+    if not settings.SEND_MAIL:
         return
+    report = (
+        Report.objects.filter(title=report_dict["title"])  # type:ignore
+        .filter(latitude=report_dict["latitude"])
+        .filter(longitude=report_dict["longitude"])
+        .first()
+    )
     recipient_list = []
     subject = f"Report {report.id} was created."
     message = (
@@ -207,4 +223,5 @@ def send_creation_mail(report):
         message=message,
         recipient_list=recipient_list,
         from_email=DEFAULT_FROM_EMAIL,
+        fail_silently=True,
     )
index add74f5c42e8f0bdf335b5e3535ce278a4b64621..e5ea4c699b234338e2b2397986bc16734c26451e 100644 (file)
@@ -12,6 +12,7 @@ https://docs.djangoproject.com/en/5.1/ref/settings/
 
 import sys
 from pathlib import Path
+from Crypto.Random import get_random_bytes
 
 
 # Build paths inside the project like this: BASE_DIR / 'subdir'.
@@ -150,3 +151,7 @@ SEND_MAIL = True
 EMAIL_HOST = "localhost"
 EMAIL_PORT = "8025"
 DEFAULT_FROM_EMAIL = "example@pinpoint-report.de"
+
+# Setup for ciphers
+# WARNING: It is advised to use a fixes 32 byte string in production
+KEY = get_random_bytes(32)