Skip to content

Commit 6b70b40

Browse files
Merge branch 'master' into release
2 parents d65197f + 20ad224 commit 6b70b40

File tree

17 files changed

+271
-15
lines changed

17 files changed

+271
-15
lines changed

docker-app/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ RUN apt-get update \
1717
libpq-dev \
1818
python3-dev \
1919
gcc \
20+
git \
2021
&& rm -rf /var/lib/apt/lists/*
2122

2223
# install `pip-compile` (as part of `pip-tools`)

docker-app/qfieldcloud/core/admin.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@
2525
from django.contrib.admin.templatetags.admin_urls import admin_urlname
2626
from django.contrib.admin.views.main import ChangeList
2727
from django.contrib.auth.models import Group
28-
from django.contrib.auth.views import redirect_to_login
28+
from django.contrib.auth.views import logout_then_login, redirect_to_login
2929
from django.core.exceptions import PermissionDenied, ValidationError
30+
from django.core.files.storage import storages
3031
from django.db.models import Q, QuerySet
3132
from django.db.models.fields.json import JSONField
3233
from django.db.models.functions import Lower
@@ -68,6 +69,7 @@
6869
from qfieldcloud.core.templatetags.filters import filesizeformat10
6970
from qfieldcloud.core.utils import get_file_storage_choices
7071
from qfieldcloud.core.utils2 import delta_utils, jobs, pg_service_file
72+
from qfieldcloud.filestorage.backend import QfcS3Boto3Storage
7173
from qfieldcloud.filestorage.models import File
7274

7375

@@ -99,6 +101,12 @@ def login(
99101
request.GET.get("next", ""), login_url=reverse(settings.LOGIN_URL)
100102
)
101103

104+
def logout( # type: ignore[override]
105+
self, request: HttpRequest, extra_context: dict[str, Any] | None = None
106+
) -> HttpResponse:
107+
"""Override the default Django admin logout view to redirect to the Allauth's logout view."""
108+
return logout_then_login(request)
109+
102110
# TODO consider adding a logout view to redirect to the Allauth's logout view, but then we lose the nice template we have right now.
103111

104112

@@ -855,6 +863,26 @@ def __init__(self, *args, **kwargs):
855863
)
856864
if self.instance.has_attachments_files:
857865
self.fields["attachments_file_storage"].disabled = True
866+
self.fields["are_attachments_versioned"].disabled = True
867+
868+
def clean_are_attachments_versioned(self):
869+
value = self.cleaned_data["are_attachments_versioned"]
870+
871+
if value:
872+
return value
873+
874+
# attachments can not be unversioned if attachments are stored on S3.
875+
attachment_storage_value = self.cleaned_data["attachments_file_storage"]
876+
attachment_storage = storages[attachment_storage_value]
877+
878+
if isinstance(attachment_storage, QfcS3Boto3Storage):
879+
raise ValidationError(
880+
_(
881+
"The '{}' attachments file storage is not compatible with unversioned attachment files."
882+
).format(attachment_storage_value)
883+
)
884+
885+
return value
858886

859887

860888
class ProjectAdmin(QFieldCloudModelAdmin):
@@ -899,6 +927,7 @@ class ProjectAdmin(QFieldCloudModelAdmin):
899927
"file_storage",
900928
"file_storage_migrated_at",
901929
"attachments_file_storage",
930+
"are_attachments_versioned",
902931
"is_attachment_download_on_demand",
903932
"project_files",
904933
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.25 on 2025-11-13 16:09
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("core", "0092_project_restricted_data_last_updated_at"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="project",
14+
name="are_attachments_versioned",
15+
field=models.BooleanField(
16+
default=True,
17+
help_text="If enabled, attachment files will make use of the file versioning system. If disabled, only the latest version of each attachment file will be kept, and stored with the extension in the filename.",
18+
verbose_name="Versioned attachment files",
19+
),
20+
),
21+
]

docker-app/qfieldcloud/core/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,6 +1262,14 @@ class Meta:
12621262
),
12631263
)
12641264

1265+
are_attachments_versioned = models.BooleanField(
1266+
default=True,
1267+
verbose_name=_("Versioned attachment files"),
1268+
help_text=_(
1269+
"If enabled, attachment files will make use of the file versioning system. If disabled, only the latest version of each attachment file will be kept, and stored with the extension in the filename."
1270+
),
1271+
)
1272+
12651273
restricted_data_last_updated_at = models.DateTimeField(
12661274
_("Restricted data last updated at"),
12671275
blank=True,

docker-app/qfieldcloud/core/staticfiles/css/admin.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
.login-logo img {
2-
width: 100%;
3-
}
4-
51
.object-tools{
62
margin-bottom: 1rem;
73
}

docker-app/qfieldcloud/core/staticfiles/css/qfieldcloud.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ h1, h2, h3, h4, h5, h6 {
3737

3838
.qfc-header-logo {
3939
height: 1.5rem;
40+
margin-left: 1rem;
41+
}
42+
43+
.qfc-logo-wrapper {
44+
display: flex;
45+
justify-content: center;
46+
align-items: center;
47+
min-height: 15rem;
48+
padding: 1rem 0;
49+
}
50+
51+
.qfc-main-logo {
52+
max-width: 100%;
53+
max-height: 15rem;
54+
width: auto;
55+
height: auto;
4056
}
4157

4258
.ml-5rem {
Lines changed: 1 addition & 0 deletions
Loading

docker-app/qfieldcloud/core/templates/account/base.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99

1010
{% sri_static 'css/vendor.css' %}
1111
{% sri_static 'css/qfieldcloud.css' %}
12-
<link rel="shortcut icon" type="image/x-icon" href="{% static 'favicon.ico' %}" />
12+
<link rel="shortcut icon" type="image/x-icon" href="{% static whitelabel.favicon %}" />
1313

1414
<title>
1515
{% block title %}
16-
{% block title_contents %}{{ title }}{% endblock title_contents %} | {{ site_title|default:_('QFieldCloud') }}
16+
{% block title_contents %}{{ title }}{% endblock title_contents %} | {{ whitelabel.site_title }}
1717
{% endblock title %}
1818
</title>
1919

@@ -25,7 +25,7 @@
2525
<!-- Navbar -->
2626
<nav class="navbar navbar-expand-md navbar-dark nav-fill w-100 bg-primary">
2727
<a href="{% url 'index' %}">
28-
<img src="{% static 'logo_sidetext_white.svg' %}" alt="QFieldCloud" class="logo-nav qfc-header-logo ml-1">
28+
<img src="{% static whitelabel.logo_navbar %}" alt="{{ whitelabel.logo_alt }}" class="qfc-header-logo">
2929
</a>
3030

3131

@@ -37,9 +37,9 @@
3737
<div class="flex-shrink-0 col-12">
3838
<div class="row">
3939
<div class="col-lg-6 offset-lg-3">
40-
<p class="text-center">
41-
<img src="{% static 'logo_undertext.svg' %}" alt="QFieldCloud" class="logo-nav" style="height: 15rem">
42-
</p>
40+
<div class="qfc-logo-wrapper">
41+
<img src="{% static whitelabel.logo_main %}" alt="{{ whitelabel.logo_alt }}" class="qfc-main-logo">
42+
</div>
4343

4444
{% block content %}{% endblock content %}
4545

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import copy
2+
from typing import Any
3+
4+
from django.conf import settings
5+
from django.http import HttpRequest
6+
from django.utils.translation import gettext as _
7+
8+
# Default whitelabel configuration
9+
DEFAULT_WHITELABEL = {
10+
# Branding
11+
"site_title": _("QFieldCloud"),
12+
# Logos (paths relative to static directory)
13+
"logo_navbar": "logo_sidetext_white.svg",
14+
"logo_main": "logo_undertext.svg",
15+
"favicon": "favicon.ico",
16+
}
17+
18+
19+
def get_whitelabel_settings() -> dict[str, Any]:
20+
"""
21+
Get whitelabel settings by merging user settings with defaults.
22+
Filters out None values from user settings to preserve defaults.
23+
"""
24+
# Start with a deep copy of defaults
25+
whitelabel_settings = copy.deepcopy(DEFAULT_WHITELABEL)
26+
27+
user_settings = {}
28+
for k, v in getattr(settings, "WHITELABEL", {}).items():
29+
if v is None:
30+
continue
31+
32+
user_settings[k] = v
33+
34+
# Update defaults with user settings
35+
whitelabel_settings.update(user_settings)
36+
37+
# Post-processing: Add any computed defaults or normalization here
38+
# Example: favicon could default to logo_navbar if not set
39+
# whitelabel_settings["favicon"] = whitelabel_settings["favicon"] or "default.ico"
40+
41+
return whitelabel_settings
42+
43+
44+
def whitelabel(request: HttpRequest) -> dict[str, Any]:
45+
"""Make whitelabel configuration available to all templates with translations."""
46+
47+
whitelabel_config = get_whitelabel_settings()
48+
49+
return {
50+
"whitelabel": whitelabel_config,
51+
}

docker-app/qfieldcloud/filestorage/backend.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,21 @@ def patch_nginx_download_redirect(self, response: HttpResponse) -> None:
259259
b64_auth = base64.b64encode(self.basic_auth.encode()).decode()
260260
basic_auth = f"Basic {b64_auth}"
261261
response["webdav_auth"] = basic_auth
262+
263+
def is_name_available(self, name, max_length=None):
264+
# TODO: Delete with QF-7176 and Django >= 5.1 upgrade.
265+
# see https://github.com/django/django/blob/6f35c2e1fd71ff8f349598a59689514e213490e7/django/core/files/storage/base.py#L54
266+
exceeds_max_length = max_length and len(name) > max_length
267+
return not self.exists(name) and not exceeds_max_length
268+
269+
def get_available_name(self, name: str, max_length: int | None = None) -> str:
270+
"""Returns a filename that is available on the configured webdav storage.
271+
272+
Arguments:
273+
name: desired relative path of the file on the webdav server.
274+
max_length: maximum length of the filename (not used)."""
275+
276+
if self.is_name_available(name, max_length):
277+
return super().get_available_name(name, max_length)
278+
279+
return name

0 commit comments

Comments
 (0)