Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documents for housing companies and apartments #482

Merged
merged 6 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
static
mediaroot
/oracle-data
.pytest_cache
.ruff_cache
Expand Down
21 changes: 21 additions & 0 deletions backend/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ def relativedelta_months(value: int) -> relativedelta:
EMAIL_HOST_USER=(str, ""),
EMAIL_HOST_PASSWORD=(str, ""),
EMAIL_USE_TLS=(bool, True),
AZURE_ACCOUNT_NAME=(str, None),
AZURE_ACCOUNT_KEY=(str, None),
AZURE_CONTAINER=(str, None),
)
env.read_env(BASE_DIR / ".env")

Expand Down Expand Up @@ -156,6 +159,24 @@ def relativedelta_months(value: int) -> relativedelta:
STATIC_ROOT = BASE_DIR / "static"
STATIC_URL = "static/"

# ----- File uploads -----------------------------------------------------------------------------------

MEDIA_ROOT = BASE_DIR / "mediaroot"
MEDIA_URL = "media/"

AZURE_ACCOUNT_NAME = env("AZURE_ACCOUNT_NAME")
AZURE_ACCOUNT_KEY = env("AZURE_ACCOUNT_KEY")
AZURE_CONTAINER = env("AZURE_CONTAINER")
AZURE_URL_EXPIRATION_SECS = 7200
AZURE_CONNECTION_STRING = (
f"DefaultEndpointsProtocol=https;"
f"AccountName={AZURE_ACCOUNT_NAME};"
f"AccountKey={AZURE_ACCOUNT_KEY};"
f"EndpointSuffix=core.windows.net"
)
if AZURE_ACCOUNT_NAME is not None:
DEFAULT_FILE_STORAGE = "storages.backends.azure_storage.AzureStorage"

# ----- Email ------------------------------------------------------------------------------------------

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
Expand Down
7 changes: 7 additions & 0 deletions backend/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ class HitasLogoutCompleteView(views.LogoutCompleteView):
path(f"{settings.STATIC_URL.lstrip('/').rstrip('/')}/<path:path>", serve, {"document_root": settings.STATIC_ROOT}),
]

if settings.DEBUG:
urlpatterns += [
path(
f"{settings.MEDIA_URL.lstrip('/').rstrip('/')}/<path:path>", serve, {"document_root": settings.MEDIA_ROOT}
),
]

