From a4fd0f3b2c134aa0b22e9c1128e3616322ba6744 Mon Sep 17 00:00:00 2001 From: NC-jsAhonen <83913239+NC-jsAhonen@users.noreply.github.com> Date: Mon, 20 Jan 2025 15:52:13 +0200 Subject: [PATCH] private file field and private files directory (#814) --- .gitignore | 1 + forms/migrations/0027_private_filefield.py | 26 +++++ forms/models/form.py | 3 +- ...add_default_collection_letter_templates.py | 7 +- leasing/migrations/0089_private_filefield.py | 96 +++++++++++++++++++ leasing/models/debt_collection.py | 7 +- .../models/infill_development_compensation.py | 3 +- leasing/models/inspection.py | 3 +- leasing/models/land_area.py | 3 +- leasing/models/land_use_agreement.py | 3 +- mvj/settings.py | 2 + .../migrations/0042_private_filefield.py | 40 ++++++++ plotsearch/models/plot_search.py | 9 +- utils/models/fields.py | 17 ++++ utils/viewsets/mixins.py | 2 +- 15 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 forms/migrations/0027_private_filefield.py create mode 100644 leasing/migrations/0089_private_filefield.py create mode 100644 plotsearch/migrations/0042_private_filefield.py create mode 100644 utils/models/fields.py diff --git a/.gitignore b/.gitignore index 7b553b69..d2e0785b 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ /.idea/ /.tox/ /media/ +/private_files/ /static/ /venv/ /laske_export_files/ diff --git a/forms/migrations/0027_private_filefield.py b/forms/migrations/0027_private_filefield.py new file mode 100644 index 00000000..13297b54 --- /dev/null +++ b/forms/migrations/0027_private_filefield.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.16 on 2025-01-14 07:42 + +import django.core.files.storage +from django.db import migrations +import forms.models.form +import utils.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("forms", "0026_alter_answer_user_alter_attachment_user"), + ] + + operations = [ + migrations.AlterField( + model_name="attachment", + name="attachment", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=forms.models.form.get_attachment_file_upload_to, + ), + ), + ] diff --git a/forms/models/form.py b/forms/models/form.py index 5c362193..9a55eb0c 100755 --- a/forms/models/form.py +++ b/forms/models/form.py @@ -5,6 +5,7 @@ from helsinki_gdpr.models import SerializableMixin from users.models import User +from utils.models.fields import PrivateFileField from ..enums import ApplicantType, FormState, SectionType from ..utils import clone_object, generate_unique_identifier @@ -209,7 +210,7 @@ def get_attachment_file_upload_to(instance, filename): class Attachment(SerializableMixin, models.Model): name = models.CharField(max_length=255) - attachment = models.FileField(upload_to=get_attachment_file_upload_to) + attachment = PrivateFileField(upload_to=get_attachment_file_upload_to) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Time created")) path = models.TextField(null=True, blank=True) diff --git a/leasing/management/commands/add_default_collection_letter_templates.py b/leasing/management/commands/add_default_collection_letter_templates.py index c97c59ac..f07c79a8 100644 --- a/leasing/management/commands/add_default_collection_letter_templates.py +++ b/leasing/management/commands/add_default_collection_letter_templates.py @@ -84,7 +84,8 @@ def check_is_directory_writable(self, directory): def handle(self, *args, **options): destination_path = ( - Path(settings.MEDIA_ROOT) / CollectionLetterTemplate.file.field.upload_to + Path(settings.PRIVATE_FILES_LOCATION) + / CollectionLetterTemplate.file.field.upload_to ) if not self.check_is_directory_writable(destination_path): raise CommandError( @@ -123,7 +124,9 @@ def handle(self, *args, **options): name=name, file=str(destination_filename) ) - destination_path = Path(settings.MEDIA_ROOT) / destination_filename + destination_path = ( + Path(settings.PRIVATE_FILES_LOCATION) / destination_filename + ) self.stdout.write( ' Copying "{}" to "{}"'.format(source_filename, destination_path) diff --git a/leasing/migrations/0089_private_filefield.py b/leasing/migrations/0089_private_filefield.py new file mode 100644 index 00000000..2808d20e --- /dev/null +++ b/leasing/migrations/0089_private_filefield.py @@ -0,0 +1,96 @@ +# Generated by Django 4.2.16 on 2025-01-14 07:42 + +import django.core.files.storage +from django.db import migrations +import leasing.models.debt_collection +import leasing.models.infill_development_compensation +import leasing.models.inspection +import leasing.models.land_area +import leasing.models.land_use_agreement +import utils.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "leasing", + "0088_remove_rent_old_dwellings_in_housing_companies_price_index_type_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="collectioncourtdecision", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=leasing.models.debt_collection.get_collection_court_decision_file_upload_to, + verbose_name="File", + ), + ), + migrations.AlterField( + model_name="collectionletter", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=leasing.models.debt_collection.get_collection_letter_file_upload_to, + verbose_name="File", + ), + ), + migrations.AlterField( + model_name="collectionlettertemplate", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to="collection_letter_templates/", + verbose_name="File", + ), + ), + migrations.AlterField( + model_name="infilldevelopmentcompensationattachment", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=leasing.models.infill_development_compensation.get_attachment_file_upload_to, + ), + ), + migrations.AlterField( + model_name="inspectionattachment", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=leasing.models.inspection.get_inspection_attachment_file_upload_to, + ), + ), + migrations.AlterField( + model_name="landuseagreementattachment", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=leasing.models.land_use_agreement.get_attachment_file_upload_to, + ), + ), + migrations.AlterField( + model_name="leaseareaattachment", + name="file", + field=utils.models.fields.PrivateFileField( + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=leasing.models.land_area.get_attachment_file_upload_to, + ), + ), + ] diff --git a/leasing/models/debt_collection.py b/leasing/models/debt_collection.py index 82c3e0f7..75c580b2 100644 --- a/leasing/models/debt_collection.py +++ b/leasing/models/debt_collection.py @@ -9,6 +9,7 @@ from field_permissions.registry import field_permissions from leasing.models.mixins import TimeStampedSafeDeleteModel from users.models import User +from utils.models.fields import PrivateFileField def get_collection_letter_file_upload_to(instance, filename): @@ -28,7 +29,7 @@ class CollectionLetter(TimeStampedSafeDeleteModel): ) # In Finnish: Tiedosto - file = models.FileField( + file = PrivateFileField( upload_to=get_collection_letter_file_upload_to, verbose_name=_("File"), blank=False, @@ -59,7 +60,7 @@ class CollectionLetterTemplate(TimeStampedSafeDeleteModel): """ name = models.CharField(verbose_name=_("Name"), max_length=255) - file = models.FileField( + file = PrivateFileField( upload_to="collection_letter_templates/", verbose_name=_("File"), blank=False, @@ -126,7 +127,7 @@ class CollectionCourtDecision(TimeStampedSafeDeleteModel): ) # In Finnish: Tiedosto - file = models.FileField( + file = PrivateFileField( upload_to=get_collection_court_decision_file_upload_to, verbose_name=_("File"), blank=False, diff --git a/leasing/models/infill_development_compensation.py b/leasing/models/infill_development_compensation.py index 5ad9a55c..16f9e8d8 100644 --- a/leasing/models/infill_development_compensation.py +++ b/leasing/models/infill_development_compensation.py @@ -9,6 +9,7 @@ from leasing.models.decision import DecisionMaker from leasing.models.lease import IntendedUse from users.models import User +from utils.models.fields import PrivateFileField from .mixins import TimeStampedSafeDeleteModel @@ -297,7 +298,7 @@ class InfillDevelopmentCompensationAttachment(TimeStampedSafeDeleteModel): ) # In Finnish: Tiedosto - file = models.FileField( + file = PrivateFileField( upload_to=get_attachment_file_upload_to, blank=False, null=False ) diff --git a/leasing/models/inspection.py b/leasing/models/inspection.py index d766acee..5279f99d 100644 --- a/leasing/models/inspection.py +++ b/leasing/models/inspection.py @@ -6,6 +6,7 @@ from field_permissions.registry import field_permissions from leasing.models.mixins import TimeStampedSafeDeleteModel from users.models import User +from utils.models.fields import PrivateFileField class Inspection(models.Model): @@ -59,7 +60,7 @@ class InspectionAttachment(TimeStampedSafeDeleteModel): ) # In Finnish: Tiedosto - file = models.FileField( + file = PrivateFileField( upload_to=get_inspection_attachment_file_upload_to, blank=False, null=False ) diff --git a/leasing/models/land_area.py b/leasing/models/land_area.py index 6aa304c9..cc654143 100755 --- a/leasing/models/land_area.py +++ b/leasing/models/land_area.py @@ -24,6 +24,7 @@ from leasing.models.lease import Lease from leasing.models.utils import normalize_identifier from users.models import User +from utils.models.fields import PrivateFileField from .mixins import ( ArchivableModel, @@ -335,7 +336,7 @@ class LeaseAreaAttachment(TimeStampedSafeDeleteModel): type = EnumField(LeaseAreaAttachmentType, verbose_name=_("Type"), max_length=30) # In Finnish: Tiedosto - file = models.FileField( + file = PrivateFileField( upload_to=get_attachment_file_upload_to, blank=False, null=False ) diff --git a/leasing/models/land_use_agreement.py b/leasing/models/land_use_agreement.py index 0d8783b4..5bb9498d 100644 --- a/leasing/models/land_use_agreement.py +++ b/leasing/models/land_use_agreement.py @@ -23,6 +23,7 @@ from leasing.models.lease import District, Municipality from leasing.utils import calculate_increase_with_360_day_calendar from users.models import User +from utils.models.fields import PrivateFileField from .mixins import NameModel, TimeStampedSafeDeleteModel @@ -334,7 +335,7 @@ class LandUseAgreementAttachment(TimeStampedSafeDeleteModel): ) # In Finnish: Tiedosto - file = models.FileField( + file = PrivateFileField( upload_to=get_attachment_file_upload_to, blank=False, null=False ) diff --git a/mvj/settings.py b/mvj/settings.py index 46e4ac60..f0ef2ab8 100644 --- a/mvj/settings.py +++ b/mvj/settings.py @@ -138,6 +138,8 @@ def get_git_revision_hash(): MEDIA_ROOT = project_root("media") STATIC_ROOT = project_root("static") +PRIVATE_FILES_LOCATION = project_root("private_files") + MEDIA_URL = "/media/" STATIC_URL = "/static/" diff --git a/plotsearch/migrations/0042_private_filefield.py b/plotsearch/migrations/0042_private_filefield.py new file mode 100644 index 00000000..c78f80df --- /dev/null +++ b/plotsearch/migrations/0042_private_filefield.py @@ -0,0 +1,40 @@ +# Generated by Django 4.2.16 on 2025-01-14 07:42 + +import django.core.files.storage +from django.db import migrations +import plotsearch.models.plot_search +import utils.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ("plotsearch", "0041_alter_areasearch_options"), + ] + + operations = [ + migrations.AlterField( + model_name="areasearchattachment", + name="attachment", + field=utils.models.fields.PrivateFileField( + blank=True, + null=True, + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=plotsearch.models.plot_search.get_area_search_attachment_upload_to, + ), + ), + migrations.AlterField( + model_name="meetingmemo", + name="meeting_memo", + field=utils.models.fields.PrivateFileField( + blank=True, + null=True, + storage=django.core.files.storage.FileSystemStorage( + base_url="/attachments/", location="attachments" + ), + upload_to=plotsearch.models.plot_search.get_meeting_memo_file_upload_to, + ), + ), + ] diff --git a/plotsearch/models/plot_search.py b/plotsearch/models/plot_search.py index 62882542..08c47e0c 100755 --- a/plotsearch/models/plot_search.py +++ b/plotsearch/models/plot_search.py @@ -34,6 +34,7 @@ ) from plotsearch.utils import map_intended_use_to_lessor from users.models import User +from utils.models.fields import PrivateFileField class PlotSearchType(NameModel): @@ -404,7 +405,7 @@ def get_meeting_memo_file_upload_to(instance, filename): class MeetingMemo(models.Model): # In Finnish: Kokousmuistio name = models.CharField(max_length=255) - meeting_memo = models.FileField( + meeting_memo = PrivateFileField( upload_to=get_meeting_memo_file_upload_to, null=True, blank=True ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Time created")) @@ -625,8 +626,10 @@ def get_area_search_attachment_upload_to(instance, filename): class AreaSearchAttachment(SerializableMixin, NameModel): - attachment = models.FileField( - upload_to=get_area_search_attachment_upload_to, null=True, blank=True + attachment = PrivateFileField( + upload_to=get_area_search_attachment_upload_to, + null=True, + blank=True, ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Time created")) diff --git a/utils/models/fields.py b/utils/models/fields.py new file mode 100644 index 00000000..63473138 --- /dev/null +++ b/utils/models/fields.py @@ -0,0 +1,17 @@ +from typing import Any + +from django.conf import settings +from django.core.files.storage import FileSystemStorage +from django.db import models + + +class PrivateFileSystemStorage(FileSystemStorage): + def __init__(self) -> None: + # base_url is not needed, but it defaults to MEDIA_URL if not explicitly set + super().__init__(location=settings.PRIVATE_FILES_LOCATION, base_url=None) + + +class PrivateFileField(models.FileField): + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.storage = PrivateFileSystemStorage() diff --git a/utils/viewsets/mixins.py b/utils/viewsets/mixins.py index 3ab68c22..50bc4eee 100644 --- a/utils/viewsets/mixins.py +++ b/utils/viewsets/mixins.py @@ -18,7 +18,7 @@ def download(self, request, pk=None, file_field: str | None = None): filename = getattr(obj, file_field).name else: filename = obj.file.name - filepath = "/".join([settings.MEDIA_ROOT, filename]) + filepath = "/".join([settings.PRIVATE_FILES_LOCATION, filename]) response = FileResponse(open(filepath, "rb"), as_attachment=True) return response