From fa0239328452fbdcb6720e7fe7ebce910777d5e1 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 5 Jun 2023 09:15:29 +0200 Subject: [PATCH 01/69] Removed libqfieldsync changes --- docker-app/qfieldcloud/core/models.py | 30 ++++++++++++++----- docker-app/qfieldcloud/core/utils2/storage.py | 7 +++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 98140be4f..802ff20eb 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime, timedelta from enum import Enum -from typing import List, Optional, cast +from typing import List, Optional, Union, cast import django_cryptography.fields from deprecated import deprecated @@ -362,7 +362,6 @@ def save(self, *args, **kwargs): class UserAccount(models.Model): - NOTIFS_IMMEDIATELY = timedelta(minutes=0) NOTIFS_HOURLY = timedelta(hours=1) NOTIFS_DAILY = timedelta(days=1) @@ -717,7 +716,6 @@ def delete(self, *args, **kwargs): class OrganizationMember(models.Model): - objects = OrganizationMemberQueryset.as_manager() class Roles(models.TextChoices): @@ -802,7 +800,6 @@ class Meta: class Team(User): - team_organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -1036,10 +1033,18 @@ class Meta: "If enabled, QFieldCloud will automatically overwrite conflicts in this project. Disabling this will force the project manager to manually resolve all the conflicts." ), ) + thumbnail_uri = models.CharField( _("Thumbnail Picture URI"), max_length=255, blank=True ) + storage_keep_versions = models.PositiveIntegerField( + default=10, + help_text=( + "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead." + ), + ) + @property def thumbnail_url(self): if self.thumbnail_uri: @@ -1246,6 +1251,20 @@ def save(self, recompute_storage=False, *args, **kwargs): self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) super().save(*args, **kwargs) + @property + def storage_versions(self) -> int: + return min( + self.storage_keep_versions, + self.owner.useraccount.current_subscription.plan.storage_keep_versions, + ) + + @storage_versions.setter + def keep_storage(self, value: Union[int, str]): + self.storage_keep_versions = min( + value if isinstance(value, int) else int(value), + self.owner.useraccount.current_subscription.plan.storage_keep_versions, + ) + class ProjectCollaboratorQueryset(models.QuerySet): def validated(self, skip_invalid=False): @@ -1386,7 +1405,6 @@ def clean(self) -> None: elif self.collaborator.is_team: team_qs = organization.teams.filter(pk=self.collaborator) if not team_qs.exists(): - raise ValidationError(_("Team does not exist.")) return super().clean() @@ -1504,7 +1522,6 @@ def method(self): class Job(models.Model): - objects = InheritanceManager() class Type(models.TextChoices): @@ -1623,7 +1640,6 @@ class Meta: class ApplyJob(Job): - deltas_to_apply = models.ManyToManyField( to=Delta, through="ApplyJobDelta", diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index bf215c5ea..c189e8f38 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -404,9 +404,10 @@ def purge_old_file_versions( logger.info(f"Cleaning up old files for {project}") - # Number of versions to keep is determined by the account type - keep_count = ( - project.owner.useraccount.current_subscription.plan.storage_keep_versions + # Number of versions to keep is determined by the account type and project configuration + keep_count = min( + project.owner.useraccount.current_subscription.plan.storage_keep_versions, + project.storage_keep_versions, ) logger.debug(f"Keeping {keep_count} versions") From 56343cbf3d9ae6c3eccd9730e02321083ca9455d Mon Sep 17 00:00:00 2001 From: Isabel Kiefer Date: Wed, 31 May 2023 09:38:04 +0200 Subject: [PATCH 02/69] Update LICENSE update (c) year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 616945945..fea4b63a1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 OPENGIS.ch +Copyright (c) 2023 OPENGIS.ch Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From a50814846acdef43051c832810ab020df604cf8b Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 1 Jun 2023 15:34:24 +0200 Subject: [PATCH 03/69] Debug `EmptyContentError: Empty content` (#662) * Removed libqfieldsync changes * Update docker-app/qfieldcloud/core/utils2/sentry.py Co-authored-by: Matthias Kuhn * Accomodating comments * doctrings * Update docker-app/qfieldcloud/core/utils2/sentry.py Co-authored-by: Matthias Kuhn --------- Co-authored-by: Matthias Kuhn --- docker-app/qfieldcloud/core/exceptions.py | 9 ++++ .../qfieldcloud/core/middleware/requests.py | 19 +++++++ .../qfieldcloud/core/tests/test_qgis_file.py | 36 +++++++++++++ docker-app/qfieldcloud/core/utils2/sentry.py | 36 +++++++++++++ .../qfieldcloud/core/views/files_views.py | 53 ++++++++++++++----- docker-app/qfieldcloud/settings.py | 3 +- 6 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 docker-app/qfieldcloud/core/middleware/requests.py create mode 100644 docker-app/qfieldcloud/core/utils2/sentry.py diff --git a/docker-app/qfieldcloud/core/exceptions.py b/docker-app/qfieldcloud/core/exceptions.py index 63a2855f5..125c578a8 100644 --- a/docker-app/qfieldcloud/core/exceptions.py +++ b/docker-app/qfieldcloud/core/exceptions.py @@ -75,6 +75,15 @@ class EmptyContentError(QFieldCloudException): status_code = status.HTTP_503_SERVICE_UNAVAILABLE +class MultipleContentsError(QFieldCloudException): + """Raised when a request contains multiple files + (i.e. when it should contain at most one)""" + + code = "multiple_contents" + message = "Multiple contents" + status_code = status.HTTP_503_SERVICE_UNAVAILABLE + + class ObjectNotFoundError(QFieldCloudException): """Raised when a requested object doesn't exist (e.g. wrong project id into the request)""" diff --git a/docker-app/qfieldcloud/core/middleware/requests.py b/docker-app/qfieldcloud/core/middleware/requests.py new file mode 100644 index 000000000..e470877b2 --- /dev/null +++ b/docker-app/qfieldcloud/core/middleware/requests.py @@ -0,0 +1,19 @@ +def attach_keys(get_response): + """ + QF-2540 + Annotate request with a str representation of relevant fields, so as to obtain a diff + by comparing with the post-serialized request later in the callstack. + """ + + def middleware(request): + request_attributes = { + "file_key": str(request.FILES.keys()), + "meta": str(request.META), + } + if "file" in request.FILES.keys(): + request_attributes["files"] = request.FILES.getlist("file") + request.attached_keys = str(request_attributes) + response = get_response(request) + return response + + return middleware diff --git a/docker-app/qfieldcloud/core/tests/test_qgis_file.py b/docker-app/qfieldcloud/core/tests/test_qgis_file.py index 701035fc8..c072a1059 100644 --- a/docker-app/qfieldcloud/core/tests/test_qgis_file.py +++ b/docker-app/qfieldcloud/core/tests/test_qgis_file.py @@ -74,6 +74,42 @@ def test_push_file(self): self.assertTrue(status.is_success(response.status_code)) self.assertEqual(Project.objects.get(pk=self.project1.pk).files_count, 1) + def test_push_multiple_files(self): + self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token1.key) + + self.assertEqual(Project.objects.get(pk=self.project1.pk).files_count, 0) + self.assertEqual( + Project.objects.get(pk=self.project1.pk).project_filename, None + ) + + # Upload multiple files + file1 = io.FileIO(testdata_path("DCIM/1.jpg"), "rb") + file2 = io.FileIO(testdata_path("DCIM/2.jpg"), "rb") + file3 = io.FileIO(testdata_path("file.txt"), "rb") + data = {"file": [file1, file2, file3]} + + should_fail_on_multiple = self.client.post( + f"/api/v1/files/{self.project1.id}/file.txt/", + data=data, + format="multipart", + ) + should_fail_on_empty = self.client.post( + f"/api/v1/files/{self.project1.id}/file.txt/", + data={"file": []}, + format="multipart", + ) + + # Assert that it didn't work + with self.subTest(): + self.assertEqual( + should_fail_on_multiple.json()["code"], "multiple_contents" + ) + self.assertEqual(should_fail_on_empty.json()["code"], "empty_content") + self.assertEqual(Project.objects.get(pk=self.project1.pk).files_count, 0) + self.assertEqual( + Project.objects.get(pk=self.project1.pk).project_filename, None + ) + def test_push_download_file(self): self.client.credentials(HTTP_AUTHORIZATION="Token " + self.token1.key) diff --git a/docker-app/qfieldcloud/core/utils2/sentry.py b/docker-app/qfieldcloud/core/utils2/sentry.py new file mode 100644 index 000000000..ab9910c48 --- /dev/null +++ b/docker-app/qfieldcloud/core/utils2/sentry.py @@ -0,0 +1,36 @@ +import logging +from io import StringIO + +import sentry_sdk + +logger = logging.getLogger(__name__) + + +def report_serialization_diff_to_sentry( + name: str, pre_serialization: str, post_serialization: str, buffer: StringIO +): + """ + Sends a report to sentry to debug QF-2540. The report includes request information from before and after middleware handle the request as well as a traceback. + + Args: + pre_serialization: str representing the request `files` keys and meta information before serialization and middleware. + post_serialization: str representing the request `files` keys and meta information after serialization and middleware. + buffer: StringIO buffer from which to extract traceback capturing callstack ahead of the calling function. + """ + + traceback = bytes(buffer.getvalue(), encoding="utf-8") + report = f"Pre:\n{pre_serialization}\n\nPost:{post_serialization}" + + with sentry_sdk.configure_scope() as scope: + try: + filename = f"{name}_contents.txt" + scope.add_attachment(bytes=bytes(report), filename=filename) + + filename = f"{name}_traceback.txt" + scope.add_attachment( + bytes=traceback, + filename=filename, + ) + except Exception as error: + sentry_sdk.capture_exception(error) + logging.error(f"Unable to send file to Sentry: failed on {error}") diff --git a/docker-app/qfieldcloud/core/views/files_views.py b/docker-app/qfieldcloud/core/views/files_views.py index 4a81471db..f2181a43c 100644 --- a/docker-app/qfieldcloud/core/views/files_views.py +++ b/docker-app/qfieldcloud/core/views/files_views.py @@ -1,5 +1,8 @@ +import copy +import io import logging from pathlib import PurePath +from traceback import print_stack import qfieldcloud.core.utils2 as utils2 from django.db import transaction @@ -8,6 +11,7 @@ from qfieldcloud.core.models import Job, ProcessProjectfileJob, Project from qfieldcloud.core.utils import S3ObjectVersion, get_project_file_with_versions from qfieldcloud.core.utils2.audit import LogEntry, audit +from qfieldcloud.core.utils2.sentry import report_serialization_diff_to_sentry from qfieldcloud.core.utils2.storage import ( get_attachment_dir_prefix, purge_old_file_versions, @@ -135,25 +139,48 @@ def get(self, request, projectid, filename): # TODO refactor this function by moving the actual upload and Project model updates to library function outside the view def post(self, request, projectid, filename, format=None): - project = Project.objects.get(id=projectid) + + if len(request.FILES.getlist("file")) > 1: + raise exceptions.MultipleContentsError() + + # QF-2540 + # Getting traceback in case the traceback provided by Sentry is too short + # Add post-serialization keys for diff-ing with pre-serialization keys + buffer = io.StringIO() + print_stack(limit=50, file=buffer) + request_attributes = { + "data": str(copy.copy(self.request.data).keys()), + "files": str(self.request.FILES.keys()), + "meta": str(self.request.META), + } + missing_error = "" if "file" not in request.data: - logger.info( - 'The key "file" was not found in `request.data`.', - extra={ - "data_for_key_text_content": str(request.data.get("text", ""))[ - :1000 - ], - "data_for_key_text_len": len(request.data.get("text", "")), - "request_content_length": request.META.get("CONTENT_LENGTH"), - "request_content_type": request.META.get("CONTENT_TYPE"), - "request_data": list(request.data.keys()), - "request_files": list(request.FILES.keys()), - }, + missing_error = 'The key "file" was not found in `request.data`. Sending report to Sentry.' + + if not request.FILES.getlist("file"): + missing_error = 'The key "file" occurs in `request.data` but maps to an empty list. Sending report to Sentry.' + + if missing_error: + logging.warning(missing_error) + + # QF-2540 + report_serialization_diff_to_sentry( + # using the 'X-Request-Id' added to the request by RequestIDMiddleware + name=f"{request.META.get('X-Request-Id')}_{projectid}", + pre_serialization=request.attached_keys, + post_serialization=str(request_attributes), + buffer=buffer, ) raise exceptions.EmptyContentError() + # get project from request or db + if hasattr(request, "project"): + project = request.project + else: + project = Project.objects.get(id=projectid) is_qgis_project_file = utils.is_qgis_project_file(filename) + # check only one qgs/qgz file per project if ( is_qgis_project_file diff --git a/docker-app/qfieldcloud/settings.py b/docker-app/qfieldcloud/settings.py index 389f83192..5fc7e4859 100644 --- a/docker-app/qfieldcloud/settings.py +++ b/docker-app/qfieldcloud/settings.py @@ -109,7 +109,6 @@ ] MIDDLEWARE = [ - "log_request_id.middleware.RequestIDMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", @@ -117,6 +116,8 @@ "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", + "qfieldcloud.core.middleware.requests.attach_keys", # QF-2540: Inspecting request after Django middlewares + "log_request_id.middleware.RequestIDMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", "django_currentuser.middleware.ThreadLocalUserMiddleware", From c3afd7551c003b3ab01b55f311fd7d23bab9dfaf Mon Sep 17 00:00:00 2001 From: faebebin Date: Mon, 5 Jun 2023 07:59:16 +0200 Subject: [PATCH 04/69] No subscriptions managed_by AnonymousUser --- docker-app/qfieldcloud/subscription/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/subscription/models.py b/docker-app/qfieldcloud/subscription/models.py index 3d9655d86..3c03dbd46 100644 --- a/docker-app/qfieldcloud/subscription/models.py +++ b/docker-app/qfieldcloud/subscription/models.py @@ -378,9 +378,13 @@ def managed_by(self, user_id: int): Args: user_id (int): the user we are searching against """ + if not user_id: + # NOTE: for logged out AnonymousUser the user_id is None + return self.none() + return self.filter( Q(account_id=user_id) - | Q(account_id__user__organization__organization_owner_id=user_id) + | Q(account__user__organization__organization_owner_id=user_id) ) def activeness(self): From 6309d05bd5171e95f81a531ebbbf4a46d1cdce4f Mon Sep 17 00:00:00 2001 From: Adrien Date: Mon, 5 Jun 2023 09:36:23 +0200 Subject: [PATCH 05/69] QF-2700 Run makemigrations --check right after building (#675) * Removed libqfieldsync changes * fixed by removing staticmethod * Removed libqfieldsync changes --- .github/workflows/test.yml | 1 + .../qfieldcloud/core/migrations/0009_geodb.py | 8 +-- docker-app/qfieldcloud/core/models.py | 58 +++++++++---------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eb2e7db3e..a51d640b0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,6 +54,7 @@ jobs: - name: Initial manage.py commands run: | + docker compose run app python manage.py makemigrations --no-input --check docker compose run app python manage.py migrate docker compose run app python manage.py collectstatic - name: Run unit and integration tests diff --git a/docker-app/qfieldcloud/core/migrations/0009_geodb.py b/docker-app/qfieldcloud/core/migrations/0009_geodb.py index c775812cb..2edbeca84 100644 --- a/docker-app/qfieldcloud/core/migrations/0009_geodb.py +++ b/docker-app/qfieldcloud/core/migrations/0009_geodb.py @@ -28,28 +28,28 @@ class Migration(migrations.Migration): ( "username", models.CharField( - default=qfieldcloud.core.models.Geodb.random_string, + default=qfieldcloud.core.models.random_string, max_length=255, ), ), ( "dbname", models.CharField( - default=qfieldcloud.core.models.Geodb.random_string, + default=qfieldcloud.core.models.random_string, max_length=255, ), ), ( "hostname", models.CharField( - default=qfieldcloud.core.models.Geodb.default_hostname, + default=qfieldcloud.core.models.default_hostname, max_length=255, ), ), ( "port", models.PositiveIntegerField( - default=qfieldcloud.core.models.Geodb.default_port + default=qfieldcloud.core.models.default_port ), ), ("created_at", models.DateTimeField(auto_now_add=True)), diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 802ff20eb..b944cba18 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -509,40 +509,40 @@ def __str__(self) -> str: return f"{self.user.username_with_full_name} ({self.__class__.__name__})" -class Geodb(models.Model): - @staticmethod - def random_string(): - """Generate random sting starting with a lowercase letter and then - lowercase letters and digits""" - - first_letter = secrets.choice(string.ascii_lowercase) - letters_and_digits = string.ascii_lowercase + string.digits - secure_str = first_letter + "".join( - secrets.choice(letters_and_digits) for i in range(15) - ) - return secure_str +def random_string() -> str: + """Generate random sting starting with a lowercase letter and then + lowercase letters and digits""" + + first_letter = secrets.choice(string.ascii_lowercase) + letters_and_digits = string.ascii_lowercase + string.digits + secure_str = first_letter + "".join( + secrets.choice(letters_and_digits) for i in range(15) + ) + return secure_str - @staticmethod - def random_password(): - """Generate secure random password composed of - letters, digits and special characters""" - password_characters = ( - string.ascii_letters + string.digits + "!#$%&()*+,-.:;<=>?@[]_{}~" - ) - secure_str = "".join(secrets.choice(password_characters) for i in range(16)) - return secure_str +def random_password() -> str: + """Generate secure random password composed of + letters, digits and special characters""" - @staticmethod - def default_hostname(): - return os.environ.get("GEODB_HOST") + password_characters = ( + string.ascii_letters + string.digits + "!#$%&()*+,-.:;<=>?@[]_{}~" + ) + secure_str = "".join(secrets.choice(password_characters) for i in range(16)) + return secure_str - @staticmethod - def default_port(): - return os.environ.get("GEODB_PORT") - user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) +def default_hostname() -> str: + return os.environ.get("GEODB_HOST") + + +def default_port() -> str: + return os.environ.get("GEODB_PORT") + +class Geodb(models.Model): + + user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) username = models.CharField(blank=False, max_length=255, default=random_string) dbname = models.CharField(blank=False, max_length=255, default=random_string) hostname = models.CharField(blank=False, max_length=255, default=default_hostname) @@ -559,7 +559,7 @@ def __init__(self, *args, password="", **kwargs) -> None: self.password = password if not self.password: - self.password = Geodb.random_password() + self.password = random_password() def size(self): try: From 81753595ad510a2bc566e573950d67ae90433287 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 5 Jun 2023 14:32:07 +0200 Subject: [PATCH 06/69] Clean start due to permission error poisoning previous HEAD --- .../0069_project_storage_keep_versions.py | 18 ++++++++++++++++++ docker-app/qfieldcloud/core/models.py | 19 ++++++++++++------- docker-app/qfieldcloud/core/utils2/storage.py | 2 ++ 3 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py diff --git a/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py b/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py new file mode 100644 index 000000000..ffcbb596c --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-06-05 12:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0068_job_container_id'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='storage_keep_versions', + field=models.PositiveIntegerField(default=10, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead."), + ), + ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 1da4a9ef9..02a6df8ac 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -362,7 +362,6 @@ def save(self, *args, **kwargs): class UserAccount(models.Model): - NOTIFS_IMMEDIATELY = timedelta(minutes=0) NOTIFS_HOURLY = timedelta(hours=1) NOTIFS_DAILY = timedelta(days=1) @@ -542,7 +541,6 @@ def default_port() -> str: class Geodb(models.Model): - user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) username = models.CharField(blank=False, max_length=255, default=random_string) dbname = models.CharField(blank=False, max_length=255, default=random_string) @@ -717,7 +715,6 @@ def delete(self, *args, **kwargs): class OrganizationMember(models.Model): - objects = OrganizationMemberQueryset.as_manager() class Roles(models.TextChoices): @@ -802,7 +799,6 @@ class Meta: class Team(User): - team_organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -1040,6 +1036,13 @@ class Meta: _("Thumbnail Picture URI"), max_length=255, blank=True ) + storage_keep_versions = models.PositiveIntegerField( + default=10, + help_text=( + "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead." + ), + ) + @property def thumbnail_url(self): if self.thumbnail_uri: @@ -1244,6 +1247,11 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) + + self.storage_keep_versions = min( + self.storage_keep_versions, + self.owner.useraccount.current_subscription.plan.storage_keep_versions + ) super().save(*args, **kwargs) @@ -1386,7 +1394,6 @@ def clean(self) -> None: elif self.collaborator.is_team: team_qs = organization.teams.filter(pk=self.collaborator) if not team_qs.exists(): - raise ValidationError(_("Team does not exist.")) return super().clean() @@ -1504,7 +1511,6 @@ def method(self): class Job(models.Model): - objects = InheritanceManager() class Type(models.TextChoices): @@ -1623,7 +1629,6 @@ class Meta: class ApplyJob(Job): - deltas_to_apply = models.ManyToManyField( to=Delta, through="ApplyJobDelta", diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index bf215c5ea..66215fc23 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -406,9 +406,11 @@ def purge_old_file_versions( # Number of versions to keep is determined by the account type keep_count = ( + project.storage_keep_versions, project.owner.useraccount.current_subscription.plan.storage_keep_versions ) + logger.debug(f"Keeping {keep_count} versions") # Process file by file From c1c6d06292da0c2dbbb29d81b380084eecd71820 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 5 Jun 2023 15:25:51 +0200 Subject: [PATCH 07/69] Format --- .../0069_project_storage_keep_versions.py | 11 +++++++---- .../0070_project_use_storage_keep_versions.py | 11 +++++++---- docker-app/qfieldcloud/core/models.py | 14 ++++++++------ docker-app/qfieldcloud/core/utils2/storage.py | 3 +-- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py b/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py index ffcbb596c..a78e9ae2d 100644 --- a/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py +++ b/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py @@ -6,13 +6,16 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0068_job_container_id'), + ("core", "0068_job_container_id"), ] operations = [ migrations.AddField( - model_name='project', - name='storage_keep_versions', - field=models.PositiveIntegerField(default=10, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead."), + model_name="project", + name="storage_keep_versions", + field=models.PositiveIntegerField( + default=10, + help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead.", + ), ), ] diff --git a/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py b/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py index 939223546..927b77e1c 100644 --- a/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py +++ b/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py @@ -6,13 +6,16 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0069_project_storage_keep_versions'), + ("core", "0069_project_storage_keep_versions"), ] operations = [ migrations.AddField( - model_name='project', - name='use_storage_keep_versions', - field=models.BooleanField(default=False, verbose_name='Opt-in to project-based max. files versions'), + model_name="project", + name="use_storage_keep_versions", + field=models.BooleanField( + default=False, + verbose_name="Opt-in to project-based max. files versions", + ), ), ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index d0c67ed66..9b702164b 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -5,7 +5,7 @@ import uuid from datetime import datetime, timedelta from enum import Enum -from typing import List, Optional, Union, cast +from typing import List, Optional, cast import django_cryptography.fields from deprecated import deprecated @@ -1255,12 +1255,14 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) - + # Adjust project's max keep versions in accordance to the owner's plan - self.storage_keep_versions = min( - self.storage_keep_versions, - self.owner.useraccount.current_subscription.plan.storage_keep_versions - ) + # if the user has opted in to this logic + if self.use_storage_keep_versions: + self.storage_keep_versions = min( + self.storage_keep_versions, + self.owner.useraccount.current_subscription.plan.storage_keep_versions, + ) super().save(*args, **kwargs) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index 66215fc23..b8ae8aff8 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -407,10 +407,9 @@ def purge_old_file_versions( # Number of versions to keep is determined by the account type keep_count = ( project.storage_keep_versions, - project.owner.useraccount.current_subscription.plan.storage_keep_versions + project.owner.useraccount.current_subscription.plan.storage_keep_versions, ) - logger.debug(f"Keeping {keep_count} versions") # Process file by file From a416278bd43da2fb3e0978f5e6b58fd616db7554 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 5 Jun 2023 15:55:36 +0200 Subject: [PATCH 08/69] Typo --- docker-app/qfieldcloud/core/utils2/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index b8ae8aff8..8b8028f6e 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -405,7 +405,7 @@ def purge_old_file_versions( logger.info(f"Cleaning up old files for {project}") # Number of versions to keep is determined by the account type - keep_count = ( + keep_count = min( project.storage_keep_versions, project.owner.useraccount.current_subscription.plan.storage_keep_versions, ) From 72b32be77f3dab300a91f09072560b5a123ecf62 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 5 Jun 2023 16:02:52 +0200 Subject: [PATCH 09/69] Removed leftover --- docker-app/qfieldcloud/core/models.py | 7 ------- docker-app/qfieldcloud/core/utils2/storage.py | 14 ++++++++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 9b702164b..1fd96a664 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1256,13 +1256,6 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) - # Adjust project's max keep versions in accordance to the owner's plan - # if the user has opted in to this logic - if self.use_storage_keep_versions: - self.storage_keep_versions = min( - self.storage_keep_versions, - self.owner.useraccount.current_subscription.plan.storage_keep_versions, - ) super().save(*args, **kwargs) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index 8b8028f6e..d0f843f15 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -405,10 +405,16 @@ def purge_old_file_versions( logger.info(f"Cleaning up old files for {project}") # Number of versions to keep is determined by the account type - keep_count = min( - project.storage_keep_versions, - project.owner.useraccount.current_subscription.plan.storage_keep_versions, - ) + # and by whether the user has opted-in to a project-based count + if project.use_storage_keep_versions: + keep_count = min( + project.storage_keep_versions, + project.owner.useraccount.current_subscription.plan.storage_keep_versions, + ) + else: + keep_count = ( + project.owner.useraccount.current_subscription.plan.storage_keep_versions + ) logger.debug(f"Keeping {keep_count} versions") From 6256390f90a04fb86e06e4c89ace9e27db22594a Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 8 Jun 2023 14:43:39 +0200 Subject: [PATCH 10/69] Accomodating comments --- docker-app/qfieldcloud/core/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 1fd96a664..abbe9c490 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -13,7 +13,7 @@ from django.contrib.auth.models import UserManager as DjangoUserManager from django.contrib.gis.db import models from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator, RegexValidator +from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.db import transaction from django.db.models import Case, Exists, F, OuterRef, Q from django.db.models import Value as V @@ -1039,6 +1039,7 @@ class Meta: storage_keep_versions = models.PositiveIntegerField( default=10, + validators=[MaxValueValidator(100), MinValueValidator(1)], help_text=( "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead." ), From 8cc057801febde5c10f6bd8daad8717e1f47e207 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 8 Jun 2023 14:48:35 +0200 Subject: [PATCH 11/69] squashed migrations --- .../migrations/0069_auto_20230608_1448.py | 24 +++++++++++++++++++ .../0069_project_storage_keep_versions.py | 21 ---------------- .../0070_project_use_storage_keep_versions.py | 21 ---------------- 3 files changed, 24 insertions(+), 42 deletions(-) create mode 100644 docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py delete mode 100644 docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py delete mode 100644 docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py diff --git a/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py b/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py new file mode 100644 index 000000000..4ef16f36d --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.18 on 2023-06-08 12:48 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0068_job_container_id'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='storage_keep_versions', + field=models.PositiveIntegerField(default=10, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead.", validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)]), + ), + migrations.AddField( + model_name='project', + name='use_storage_keep_versions', + field=models.BooleanField(default=False, verbose_name='Opt-in to project-based max. files versions'), + ), + ] diff --git a/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py b/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py deleted file mode 100644 index a78e9ae2d..000000000 --- a/docker-app/qfieldcloud/core/migrations/0069_project_storage_keep_versions.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.18 on 2023-06-05 12:05 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0068_job_container_id"), - ] - - operations = [ - migrations.AddField( - model_name="project", - name="storage_keep_versions", - field=models.PositiveIntegerField( - default=10, - help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead.", - ), - ), - ] diff --git a/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py b/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py deleted file mode 100644 index 927b77e1c..000000000 --- a/docker-app/qfieldcloud/core/migrations/0070_project_use_storage_keep_versions.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.18 on 2023-06-05 13:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0069_project_storage_keep_versions"), - ] - - operations = [ - migrations.AddField( - model_name="project", - name="use_storage_keep_versions", - field=models.BooleanField( - default=False, - verbose_name="Opt-in to project-based max. files versions", - ), - ), - ] From c6b87356e3d74c054c6be9bf47fb775c5a1178c8 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 9 Jun 2023 08:57:06 +0200 Subject: [PATCH 12/69] format --- .../migrations/0069_auto_20230608_1448.py | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) mode change 100644 => 100755 docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py diff --git a/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py b/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py old mode 100644 new mode 100755 index 4ef16f36d..d4d321886 --- a/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py +++ b/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py @@ -7,18 +7,28 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0068_job_container_id'), + ("core", "0068_job_container_id"), ] operations = [ migrations.AddField( - model_name='project', - name='storage_keep_versions', - field=models.PositiveIntegerField(default=10, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead.", validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)]), + model_name="project", + name="storage_keep_versions", + field=models.PositiveIntegerField( + default=10, + help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead.", + validators=[ + django.core.validators.MaxValueValidator(100), + django.core.validators.MinValueValidator(1), + ], + ), ), migrations.AddField( - model_name='project', - name='use_storage_keep_versions', - field=models.BooleanField(default=False, verbose_name='Opt-in to project-based max. files versions'), + model_name="project", + name="use_storage_keep_versions", + field=models.BooleanField( + default=False, + verbose_name="Opt-in to project-based max. files versions", + ), ), ] From 5dcb4546ea6f28b7b8a5c0b99b0065ef79964ca5 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 12 Jun 2023 10:17:58 +0200 Subject: [PATCH 13/69] Adjusted to agreed upon is_premium business logic --- .../migrations/0070_auto_20230612_1017.py | 32 +++++++++++++++++++ docker-app/qfieldcloud/core/models.py | 22 ++++++++++--- docker-app/qfieldcloud/core/utils2/storage.py | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) create mode 100644 docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py diff --git a/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py b/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py new file mode 100644 index 000000000..a46ab3d0d --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.18 on 2023-06-12 08:17 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0069_auto_20230608_1448'), + ] + + operations = [ + migrations.RemoveField( + model_name='project', + name='storage_keep_versions', + ), + migrations.RemoveField( + model_name='project', + name='use_storage_keep_versions', + ), + migrations.AddField( + model_name='project', + name='is_premium', + field=models.BooleanField(default=False, verbose_name="Whether the project's owner is a premium user"), + ), + migrations.AddField( + model_name='project', + name='keep_file_versions', + field=models.PositiveIntegerField(default=3, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)]), + ), + ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index abbe9c490..95478fdb2 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1037,16 +1037,16 @@ class Meta: _("Thumbnail Picture URI"), max_length=255, blank=True ) - storage_keep_versions = models.PositiveIntegerField( - default=10, + keep_file_versions = models.PositiveIntegerField( + default=3, validators=[MaxValueValidator(100), MinValueValidator(1)], help_text=( - "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead." + "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)" ), ) - use_storage_keep_versions = models.BooleanField( - _("Opt-in to project-based max. files versions"), + is_premium = models.BooleanField( + _("Whether the project's owner is a premium user"), default=False, null=False, blank=False, @@ -1257,6 +1257,18 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) + active_package = ( + self.owner.useraccount.current_subscription.active_storage_package() + ) + + # Premium users, and only them, get to increase their max. keep_files versions beyond 3 + if active_package and active_package.type == "pro": + self.is_premium = True + else: + self.is_premium = False + if self.keep_file_versions > 3: + self.keep_file_versions = 3 + super().save(*args, **kwargs) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index d0f843f15..5290a75d6 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -408,7 +408,7 @@ def purge_old_file_versions( # and by whether the user has opted-in to a project-based count if project.use_storage_keep_versions: keep_count = min( - project.storage_keep_versions, + project.keep_file_versions, project.owner.useraccount.current_subscription.plan.storage_keep_versions, ) else: From 7d04ceb5d82bf39b7818a662b90e51ff0008942f Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 12 Jun 2023 11:00:15 +0200 Subject: [PATCH 14/69] models --- .../migrations/0070_auto_20230612_1017.py | 32 ++++++++++++------- docker-app/qfieldcloud/core/models.py | 6 ++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py b/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py index a46ab3d0d..4875733b7 100644 --- a/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py +++ b/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py @@ -7,26 +7,36 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0069_auto_20230608_1448'), + ("core", "0069_auto_20230608_1448"), ] operations = [ migrations.RemoveField( - model_name='project', - name='storage_keep_versions', + model_name="project", + name="storage_keep_versions", ), migrations.RemoveField( - model_name='project', - name='use_storage_keep_versions', + model_name="project", + name="use_storage_keep_versions", ), migrations.AddField( - model_name='project', - name='is_premium', - field=models.BooleanField(default=False, verbose_name="Whether the project's owner is a premium user"), + model_name="project", + name="is_premium", + field=models.BooleanField( + default=False, + verbose_name="Whether the project's owner is a premium user", + ), ), migrations.AddField( - model_name='project', - name='keep_file_versions', - field=models.PositiveIntegerField(default=3, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)]), + model_name="project", + name="keep_file_versions", + field=models.PositiveIntegerField( + default=3, + help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", + validators=[ + django.core.validators.MaxValueValidator(100), + django.core.validators.MinValueValidator(1), + ], + ), ), ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 95478fdb2..f448abe06 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1038,15 +1038,15 @@ class Meta: ) keep_file_versions = models.PositiveIntegerField( - default=3, - validators=[MaxValueValidator(100), MinValueValidator(1)], help_text=( "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)" ), + default=3, + validators=[MaxValueValidator(100), MinValueValidator(1)], ) is_premium = models.BooleanField( - _("Whether the project's owner is a premium user"), + help_text=_("Whether the project's owner is a premium user"), default=False, null=False, blank=False, From a39be6171d15f5e29cd4368c7e45316c94284979 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 12 Jun 2023 11:27:53 +0200 Subject: [PATCH 15/69] better logic --- docker-app/qfieldcloud/core/models.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index f448abe06..6c9ebfc2d 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1257,17 +1257,15 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) - active_package = ( - self.owner.useraccount.current_subscription.active_storage_package() - ) + active_subscription = self.owner.useraccount.current_subscription + self.is_premium = active_subscription and active_subscription.plan.code in { + "pro", + "organization", + } # Premium users, and only them, get to increase their max. keep_files versions beyond 3 - if active_package and active_package.type == "pro": - self.is_premium = True - else: - self.is_premium = False - if self.keep_file_versions > 3: - self.keep_file_versions = 3 + if not self.is_premium and self.keep_file_versions > 3: + self.keep_file_versions = 3 super().save(*args, **kwargs) From a478aba4660fe1918878c7d73f36315a2804e02d Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 13 Jun 2023 10:14:53 +0200 Subject: [PATCH 16/69] missing migration --- .../0071_alter_project_is_premium.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py diff --git a/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py b/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py new file mode 100644 index 000000000..95f8dae48 --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-06-13 08:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0070_auto_20230612_1017'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='is_premium', + field=models.BooleanField(default=False, help_text="Whether the project's owner is a premium user"), + ), + ] From d5a7c6c6a811f5be164e974cce6bdb5535f26de7 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 13 Jun 2023 10:21:48 +0200 Subject: [PATCH 17/69] fixed wrong permissions on latest migration file --- .../core/migrations/0071_alter_project_is_premium.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) mode change 100644 => 100755 docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py diff --git a/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py b/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py old mode 100644 new mode 100755 index 95f8dae48..02cc28db4 --- a/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py +++ b/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py @@ -6,13 +6,15 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0070_auto_20230612_1017'), + ("core", "0070_auto_20230612_1017"), ] operations = [ migrations.AlterField( - model_name='project', - name='is_premium', - field=models.BooleanField(default=False, help_text="Whether the project's owner is a premium user"), + model_name="project", + name="is_premium", + field=models.BooleanField( + default=False, help_text="Whether the project's owner is a premium user" + ), ), ] From ccacb8cb39fc8dbca86adb24dacf3306ccc2b9f3 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 13 Jun 2023 15:37:26 +0200 Subject: [PATCH 18/69] fixed leftover name --- docker-app/qfieldcloud/core/utils2/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index 5290a75d6..7836c5378 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -406,7 +406,7 @@ def purge_old_file_versions( # Number of versions to keep is determined by the account type # and by whether the user has opted-in to a project-based count - if project.use_storage_keep_versions: + if project.is_premium: keep_count = min( project.keep_file_versions, project.owner.useraccount.current_subscription.plan.storage_keep_versions, From 71371acfe630c4dbf2bb999522c501ba15d04a27 Mon Sep 17 00:00:00 2001 From: Marco Bernasocchi Date: Tue, 13 Jun 2023 17:07:09 +0200 Subject: [PATCH 19/69] add good looking error pages --- conf/nginx/pages/403.html | 19 +++++-- conf/nginx/pages/404.html | 19 +++++-- conf/nginx/pages/500.html | 21 ++++++-- conf/nginx/pages/sad_nyuki.svg | 91 ++++++++++++++++++++++++++++++++++ conf/nginx/pages/style.css | 21 ++++++++ 5 files changed, 156 insertions(+), 15 deletions(-) create mode 100644 conf/nginx/pages/sad_nyuki.svg create mode 100644 conf/nginx/pages/style.css diff --git a/conf/nginx/pages/403.html b/conf/nginx/pages/403.html index 0da8e2cd1..98a644d74 100644 --- a/conf/nginx/pages/403.html +++ b/conf/nginx/pages/403.html @@ -1,12 +1,21 @@ + - - 403 Forbidden + Error 404 - Forbidden + - -

403 Forbidden

+ + +
+ Sad Nyuki +

404

+

Forbidden

+

Sorry, but you don't have permission to access this page or resource.

+ Go back to Home +
- + + \ No newline at end of file diff --git a/conf/nginx/pages/404.html b/conf/nginx/pages/404.html index 8aa91d829..6e7460265 100644 --- a/conf/nginx/pages/404.html +++ b/conf/nginx/pages/404.html @@ -1,12 +1,21 @@ + - - 404 Not Found + Error 404 - Page Not Found + - -

404 Not Found

+ + +
+ Sad Nyuki +

404

+

Page Not Found

+

We're sorry, but the page you were trying to view does not exist.

+ Go back to Home +
- + + \ No newline at end of file diff --git a/conf/nginx/pages/500.html b/conf/nginx/pages/500.html index 02645e68b..ae06a0bc6 100644 --- a/conf/nginx/pages/500.html +++ b/conf/nginx/pages/500.html @@ -1,12 +1,23 @@ + - - 500 Internal Server Error + Error 500 - Internal Server Error + - -

500 Internal Server Error

+ + +
+ Sad Nyuki +

500

+

Internal Server Error

+

We're sorry, but something went wrong on our end. Our team has been notified and we're working to fix it as + soon as possible.

+ + Go back to Home +
- + + \ No newline at end of file diff --git a/conf/nginx/pages/sad_nyuki.svg b/conf/nginx/pages/sad_nyuki.svg new file mode 100644 index 000000000..4387d3ee9 --- /dev/null +++ b/conf/nginx/pages/sad_nyuki.svg @@ -0,0 +1,91 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/conf/nginx/pages/style.css b/conf/nginx/pages/style.css new file mode 100644 index 000000000..b5f2398e1 --- /dev/null +++ b/conf/nginx/pages/style.css @@ -0,0 +1,21 @@ +@import url('https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css'); + + +a { + color: #4a6fae; +} +a :hover { + color: #3f5e93; +} + +.btn-primary { + color: #fff; + background-color: #4a6fae; + border-color: #4a6fae; +} + +.btn-primary:hover { + color: #fff; + background-color: #3f5e93; + border-color: #3b588a; +} \ No newline at end of file From ffb10d82316376176cf479407404787b87fcb08f Mon Sep 17 00:00:00 2001 From: Marco Bernasocchi Date: Tue, 13 Jun 2023 17:07:09 +0200 Subject: [PATCH 20/69] fix formatting --- conf/nginx/pages/403.html | 2 +- conf/nginx/pages/404.html | 2 +- conf/nginx/pages/500.html | 2 +- conf/nginx/pages/style.css | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/conf/nginx/pages/403.html b/conf/nginx/pages/403.html index 98a644d74..1d34669a1 100644 --- a/conf/nginx/pages/403.html +++ b/conf/nginx/pages/403.html @@ -18,4 +18,4 @@

404

- \ No newline at end of file + diff --git a/conf/nginx/pages/404.html b/conf/nginx/pages/404.html index 6e7460265..2de921d5b 100644 --- a/conf/nginx/pages/404.html +++ b/conf/nginx/pages/404.html @@ -18,4 +18,4 @@

404

- \ No newline at end of file + diff --git a/conf/nginx/pages/500.html b/conf/nginx/pages/500.html index ae06a0bc6..0498cb88b 100644 --- a/conf/nginx/pages/500.html +++ b/conf/nginx/pages/500.html @@ -20,4 +20,4 @@

500

- \ No newline at end of file + diff --git a/conf/nginx/pages/style.css b/conf/nginx/pages/style.css index b5f2398e1..fd939ed8d 100644 --- a/conf/nginx/pages/style.css +++ b/conf/nginx/pages/style.css @@ -18,4 +18,4 @@ a :hover { color: #fff; background-color: #3f5e93; border-color: #3b588a; -} \ No newline at end of file +} From 3060fbab7c6e11a9663b61ab7a0b310eefd1ed16 Mon Sep 17 00:00:00 2001 From: Fabian Binder Date: Wed, 14 Jun 2023 08:12:52 +0200 Subject: [PATCH 21/69] Typo conf/nginx/pages/403.html Co-authored-by: Ivan Ivanov --- conf/nginx/pages/403.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/nginx/pages/403.html b/conf/nginx/pages/403.html index 1d34669a1..bb18037d3 100644 --- a/conf/nginx/pages/403.html +++ b/conf/nginx/pages/403.html @@ -11,7 +11,7 @@
Sad Nyuki -

404

+

403

Forbidden

Sorry, but you don't have permission to access this page or resource.

Go back to Home From fedb75b0c3d5c016124b8267419317ec7ebd002f Mon Sep 17 00:00:00 2001 From: faebebin Date: Wed, 14 Jun 2023 11:11:58 +0200 Subject: [PATCH 22/69] Define css styles in template - avoid import --- conf/nginx/pages/403.html | 182 ++++++++++++++++++++++++++++++++++++- conf/nginx/pages/404.html | 181 +++++++++++++++++++++++++++++++++++- conf/nginx/pages/500.html | 182 ++++++++++++++++++++++++++++++++++++- conf/nginx/pages/style.css | 21 ----- 4 files changed, 542 insertions(+), 24 deletions(-) delete mode 100644 conf/nginx/pages/style.css diff --git a/conf/nginx/pages/403.html b/conf/nginx/pages/403.html index bb18037d3..c4d651c2a 100644 --- a/conf/nginx/pages/403.html +++ b/conf/nginx/pages/403.html @@ -5,7 +5,6 @@ Error 404 - Forbidden - @@ -16,6 +15,187 @@

403

Sorry, but you don't have permission to access this page or resource.

Go back to Home
+ + diff --git a/conf/nginx/pages/404.html b/conf/nginx/pages/404.html index 2de921d5b..be1a91052 100644 --- a/conf/nginx/pages/404.html +++ b/conf/nginx/pages/404.html @@ -5,7 +5,6 @@ Error 404 - Page Not Found - @@ -16,6 +15,186 @@

404

We're sorry, but the page you were trying to view does not exist.

Go back to Home + diff --git a/conf/nginx/pages/500.html b/conf/nginx/pages/500.html index 0498cb88b..93baadd38 100644 --- a/conf/nginx/pages/500.html +++ b/conf/nginx/pages/500.html @@ -5,7 +5,6 @@ Error 500 - Internal Server Error - @@ -18,6 +17,187 @@

500

Go back to Home + + diff --git a/conf/nginx/pages/style.css b/conf/nginx/pages/style.css deleted file mode 100644 index fd939ed8d..000000000 --- a/conf/nginx/pages/style.css +++ /dev/null @@ -1,21 +0,0 @@ -@import url('https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css'); - - -a { - color: #4a6fae; -} -a :hover { - color: #3f5e93; -} - -.btn-primary { - color: #fff; - background-color: #4a6fae; - border-color: #4a6fae; -} - -.btn-primary:hover { - color: #fff; - background-color: #3f5e93; - border-color: #3b588a; -} From 56d98968adff3004ce0c6fa13529c6fb7b28979d Mon Sep 17 00:00:00 2001 From: Fabian Binder Date: Wed, 14 Jun 2023 11:17:55 +0200 Subject: [PATCH 23/69] typo conf/nginx/pages/403.html Co-authored-by: Ivan Ivanov --- conf/nginx/pages/403.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/nginx/pages/403.html b/conf/nginx/pages/403.html index c4d651c2a..5f5cf1f18 100644 --- a/conf/nginx/pages/403.html +++ b/conf/nginx/pages/403.html @@ -4,7 +4,7 @@ - Error 404 - Forbidden + Error 403 - Forbidden From 2549b6165f9c266728d4478ef545cdfc89f029b4 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 16 Jun 2023 08:28:52 +0200 Subject: [PATCH 24/69] squashed migrations; aligning naming class attributes Project -> Plan where that matters; pre-commit --- .../migrations/0069_auto_20230608_1448.py | 34 --------------- .../migrations/0069_auto_20230616_0827.py | 24 +++++++++++ .../migrations/0070_auto_20230612_1017.py | 42 ------------------- .../0071_alter_project_is_premium.py | 20 --------- docker-app/qfieldcloud/core/models.py | 9 ++-- docker-app/qfieldcloud/core/utils2/storage.py | 4 +- 6 files changed, 31 insertions(+), 102 deletions(-) delete mode 100755 docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py create mode 100644 docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py delete mode 100644 docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py delete mode 100755 docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py diff --git a/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py b/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py deleted file mode 100755 index d4d321886..000000000 --- a/docker-app/qfieldcloud/core/migrations/0069_auto_20230608_1448.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.2.18 on 2023-06-08 12:48 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0068_job_container_id"), - ] - - operations = [ - migrations.AddField( - model_name="project", - name="storage_keep_versions", - field=models.PositiveIntegerField( - default=10, - help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead.", - validators=[ - django.core.validators.MaxValueValidator(100), - django.core.validators.MinValueValidator(1), - ], - ), - ), - migrations.AddField( - model_name="project", - name="use_storage_keep_versions", - field=models.BooleanField( - default=False, - verbose_name="Opt-in to project-based max. files versions", - ), - ), - ] diff --git a/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py b/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py new file mode 100644 index 000000000..bc4158432 --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.18 on 2023-06-16 06:27 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0068_job_container_id'), + ] + + operations = [ + migrations.AddField( + model_name='project', + name='is_premium', + field=models.BooleanField(default=False, help_text="Whether the project's owner is a premium user"), + ), + migrations.AddField( + model_name='project', + name='storage_keep_versions', + field=models.PositiveIntegerField(default=3, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)]), + ), + ] diff --git a/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py b/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py deleted file mode 100644 index 4875733b7..000000000 --- a/docker-app/qfieldcloud/core/migrations/0070_auto_20230612_1017.py +++ /dev/null @@ -1,42 +0,0 @@ -# Generated by Django 3.2.18 on 2023-06-12 08:17 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0069_auto_20230608_1448"), - ] - - operations = [ - migrations.RemoveField( - model_name="project", - name="storage_keep_versions", - ), - migrations.RemoveField( - model_name="project", - name="use_storage_keep_versions", - ), - migrations.AddField( - model_name="project", - name="is_premium", - field=models.BooleanField( - default=False, - verbose_name="Whether the project's owner is a premium user", - ), - ), - migrations.AddField( - model_name="project", - name="keep_file_versions", - field=models.PositiveIntegerField( - default=3, - help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", - validators=[ - django.core.validators.MaxValueValidator(100), - django.core.validators.MinValueValidator(1), - ], - ), - ), - ] diff --git a/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py b/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py deleted file mode 100755 index 02cc28db4..000000000 --- a/docker-app/qfieldcloud/core/migrations/0071_alter_project_is_premium.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 3.2.18 on 2023-06-13 08:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0070_auto_20230612_1017"), - ] - - operations = [ - migrations.AlterField( - model_name="project", - name="is_premium", - field=models.BooleanField( - default=False, help_text="Whether the project's owner is a premium user" - ), - ), - ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 6c9ebfc2d..46eef36e7 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1037,7 +1037,7 @@ class Meta: _("Thumbnail Picture URI"), max_length=255, blank=True ) - keep_file_versions = models.PositiveIntegerField( + storage_keep_versions = models.PositiveIntegerField( help_text=( "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)" ), @@ -1257,15 +1257,16 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) + # Determining if the project's owner's account is Premium (= Pro or billed organization) active_subscription = self.owner.useraccount.current_subscription self.is_premium = active_subscription and active_subscription.plan.code in { "pro", "organization", } - # Premium users, and only them, get to increase their max. keep_files versions beyond 3 - if not self.is_premium and self.keep_file_versions > 3: - self.keep_file_versions = 3 + # Premium users, and only them, get to increase their max. project.storage_keep_versions beyond 3 + if not self.is_premium and self.storage_keep_versions > 3: + self.storage_keep_versions = 3 super().save(*args, **kwargs) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index 7836c5378..b0c806865 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -406,9 +406,9 @@ def purge_old_file_versions( # Number of versions to keep is determined by the account type # and by whether the user has opted-in to a project-based count - if project.is_premium: + if project.owner.useraccount.curren_subscription.plan.is_premium: keep_count = min( - project.keep_file_versions, + project.storage_keep_versions, project.owner.useraccount.current_subscription.plan.storage_keep_versions, ) else: From 08344683adda7bb8e5fe63db75aa46ea330531b3 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 16 Jun 2023 08:35:47 +0200 Subject: [PATCH 25/69] permissions --- .../migrations/0069_auto_20230616_0827.py | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py b/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py index bc4158432..ff74518b2 100644 --- a/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py +++ b/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py @@ -7,18 +7,27 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0068_job_container_id'), + ("core", "0068_job_container_id"), ] operations = [ migrations.AddField( - model_name='project', - name='is_premium', - field=models.BooleanField(default=False, help_text="Whether the project's owner is a premium user"), + model_name="project", + name="is_premium", + field=models.BooleanField( + default=False, help_text="Whether the project's owner is a premium user" + ), ), migrations.AddField( - model_name='project', - name='storage_keep_versions', - field=models.PositiveIntegerField(default=3, help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", validators=[django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1)]), + model_name="project", + name="storage_keep_versions", + field=models.PositiveIntegerField( + default=3, + help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", + validators=[ + django.core.validators.MaxValueValidator(100), + django.core.validators.MinValueValidator(1), + ], + ), ), ] From 3c56923e94829e9d15e7a7cb1f37b92c15ffe9f1 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 16 Jun 2023 09:21:00 +0200 Subject: [PATCH 26/69] typo --- docker-app/qfieldcloud/core/utils2/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index b0c806865..90cf00770 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -406,7 +406,7 @@ def purge_old_file_versions( # Number of versions to keep is determined by the account type # and by whether the user has opted-in to a project-based count - if project.owner.useraccount.curren_subscription.plan.is_premium: + if project.owner.useraccount.current_subscription.plan.is_premium: keep_count = min( project.storage_keep_versions, project.owner.useraccount.current_subscription.plan.storage_keep_versions, From 5c4a9954de7b806d7cb3c36d3ae54275916a49c9 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 16 Jun 2023 11:42:14 +0200 Subject: [PATCH 27/69] empty lines --- .../migrations/0069_auto_20230616_0827.py | 22 ++++----- docker-app/qfieldcloud/core/models.py | 46 ++++++++++--------- docker-app/qfieldcloud/core/utils2/storage.py | 8 ++-- 3 files changed, 39 insertions(+), 37 deletions(-) diff --git a/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py b/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py index ff74518b2..9d66f838c 100644 --- a/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py +++ b/docker-app/qfieldcloud/core/migrations/0069_auto_20230616_0827.py @@ -1,33 +1,29 @@ -# Generated by Django 3.2.18 on 2023-06-16 06:27 +# Generated by Django 3.2.18 on 2023-06-16 07:58 import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ("core", "0068_job_container_id"), ] operations = [ - migrations.AddField( - model_name="project", - name="is_premium", - field=models.BooleanField( - default=False, help_text="Whether the project's owner is a premium user" - ), - ), migrations.AddField( model_name="project", name="storage_keep_versions", field=models.PositiveIntegerField( - default=3, - help_text="If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)", + verbose_name="File versions to keep", + help_text=( + "Use this value to limit the maximum number of file versions. If empty, your current plan's default will be used. Available to Premium users only." + ), validators=[ - django.core.validators.MaxValueValidator(100), django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(100), ], + null=True, + blank=True, ), - ), + ) ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 46eef36e7..6a2737e6b 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -362,6 +362,7 @@ def save(self, *args, **kwargs): class UserAccount(models.Model): + NOTIFS_IMMEDIATELY = timedelta(minutes=0) NOTIFS_HOURLY = timedelta(hours=1) NOTIFS_DAILY = timedelta(days=1) @@ -541,6 +542,7 @@ def default_port() -> str: class Geodb(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True) username = models.CharField(blank=False, max_length=255, default=random_string) dbname = models.CharField(blank=False, max_length=255, default=random_string) @@ -715,6 +717,7 @@ def delete(self, *args, **kwargs): class OrganizationMember(models.Model): + objects = OrganizationMemberQueryset.as_manager() class Roles(models.TextChoices): @@ -799,6 +802,7 @@ class Meta: class Team(User): + team_organization = models.ForeignKey( Organization, on_delete=models.CASCADE, @@ -1032,24 +1036,22 @@ class Meta: "If enabled, QFieldCloud will automatically overwrite conflicts in this project. Disabling this will force the project manager to manually resolve all the conflicts." ), ) - thumbnail_uri = models.CharField( _("Thumbnail Picture URI"), max_length=255, blank=True ) + # Duplicating logic from the plan's storage_keep_versions + # so that users use less file versions (therefore storage) + # than as per their plan's default on specific projects. + # WARNING: If storage_keep_versions == 0, it will delete all file versions (hence the file itself) ! storage_keep_versions = models.PositiveIntegerField( - help_text=( - "If enabled, QFieldCloud will use this value to limit the maximum number of versions per file in the current project with this value. If the value is larger than the maximum number of versions per file your current plan entitles you to, the current plan's value will be used instead (by default 3 for non-paying customers; 10 for paying customers)" + _("File versions to keep"), + help_text=_( + "Use this value to limit the maximum number of file versions. If empty, your current plan's default will be used. Available to Premium users only." ), - default=3, - validators=[MaxValueValidator(100), MinValueValidator(1)], - ) - - is_premium = models.BooleanField( - help_text=_("Whether the project's owner is a premium user"), - default=False, - null=False, - blank=False, + null=True, + blank=True, + validators=[MinValueValidator(1), MaxValueValidator(100)], ) @property @@ -1257,16 +1259,15 @@ def save(self, recompute_storage=False, *args, **kwargs): if recompute_storage: self.file_storage_bytes = storage.get_project_file_storage_in_bytes(self.id) - # Determining if the project's owner's account is Premium (= Pro or billed organization) - active_subscription = self.owner.useraccount.current_subscription - self.is_premium = active_subscription and active_subscription.plan.code in { - "pro", - "organization", - } + # Ensure that the Project's storage_keep_versions is at least 1, and reflects the plan's default storage_keep_versions value. + if not self.storage_keep_versions: + self.storage_keep_versions = ( + self.owner.useraccount.current_subscription.plan.storage_keep_versions + ) - # Premium users, and only them, get to increase their max. project.storage_keep_versions beyond 3 - if not self.is_premium and self.storage_keep_versions > 3: - self.storage_keep_versions = 3 + assert ( + self.storage_keep_versions >= 1 + ), "If 0, storage_keep_versions mean that all file versions are deleted!" super().save(*args, **kwargs) @@ -1410,6 +1411,7 @@ def clean(self) -> None: elif self.collaborator.is_team: team_qs = organization.teams.filter(pk=self.collaborator) if not team_qs.exists(): + raise ValidationError(_("Team does not exist.")) return super().clean() @@ -1527,6 +1529,7 @@ def method(self): class Job(models.Model): + objects = InheritanceManager() class Type(models.TextChoices): @@ -1645,6 +1648,7 @@ class Meta: class ApplyJob(Job): + deltas_to_apply = models.ManyToManyField( to=Delta, through="ApplyJobDelta", diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index 90cf00770..d2f1be6ef 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -407,15 +407,17 @@ def purge_old_file_versions( # Number of versions to keep is determined by the account type # and by whether the user has opted-in to a project-based count if project.owner.useraccount.current_subscription.plan.is_premium: - keep_count = min( - project.storage_keep_versions, - project.owner.useraccount.current_subscription.plan.storage_keep_versions, + keep_count = ( + project.storage_keep_versions + or project.owner.useraccount.current_subscription.plan.storage_keep_versions ) else: keep_count = ( project.owner.useraccount.current_subscription.plan.storage_keep_versions ) + assert keep_count >= 1, "Ensure that we don't destroy all file versions !" + logger.debug(f"Keeping {keep_count} versions") # Process file by file From 0b46cfa05544f8f430098baa414244ceba57b5b5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bach Date: Sat, 17 Jun 2023 17:52:02 +0200 Subject: [PATCH 28/69] New line handling --- .gitattributes | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..54e724cce --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# +# The above will handle all files NOT found below +# +# These files are text and should be normalized (Convert crlf => lf) +*.bash text eol=lf +*.sh text eol=lf From 6dce26451af6ef6601139cfa0ee0e48a86cd604f Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Tue, 20 Jun 2023 02:41:32 +0300 Subject: [PATCH 29/69] Bump QGIS 3.30.3 --- docker-qgis/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-qgis/Dockerfile b/docker-qgis/Dockerfile index 568720db2..627f1b8e4 100644 --- a/docker-qgis/Dockerfile +++ b/docker-qgis/Dockerfile @@ -1,4 +1,4 @@ -FROM qgis/qgis:final-3_30_2 +FROM qgis/qgis:final-3_30_3 RUN apt-get update \ && apt-get upgrade -y \ From 56636e678b0cfb22b761c26b2e4991a8569e4ba0 Mon Sep 17 00:00:00 2001 From: Fabian Binder Date: Wed, 21 Jun 2023 10:15:11 +0200 Subject: [PATCH 30/69] Correct docker port command README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cbf4bdab1..b13ae3206 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,8 @@ debugpy.wait_for_client() # optional Or alternativley, prefix your commands with `python -m debugpy --listen 0.0.0.0:5680 --wait-for-client`. - docker compose run app -p 5680:5680 python -m debugpy --listen 0.0.0.0:5680 --wait-for-client manage.py test - docker compose run worker_wrapper -p 5681:5681 python -m debugpy --listen 0.0.0.0:5681 --wait-for-client manage.py test + docker compose run -p 5680:5680 app python -m debugpy --listen 0.0.0.0:5680 --wait-for-client manage.py test + docker compose run -p 5681:5681 worker_wrapper python -m debugpy --listen 0.0.0.0:5681 --wait-for-client manage.py test Note if you run tests using the `docker-compose.test.yml` configuration, the `app` and `worker-wrapper` containers expose ports `5680` and `5681` respectively. From 9ce8ca516dc345b5d7b694e7b47b4a67838b1328 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Jun 2023 11:28:28 +0300 Subject: [PATCH 31/69] Set dequeue transaction level to repeatable read so we have stable data --- docker-app/qfieldcloud/core/management/commands/dequeue.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/management/commands/dequeue.py b/docker-app/qfieldcloud/core/management/commands/dequeue.py index 0ee4f00f4..efa8d863e 100644 --- a/docker-app/qfieldcloud/core/management/commands/dequeue.py +++ b/docker-app/qfieldcloud/core/management/commands/dequeue.py @@ -5,7 +5,7 @@ from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.management.base import BaseCommand -from django.db import transaction +from django.db import connection, transaction from qfieldcloud.core.models import Job from worker_wrapper.wrapper import ( DeltaApplyJobRun, @@ -51,6 +51,9 @@ def handle(self, *args, **options): queued_job = None with transaction.atomic(): + with connection.cursor() as cursor: + cursor.execute("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ") + busy_projects_ids_qs = Job.objects.filter( status__in=[ Job.Status.QUEUED, From e36a0b42adb8830cd6dfaff7453bad90e39416c3 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Jun 2023 12:29:39 +0300 Subject: [PATCH 32/69] Ensure connection to the master node as soon as we have the DB session --- .../qfieldcloud/core/management/commands/dequeue.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docker-app/qfieldcloud/core/management/commands/dequeue.py b/docker-app/qfieldcloud/core/management/commands/dequeue.py index efa8d863e..05011de66 100644 --- a/docker-app/qfieldcloud/core/management/commands/dequeue.py +++ b/docker-app/qfieldcloud/core/management/commands/dequeue.py @@ -48,6 +48,15 @@ def handle(self, *args, **options): cancel_orphaned_workers() + with connection.cursor() as cursor: + # NOTE `pg_is_in_recovery` returns `FALSE` if connected to the master node + cursor.execute("SELECT pg_is_in_recovery()") + # there is no way `cursor.fetchone()` returns no rows, therefore ignore the type warning + if cursor.fetchone()[0]: # type: ignore + raise Exception( + "Expected `worker_wrapper` to be connected to the master DB node!" + ) + queued_job = None with transaction.atomic(): From eeb17fb45aba7f76c44aac5b7a1365e8e0370b38 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 22 Jun 2023 11:30:53 +0200 Subject: [PATCH 33/69] Making sure strings are encoded when sending to Sentry --- docker-app/qfieldcloud/core/utils2/sentry.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docker-app/qfieldcloud/core/utils2/sentry.py b/docker-app/qfieldcloud/core/utils2/sentry.py index ab9910c48..34d94faeb 100644 --- a/docker-app/qfieldcloud/core/utils2/sentry.py +++ b/docker-app/qfieldcloud/core/utils2/sentry.py @@ -17,18 +17,20 @@ def report_serialization_diff_to_sentry( post_serialization: str representing the request `files` keys and meta information after serialization and middleware. buffer: StringIO buffer from which to extract traceback capturing callstack ahead of the calling function. """ - - traceback = bytes(buffer.getvalue(), encoding="utf-8") - report = f"Pre:\n{pre_serialization}\n\nPost:{post_serialization}" - with sentry_sdk.configure_scope() as scope: try: filename = f"{name}_contents.txt" - scope.add_attachment(bytes=bytes(report), filename=filename) + scope.add_attachment( + bytes=bytes( + f"Pre:\n{pre_serialization}\n\nPost:{post_serialization}", + encoding="utf8", + ), + filename=filename, + ) filename = f"{name}_traceback.txt" scope.add_attachment( - bytes=traceback, + bytes=bytes(buffer.getvalue(), encoding="utf8"), filename=filename, ) except Exception as error: From ec9e7d817f0e1f52ffce24db3d1f45306bb33678 Mon Sep 17 00:00:00 2001 From: Adrien Date: Thu, 22 Jun 2023 12:02:26 +0200 Subject: [PATCH 34/69] A more moderate approach, whereby the steps preparing and running the tests are run as a matrix of jobs (#695) --- .github/workflows/test.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a51d640b0..af76d0ae0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,15 @@ jobs: test: name: Run tests runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + django_apps: + - authentication + - notifs + - subscription + - core + continue-on-error: true steps: - name: Checkout repo uses: actions/checkout@v3 @@ -59,7 +68,7 @@ jobs: docker compose run app python manage.py collectstatic - name: Run unit and integration tests run: | - docker compose run app python manage.py test --keepdb -v2 qfieldcloud + docker compose run app python manage.py test --keepdb -v2 qfieldcloud.${{ matrix.django_apps }} - name: "failure logs" if: failure() From 7007286d9a0e5ef46322a36c912303ab97b6e008 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Jun 2023 17:15:21 +0300 Subject: [PATCH 35/69] Add a rule about commits testability in PRs --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index b13ae3206..14ad4afe5 100644 --- a/README.md +++ b/README.md @@ -302,6 +302,14 @@ users. The template db has the following extensions installed: You can use either the integrated `minio` object storage, or use an external provider (e. g. S3) with versioning enabled. Check the corresponding `STORAGE_*` environment variables for more info. +## Collaboration + +Contributions welcome! + +Any PR including the `[WIP]` should be: +- able to be checked-out without breaking the stack; +- the specific feature being developed/modified should testable locally (does not mean it should work correctly). + ## Resources - [QField Cloud "marketing" page](https://qfield.cloud) From 22472d2929bbe85443f53a163836137e6d445b30 Mon Sep 17 00:00:00 2001 From: Fabian Binder Date: Thu, 22 Jun 2023 16:36:07 +0200 Subject: [PATCH 36/69] typo in ## Collaboration --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14ad4afe5..f7a556199 100644 --- a/README.md +++ b/README.md @@ -308,7 +308,7 @@ Contributions welcome! Any PR including the `[WIP]` should be: - able to be checked-out without breaking the stack; -- the specific feature being developed/modified should testable locally (does not mean it should work correctly). +- the specific feature being developed/modified should be testable locally (does not mean it should work correctly). ## Resources From 0eda7c8e4582bf44992c394e931489bbea75b4e0 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 26 Jun 2023 10:56:15 +0200 Subject: [PATCH 37/69] format --- .../qfieldcloud/core/tests/test_sentry.py | 25 +++++++++++++++++++ docker-app/qfieldcloud/core/utils2/sentry.py | 4 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docker-app/qfieldcloud/core/tests/test_sentry.py diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py new file mode 100644 index 000000000..87f8c741b --- /dev/null +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -0,0 +1,25 @@ +from io import StringIO + +from rest_framework.test import APITestCase + +from ..utils2.sentry import report_serialization_diff_to_sentry + + +class QfcTestCase(APITestCase): + def test_logging_with_sentry(self): + mock_payload = { + "name": "request_id_file_name", + "pre_serialization": { + "data": str(["some", "pre-serialization", "data", "keys"]), + "files": str(["some", "files", "to be", "listed"]), + "meta": str({"metadata": "of the request pre-serialization"}), + }, + "post_serialization": { + "data": str(["some", "post-serialization", "data", "keys"]), + "files": str(["some", "files", "to be", "listed"]), + "meta": str({"metadata": "of the request post-serialization"}), + }, + "buffer": StringIO("The traceback of the exception to raise"), + } + result = report_serialization_diff_to_sentry(**mock_payload) + self.assertTrue(result) diff --git a/docker-app/qfieldcloud/core/utils2/sentry.py b/docker-app/qfieldcloud/core/utils2/sentry.py index 34d94faeb..4db4ae653 100644 --- a/docker-app/qfieldcloud/core/utils2/sentry.py +++ b/docker-app/qfieldcloud/core/utils2/sentry.py @@ -8,7 +8,7 @@ def report_serialization_diff_to_sentry( name: str, pre_serialization: str, post_serialization: str, buffer: StringIO -): +) -> bool: """ Sends a report to sentry to debug QF-2540. The report includes request information from before and after middleware handle the request as well as a traceback. @@ -33,6 +33,8 @@ def report_serialization_diff_to_sentry( bytes=bytes(buffer.getvalue(), encoding="utf8"), filename=filename, ) + return True except Exception as error: sentry_sdk.capture_exception(error) logging.error(f"Unable to send file to Sentry: failed on {error}") + return False From d10991b9e1e92b853f99001c8eecc20385ea2fb5 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 26 Jun 2023 11:20:18 +0200 Subject: [PATCH 38/69] str not dict --- .../qfieldcloud/core/tests/test_sentry.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index 87f8c741b..f8d484bbd 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -9,16 +9,16 @@ class QfcTestCase(APITestCase): def test_logging_with_sentry(self): mock_payload = { "name": "request_id_file_name", - "pre_serialization": { - "data": str(["some", "pre-serialization", "data", "keys"]), - "files": str(["some", "files", "to be", "listed"]), - "meta": str({"metadata": "of the request pre-serialization"}), - }, - "post_serialization": { - "data": str(["some", "post-serialization", "data", "keys"]), - "files": str(["some", "files", "to be", "listed"]), - "meta": str({"metadata": "of the request post-serialization"}), - }, + "pre_serialization": str({ + "data": ["some", "pre-serialization", "data", "keys"], + "files": ["some", "files", "to be", "listed"], + "meta": ["metadata": "of the request pre-serialization"], + }), + "post_serialization": str({ + "data": ["some", "post-serialization", "data", "keys"], + "files": ["some", "files", "to be", "listed"], + "meta": {"metadata": "of the request post-serialization"}, + }), "buffer": StringIO("The traceback of the exception to raise"), } result = report_serialization_diff_to_sentry(**mock_payload) From 47f0da8f3f51c1cf5017883a9989b1ed5f6d9539 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 26 Jun 2023 11:22:03 +0200 Subject: [PATCH 39/69] typo --- docker-app/qfieldcloud/core/tests/test_sentry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index f8d484bbd..7030b90e9 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -12,7 +12,7 @@ def test_logging_with_sentry(self): "pre_serialization": str({ "data": ["some", "pre-serialization", "data", "keys"], "files": ["some", "files", "to be", "listed"], - "meta": ["metadata": "of the request pre-serialization"], + "meta": ["metadata", "of the request pre-serialization"], }), "post_serialization": str({ "data": ["some", "post-serialization", "data", "keys"], From 503f0e7b533b39b71da6939eec2b43cac3e05b18 Mon Sep 17 00:00:00 2001 From: Adrien Date: Mon, 26 Jun 2023 11:57:53 +0200 Subject: [PATCH 40/69] QF-2711 Fine tune error handling in the `files` API endpoint (#684) * Rebasing on main * dropping libqfieldsync changes * Update docker-app/qfieldcloud/core/exceptions.py Co-authored-by: Ivan Ivanov * renamed exception * improved clarity around ensuring the existence of bucket resource * new test * doctrings; assertions * cleanup * removed argument * leftover Co-authored-by: Ivan Ivanov --- docker-app/qfieldcloud/core/utils.py | 17 +++++++++++++---- .../qfieldcloud/core/views/files_views.py | 17 ++++------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docker-app/qfieldcloud/core/utils.py b/docker-app/qfieldcloud/core/utils.py index 49f94ea9f..b9c4b62c1 100644 --- a/docker-app/qfieldcloud/core/utils.py +++ b/docker-app/qfieldcloud/core/utils.py @@ -109,13 +109,22 @@ def get_s3_session() -> boto3.Session: def get_s3_bucket() -> mypy_boto3_s3.service_resource.Bucket: - """Get a new S3 Bucket instance using Django settings""" + """ + Get a new S3 Bucket instance using Django settings. + """ - session = get_s3_session() + bucket_name = settings.STORAGE_BUCKET_NAME + + assert bucket_name, "Expected `bucket_name` to be non-empty string!" - # Get the bucket objects + session = get_s3_session() s3 = session.resource("s3", endpoint_url=settings.STORAGE_ENDPOINT_URL) - return s3.Bucket(settings.STORAGE_BUCKET_NAME) + + # Ensure the bucket exists + s3.meta.client.head_bucket(Bucket=bucket_name) + + # Get the bucket resource + return s3.Bucket(bucket_name) def get_s3_client() -> mypy_boto3_s3.Client: diff --git a/docker-app/qfieldcloud/core/views/files_views.py b/docker-app/qfieldcloud/core/views/files_views.py index 9b6d59001..ee1f047f9 100644 --- a/docker-app/qfieldcloud/core/views/files_views.py +++ b/docker-app/qfieldcloud/core/views/files_views.py @@ -18,8 +18,9 @@ purge_old_file_versions, ) from rest_framework import permissions, status, views -from rest_framework.exceptions import NotFound, server_error +from rest_framework.exceptions import NotFound from rest_framework.parsers import MultiPartParser +from rest_framework.request import Request from rest_framework.response import Response logger = logging.getLogger(__name__) @@ -42,23 +43,13 @@ class ListFilesView(views.APIView): permission_classes = [permissions.IsAuthenticated, ListFilesViewPermissions] - def get(self, request, projectid): + def get(self, request: Request, projectid: str) -> Response: try: project = Project.objects.get(id=projectid) - bucket = utils.get_s3_bucket() - if not bucket.creation_date: - # Let DRF return 500 when bucket does not exist, since it needed but - # not what the client tried to get - return server_error( - request=request, - reason=f"Unable to fetch needed resource for {projectid}", - ) except ObjectDoesNotExist: - # map failure to get from db into failure to GET from API raise NotFound(detail=projectid) - except Exception as unknown_reason: - return server_error(request=request, reason=str(unknown_reason)) + bucket = utils.get_s3_bucket() prefix = f"projects/{projectid}/files/" files = {} From f8f844c1303b151ff90f4e0ee0ff821715cf1bd7 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 26 Jun 2023 11:43:15 +0200 Subject: [PATCH 41/69] format --- .../qfieldcloud/core/tests/test_sentry.py | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index 7030b90e9..63372e870 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -9,16 +9,32 @@ class QfcTestCase(APITestCase): def test_logging_with_sentry(self): mock_payload = { "name": "request_id_file_name", - "pre_serialization": str({ - "data": ["some", "pre-serialization", "data", "keys"], - "files": ["some", "files", "to be", "listed"], - "meta": ["metadata", "of the request pre-serialization"], - }), - "post_serialization": str({ - "data": ["some", "post-serialization", "data", "keys"], - "files": ["some", "files", "to be", "listed"], - "meta": {"metadata": "of the request post-serialization"}, - }), + "pre_serialization": str( + { + "data": [ + "some", + "pre-serialization", + "data", + "keys", + "日本人 中國的 ~=[]()%+{}@;’#!$_&- éè ;∞¥₤€", + ], + "files": ["some", "files", "to be", "listed"], + "meta": ["metadata", "of the request pre-serialization"], + } + ), + "post_serialization": str( + { + "data": [ + "some", + "post-serialization", + "data", + "keys", + "日本人 中國的 ~=[]()%+{}@;’#!$_&- éè ;∞¥₤€", + ], + "files": ["some", "files", "to be", "listed"], + "meta": {"metadata": "of the request post-serialization"}, + } + ), "buffer": StringIO("The traceback of the exception to raise"), } result = report_serialization_diff_to_sentry(**mock_payload) From 85fdf4645af1a13792b3453859bcdfbd5610d373 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 27 Jun 2023 10:39:44 +0200 Subject: [PATCH 42/69] test now captures messages so that the attachement has something to attach to --- docker-app/qfieldcloud/core/tests/test_sentry.py | 9 +++++++-- docker-app/qfieldcloud/core/utils2/sentry.py | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index 63372e870..ef2dc2ecc 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -1,11 +1,15 @@ from io import StringIO from rest_framework.test import APITestCase +from sentry_sdk import capture_message from ..utils2.sentry import report_serialization_diff_to_sentry class QfcTestCase(APITestCase): + def test_sending_message_to_sentry(self): + capture_message("Hello Sentry from test_sentry!") + def test_logging_with_sentry(self): mock_payload = { "name": "request_id_file_name", @@ -36,6 +40,7 @@ def test_logging_with_sentry(self): } ), "buffer": StringIO("The traceback of the exception to raise"), + "capture_message": True, } - result = report_serialization_diff_to_sentry(**mock_payload) - self.assertTrue(result) + is_sent = report_serialization_diff_to_sentry(**mock_payload) + self.assertTrue(is_sent) diff --git a/docker-app/qfieldcloud/core/utils2/sentry.py b/docker-app/qfieldcloud/core/utils2/sentry.py index 4db4ae653..bd0943211 100644 --- a/docker-app/qfieldcloud/core/utils2/sentry.py +++ b/docker-app/qfieldcloud/core/utils2/sentry.py @@ -7,7 +7,11 @@ def report_serialization_diff_to_sentry( - name: str, pre_serialization: str, post_serialization: str, buffer: StringIO + name: str, + pre_serialization: str, + post_serialization: str, + buffer: StringIO, + capture_message=False, ) -> bool: """ Sends a report to sentry to debug QF-2540. The report includes request information from before and after middleware handle the request as well as a traceback. @@ -33,6 +37,8 @@ def report_serialization_diff_to_sentry( bytes=bytes(buffer.getvalue(), encoding="utf8"), filename=filename, ) + if capture_message: + sentry_sdk.capture_message("Sending to Sentry...", scope=scope) return True except Exception as error: sentry_sdk.capture_exception(error) From c631153eaedf3a4e018a2ec753ace3d8e900e903 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 27 Jun 2023 10:43:35 +0200 Subject: [PATCH 43/69] doctring --- docker-app/qfieldcloud/core/tests/test_sentry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index ef2dc2ecc..52227b2d2 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -40,7 +40,7 @@ def test_logging_with_sentry(self): } ), "buffer": StringIO("The traceback of the exception to raise"), - "capture_message": True, + "capture_message": True, # so that sentry receives attachments even when there's no exception/event } is_sent = report_serialization_diff_to_sentry(**mock_payload) self.assertTrue(is_sent) From 1faf5cf615a99ea1ee86802daf27cf674f3594b7 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 27 Jun 2023 10:47:00 +0200 Subject: [PATCH 44/69] removing leftover --- docker-app/qfieldcloud/core/tests/test_sentry.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index 52227b2d2..2dacf6dbd 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -1,15 +1,11 @@ from io import StringIO from rest_framework.test import APITestCase -from sentry_sdk import capture_message from ..utils2.sentry import report_serialization_diff_to_sentry class QfcTestCase(APITestCase): - def test_sending_message_to_sentry(self): - capture_message("Hello Sentry from test_sentry!") - def test_logging_with_sentry(self): mock_payload = { "name": "request_id_file_name", From 5a3c57e2467906635894607d81443a9f8004179f Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Tue, 27 Jun 2023 10:47:36 +0200 Subject: [PATCH 45/69] naming var --- docker-app/qfieldcloud/core/tests/test_sentry.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index 2dacf6dbd..74f409988 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -38,5 +38,5 @@ def test_logging_with_sentry(self): "buffer": StringIO("The traceback of the exception to raise"), "capture_message": True, # so that sentry receives attachments even when there's no exception/event } - is_sent = report_serialization_diff_to_sentry(**mock_payload) - self.assertTrue(is_sent) + will_be_sent = report_serialization_diff_to_sentry(**mock_payload) + self.assertTrue(will_be_sent) From f5f941fc95237a4534693ae2367c03b59b2d3853 Mon Sep 17 00:00:00 2001 From: faebebin Date: Tue, 27 Jun 2023 16:48:18 +0200 Subject: [PATCH 46/69] Fix wrong test in test_organization --- docker-app/qfieldcloud/core/tests/test_organization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-app/qfieldcloud/core/tests/test_organization.py b/docker-app/qfieldcloud/core/tests/test_organization.py index 37181850e..a808a6d45 100644 --- a/docker-app/qfieldcloud/core/tests/test_organization.py +++ b/docker-app/qfieldcloud/core/tests/test_organization.py @@ -259,7 +259,7 @@ def _active_users_count(base_date=None): # User 3 creates a job Job.objects.create( project=project1, - created_by=self.user3, + created_by=self.user4, ) - # There are still 2 billable users, because self.user3 is staff + # There are still 2 billable users, because self.user4 is staff self.assertEqual(_active_users_count(), 2) From af06de4db2c44a7070fb4852726753f301e05477 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 11:08:57 +0200 Subject: [PATCH 47/69] A Project's files list now has action buttons disabled until a file version is selected. --- .../core/templates/admin/project_files_widget.html | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index b0675d27e..cb2d693fb 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -215,6 +215,9 @@ $option.innerHTML = `${version.display} (${version.size} bytes)`; $versionsSelect.appendChild($option); + $infoBtn.disabled = true; + $downloadBtn.disabled = true; + $deleteBtn.disabled = true; } $tbody.appendChild($trow); @@ -223,7 +226,16 @@ $dialogBtn.addEventListener('click', () => { $dialog.close(); }); - + + $versionsSelect.addEventListener('change', event => { + versions_ids = file.versions.map(obj => obj.version_id) + selected_version_id = event.target.value + disable_if_not_version = !versions_ids.includes(selected_version_id) + $infoBtn.disabled = disable_if_not_version; + $downloadBtn.disabled = disable_if_not_version; + $deleteBtn.disabled = disable_if_not_version; + }) + $infoBtn.addEventListener('click', () => { $dialog.showModal(); $dialogPre.innerHTML = JSON.stringify(file, null, 2); From d5de0866d2b3f2302f39b7e1ee484c10d6f19a51 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 11:10:26 +0200 Subject: [PATCH 48/69] format --- .../core/templates/admin/project_files_widget.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index cb2d693fb..38b252d34 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -226,7 +226,7 @@ $dialogBtn.addEventListener('click', () => { $dialog.close(); }); - + $versionsSelect.addEventListener('change', event => { versions_ids = file.versions.map(obj => obj.version_id) selected_version_id = event.target.value @@ -235,7 +235,7 @@ $downloadBtn.disabled = disable_if_not_version; $deleteBtn.disabled = disable_if_not_version; }) - + $infoBtn.addEventListener('click', () => { $dialog.showModal(); $dialogPre.innerHTML = JSON.stringify(file, null, 2); From df24a84e6d07259a17b74904cfd7562a23268455 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 14:36:01 +0200 Subject: [PATCH 49/69] Not using versions selector for anything but displaying. Fixed Download last version. --- .../templates/admin/project_files_widget.html | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 38b252d34..835efb9fd 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -44,13 +44,13 @@ - - - + + + @@ -95,7 +95,8 @@ width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ - background-color: rgb(0,0,0); /* Fallback color */ + background-color: rgb(0,0,0 + ); /* Fallback color */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ } @@ -215,9 +216,6 @@ $option.innerHTML = `${version.display} (${version.size} bytes)`; $versionsSelect.appendChild($option); - $infoBtn.disabled = true; - $downloadBtn.disabled = true; - $deleteBtn.disabled = true; } $tbody.appendChild($trow); @@ -227,15 +225,6 @@ $dialog.close(); }); - $versionsSelect.addEventListener('change', event => { - versions_ids = file.versions.map(obj => obj.version_id) - selected_version_id = event.target.value - disable_if_not_version = !versions_ids.includes(selected_version_id) - $infoBtn.disabled = disable_if_not_version; - $downloadBtn.disabled = disable_if_not_version; - $deleteBtn.disabled = disable_if_not_version; - }) - $infoBtn.addEventListener('click', () => { $dialog.showModal(); $dialogPre.innerHTML = JSON.stringify(file, null, 2); @@ -243,10 +232,9 @@ $downloadBtn.addEventListener('click', () => { const pathToFile = `files/${projectId}/${file.name}/` - const buildApiUrlWithPath = () => buildApiUrl(pathToFile, { - version: $versionsSelect.value, - }); - window.open($versionsSelect.value ? buildApiUrlWithPath() : pathToFile); + const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => a-b).slice(-1) + const buildApiUrlWithPath = () => buildApiUrl(pathToFile, { version }) + window.open(buildApiUrlWithPath()); }); $deleteBtn.addEventListener('click', () => { From e6f10ffade4aea2346ccccc2db56bd3cce001f2e Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 14:38:01 +0200 Subject: [PATCH 50/69] semi-colons --- .../core/templates/admin/project_files_widget.html | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 835efb9fd..9a41b94a8 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -231,10 +231,9 @@ }); $downloadBtn.addEventListener('click', () => { - const pathToFile = `files/${projectId}/${file.name}/` - const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => a-b).slice(-1) - const buildApiUrlWithPath = () => buildApiUrl(pathToFile, { version }) - window.open(buildApiUrlWithPath()); + const pathToFile = `files/${projectId}/${file.name}/`; + const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => a-b).slice(-1); + window.open(buildApiUrl(pathToFile, { version })); }); $deleteBtn.addEventListener('click', () => { From ac9d0e536c11d3b97fc00e178ffa7d19a5f88f99 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 14:47:43 +0200 Subject: [PATCH 51/69] Label and HTML text to make things crystal clear. --- .../core/templates/admin/project_files_widget.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 9a41b94a8..fbb9392b7 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -43,14 +43,15 @@ - - + From f08dcbb6443eaf4f536fcb882498a215bfe98bda Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 15:00:32 +0200 Subject: [PATCH 52/69] removed leftover --- .../core/templates/admin/project_files_widget.html | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index fbb9392b7..f4c0ed5a7 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -43,14 +43,13 @@ - - - + + @@ -96,8 +95,7 @@ width: 100%; /* Full width */ height: 100%; /* Full height */ overflow: auto; /* Enable scroll if needed */ - background-color: rgb(0,0,0 - ); /* Fallback color */ + background-color: rgb(0,0,0); /* Fallback color */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ } From 00c436d6b504345e848bc8a522a04a3bd12bb1ac Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 15:10:13 +0200 Subject: [PATCH 53/69] fixed ordering in sort --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index f4c0ed5a7..1cc69aac3 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -231,7 +231,7 @@ $downloadBtn.addEventListener('click', () => { const pathToFile = `files/${projectId}/${file.name}/`; - const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => a-b).slice(-1); + const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => b-a).slice(-1); window.open(buildApiUrl(pathToFile, { version })); }); From 779cf47c8b8ec23acfb4e54f6de1844a945aa8b9 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 15:14:56 +0200 Subject: [PATCH 54/69] comment --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 1cc69aac3..e3ef7f095 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -231,7 +231,8 @@ $downloadBtn.addEventListener('click', () => { const pathToFile = `files/${projectId}/${file.name}/`; - const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => b-a).slice(-1); + //selected or last file version + const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => b-a)[0]; window.open(buildApiUrl(pathToFile, { version })); }); From 20c05aa29b533bf1431307fb44ad05e8ae4d4a4c Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 16:53:22 +0200 Subject: [PATCH 55/69] Changed text for Download button --- .../core/templates/admin/project_files_widget.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index e3ef7f095..d2ea6db27 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -43,14 +43,14 @@ - + - - - + + + From b40ba53f52ba783b3c2624e5795a876f1cad745c Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 17:05:17 +0200 Subject: [PATCH 56/69] Moved version selector, relabelled --- .../core/templates/admin/project_files_widget.html | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index d2ea6db27..1be6ebc78 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -19,6 +19,7 @@ {% trans 'Last modified' %} {% trans 'Last size' %} {% trans 'Version' %} + {% trans 'Selected version' %} {% trans 'Actions' %} @@ -42,15 +43,17 @@ + + + - - - + + From ac15a2b7153134cd1dbdf6ce31b42fb141ca6e8e Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Thu, 29 Jun 2023 17:08:29 +0200 Subject: [PATCH 57/69] plurals --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 1be6ebc78..8c89c2f5a 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -18,7 +18,7 @@ {% trans 'Filename' %} {% trans 'Last modified' %} {% trans 'Last size' %} - {% trans 'Version' %} + {% trans 'Details' %} {% trans 'Selected version' %} {% trans 'Actions' %} From e8e597f82236955027bcf1ab1ad31f959a36307a Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 30 Jun 2023 08:33:02 +0200 Subject: [PATCH 58/69] labels --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 8c89c2f5a..f4a18e6da 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -53,7 +53,7 @@ - + From 2bd2a615fe8fcfd9f7174fa6565a97caa15bcc5d Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 30 Jun 2023 08:33:44 +0200 Subject: [PATCH 59/69] labels --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index f4a18e6da..a4a4bb1e2 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -53,7 +53,7 @@ - + From cdc37a39ccfbf39b70011efea4e993850e4f5785 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 30 Jun 2023 10:03:25 +0200 Subject: [PATCH 60/69] Help text under a project's visibility setting --- .../migrations/0070_alter_project_is_public.py | 18 ++++++++++++++++++ docker-app/qfieldcloud/core/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py diff --git a/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py b/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py new file mode 100644 index 000000000..c34a4428a --- /dev/null +++ b/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.18 on 2023-06-30 08:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0069_auto_20230616_0827'), + ] + + operations = [ + migrations.AlterField( + model_name='project', + name='is_public', + field=models.BooleanField(default=False, help_text='Projects marked as public are visible to (but not editable by) anyone.'), + ), + ] diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 6a2737e6b..84b34ece5 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -994,7 +994,7 @@ class Meta: is_public = models.BooleanField( default=False, help_text=_( - "Projects that are marked as public would be visible and editable to anyone." + "Projects marked as public are visible to (but not editable by) anyone." ), ) owner = models.ForeignKey( From 55406a651ff0fbecdb23ec05e1a84647cacaf10f Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 30 Jun 2023 10:05:44 +0200 Subject: [PATCH 61/69] format --- .../core/migrations/0070_alter_project_is_public.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py b/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py index c34a4428a..c40fbcce8 100644 --- a/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py +++ b/docker-app/qfieldcloud/core/migrations/0070_alter_project_is_public.py @@ -6,13 +6,16 @@ class Migration(migrations.Migration): dependencies = [ - ('core', '0069_auto_20230616_0827'), + ("core", "0069_auto_20230616_0827"), ] operations = [ migrations.AlterField( - model_name='project', - name='is_public', - field=models.BooleanField(default=False, help_text='Projects marked as public are visible to (but not editable by) anyone.'), + model_name="project", + name="is_public", + field=models.BooleanField( + default=False, + help_text="Projects marked as public are visible to (but not editable by) anyone.", + ), ), ] From 8f97b725cdedf6a8ded8680c05a7eeb7712bcdd6 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Fri, 30 Jun 2023 10:13:10 +0200 Subject: [PATCH 62/69] doctring, skipIf --- docker-app/qfieldcloud/core/tests/test_sentry.py | 6 ++++++ docker-app/qfieldcloud/core/utils2/sentry.py | 1 + 2 files changed, 7 insertions(+) diff --git a/docker-app/qfieldcloud/core/tests/test_sentry.py b/docker-app/qfieldcloud/core/tests/test_sentry.py index 74f409988..ed782c346 100644 --- a/docker-app/qfieldcloud/core/tests/test_sentry.py +++ b/docker-app/qfieldcloud/core/tests/test_sentry.py @@ -1,4 +1,6 @@ from io import StringIO +from os import environ +from unittest import skipIf from rest_framework.test import APITestCase @@ -6,6 +8,10 @@ class QfcTestCase(APITestCase): + @skipIf( + environ.get("SENTRY_DSN", False), + "Do not run this test when Sentry's DSN is not set.", + ) def test_logging_with_sentry(self): mock_payload = { "name": "request_id_file_name", diff --git a/docker-app/qfieldcloud/core/utils2/sentry.py b/docker-app/qfieldcloud/core/utils2/sentry.py index bd0943211..00d14bd75 100644 --- a/docker-app/qfieldcloud/core/utils2/sentry.py +++ b/docker-app/qfieldcloud/core/utils2/sentry.py @@ -20,6 +20,7 @@ def report_serialization_diff_to_sentry( pre_serialization: str representing the request `files` keys and meta information before serialization and middleware. post_serialization: str representing the request `files` keys and meta information after serialization and middleware. buffer: StringIO buffer from which to extract traceback capturing callstack ahead of the calling function. + capture_message: bool used as a flag by the caller to create an extra event against Sentry to attach the files to. """ with sentry_sdk.configure_scope() as scope: try: From fe0788fa280446eb3e6c9e364282ddb277fe2199 Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Thu, 22 Jun 2023 02:43:17 +0300 Subject: [PATCH 63/69] Added `can_delete_unnecessary_file_versions` perm check --- docker-app/qfieldcloud/core/permissions_utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docker-app/qfieldcloud/core/permissions_utils.py b/docker-app/qfieldcloud/core/permissions_utils.py index 86196f589..1b704cf6d 100644 --- a/docker-app/qfieldcloud/core/permissions_utils.py +++ b/docker-app/qfieldcloud/core/permissions_utils.py @@ -288,6 +288,16 @@ def can_delete_files(user: QfcUser, project: Project) -> bool: ) +def can_delete_unnecessary_file_versions(user: QfcUser, project: Project) -> bool: + return user_has_project_roles( + user, + project, + [ + ProjectCollaborator.Roles.ADMIN, + ], + ) + + def can_create_deltas(user: QfcUser, project: Project) -> bool: """Whether the user can store deltas in a project.""" return user_has_project_roles( From 8ca242ca43916d9bd1c9e02d59418ad1f2dfb58d Mon Sep 17 00:00:00 2001 From: Ivan Ivanov Date: Mon, 3 Jul 2023 07:46:36 +0300 Subject: [PATCH 64/69] Add `Project.owner_aware_storage_keep_versions` to determine the file versions by project and owner's subscription plan --- docker-app/qfieldcloud/core/models.py | 20 +++++++++++++++++++ docker-app/qfieldcloud/core/utils2/storage.py | 18 ++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docker-app/qfieldcloud/core/models.py b/docker-app/qfieldcloud/core/models.py index 6a2737e6b..5f640787e 100644 --- a/docker-app/qfieldcloud/core/models.py +++ b/docker-app/qfieldcloud/core/models.py @@ -1054,6 +1054,26 @@ class Meta: validators=[MinValueValidator(1), MaxValueValidator(100)], ) + @property + def owner_aware_storage_keep_versions(self) -> int: + """Determine the storage versions to keep based on the owner's subscription plan and project settings. + + Returns: + int: the number of file versions, should be always greater than 1 + """ + subscription = self.owner.useraccount.current_subscription + + if subscription.plan.is_premium: + keep_count = ( + self.storage_keep_versions or subscription.plan.storage_keep_versions + ) + else: + keep_count = subscription.plan.storage_keep_versions + + assert keep_count >= 1, "Ensure that we don't destroy all file versions!" + + return keep_count + @property def thumbnail_url(self): if self.thumbnail_uri: diff --git a/docker-app/qfieldcloud/core/utils2/storage.py b/docker-app/qfieldcloud/core/utils2/storage.py index d2f1be6ef..00e0b034d 100644 --- a/docker-app/qfieldcloud/core/utils2/storage.py +++ b/docker-app/qfieldcloud/core/utils2/storage.py @@ -402,23 +402,9 @@ def purge_old_file_versions( accounts """ - logger.info(f"Cleaning up old files for {project}") - - # Number of versions to keep is determined by the account type - # and by whether the user has opted-in to a project-based count - if project.owner.useraccount.current_subscription.plan.is_premium: - keep_count = ( - project.storage_keep_versions - or project.owner.useraccount.current_subscription.plan.storage_keep_versions - ) - else: - keep_count = ( - project.owner.useraccount.current_subscription.plan.storage_keep_versions - ) - - assert keep_count >= 1, "Ensure that we don't destroy all file versions !" + keep_count = project.owner_aware_storage_keep_versions - logger.debug(f"Keeping {keep_count} versions") + logger.info(f"Cleaning up old files for {project} to {keep_count} versions") # Process file by file for file in qfieldcloud.core.utils.get_project_files_with_versions(project.pk): From e389602a4d4943e979ff677d8fe0ea3b86eeea72 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 3 Jul 2023 15:28:09 +0200 Subject: [PATCH 65/69] added missing span column value, simplified and fixed wording --- .../core/templates/admin/project_files_widget.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index a4a4bb1e2..24412f1b3 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -19,12 +19,12 @@ {% trans 'Last modified' %} {% trans 'Last size' %} {% trans 'Details' %} - {% trans 'Selected version' %} + {% trans 'File version' %} {% trans 'Actions' %} - {% trans 'Click the "Refresh Files List" button to get the files list.' %} + {% trans 'Click the "Refresh Files List" button to get the files list.' %} From 13b42baf4a13d59f2c3afc9855b216a8d1d4c130 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 3 Jul 2023 15:30:19 +0200 Subject: [PATCH 66/69] added translation --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 24412f1b3..36bc7d8fa 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -52,7 +52,7 @@ - + From bd577febb554822b056ee90d327c6bc6058e7871 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 3 Jul 2023 15:35:44 +0200 Subject: [PATCH 67/69] added extraspace, spanning function over several lines --- .../core/templates/admin/project_files_widget.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 36bc7d8fa..b1d47492c 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -234,8 +234,12 @@ $downloadBtn.addEventListener('click', () => { const pathToFile = `files/${projectId}/${file.name}/`; - //selected or last file version - const version = $versionsSelect.value || file.versions.map(obj => obj.version_id).sort((a, b) => b-a)[0]; + // selected or most recent file version + const version = $versionsSelect.value || ( + file.versions + .map(obj => obj.version_id) + .sort((a, b) => b - a)[0] + ); window.open(buildApiUrl(pathToFile, { version })); }); From 2af6b7519e0f8fe0efb68a2582febe8cf3479cfb Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 3 Jul 2023 15:50:39 +0200 Subject: [PATCH 68/69] localeCompare --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index b1d47492c..0b464837b 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -238,7 +238,7 @@ const version = $versionsSelect.value || ( file.versions .map(obj => obj.version_id) - .sort((a, b) => b - a)[0] + .sort((a, b) => String(b).localeCompare(String(a).localeCompare()))[0] ); window.open(buildApiUrl(pathToFile, { version })); }); From c3a3e05504de3899190e58cb27de25841c113f64 Mon Sep 17 00:00:00 2001 From: why-not-try-calmer Date: Mon, 3 Jul 2023 15:51:16 +0200 Subject: [PATCH 69/69] typo --- .../qfieldcloud/core/templates/admin/project_files_widget.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html index 0b464837b..54f058c78 100644 --- a/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html +++ b/docker-app/qfieldcloud/core/templates/admin/project_files_widget.html @@ -238,7 +238,7 @@ const version = $versionsSelect.value || ( file.versions .map(obj => obj.version_id) - .sort((a, b) => String(b).localeCompare(String(a).localeCompare()))[0] + .sort((a, b) => String(b).localeCompare(String(a)))[0] ); window.open(buildApiUrl(pathToFile, { version })); });