Skip to content

Commit ab0d461

Browse files
committed
Merge branch 'master' into release
2 parents b163b44 + b4c8e75 commit ab0d461

File tree

25 files changed

+619
-390
lines changed

25 files changed

+619
-390
lines changed

.env.example

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,6 @@ QFIELDCLOUD_DEFAULT_TIME_ZONE="Europe/Zurich"
331331
# DEFAULT: ""
332332
# QFIELDCLOUD_QGIS_IMAGE_NAME=""
333333

334-
# QFieldCloud `libqfieldsync` volume path to be mounted by the `worker_wrapper` into `worker` containers.
335-
# If empty value or invalid value, the pip installed version defined in `requirements_libqfieldsync.txt` will be used.
336-
# DEFAULT: ""
337-
QFIELDCLOUD_LIBQFIELDSYNC_VOLUME_PATH=""
338-
339-
# QFieldCloud SDK volume path to be mounted by the `worker_wrapper` into `worker` containers.
340-
# If empty value or invalid value, the pip installed version defined in `requirements_libqfieldsync.txt` will be used.
341-
# DEFAULT: ""
342-
QFIELDCLOUD_QFIELDCLOUD_SDK_VOLUME_PATH=""
343-
344334
# The Django development port. Not used in production.
345335
# DEFAULT: 8011
346336
DJANGO_DEV_PORT=8011
@@ -374,10 +364,34 @@ COMPOSE_FILE=docker-compose.yml:docker-compose.override.local.yml:docker-compose
374364
# DEFAULT: :
375365
COMPOSE_PATH_SEPARATOR=:
376366

367+
##################
368+
# Debug settings
369+
##################
370+
377371
# Debugpy port used for the `app` service
372+
# NOTE modifying this value requires modification of `.vscode/launch.json` file too
378373
# DEFAULT: 5678
379-
DEBUG_DEBUGPY_APP_PORT=5678
374+
DEBUG_APP_DEBUGPY_PORT=5678
380375

381376
# Debugpy port used for the `worker_wrapper` service
377+
# NOTE modifying this value requires modification of `.vscode/launch.json` file too
382378
# DEFAULT: 5679
383-
DEBUG_DEBUGPY_WORKER_WRAPPER_PORT=5679
379+
DEBUG_WORKER_WRAPPER_DEBUGPY_PORT=5679
380+
381+
# Debugpy port used for the `qgis` service
382+
# NOTE modifying this value requires modification of `.vscode/launch.json` file too
383+
# NOTE setting a value to this variable (e.g. 5680) will make the `qgis` container wait until a debugger is attached
384+
# DEFAULT: ""
385+
DEBUG_QGIS_DEBUGPY_PORT=""
386+
387+
# Host path to `libqfieldsync` which will be mounted by the `worker_wrapper` into the `worker` containers to facilitate development and debugging of `libqfieldsync`.
388+
# If empty value or invalid value, the pip installed version defined in `requirements_libqfieldsync.txt` will be used.
389+
# The provided path must be the root of the https://github.com/opengisch/libqfieldsync repository.
390+
# DEFAULT: ""
391+
DEBUG_QGIS_LIBQFIELDSYNC_HOST_PATH=""
392+
393+
# Host path to `qfieldcloud-sdk-python` which will be mounted by the `worker_wrapper` into the `worker` containers to facilitate development and debugging of `qfieldcloud-sdk`.
394+
# If empty value or invalid value, the pip installed version defined in `requirements.txt` will be used.
395+
# The provided path must be the root of the https://github.com/opengisch/qfieldcloud-sdk-python repository.
396+
# DEFAULT: ""
397+
DEBUG_QGIS_QFIELDCLOUD_SDK_HOST_PATH=""

.vscode/launch.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,26 @@
4949
],
5050
// enable only if you want to debug vendor modules installed via `pip` or `apt`
5151
// "justMyCode": false,
52-
}
52+
},
53+
{
54+
"name": "QFieldCloud Debug (Attach) - qgis",
55+
"type": "debugpy",
56+
"request": "attach",
57+
"connect": {
58+
"host": "localhost",
59+
// must match the value of `DEBUG_DEBUGPY_QGIS_PORT` from `.env` file, by default 5680
60+
"port": 5680
61+
},
62+
"pathMappings": [
63+
{
64+
"localRoot": "${workspaceFolder}/docker-qgis",
65+
"remoteRoot": "/usr/src/app"
66+
},
67+
{
68+
"localRoot": "${workspaceFolder}/docker-qgis/libqfieldsync",
69+
"remoteRoot": "/libqfieldsync"
70+
},
71+
],
72+
},
5373
]
5474
}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -201,8 +201,8 @@ Create an <code>.env.test</code> file with the following variables that override
201201
SMTP4DEV_IMAP_PORT=8143
202202
COMPOSE_PROJECT_NAME=qfieldcloud_test
203203
COMPOSE_FILE=docker-compose.yml:docker-compose.override.standalone.yml:docker-compose.override.test.yml
204-
DEBUG_DEBUGPY_APP_PORT=5781
205-
DEBUG_DEBUGPY_WORKER_WRAPPER_PORT=5780
204+
DEBUG_APP_DEBUGPY_PORT=5781
205+
DEBUG_WORKER_WRAPPER_DEBUGPY_PORT=5780
206206
DEMGEN_PORT=8201
207207

