Skip to content
Snippets Groups Projects
Commit afcb4aab authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

multiple approval states, send out notifications and delete as per request

parent 51ee6c5e
Branches
No related tags found
No related merge requests found
Pipeline #13516 passed
......@@ -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",
......
......@@ -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."
)
}
)
......
# 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",
),
]
# 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",
),
),
]
......@@ -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(
......
......@@ -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 & (
......
......@@ -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 %}
......
......@@ -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
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment