diff --git a/contracts/admin.py b/contracts/admin.py index b6080662ca94e9486ea19da965061c3a53ba0f9c..8a6d17b9411672418ee5ee0b609952d2b496368e 100644 --- a/contracts/admin.py +++ b/contracts/admin.py @@ -2,6 +2,7 @@ import copy import json import logging import typing +import uuid import requests from admin_auto_filters.filters import AutocompleteFilterFactory @@ -94,7 +95,9 @@ class OwnPermissionsMixin( ParentContractApprovedPermissionsMixin = permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - obj_conditional=lambda request, obj: get_obj_contract(obj).is_approved, + obj_conditional=lambda request, obj: not get_obj_contract( + obj + ).is_editable_without_approve_permission, ) @@ -163,7 +166,7 @@ class ContractAdmin( permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - obj_conditional=lambda request, obj: obj.is_approved, + obj_conditional=lambda request, obj: not obj.is_editable_without_approve_permission, ), MarkdownxGuardedModelAdmin, NestedModelAdmin, @@ -251,6 +254,7 @@ class ContractAdmin( ] }, ), + ("Stav", {"fields": ["status"]}), ( "Doplňující informace", { @@ -275,33 +279,22 @@ class ContractAdmin( "publishing_rejection_comment", ) - if obj is not None and request.user.has_perm("contracts.approve"): - fieldsets.insert( - 5, - ("Schválení", {"fields": ["is_approved"]}), - ) - return fieldsets - def get_fieldsets_and_inlines_order(self, context) -> list: - order = [ - FieldsetInlineOrder.FIELDSET, - FieldsetInlineOrder.FIELDSET, - FieldsetInlineOrder.FIELDSET, - FieldsetInlineOrder.INLINE, - FieldsetInlineOrder.INLINE, - FieldsetInlineOrder.INLINE, - FieldsetInlineOrder.FIELDSET, - FieldsetInlineOrder.INLINE, - FieldsetInlineOrder.INLINE, - FieldsetInlineOrder.FIELDSET, - FieldsetInlineOrder.FIELDSET, - ] - - if context["user"].has_perm("contracts.approve"): - order.insert(11, FieldsetInlineOrder.FIELDSET) - - return order + fieldsets_and_inlines_order = order = [ + FieldsetInlineOrder.FIELDSET, + FieldsetInlineOrder.FIELDSET, + FieldsetInlineOrder.FIELDSET, + FieldsetInlineOrder.INLINE, + FieldsetInlineOrder.INLINE, + FieldsetInlineOrder.INLINE, + FieldsetInlineOrder.FIELDSET, + FieldsetInlineOrder.INLINE, + FieldsetInlineOrder.INLINE, + FieldsetInlineOrder.FIELDSET, + FieldsetInlineOrder.INLINE, + FieldsetInlineOrder.FIELDSET, + ] def get_queryset(self, request): queryset = super().get_queryset(request) @@ -314,7 +307,8 @@ class ContractAdmin( if not request.user.has_perm("contracts.approve"): queryset = queryset.filter( - models.Q(is_approved=True) | models.Q(created_by=request.user) + models.Q(status=Contract.StatusTypes.APPROVED) + | models.Q(created_by=request.user) ) return queryset @@ -343,45 +337,125 @@ class ContractAdmin( from users.models import User - if is_new: - try: - sso_ids = [] - - for user in User.objects.filter(is_staff=True).all(): - if user.is_superuser or user.has_perm("contracts.approve"): - sso_ids.append(user.sso_id) - - requests.post( - settings.NASTENKA_API_URL, - data=json.dumps( - { - "name": f"Nová smlouva ke schválení - {obj.name}", - "description": ( - obj.summary - if obj.summary is not None - else "Bez popisu." - ), - "contract_id": obj.id, - "sso_ids": sso_ids, - } - ), - headers={ - "Authorization": f"Token {settings.NASTENKA_API_TOKEN}", - "Content-Type": "application/json", - }, - ) - except Exception as exception: - logger.error( - "Failed to synchronizace Nástěnka notices: %s", str(exception) - ) + if is_new or obj.status != form.initial["status"]: + headers = { + "Authorization": f"Token {settings.NASTENKA_API_TOKEN}", + "Content-Type": "application/json", + } + + if obj.status == obj.StatusTypes.TO_BE_APPROVED: + try: + sso_ids = [] + + for user in User.objects.filter(is_staff=True).all(): + print(user, user.has_perm("contracts.approve")) + + if user.has_perm("contracts.approve"): + sso_ids.append(user.sso_id) + + notice = requests.post( + settings.NASTENKA_API_URL, + data=json.dumps( + { + "name": f"Smlouva ke schválení - {obj.name}", + "description": ( + obj.summary + if obj.summary is not None + else "Bez popisu." + ), + "contract_id": obj.id, + "sso_ids": sso_ids, + } + ), + headers=headers, + ) + + notice.raise_for_status() + notice = notice.json() + + obj.to_be_approved_nastenka_notice_id = uuid.UUID(notice["id"]) + obj.save() + except Exception as exception: + logger.error( + 'Failed to send out notice "to be approved" Nástěnka notification: %s', + str(exception), + ) + elif obj.status == obj.StatusTypes.APPROVED: + try: + user = User.objects.filter(id=obj.created_by.id).first() + + requests.post( + settings.NASTENKA_API_URL, + data=json.dumps( + { + "name": f"Smlouva schválena - {obj.name}", + "description": ( + obj.summary + if obj.summary is not None + else "Bez popisu." + ), + "contract_id": obj.id, + "sso_ids": [user.sso_id], + } + ), + headers=headers, + ).raise_for_status() + + if obj.to_be_approved_nastenka_notice_id is not None: + requests.delete( + f"{settings.NASTENKA_API_URL}/{obj.to_be_approved_nastenka_notice_id}", + headers=headers, + ).raise_for_status() + + obj.to_be_approved_nastenka_notice_id = None + obj.save() + except Exception as exception: + logger.error( + 'Failed to send out "contract approved" Nástěnka notification: %s', + str(exception), + ) + elif obj.status == obj.StatusTypes.REJECTED: + try: + user = User.objects.filter(id=obj.created_by.id).first() + + requests.post( + settings.NASTENKA_API_URL, + data=json.dumps( + { + "name": f"Smlouva odmítnuta - {obj.name}", + "description": ( + obj.summary + if obj.summary is not None + else "Bez popisu." + ), + "contract_id": obj.id, + "sso_ids": [user.sso_id], + } + ), + headers=headers, + ).raise_for_status() + + if obj.to_be_approved_nastenka_notice_id is not None: + requests.delete( + f"{settings.NASTENKA_API_URL}/{obj.to_be_approved_nastenka_notice_id}", + headers=headers, + ).raise_for_status() + + obj.to_be_approved_nastenka_notice_id = None + obj.save() + except Exception as exception: + logger.error( + 'Failed to send out "contract rejected" Nástěnka notification: %s', + str(exception), + ) return parent_save_response def has_change_permission(self, request, obj=None): if ( obj is not None - and obj.is_approved and not request.user.has_perm("contracts.edit_when_approved") + and not obj.is_editable_without_approve_permission ): return False @@ -390,8 +464,8 @@ class ContractAdmin( def has_delete_permission(self, request, obj=None): if ( obj is not None - and obj.is_approved and not request.user.has_perm("contracts.delete_when_approved") + and not obj.is_editable_without_approve_permission ): return False @@ -405,7 +479,7 @@ class ContractAdmin( "Naše smluvná strana", "contractee_signatures__contractee" ), AutocompleteFilterFactory("Jiná smluvní strana", "signee_signatures__signee"), - "is_approved", + "status", "is_valid", "is_public", "paper_form_state", @@ -416,7 +490,7 @@ class ContractAdmin( list_display = ( "name", - "is_approved", + "status", "is_public", "created_on", ) @@ -539,7 +613,9 @@ class SigneeSignatureRepresentativeAdmin( permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - obj_conditional=lambda request, obj: get_obj_signee_contract(obj).is_approved, + obj_conditional=lambda request, obj: not get_obj_signee_contract( + obj + ).is_editable_without_approve_permission, ), permissions_mixin_factory( "contracts.edit_others", @@ -556,9 +632,9 @@ class ContracteeSignatureRepresentativeAdmin( permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - obj_conditional=lambda request, obj: get_obj_contractee_contract( + obj_conditional=lambda request, obj: not get_obj_contractee_contract( obj - ).is_approved, + ).is_editable_without_approve_permission, ), permissions_mixin_factory( "contracts.edit_others", diff --git a/contracts/forms.py b/contracts/forms.py index a33c8323e440aed38d6a8cd758980f0385424ae9..04ca974db87942eedf120174c3f6ae49f203d9de 100644 --- a/contracts/forms.py +++ b/contracts/forms.py @@ -28,14 +28,15 @@ class ContractAdminForm(forms.ModelForm): if ( not self.current_user.is_superuser - and self.instance.is_approved - is not clean_data.get("is_approved", self.instance.is_approved) + and self.instance.status + in (self.instance.StatusTypes.APPROVED, self.instance.StatusTypes.REJECTED) + and (self.instance.status == clean_data.get("status", self.instance.status)) and self.current_user == self.instance.created_by ): raise ValidationError( { - "is_approved": ( - "Smlouva nemůže být schválena uživatelem, " "který ji nahrál." + "status": ( + "Smlouva nemůže být schválena uživatelem, který ji nahrál." ) } ) diff --git a/contracts/migrations/0063_remove_contract_is_approved_contract_status.py b/contracts/migrations/0063_remove_contract_is_approved_contract_status.py new file mode 100644 index 0000000000000000000000000000000000000000..ccb43df82625f9983f95891dd28493acd83fd888 --- /dev/null +++ b/contracts/migrations/0063_remove_contract_is_approved_contract_status.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.4 on 2023-07-01 13:37 + +from django.db import migrations, models + + +def migrate_between_fields(apps, schema) -> None: + Contract = apps.get_model("contracts", "contract") + + from contracts.models import Contract as ContractOrignalModel + + for contract in Contract.objects.filter(is_approved=True).all(): + contract.status = ContractOrignalModel.StatusTypes.APPROVED + contract.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("contracts", "0062_contract_paper_form_person_responsible"), + ] + + operations = [ + migrations.AddField( + model_name="contract", + name="status", + field=models.CharField( + choices=[ + ("work_in_progress", "Rozpracovaná"), + ("to_be_approved", "Ke schválení"), + ("approved", "Schválená"), + ("rejected", "Zamítnutá"), + ], + default="work_in_progress", + help_text='Označením jako "Ke schválení" se smlouva předá schvalovateli.', + max_length=16, + verbose_name="Status", + ), + ), + migrations.RunPython(migrate_between_fields), + migrations.RemoveField( + model_name="contract", + name="is_approved", + ), + ] diff --git a/contracts/migrations/0064_contract_to_be_approved_nastenka_notice_id.py b/contracts/migrations/0064_contract_to_be_approved_nastenka_notice_id.py new file mode 100644 index 0000000000000000000000000000000000000000..ca479c82c41c1e2d298dd26b9b4a40b63e19bc77 --- /dev/null +++ b/contracts/migrations/0064_contract_to_be_approved_nastenka_notice_id.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.4 on 2023-07-01 16:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("contracts", "0063_remove_contract_is_approved_contract_status"), + ] + + operations = [ + migrations.AddField( + model_name="contract", + name="to_be_approved_nastenka_notice_id", + field=models.UUIDField( + blank=True, + null=True, + verbose_name="ID oznámení o nové smlouvě v Nástěnce", + ), + ), + ] diff --git a/contracts/models.py b/contracts/models.py index c948ca56e8065b48a4b6dd3ee1e21c28904ceee5..c2a6786767a195a5e860efae497005df6669e7e1 100644 --- a/contracts/models.py +++ b/contracts/models.py @@ -40,7 +40,7 @@ class OwnPermissionsMixin(models.Model): class ContractCountMixin(models.Model): def get_contract_count(self, user) -> None: - filter = {"is_approved": True} + filter = {"status": Contract.StatusTypes.APPROVED} if not user.has_perm("contract.view_confidential"): filter["is_public"] = True @@ -53,10 +53,10 @@ class ContractCountMixin(models.Model): class SignatureCountMixin(models.Model): def get_signature_count(self, user) -> None: - filter = {"contract__is_approved": True} + filter = {"contract__status": Contract.StatusTypes.APPROVED} if not user.has_perm("contract.view_confidential"): - filter["contract__is_approved"] = True + filter["contract__is_public"] = True return self.signatures.filter(**filter).count() @@ -490,13 +490,18 @@ class Contract(NameStrMixin, models.Model): # BEGIN Approval fields - 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." - ), + class StatusTypes(models.TextChoices): + WORK_IN_PROGRESS = "work_in_progress", "Rozpracovaná" + TO_BE_APPROVED = "to_be_approved", "Ke schválení" + APPROVED = "approved", "Schválená" + REJECTED = "rejected", "Zamítnutá" + + status = models.CharField( + choices=StatusTypes.choices, + max_length=16, + default=StatusTypes.WORK_IN_PROGRESS, + verbose_name="Status", + help_text='Označením jako "Ke schválení" se smlouva předá schvalovateli.', ) # END Approval fields @@ -675,6 +680,10 @@ class Contract(NameStrMixin, models.Model): help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat a pro tajné čtenáře.", ) + to_be_approved_nastenka_notice_id = models.UUIDField( + blank=True, null=True, verbose_name="ID oznámení o nové smlouvě v Nástěnce" + ) + @property def primary_contract_url(self) -> typing.Union[None, str]: if self.primary_contract is None: @@ -685,6 +694,13 @@ class Contract(NameStrMixin, models.Model): args=(self.primary_contract.id,), ) + @property + def is_editable_without_approve_permission(self) -> bool: + return self.status not in ( + self.StatusTypes.TO_BE_APPROVED, + self.StatusTypes.APPROVED, + ) + @property def url(self) -> str: return reverse( diff --git a/contracts/views.py b/contracts/views.py index a3f23e9f6ba62af623e9d113e7c2d5299a432f64..d6917270669ea37ef9a93c3d66e4b54f829f3d4a 100644 --- a/contracts/views.py +++ b/contracts/views.py @@ -53,7 +53,7 @@ def get_paginated_contracts(request, filter=None, annotations=None) -> tuple: if filter is None: filter = models.Q() - filter = filter & models.Q(is_approved=True) + filter = filter & models.Q(status=Contract.StatusTypes.APPROVED) if not request.user.has_perm("contracts.view_confidential"): additional_filter = models.Q(is_public=True) @@ -93,7 +93,7 @@ def index(request): def view_contract(request, id: int): - filter = models.Q(is_approved=True) + filter = models.Q(status=Contract.StatusTypes.APPROVED) if not request.user.has_perm("contracts.view_confidential"): filter = filter & ( diff --git a/registry/templates/admin/index.html b/registry/templates/admin/index.html index f96c70270891fdaecba7b90c9f114447e32223e3..7f6888e3b7504c23f352f5f88f1ffc2378ae4f42 100644 --- a/registry/templates/admin/index.html +++ b/registry/templates/admin/index.html @@ -6,7 +6,7 @@ <div class="index-action-buttons"> {% if request.user.can_approve_contracts %} <a - href="contracts/contract/?is_approved__exact=0" + href="contracts/contract/?status__exact=approved" aria-role="button" >Smlouvy ke schválení ({{ request.user.contracts_to_approve_count }})</a> {% endif %} diff --git a/users/models.py b/users/models.py index ab09f228f296456be463480262c1e102c048a9f6..23c2b6e3dd0fdadcb1571cab5c733485bfae30d1 100644 --- a/users/models.py +++ b/users/models.py @@ -63,7 +63,7 @@ class User(pirates_models.AbstractUser): from contracts.models import Contract - return Contract.objects.filter(is_approved=False).count() + return Contract.objects.filter(status=Contract.StatusTypes.APPROVED).count() # https://docs.djangoproject.com/en/4.1/ref/models/instances/#customizing-model-loading @classmethod