208208
Build the test docker compose stack:

docker-app/qfieldcloud/core/admin.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -866,7 +866,6 @@ class ProjectAdmin(QFieldCloudModelAdmin):
866866
"description",
867867
"created_at",
868868
"updated_at",
869-
"is_locked",
870869
)
871870
list_filter = (
872871
"is_public",
@@ -893,7 +892,7 @@ class ProjectAdmin(QFieldCloudModelAdmin):
893892
"data_last_updated_at",
894893
"data_last_packaged_at",
895894
"project_details__pre",
896-
"is_locked",
895+
"locked_at",
897896
"is_featured",
898897
"file_storage",
899898
"file_storage_migrated_at",
@@ -911,7 +910,7 @@ class ProjectAdmin(QFieldCloudModelAdmin):
911910
"data_last_updated_at",
912911
"data_last_packaged_at",
913912
"project_details__pre",
914-
"is_locked",
913+
"locked_at",
915914
"file_storage_migrated_at",
916915
)
917916
inlines = (
@@ -1629,7 +1628,7 @@ class FaultyDeltaFilesAdmin(QFieldCloudModelAdmin):
16291628
"user_agent",
16301629
)
16311630

1632-
list_filter = ("project", "project__owner__username", "created_at")
1631+
list_filter = ("created_at",)
16331632

16341633
list_select_related = ("project", "project__owner")
16351634

docker-app/qfieldcloud/core/management/commands/dequeue.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import logging
22
import signal
33
from time import sleep
4+
from typing import Any
45

56
from django.conf import settings
67
from django.contrib.contenttypes.models import ContentType
7-
from django.core.management.base import BaseCommand
8+
from django.core.management.base import BaseCommand, CommandParser
89
from django.db import connection, transaction
910
from django.db.models import Q
1011
from qfieldcloud.core.models import Job
1112
from worker_wrapper.wrapper import (
1213
DeltaApplyJobRun,
14+
JobRun,
1315
PackageJobRun,
1416
ProcessProjectfileJobRun,
1517
cancel_orphaned_workers,
@@ -21,23 +23,28 @@
2123
class GracefulKiller:
2224
alive = True
2325

24-
def __init__(self):
26+
def __init__(self) -> None:
2527
signal.signal(signal.SIGINT, self._kill)
2628
signal.signal(signal.SIGTERM, self._kill)
2729

28-
def _kill(self, *args):
30+
def _kill(self, *_args: Any) -> None:
2931
self.alive = False
3032

3133

3234
class Command(BaseCommand):
3335
help = "Dequeue QFieldCloud Jobs from the DB"
3436

35-
def add_arguments(self, parser):
37+
def add_arguments(self, parser: CommandParser) -> None:
3638
parser.add_argument(
3739
"--single-shot", action="store_true", help="Don't run infinite loop."
3840
)
3941

40-
def handle(self, *args, **options):
42+
def handle(
43+
self,
44+
*args: Any,
45+
single_shot: bool | None = None,
46+
**kwargs: Any,
47+
) -> None:
4148
logging.info("Dequeue QFieldCloud Jobs from the DB")
4249
killer = GracefulKiller()
4350

@@ -71,14 +78,14 @@ def handle(self, *args, **options):
7178
]
7279
).values("project_id")
7380

74-
# select all the pending jobs, that their project has no other active job or `is_locked` flag is `True`
81+
# select all the pending jobs, that their project has no other active job or `locked_at` is not null
7582
jobs_qs = (
7683
Job.objects.select_for_update(skip_locked=True)
7784
.filter(status=Job.Status.PENDING)
7885
.exclude(
7986
Q(project_id__in=busy_projects_ids_qs)
8087
# skip all projects that are currently locked, most probably because of file transfer
81-
| Q(project__is_locked=True),
88+
| Q(project__locked_at__isnull=False),
8289
)
8390
.order_by("created_at")
8491
)
@@ -96,19 +103,19 @@ def handle(self, *args, **options):
96103
self._run(queued_job)
97104
queued_job = None
98105
else:
99-
if options["single_shot"]:
106+
if single_shot:
100107
break
101108

102109
for _i in range(SECONDS):
103110
if killer.alive:
104111
cancel_orphaned_workers()
105112
sleep(1)
106113

107-
if options["single_shot"]:
114+
if single_shot:
108115
break
109116

110-
def _run(self, job: Job):
111-
job_run_classes = {
117+
def _run(self, job: Job) -> None:
118+
job_run_classes: dict[Job.Type, type[JobRun]] = {
112119
Job.Type.PACKAGE: PackageJobRun,
113120
Job.Type.DELTA_APPLY: DeltaApplyJobRun,
114121
Job.Type.PROCESS_PROJECTFILE: ProcessProjectfileJobRun,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 4.2.24 on 2025-09-17 13:34
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("core", "0088_alter_project_file_storage"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveField(
13+
model_name="project",
14+
name="is_locked",
15+
),
16+
migrations.AddField(
17+
model_name="project",
18+
name="locked_at",
19+
field=models.DateTimeField(
20+
blank=True,
21+
editable=False,
22+
help_text="If not null, it means that the project is being migrated, and the datetime represents when the project was temporarily locked. Locking is internal QFieldCloud mechanism related to file storage migration or other file operations.",
23+
null=True,
24+
verbose_name="Locked at",
25+
),
26+
),
27+
]

docker-app/qfieldcloud/core/models.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from enum import Enum
99
from pathlib import Path
1010
from typing import TYPE_CHECKING, Any, cast
11+
from uuid import uuid4
1112

1213
import django_cryptography.fields
1314
from deprecated import deprecated
@@ -1061,7 +1062,17 @@ def get_project_file_storage_default() -> str:
10611062

10621063

10631064
def get_project_thumbnail_upload_to(instance: "Project", _filename: str) -> str:
1064-
return f"projects/{instance.id}/meta/thumbnail.png"
1065+
"""Variable storage key for thumbnails.
1066+
1067+
We use a variable storage key to avoid creating object storage level versions for
1068+
thumbnails that we then would need to manage (purge old versions, make sure we don't
1069+
exceed version limit).
1070+
"""
1071+
ts = datetime.now().strftime("v%Y%m%d%H%M%S")
1072+
# Random suffix because the second precision of the timestamp alone might
1073+
# not be enough to avoid collisions
1074+
suffix = str(uuid4())[:8]
1075+
return f"projects/{instance.id}/meta/thumbnail_{ts}_{suffix}.png"
10651076

10661077

10671078
class Project(models.Model):
@@ -1269,12 +1280,14 @@ class Meta:
12691280
editable=False,
12701281
)
12711282

1272-
is_locked = models.BooleanField(
1273-
_("Is locked"),
1283+
locked_at = models.DateTimeField(
1284+
_("Locked at"),
12741285
help_text=_(
1275-
"If set to true, the project is temporarily locked. Locking is internal QFieldCloud mechanism related to file storage migration or other file operations."
1286+
"If not null, it means that the project is being migrated, and the datetime represents when the project was temporarily locked. Locking is internal QFieldCloud mechanism related to file storage migration or other file operations."
12761287
),
1277-
default=False,
1288+
blank=True,
1289+
null=True,
1290+
editable=False,
12781291
)
12791292

12801293
is_featured = models.BooleanField(
@@ -1842,7 +1855,7 @@ def delete(self, *args, **kwargs):
18421855
Todo:
18431856
* Delete with QF-4963 Drop support for legacy storage
18441857
"""
1845-
if self.legacy_thumbnail_uri:
1858+
if self.uses_legacy_storage:
18461859
storage.delete_project_thumbnail(self)
18471860

18481861
return super().delete(*args, **kwargs)

docker-app/qfieldcloud/core/templates/admin/project_files_widget.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<tr>
1818
<th>{% trans 'Filename' %}</th>
1919
<th>{% trans 'Last modified' %}</th>
20-
<th class="qfc-admin-project-files-text-right">{% trans 'Last size' %}</th>
20+
<th class="qfc-admin-project-files-text-right">{% trans 'Size' %}</th>
2121
<th>{% trans 'Details' %}</th>
2222
<th>{% trans 'File version' %}</th>
2323
<th>{% trans 'Actions' %}</th>
@@ -191,7 +191,7 @@
191191

192192
$trow.querySelector('td:nth-child(1)').innerHTML = file.name;
193193
$trow.querySelector('td:nth-child(2)').innerHTML = file.last_modified;
194-
$trow.querySelector('td:nth-child(3)').innerHTML = `<span title="${file.size} bytes">${filesize10(file.size)} KB</span>`;
194+
$trow.querySelector('td:nth-child(3)').innerHTML = `<span title="${file.size} bytes">${filesize10(file.size)}</span>`;
195195

196196
for (const version of file.versions) {
197197
const $option = document.createElement('option');

docker-app/qfieldcloud/filestorage/management/commands/migratefilestorage.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import logging
22
import traceback
33
import uuid
4-
from collections.abc import Collection
54
from datetime import datetime
65

76
from django.core.management.base import BaseCommand
@@ -55,8 +54,6 @@ def add_arguments(self, parser):
5554
)
5655

5756
def handle(self, *args, **options) -> None:
58-
projects: Collection = []
59-
6057
force: bool = options.get("force", False)
6158
accept: bool = options.get("accept", False)
6259
no_raise: bool = options.get("no_raise", False)
@@ -169,7 +166,7 @@ def handle(self, *args, **options) -> None:
169166

170167
exit(1)
171168

172-
logger.info(f"The storage migration will affect {len(projects)} project(s):")
169+
logger.info(f"The storage migration will affect {projects_count} project(s):")
173170

174171
for project in project_qs:
175172
logger.info(f"{project.id};{project.owner.username};{project.name}")

0 commit comments

Comments
 (0)