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

finish requirements for admin, implement private information in frontend, permissions

parent ac77e322
Branches
No related tags found
No related merge requests found
Pipeline #11915 passed
import copy
from dal_admin_filters import AutocompleteFilter
from django.contrib import admin
from django.utils.html import format_html
......@@ -111,7 +113,7 @@ class ContractAdmin(FieldsetsInlineMixin, MarkdownxGuardedModelAdmin):
"id_number",
"types",
"summary",
"public_state",
"is_public",
"publishing_rejection_comment",
"legal_state",
"primary_contract",
......@@ -176,18 +178,23 @@ class ContractAdmin(FieldsetsInlineMixin, MarkdownxGuardedModelAdmin):
]
def get_fieldsets(self, request, obj=None):
fieldsets_with_inlines = copy.deepcopy(self.fieldsets_with_inlines)
if request.user.is_superuser or request.user.has_perm("approve", self):
self.fieldsets_with_inlines.insert(
fieldsets_with_inlines.insert(
8,
(
"Schválení",
{
"fields": ["approval_state"]
"fields": ["is_approved"]
}
),
)
return super().get_fieldsets(request, obj)
return [
self.make_placeholder(index, fieldset)
for index, fieldset in enumerate(fieldsets_with_inlines)
]
def save_model(self, request, obj, form, change) -> None:
if obj.created_by is None:
......@@ -210,9 +217,9 @@ class ContractAdmin(FieldsetsInlineMixin, MarkdownxGuardedModelAdmin):
list_filter = (
"types",
"approval_state",
"is_approved",
"legal_state",
"public_state",
"is_public",
"paper_form_state",
ContractAuthorPlaceholderFilter,
"issues",
......@@ -223,8 +230,8 @@ class ContractAdmin(FieldsetsInlineMixin, MarkdownxGuardedModelAdmin):
list_display = (
"name",
"approval_state",
"public_state",
"is_approved",
"is_public",
)
# END Contracts
......
# Generated by Django 4.1.4 on 2023-03-20 10:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contracts', '0003_contractcontracteerepresentative_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='contract',
options={'permissions': (('approve', 'Schválit / zrušit schválení'), ('view_confidential', 'Zobrazit tajné informace')), 'verbose_name': 'Smlouva', 'verbose_name_plural': 'Smlouvy'},
),
migrations.AlterModelOptions(
name='contractsigneerepresentative',
options={'verbose_name': 'Zástupce jiné smluvní strany', 'verbose_name_plural': 'Zástupci jiných smluvních stran'},
),
migrations.AlterModelOptions(
name='contracttype',
options={'verbose_name': 'Typ smlouvy', 'verbose_name_plural': 'Typy smluv'},
),
migrations.AlterModelOptions(
name='signeesignature',
options={'verbose_name': 'Podpis jiné smluvní strany', 'verbose_name_plural': 'Podpisy jiných smluvních stran'},
),
migrations.RemoveField(
model_name='contract',
name='approval_state',
),
migrations.RemoveField(
model_name='contract',
name='public_state',
),
migrations.AddField(
model_name='contract',
name='is_approved',
field=models.BooleanField(default=True, help_text='Může měnit jen schvalovatel. Pokud je smlouva veřejná, schválením se vypustí ven.', verbose_name='Je schválená'),
preserve_default=False,
),
migrations.AddField(
model_name='contract',
name='is_public',
field=models.BooleanField(default=True, verbose_name='Je veřejná'),
preserve_default=False,
),
migrations.AlterField(
model_name='contractee',
name='address_country',
field=models.CharField(default='CZ', max_length=256, verbose_name='Země'),
),
migrations.AlterField(
model_name='signee',
name='address_country',
field=models.CharField(default='CZ', max_length=256, verbose_name='Země'),
),
]
import math
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from markdownx.models import MarkdownxField
from shared.models import NameStrMixin
......@@ -103,6 +103,13 @@ class Signee(models.Model):
verbose_name = "Jiná smluvní strana"
verbose_name_plural = "Ostatní smluvní strany"
@property
def entity_has_public_address(self) -> bool:
return self.entity_type in (
self.EntityTypes.LEGAL_ENTITY,
self.EntityTypes.OTHER
)
def __str__(self) -> str:
result = self.name
......@@ -263,19 +270,11 @@ class Contract(NameStrMixin, models.Model):
# BEGIN Approval fields
class ApprovalStates(models.TextChoices):
NO = "no", "Neschválená"
YES = "yes", "Schválená"
approval_state = models.CharField(
max_length=7,
choices=ApprovalStates.choices,
blank=True,
null=True,
verbose_name="Stav schválení",
is_approved = models.BooleanField(
verbose_name="Je schválená",
help_text=(
"Může měnit jen schvalovatel. Pokud je smlouva "
"veřejná, se stavem \"Schválená\" se vypustí ven."
"veřejná, schválením se vypustí ven."
),
)
......@@ -337,10 +336,8 @@ class Contract(NameStrMixin, models.Model):
verbose_name="Stav právního ujednání",
)
public_state = models.CharField(
max_length=7,
choices=PublicStates.choices,
verbose_name="Veřejnost smlouvy",
is_public = models.BooleanField(
verbose_name="Je veřejná",
)
paper_form_state = models.CharField(
......@@ -422,7 +419,7 @@ class Contract(NameStrMixin, models.Model):
blank=True,
null=True,
verbose_name="Poznámky",
help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat.",
help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat a pro tajné čtenáře.",
)
class Meta:
......@@ -431,7 +428,10 @@ class Contract(NameStrMixin, models.Model):
verbose_name = "Smlouva"
verbose_name_plural = "Smlouvy"
permissions = (("approve", "Schválit / zrušit schválení"),)
permissions = (
("approve", "Schválit / zrušit schválení"),
("view_confidential", "Zobrazit tajné informace"),
)
def get_public_files(self):
return ContractFile.objects.filter(
......@@ -439,6 +439,43 @@ class Contract(NameStrMixin, models.Model):
is_public=True,
).all()
def get_private_files(self):
return ContractFile.objects.filter(
contract=self,
is_public=False,
).all()
def calculate_signing_parties_sign_date(self) -> None:
contractees_latest_sign_date = None
signees_latest_sign_date = None
for contractee_signature in self.contractee_signatures.all():
if (
contractees_latest_sign_date is None
or contractee_signature.date > contractees_latest_sign_date
):
contractees_latest_sign_date = contractee_signature.date
for signee_signature in self.signee_signatures.all():
if (
signees_latest_sign_date is None
or signee_signature.date > signees_latest_sign_date
):
signees_latest_sign_date = signee_signature.date
if (
contractees_latest_sign_date is not None
and signees_latest_sign_date is not None
):
self.all_parties_sign_date = max(
(
contractees_latest_sign_date,
signees_latest_sign_date
)
)
self.save()
class ContractContracteeRepresentative(RepresentativeMixin, models.Model):
contract = models.ForeignKey(
......@@ -597,6 +634,17 @@ class SigneeSignature(models.Model):
return f"{str(self.signee)} - {self.date}"
@receiver(post_save, sender=ContracteeSignature)
@receiver(post_save, sender=SigneeSignature)
def signing_parties_post_save_update_dates(
sender,
instance,
*args,
**kwargs
) -> None:
instance.contract.calculate_signing_parties_sign_date()
class ContractIntent(NameStrMixin, models.Model):
name = models.CharField(
max_length=128,
......
{% extends "shared/includes/base.html" %}
{% load subtract %}
{% load subtract markdownify %}
{% block content %}
<h1 class="head-alt-lg mb-10"><i class="ico--file-blank mr-4"></i>{{ contract.name }}</h1>
......@@ -20,7 +20,7 @@
{% endif %}
<tr>
<td class="w-1/5 !p-2.5">Typy</td>
<td class="w-4/5 !p-2.5">
<td class="w-4/5 !pl-2.5 !py-1 !pr-1">
<ul class="flex flex-wrap gap-1.5">
{% for type in contract.types.all %}
<li class="flex">
......@@ -44,6 +44,17 @@
<td class="w-1/5 !p-2.5">Souhrn</td>
<td class="w-4/5 !p-2.5">{{ contract.summary }}</td>
</tr>
{% if user.can_view_confidential %}
<tr>
<td class="w-1/5 !p-2.5">Je veřejná</td>
<td class="w-4/5 !p-2.5">
<i
class="{% if contract.legal_state == contract.LegalStates.VALID %}ico--checkmark{% else %}ico--cross{% endif %} __tooltipped"
aria-label="{{ contract.get_legal_state_display }}"
></i>
</td>
</tr>
{% endif %}
<tr>
<td class="w-1/5 !p-2.5">Je platná</td>
<td class="w-4/5 !p-2.5">
......@@ -53,9 +64,22 @@
></i>
</td>
</tr>
{% if contract.cost_amount %}
<tr>
<td class="w-1/5 !p-2.5">Soubory</td>
<td class="w-1/5 !p-2.5">Náklady</td>
<td class="w-4/5 !p-2.5">
{{ contract.cost_amount }} Kč
{% if contract.cost_unit != contract.CostUnits.TOTAL %}
/ {{ contract.get_cost_unit_display }}
{% else %}
celkem
{% endif %}
</td>
</tr>
{% endif %}
<tr>
<td class="w-1/5 !p-2.5">{% if user.can_view_confidential %}Veřejné s{% else %}S{% endif %}oubory</td>
<td class="w-4/5 !pl-2.5 !py-1 !pr-1">
{% with contract.get_public_files as files %}
{% if files|length != 0 %}
<ul class="flex gap-2">
......@@ -71,29 +95,57 @@
{% endwith %}
</td>
</tr>
{% if user.can_view_confidential %}
<tr>
<td class="w-1/5 !p-2.5">
<div class="flex items-center">
<i
class="ico--eye-off text-red-600 mr-2 __tooltipped"
aria-label="Neveřejný údaj"
></i>
Neveřejné soubory
</div>
</td>
<td class="w-4/5 !pl-2.5 !py-1 !pr-1">
{% with contract.get_private_files as files %}
{% if files|length != 0 %}
<ul class="flex gap-2">
{% for file in files %}
<li class="flex">
{% include "contracts/includes/tag.html" with url=file.file.url icon="ico--attachment" content=file.name %}
</li>
{% endfor %}
</ul>
{% else %}
<span class="text-grey-200">Žádné</span>
{% endif %}
{% endwith %}
</td>
</tr>
{% endif %}
</tbody>
<table>
</table>
<div class="flex gap-7">
<table class="table table-auto w-full table--striped table--bordered mb-7">
<tbody>
<tr>
<td colspan="2" class="text-lg font-bold">
<i class="ico--clock mr-3"></i>Data
<i class="ico--folder-open mr-3"></i>Fyzický dokument
</td>
</tr>
<tr>
<td class="w-1/5 !p-2.5">Začátek účinnosti</td>
<td class="w-4/5 !p-2.5">{{ contract.valid_start_date }}</td>
<td class="w-1/5 !p-2.5">Stav</td>
<td class="w-4/5 !p-2.5">{{ contract.get_paper_form_state_display }}</td>
</tr>
<tr>
<td class="w-1/5 !p-2.5">Konec účinnosti</td>
<td class="w-4/5 !p-2.5">
{% if contract.valid_end_date %}
{{ contract.valid_end_date }}
{% else %}
<span class="text-grey-200">Neurčitý</span>
{% endif %}
<td class="w-1/5 !p-2.5">Spisovna</td>
<td class="w-4/5 !pl-2.5 !py-1 !pr-1">
<div class="flex gap-3 items-center">
{% include "contracts/includes/tag.html" with url="#TODO" icon="ico--drawer" content=contract.filing_area.name %}
</div>
</td>
</tr>
</tbody>
......@@ -103,31 +155,27 @@
<tbody>
<tr>
<td colspan="2" class="text-lg font-bold">
<i class="ico--folder-open mr-3"></i>Fyzický dokument
<i class="ico--calendar mr-3"></i>Data
</td>
</tr>
<tr>
<td class="w-1/5 !p-2.5">Stav</td>
<td class="w-4/5 !p-2.5">{{ contract.get_paper_form_state_display }}</td>
<td class="w-1/5 !p-2.5 whitespace-nowrap">Začátek účinnosti</td>
<td class="w-4/5 !p-2.5">{{ contract.valid_start_date }}</td>
</tr>
<tr>
<td class="w-1/5 !p-2.5">Spisovna</td>
<td class="w-1/5 !p-2.5 whitespace-nowrap">Konec účinnosti</td>
<td class="w-4/5 !p-2.5">
<div class="flex gap-3 items-center">
<div class="flex">
{% include "contracts/includes/tag.html" with url="#TODO" icon="ico--drawer" content=contract.filing_area.name %}
</div>
<span>|</span>
<span>
zodpovědná osoba
{{ contract.filing_area.person_responsible }}
</span>
</div>
{% if contract.valid_end_date %}
{{ contract.valid_end_date }}
{% else %}
<span class="text-grey-200">Neurčitý</span>
{% endif %}
</td>
</tr>
</tbody>
</table>
</div>
<table class="table table-auto w-full table--striped table--bordered mb-7">
<tbody>
......@@ -165,7 +213,7 @@
</tr>
<tr>
<td class="w-1/5 !p-2.5">Záměry</td>
<td class="w-4/5 !p-2.5">
<td class="w-4/5 !pl-2.5 !py-1 !pr-1">
{% with contract.intents.all as intents %}
{% if intents|length != 0 %}
<ul class="flex gap-2">
......@@ -184,29 +232,6 @@
</tbody>
</table>
{% if contract.cost_amount %}
<table class="table table-auto w-full table--striped table--bordered mb-7">
<tbody>
<tr>
<td colspan="2" class="text-lg font-bold">
<i class="ico--calculator mr-3"></i>Finance
</td>
</tr>
<tr>
<td class="w-1/5 !p-2.5">Náklady</td>
<td class="w-4/5 !p-2.5">
{{ contract.cost_amount }} Kč
{% if contract.cost_unit != contract.CostUnits.TOTAL %}
/ {{ contract.get_cost_unit_display }}
{% else %}
celkem
{% endif %}
</td>
</tr>
</tbody>
</table>
{% endif %}
{% with contract.issues.all as issues %}
{% if issues|length != 0 %}
<table class="table table-auto w-full table--striped table--bordered mb-10">
......@@ -218,17 +243,36 @@
</tr>
<tr>
<td class="w-1/5 !p-2.5">Poznámky</td>
<td class="w-4/5 !p-2.5">
<td class="w-1/5 !p-2.5">{% if not user.can_view_confidential %}Poznámky{% else %}Problémy{% endif %}</td>
<td class="w-4/5 !pl-2.5 !py-1 !pr-1">
<ul class="flex gap-2">
{% for issue in issues %}
<li class="flex">
{% include "contracts/includes/tag.html" with url="#TODO" content=issue.name %}
{% include "contracts/includes/tag.html" with url="#TODO" icon="ico--warning" content=issue.name %}
</li>
{% endfor %}
</ul>
</td>
</tr>
{% if user.can_view_confidential and contract.notes %}
<tr>
<td class="w-1/5 !p-2.5">
<div class="flex items-center">
<i
class="ico--eye-off text-red-600 mr-2 __tooltipped"
aria-label="Neveřejný údaj"
></i>
Interní poznámky
</div>
</td>
<td class="w-4/5 !p-2.5">
<p class="prose min-w-none">
{{ contract.notes|markdownify|safe }}
</p>
</td>
</tr>
{% endif %}
</tbody>
</table>
{% endif %}
......@@ -236,7 +280,15 @@
<h2 class="text-xl font-bold mb-5"><i class="ico--pencil mr-3"></i>Podpisy</h2>
<h3 class="text-lg font-bold mb-4">Naší smluvní strany</h3>
{% if contract.all_parties_sign_date %}
<p class="text-lg mb-5">
<i class="ico--calendar mr-3"></i>
Datum podpisu všech smluvních stran:
<strong>{{ contract.all_parties_sign_date }}</strong>
</p>
{% endif %}
<h3 class="text-lg font-bold mb-4">Podpisy naší smluvní strany</h3>
{% with contract.contractee_signatures.all as signatures %}
{% if signatures|length != 0 %}
......@@ -268,16 +320,21 @@
{% endif %}
</address>
{% with contract.contractee_representatives.all as representatives %}
{% if representatives %}
<div class="block mb-2">
<p class="block mb-2">
<strong>Zástupci:</strong>
</p>
<ul class="list-disc ml-6">
{% for representative in signature.contractee.representatives.all %}
{% for representative in representatives %}
<li>{{ representative.name }}, {{ representative.function }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
<p>
<strong>Datum podpisu:</strong> {{ signature.date }}
</p>
......@@ -289,7 +346,7 @@
{% endif %}
{% endwith %}
<h3 class="text-lg font-bold mb-5">Ostatních smluvních stran</h3>
<h3 class="text-lg font-bold mb-5">Podpisy otatních smluvních stran</h3>
{% with contract.signee_signatures.all as signatures %}
{% if signatures|length != 0 %}
......@@ -315,12 +372,21 @@
<div class="mb-1">
{{ signature.signee.get_entity_type_display }}
{% if signature.signee.entity_type == signature.signee.EntityTypes.NATURAL_PERSON or signature.signee.entity_type == signature.signee.EntityTypes.BUSINESS_NATURAL_PERSON %}
<span class="text-gray-300">(zobrazujeme pouze obec)</span>
{% if not signature.signee.entity_has_public_address %}
{% if user.can_view_confidential %}
<div class="my-2 flex items-center">
<i
class="ico--eye-off text-red-600 mr-2 __tooltipped"
aria-label="Neveřejný údaj"
></i><span class="text-gray-500">Máš přístup k celé adrese.</span>
</div>
{% else %}
<span class="text-gray-500">(zobrazujeme pouze obec)</span>
{% endif %}
{% endif %}
</div>
{% if signature.signee.entity_type == signature.signee.EntityTypes.LEGAL_ENTITY or signature.signee.entity_type == signature.signee.EntityTypes.OTHER %}
{% if user.can_view_confidential or signature.signee.entity_has_public_address %}
{{ signature.signee.address_street_with_number }}<br>
{{ signature.signee.address_zip }} {{ signature.signee.address_district }}<br>
{{ signature.signee.get_address_country_display }}<br>
......@@ -333,16 +399,21 @@
{% endif %}
</address>
{% with contract.signee_representatives.all as representatives %}
{% if representatives %}
<div class="block mb-2">
<p class="block mb-2">
<strong>Zástupci:</strong>
</p>
<ul class="list-disc ml-6">
{% for representative in signature.signee.representatives.all %}
{% for representative in representatives %}
<li>{{ representative.name }}, {{ representative.function }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endwith %}
<p>
<strong>Datum podpisu:</strong> {{ signature.date }}
</p>
......
from django.conf import settings
from django.core.paginator import Paginator
from django.shortcuts import render
from guardian.shortcuts import get_objects_for_user
from .models import Contract
......@@ -8,11 +9,16 @@ from .models import Contract
def index(request):
filter = {
"is_approved": True
}
if not request.user.has_perm("contracts.view_confidential"):
filter["is_public"] = True
contracts = (
Contract.objects.filter(
approval_state=Contract.ApprovalStates.YES,
public_state=Contract.PublicStates.YES,
)
get_objects_for_user(request.user, "contracts.view_contract")
.filter(**filter)
.order_by("valid_start_date")
.all()
)
......@@ -35,10 +41,18 @@ def index(request):
def view_contract(request, id: int):
contract = Contract.objects.filter(
approval_state=Contract.ApprovalStates.YES,
public_state=Contract.PublicStates.YES,
).get(id=id)
filter = {
"is_approved": True
}
if not request.user.has_perm("contracts.view_confidential"):
filter["is_public"] = True
contract = (
get_objects_for_user(request.user, "contracts.view_contract")
.filter(**filter)
.get(id=id)
)
return render(
request,
......@@ -47,7 +61,7 @@ def view_contract(request, id: int):
"site_url": settings.SITE_URL,
"user": request.user,
"title": contract.name,
"description": "", # TODO
"description": contract.summary,
"contract": contract,
},
)
import copy
from django_admin_index.models import AppGroup, AppGroupQuerySet
from django.conf import settings
......@@ -23,18 +25,28 @@ def get_app_list(self, request):
admin.AdminSite.get_app_list = get_app_list
class CustomOrderAppGroupQuerySet(AppGroupQuerySet):
original_as_list_func = copy.deepcopy(AppGroupQuerySet.as_list)
def as_list(self, request, include_remaining=True):
result = super().as_list(request, include_remaining=include_remaining)
result = original_as_list_func(
self,
request,
include_remaining=include_remaining
)
for item in result:
if app_label not in settings.ADMIN_ORDERING:
if item["app_label"] not in settings.ADMIN_ORDERING:
continue
item["models"].sort(
key=lambda model: settings.ADMIN_ORDERING[app_name].index(model["name"])
key=lambda model: (
settings.ADMIN_ORDERING[item["app_label"]]
.index(model["object_name"])
)
)
return result
AppGroup.objects = AppGroupQuerySet.as_manager()
AppGroupQuerySet.as_list = as_list
......@@ -6,7 +6,7 @@
<div class="index-action-buttons">
{% if request.user.can_approve_contracts %}
<a
href="contracts/contract/?approval_state__exact=no"
href="contracts/contract/?is_approved__exact=0"
aria-role="button"
>Smlouvy ke schválení ({{ request.user.contracts_to_approve_count }})</a>
{% endif %}
......
import html
import markdownx
from django import template
register = template.Library()
@register.filter
def markdownify(text: str) -> str:
return markdownx.utils.markdownify(html.escape(text))
......@@ -24,6 +24,10 @@ class User(pirates_models.AbstractUser):
# TODO: Do we need the superuser check?
return self.is_superuser or self.has_perm("contracts.add")
@property
def can_view_confidential(self) -> bool:
return self.is_superuser or self.has_perm("contracts.view_confidential")
@property
def contracts_to_approve_count(self) -> int:
if not self.can_approve_contracts:
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment