diff --git a/Dockerfile b/Dockerfile index ce121586502dc8cd65ed6c9387a44ace785af2f2..6985c0dd97f345f7008c931855866033f711fdbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,24 @@ FROM python:3.10 RUN mkdir /app -WORKDIR /app RUN curl -fsSL https://deb.nodesource.com/setup_19.x | bash - -RUN apt-get install nodejs && rm -rf /var/lib/apt/lists/* +RUN apt-get -y install autoconf automake libtool pkg-config nodejs git +RUN rm -rf /var/lib/apt/lists/* + +RUN mkdir /temp +RUN mkdir /temp/libpostal-build +WORKDIR /temp + +RUN git clone https://github.com/openvenues/libpostal +WORKDIR /temp/libpostal +RUN ./bootstrap.sh +RUN ./configure --datadir=/temp/libpostal-build +RUN make -j4 +RUN make install +RUN ldconfig + +WORKDIR /app COPY . . diff --git a/Makefile b/Makefile index 7c3c25a256b1cd92fb50cd551728a9a7e1fef5a0..3dd614c86ac23378503faeb83ea56ac0ea4bf49c 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ help: @echo "Application:" @echo " run Run the application on port ${PORT}" @echo " shell Access the Django shell" + @echo " sync Sync with the old contract registry" @echo "" @echo "Database:" @echo " migrations Generate migrations" @@ -47,6 +48,9 @@ run: venv shell: venv ${VENV}/bin/python manage.py shell --settings=${SETTINGS} +sync: + ${VENV}/bin/python manage.py import_old_contracts https://github.com/pirati-web/smlouvy.pirati.cz.git gh-pages --settings=${SETTINGS} --delete -v 3 + migrations: venv ${VENV}/bin/python manage.py makemigrations --settings=${SETTINGS} diff --git a/contracts/admin.py b/contracts/admin.py index e020611ab798557099a241de696b2aae3c94aea1..d80d314b534322dad59af72d9c5df6ea1c9dcee9 100644 --- a/contracts/admin.py +++ b/contracts/admin.py @@ -1,6 +1,7 @@ import copy import typing +from admin_auto_filters.filters import AutocompleteFilterFactory from django.contrib import admin from django.contrib.auth.models import Permission from django.db import models @@ -41,16 +42,14 @@ class IndexHiddenModelAdmin(MarkdownxGuardedModelAdmin): def permissions_mixin_factory( change_permission: str, delete_permission: str, - obj_conditional: typing.Callable, - change_attr_func: typing.Callable = lambda obj: obj, - delete_attr_func: typing.Callable = lambda obj: obj, + obj_conditional: typing.Callable = lambda request, obj: True ) -> object: class Mixin: def has_change_permission(self, request, obj=None) -> bool: if ( obj is not None and obj_conditional(request, obj) - and not request.user.has_perm(change_permission, change_attr_func(obj)) + and not request.user.has_perm(change_permission) ): return False @@ -60,7 +59,7 @@ def permissions_mixin_factory( if ( obj is not None and obj_conditional(request, obj) - and not request.user.has_perm(delete_permission, delete_attr_func(obj)) + and not request.user.has_perm(change_permission) ): return False @@ -69,14 +68,14 @@ def permissions_mixin_factory( return Mixin -get_obj_contract = lambda obj: obj.contract +get_obj_contract = lambda request, obj: obj.contract class OwnPermissionsMixin( permissions_mixin_factory( "contracts.edit_others", "contracts.delete_others", - lambda request, obj: obj.created_by != request.user, + obj_conditional=lambda request, obj: obj.created_by != request.user, ) ): def save_model(self, request, obj, form, change): @@ -89,18 +88,14 @@ class OwnPermissionsMixin( ParentContractApprovedPermissionsMixin = permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - lambda request, obj: get_obj_contract(obj).is_approved, - get_obj_contract, - get_obj_contract, + obj_conditional=lambda request, obj: get_obj_contract(obj).is_approved, ) ParentContractOwnPermissionsMixin = permissions_mixin_factory( "contracts.edit_others", "contracts.delete_others", - lambda request, obj: get_obj_contract(obj).created_by != request.user, - get_obj_contract, - get_obj_contract, + obj_conditional=lambda request, obj: get_obj_contract(obj).created_by != request.user, ) @@ -161,18 +156,13 @@ class ContractAdmin( permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - lambda request, obj: obj.is_approved, + obj_conditional=lambda request, obj: obj.is_approved, ), MarkdownxGuardedModelAdmin, NestedModelAdmin, ): form = ContractAdminForm - ordering = ( - "-created_on", - "-updated_on", - "-name", - ) search_fields = ("name",) readonly_fields = ( @@ -364,12 +354,17 @@ class ContractAdmin( return super().has_change_permission(request, obj) list_filter = ( - "types", + AutocompleteFilterFactory("Typ", "types"), + AutocompleteFilterFactory("Spisovna", "filing_area"), + AutocompleteFilterFactory("Problém", "issues"), + AutocompleteFilterFactory( + "Naše smluvná strana", "contractee_signatures__contractee" + ), + AutocompleteFilterFactory("Jiná smluvní strana", "signee_signatures__signee"), "is_approved", "is_valid", "is_public", "paper_form_state", - "issues", ("all_parties_sign_date", DateRangeFilter), ("valid_start_date", DateRangeFilter), ("valid_end_date", DateRangeFilter), @@ -385,19 +380,16 @@ class ContractAdmin( class ContractTypeAdmin(MarkdownxGuardedModelAdmin): model = ContractType - ordering = ("name",) search_fields = ("name",) class ContractIssueAdmin(MarkdownxGuardedModelAdmin): model = ContractIssue - ordering = ("name",) search_fields = ("name",) class ContractFilingAreaAdmin(MarkdownxGuardedModelAdmin): model = ContractFilingArea - ordering = ("name",) search_fields = ( "name", "person_responsible", @@ -425,7 +417,6 @@ class ContracteeAdmin(OwnPermissionsMixin, MarkdownxGuardedModelAdmin): "name", "department", ) - ordering = ("name",) class SigneeAdmin(OwnPermissionsMixin, MarkdownxGuardedModelAdmin): @@ -446,7 +437,6 @@ class SigneeAdmin(OwnPermissionsMixin, MarkdownxGuardedModelAdmin): "name", "department", ) - ordering = ("name",) form = SigneeAdminForm @@ -498,8 +488,8 @@ class SigneeAdmin(OwnPermissionsMixin, MarkdownxGuardedModelAdmin): load_ares_data_button.short_description = "ARES" -get_obj_signee_contract = lambda obj: obj.signee.contract -get_obj_contractee_contract = lambda obj: obj.contractee.contract +get_obj_signee_contract = lambda request, obj: obj.signee.contract +get_obj_contractee_contract = lambda request, obj: obj.contractee.contract class SigneeSignatureRepresentativeAdmin( @@ -507,17 +497,12 @@ class SigneeSignatureRepresentativeAdmin( permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - lambda request, obj: get_obj_signee_contract(obj).is_approved, - get_obj_signee_contract, - get_obj_signee_contract, + obj_conditional=lambda request, obj: get_obj_signee_contract(obj).is_approved, ), permissions_mixin_factory( "contracts.edit_others", "contracts.delete_others", - lambda request, obj: get_obj_contractee_contract(obj).created_by - != request.user, - get_obj_signee_contract, - get_obj_signee_contract, + obj_conditional=lambda request, obj: get_obj_contractee_contract(obj).created_by != request.user, ), ): pass @@ -528,17 +513,12 @@ class ContracteeSignatureRepresentativeAdmin( permissions_mixin_factory( "contracts.edit_when_approved", "contracts.delete_when_approved", - lambda request, obj: get_obj_contractee_contract(obj).is_approved, - get_obj_contractee_contract, - get_obj_contractee_contract, + obj_conditional=lambda request, obj: get_obj_contractee_contract(obj).is_approved, ), permissions_mixin_factory( "contracts.edit_others", "contracts.delete_others", - lambda request, obj: get_obj_contractee_contract(obj).created_by - != request.user, - get_obj_contractee_contract, - get_obj_contractee_contract, + obj_conditional=lambda request, obj: get_obj_contractee_contract(obj).created_by != request.user, ), ): pass diff --git a/contracts/management/commands/import_old_contracts.py b/contracts/management/commands/import_old_contracts.py index 53c929456a7037f0d455db7ca258f74189c5f115..36a3b9e416f290261f1736fd9e15495626ec8f46 100644 --- a/contracts/management/commands/import_old_contracts.py +++ b/contracts/management/commands/import_old_contracts.py @@ -2,15 +2,31 @@ import io import os import re import string +import shutil from datetime import date, datetime import yaml from django.conf import settings +from django.core.files import File from django.core.management.base import BaseCommand +from django.db import models +from postal.parser import parse_address from git import Repo -from ...models import Contract, ContractFilingArea, ContractType +from ...models import ( + Contract, + Contractee, + ContracteeSignature, + ContracteeSignatureRepresentative, + ContractFile, + ContractFilingArea, + ContractIssue, + ContractType, + Signee, + SigneeSignature, + SigneeSignatureRepresentative, +) class Command(BaseCommand): @@ -22,10 +38,11 @@ class Command(BaseCommand): self.normal_import_count = 0 self.partial_import_count = 0 self.already_imported_count = 0 + self.normalization_count = 0 self.issue_count = 0 self.fatal_error_count = 0 - def add_arguments(self, parser): + def add_arguments(self, parser) -> None: parser.add_argument( "repo_url", type=str, @@ -41,6 +58,11 @@ class Command(BaseCommand): type=str, help="Directory to store the cloned repository in", ) + parser.add_argument( + "--delete", + action="store_true", + help="Delete the old temporary storage directory, if it exists.", + ) parser.add_argument( "--existing", action="store_true", @@ -52,10 +74,19 @@ class Command(BaseCommand): help="Purge all previous contracts, types and filing areas before the import.", ) + def use_issue(self, contract, name: str) -> None: + try: + issue = ContractIssue.objects.get(name=name) + except ContractIssue.DoesNotExist: + issue = ContractIssue(name=name) + + return issue + def normalize_type(self, type_name: str) -> str: type_name = string.capwords(type_name) patterns = ( + (r"\s\s+", " "), (r" O ", " o "), (r" S ", " s "), (r" K ", " k "), @@ -127,10 +158,74 @@ class Command(BaseCommand): return type_name + def normalize_department(self, type_name: str) -> str: + type_name = type_name.strip() + + patterns = ( + (r"\s\s+", " "), + (r"^Kraské sdružení Praha$", "Krajské sdružení Praha"), + (r"^republikové předsednictvo$", "Republikové předsednictvo"), + (r"^KS ", "Krajské sdružení "), + (r"^(MS |místní sdružení )", "Místní sdružení "), + (r"^(Ústecký kraj|Ustecký kraj)$", "Krajské sdružení Ústecký kraj"), + ( + r"^Moravskoslezský kraj$", + "Krajské sdružení Moravskoslezský kraj", + ), + ( + r"^Karlovarský kraj$", + "Krajské sdružení Karlovarský kraj", + ), + ( + r"Jihočeská kraj", + "Jihočeský kraj", + ), + (r"^(Krajského |krajské |Kajské )", "Krajské "), + ("^Poslanecký klub České pirátské strany$", "Poslanecký klub"), + (r"Středočeký", "Středočeský"), + (r"^Zahraničního odboru$", "Zahraniční odbor"), + (r"JčK", "Jihočeský kraj"), + (r"SčK", "Středočeský kraj"), + (r"(ÚsK|UsK)", "Ústecký kraj"), + (r"JmK", "Jihomoravský kraj"), + (r"PaK", "Pardubický kraj"), + (r"KhK", "Královehradecký kraj"), + (r"(Prsonální|personální)", "Personální"), + (r"^administrativní ", "Administrativní "), + (r"technický", "Technický"), + (r"Mediálni", "Technický"), + (r"^řešitel ", "Řešitel "), + (r"^předsednictvo ", "Předsednictvo "), + (r"olomoucký", "Olomoucký"), + (r"^místní ", "Místní "), + (r"^celostátní ", "Celostátní "), + (r"odbor", "Odbor"), + (r"PKS", "Předsednictvo krajského sdružení"), + (r"( KS | Krajského sdružení )", " krajského sdružení "), + ( + r"^(Předsednictvo krajského sdružení |Předsednictvo |Místní předsednictvo )", + "", + ), + (r"^Krajské předsednictvo ", "Krajské sdružení "), + (r"ého kraje$", "ý kraj"), + (r"^Olomouc$", "Místní sdružení Olomouc"), + (r"^Olomoucký kraj$", "Krajské sdružení Olomoucký kraj"), + (r"^Pardubický kraj$", "Krajské sdružení Pardubický kraj"), + (r"^Jihočeský kraj$", "Krajské sdružení Jihočeský kraj"), + (r"^Královehradecký kraj$", "Krajské sdružení Královehradecký kraj"), + (r"^Pardubický kraj$", "Krajské sdružení Pardubický kraj"), + ) + + for pattern in patterns: + type_name = re.sub(pattern[0], pattern[1], type_name) + + return type_name + def normalize_filing_area(self, area_name: str) -> str: area_name = string.capwords(area_name) patterns = ( + (r"\s\s+", " "), ( r"^(Cenrální Spisovna|Censtrální Spisovna|Centrála|Centrálách Archiv Str" r"any|Centrála Strany|Centrální Achiv Strany|Centrální Archiv|Centralni " @@ -216,6 +311,24 @@ class Command(BaseCommand): return area_name + def normalize_filename(self, filename: str) -> str: + filename = string.capwords(filename) + + patterns = ( + (r"\s\s+", " "), + ( + r"^((P|p)odepsaná (V|v)erze|Strojově Čitelná Verze)$", + "Anonymizovaná verze", + ), + (r"^(P|p)odepsaná (V|v)erze 2$", "Anonymizovaná verze 2"), + (r"^(U|u)pravitelná (V|v)erze$", "Upravitelná verze"), + ) + + for pattern in patterns: + filename = re.sub(pattern[0], pattern[1], filename) + + return filename + def parse_index( self, contract_root: str, @@ -235,6 +348,7 @@ class Command(BaseCommand): .replace("-32", "-31") .replace("\t", " ") ) + self.normalization_count += 1 yaml_source = split_contents[1] @@ -250,14 +364,407 @@ class Command(BaseCommand): return parsed_metadata - def assign_contract_metadata( + def assign_signing_party_metadata( + self, slug: str, contract: Contract, signing_party: dict + ) -> tuple[ + Contract | Signee, + list[ContractIssue], + list[ContracteeSignatureRepresentative | SigneeSignatureRepresentative], + bool, + int, + ]: + issue_count = 0 + issues = [] + + if ( + "jméno" not in signing_party + and "název" not in signing_party + and "název společnosti" not in signing_party + ): + issue_count += 1 + contract.notes += ( + f"Nepojmenovaná smluvní strana, zdrojová data: {signing_party}\n" + ) + issues.append(self.use_issue(contract, "Špatně pojmenovaná smluvní strana")) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an unnamed signing party: {signing_party}." + ) + ) + + return issue_count + + name = signing_party.get( + "jméno", signing_party.get("název", signing_party.get("název společnosti")) + ).strip() + + patterns = ( + (r"\s\s+", " "), + (r"^(1\. Pirátská s\.r\.o|1\.Pirátská s\.r\.o\.)$", "1. Pirátská s.r.o."), + (r"České pojišťovna, a.s.", "Česká pojišťovna, a.s."), + (r"Datrolex, s.r.o.", "DATROLEX, s.r.o."), + (r"^Jiri ", "Jiří "), + ( + ( + r"^(Křesťanská a demokratická unie – Československá strana lidová|" + r"Křesťansko demokratická unie – Československá strana lidová|Křes" + r"ťanská a demokratická unie - Československá strana lidová)$" + ), + "Křesťanská a demokratická unie – Československá strana lidová", + ), + (r"LN - Audit s\.r\.o\." "LN-AUDIT s.r.o."), + (r"Olga Richteová", "Olga Richterová"), + ( + r"^(politické hnutí Změna|PolitickéHnutí Změna)$", + "Politické hnutí Změna", + ), + (r"^Systemický institut s\.r\.o\$", "Systemický institut, s.r.o."), + (r"^Václav fořtík$", "Václav Fořtík"), + (r"^Vodafone$", "Vodafone Czech Republic a.s."), + (r"^VojtěchHolík$", "Vojtěch Holík"), + (r"^Vojtech ", "Vojtěch "), + (r"^Zdenek ", "Zdeněk "), + (r" Bohmova$", " Bohmová"), + (r" (KUdláčková|Kudlláčková)$", " Kudláčková"), + (r"^Jiří knotek$", "Jiří Knotek"), + (r"^JIří Roubíček$", "Jiří Roubíček"), + (r"^Koalice Vlasta\. z\.s\.$", "Koalice Vlasta, z.s."), + (r"^Mikuáš ", "Mikuláš "), + (r"^Strana zelených$", "Strana Zelených"), + (r"^Systemický institut s\.r\.o\.$", "Systemický institut, s.r.o."), + (r"^Adéla hradilová$", "Adéla Hradilová"), + ) + + for pattern in patterns: + name = re.sub(pattern[0], pattern[1], name) + + self.normalization_count += 1 + + is_contractee = False + representatives = [] + + if name.lower() in ( + "česká pirátská strana", + "česká pirástká strana", + "česká pirátkská strana", + "česká pirátská stran", + ): + model = Contractee + representative_model = ContracteeSignatureRepresentative + instance = model() + is_contractee = True + else: + model = Signee + representative_model = SigneeSignatureRepresentative + instance = model(name=name, address_country="Česká republika") + + for signing_party_key, signing_party_value in signing_party.items(): + if isinstance(signing_party_value, str): + signing_party_value = signing_party_value.strip() + + match signing_party_key: + case ["sídlo" | "bydliště"]: + if is_contractee: + continue + + if not isinstance(signing_party_value, str): + issue_count += 1 + contract.notes += f"Špatně zadané sídlo smluvní strany: {signing_party_value}\n" + issues.append( + self.use_issue( + contract, "Špatně zadané sídlo smluvní strany" + ) + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an invalid signing party address: {signing_party_value}." + ) + ) + + continue + + raw_parsed_address_data = parse_address(signing_party_value) + address = {} + + # Convert to a dict first, so that we can access other keys in the loop later + for address_info in raw_parsed_address_data: + address_value, address_key = address_info + address[address_key] = address_value + + instance.address_street_with_number = "" + + if "road" in address: + instance.address_street_with_number = string.capwords( + address["road"] + ) + + if "house_number" in address: + instance.address_street_with_number += ( + f" {address['house_number']}" + ) + + for address_key, address_value in address.items(): + match address_key: + case "city": + if "district" not in address: + instance.address_district = string.capwords( + address_value + ) + case "house": + if "city" not in address and "district" not in address: + instance.address_district = string.capwords( + address_value + ) + case "city_district": + instance.address_district = string.capwords( + address_value + ) + case "postcode": + instance.address_zip = string.capwords(address_value) + + self.normalization_count += 1 + case "IČ": + if is_contractee: + continue + + if not isinstance(signing_party_value, int | str): + issue_count += 1 + contract.notes += ( + f"Špatně zadané IČO smluvní strany: {signing_party_value}\n" + ) + issues.append( + self.use_issue(contract, "Špatně zadané IČO smluvní strany") + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an invalid signing party IČO: {signing_party_value}." + ) + ) + + continue + + instance.ico_number = str(signing_party_value) + case "zástupce": + if not isinstance(signing_party_value, str | list): + issue_count += 1 + contract.notes += f"Špatně zadaný zástupce smluvní strany: {signing_party_value}\n" + issues.append( + self.use_issue( + contract, "Špatně zadaný zástupce smluvní strany" + ) + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an invalid signing party " + f"representative: {signing_party_value}." + ) + ) + + continue + + if isinstance(signing_party_value, str): + signing_party_value = re.sub(r",$", "", signing_party_value) + self.normalization_count += 1 + + function = None + + if "funkce" in signing_party: + if isinstance(signing_party["funkce"], str): + function = signing_party["funkce"] + else: + issue_count += 1 + contract.notes += f"Špatně zadaná funkce zástupce smluvní strany: {signing_party['funkce']}\n" + issues.append( + self.use_issue( + contract, + "Špatně zadaná funkce zástupce smluvní strany", + ) + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an invalid signing party " + f"representative function: {signing_party['funkce']}." + ) + ) + + representatives.append( + representative_model(name=signing_party_value) + ) + else: + for representative_name in signing_party_value: + if not isinstance(representative_name, str): + issue_count += 1 + contract.notes += f"Špatně zadaný jeden ze zástupců smluvní strany: {representative_name}\n" + issues.append( + self.use_issue( + contract, + "Špatně zadaný zástupce smluvní strany", + ) + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an invalid signing party " + f"representative list item: {representative_name}." + ) + ) + + continue + + representative_name = re.sub(r",$", "", representative_name) + self.normalization_count += 1 + + representatives.append( + representative_model(name=signing_party_value) + ) + case "orgán": + if not isinstance(signing_party_value, str): + issue_count += 1 + contract.notes += f"Špatně zadaný orgán smluvní strany: {signing_party_value}\n" + issues.append( + self.use_issue( + contract, "Špatně zadaný orgán smluvní strany" + ) + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Contract {slug} has an invalid signing party " + f"department: {signing_party_value}." + ) + ) + + continue + + if signing_party_value in ( + "Česká Pirátská strana", + "Česká pirátská strana", + ): + # Irrelevant + continue + + instance.department = self.normalize_department(signing_party_value) + self.normalization_count += 1 + + if model is Signee: + if ( + "s.r.o" in instance.name + or "s. r. o." in instance.name + or "a.s." in instance.name + or "a. s." in instance.name + or "o.s." in instance.name + or "o. s." in instance.name + or "z.s." in instance.name + or "z. s." in instance.name + or "1" in instance.name + or "2" in instance.name + or "3" in instance.name + or "4" in instance.name + or "5" in instance.name + or "6" in instance.name + or "7" in instance.name + or "8" in instance.name + or "9" in instance.name + or "0" in instance.name + or len(instance.name.split(" ")) not in (2, 3) + ): + instance.entity_type = instance.EntityTypes.LEGAL_ENTITY + else: + if instance.ico_number is None: + instance.entity_type = instance.EntityTypes.NATURAL_PERSON + else: + instance.entity_type = instance.EntityTypes.BUSINESS_NATURAL_PERSON + + # Do our best to merge signing parties together. + existing_instances = model.objects.filter( + ( + models.Q(name=instance.name) + & ( + models.Q(department=instance.department) + if instance.department is not None + else models.Q(department__isnull=True) + ) + & ( + ( + models.Q( + address_street_with_number=instance.address_street_with_number + ) + if instance.address_street_with_number is not None + else models.Q(address_street_with_number__isnull=True) + ) + | ( + models.Q(date_of_birth=instance.date_of_birth) + if model is Signee and instance.date_of_birth is not None + else ( + models.Q(date_of_birth__isnull=True) + if model is Signee + else models.Value(False) + ) + ) + ) + ) + | ( + ( + models.Q(ico_number=instance.ico_number) + & ( + models.Q(department=instance.department) + if instance.department is not None + else models.Q(department__isnull=True) + ) + ) + if instance.ico_number is not None + else models.Value(False) + ) + ).all() + + if len(existing_instances) != 0: + for position, existing_instance in enumerate(existing_instances): + if ( + existing_instance.ico_number is None + and instance.ico_number is not None + ): + existing_instance.ico_number = instance.ico_number + existing_instance.save() + instance = existing_instance + break + elif ( + existing_instance.ico_number == instance.ico_number + or instance.ico_number is None + ): + instance = existing_instance + break + elif position == len(existing_instances) - 1: + instance.save() + else: + instance.save() + + return instance, issues, representatives, is_contractee, issue_count + + def assign_contract_data( self, contract: Contract, metadata: dict, slug: str, + contract_root: str, ) -> None: filing_area = None types = [] + signees = [] + contractees = [] + files = [] + issues = [] is_already_imported = False observed_issues_count = 0 @@ -275,6 +782,9 @@ class Command(BaseCommand): elif value is not None: observed_issues_count += 1 contract.notes += f"Špatně zadaný začátek platnosti: {value}\n" + issues.append( + self.use_issue(contract, "Špatně zadaný začátek platnosti") + ) if self.verbosity >= 2: self.stderr.write( @@ -291,6 +801,9 @@ class Command(BaseCommand): ): observed_issues_count += 1 contract.notes += f"Špatně zadaný konec platnosti: {value}\n" + issues.append( + self.use_issue(contract, "Špatně zadaný konec platnosti") + ) if self.verbosity >= 2: self.stderr.write( @@ -313,6 +826,7 @@ class Command(BaseCommand): case "použité smluvní typy": if isinstance(value, str): value = self.normalize_type(value) + self.normalization_count += 1 try: type_instance = ContractType.objects.get(name=value) @@ -325,6 +839,7 @@ class Command(BaseCommand): elif not isinstance(value, list): observed_issues_count += 1 contract.notes += f"Špatně zadané typy: {value}\n" + issues.append(self.use_issue(contract, "Špatně zadané typy")) if self.verbosity >= 2: self.stderr.write( @@ -338,7 +853,8 @@ class Command(BaseCommand): for type_name in value: if not isinstance(type_name, str): observed_issues_count += 1 - contract.notes += f"Nezaevidovaný typ: {type_name}\n" + contract.notes += f"Špatně zadaný typ: {type_name}\n" + issues.append(self.use_issue(contract, "Špatně zadaný typ")) if self.verbosity >= 2: self.stderr.write( @@ -350,6 +866,7 @@ class Command(BaseCommand): continue type_name = self.normalize_type(type_name) + self.normalization_count += 1 try: type_instance = ContractType.objects.get(name=type_name) @@ -379,6 +896,7 @@ class Command(BaseCommand): else: observed_issues_count += 1 contract.notes += f"Neznámý stav: {value}\n" + issues.append(self.use_issue(contract, "Neznámý právní stav")) if self.verbosity >= 2: self.stderr.write( @@ -399,6 +917,9 @@ class Command(BaseCommand): contract.notes += ( f"Původní, špatně zadané náklady: {value}\n" ) + issues.append( + self.use_issue(contract, "Špatně zadané náklady") + ) if self.verbosity >= 2: self.stderr.write( @@ -426,6 +947,9 @@ class Command(BaseCommand): ): observed_issues_count += 1 contract.notes += f"Původní, neropoznané náklady: {value}\n" + issues.append( + self.use_issue(contract, "Špatně zadané náklady") + ) if self.verbosity >= 2: self.stderr.write( @@ -441,6 +965,7 @@ class Command(BaseCommand): elif value not in (None, "0"): observed_issues_count += 1 contract.notes += f"Původní, neropoznané náklady: {value}\n" + issues.append(self.use_issue(contract, "Špatně zadané náklady")) if self.verbosity >= 2: self.stderr.write( @@ -451,6 +976,7 @@ class Command(BaseCommand): case "místo uložení": if isinstance(value, str): value = self.normalize_filing_area(value) + self.normalization_count += 1 try: contract.paper_form_state = contract.PaperFormStates.STORED @@ -464,6 +990,9 @@ class Command(BaseCommand): else: observed_issues_count += 1 contract.notes += f"Špatně zadaná spisovna: {value}\n" + issues.append( + self.use_issue(contract, "Špatně zadaná spisovna") + ) if self.verbosity >= 2: self.stderr.write( @@ -471,14 +1000,164 @@ class Command(BaseCommand): f"Contract {slug} has an invalid filing area: {value}." ) ) + case "smluvní strany": + if not isinstance(value, list): + observed_issues_count += 1 + contract.notes += ( + f"Špatně zadané smluvní strany, nejsou seznam: {value}\n" + ) + issues.append( + self.use_issue(contract, "Špatně zadaný smluvní strany") + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Signing parties on {slug} are not a list: {value}." + ) + ) + + continue + + for signing_party in value: + if not isinstance(signing_party, dict): + observed_issues_count += 1 + contract.notes += ( + f"Špatně zadaná smluvní strana: {signing_party}\n" + ) + issues.append( + self.use_issue(contract, "Špatně zadaná smluvní strana") + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Signing party on {slug} is not a dictionary: {value}." + ) + ) + + continue + + ( + instance, + signing_party_issues, + representatives, + is_contractee, + signing_party_issue_count, + ) = self.assign_signing_party_metadata( + slug, contract, signing_party + ) + + issues += signing_party_issues + observed_issues_count += signing_party_issue_count + + # Store representatives in relation to the instance, hacky but good enough + instance._representatives = representatives + + if is_contractee: + contractees.append(instance) + else: + signees.append(instance) + case "soubory": + if not isinstance(value, list): + observed_issues_count += 1 + contract.notes += f"Špatně zadané soubory.\n" + issues.append(self.use_issue(contract, "Špatně zadané soubory")) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Files for {slug} are not a list: {value}." + ) + ) + + continue + + for file_data in value: + if not isinstance(file_data, dict): + observed_issues_count += 1 + contract.notes += ( + f"Špatně zadané informace o souboru: {file_data}.\n" + ) + issues.append( + self.use_issue( + contract, "Špatně zadané informace o souboru" + ) + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"File data in {slug} is not a dict: {file_data}." + ) + ) continue + for file_key, file_value in file_data.items(): + file_key = file_key.strip() + + if file_key.lower() in ("název", "náhled", "náhlad"): + continue + + if not isinstance(file_value, str): + observed_issues_count += 1 + contract.notes += f"Špatně zadaný název souboru {file_key}: {file_value}.\n" + issues.append( + self.use_issue(contract, "Neplatný název souboru") + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Filename in {slug} for file {file_key} invalid: {file_value}." + ) + ) + + continue + + file_path = os.path.join(contract_root, file_value) + + if not os.path.isfile(file_path): + observed_issues_count += 1 + contract.notes += ( + f"Neexistující soubor: {file_value}.\n" + ) + issues.append( + self.use_issue(contract, "Neexistující soubor") + ) + + if self.verbosity >= 2: + self.stderr.write( + self.style.NOTICE( + f"Filename in {slug} does not correspond to a file: {file_value}." + ) + ) + + continue + + with open(file_path, "rb") as open_file: + self.normalization_count += 1 + file = ContractFile( + contract=contract, + name=self.normalize_filename(file_key), + is_public=True, + ) + + file.file.save( + file_value, + File(open_file), + save=False, + ) + + files.append(file) + if not is_already_imported: if contract.name in (None, "") or ( isinstance(contract.name, str) and re.sub(r"/\s\s+/", "", contract.name) == "" ): + self.normalization_count += 1 contract.name = slug if contract.valid_start_date is None: @@ -499,8 +1178,49 @@ class Command(BaseCommand): # Save primary key first contract.save() + for contractee in contractees: + contractee.save() + + signature = ContracteeSignature( + contract=contract, + contractee=contractee, + date=contract.valid_start_date, + ) + + signature.save() + + contract.contractee_signatures.add(signature) + + for representative in contractee._representatives: + representative.signature = signature + representative.save() + + for signee in signees: + signee.save() + + signature = SigneeSignature( + contract=contract, + signee=signee, + date=contract.valid_start_date, + ) + + signature.save() + + contract.signee_signatures.add(signature) + + for representative in signee._representatives: + representative.signature = signature + representative.save() + + for file in files: + file.save() + + for issue in issues: + issue.save() + contract.filing_area = filing_area contract.types.set(types) + contract.issues.set(issues) contract.save() else: self.already_imported_count += 1 @@ -535,10 +1255,11 @@ class Command(BaseCommand): return - self.assign_contract_metadata( + self.assign_contract_data( contract, metadata, os.path.basename(contract_root), + contract_root, ) elif file_.endswith(".pdf"): # TODO @@ -611,7 +1332,8 @@ class Command(BaseCommand): f"Saved a total of {self.normal_import_count + self.partial_import_count} contracts.\n" f" {self.partial_import_count} contained a total of {self.issue_count} issues.\n" f" {self.already_imported_count} were already saved previously and skipped.\n" - f" {self.fatal_error_count} potential contracts were unparseable." + f" {self.fatal_error_count} potential contracts were unparseable.\n" + f" {self.normalization_count} data points were normalized." ) ) @@ -623,7 +1345,7 @@ class Command(BaseCommand): (options["directory"] if options["directory"] is not None else "git"), ) - if os.path.exists(git_dir): + if os.path.exists(git_dir) and not options["delete"]: if not options["existing"]: if self.verbosity >= 1: self.stderr.write( @@ -640,6 +1362,12 @@ class Command(BaseCommand): if self.verbosity >= 2: self.stdout.write("Using existing git storage directory.") else: + if options["delete"] and os.path.exists(git_dir): + if self.verbosity >= 2: + self.stdout.write("Deleting old git storage directory.") + + shutil.rmtree(git_dir) + if self.verbosity >= 2: self.stdout.write("Cloning repository.") @@ -653,9 +1381,17 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS("Finished cloning repository.")) if options["purge"]: - Contract.objects.filter().delete() - ContractType.objects.filter().delete() - ContractFilingArea.objects.filter().delete() + for model in ( + Contract, + ContractType, + ContractFilingArea, + ContractFile, + Contractee, + ContracteeSignature, + Signee, + SigneeSignature, + ): + model.objects.filter().delete() if self.verbosity >= 1: self.stdout.write(self.style.SUCCESS("Deleted all previous records.")) diff --git a/contracts/migrations/0053_alter_contractfile_file.py b/contracts/migrations/0053_alter_contractfile_file.py index aa0900d944457b4e00028d0ee4a751df7f62d332..b5fefd1c6fa3aba07b7cedbbd7d815fee1496ba0 100644 --- a/contracts/migrations/0053_alter_contractfile_file.py +++ b/contracts/migrations/0053_alter_contractfile_file.py @@ -1,19 +1,22 @@ # Generated by Django 4.1.4 on 2023-04-21 12:10 -import contracts.models from django.db import migrations +import contracts.models + class Migration(migrations.Migration): - dependencies = [ - ('contracts', '0052_remove_contract_legal_state_contract_is_valid'), + ("contracts", "0052_remove_contract_legal_state_contract_is_valid"), ] operations = [ migrations.AlterField( - model_name='contractfile', - name='file', - field=contracts.models.ContractFileField(upload_to=contracts.models.get_contract_file_loaction, verbose_name='Soubor'), + model_name="contractfile", + name="file", + field=contracts.models.ContractFileField( + upload_to=contracts.models.get_contract_file_loaction, + verbose_name="Soubor", + ), ), ] diff --git a/contracts/migrations/0054_alter_signee_address_zip_alter_signee_ico_number.py b/contracts/migrations/0054_alter_signee_address_zip_alter_signee_ico_number.py new file mode 100644 index 0000000000000000000000000000000000000000..52d1ce4ff5182c5a11d9e6b9df574bb24679e659 --- /dev/null +++ b/contracts/migrations/0054_alter_signee_address_zip_alter_signee_ico_number.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1.4 on 2023-04-21 22:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("contracts", "0053_alter_contractfile_file"), + ] + + operations = [ + migrations.AlterField( + model_name="signee", + name="address_zip", + field=models.CharField( + blank=True, + help_text="Veřejné pouze, když typ není nastaven na fyzickou osobu.", + max_length=256, + null=True, + verbose_name="PSČ", + ), + ), + migrations.AlterField( + model_name="signee", + name="ico_number", + field=models.CharField( + blank=True, + help_text="U právnických a podnikajících fyzických osob musí být vyplněno. Vyplněním můžeš automaticky načíst data z ARES.", + max_length=256, + null=True, + verbose_name="IČO", + ), + ), + ] diff --git a/contracts/migrations/0055_alter_contractee_address_zip_and_more.py b/contracts/migrations/0055_alter_contractee_address_zip_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..1bbcaaead5596e9a3ed11cb27999276ad4daa13c --- /dev/null +++ b/contracts/migrations/0055_alter_contractee_address_zip_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1.4 on 2023-04-21 22:52 + +from django.db import migrations, models + +import contracts.models + + +class Migration(migrations.Migration): + dependencies = [ + ("contracts", "0054_alter_signee_address_zip_alter_signee_ico_number"), + ] + + operations = [ + migrations.AlterField( + model_name="contractee", + name="address_zip", + field=models.CharField( + default=contracts.models.get_default_contractee_zip, + max_length=256, + verbose_name="PSČ", + ), + ), + migrations.AlterField( + model_name="contractee", + name="ico_number", + field=models.CharField( + blank=True, + default=contracts.models.get_default_contractee_ico_number, + max_length=256, + null=True, + verbose_name="IČO", + ), + ), + ] diff --git a/contracts/migrations/0056_rename_contractee_signature_contracteesignaturerepresentative_signature_and_more.py b/contracts/migrations/0056_rename_contractee_signature_contracteesignaturerepresentative_signature_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..c80d53df487dc8a2b924de82067876fcac22c760 --- /dev/null +++ b/contracts/migrations/0056_rename_contractee_signature_contracteesignaturerepresentative_signature_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.4 on 2023-04-22 22:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("contracts", "0055_alter_contractee_address_zip_and_more"), + ] + + operations = [ + migrations.RenameField( + model_name="contracteesignaturerepresentative", + old_name="contractee_signature", + new_name="signature", + ), + migrations.RenameField( + model_name="signeesignaturerepresentative", + old_name="signee_signature", + new_name="signature", + ), + ] diff --git a/contracts/migrations/0057_alter_contract_options_alter_contractee_options_and_more.py b/contracts/migrations/0057_alter_contract_options_alter_contractee_options_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..37e8323528e972094deedab264181334dc7e38b4 --- /dev/null +++ b/contracts/migrations/0057_alter_contract_options_alter_contractee_options_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 4.1.4 on 2023-04-23 10:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ( + "contracts", + "0056_rename_contractee_signature_contracteesignaturerepresentative_signature_and_more", + ), + ] + + operations = [ + migrations.AlterModelOptions( + name="contract", + options={ + "ordering": ("-created_on", "-updated_on", "-name"), + "permissions": [ + ("approve", "Schválit / zrušit schválení"), + ("view_confidential", "Zobrazit tajné informace"), + ("edit_when_approved", "Upravit schválené"), + ("delete_when_approved", "Odstranit schválené"), + ("edit_others", "Upravit cizí"), + ("delete_others", "Odstranit cizí"), + ("can_edit_contract_settings", "Can edit Smlouva settings"), + ], + "verbose_name": "Smlouva", + "verbose_name_plural": "Smlouvy", + }, + ), + migrations.AlterModelOptions( + name="contractee", + options={ + "ordering": ["-name", "-department"], + "permissions": [ + ("edit_others", "Upravit cizí"), + ("delete_others", "Odstranit cizí"), + ], + "verbose_name": "Naše smluvní strana", + "verbose_name_plural": "Naše smluvní strany", + }, + ), + migrations.AlterModelOptions( + name="contractfilingarea", + options={ + "ordering": ["-name"], + "verbose_name": "Spisovna", + "verbose_name_plural": "Spisovny", + }, + ), + migrations.AlterModelOptions( + name="contractissue", + options={ + "ordering": ["-name"], + "verbose_name": "Problém se smlouvou", + "verbose_name_plural": "Problémy se smlouvami", + }, + ), + migrations.AlterModelOptions( + name="contracttype", + options={ + "ordering": ["-name"], + "verbose_name": "Typ smlouvy", + "verbose_name_plural": "Typy smluv", + }, + ), + migrations.AlterModelOptions( + name="signee", + options={ + "ordering": ["-name", "-department"], + "permissions": [ + ("edit_others", "Upravit cizí"), + ("delete_others", "Odstranit cizí"), + ], + "verbose_name": "Jiná smluvní strana", + "verbose_name_plural": "Ostatní smluvní strany", + }, + ), + ] diff --git a/contracts/migrations/0058_alter_contract_options_alter_contractee_options_and_more.py b/contracts/migrations/0058_alter_contract_options_alter_contractee_options_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..52f5e1f0dbb24bfcfb4f5b3b5d02387a6be89e56 --- /dev/null +++ b/contracts/migrations/0058_alter_contract_options_alter_contractee_options_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 4.1.4 on 2023-04-23 10:29 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("contracts", "0057_alter_contract_options_alter_contractee_options_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="contract", + options={ + "ordering": ("-created_on", "-updated_on", "name"), + "permissions": [ + ("approve", "Schválit / zrušit schválení"), + ("view_confidential", "Zobrazit tajné informace"), + ("edit_when_approved", "Upravit schválené"), + ("delete_when_approved", "Odstranit schválené"), + ("edit_others", "Upravit cizí"), + ("delete_others", "Odstranit cizí"), + ("can_edit_contract_settings", "Can edit Smlouva settings"), + ], + "verbose_name": "Smlouva", + "verbose_name_plural": "Smlouvy", + }, + ), + migrations.AlterModelOptions( + name="contractee", + options={ + "ordering": ["name", "department"], + "permissions": [ + ("edit_others", "Upravit cizí"), + ("delete_others", "Odstranit cizí"), + ], + "verbose_name": "Naše smluvní strana", + "verbose_name_plural": "Naše smluvní strany", + }, + ), + migrations.AlterModelOptions( + name="contractfilingarea", + options={ + "ordering": ["name"], + "verbose_name": "Spisovna", + "verbose_name_plural": "Spisovny", + }, + ), + migrations.AlterModelOptions( + name="contractissue", + options={ + "ordering": ["name"], + "verbose_name": "Problém se smlouvou", + "verbose_name_plural": "Problémy se smlouvami", + }, + ), + migrations.AlterModelOptions( + name="contracttype", + options={ + "ordering": ["name"], + "verbose_name": "Typ smlouvy", + "verbose_name_plural": "Typy smluv", + }, + ), + migrations.AlterModelOptions( + name="signee", + options={ + "ordering": ["name", "department"], + "permissions": [ + ("edit_others", "Upravit cizí"), + ("delete_others", "Odstranit cizí"), + ], + "verbose_name": "Jiná smluvní strana", + "verbose_name_plural": "Ostatní smluvní strany", + }, + ), + ] diff --git a/contracts/migrations/0059_alter_contract_is_valid.py b/contracts/migrations/0059_alter_contract_is_valid.py new file mode 100644 index 0000000000000000000000000000000000000000..df29dad1e83e8eb129a54642ba1ab5af5a3fbd6b --- /dev/null +++ b/contracts/migrations/0059_alter_contract_is_valid.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2023-05-01 20:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0058_alter_contract_options_alter_contractee_options_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='contract', + name='is_valid', + field=models.BooleanField(default=False, help_text='Právní vztah vyplývající ze smlouvy je aktuálně účinný a platný', verbose_name='Je právně platná'), + ), + ] diff --git a/contracts/models.py b/contracts/models.py index 72d01d2a5649dcb0ff475234d4d607ae49870e3e..5e9719e79202e770c039ae56195f6491da9d5ba6 100644 --- a/contracts/models.py +++ b/contracts/models.py @@ -8,8 +8,8 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.core.validators import RegexValidator, URLValidator from django.db import models -from django.db.models.signals import post_save from django.db.models.fields.files import FieldFile +from django.db.models.signals import post_save from django.dispatch import receiver from django.urls import reverse from django.utils.safestring import mark_safe @@ -156,7 +156,7 @@ class Signee( ) address_zip = models.CharField( - max_length=16, + max_length=256, blank=True, null=True, verbose_name="PSČ", @@ -172,7 +172,7 @@ class Signee( ) ico_number = models.CharField( - max_length=16, + max_length=256, blank=True, null=True, verbose_name="IČO", @@ -274,6 +274,8 @@ class Signee( verbose_name = "Jiná smluvní strana" verbose_name_plural = "Ostatní smluvní strany" + ordering = ["name", "department"] + permissions = OwnPermissionsMixin.Meta.permissions @@ -323,7 +325,7 @@ class Contractee( ) address_zip = models.CharField( - max_length=16, + max_length=256, default=get_default_contractee_zip, verbose_name="PSČ", ) @@ -335,7 +337,7 @@ class Contractee( ) ico_number = models.CharField( - max_length=16, + max_length=256, blank=True, null=True, default=get_default_contractee_ico_number, @@ -367,6 +369,8 @@ class Contractee( verbose_name = "Naše smluvní strana" verbose_name_plural = "Naše smluvní strany" + ordering = ["name", "department"] + permissions = OwnPermissionsMixin.Meta.permissions @@ -383,6 +387,8 @@ class ContractType(ContractCountMixin, NameStrMixin, models.Model): class Meta: app_label = "contracts" + ordering = ["name"] + verbose_name = "Typ smlouvy" verbose_name_plural = "Typy smluv" @@ -400,6 +406,8 @@ class ContractIssue(ContractCountMixin, NameStrMixin, models.Model): class Meta: app_label = "contracts" + ordering = ["name"] + verbose_name = "Problém se smlouvou" verbose_name_plural = "Problémy se smlouvami" @@ -422,6 +430,8 @@ class ContractFilingArea(ContractCountMixin, NameStrMixin, models.Model): class Meta: app_label = "contracts" + ordering = ["name"] + verbose_name = "Spisovna" verbose_name_plural = "Spisovny" @@ -554,6 +564,7 @@ class Contract(NameStrMixin, models.Model): is_valid = models.BooleanField( default=False, verbose_name="Je právně platná", + help_text="Právní vztah vyplývající ze smlouvy je aktuálně účinný a platný", ) is_public = models.BooleanField( @@ -789,6 +800,12 @@ class Contract(NameStrMixin, models.Model): verbose_name = "Smlouva" verbose_name_plural = "Smlouvy" + ordering = ( + "-created_on", + "-updated_on", + "name", + ) + permissions = [ ("approve", "Schválit / zrušit schválení"), ("view_confidential", "Zobrazit tajné informace"), @@ -827,7 +844,9 @@ def get_contract_file_loaction(instance, filename): class ContractFileProxy(FieldFile): @property def url(self) -> str: - return reverse("contracts:download_contract_file", args=(str(self.instance.id),)) + return reverse( + "contracts:download_contract_file", args=(str(self.instance.id),) + ) class ContractFileField(models.FileField): @@ -947,7 +966,7 @@ class SigneeSignature(models.Model): class ContracteeSignatureRepresentative(RepresentativeMixin, models.Model): - contractee_signature = models.ForeignKey( + signature = models.ForeignKey( ContracteeSignature, on_delete=models.CASCADE, related_name="representatives", @@ -972,7 +991,7 @@ class ContracteeSignatureRepresentative(RepresentativeMixin, models.Model): class SigneeSignatureRepresentative(RepresentativeMixin, models.Model): - signee_signature = models.ForeignKey( + signature = models.ForeignKey( SigneeSignature, on_delete=models.CASCADE, related_name="representatives", diff --git a/package-lock.json b/package-lock.json index 555756a82080856216af658fc1ac4af15cb36bba..b57b2e441884312be7b106c23e62d1eb89ff4d60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -515,9 +515,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001468", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001468.tgz", - "integrity": "sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==", + "version": "1.0.30001481", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", + "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", "funding": [ { "type": "opencollective", @@ -526,6 +526,10 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ] }, @@ -2568,9 +2572,9 @@ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" }, "caniuse-lite": { - "version": "1.0.30001468", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001468.tgz", - "integrity": "sha512-zgAo8D5kbOyUcRAgSmgyuvBkjrGk5CGYG5TYgFdpQv+ywcyEpo1LOWoG8YmoflGnh+V+UsNuKYedsoYs0hzV5A==" + "version": "1.0.30001481", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", + "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==" }, "chokidar": { "version": "3.5.3", diff --git a/registry/settings/base.py b/registry/settings/base.py index 15f0ca7001cdb133c88efd3666e6fdba61f893ea..c2f29bd217dd4272ac6a7fdeb5727ef3a17861a3 100644 --- a/registry/settings/base.py +++ b/registry/settings/base.py @@ -53,6 +53,7 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "admin_auto_filters", "dbsettings", "nested_admin", "rangefilter", diff --git a/requirements/base.txt b/requirements/base.txt index c8f33c620f3939b2097253df401d14bfc05fd049..e4fe2396bd4a6f5a08f1182a3859258f20f97548 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,5 +1,6 @@ clamd==1.0.2 django==4.1.4 +django-admin-autocomplete-filter==0.7.1 django-admin-index==2.0.2 django-admin-interface==0.24.2 django-admin-rangefilter==0.9.0 @@ -17,5 +18,6 @@ django-http-exceptions==1.4.0 django-guardian==2.4.0 GitPython==3.1.31 Markdown==3.4.3 +postal==1.1.10 PyJWT==2.6.0 PyYAML==6.0 diff --git a/shared/templates/shared/includes/base.html b/shared/templates/shared/includes/base.html index 2755e32994e14b2e262d52630180fec8ad7c6d8e..5520d1980abf67c68c5d7bffbbba687e26ca588a 100644 --- a/shared/templates/shared/includes/base.html +++ b/shared/templates/shared/includes/base.html @@ -60,7 +60,7 @@ <div class="container container--default navbar__content" :class="{'navbar__content--initialized': true}"> <div class="navbar__brand my-4 flex items-center lg:pr-8 lg:my-0"> <a href="/"> - <img src="https://styleguide.pirati.cz/2.12.x/images/logo-round-white.svg" class="w-8" /> + <img src="https://styleguide.pirati.cz/2.12.x/images/logo-round-white.svg" class="w-8"> </a> <a href="/" class="pl-4 font-bold text-xl hover:no-underline lg:border-r lg:border-grey-300 lg:pr-8">Registr smluv</a> </div> @@ -190,8 +190,8 @@ </a> <a href="https://nalodeni.pirati.cz" class="btn btn--icon btn--blue-300 btn--hoveractive text-lg btn--fullwidth sm:btn--autowidth"> <div class="btn__body-wrap"> - <div class="btn__body ">Naloď se</div> - <div class="btn__icon "><i class="ico--anchor"></i></div> + <div class="btn__body">Naloď se</div> + <div class="btn__icon"><i class="ico--anchor"></i></div> </div> </a> </div> diff --git a/static_src/admin/contract_file_form.js b/static_src/admin/contract_file_form.js index 38f84779a4fa84d832f7e7d297507c70e1c48f06..b5210e1ca542a749ead5f32da4f938ceac647b3b 100644 --- a/static_src/admin/contract_file_form.js +++ b/static_src/admin/contract_file_form.js @@ -6,6 +6,7 @@ $(window).ready( `<datalist id="file-types"> <option value="Původní verze"> <option value="Anonymizovaná verze"> + <option value="Upravitelná verze"> </datalist>` ); } diff --git a/users/models.py b/users/models.py index e9332ea84b61f7e978f30b131fd6ecffbcc813c6..64dee9b879678b89f0304418b428213490a116b4 100644 --- a/users/models.py +++ b/users/models.py @@ -33,7 +33,7 @@ class User(pirates_models.AbstractUser): @property def can_create_contracts(self) -> bool: - return self.has_perm("contracts.add") + return self.has_perm("contracts.add_contract") @property def can_view_confidential(self) -> bool: