Skip to content

Commit

Permalink
Merged master into release
Browse files Browse the repository at this point in the history
  • Loading branch information
suricactus committed Jul 27, 2024
2 parents 9daa49a + 5709b2d commit 16ad859
Show file tree
Hide file tree
Showing 25 changed files with 320 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ DJANGO_ALLOWED_HOSTS="localhost 127.0.0.1 0.0.0.0 app nginx"

SECRET_KEY=change_me

LETSENCRYPT_EMAIL="info@opengis.ch"
LETSENCRYPT_EMAIL="test@example.com"
LETSENCRYPT_RSA_KEY_SIZE=4096
# Set to 1 if you're testing your setup to avoid hitting request limits
LETSENCRYPT_STAGING=1
Expand Down
5 changes: 0 additions & 5 deletions .flake8

This file was deleted.

49 changes: 12 additions & 37 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,48 +9,23 @@ repos:
args:
- '--fix=lf'

# Remove unused imports/variables
- repo: https://github.com/myint/autoflake
rev: v2.2.1
# Lint and format
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.4
hooks:
- id: autoflake
args:
- "--in-place"
- "--remove-all-unused-imports"
- "--remove-unused-variables"

# Sort imports
- repo: https://github.com/pycqa/isort
rev: "5.12.0"
hooks:
- id: isort
args: ["--profile", "black"]

# Black formatting
- repo: https://github.com/psf/black
rev: "23.7.0"
hooks:
- id: black

# tool to automatically upgrade syntax for newer versions of the language
- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
hooks:
- id: pyupgrade
args: [--py37-plus]
# Run the linter.
- id: ruff
args: [ --fix ]

# Lint files
- repo: https://github.com/pycqa/flake8
rev: "6.1.0"
hooks:
- id: flake8
additional_dependencies: [flake8-match==1.0.0, flake8-walrus==1.1.0]
# Run the formatter.
- id: ruff-format

# Static type-checking with mypy
- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.5.1'
rev: 'v1.10.0'
hooks:
- id: mypy
additional_dependencies: [types-pytz, types-Deprecated, types-PyYAML, types-requests, types-tabulate, types-jsonschema, django-stubs]
additional_dependencies: [types-pytz, types-Deprecated, types-PyYAML, types-requests, types-tabulate, types-jsonschema, django-stubs, django-stubs-ext]
pass_filenames: false
entry: bash -c 'mypy -p docker-qgis -p docker-app "$@"' --
entry: bash -c 'mypy -p docker-app -p docker-qgis "$@"' --
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ QFieldCloud is a Django based service designed to synchronize projects and data

QFieldCloud allows seamless synchronization of your field data with your spatial infrastructure with change tracking, team management and online-offline work capabilities in QField.

# Hosted solution

## Hosted solution

If you're interested in quickly getting up and running, we suggest subscribing to the version hosted by OPENGIS.ch at https://qfield.cloud. This is also the instance that is integrated by default into QField.
<a href="https://qfield.cloud"><img alt="QFieldCloud logo" src="https://qfield.cloud/img/logo_horizontal_embedded_font.svg" width="100%"/></a>
Expand All @@ -15,8 +16,20 @@ If you're interested in quickly getting up and running, we suggest subscribing t
QField and QFieldCloud documentation is deployed [here](https://docs.qfield.org).


## Feature requests and issue reports

If you are interested in upcoming developments, or you want to suggest a new feature, please visit our ideas platform at [ideas.qfield.org](https://ideas.qfield.org).
Here, you can submit a new request or upvote existing ones.
To expedite developments by funding a feature, please email us at sales@qfield.cloud.

For questions about using the hosted service at [app.qfield.cloud](https://app.qfield.cloud), submit a ticket to our dedicated support platform at [tickets.qfield.cloud](https://tickets.qfield.cloud).

For self-hosted issues, please use the GitHub issues at https://github.com/opengisch/qfieldcloud/issues .


## Development


### Clone the repository

Clone the repository and all its submodules:
Expand Down
2 changes: 1 addition & 1 deletion docker-app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ COPY --from=build /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python

# install debug dependencies
ARG DEBUG_BUILD
RUN if [ "$DEBUG_BUILD" = "1" ]; then pip3 install debugpy; fi
RUN if [ "$DEBUG_BUILD" = "1" ]; then pip3 install debugpy ipython; fi

# add app group
RUN addgroup --system app && adduser --system app --ingroup app
Expand Down
20 changes: 19 additions & 1 deletion docker-app/qfieldcloud/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ def admin_urlname_by_obj(value, arg):
"email",
"date_joined",
"last_login",
"has_newsletter_subscription",
"has_accepted_tos",
"verified",
"owner_id",
"owner_username",
Expand All @@ -169,6 +171,8 @@ def admin_urlname_by_obj(value, arg):
"owner_last_name",
"owner_date_joined",
"owner_last_login",
"owner_has_newsletter_subscription",
"owner_has_accepted_tos",
],
)

Expand Down Expand Up @@ -217,22 +221,29 @@ def gen_users_email_addresses(self) -> Generator[UserEmailDetails, None, None]:
u.date_joined,
u.last_login,
u.type,
p.has_newsletter_subscription,
p.has_accepted_tos,
ae.verified,
oo.id AS "owner_id",
oo.username AS "owner_username",
oo.email AS "owner_email",
oo.first_name AS "owner_first_name",
oo.last_name AS "owner_last_name",
oo.date_joined AS "owner_date_joined",
oo.last_login AS "owner_last_login"
oo.last_login AS "owner_last_login",
p.has_newsletter_subscription AS "owner_has_newsletter_subscription",
p.has_accepted_tos AS "owner_has_accepted_tos"
FROM
u
LEFT JOIN account_emailaddress ae ON ae.user_id = u.id
LEFT JOIN core_person p ON p.user_ptr_id = u.id
LEFT JOIN core_organization o ON o.user_ptr_id = u.id
LEFT JOIN u oo ON oo.id = o.organization_owner_id
LEFT JOIN core_person oop ON oop.user_ptr_id = oo.id
ORDER BY u.id
"""
)

return (
UserEmailDetails(
row.id,
Expand All @@ -243,6 +254,8 @@ def gen_users_email_addresses(self) -> Generator[UserEmailDetails, None, None]:
row.email,
row.date_joined,
row.last_login,
row.has_newsletter_subscription,
row.has_accepted_tos,
row.verified,
row.owner_id,
row.owner_username,
Expand All @@ -251,6 +264,8 @@ def gen_users_email_addresses(self) -> Generator[UserEmailDetails, None, None]:
row.owner_last_name,
row.owner_date_joined,
row.owner_last_login,
row.owner_has_newsletter_subscription,
row.owner_has_accepted_tos,
)
for row in raw_queryset
)
Expand Down Expand Up @@ -510,6 +525,8 @@ def save_model(self, request, obj, form, change):
obj.set_password(obj.password)
else:
obj.set_password(obj.password)

obj.clean()
obj.save()

def get_urls(self):
Expand Down Expand Up @@ -758,6 +775,7 @@ class ProjectAdmin(QFieldCloudModelAdmin):
"status",
"status_code",
"project_filename",
"has_restricted_projectfiles",
"file_storage_bytes",
"storage_keep_versions",
"packaging_offliner",
Expand Down
9 changes: 9 additions & 0 deletions docker-app/qfieldcloud/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ class MultipleProjectsError(QFieldCloudException):
status_code = status.HTTP_400_BAD_REQUEST


class RestrictedProjectModificationError(QFieldCloudException):
"""Raised when a user with insufficient role is trying to modify QGIS/QField projectfiles
of a project that has the 'has_restricted_projectfiles' flag set"""

code = "restricted_project_modification"
message = "Restricted project modification"
status_code = status.HTTP_400_BAD_REQUEST


class DeltafileValidationError(QFieldCloudException):
"""Raised when a deltafile validation fails"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.25 on 2024-05-25 10:20

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0075_auto_20240323_1419"),
]

operations = [
migrations.AddField(
model_name="project",
name="has_restricted_projectfiles",
field=models.BooleanField(
default=False,
help_text="Restrict modifications of QGIS/QField projectfiles to managers and administrators.",
),
),
]
38 changes: 38 additions & 0 deletions docker-app/qfieldcloud/core/migrations/0077_alter_user_username.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 3.2.25 on 2024-07-10 14:25

import django.core.validators
from django.db import migrations, models
import qfieldcloud.core.validators


class Migration(migrations.Migration):
dependencies = [
("core", "0076_project_restrict_project_modification"),
]

operations = [
migrations.AlterField(
model_name="user",
name="username",
field=models.CharField(
error_messages={"unique": "A user with that username already exists."},
help_text="Between 3 and 150 characters. Letters, digits, underscores '_' or hyphens '-' only. Must begin with a letter.",
max_length=150,
unique=True,
validators=[
django.core.validators.RegexValidator(
"^[-a-zA-Z0-9_]+$",
"Only letters, numbers, underscores '_' or hyphens '-' are allowed.",
),
django.core.validators.RegexValidator(
"^[a-zA-Z].*$", "Must begin with a letter."
),
django.core.validators.RegexValidator(
"^.{3,}$", "Must be at least 3 characters long."
),
qfieldcloud.core.validators.reserved_words_validator,
],
verbose_name="username",
),
),
]
49 changes: 34 additions & 15 deletions docker-app/qfieldcloud/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,17 +227,15 @@ class Type(models.IntegerChoices):
max_length=150,
unique=True,
help_text=_(
"Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
"Between 3 and 150 characters. Letters, digits, underscores '_' or hyphens '-' only. Must begin with a letter."
),
validators=[
RegexValidator(
r"^[-a-zA-Z0-9_]+$",
"Only letters, numbers, underscores or hyphens are allowed.",
),
RegexValidator(r"^[a-zA-Z].*$", _("The name must begin with a letter.")),
RegexValidator(
r"^.{3,}$", _("The name must be at least 3 characters long.")
"Only letters, numbers, underscores '_' or hyphens '-' are allowed.",
),
RegexValidator(r"^[a-zA-Z].*$", _("Must begin with a letter.")),
RegexValidator(r"^.{3,}$", _("Must be at least 3 characters long.")),
validators.reserved_words_validator,
],
error_messages={
Expand Down Expand Up @@ -363,8 +361,22 @@ class Meta:
verbose_name = "person"
verbose_name_plural = "people"

def clean(self):
person_qs = self.__class__.objects.filter(email__iexact=self.email)

if self.pk:
person_qs = person_qs.exclude(pk=self.pk)

if person_qs.exists():
raise ValidationError(
_("This email is already taken by another user!").format(self.email)
)

return super().clean()

def save(self, *args, **kwargs):
self.type = User.Type.PERSON

return super().save(*args, **kwargs)


Expand Down Expand Up @@ -398,7 +410,9 @@ class UserAccount(models.Model):
twitter = models.CharField(max_length=255, default="", blank=True)
is_email_public = models.BooleanField(default=False)
avatar_uri = models.CharField(_("Profile Picture URI"), max_length=255, blank=True)
timezone = TimeZoneField(default="Europe/Zurich", choices_display="WITH_GMT_OFFSET")
timezone = TimeZoneField(
default=settings.TIME_ZONE, choices_display="WITH_GMT_OFFSET"
)

notifs_frequency = models.DurationField(
verbose_name=_("Email frequency for notifications"),
Expand Down Expand Up @@ -732,9 +746,7 @@ def clean(self) -> None:
if self.organization.organization_owner == self.member:
raise ValidationError(_("Cannot add the organization owner as a member."))

max_organization_members = (
self.organization.useraccount.current_subscription.plan.max_organization_members
)
max_organization_members = self.organization.useraccount.current_subscription.plan.max_organization_members
if (
max_organization_members > -1
and self.organization.members.count() >= max_organization_members
Expand Down Expand Up @@ -933,8 +945,9 @@ class Status(models.TextChoices):

class StatusCode(models.TextChoices):
OK = "ok", _("Ok")
FAILED_PROCESS_PROJECTFILE = "failed_process_projectfile", _(
"Failed process projectfile"
FAILED_PROCESS_PROJECTFILE = (
"failed_process_projectfile",
_("Failed process projectfile"),
)
TOO_MANY_COLLABORATORS = "too_many_collaborators", _("Too many collaborators")

Expand Down Expand Up @@ -1016,6 +1029,14 @@ 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."
),
)

has_restricted_projectfiles = models.BooleanField(
default=False,
help_text=_(
"Restrict modifications of QGIS/QField projectfiles to managers and administrators."
),
)

thumbnail_uri = models.CharField(
_("Thumbnail Picture URI"), max_length=255, blank=True
)
Expand Down Expand Up @@ -1268,9 +1289,7 @@ def status(self) -> Status:
else:
status = Project.Status.OK
status_code = Project.StatusCode.OK
max_premium_collaborators_per_private_project = (
self.owner.useraccount.current_subscription.plan.max_premium_collaborators_per_private_project
)
max_premium_collaborators_per_private_project = self.owner.useraccount.current_subscription.plan.max_premium_collaborators_per_private_project

# TODO use self.problems to get if there are project problems
if not self.project_filename or not self.project_details:
Expand Down
Loading

0 comments on commit 16ad859

Please sign in to comment.