diff --git a/README.md b/README.md index 93b1e17c4e3aab6b2633c7ee7f06aedb9954ba0d..e0f2388b998745773e9fce9649e95a5bd07c4d3e 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,150 @@ -# Contract registry +# Registr smluv -The Czech Pirate Party's transparent evidence system for its contracts and -information about them. +Aplikace pro transparentní evidenci smluv a informací s nimi spojených. -## Basic requirements -- `make` -- `python`, 3.9 or newer -- `python-virtualenv` -- A PostgreSQL server +## Struktura projektu +``` +. +├── registry = Nastavení projektu, URLs. +├── shared = Sdílené modely, templaty a statické soubory pro všechny ostatní aplikace. +├── static_src = Zdrojové CSS a JS, které příkazem 'make build' buildujeme. +├── requirements = Pythonové závislosti z PyPI. +└── env.example = Příklad .env souboru. +``` + +## Konfigurace + +Je třeba definovat minimálně následující environment proměnné: +| proměnná | popis | +| --- | --- | +| `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 | + +V produkci je potřeba: +| proměnná | popis | +| --- | --- | +| `ALLOWED_HOSTS` | Seznam domén, skrz které se na server lze připojovat. [Více info](https://docs.djangoproject.com/en/4.1/ref/settings/#allowed-hosts) | + +## Vývoj + +V produkci používáme Docker. Při vývoji se hodí přiložený `Makefile`, pro automatizování často prováděných akcí. Pro nápovědu zavolej: -## Setup +```bash +$ make help +``` -Copy `env.example` to `.env`. +### Lokální setup -Set the ``DATABASE_URL`` environment variable in `.env` to one you can access your database server with. The format should be as per RFC 1738, such as `postgresql://login:password@localhost:5432/database_name`. It's also important to change the ``SECRET_KEY`` variable. +Požadavky: +- Python 3.9+ +- Linuxové prostředí, na Windows netestováno -Then, run the following commands: +Zkopíruj `env.example` do `.env`, nastav potřebné proměnné. + +Vytvoř virtualenv: ```bash -make venv # Create virtual environment -make install # Install Python and Node.js dependencies -make build # Build static files +$ make venv ``` -## Running +Vytvoří virtualenv ve složce `.venv`. Předpokládá že výchozí `python` v terminálu +je Python 3. Pokud tomu tak není, použij třeba [Pyenv](https://github.com/pyenv/pyenv) +pro instalaci více verzí Pythonu bez rizika rozbití systému. + +### Aktivace virtualenvu -To run with the default settings on the port set in `Makefile`, run: +Před prací na projektu je třeba aktivovat virtualenv. To bohužel nejde dělat +pomocí nástroje `make`. Je třeba zavolat příkaz: ```bash -make run +$ source .venv/bin/activate ``` -For more customization, it's better practice to run the server starting command directly: +Pro shell lze vytvořit alias. Do `~/.bash_profile`, `~/.zshrc` nebo jiného +konfiguračního souboru dle tvého shellu přidej: + +```bash +$ alias senv='source .venv/bin/activate' +``` + +A pak můžeš virtualenv aktivovat pouze jednoduchým voláním: + +```bash +$ senv +``` + +Pro sofistikovanější řešení, které aktivuje virtualenv při změně adresáře na +adresář s projektem, slouží nástroj [direnv](https://direnv.net/). + +Deaktivace virtualenv se dělá příkazem: + +```bash +$ deactivate +``` + +### Instalace závislostí + +Spusť: + +```bash +$ make install +``` + +Tím se nainstalují Pythonové závislosti a virtuální Node.js 19x nutné k buildu statických souborů. + +### Build statických souborů + +Spusť: + +```bash +$ make build +``` + +Tím se vytvoří CSS a JS soubory, které se dají použít v prohlížečovém prostředí. + +### Spuštění development serveru + +Django development server na portu `8012` se spustí příkazem: + +```bash +$ make run +``` + +Poté můžeš web otevřít na adrese [http://localhost:8012](http://localhost:8012). + +### Code quality + +K formátování kódu se používá [black](https://github.com/psf/black). Doporučujeme +ho nainstalovat do tvého editoru, aby soubory přeformátoval po uložení. + +Přeformátování kódu nástrojem `black` je součástí `pre-commit` hooks (viz níže). + +Součástí `pre-commit` hooků je také automatické seřazení importů v Pythonních +souborech nástrojem [isort](https://github.com/timothycrosley/isort/). + +### Pre-commit hooky + +Použivá se [pre-commit](https://pre-commit.com/) framework pro management git +pre-commit hooks. + +Máš-li pre-commit framework [nainstalovaný](https://pre-commit.com/#installation), +spusť příkaz: + +```bash +$ make install-hooks +``` + +Ten naisntaluje hooky pro projekt. A poté při každém commitu dojde k požadovaným +akcím na změněných souborech. + +Ručně se dají hooky na všechny soubory spustit příkazem: ```bash -source .venv/bin/activate # Load the virtual environment -python manage.py runserver # [ Your settings here ] +$ make hooks ``` diff --git a/registry/settings/production.py b/registry/settings/production.py index 9b5ed21c9e3f65438f966db7f1913bc34b7ffa97..d0c94518f5c212b4d9ce93f77ff9388f9d4465ed 100644 --- a/registry/settings/production.py +++ b/registry/settings/production.py @@ -1 +1,10 @@ +""" +Production settings. +""" + from .base import * + +ALLOWED_HOSTS = env.list("ALLOWED_HOSTS") + +MIDDLEWARE.insert(1, "whitenoise.middleware.WhiteNoiseMiddleware") +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" diff --git a/requirements/production.txt b/requirements/production.txt index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..ce5169e4440b67843ee5d28199ed63e0d8323cfc 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -0,0 +1,2 @@ +gunicorn==20.1.0 +whitenoise==6.3.0 diff --git a/shared/migrations/0001_initial.py b/shared/migrations/0001_initial.py index 0d45e6af3e395ec6d04e68da404bee7bae83db3e..c0a4def389d06410cc2907e8d84eb59175788879 100644 --- a/shared/migrations/0001_initial.py +++ b/shared/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.4 on 2023-02-03 04:50 +# Generated by Django 4.1.4 on 2023-02-03 15:13 import django.db.models.deletion import django.utils.timezone @@ -145,18 +145,6 @@ class Migration(migrations.Migration): "contains_nda", models.BooleanField(default=False, verbose_name="Obsahuje NDA"), ), - ( - "is_anonymized", - models.BooleanField(default=False, verbose_name="Je anonymizovaná"), - ), - ( - "external_signer_signature_date", - models.DateField(verbose_name="Datum podpisu druhé strany"), - ), - ( - "local_signer_signature_date", - models.DateField(verbose_name="Datum podpisu naší strany"), - ), ( "all_parties_sign_date", models.DateField(verbose_name="Datum podpisu všech stran"), @@ -165,7 +153,7 @@ class Migration(migrations.Migration): "valid_start_date", models.DateField(verbose_name="Začátek účinnosti"), ), - ("valid_end_date", models.DateField(verbose_name="Začátek platnosti")), + ("valid_end_date", models.DateField(verbose_name="Konec platnosti")), ( "legal_state", models.CharField( @@ -208,6 +196,7 @@ class Migration(migrations.Migration): "publishing_rejection_comment", models.CharField( blank=True, + help_text="Obsah není veřejně přístupný.", max_length=65536, null=True, verbose_name="Důvod nezveřejnění", @@ -232,14 +221,25 @@ class Migration(migrations.Migration): "summary", models.CharField( blank=True, + help_text="Obsah není veřejně přístupný.", max_length=65536, null=True, verbose_name="Rekapitulace", ), ), ( - "contract_file", - models.FileField(upload_to="", verbose_name="Smlouva (PDF)"), + "anonymized_contract_file", + models.FileField( + upload_to="", verbose_name="Anonymizovaná smlouva (PDF)" + ), + ), + ( + "original_contract_file", + models.FileField( + help_text="Obsah není veřejně přístupný.", + upload_to="", + verbose_name="Originální verze smlouvy (PDF)", + ), ), ( "expected_cost_total", @@ -296,17 +296,31 @@ class Migration(migrations.Migration): ("name", models.CharField(max_length=256, verbose_name="Jméno")), ( "is_legal_entity", - models.BooleanField(verbose_name="Je právnická osoba"), + 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(max_length=256, verbose_name="Ulice, č.p."), + models.CharField( + help_text="Viditelné pouze u právnických osob.", + max_length=256, + verbose_name="Ulice, č.p.", + ), ), ( "address_district", models.CharField(max_length=256, verbose_name="Obec"), ), - ("address_zip", models.CharField(max_length=16, verbose_name="PSČ")), + ( + "address_zip", + models.CharField( + help_text="Viditelné pouze u právnických osob.", + max_length=16, + verbose_name="PSČ", + ), + ), ( "address_country", django_countries.fields.CountryField( @@ -409,26 +423,72 @@ 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", + ), + ), ( "address_street_with_number", - models.CharField(max_length=256, verbose_name="Ulice, č.p."), + models.CharField( + 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( + default="128 00", max_length=16, verbose_name="PSČ" + ), ), - ("address_zip", models.CharField(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" + 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" + ), + ), + ( + "representative_role", + models.CharField( + blank=True, + max_length=256, + null=True, + verbose_name="Funkce zástupce", + ), + ), + ( + "department", + models.CharField( + blank=True, + max_length=128, + null=True, + verbose_name="Organizační složka", ), ), ("color", models.CharField(max_length=6, verbose_name="Barva")), @@ -493,19 +553,61 @@ class Migration(migrations.Migration): "verbose_name_plural": "Poznámky ke smlouvě", }, ), + migrations.CreateModel( + name="ContractLocalSignature", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Datum podpisu")), + ( + "signer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="shared.contractlocalsigner", + ), + ), + ], + ), + migrations.CreateModel( + name="ContractExternalSignature", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(verbose_name="Datum podpisu")), + ( + "signer", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="shared.contractexternalsigner", + ), + ), + ], + ), migrations.AddField( model_name="contract", - name="external_signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="shared.contractexternalsigner", - ), + name="external_signature", + field=models.ManyToManyField(to="shared.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="shared.contractfilingarea", @@ -518,11 +620,8 @@ class Migration(migrations.Migration): ), migrations.AddField( model_name="contract", - name="local_signer", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="shared.contractlocalsigner", - ), + name="local_signature", + field=models.ManyToManyField(to="shared.contractlocalsignature"), ), migrations.AddField( model_name="contract", @@ -539,6 +638,7 @@ class Migration(migrations.Migration): 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, @@ -558,6 +658,7 @@ class Migration(migrations.Migration): 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, diff --git a/shared/migrations/0002_contractlocalsigner_department_and_more.py b/shared/migrations/0002_contractlocalsigner_department_and_more.py deleted file mode 100644 index 010d1a213b41d265fa8d9b84f5d533e3f5a66cb2..0000000000000000000000000000000000000000 --- a/shared/migrations/0002_contractlocalsigner_department_and_more.py +++ /dev/null @@ -1,78 +0,0 @@ -# Generated by Django 4.1.4 on 2023-02-03 05:01 - -import django_countries.fields -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("shared", "0001_initial"), - ] - - operations = [ - migrations.AddField( - model_name="contractlocalsigner", - name="department", - field=models.CharField( - blank=True, max_length=128, null=True, verbose_name="Organizační složka" - ), - ), - migrations.AddField( - model_name="contractlocalsigner", - name="representative_name", - field=models.CharField( - blank=True, max_length=256, null=True, verbose_name="Zástupce" - ), - ), - migrations.AddField( - model_name="contractlocalsigner", - name="representative_role", - field=models.CharField( - blank=True, max_length=256, null=True, verbose_name="Funkce zástupce" - ), - ), - migrations.AlterField( - model_name="contractlocalsigner", - name="address_country", - field=django_countries.fields.CountryField( - default="CZ", max_length=2, verbose_name="Země" - ), - ), - migrations.AlterField( - model_name="contractlocalsigner", - name="address_district", - field=models.CharField( - default="Praha 2", max_length=256, verbose_name="Obec" - ), - ), - migrations.AlterField( - model_name="contractlocalsigner", - name="address_street_with_number", - field=models.CharField( - default="Na Moráni 360/3", max_length=256, verbose_name="Ulice, č.p." - ), - ), - migrations.AlterField( - model_name="contractlocalsigner", - name="address_zip", - field=models.CharField(default="128 00", max_length=16, verbose_name="PSČ"), - ), - migrations.AlterField( - model_name="contractlocalsigner", - name="ico_number", - field=models.CharField( - blank=True, - default="71339698", - max_length=16, - null=True, - verbose_name="IČO", - ), - ), - migrations.AlterField( - model_name="contractlocalsigner", - name="name", - field=models.CharField( - default="Česká pirátská strana", max_length=256, verbose_name="Jméno" - ), - ), - ] diff --git a/shared/models.py b/shared/models.py index 6e90b497a5ce692982fb53e26041385f5ef4f5eb..e48deae16e8c2c6a048db431fd5f7582764cf369 100644 --- a/shared/models.py +++ b/shared/models.py @@ -17,11 +17,13 @@ class ContractExternalSigner(models.Model): is_legal_entity = models.BooleanField( verbose_name="Je právnická osoba", + help_text="Důležité označit správně! Pokud není osoba právnická, zveřejňujeme pouze obec a zemi.", ) address_street_with_number = models.CharField( max_length=256, verbose_name="Ulice, č.p.", + help_text="Viditelné pouze u právnických osob.", ) # WARNING: Legal entity status dependent! address_district = models.CharField( @@ -32,6 +34,7 @@ class ContractExternalSigner(models.Model): address_zip = models.CharField( max_length=16, verbose_name="PSČ", + help_text="Viditelné pouze u právnických osob.", ) # WARNING: Legal entity status dependent! address_country = CountryField( @@ -77,6 +80,17 @@ class ContractExternalSigner(models.Model): verbose_name_plural = "Druhé smluvní strany" +class ContractExternalSignature(models.Model): + signer = models.ForeignKey( + ContractExternalSigner, + on_delete=models.CASCADE, + ) + + date = models.DateField( + verbose_name="Datum podpisu", + ) + + class ContractLocalSigner(models.Model): name = models.CharField( max_length=256, @@ -147,6 +161,17 @@ class ContractLocalSigner(models.Model): verbose_name_plural = "Naše smlouvní strany" +class ContractLocalSignature(models.Model): + signer = models.ForeignKey( + ContractLocalSigner, + on_delete=models.CASCADE, + ) + + date = models.DateField( + verbose_name="Datum podpisu", + ) + + class ContractSubtype(models.Model): name = models.CharField( max_length=32, @@ -209,37 +234,19 @@ class Contract(models.Model): verbose_name="Obsahuje NDA", ) - is_anonymized = models.BooleanField( - default=False, - verbose_name="Je anonymizovaná", - ) # WARNING: Seems to only be used for amendments - - external_signer = models.ForeignKey( - ContractExternalSigner, - on_delete=models.CASCADE, - ) - - # NOTE: Should we allow these to be null, if a contract is logged before it is signed? - external_signer_signature_date = models.DateField( - verbose_name="Datum podpisu druhé strany", - ) - - local_signer = models.ForeignKey( - ContractLocalSigner, - on_delete=models.CASCADE, - ) + external_signature = models.ManyToManyField(ContractExternalSignature) - local_signer_signature_date = models.DateField( - verbose_name="Datum podpisu naší strany", - ) + local_signature = models.ManyToManyField(ContractLocalSignature) all_parties_sign_date = models.DateField( verbose_name="Datum podpisu všech stran", ) # WARNING: Exclude in admin, autofill - valid_start_date = models.DateField(verbose_name="Začátek účinnosti") + valid_start_date = models.DateField( + verbose_name="Začátek účinnosti", + ) valid_end_date = models.DateField( - verbose_name="Začátek platnosti", + verbose_name="Konec platnosti", ) uploaded_by = models.ForeignKey( @@ -247,7 +254,8 @@ class Contract(models.Model): on_delete=models.CASCADE, related_name="uploaded_contracts", verbose_name="Nahráno uživatelem", - ) + help_text="Informace není veřejně přístupná.", + ) # WARNING: exclude in admin class LegalStates(models.TextChoices): VALID = "valid", "Platná" @@ -289,14 +297,16 @@ class Contract(models.Model): on_delete=models.CASCADE, related_name="public_status_altered_contracts", verbose_name="Zveřejněno / nezveřejněno uživatelem", - ) + help_text="Obsah není veřejně přístupný.", + ) # WARNING: exclude in admin publishing_rejection_comment = models.CharField( max_length=65536, blank=True, null=True, verbose_name="Důvod nezveřejnění", - ) + help_text="Obsah není veřejně přístupný.", + ) # WARNING: exclude in admin tender_url = models.URLField( max_length=256, @@ -317,10 +327,16 @@ class Contract(models.Model): blank=True, null=True, verbose_name="Rekapitulace", + help_text="Obsah není veřejně přístupný.", + ) + + anonymized_contract_file = models.FileField( + verbose_name="Anonymizovaná smlouva (PDF)", ) - contract_file = models.FileField( - verbose_name="Smlouva (PDF)", + original_contract_file = models.FileField( + verbose_name="Originální verze smlouvy (PDF)", + help_text="Obsah není veřejně přístupný.", ) primary_contract = models.ForeignKey( @@ -358,6 +374,7 @@ class Contract(models.Model): on_delete=models.CASCADE, blank=True, null=True, + help_text="Obsah není veřejně přístupný.", ) # WARNING: Dependent on the type! class Meta: