diff --git a/.gitignore b/.gitignore index 7f39366a4000f66c41931365232ec5270d16eec1..4e0625910ed5e6611aa4ff5d1338f8417930b7a1 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ shared/static/shared/*.css webpack-stats.json .venv media/* +git diff --git a/contracts/management/commands/import_old_contracts.py b/contracts/management/commands/import_old_contracts.py index 0c12660994ba6026c9a7b33e1507b1759d2b65b0..a38c7d557e9954986696b143b3d0e3e135155598 100644 --- a/contracts/management/commands/import_old_contracts.py +++ b/contracts/management/commands/import_old_contracts.py @@ -1,9 +1,15 @@ +import io import os +from datetime import date, datetime + +import yaml + +from ...models import Contract, ContractFilingArea, ContractType + from django.conf import settings from django.core.management.base import BaseCommand from git import Repo -from markdown import Markdown class Command(BaseCommand): @@ -25,6 +31,210 @@ class Command(BaseCommand): type=str, help="Directory to store the cloned repository in", ) + parser.add_argument( + "--existing", + action="store_true", + help="Use the existing storage directory, as long as it exists.", + ) + + def parse_index( + self, + contract_root: str, + open_file, + ) -> dict: + split_contents = open_file.read().split("---") + + if len(split_contents) < 2: + self.stderr.write( + self.style.NOTICE(f"{contract_root} index does not have valid metadata.") + ) + raise ValueError + + yaml_source = split_contents[1] + + try: + parsed_metadata = yaml.safe_load(io.StringIO(yaml_source)) + except yaml.YAMLError as exc: + self.stderr.write( + self.style.NOTICE(f"Failed to parse {contract_root} metadata.") + ) + + raise ValueError from exc + + return parsed_metadata + + def assign_contract_metadata( + self, + contract: Contract, + metadata: dict, + ) -> None: + filing_area = None + types = [] + contract_already_exists = False + + for key, value in metadata.items(): + key = key.strip() + + if isinstance(value, str): + value = value.strip() + + if key == "datum účinnosti" and isinstance(value, date): + contract.valid_start_date = value + elif key == "datum ukončení" and isinstance(value, date): + contract.valid_end_date = value + elif key == "title": + contract.name = value + + if Contract.objects.filter(name=value).exists(): + contract_already_exists = True + self.stdout.write(f"{contract.name} already exists.") + + break + elif key == "použité smluvní typy": + if not isinstance(value, list): + continue + + for type_name in value: + if not isinstance(type_name, str): + continue + + type_name = type_name.strip() + + try: + type_instance = ContractType.objects.get(name=type_name) + except ContractType.DoesNotExist: + type_instance = ContractType(name=type_name) + + types.append(type_instance) + elif key == "předmět": + contract.summary = value + elif key == "stav": + pass # TODO + elif key == "náklady": + if isinstance(value, int): + if value <= 0: + continue + + contract.cost_amount = value + contract.cost_unit = contract.CostUnits.TOTAL + elif contract.name is not None: + self.stdout.write( + self.style.WARNING(f"Could not parse cost for contract {contract.name}.") + ) + elif key == "místo uložení": + try: + filing_area = ContractFilingArea.objects.get(name=value) + except ContractFilingArea.DoesNotExist: + if isinstance(value, str): + filing_area = ContractFilingArea(name=value) + + if not contract_already_exists: + if filing_area is not None: + filing_area.save() + + for type_ in types: + type_.save() + + # Save primary key first + contract.save() + + contract.filing_area = filing_area + contract.types.set(types) + contract.save() + + def import_contract_from_files( + self, + contract_root: str, + files: list[str], + valid_start_date: datetime + ) -> None: + for file_ in files: + with open( + os.path.join( + contract_root, + file_, + ), + "r" + ) as open_file: + contract = Contract() + + if file_ == "index.html": + try: + metadata = self.parse_index(contract_root, open_file) + except ValueError: + continue + + if metadata is None: + continue + + self.assign_contract_metadata(contract, metadata) + elif file_.endswith(".pdf"): + continue + + contract.save() + + def import_all_contracts(self, git_dir) -> None: + year_root = os.path.join( + git_dir, + "smlouvy", + ) + + saved_count = 0 + + for year_directory in os.listdir(year_root): + if int(year_directory) == 0: + continue # Out of range, TODO + + month_root = os.path.join( + year_root, + year_directory, + ) + + for month_directory in os.listdir(month_root): + day_root = os.path.join( + month_root, + month_directory, + ) + + for day_directory in os.listdir(day_root): + contract_root = os.path.join( + git_dir, + "smlouvy", + year_directory, + month_directory, + day_directory, + ) + + for contract_directory in os.listdir(contract_root): + this_contract_directory = os.path.join( + contract_root, + contract_directory, + ) + + if not os.path.isdir(this_contract_directory): + self.stderr.write( + self.style.NOTICE( + f"{this_contract_directory} is not a directory, skipping." + ) + ) + continue + + valid_start_date = datetime( + year=int(year_directory), + month=int(month_directory), + day=int(day_directory) + ) + + self.import_contract_from_files( + this_contract_directory, + os.listdir(this_contract_directory), + valid_start_date, + ) + saved_count += 1 + + self.stdout.write( + self.style.SUCCESS(f"Saved {saved_count} contracts.") + ) def handle(self, *args, **options): git_dir = os.path.join( @@ -36,15 +246,31 @@ class Command(BaseCommand): ) ) - Repo.clone_from( - options["repo_url"], - git_dir, - branch=options["branch"], - ) + if os.path.exists(git_dir): + if not options["existing"]: + self.stderr.write( + self.style.ERROR( + f"Temporary git storage directory ({git_dir}) already exists. " + "As it could contain other data, it will not be removed. Please " + "remove it manually and try again." + ) + ) - md = markdown.Markdown(extensions=['meta']) - parsed_metadata = md.convert("").Meta + return + else: + self.stdout.write("Using existing git storage directory.") + else: + Repo.clone_from( + options["repo_url"], + git_dir, + branch=options["branch"], + ) + + self.stdout.write("Cloning repository.") - + self.stdout.write("\n") + self.import_all_contracts(git_dir) - self.stdout.write("\nGit repository sync complete.") + self.stdout.write( + self.style.SUCCESS("\nGit repository sync complete.") + ) diff --git a/contracts/migrations/0050_alter_contractfilingarea_name_and_more.py b/contracts/migrations/0050_alter_contractfilingarea_name_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..a83603908add22789c47efcadbfa3842869c0e40 --- /dev/null +++ b/contracts/migrations/0050_alter_contractfilingarea_name_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.1.4 on 2023-04-20 09:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0049_alter_contract_options'), + ] + + operations = [ + migrations.AlterField( + model_name='contractfilingarea', + name='name', + field=models.CharField(max_length=128, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contractissue', + name='name', + field=models.CharField(max_length=128, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contracttype', + name='name', + field=models.CharField(max_length=128, verbose_name='Jméno'), + ), + ] diff --git a/contracts/migrations/0051_alter_contract_cost_unit_other_and_more.py b/contracts/migrations/0051_alter_contract_cost_unit_other_and_more.py new file mode 100644 index 0000000000000000000000000000000000000000..a139cef7b2c7d7486ee72d2aa983a1718b6d1710 --- /dev/null +++ b/contracts/migrations/0051_alter_contract_cost_unit_other_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 4.1.4 on 2023-04-20 09:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contracts', '0050_alter_contractfilingarea_name_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='contract', + name='cost_unit_other', + field=models.CharField(blank=True, help_text='Je nutno vyplnit v případě, že máš vybranou možnost "jiné" v jednotce nákladů.', max_length=256, null=True, verbose_name='Jednotka nákladů (jiné)'), + ), + migrations.AlterField( + model_name='contract', + name='id_number', + field=models.CharField(blank=True, max_length=256, null=True, verbose_name='Identifikační číslo'), + ), + migrations.AlterField( + model_name='contract', + name='name', + field=models.CharField(max_length=256, verbose_name='Název'), + ), + migrations.AlterField( + model_name='contractapproval', + name='name', + field=models.CharField(max_length=256, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contractee', + name='department', + field=models.CharField(blank=True, max_length=256, null=True, verbose_name='Organizační složka'), + ), + migrations.AlterField( + model_name='contractfile', + name='name', + field=models.CharField(blank=True, max_length=256, null=True, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contractfilingarea', + name='name', + field=models.CharField(max_length=256, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contractintent', + name='name', + field=models.CharField(max_length=256, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contractissue', + name='name', + field=models.CharField(max_length=256, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='contracttype', + name='name', + field=models.CharField(max_length=256, verbose_name='Jméno'), + ), + migrations.AlterField( + model_name='signee', + name='department', + field=models.CharField(blank=True, max_length=256, null=True, verbose_name='Organizační složka'), + ), + ] diff --git a/contracts/models.py b/contracts/models.py index 5638b133a45aa99e80cb34507f24872ebcf59873..3ffa21a8476eb92bd363956b8f7109d8bbc5b2cf 100644 --- a/contracts/models.py +++ b/contracts/models.py @@ -188,7 +188,7 @@ class Signee( ) # WARNING: Legal entity status dependent! department = models.CharField( - max_length=128, + max_length=256, blank=True, null=True, verbose_name="Organizační složka", @@ -341,7 +341,7 @@ class Contractee( ) department = models.CharField( - max_length=128, + max_length=256, blank=True, null=True, verbose_name="Organizační složka", @@ -370,7 +370,7 @@ class Contractee( class ContractType(ContractCountMixin, NameStrMixin, models.Model): name = models.CharField( - max_length=32, + max_length=256, verbose_name="Jméno", ) @@ -387,7 +387,7 @@ class ContractType(ContractCountMixin, NameStrMixin, models.Model): class ContractIssue(ContractCountMixin, NameStrMixin, models.Model): name = models.CharField( - max_length=32, + max_length=256, verbose_name="Jméno", ) @@ -404,7 +404,7 @@ class ContractIssue(ContractCountMixin, NameStrMixin, models.Model): class ContractFilingArea(ContractCountMixin, NameStrMixin, models.Model): name = models.CharField( - max_length=32, + max_length=256, verbose_name="Jméno", ) @@ -490,12 +490,12 @@ class Contract(NameStrMixin, models.Model): # END Approval fields name = models.CharField( - max_length=128, + max_length=256, verbose_name="Název", ) id_number = models.CharField( - max_length=128, + max_length=256, blank=True, null=True, verbose_name="Identifikační číslo", @@ -622,7 +622,7 @@ class Contract(NameStrMixin, models.Model): ) cost_unit_other = models.CharField( - max_length=128, + max_length=256, verbose_name="Jednotka nákladů (jiné)", help_text='Je nutno vyplnit v případě, že máš vybranou možnost "jiné" v jednotce nákladů.', blank=True, @@ -825,7 +825,7 @@ def get_contract_file_loaction(instance, filename): class ContractFile(NameStrMixin, models.Model): name = models.CharField( - max_length=128, + max_length=256, blank=True, null=True, verbose_name="Jméno", @@ -995,7 +995,7 @@ def signing_parties_post_save_update_dates(sender, instance, *args, **kwargs) -> class ContractApproval(NameStrMixin, models.Model): name = models.CharField( - max_length=128, + max_length=256, verbose_name="Jméno", ) @@ -1024,7 +1024,7 @@ class ContractApproval(NameStrMixin, models.Model): class ContractIntent(NameStrMixin, models.Model): name = models.CharField( - max_length=128, + max_length=256, verbose_name="Jméno", ) diff --git a/requirements/base.txt b/requirements/base.txt index 39653cd5c070b3e48443e667ed43630780fbd4df..c8f33c620f3939b2097253df401d14bfc05fd049 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -18,3 +18,4 @@ django-guardian==2.4.0 GitPython==3.1.31 Markdown==3.4.3 PyJWT==2.6.0 +PyYAML==6.0