From ee6873d50d417a35cc8a42d62fb526068585cbbe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org>
Date: Fri, 24 Mar 2023 16:21:19 +0100
Subject: [PATCH] permission based counters

---
 contracts/models.py                           | 94 ++++++++++++-------
 .../contracts/view_contract_filing_areas.html |  3 +-
 .../contracts/view_contract_issues.html       |  3 +-
 .../contracts/view_contract_types.html        |  3 +-
 .../templates/contracts/view_contractees.html |  3 +-
 .../templates/contracts/view_signees.html     |  3 +-
 contracts/templatetags/counters.py            | 13 +++
 contracts/templatetags/subtract.py            |  2 +-
 oidc/auth.py                                  |  2 +-
 users/models.py                               | 58 ++++++------
 10 files changed, 114 insertions(+), 70 deletions(-)
 create mode 100644 contracts/templatetags/counters.py

diff --git a/contracts/models.py b/contracts/models.py
index 9fe9d08..e2589a9 100644
--- a/contracts/models.py
+++ b/contracts/models.py
@@ -11,6 +11,32 @@ from shared.models import NameStrMixin
 from users.models import User
 
 
+class ContractCountMixin(models.Model):
+    def get_contract_count(self, user) -> None:
+        filter = {"is_approved": True}
+
+        if not user.has_perm("contract.view_confidential"):
+            filter["is_public"] = True
+
+        return self.contracts.filter(**filter).count()
+
+    class Meta:
+        abstract = True
+
+
+class SignatureCountMixin(models.Model):
+    def get_signature_count(self, user) -> None:
+        filter = {"contract__is_approved": True}
+
+        if not user.has_perm("contract.view_confidential"):
+            filter["contract__is_approved"] = True
+
+        return self.signatures.filter(**filter).count()
+
+    class Meta:
+        abstract = True
+
+
 class RepresentativeMixin:
     @property
     def name(self):
@@ -29,7 +55,7 @@ class RepresentativeMixin:
         return result
 
 
-class Signee(models.Model):
+class Signee(SignatureCountMixin, models.Model):
     name = models.CharField(
         max_length=256,
         verbose_name="Jméno",
@@ -100,12 +126,6 @@ class Signee(models.Model):
         verbose_name="Role",
     )
 
-    class Meta:
-        app_label = "contracts"
-
-        verbose_name = "Jiná smluvní strana"
-        verbose_name_plural = "Ostatní smluvní strany"
-
     @property
     def url(self) -> str:
         return reverse("contracts:view_signee", args=(self.id,))
@@ -131,8 +151,14 @@ class Signee(models.Model):
 
         return result
 
+    class Meta:
+        app_label = "contracts"
+
+        verbose_name = "Jiná smluvní strana"
+        verbose_name_plural = "Ostatní smluvní strany"
 
-class Contractee(models.Model):
+
+class Contractee(SignatureCountMixin, models.Model):
     name = models.CharField(
         max_length=256,
         default=settings.DEFAULT_CONTRACTEE_NAME,
@@ -189,12 +215,6 @@ class Contractee(models.Model):
     def url(self) -> str:
         return reverse("contracts:view_contractee", args=(self.id,))
 
-    class Meta:
-        app_label = "contracts"
-
-        verbose_name = "Naše smluvní strana"
-        verbose_name_plural = "Naše smluvní strany"
-
     def __str__(self) -> str:
         result = self.name
 
@@ -203,8 +223,14 @@ class Contractee(models.Model):
 
         return result
 
+    class Meta:
+        app_label = "contracts"
+
+        verbose_name = "Naše smluvní strana"
+        verbose_name_plural = "Naše smluvní strany"
+
 
-class ContractType(NameStrMixin, models.Model):
+class ContractType(ContractCountMixin, NameStrMixin, models.Model):
     name = models.CharField(
         max_length=32,
         verbose_name="Jméno",
@@ -221,7 +247,7 @@ class ContractType(NameStrMixin, models.Model):
         verbose_name_plural = "Typy smluv"
 
 
-class ContractIssue(NameStrMixin, models.Model):
+class ContractIssue(ContractCountMixin, NameStrMixin, models.Model):
     name = models.CharField(
         max_length=32,
         verbose_name="Jméno",
@@ -238,7 +264,7 @@ class ContractIssue(NameStrMixin, models.Model):
         verbose_name_plural = "Problémy se smlouvami"
 
 
-class ContractFilingArea(NameStrMixin, models.Model):
+class ContractFilingArea(ContractCountMixin, NameStrMixin, models.Model):
     name = models.CharField(
         max_length=32,
         verbose_name="Jméno",
@@ -448,17 +474,6 @@ class Contract(NameStrMixin, models.Model):
         help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat a pro tajné čtenáře.",
     )
 
-    class Meta:
-        app_label = "contracts"
-
-        verbose_name = "Smlouva"
-        verbose_name_plural = "Smlouvy"
-
-        permissions = (
-            ("approve", "Schválit / zrušit schválení"),
-            ("view_confidential", "Zobrazit tajné informace"),
-        )
-
     @property
     def primary_contract_url(self) -> typing.Union[None, str]:
         if self.primary_contract is None:
@@ -516,6 +531,17 @@ class Contract(NameStrMixin, models.Model):
 
         self.save()
 
+    class Meta:
+        app_label = "contracts"
+
+        verbose_name = "Smlouva"
+        verbose_name_plural = "Smlouvy"
+
+        permissions = (
+            ("approve", "Schválit / zrušit schválení"),
+            ("view_confidential", "Zobrazit tajné informace"),
+        )
+
 
 class ContractFile(NameStrMixin, models.Model):
     name = models.CharField(
@@ -575,15 +601,15 @@ class ContracteeSignature(models.Model):
         verbose_name="Datum podpisu",
     )
 
+    def __str__(self) -> str:
+        return f"{str(self.contractee)} - {self.date}"
+
     class Meta:
         app_label = "contracts"
 
         verbose_name = "Podpis naší smluvní strany"
         verbose_name_plural = "Podpisy našich smluvních stran"
 
-    def __str__(self) -> str:
-        return f"{str(self.contractee)} - {self.date}"
-
 
 class SigneeSignature(models.Model):
     signee = models.ForeignKey(
@@ -604,15 +630,15 @@ class SigneeSignature(models.Model):
         verbose_name="Datum podpisu",
     )
 
+    def __str__(self) -> str:
+        return f"{str(self.signee)} - {self.date}"
+
     class Meta:
         app_label = "contracts"
 
         verbose_name = "Podpis jiné smluvní strany"
         verbose_name_plural = "Podpisy jiných smluvních stran"
 
-    def __str__(self) -> str:
-        return f"{str(self.signee)} - {self.date}"
-
 
 class ContracteeSignatureRepresentative(RepresentativeMixin, models.Model):
     contractee_signature = models.ForeignKey(
diff --git a/contracts/templates/contracts/view_contract_filing_areas.html b/contracts/templates/contracts/view_contract_filing_areas.html
index 4ee14b8..0bd344f 100644
--- a/contracts/templates/contracts/view_contract_filing_areas.html
+++ b/contracts/templates/contracts/view_contract_filing_areas.html
@@ -1,4 +1,5 @@
 {% extends "shared/includes/base.html" %}
+{% load counters %}
 
 {% block content %}
     <div class="flex gap-4 mb-10">
@@ -30,7 +31,7 @@
                         {{ filing_area.person_responsible }}
                     </td>
                     <td>
-                        {{ filing_area.contracts.count }}
+                        {{ filing_area|contract_count:user }}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/contracts/templates/contracts/view_contract_issues.html b/contracts/templates/contracts/view_contract_issues.html
index bbabe02..b323c33 100644
--- a/contracts/templates/contracts/view_contract_issues.html
+++ b/contracts/templates/contracts/view_contract_issues.html
@@ -1,4 +1,5 @@
 {% extends "shared/includes/base.html" %}
+{% load counters %}
 
 {% block content %}
     <div class="flex gap-4 mb-10">
@@ -38,7 +39,7 @@
                         >{{ issue.name }}</a>
                     </td>
                     <td>
-                        {{ issue.contracts.count }}
+                        {{ issue|contract_count:user }}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/contracts/templates/contracts/view_contract_types.html b/contracts/templates/contracts/view_contract_types.html
index 6a674b6..dd165b3 100644
--- a/contracts/templates/contracts/view_contract_types.html
+++ b/contracts/templates/contracts/view_contract_types.html
@@ -1,4 +1,5 @@
 {% extends "shared/includes/base.html" %}
+{% load counters %}
 
 {% block content %}
     <div class="flex gap-4 mb-10">
@@ -26,7 +27,7 @@
                         >{{ type.name }}</a>
                     </td>
                     <td>
-                        {{ type.contracts.count }}
+                        {{ type|contract_count:user }}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/contracts/templates/contracts/view_contractees.html b/contracts/templates/contracts/view_contractees.html
index 6c33a7b..c8c51cb 100644
--- a/contracts/templates/contracts/view_contractees.html
+++ b/contracts/templates/contracts/view_contractees.html
@@ -1,4 +1,5 @@
 {% extends "shared/includes/base.html" %}
+{% load counters %}
 
 {% block content %}
     <div class="flex gap-4 mb-10">
@@ -34,7 +35,7 @@
                         </a>
                     </td>
                     <td>
-                        {{ contractee.signatures.count }}
+                        {{ contractee|signature_count:user }}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/contracts/templates/contracts/view_signees.html b/contracts/templates/contracts/view_signees.html
index 758e132..2474256 100644
--- a/contracts/templates/contracts/view_signees.html
+++ b/contracts/templates/contracts/view_signees.html
@@ -1,4 +1,5 @@
 {% extends "shared/includes/base.html" %}
+{% load counters %}
 
 {% block content %}
     <div class="flex gap-4 mb-10">
@@ -34,7 +35,7 @@
                         </a>
                     </td>
                     <td>
-                        {{ signee.signatures.count }}
+                        {{ signee|signature_count:user }}
                     </td>
                 </tr>
             {% endfor %}
diff --git a/contracts/templatetags/counters.py b/contracts/templatetags/counters.py
new file mode 100644
index 0000000..2757785
--- /dev/null
+++ b/contracts/templatetags/counters.py
@@ -0,0 +1,13 @@
+from django import template
+
+register = template.Library()
+
+
+@register.filter
+def contract_count(model, user) -> int:
+    return model.get_contract_count(user)
+
+
+@register.filter
+def signature_count(model, user) -> int:
+    return model.get_signature_count(user)
diff --git a/contracts/templatetags/subtract.py b/contracts/templatetags/subtract.py
index baf7853..d39e880 100644
--- a/contracts/templatetags/subtract.py
+++ b/contracts/templatetags/subtract.py
@@ -3,6 +3,6 @@ from django import template
 register = template.Library()
 
 
-@register.filter(name="subtract")
+@register.filter
 def subtract(value, arg):
     return value - arg
diff --git a/oidc/auth.py b/oidc/auth.py
index 78db056..07cea60 100644
--- a/oidc/auth.py
+++ b/oidc/auth.py
@@ -17,7 +17,7 @@ class RegistryOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend):
             user_groups = user.groups.all()
 
         for group in access_token["groups"]:
-            if group.startswith("_"):
+            if group.startswith("_"):  # Ignore internal Keycloak groups
                 continue
 
             group_name = f"sso_{group}"
diff --git a/users/models.py b/users/models.py
index 2d39275..d12e77b 100644
--- a/users/models.py
+++ b/users/models.py
@@ -15,17 +15,26 @@ class User(pirates_models.AbstractUser):
         ),
     )
 
-    def set_unusable_password(self) -> None:
-        # Purely for compatibility with Guardian
-        pass
+    @property
+    def can_approve_contracts(self) -> bool:
+        return self.has_perm("contracts.approve")
 
-    def get_username(self) -> str:
-        first_name = self.first_name
+    @property
+    def can_create_contracts(self) -> bool:
+        return self.has_perm("contracts.add")
 
-        if len(first_name) != 0:
-            first_name += " "
+    @property
+    def can_view_confidential(self) -> bool:
+        return self.has_perm("contracts.view_confidential")
 
-        return f"{first_name}{self.last_name}"
+    @property
+    def contracts_to_approve_count(self) -> int:
+        if not self.can_approve_contracts:
+            return 0
+
+        from contracts.models import Contract
+
+        return Contract.objects.filter(is_approved=False).count()
 
     # https://docs.djangoproject.com/en/4.1/ref/models/instances/#customizing-model-loading
     @classmethod
@@ -53,6 +62,18 @@ class User(pirates_models.AbstractUser):
 
         return instance
 
+    def set_unusable_password(self) -> None:
+        # Purely for compatibility with Guardian
+        pass
+
+    def get_username(self) -> str:
+        first_name = self.first_name
+
+        if len(first_name) != 0:
+            first_name += " "
+
+        return f"{first_name}{self.last_name}"
+
     def save(self, *args, saved_by_auth: bool = False, **kwargs):
         if (
             not self._state.adding
@@ -70,27 +91,6 @@ class User(pirates_models.AbstractUser):
         self.is_staff_based_on_group = True
         self.is_staff = self.groups.filter(name=settings.DEFAULT_STAFF_GROUP).exists()
 
-    @property
-    def can_approve_contracts(self) -> bool:
-        return self.has_perm("contracts.approve")
-
-    @property
-    def can_create_contracts(self) -> bool:
-        return self.has_perm("contracts.add")
-
-    @property
-    def can_view_confidential(self) -> bool:
-        return self.has_perm("contracts.view_confidential")
-
-    @property
-    def contracts_to_approve_count(self) -> int:
-        if not self.can_approve_contracts:
-            return 0
-
-        from contracts.models import Contract
-
-        return Contract.objects.filter(is_approved=False).count()
-
     class Meta:
         app_label = "users"
         verbose_name = "Uživatel"
-- 
GitLab