diff --git a/.gitignore b/.gitignore index 0a533b6336363200760160adadd001271a20108a..2f272e7c1b472b8ba25fa864a333bb76a4cd1161 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ staticfiles node_modules/* shared/static/* webpack-stats.json +package-lock.json .venv diff --git a/README.md b/README.md index ed15f886bea2f0105b801a0f1bfd4523427ffd05..a2042d02b073bdedf3dbb92cb17e14af6911e389 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,12 @@ Je třeba definovat minimálně následující environment proměnné: | --- | --- | | `DATABASE_URL` | URL pro připojení k databázi ve formátu `postgresql://username:password@host:5432/database_name` | | `SECRET_KEY` | Tajný klíč např. pro šifrování | -| `DEFAULT_LOCAL_SIGNER_NAME` | Defaultní jméno naší podepisující strany | -| `DEFAULT_LCOAL_SIGNER_STREET` | Defaultní ulice a č.p. naší podepisující strany | -| `DEFAULT_LOCAL_SIGNER_ZIP` | Defaultní PSČ naší podepisující strany | -| `DEFAULT_LOCAL_SIGNER_DISTRICT` | Defaultní obec naší podepisující strany | -| `DEFAULT_LOCAL_SIGNER_COUNTRY` | Defaultní země naší podepisující strany, např. `CZ`, `DE` | -| `DEFAULT_LOCAL_SIGNER_ICO_NUMBER` | Defaultní IČO naší podepisující strany | +| `DEFAULT_CONTRACTEE_NAME` | Defaultní jméno naší podepisující strany | +| `DEFAULT_CONTRACTEE_STREET` | Defaultní ulice a č.p. naší podepisující strany | +| `DEFAULT_CONTRACTEE_ZIP` | Defaultní PSČ naší podepisující strany | +| `DEFAULT_CONTRACTEE_DISTRICT` | Defaultní obec naší podepisující strany | +| `DEFAULT_CONTRACTEE_COUNTRY` | Defaultní země naší podepisující strany, např. `CZ`, `DE` | +| `DEFAULT_CONTRACTEE_ICO_NUMBER` | Defaultní IČO naší podepisující strany | V produkci je potřeba: | proměnná | popis | diff --git a/contracts/admin.py b/contracts/admin.py index 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..a9093aa47b4f0520b2789d9801801bbc84e76e53 100644 --- a/contracts/admin.py +++ b/contracts/admin.py @@ -1,3 +1,42 @@ from django.contrib import admin -# Register your models here. +from shared.admin import MarkdownxGuardedModelAdmin + +from .models import ( + Contract, + Contractee, + ContracteeRepresentative, + ContracteeSignature, + ContractFilingArea, + ContractIntent, + ContractIssue, + ContractSubtype, + Signee, + SigneeRepresentative, + SigneeSignature, +) + + +class IndexHiddenModelAdmin(MarkdownxGuardedModelAdmin): + def has_module_permission(self, request): + return False + + +for model in ( + SigneeRepresentative, + SigneeSignature, + ContracteeRepresentative, + ContracteeSignature, + ContractSubtype, + ContractIntent, +): + admin.site.register(model, IndexHiddenModelAdmin) + +for model in ( + Signee, + Contractee, + ContractIssue, + ContractFilingArea, + Contract, +): + admin.site.register(model, MarkdownxGuardedModelAdmin) diff --git a/contracts/apps.py b/contracts/apps.py index d3cd662eed4aff0423a4500a18c15f1479ac238a..d1698620e6fb592dd3b238ad4c6bf9d537585580 100644 --- a/contracts/apps.py +++ b/contracts/apps.py @@ -4,3 +4,4 @@ from django.apps import AppConfig class ContractsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "contracts" + verbose_name = "Smlouvy" diff --git a/contracts/migrations/0001_initial.py b/contracts/migrations/0001_initial.py index c049786ba448c708abb09138dd3a185d6dc0399b..7b1063e2c2157e2985b77d84616d85eebf1f2362 100644 --- a/contracts/migrations/0001_initial.py +++ b/contracts/migrations/0001_initial.py @@ -1,14 +1,18 @@ -# Generated by Django 4.1.4 on 2023-02-03 15:25 +# Generated by Django 4.1.4 on 2023-02-15 03:36 +import django.db.models.deletion import django_countries.fields import markdownx.models +from django.conf import settings from django.db import migrations, models class Migration(migrations.Migration): initial = True - dependencies = [] + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] operations = [ migrations.CreateModel( @@ -112,6 +116,15 @@ class Migration(migrations.Migration): max_length=128, verbose_name="Identifikační číslo" ), ), + ( + "notes", + markdownx.models.MarkdownxField( + blank=True, + help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat.", + null=True, + verbose_name="Obsah", + ), + ), ( "summary", models.CharField( @@ -152,15 +165,6 @@ class Migration(migrations.Migration): "expected_cost_hour", models.IntegerField(verbose_name="Očekáváná cena za hodinu"), ), - ( - "intent_url", - models.URLField( - blank=True, - max_length=256, - null=True, - verbose_name="Odkaz na záměr", - ), - ), ( "agreement_url", models.URLField( @@ -177,7 +181,7 @@ class Migration(migrations.Migration): }, ), migrations.CreateModel( - name="ContractExternalSignature", + name="Contractee", fields=[ ( "id", @@ -188,80 +192,48 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("date", models.DateField(verbose_name="Datum podpisu")), - ], - ), - migrations.CreateModel( - name="ContractExternalSigner", - fields=[ ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256, verbose_name="Jméno")), - ( - "is_legal_entity", - models.BooleanField( - help_text="Důležité označit správně! Pokud není osoba právnická, zveřejňujeme pouze obec a zemi.", - verbose_name="Je právnická osoba", + "name", + models.CharField( + default="Česká pirátská strana", + max_length=256, + verbose_name="Jméno", ), ), ( "address_street_with_number", models.CharField( - help_text="Viditelné pouze u právnických osob.", + default="Na Moráni 360/3", max_length=256, verbose_name="Ulice, č.p.", ), ), ( "address_district", - models.CharField(max_length=256, verbose_name="Obec"), + models.CharField( + default="Praha 2", max_length=256, verbose_name="Obec" + ), ), ( "address_zip", models.CharField( - help_text="Viditelné pouze u právnických osob.", - max_length=16, - verbose_name="PSČ", + default="128 00", max_length=16, verbose_name="PSČ" ), ), ( "address_country", django_countries.fields.CountryField( - max_length=2, verbose_name="Země" + default="CZ", max_length=2, verbose_name="Země" ), ), ( "ico_number", - models.CharField( - blank=True, max_length=16, null=True, verbose_name="IČO" - ), - ), - ( - "date_of_birth", - models.DateField( - blank=True, null=True, verbose_name="Datum narození" - ), - ), - ( - "representative_name", - models.CharField( - blank=True, max_length=256, null=True, verbose_name="Zástupce" - ), - ), - ( - "representative_role", models.CharField( blank=True, - max_length=256, + default="71339698", + max_length=16, null=True, - verbose_name="Funkce zástupce", + verbose_name="IČO", ), ), ( @@ -273,10 +245,11 @@ class Migration(migrations.Migration): verbose_name="Organizační složka", ), ), + ("color", models.CharField(max_length=6, verbose_name="Barva")), ], options={ - "verbose_name": "Druhá smluvní strana", - "verbose_name_plural": "Druhé smluvní strany", + "verbose_name": "Naše smluvní strana", + "verbose_name_plural": "Naše smluvní strany", }, ), migrations.CreateModel( @@ -318,11 +291,11 @@ class Migration(migrations.Migration): ], options={ "verbose_name": "Problém se smlouvou", - "verbose_name_plural": "Problémy se smlouvou", + "verbose_name_plural": "Problémy se smlouvami", }, ), migrations.CreateModel( - name="ContractLocalSignature", + name="ContractSubtype", fields=[ ( "id", @@ -333,11 +306,15 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("date", models.DateField(verbose_name="Datum podpisu")), + ("name", models.CharField(max_length=32, verbose_name="Jméno")), ], + options={ + "verbose_name": "Podtyp smlouvy", + "verbose_name_plural": "Podtypy smlouvy", + }, ), migrations.CreateModel( - name="ContractLocalSigner", + name="Signee", fields=[ ( "id", @@ -348,63 +325,50 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), + ("name", models.CharField(max_length=256, verbose_name="Jméno")), ( - "name", - models.CharField( - default="Česká pirátská strana", - max_length=256, - verbose_name="Jméno", + "is_legal_entity", + models.BooleanField( + help_text="Důležité označit správně! Pokud není osoba právnická, zveřejňujeme pouze obec a zemi.", + verbose_name="Je právnická osoba", ), ), ( "address_street_with_number", models.CharField( - default="Na Moráni 360/3", + help_text="Viditelné pouze u právnických osob.", max_length=256, verbose_name="Ulice, č.p.", ), ), ( "address_district", - models.CharField( - default="Praha 2", max_length=256, verbose_name="Obec" - ), + models.CharField(max_length=256, verbose_name="Obec"), ), ( "address_zip", models.CharField( - default="128 00", max_length=16, verbose_name="PSČ" + help_text="Viditelné pouze u právnických osob.", + max_length=16, + verbose_name="PSČ", ), ), ( "address_country", django_countries.fields.CountryField( - default="CZ", max_length=2, verbose_name="Země" + max_length=2, verbose_name="Země" ), ), ( "ico_number", models.CharField( - blank=True, - default="71339698", - max_length=16, - null=True, - verbose_name="IČO", - ), - ), - ( - "representative_name", - models.CharField( - blank=True, max_length=256, null=True, verbose_name="Zástupce" + blank=True, max_length=16, null=True, verbose_name="IČO" ), ), ( - "representative_role", - models.CharField( - blank=True, - max_length=256, - null=True, - verbose_name="Funkce zástupce", + "date_of_birth", + models.DateField( + blank=True, null=True, verbose_name="Datum narození" ), ), ( @@ -416,15 +380,14 @@ class Migration(migrations.Migration): verbose_name="Organizační složka", ), ), - ("color", models.CharField(max_length=6, verbose_name="Barva")), ], options={ - "verbose_name": "Naše smluvní strana", - "verbose_name_plural": "Naše smlouvní strany", + "verbose_name": "Druhá smluvní strana", + "verbose_name_plural": "Druhé smluvní strany", }, ), migrations.CreateModel( - name="ContractNote", + name="SigneeSignature", fields=[ ( "id", @@ -435,16 +398,23 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("created_date", models.DateTimeField(verbose_name="Datum vytvoření")), - ("content", markdownx.models.MarkdownxField(verbose_name="Obsah")), + ("date", models.DateField(verbose_name="Datum podpisu")), + ( + "signee", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="signatures", + to="contracts.signee", + ), + ), ], options={ - "verbose_name": "Poznámka ke smlouvě", - "verbose_name_plural": "Poznámky ke smlouvě", + "verbose_name": "Podpis druhé smluvní strany", + "verbose_name_plural": "Podpisy druhé smluvní strany", }, ), migrations.CreateModel( - name="ContractSubtype", + name="SigneeRepresentative", fields=[ ( "id", @@ -455,11 +425,182 @@ class Migration(migrations.Migration): verbose_name="ID", ), ), - ("name", models.CharField(max_length=32, verbose_name="Jméno")), + ("name", models.CharField(max_length=256, verbose_name="Jméno")), + ("role", models.CharField(max_length=256, verbose_name="Funkce")), + ( + "signee", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="representatives", + to="contracts.signee", + ), + ), ], options={ - "verbose_name": "Podtyp smlouvy", - "verbose_name_plural": "Podtypy smlouvy", + "verbose_name": "Zástupce druhé smluvní strany", + "verbose_name_plural": "Zástupci druhé smluvní strany", }, ), + migrations.CreateModel( + name="ContractIntent", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "url", + models.URLField( + blank=True, max_length=256, null=True, verbose_name="Odkaz" + ), + ), + ( + "contract", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="intents", + to="contracts.contract", + ), + ), + ], + options={ + "verbose_name": "Záměr", + "verbose_name_plural": "Záměry", + }, + ), + migrations.CreateModel( + name="ContracteeSignature", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Datum podpisu")), + ( + "contractee", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="signatures", + to="contracts.contractee", + ), + ), + ], + options={ + "verbose_name": "Podpis naší smluvní strany", + "verbose_name_plural": "Podpisy naší smluvní strany", + }, + ), + migrations.CreateModel( + name="ContracteeRepresentative", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=256, verbose_name="Jméno")), + ("role", models.CharField(max_length=256, verbose_name="Funkce")), + ( + "contractee", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="representatives", + to="contracts.contractee", + ), + ), + ], + options={ + "verbose_name": "Zástupce naší smluvní strany", + "verbose_name_plural": "Zástupci naší smluvní strany", + }, + ), + migrations.AddField( + model_name="contract", + name="contractee_signatures", + field=models.ManyToManyField( + to="contracts.contracteesignature", verbose_name="Naše podpisy" + ), + ), + migrations.AddField( + model_name="contract", + name="filing_area", + field=models.ForeignKey( + blank=True, + help_text="Obsah není veřejně přístupný.", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="filed_contracts", + to="contracts.contractfilingarea", + ), + ), + migrations.AddField( + model_name="contract", + name="issues", + field=models.ManyToManyField( + to="contracts.contractissue", verbose_name="Problémy" + ), + ), + migrations.AddField( + model_name="contract", + name="primary_contract", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="subcontracts", + to="contracts.contract", + verbose_name="Hlavní smlouva", + ), + ), + migrations.AddField( + model_name="contract", + name="public_status_set_by", + field=models.ForeignKey( + help_text="Obsah není veřejně přístupný.", + on_delete=django.db.models.deletion.CASCADE, + related_name="public_status_altered_contracts", + to=settings.AUTH_USER_MODEL, + verbose_name="Zveřejněno / nezveřejněno uživatelem", + ), + ), + migrations.AddField( + model_name="contract", + name="signee_signature", + field=models.ManyToManyField( + to="contracts.signeesignature", + verbose_name="Podpisy druhé smluvní strany", + ), + ), + migrations.AddField( + model_name="contract", + name="subtype", + field=models.ManyToManyField( + to="contracts.contractsubtype", verbose_name="Podtypy" + ), + ), + migrations.AddField( + model_name="contract", + name="uploaded_by", + field=models.ForeignKey( + help_text="Informace není veřejně přístupná.", + on_delete=django.db.models.deletion.CASCADE, + related_name="uploaded_contracts", + to=settings.AUTH_USER_MODEL, + verbose_name="Nahráno uživatelem", + ), + ), ] diff --git a/contracts/migrations/0002_initial.py b/contracts/migrations/0002_initial.py deleted file mode 100644 index f85a350f4139cb677dc01ddd42d22a5b6ccf2ef1..0000000000000000000000000000000000000000 --- a/contracts/migrations/0002_initial.py +++ /dev/null @@ -1,118 +0,0 @@ -# Generated by Django 4.1.4 on 2023-02-03 15:25 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("contracts", "0001_initial"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name="contractnote", - name="author", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="contract_notes", - to=settings.AUTH_USER_MODEL, - verbose_name="Autor", - ), - ), - migrations.AddField( - model_name="contractnote", - name="contract", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="contracts.contract" - ), - ), - migrations.AddField( - model_name="contractlocalsignature", - name="signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="contracts.contractlocalsigner", - ), - ), - migrations.AddField( - model_name="contractexternalsignature", - name="signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="contracts.contractexternalsigner", - ), - ), - migrations.AddField( - model_name="contract", - name="external_signature", - field=models.ManyToManyField(to="contracts.contractexternalsignature"), - ), - migrations.AddField( - model_name="contract", - name="filing_area", - field=models.ForeignKey( - blank=True, - help_text="Obsah není veřejně přístupný.", - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="contracts.contractfilingarea", - ), - ), - migrations.AddField( - model_name="contract", - name="issues", - field=models.ManyToManyField(to="contracts.contractissue"), - ), - migrations.AddField( - model_name="contract", - name="local_signature", - field=models.ManyToManyField(to="contracts.contractlocalsignature"), - ), - migrations.AddField( - model_name="contract", - name="primary_contract", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="contracts.contract", - verbose_name="Hlavní smlouva", - ), - ), - migrations.AddField( - model_name="contract", - name="public_status_set_by", - field=models.ForeignKey( - help_text="Obsah není veřejně přístupný.", - on_delete=django.db.models.deletion.CASCADE, - related_name="public_status_altered_contracts", - to=settings.AUTH_USER_MODEL, - verbose_name="Zveřejněno / nezveřejněno uživatelem", - ), - ), - migrations.AddField( - model_name="contract", - name="subtype", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="contracts.contractsubtype", - verbose_name="Podtyp", - ), - ), - migrations.AddField( - model_name="contract", - name="uploaded_by", - field=models.ForeignKey( - help_text="Informace není veřejně přístupná.", - on_delete=django.db.models.deletion.CASCADE, - related_name="uploaded_contracts", - to=settings.AUTH_USER_MODEL, - verbose_name="Nahráno uživatelem", - ), - ), - ] diff --git a/contracts/migrations/0003_contractexternalsignerrepresentative_contractintent_and_more.py b/contracts/migrations/0003_contractexternalsignerrepresentative_contractintent_and_more.py deleted file mode 100644 index 7353a6556bc4f029f92a522c7e513bd064e90203..0000000000000000000000000000000000000000 --- a/contracts/migrations/0003_contractexternalsignerrepresentative_contractintent_and_more.py +++ /dev/null @@ -1,189 +0,0 @@ -# Generated by Django 4.1.4 on 2023-02-11 09:11 - -import django.db.models.deletion -import markdownx.models -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("contracts", "0002_initial"), - ] - - operations = [ - migrations.CreateModel( - name="ContractExternalSignerRepresentative", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256, verbose_name="Jméno")), - ("role", models.CharField(max_length=256, verbose_name="Funkce")), - ], - options={ - "verbose_name": "Zástupce druhé smluvní strany", - "verbose_name_plural": "Zástupci druhé smluvní strany", - }, - ), - migrations.CreateModel( - name="ContractIntent", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "url", - models.URLField( - blank=True, max_length=256, null=True, verbose_name="Odkaz" - ), - ), - ], - options={ - "verbose_name": "Záměr", - "verbose_name_plural": "Záměry", - }, - ), - migrations.CreateModel( - name="ContractLocalSignerRepresentative", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256, verbose_name="Jméno")), - ("role", models.CharField(max_length=256, verbose_name="Funkce")), - ], - options={ - "verbose_name": "Zástupce naší smluvní strany", - "verbose_name_plural": "Zástupci naší smluvní strany", - }, - ), - migrations.RemoveField( - model_name="contract", - name="intent_url", - ), - migrations.RemoveField( - model_name="contractexternalsigner", - name="representative_name", - ), - migrations.RemoveField( - model_name="contractexternalsigner", - name="representative_role", - ), - migrations.RemoveField( - model_name="contractlocalsigner", - name="representative_name", - ), - migrations.RemoveField( - model_name="contractlocalsigner", - name="representative_role", - ), - migrations.AddField( - model_name="contract", - name="notes", - field=markdownx.models.MarkdownxField( - blank=True, - help_text="Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat.", - null=True, - verbose_name="Obsah", - ), - ), - migrations.AlterField( - model_name="contract", - name="filing_area", - field=models.ForeignKey( - blank=True, - help_text="Obsah není veřejně přístupný.", - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="filed_contracts", - to="contracts.contractfilingarea", - ), - ), - migrations.AlterField( - model_name="contract", - name="primary_contract", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="subcontracts", - to="contracts.contract", - verbose_name="Hlavní smlouva", - ), - ), - migrations.RemoveField( - model_name="contract", - name="subtype", - ), - migrations.AlterField( - model_name="contractexternalsignature", - name="signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="signatures", - to="contracts.contractexternalsigner", - ), - ), - migrations.AlterField( - model_name="contractlocalsignature", - name="signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="signatures", - to="contracts.contractlocalsigner", - ), - ), - migrations.DeleteModel( - name="ContractNote", - ), - migrations.AddField( - model_name="contractlocalsignerrepresentative", - name="signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="representatives", - to="contracts.contractlocalsigner", - ), - ), - migrations.AddField( - model_name="contractintent", - name="contract", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="intents", - to="contracts.contract", - ), - ), - migrations.AddField( - model_name="contractexternalsignerrepresentative", - name="signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="representatives", - to="contracts.contractexternalsigner", - ), - ), - migrations.AddField( - model_name="contract", - name="subtype", - field=models.ManyToManyField(to="contracts.contractsubtype"), - ), - ] diff --git a/contracts/migrations/0004_contracteerepresentative_contracteesignature_and_more.py b/contracts/migrations/0004_contracteerepresentative_contracteesignature_and_more.py deleted file mode 100644 index 1f8e7fb62aed08c72a25a691841925c3cfe68dc4..0000000000000000000000000000000000000000 --- a/contracts/migrations/0004_contracteerepresentative_contracteesignature_and_more.py +++ /dev/null @@ -1,119 +0,0 @@ -# Generated by Django 4.1.4 on 2023-02-11 12:22 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ( - "contracts", - "0003_contractexternalsignerrepresentative_contractintent_and_more", - ), - ] - - operations = [ - migrations.CreateModel( - name="ContracteeRepresentative", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("name", models.CharField(max_length=256, verbose_name="Jméno")), - ("role", models.CharField(max_length=256, verbose_name="Funkce")), - ], - options={ - "verbose_name": "Zástupce naší smluvní strany", - "verbose_name_plural": "Zástupci naší smluvní strany", - }, - ), - migrations.CreateModel( - name="ContracteeSignature", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("date", models.DateField(verbose_name="Datum podpisu")), - ], - ), - migrations.RenameModel( - old_name="ContractExternalSigner", - new_name="Signee", - ), - migrations.RenameModel( - old_name="ContractExternalSignerRepresentative", - new_name="SigneeRepresentative", - ), - migrations.RenameModel( - old_name="ContractExternalSignature", - new_name="SigneeSignature", - ), - migrations.RemoveField( - model_name="contractlocalsignerrepresentative", - name="signer", - ), - migrations.RenameField( - model_name="contract", - old_name="external_signature", - new_name="signee_signature", - ), - migrations.RenameField( - model_name="signeerepresentative", - old_name="signer", - new_name="signee", - ), - migrations.RenameField( - model_name="signeesignature", - old_name="signer", - new_name="signee", - ), - migrations.RemoveField( - model_name="contract", - name="local_signature", - ), - migrations.RenameModel( - old_name="ContractLocalSigner", - new_name="Contractee", - ), - migrations.DeleteModel( - name="ContractLocalSignature", - ), - migrations.DeleteModel( - name="ContractLocalSignerRepresentative", - ), - migrations.AddField( - model_name="contracteesignature", - name="contractee", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="signatures", - to="contracts.contractee", - ), - ), - migrations.AddField( - model_name="contracteerepresentative", - name="contractee", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="representatives", - to="contracts.contractee", - ), - ), - migrations.AddField( - model_name="contract", - name="contractee_signatures", - field=models.ManyToManyField(to="contracts.contracteesignature"), - ), - ] diff --git a/contracts/models.py b/contracts/models.py index 8cc2d366f8c6a5c548c5843e76ea029e5ef90b93..f3b62b7e35fab401d49188d5d7b1737d6e40f77b 100644 --- a/contracts/models.py +++ b/contracts/models.py @@ -96,34 +96,38 @@ class SigneeSignature(models.Model): verbose_name="Datum podpisu", ) + class Meta: + verbose_name = "Podpis druhé smluvní strany" + verbose_name_plural = "Podpisy druhé smluvní strany" + class Contractee(models.Model): name = models.CharField( max_length=256, - default=settings.DEFAULT_LOCAL_SIGNER_NAME, + default=settings.DEFAULT_CONTRACTEE_NAME, verbose_name="Jméno", ) address_street_with_number = models.CharField( max_length=256, - default=settings.DEFAULT_LCOAL_SIGNER_STREET, + default=settings.DEFAULT_CONTRACTEE_STREET, verbose_name="Ulice, č.p.", ) address_district = models.CharField( max_length=256, - default=settings.DEFAULT_LOCAL_SIGNER_DISTRICT, + default=settings.DEFAULT_CONTRACTEE_DISTRICT, verbose_name="Obec", ) address_zip = models.CharField( max_length=16, - default=settings.DEFAULT_LOCAL_SIGNER_ZIP, + default=settings.DEFAULT_CONTRACTEE_ZIP, verbose_name="PSČ", ) address_country = CountryField( - default=settings.DEFAULT_LOCAL_SIGNER_COUNTRY, + default=settings.DEFAULT_CONTRACTEE_COUNTRY, verbose_name="Země", ) @@ -131,7 +135,7 @@ class Contractee(models.Model): max_length=16, blank=True, null=True, - default=settings.DEFAULT_LOCAL_SIGNER_ICO_NUMBER, + default=settings.DEFAULT_CONTRACTEE_ICO_NUMBER, verbose_name="IČO", ) @@ -150,7 +154,7 @@ class Contractee(models.Model): class Meta: verbose_name = "Naše smluvní strana" - verbose_name_plural = "Naše smlouvní strany" + verbose_name_plural = "Naše smluvní strany" class ContracteeRepresentative(models.Model): @@ -186,6 +190,10 @@ class ContracteeSignature(models.Model): verbose_name="Datum podpisu", ) + class Meta: + verbose_name = "Podpis naší smluvní strany" + verbose_name_plural = "Podpisy naší smluvní strany" + class ContractSubtype(models.Model): name = models.CharField( @@ -206,7 +214,7 @@ class ContractIssue(models.Model): class Meta: verbose_name = "Problém se smlouvou" - verbose_name_plural = "Problémy se smlouvou" + verbose_name_plural = "Problémy se smlouvami" class ContractFilingArea(models.Model): @@ -238,16 +246,25 @@ class Contract(models.Model): verbose_name="Typ", ) - subtype = models.ManyToManyField(ContractSubtype) + subtype = models.ManyToManyField( + ContractSubtype, + verbose_name="Podtypy", + ) contains_nda = models.BooleanField( default=False, verbose_name="Obsahuje NDA", ) - signee_signature = models.ManyToManyField(SigneeSignature) + signee_signature = models.ManyToManyField( + SigneeSignature, + verbose_name="Podpisy druhé smluvní strany", + ) - contractee_signatures = models.ManyToManyField(ContracteeSignature) + contractee_signatures = models.ManyToManyField( + ContracteeSignature, + verbose_name="Naše podpisy", + ) all_parties_sign_date = models.DateField( verbose_name="Datum podpisu všech stran", @@ -331,7 +348,10 @@ class Contract(models.Model): verbose_name="Identifikační číslo", ) - issues = models.ManyToManyField(ContractIssue) + issues = models.ManyToManyField( + ContractIssue, + verbose_name="Problémy", + ) notes = MarkdownxField( blank=True, null=True, diff --git a/contracts/urls.py b/contracts/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..b837598466bc6ed8d1a8c329c8a279607b3d1f0c --- /dev/null +++ b/contracts/urls.py @@ -0,0 +1,6 @@ +from django.urls import path + +from . import views + +app_name = "contracts" +urlpatterns = [] diff --git a/env.example b/env.example index c771c2f1724ba1c170c691974ff286cb256a0cf8..94d143402bd81d31d93aff0790f114dbf60db0ca 100644 --- a/env.example +++ b/env.example @@ -2,9 +2,15 @@ DATABASE_URL="postgresql://contracts:contracts@localhost:5432/contracts" SECRET_KEY=supersecret -DEFAULT_LOCAL_SIGNER_NAME="Česká pirátská strana" -DEFAULT_LCOAL_SIGNER_STREET="Na Moráni 360/3" -DEFAULT_LOCAL_SIGNER_ZIP="128 00" -DEFAULT_LOCAL_SIGNER_DISTRICT="Praha 2" -DEFAULT_LOCAL_SIGNER_COUNTRY="CZ" -DEFAULT_LOCAL_SIGNER_ICO_NUMBER="71339698" +SITE_URL="http://localhost:8006" + +OIDC_RP_REALM_URL="http://localhost:8080/realms/master/" +OIDC_RP_CLIENT_ID=contracts +OIDC_RP_CLIENT_SECRET=VCn4LVAUc6RGLSup7VaAKsmrKUbWguaP + +DEFAULT_CONTRACTEE_NAME="Česká pirátská strana" +DEFAULT_CONTRACTEE_STREET="Na Moráni 360/3" +DEFAULT_CONTRACTEE_ZIP="128 00" +DEFAULT_CONTRACTEE_DISTRICT="Praha 2" +DEFAULT_CONTRACTEE_COUNTRY="CZ" +DEFAULT_CONTRACTEE_ICO_NUMBER="71339698" diff --git a/oidc/__init__.py b/oidc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/oidc/admin.py b/oidc/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/oidc/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/oidc/apps.py b/oidc/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..742acac1713eae78fd9b35b887dfc5dfe22b1989 --- /dev/null +++ b/oidc/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class OidcConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "oidc" diff --git a/oidc/auth.py b/oidc/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..1c9b54f233e2ed60772fdae164aeef6fb4fc6df8 --- /dev/null +++ b/oidc/auth.py @@ -0,0 +1,57 @@ +import logging + +from django.conf import settings +from django.contrib.auth.models import Group +from pirates.auth import PiratesOIDCAuthenticationBackend + +logging.basicConfig(level=logging.DEBUG) + + +class NastenkaOIDCAuthenticationBackend(PiratesOIDCAuthenticationBackend): + def _assign_new_user_groups(self, user, claims, user_groups=None) -> None: + if user_groups is None: + user_groups = user.groups.all() + + for role in claims["resource_access"][settings.OIDC_RP_RESOURCE_ACCESS_CLIENT][ + "roles" + ]: + group_name = f"sso_{role}" + + group = Group.objects.filter(name=group_name) + + if not group.exists(): + group = Group(name=group_name) + group.save() + else: + group = group[0] + + if group not in user_groups: + user.groups.add(group) + + user.save() + + def create_user(self, claims): + user = super().create_user(claims) + + if "resource_access" not in claims: + return user + + self._assign_new_user_groups(user, claims) + + return user + + def update_user(self, user, claims): + if "resource_access" not in claims: + return user + + user_groups = user.groups.all() + + for group in user_groups: + if group.name.replace("sso_", "") not in ( + claims["resource_access"][settings.OIDC_RP_CLIENT_ID]["roles"] + ): + user.groups.remove(group) + + self._assign_new_user_groups(user, claims, user_groups) + + return user diff --git a/oidc/models.py b/oidc/models.py new file mode 100644 index 0000000000000000000000000000000000000000..71a836239075aa6e6e4ecb700e9c42c95c022d91 --- /dev/null +++ b/oidc/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/oidc/templates/oidc/base.html b/oidc/templates/oidc/base.html new file mode 100644 index 0000000000000000000000000000000000000000..610ef367e1ed055f1bf237faa82ac0079fa6475d --- /dev/null +++ b/oidc/templates/oidc/base.html @@ -0,0 +1,32 @@ +{% load static %} +{% load render_bundle from webpack_loader %} + +<!DOCTYPE html> +<html lang="cs" class="h-full"> + <head> + <meta encoding="utf-8"> + <link + rel="stylesheet" + type="text/css" + href="{% static "shared/style.css" %}" + > + + <link + rel="stylesheet" + href="https://gfonts.pirati.cz/css2?family=Bebas+Neue&family=Roboto+Condensed:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&display=swap" + > + + <link + rel="stylesheet" + type="text/css" + href="{% static "shared/fonts/pirati-ui/style.css" %}" + > + + <title>{% block current_page %}{% endblock %}</title> + + {% render_bundle "base" %} + </head> + <body> + {% block content %}{% endblock %} + </body> +</html> diff --git a/oidc/templates/oidc/login.html b/oidc/templates/oidc/login.html new file mode 100644 index 0000000000000000000000000000000000000000..f00d86ae7671b9d0a0542260ad4c21ca0b76dc73 --- /dev/null +++ b/oidc/templates/oidc/login.html @@ -0,0 +1,13 @@ +{% extends "oidc/base.html" %} + +{% block content %} + {% if user.is_authenticated %} + <p>Current user: {{ user.email }}</p> + <form action="{% url "oidc_logout" %}" method="POST"> + {% csrf_token %} + <input type="submit" value="logout"> + </form> + {% else %} + <a href="{% url "oidc_authentication_init" %}">Přihlásit se</a> + {% endif %} +{% endblock %} diff --git a/oidc/tests.py b/oidc/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/oidc/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/oidc/urls.py b/oidc/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a15ad64f99388e3d9dbdee1be71aadec38097a99 --- /dev/null +++ b/oidc/urls.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import views + +app_name = "oidc" +urlpatterns = [ + path("login", views.login, name="login"), +] diff --git a/oidc/views.py b/oidc/views.py new file mode 100644 index 0000000000000000000000000000000000000000..23dbba52821cdaeda52388fe8cd7da5fadf6c39d --- /dev/null +++ b/oidc/views.py @@ -0,0 +1,8 @@ +from django.shortcuts import render + + +def login(request): + return render( + request, + "oidc/login.html", + ) diff --git a/registry/settings/base.py b/registry/settings/base.py index 7fa8ccf883d258a48cf5995450e18c4faa4e4bb6..5f69f7023e33d43cc9148c51928b6e917cdb8d0f 100644 --- a/registry/settings/base.py +++ b/registry/settings/base.py @@ -36,7 +36,7 @@ ALLOWED_HOSTS = [] STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") -# Application definition +## Application definition INSTALLED_APPS = [ "django.contrib.admin", @@ -45,7 +45,12 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "guardian", + "markdownx", + "pirates", + "webpack_loader", "contracts", + "oidc", "shared", "users", ] @@ -58,6 +63,8 @@ MIDDLEWARE = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django_http_exceptions.middleware.ExceptionHandlerMiddleware", + "django_http_exceptions.middleware.ThreadLocalRequestMiddleware", ] ROOT_URLCONF = "registry.urls" @@ -81,14 +88,21 @@ TEMPLATES = [ WSGI_APPLICATION = "registry.wsgi.application" -# Database +## Database # https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = {"default": dj_database_url.config(conn_max_age=600)} +# Default primary key field type +# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +## Authentication # Password validation -# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { @@ -106,33 +120,67 @@ AUTH_PASSWORD_VALIDATORS = [ ] AUTH_USER_MODEL = "users.User" +AUTHENTICATION_BACKENDS = ( + "oidc.auth.NastenkaOIDCAuthenticationBackend", + "django.contrib.auth.backends.ModelBackend", + "guardian.backends.ObjectPermissionBackend", +) -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ +LOGIN_URL = "/oidc/login" +LOGIN_REDIRECT_URL = "/" +LOGOUT_REDIRECT_URL = "/oidc/login" -LANGUAGE_CODE = "en-us" +OIDC_RP_CLIENT_ID = env.str("OIDC_RP_CLIENT_ID") +OIDC_RP_CLIENT_SECRET = env.str("OIDC_RP_CLIENT_SECRET") +OIDC_RP_REALM_URL = env.str("OIDC_RP_REALM_URL") +OIDC_RP_SCOPES = "openid email roles" +OIDC_RP_SIGN_ALGO = "RS256" +OIDC_RP_RESOURCE_ACCESS_CLIENT = env.str( + "OIDC_RESOURCE_ACCESS_CLIENT", OIDC_RP_CLIENT_ID +) -TIME_ZONE = "UTC" +OIDC_OP_JWKS_ENDPOINT = OIDC_RP_REALM_URL + "protocol/openid-connect/certs" +OIDC_OP_AUTHORIZATION_ENDPOINT = OIDC_RP_REALM_URL + "protocol/openid-connect/auth" +OIDC_OP_TOKEN_ENDPOINT = OIDC_RP_REALM_URL + "protocol/openid-connect/token" +OIDC_OP_USER_ENDPOINT = OIDC_RP_REALM_URL + "protocol/openid-connect/userinfo" -USE_I18N = True +## Internationalization +# https://docs.djangoproject.com/en/4.0/topics/i18n/ + +LANGUAGE_CODE = "cs-cz" +TIME_ZONE = "Europe/Prague" +USE_I18N = True USE_TZ = True -# Static files (CSS, JavaScript, Images) +## Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.0/howto/static-files/ STATIC_URL = "static/" -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field +WEBPACK_LOADER = { + "DEFAULT": { + "CACHE": not DEBUG, + "BUNDLE_DIR_NAME": "shared", + "STATS_FILE": os.path.join(BASE_DIR, "webpack-stats.json"), + "POLL_INTERVAL": 0.1, + "IGNORE": [r".+\.hot-update.js", r".+\.map"], + } +} + + +## Server + +USE_X_FORWARDED_HOST = True +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" +## App-specific -DEFAULT_LOCAL_SIGNER_NAME = env.str("DEFAULT_LOCAL_SIGNER_NAME") -DEFAULT_LCOAL_SIGNER_STREET = env.str("DEFAULT_LCOAL_SIGNER_STREET") -DEFAULT_LOCAL_SIGNER_ZIP = env.str("DEFAULT_LOCAL_SIGNER_ZIP") -DEFAULT_LOCAL_SIGNER_DISTRICT = env.str("DEFAULT_LOCAL_SIGNER_DISTRICT") -DEFAULT_LOCAL_SIGNER_COUNTRY = env.str("DEFAULT_LOCAL_SIGNER_COUNTRY") -DEFAULT_LOCAL_SIGNER_ICO_NUMBER = env.str("DEFAULT_LOCAL_SIGNER_ICO_NUMBER") +DEFAULT_CONTRACTEE_NAME = env.str("DEFAULT_CONTRACTEE_NAME") +DEFAULT_CONTRACTEE_STREET = env.str("DEFAULT_CONTRACTEE_STREET") +DEFAULT_CONTRACTEE_ZIP = env.str("DEFAULT_CONTRACTEE_ZIP") +DEFAULT_CONTRACTEE_DISTRICT = env.str("DEFAULT_CONTRACTEE_DISTRICT") +DEFAULT_CONTRACTEE_COUNTRY = env.str("DEFAULT_CONTRACTEE_COUNTRY") +DEFAULT_CONTRACTEE_ICO_NUMBER = env.str("DEFAULT_CONTRACTEE_ICO_NUMBER") diff --git a/registry/urls.py b/registry/urls.py index 417ef5128c133640e46663d4ae520e1aabcf100a..b80111c3fad8f33984c62bf7bf16cbf0e623baf1 100644 --- a/registry/urls.py +++ b/registry/urls.py @@ -1,7 +1,7 @@ -"""registry URL Configuration +"""URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.0/topics/http/urls/ + https://docs.djangoproject.com/en/4.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views @@ -14,8 +14,12 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import include, path +from pirates.urls import urlpatterns as pirates_urlpatterns urlpatterns = [ + path("markdownx/", include("markdownx.urls")), + path("contracts/", include("contracts.urls")), + path("oidc/", include("oidc.urls")), path("admin/", admin.site.urls), -] +] + pirates_urlpatterns diff --git a/shared/admin.py b/shared/admin.py index 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..2b2f1ef9507424d98caa1d8d1ec9e3134deeec01 100644 --- a/shared/admin.py +++ b/shared/admin.py @@ -1,3 +1,7 @@ from django.contrib import admin +from guardian.admin import GuardedModelAdmin +from markdownx.admin import MarkdownxModelAdmin -# Register your models here. + +class MarkdownxGuardedModelAdmin(MarkdownxModelAdmin, GuardedModelAdmin): + pass diff --git a/users/models.py b/users/models.py index ea62fb3fa8eef5bddf9697561fab74f215460fd4..11e82baf8f2eae2b3caf8dcbdd10dce9d39caef4 100644 --- a/users/models.py +++ b/users/models.py @@ -2,4 +2,14 @@ from pirates import models as pirates_models class User(pirates_models.AbstractUser): - pass + def set_unusable_password(self) -> None: + # Purely for compatibility with Guardian + pass + + def get_username(self) -> str: + first_name = self.first_name + + if len(first_name) != 0: + first_name += " " + + return f"{first_name}{self.last_name}"