diff --git a/.travis.yml b/.travis.yml index 72bd3a32..c0a7d965 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,10 +16,10 @@ python: - "3.7" addons: - postgresql: '9.5' + postgresql: '10' apt: packages: - - postgresql-9.5-postgis-2.4 + - postgresql-10-postgis-2.4 install: - pip install -U pip @@ -33,7 +33,7 @@ script: - python manage.py makemigrations --dry-run --check - black --check . - flake8 - - isort --check-only --diff + - isort . --check-only --diff - pytest -ra -vvv --doctest-modules --cov=. - ./run-type-checks diff --git a/batchrun/.isort.cfg b/batchrun/.isort.cfg deleted file mode 100644 index 97824efe..00000000 --- a/batchrun/.isort.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[isort] -multi_line_output=4 -not_skip=__init__.py -line_length=79 -known_standard_library=dataclasses diff --git a/batchrun/admin.py b/batchrun/admin.py index 9abfeec3..af36b5bb 100644 --- a/batchrun/admin.py +++ b/batchrun/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin +from rangefilter.filter import DateRangeFilter # type: ignore from .admin_utils import PreciseTimeFormatter, ReadOnlyAdmin from .models import ( @@ -23,14 +24,21 @@ class JobAdmin(admin.ModelAdmin): list_display = ["name", "comment", "command"] +class JobRunLogEntryInline(admin.TabularInline): + model = JobRunLogEntry + show_change_link = True + + @admin.register(JobRun) class JobRunAdmin(ReadOnlyAdmin): date_hierarchy = "started_at" + inlines = [JobRunLogEntryInline] list_display = ["started_at_p", "stopped_at_p", "job", "exit_code"] - list_filter = ["exit_code"] + list_filter = ("started_at", ("started_at", DateRangeFilter), "job", "exit_code") # auto_now_add_fields don't show even in readonlyadmin. # Therefore we'll add all the fields by hand in a suitable order readonly_fields = ("job", "pid", "started_at_p", "stopped_at_p", "exit_code") + search_fields = ["log_entries__text"] exclude = ["stopped_at"] started_at_p = PreciseTimeFormatter(JobRun, "started_at") diff --git a/batchrun/api/viewsets.py b/batchrun/api/viewsets.py index 9ba35b04..393dfe6f 100644 --- a/batchrun/api/viewsets.py +++ b/batchrun/api/viewsets.py @@ -28,7 +28,7 @@ class Meta: class JobRunViewSet(viewsets.ReadOnlyModelViewSet): # type: ignore - queryset = models.JobRun.objects.all() + queryset = models.JobRun.objects.all_with_deleted() # type: ignore serializer_class = JobRunSerializer filterset_fields = ["exit_code"] @@ -40,6 +40,6 @@ class Meta: class JobRunLogEntryViewSet(viewsets.ReadOnlyModelViewSet): # type: ignore - queryset = models.JobRunLogEntry.objects.all() + queryset = models.JobRunLogEntry.objects.all_with_deleted() # type: ignore serializer_class = JobRunLogEntrySerializer filterset_fields = ["run", "kind"] diff --git a/batchrun/job_launching.py b/batchrun/job_launching.py index 28e9fe36..aa8dd6df 100644 --- a/batchrun/job_launching.py +++ b/batchrun/job_launching.py @@ -30,7 +30,7 @@ def run_job(job: Job) -> JobRun: launcher = JobRunLauncher(job_run) launcher.start() launcher.join() - return job_run + return job_run # type: ignore class JobRunLauncher(multiprocessing.Process): diff --git a/batchrun/management/commands/batchrun_execute_job_run.py b/batchrun/management/commands/batchrun_execute_job_run.py index b4efdef7..ccedd641 100644 --- a/batchrun/management/commands/batchrun_execute_job_run.py +++ b/batchrun/management/commands/batchrun_execute_job_run.py @@ -16,5 +16,5 @@ def add_arguments(cls, parser: argparse.ArgumentParser) -> None: def handle(self, *args: Any, **options: Any) -> None: job_run_id = options.get("job_run_id") - job_run = JobRun.objects.get(pk=job_run_id) # type: ignore + job_run = JobRun.objects.get(pk=job_run_id) execute_job_run(job_run) diff --git a/batchrun/management/commands/drop_old_log_entries.py b/batchrun/management/commands/drop_old_log_entries.py index ad6f5540..8dad5788 100644 --- a/batchrun/management/commands/drop_old_log_entries.py +++ b/batchrun/management/commands/drop_old_log_entries.py @@ -4,7 +4,7 @@ from dateutil.relativedelta import relativedelta from django.core.management.base import BaseCommand -from ...models import JobRun, JobRunLogEntry +from ...models import JobRun class Command(BaseCommand): @@ -33,13 +33,6 @@ def handle(self, *args: Any, **options: Any) -> None: JobRun.objects.count(), ) ) - log_entries_to_delete = JobRunLogEntry.objects.filter( - run__in=runs_before_cutoff - ) - self.stdout.write( - "Proceeding to delete {} / {} stored JobRunLogEntries".format( - log_entries_to_delete.count(), JobRunLogEntry.objects.count() - ) - ) - log_entries_to_delete.delete() + runs_before_cutoff.delete() + self.stdout.write("Done!") diff --git a/batchrun/migrations/0002_add_safedelete_to_logs.py b/batchrun/migrations/0002_add_safedelete_to_logs.py new file mode 100644 index 00000000..6061f134 --- /dev/null +++ b/batchrun/migrations/0002_add_safedelete_to_logs.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.13 on 2020-12-16 09:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("batchrun", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="jobrun", + name="deleted", + field=models.DateTimeField(editable=False, null=True), + ), + migrations.AddField( + model_name="jobrunlogentry", + name="deleted", + field=models.DateTimeField(editable=False, null=True), + ), + migrations.AlterField( + model_name="jobrunlogentry", + name="run", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="log_entries", + to="batchrun.JobRun", + verbose_name="run", + ), + ), + ] diff --git a/batchrun/models.py b/batchrun/models.py index eb8341a9..c6d10ba0 100644 --- a/batchrun/models.py +++ b/batchrun/models.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext from django.utils.translation import ugettext_lazy as _ from enumfields import EnumField +from safedelete import SOFT_DELETE_CASCADE # type: ignore from safedelete.models import SafeDeleteModel from ._times import utc_now @@ -250,11 +251,13 @@ def update_run_queue(self, max_items_to_create: int = 10) -> None: items.exclude(pk__in=fresh_ids).delete() -class JobRun(models.Model): +class JobRun(SafeDeleteModel): """ Instance of a job currently running or ran in the past. """ + _safedelete_policy = SOFT_DELETE_CASCADE + job = models.ForeignKey(Job, on_delete=models.PROTECT, verbose_name=_("job")) pid = models.IntegerField( null=True, @@ -280,7 +283,7 @@ def __str__(self) -> str: return f"{self.job} [{self.pid}] ({self.started_at:%Y-%m-%dT%H:%M})" -class JobRunLogEntry(models.Model): +class JobRunLogEntry(SafeDeleteModel): """ Entry in a log for a run of a job. @@ -293,7 +296,7 @@ class JobRunLogEntry(models.Model): run = models.ForeignKey( JobRun, - on_delete=models.PROTECT, + on_delete=models.CASCADE, related_name="log_entries", verbose_name=_("run"), ) diff --git a/docker/postgres/Dockerfile b/docker/postgres/Dockerfile index e5d0a718..441cbe0b 100644 --- a/docker/postgres/Dockerfile +++ b/docker/postgres/Dockerfile @@ -1,8 +1,8 @@ -FROM postgres:9.5 +FROM postgres:10.15 RUN apt-get update \ && apt-get install --no-install-recommends -y \ - postgis postgresql-9.5-postgis-2.4 postgresql-9.5-postgis-2.4-scripts + postgis postgresql-10-postgis-2.4 postgresql-10-postgis-2.4-scripts RUN localedef -i fi_FI -c -f UTF-8 -A /usr/share/locale/locale.alias fi_FI.UTF-8 diff --git a/leasing/models/land_area.py b/leasing/models/land_area.py index 9a6ad139..72519627 100644 --- a/leasing/models/land_area.py +++ b/leasing/models/land_area.py @@ -507,6 +507,25 @@ def save(self, *args, **kwargs): if self.id and not self.tracker.changed(): return + # There can be only one master plan unit per lease area and identifier + if self.is_master: + master_plan_unit_count = ( + PlanUnit.objects.filter( + lease_area=self.lease_area, + identifier=self.identifier, + is_master=True, + ) + .exclude(id=self.id) + .count() + ) + if master_plan_unit_count: + raise Exception( + _( + "The master plan unit has already created. " + "There can be only one master plan unit per lease area and identifier." + ) + ) + skip_modified_update = kwargs.pop("skip_modified_update", False) if skip_modified_update: modified_at_field = self._meta.get_field("modified_at") diff --git a/leasing/serializers/invoice.py b/leasing/serializers/invoice.py index 421c28b8..1338ff7f 100644 --- a/leasing/serializers/invoice.py +++ b/leasing/serializers/invoice.py @@ -62,8 +62,8 @@ def __init__(self, instance=None, data=empty, **kwargs): super().__init__(instance=instance, data=data, **kwargs) # Lease field must be added dynamically to prevent circular imports - from leasing.serializers.lease import LeaseSuccinctSerializer from leasing.models.lease import Lease + from leasing.serializers.lease import LeaseSuccinctSerializer self.fields["lease"] = InstanceDictPrimaryKeyRelatedField( instance_class=Lease, @@ -507,8 +507,8 @@ def __init__(self, instance=None, data=empty, **kwargs): super().__init__(instance=instance, data=data, **kwargs) # Lease field must be added dynamically to prevent circular imports - from leasing.serializers.lease import LeaseSuccinctSerializer from leasing.models.lease import Lease + from leasing.serializers.lease import LeaseSuccinctSerializer self.fields["lease"] = InstanceDictPrimaryKeyRelatedField( instance_class=Lease, diff --git a/leasing/tests/models/test_land_area.py b/leasing/tests/models/test_land_area.py new file mode 100644 index 00000000..22c30afd --- /dev/null +++ b/leasing/tests/models/test_land_area.py @@ -0,0 +1,19 @@ +import pytest + + +@pytest.mark.django_db +def test_plan_unit_cannot_create_another_master_plan_unit( + lease_test_data, plan_unit_factory +): + master_plan_unit = plan_unit_factory( + area=100, + lease_area=lease_test_data["lease_area"], + identifier="1234", + is_master=True, + ) + + another_master_plan_unit = master_plan_unit + another_master_plan_unit.pk = None + + with pytest.raises(Exception): + another_master_plan_unit.save() diff --git a/locale/fi/LC_MESSAGES/django.po b/locale/fi/LC_MESSAGES/django.po index b824274a..54500bea 100644 --- a/locale/fi/LC_MESSAGES/django.po +++ b/locale/fi/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: MVJ 0.1\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-11-23 11:25+0200\n" +"POT-Creation-Date: 2021-01-07 15:34+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: \n" "Language: fi\n" @@ -1693,6 +1693,11 @@ msgctxt "Model name" msgid "Plan units" msgstr "Kaavayksiköt" +msgid "" +"The master plan unit has already created. There can be only one master plan " +"unit per lease area and identifier." +msgstr "Alkuperäinen kaavayksikkö on jo luotu. Alkuperäisiä kaavayksiköitä voi olla vain yksi per vuokra-alueen kaavayksikön tunnus." + msgid "Due dates position" msgstr "Eräpäivien sijainti" @@ -1781,6 +1786,14 @@ msgstr "Maankäyttösopimukset" msgid "Estate id" msgstr "Kohteen tunniste" +msgctxt "Model name" +msgid "Land use agreement decision type" +msgstr "" + +msgctxt "Model name" +msgid "Land use agreement decision types" +msgstr "" + msgctxt "Model name" msgid "Land use agreement decision" msgstr "Maankäyttösopimus päätös" @@ -1789,6 +1802,22 @@ msgctxt "Model name" msgid "Land use agreement decisions" msgstr "Maankäyttösopimus päätökset" +msgctxt "Model name" +msgid "Land use agreement decision condition type" +msgstr "" + +msgctxt "Model name" +msgid "Land use agreement decision condition types" +msgstr "" + +msgctxt "Model name" +msgid "Land use agreement decision condition" +msgstr "" + +msgctxt "Model name" +msgid "Land use agreement decision conditions" +msgstr "" + msgctxt "Model name" msgid "Land use agreement address" msgstr "Maankäyttösopimus osoite" @@ -1797,11 +1826,17 @@ msgctxt "Model name" msgid "Land use agreement addresses" msgstr "Maankäyttösopimus osoitteet" -msgid "Numerator" -msgstr "Jaettava" +msgid "Obligated area (f-m2)" +msgstr "" -msgid "Denominator" -msgstr "Jakaja" +msgid "Actualized area (f-m2)" +msgstr "" + +msgid "Subvention amount" +msgstr "Subvention määrä" + +msgid "Compensation percent" +msgstr "" msgctxt "Model name" msgid "Land use agreement litigant" @@ -2075,6 +2110,12 @@ msgctxt "Model name" msgid "Leasehold transfer properties" msgstr "Vuokraoikeuden siirron kohteet" +msgid "Numerator" +msgstr "Jaettava" + +msgid "Denominator" +msgstr "Jakaja" + msgctxt "Model name" msgid "Leasehold transfer party" msgstr "Vuokraoikeuden siirron osapuoli" @@ -2302,9 +2343,6 @@ msgstr "Hallintamuoto (Subventio)" msgid "Rent adjustment" msgstr "Alennus/Korotus" -msgid "Subvention amount" -msgstr "Subvention määrä" - msgctxt "Model name" msgid "Management subvention (Rent adjustment)" msgstr "Hallintamuotosubventio (Alennus)" @@ -2831,12 +2869,12 @@ msgstr "" msgid "from_lease and to_lease cannot be the same Lease" msgstr "Ei voi olla sama vuokraus" -msgid "The target information has changed!" -msgstr "Kohteen tiedot ovat muuttuneet!" - msgid "The target has been removed from the system!" msgstr "Kohde on poistunut järjestelmästä!" +msgid "The target information has changed!" +msgstr "Kohteen tiedot ovat muuttuneet!" + msgid "Fixed initial rent end date must match rent cycle end date" msgstr "Alkuvuosivuokran loppu pitää olla sama kuin vuokrakauden loppu" @@ -3054,6 +3092,15 @@ msgstr "Ei oikeutta lisätä muiden käyttäjien ui dataa" msgid "Data integrity error" msgstr "Tietoeheysvirhe" +msgid "Finnish" +msgstr "Suomi" + +msgid "Swedish" +msgstr "Ruotsi" + +msgid "English" +msgstr "Englanti" + msgid "Sender email address. Example: john@example.com" msgstr "Lähettäjän sähköpostiosoite. Esimerkki: matti@example.com" diff --git a/mvj/settings.py b/mvj/settings.py index 9771b302..4ef4b21b 100644 --- a/mvj/settings.py +++ b/mvj/settings.py @@ -129,6 +129,7 @@ def get_git_revision_hash(): "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.gis", + "rangefilter", "helusers", "crispy_forms", "django_filters", diff --git a/requirements-dev.txt b/requirements-dev.txt index 84e41254..c8861d96 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -27,7 +27,7 @@ importlib-metadata==2.0.0 # via pluggy, pytest inflection==0.3.1 # via pytest-factoryboy ipython-genutils==0.2.0 # via traitlets ipython==7.13.0 # via -r requirements-dev.in -isort==4.3.21 # via -r requirements-dev.in +isort==5.6.4 # via -r requirements-dev.in jdcal==1.4.1 # via openpyxl jedi==0.16.0 # via ipython mccabe==0.6.1 # via flake8 diff --git a/requirements.in b/requirements.in index eb4848be..368e6f34 100644 --- a/requirements.in +++ b/requirements.in @@ -3,6 +3,7 @@ dataclasses Django~=2.2.13 django-anymail django-auditlog +django-admin-rangefilter django-jsonfield-compat django-constance[database] django-cors-headers diff --git a/requirements.txt b/requirements.txt index 0b18598f..a2d39375 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ cryptography==3.2 # via paramiko database-sanitizer==1.1.0 # via django-sanitized-dump dataclasses==0.6 # via -r requirements.in defusedxml==0.6.0 # via zeep +django-admin-rangefilter==0.6.3 # via -r requirements.in django-anymail==7.0.0 # via -r requirements.in django-auditlog==0.4.7 # via -r requirements.in django-constance[database]==2.7.0 # via -r requirements.in diff --git a/setup.cfg b/setup.cfg index 768c312c..cbc1caf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,7 +24,6 @@ multi_line_output=3 line_length = 88 include_trailing_comma = true skip=.tox,dist,venv,docs,migrations,.git -not_skip=__init__.py [pydocstyle] ignore=D100,D104,D105,D200,D203,D400