From 9cb41d35ad4a3774465fb8198f07bd2a42210718 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org>
Date: Fri, 31 Mar 2023 00:58:50 +0200
Subject: [PATCH] allow authors to view own contracts despite unapproval /
 unpublishing, default is_public to true, textfield unapproval reasoning,
 validation

---
 contracts/admin.py                            | 12 ++++-
 ...other_alter_contract_cost_unit_and_more.py | 28 ++++++++++
 .../0023_alter_contractfile_is_public.py      | 18 +++++++
 .../0024_alter_contract_is_approved.py        | 18 +++++++
 contracts/models.py                           | 52 ++++++++++++++++++-
 contracts/views.py                            | 43 +++++++++------
 6 files changed, 152 insertions(+), 19 deletions(-)
 create mode 100644 contracts/migrations/0022_contract_cost_amount_other_alter_contract_cost_unit_and_more.py
 create mode 100644 contracts/migrations/0023_alter_contractfile_is_public.py
 create mode 100644 contracts/migrations/0024_alter_contract_is_approved.py

diff --git a/contracts/admin.py b/contracts/admin.py
index 0d7f8d0..7b4aa4a 100644
--- a/contracts/admin.py
+++ b/contracts/admin.py
@@ -3,6 +3,7 @@ import typing
 
 from django.contrib import admin
 from django.contrib.auth.models import Permission
+from django.db import models
 from django.utils.html import format_html
 from import_export import resources
 from nested_admin import NestedModelAdmin, NestedStackedInline, NestedTabularInline