if settings.DEBUG_TOOLBAR:
urlpatterns += [
path("__debug__/", include("debug_toolbar.urls")),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Generated by Django 4.2.4 on 2024-06-19 09:32

import uuid

import django.db.models.deletion
import django.db.models.manager
from django.db import migrations, models

import hitas.models._base
import hitas.models.utils


class Migration(migrations.Migration):
dependencies = [
("hitas", "0014_apartment_updated_acquisition_price"),
]

operations = [
migrations.CreateModel(
name="HousingCompanyDocument",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("deleted", models.DateTimeField(db_index=True, editable=False, null=True)),
("deleted_by_cascade", models.BooleanField(default=False, editable=False)),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
("display_name", models.CharField(max_length=1024)),
("original_filename", models.CharField(max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
("modified_at", models.DateTimeField(auto_now=True)),
(
"file",
models.FileField(
upload_to=hitas.models.document.DocumentFilenameGenerator("housingcompany_documents")
),
),
(
"housing_company",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="hitas.housingcompany"
),
),
],
options={
"abstract": False,
},
bases=(
hitas.models._base.PostFetchModelMixin,
hitas.models._base.AuditLogAdditionalDataMixin,
models.Model,
),
managers=[
("objects", django.db.models.manager.Manager()),
("all_objects", django.db.models.manager.Manager()),
],
),
migrations.CreateModel(
name="AparmentDocument",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("deleted", models.DateTimeField(db_index=True, editable=False, null=True)),
("deleted_by_cascade", models.BooleanField(default=False, editable=False)),
("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
("display_name", models.CharField(max_length=1024)),
("original_filename", models.CharField(max_length=255)),
("created_at", models.DateTimeField(auto_now_add=True)),
("modified_at", models.DateTimeField(auto_now=True)),
(
"file",
models.FileField(upload_to=hitas.models.document.DocumentFilenameGenerator("apartment_documents")),
),
(
"apartment",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="documents", to="hitas.apartment"
),
),
],
options={
"abstract": False,
},
bases=(
hitas.models._base.PostFetchModelMixin,
hitas.models._base.AuditLogAdditionalDataMixin,
models.Model,
),
managers=[
("objects", django.db.models.manager.Manager()),
("all_objects", django.db.models.manager.Manager()),
],
),
]
1 change: 1 addition & 0 deletions backend/hitas/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from hitas.models.building import Building
from hitas.models.codes import AbstractCode, ApartmentType, BuildingType, Developer
from hitas.models.condition_of_sale import ConditionOfSale
from hitas.models.document import AparmentDocument, HousingCompanyDocument
from hitas.models.email_template import EmailTemplate
from hitas.models.external_sales_data import ExternalSalesData
from hitas.models.housing_company import (
Expand Down
98 changes: 98 additions & 0 deletions backend/hitas/models/document.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import os
from uuid import uuid4

from crum import get_current_request
from django.db import models
from django.dispatch import receiver
from django.urls import reverse
from django.utils.deconstruct import deconstructible
from safedelete import SOFT_DELETE_CASCADE
from safedelete.signals import pre_softdelete

from hitas.models._base import ExternalSafeDeleteHitasModel


@deconstructible
class DocumentFilenameGenerator:
def __init__(self, directory=""):
self.upload_to_directory = directory

def __call__(self, instance, filename):
_, extension = os.path.splitext(filename)
generated_filename = f"{uuid4()}{extension}"
parent_id = str(instance.get_parent().pk)
return os.path.join(self.upload_to_directory, parent_id, generated_filename)

def __eq__(self, other):
return isinstance(other, DocumentFilenameGenerator) and self.upload_to_directory == other.upload_to_directory


class BaseDocument(ExternalSafeDeleteHitasModel):
_safedelete_policy = SOFT_DELETE_CASCADE

display_name = models.CharField(max_length=1024)
original_filename = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
modified_at = models.DateTimeField(auto_now=True)

class Meta:
abstract = True


class HousingCompanyDocument(BaseDocument):
file = models.FileField(upload_to=DocumentFilenameGenerator("housingcompany_documents"))
housing_company = models.ForeignKey("HousingCompany", on_delete=models.CASCADE, related_name="documents")

def get_parent(self):
return self.housing_company

def get_file_link(self):
request = get_current_request()
return request.build_absolute_uri(
reverse(
"hitas:document-redirect",
kwargs={
"housing_company_uuid": self.housing_company.uuid.hex,
"uuid": self.uuid.hex,
},
)
)


class AparmentDocument(BaseDocument):
file = models.FileField(upload_to=DocumentFilenameGenerator("apartment_documents"))
apartment = models.ForeignKey("Apartment", on_delete=models.CASCADE, related_name="documents")

def get_parent(self):
return self.apartment

def get_file_link(self):
request = get_current_request()
return request.build_absolute_uri(
reverse(
"hitas:document-redirect",
kwargs={
"housing_company_uuid": self.apartment.housing_company.uuid.hex,
"apartment_uuid": self.apartment.uuid.hex,
"uuid": self.uuid.hex,
},
)
)


@receiver(pre_softdelete, sender=HousingCompanyDocument)
@receiver(pre_softdelete, sender=AparmentDocument)
def handle_file_deletion_on_delete(sender, instance, **kwargs):
instance.file.delete(save=False)


@receiver(models.signals.pre_save, sender=HousingCompanyDocument)
@receiver(models.signals.pre_save, sender=AparmentDocument)
def handle_file_deletion_on_save(sender, instance, **kwargs):
try:
old_instance = sender.objects.get(pk=instance.pk)
except sender.DoesNotExist:
return
if old_instance.file.name != instance.file.name:
# Delete the old file before saving the new one so that there are no dangling files
old_instance.file.delete(save=False)
6 changes: 6 additions & 0 deletions backend/hitas/tests/apis/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ def generic(
) -> Response:
response: Response = super().generic(method, path, data, content_type, secure=False, **kwargs)

if content_type and content_type.startswith("multipart/form-data"):
# openapi-core 0.16.6 can not validate multipart/form-data requests
# and there are issues with upgrading to a newer openapi-core versions.
# Until the openapi-core can be upgraded, disable request validation for file uploads.
openapi_validate_request = False

validate_openapi(
data,
response,
Expand Down
2 changes: 2 additions & 0 deletions backend/hitas/tests/apis/test_api_apartment.py
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ def test__api__apartment__retrieve(api_client: HitasAPIClient):
},
],
},
"documents": [],
"notes": ap1.notes,
"conditions_of_sale": [
{
Expand Down Expand Up @@ -1979,6 +1980,7 @@ def test__api__apartment__update(api_client: HitasAPIClient, minimal_data: bool)
"construction_price_index": [],
"market_price_index": [],
},
"documents": [],
"notes": None,
"ownerships": [],
"prices": {
Expand Down
Loading
Loading