@@ -300,10 +301,17 @@ class ContractAdmin(
         queryset = super().get_queryset(request)
 
         if not request.user.has_perm("contracts.view_confidential"):
-            queryset = queryset.filter(is_public=True)
+            # Allow user to view their own objects, even if not public
+            queryset = queryset.filter(
+                models.Q(is_public=True)
+                | models.Q(created_by=request.user)
+            )
 
         if not request.user.has_perm("contracts.approve"):
-            queryset = queryset.filter(is_approved=True)
+            queryset = queryset.filter(
+                models.Q(is_approved=True)
+                | models.Q(created_by=request.user)
+            )
 
         return queryset
 
diff --git a/contracts/migrations/0022_contract_cost_amount_other_alter_contract_cost_unit_and_more.py b/contracts/migrations/0022_contract_cost_amount_other_alter_contract_cost_unit_and_more.py
new file mode 100644
index 0000000..8613bcd
--- /dev/null
+++ b/contracts/migrations/0022_contract_cost_amount_other_alter_contract_cost_unit_and_more.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.1.4 on 2023-03-30 21:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0021_contract_updated_on_alter_contractee_address_country_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='contract',
+            name='cost_amount_other',
+            field=models.CharField(blank=True, help_text="Je nutno vyplnit v případě, že máš vybranou možnost 'jiné' v jednotce nákladů.", max_length=128, null=True, verbose_name='Jednotka nákladů (jiné)'),
+        ),
+        migrations.AlterField(
+            model_name='contract',
+            name='cost_unit',
+            field=models.CharField(blank=True, choices=[('hour', 'Hodina'), ('month', 'Měsíc'), ('year', 'Rok'), ('total', 'Celkem'), ('other', 'Jiné')], max_length=5, null=True, verbose_name='Jednotka nákladů'),
+        ),
+        migrations.AlterField(
+            model_name='contract',
+            name='publishing_rejection_comment',
+            field=models.TextField(blank=True, help_text='Obsah není veřejně přístupný.', max_length=65536, null=True, verbose_name='Důvod nezveřejnění'),
+        ),
+    ]
diff --git a/contracts/migrations/0023_alter_contractfile_is_public.py b/contracts/migrations/0023_alter_contractfile_is_public.py
new file mode 100644
index 0000000..b948a41
--- /dev/null
+++ b/contracts/migrations/0023_alter_contractfile_is_public.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-30 21:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0022_contract_cost_amount_other_alter_contract_cost_unit_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contractfile',
+            name='is_public',
+            field=models.BooleanField(default=True, verbose_name='Veřejně dostupný'),
+        ),
+    ]
diff --git a/contracts/migrations/0024_alter_contract_is_approved.py b/contracts/migrations/0024_alter_contract_is_approved.py
new file mode 100644
index 0000000..d3bfc0e
--- /dev/null
+++ b/contracts/migrations/0024_alter_contract_is_approved.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.1.4 on 2023-03-30 22:33
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0023_alter_contractfile_is_public'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contract',
+            name='is_approved',
+            field=models.BooleanField(default=False, help_text='Mohou měnit jen schvalovatelé. Pokud je smlouva veřejná, schválením se vypustí ven.', verbose_name='Je schválená'),
+        ),
+    ]
diff --git a/contracts/models.py b/contracts/models.py
index eedfe6d..a3d10c8 100644
--- a/contracts/models.py
+++ b/contracts/models.py
@@ -374,6 +374,7 @@ class Contract(NameStrMixin, models.Model):
 
     is_approved = models.BooleanField(
         verbose_name="Je schválená",
+        default=False,
         help_text=(
             "Mohou měnit jen schvalovatelé. Pokud je "
             "smlouva veřejná, schválením se vypustí ven."
@@ -452,7 +453,7 @@ class Contract(NameStrMixin, models.Model):
         verbose_name="Stav fyzického dokumentu",
     )
 
-    publishing_rejection_comment = models.CharField(
+    publishing_rejection_comment = models.TextField(
         max_length=65536,
         blank=True,
         null=True,
@@ -487,6 +488,7 @@ class Contract(NameStrMixin, models.Model):
         MONTH = "month", "Měsíc"
         YEAR = "year", "Rok"
         TOTAL = "total", "Celkem"
+        OTHER = "other", "Jiné"
 
     cost_amount = models.PositiveIntegerField(
         blank=True, null=True, verbose_name="Náklady (Kč)"
@@ -500,6 +502,14 @@ class Contract(NameStrMixin, models.Model):
         verbose_name="Jednotka nákladů",
     )
 
+    cost_amount_other = models.CharField(
+        max_length=128,
+        verbose_name="Jednotka nákladů (jiné)",
+        help_text="Je nutno vyplnit v případě, že máš vybranou možnost 'jiné' v jednotce nákladů.",
+        blank=True,
+        null=True,
+    )
+
     filing_area = models.ForeignKey(
         ContractFilingArea,
         on_delete=models.SET_NULL,
@@ -585,6 +595,44 @@ class Contract(NameStrMixin, models.Model):
         self.save()
 
     def clean(self):
+        if (
+            not self.is_public
+            and self.publishing_rejection_comment is None
+        ):
+            raise ValidationError(
+                {
+                    "publishing_rejection_comment": "Pokud smlouva není veřejná, toto pole musí být vyplněné."
+                }
+            )
+        elif (
+            self.is_public
+            and self.publishing_rejection_comment is not None
+        ):
+            raise ValidationError(
+                {
+                    "publishing_rejection_comment": "Nemůže být definováno, pokud je smlouva veřejná."
+                }
+            )
+
+        if (
+            self.cost_unit == self.CostUnits.OTHER[1]
+            and self.cost_amount_other is None
+        ):
+            raise ValidationError(
+                {
+                    "cost_amount_other": "Musí být definováno, pokud je vybrána jednotka nákladů 'jiné'."
+                }
+            )
+        elif (
+            self.cost_unit != self.CostUnits.OTHER[1]
+            and self.cost_amount_other is not None
+        ):
+            raise ValidationError(
+                {
+                    "cost_amount_other": "Nemůže být definováno, pokud není vybrána jednotka nákladů 'jiné'."
+                }
+            )
+
         if (
             self.primary_contract is not None
             and self.is_public
@@ -625,7 +673,7 @@ class ContractFile(NameStrMixin, models.Model):
 
     is_public = models.BooleanField(
         verbose_name="Veřejně dostupný",
-        default=False,
+        default=True,
     )
 
     file = models.FileField(
diff --git a/contracts/views.py b/contracts/views.py
index 2c57b6f..0fefc00 100644
--- a/contracts/views.py
+++ b/contracts/views.py
@@ -1,8 +1,7 @@
-import typing
-
 import requests
 from django.conf import settings
 from django.core.paginator import Paginator
+from django.db import models
 from django.http import HttpResponse
 from django.shortcuts import get_object_or_404, render
 from django_downloadview import ObjectDownloadView
@@ -46,18 +45,25 @@ def get_pagination(request, objects) -> tuple:
     return page, paginator
 
 
-def get_paginated_contracts(request, filter: typing.Union[None, dict] = None) -> tuple:
+def get_paginated_contracts(request, filter=None) -> tuple:
     if filter is None:
-        filter = {}
+        filter = models.Q()
 
-    filter["is_approved"] = True
+    filter = models.Q(is_approved=True)
 
     if not request.user.has_perm("contracts.view_confidential"):
-        filter["is_public"] = True
+        filter = filter & (
+            models.Q(is_public=True) |
+            (
+                models.Q(created_by=request.user)
+                if not request.user.is_anonymous
+                else True
+            )
+        )
 
     contracts = (
         get_objects_for_user(request.user, "contracts.view_contract")
-        .filter(**filter)
+        .filter(filter)
         .order_by("valid_start_date")
         .all()
     )
@@ -84,15 +90,22 @@ def index(request):
 
 
 def view_contract(request, id: int):
-    filter = {"is_approved": True}
+    filter = models.Q(is_approved=True)
 
     if not request.user.has_perm("contracts.view_confidential"):
-        filter["is_public"] = True
+        filter = filter & (
+            models.Q(is_public=True) |
+            (
+                models.Q(created_by=request.user)
+                if not request.user.is_anonymous
+                else True
+            )
+        )
 
     contract = get_object_or_404(
         (
             get_objects_for_user(request.user, "contracts.view_contract")
-            .filter(**filter)
+            .filter(filter)
         ),
         id=id
     )
@@ -119,7 +132,7 @@ def view_contract_filing_area(request, id: int):
     )
 
     contracts_page, contracts_paginator = get_paginated_contracts(
-        request, {"filing_area": filing_area}
+        request, models.Q(filing_area=filing_area)
     )
 
     return render(
@@ -146,7 +159,7 @@ def view_contract_issue(request, id: int):
     )
 
     contracts_page, contracts_paginator = get_paginated_contracts(
-        request, {"issues": issue}
+        request, models.Q(issues=issue)
     )
 
     return render(
@@ -170,7 +183,7 @@ def view_contract_type(request, id: int):
     )
 
     contracts_page, contracts_paginator = get_paginated_contracts(
-        request, {"types": type_}
+        request, models.Q(types=type_)
     )
 
     return render(
@@ -194,7 +207,7 @@ def view_contractee(request, id: int):
     )
 
     contracts_page, contracts_paginator = get_paginated_contracts(
-        request, {"contractee_signatures__contractee": contractee}
+        request, models.Q(contractee_signatures__contractee=contractee)
     )
 
     return render(
@@ -218,7 +231,7 @@ def view_signee(request, id: int):
     )
 
     contracts_page, contracts_paginator = get_paginated_contracts(
-        request, {"signee_signatures__signee": signee}
+        request, models.Q(signee_signatures__signee=signee)
     )
 
     return render(
-- 
GitLab