Skip to content

Commit

Permalink
Merge branch 'master' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
suricactus committed May 16, 2023
2 parents b47bd3b + b0a7472 commit f38ef4a
Show file tree
Hide file tree
Showing 33 changed files with 1,005 additions and 262 deletions.
2 changes: 1 addition & 1 deletion docker-app/qfieldcloud/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1080,7 +1080,7 @@ class OrganizationAdmin(QFieldCloudModelAdmin):
@admin.display(description=_("Active users (last billing period)"))
def active_users(self, instance) -> int:
try:
return instance.current_subscription.active_users_count
return instance.current_subscription_vw.active_users_count
except Exception:
return None

Expand Down
10 changes: 2 additions & 8 deletions docker-app/qfieldcloud/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from deprecated import deprecated
from rest_framework import status


Expand Down Expand Up @@ -153,14 +154,6 @@ class QGISPackageError(QFieldCloudException):
message = "QGIS is unable to open the QGIS project"


class QuotaError(QFieldCloudException):
"""Raised when a quota limitation is hit"""

code = "over_quota"
message = "Quota error"
status_code = status.HTTP_402_PAYMENT_REQUIRED


class ProjectAlreadyExistsError(QFieldCloudException):
"""Raised when a quota limitation is hit"""

Expand All @@ -169,6 +162,7 @@ class ProjectAlreadyExistsError(QFieldCloudException):
status_code = status.HTTP_400_BAD_REQUEST


@deprecated("moved to subscription")
class ReachedMaxOrganizationMembersError(QFieldCloudException):
"""Raised when an organization has exhausted its quota of members"""

Expand Down
27 changes: 27 additions & 0 deletions docker-app/qfieldcloud/core/migrations/0066_delta_client_id.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 3.2.18 on 2023-05-09 21:43

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0065_auto_20230422_1101"),
]

operations = [
migrations.AddField(
model_name="delta",
name="client_id",
field=models.UUIDField(db_index=True, null=True, editable=False),
),
migrations.RunSQL(
"UPDATE core_delta SET client_id = COALESCE((content->>'clientId')::uuid, deltafile_id)",
migrations.RunSQL.noop,
),
migrations.AlterField(
model_name="delta",
name="client_id",
field=models.UUIDField(db_index=True, null=False, editable=False),
),
]
23 changes: 23 additions & 0 deletions docker-app/qfieldcloud/core/migrations/0067_auto_20230515_1320.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 3.2.18 on 2023-05-15 11:20

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("core", "0066_delta_client_id"),
]

operations = [
migrations.AddField(
model_name="job",
name="docker_finished_at",
field=models.DateTimeField(blank=True, editable=False, null=True),
),
migrations.AddField(
model_name="job",
name="docker_started_at",
field=models.DateTimeField(blank=True, editable=False, null=True),
),
]
118 changes: 84 additions & 34 deletions docker-app/qfieldcloud/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
from django.utils.translation import gettext as _
from model_utils.managers import InheritanceManager, InheritanceManagerMixin
from qfieldcloud.core import geodb_utils, utils, validators
from qfieldcloud.core.exceptions import ReachedMaxOrganizationMembersError
from qfieldcloud.core.utils2 import storage
from qfieldcloud.subscription.exceptions import ReachedMaxOrganizationMembersError
from timezone_field import TimeZoneField

# http://springmeblog.com/2018/how-to-implement-multiple-user-types-with-django/
Expand Down Expand Up @@ -73,11 +73,11 @@ def for_project(self, project: "Project", skip_invalid: bool):
)

max_premium_collaborators_per_private_project_q = Q(
project_roles__project__owner__useraccount__current_subscription__plan__max_premium_collaborators_per_private_project=V(
project_roles__project__owner__useraccount__current_subscription_vw__plan__max_premium_collaborators_per_private_project=V(
-1
)
) | Q(
project_roles__project__owner__useraccount__current_subscription__plan__max_premium_collaborators_per_private_project__gte=count_collaborators
project_roles__project__owner__useraccount__current_subscription_vw__plan__max_premium_collaborators_per_private_project__gte=count_collaborators
)

project_role_is_valid_condition_q = is_public_q | (
Expand Down Expand Up @@ -307,7 +307,7 @@ def save(self, *args, **kwargs):

if not skip_account_creation:
account, _created = UserAccount.objects.get_or_create(user=self)
Subscription.get_or_create_active_subscription(account)
Subscription.get_or_create_current_subscription(account)
else:
super().save(*args, **kwargs)

Expand Down Expand Up @@ -403,11 +403,16 @@ class UserAccount(models.Model):
)

@property
def active_subscription(self):
def current_subscription(self):
from qfieldcloud.subscription.models import get_subscription_model

Subscription = get_subscription_model()
return Subscription.get_or_create_active_subscription(self)
return Subscription.get_or_create_current_subscription(self)

@property
@deprecated("Use `current_subscription` instead")
def active_subscription(self):
return self.current_subscription()

@property
def upcoming_subscription(self):
Expand Down Expand Up @@ -459,17 +464,17 @@ def storage_free_bytes(self) -> float:
"""Returns the storage quota left in bytes (quota from account and packages minus storage of all owned projects)"""

return (
self.active_subscription.active_storage_total_bytes
self.current_subscription.active_storage_total_bytes
- self.storage_used_bytes
)

@property
def storage_used_ratio(self) -> float:
"""Returns the storage used in fraction of the total storage"""
if self.active_subscription.active_storage_total_bytes > 0:
if self.current_subscription.active_storage_total_bytes > 0:
return min(
self.storage_used_bytes
/ self.active_subscription.active_storage_total_bytes,
/ self.current_subscription.active_storage_total_bytes,
1,
)
else:
Expand All @@ -483,7 +488,7 @@ def storage_free_ratio(self) -> float:
@property
def has_premium_support(self) -> bool:
"""A user has premium support if they have an active premium subscription plan or a at least one organization that they have admin role."""
subscription = self.active_subscription
subscription = self.current_subscription
if subscription.plan.is_premium:
return True

Expand Down Expand Up @@ -746,7 +751,7 @@ def clean(self) -> None:
raise ValidationError(_("Cannot add the organization owner as a member."))

max_organization_members = (
self.organization.useraccount.active_subscription.plan.max_organization_members
self.organization.useraccount.current_subscription.plan.max_organization_members
)
if (
max_organization_members > -1
Expand Down Expand Up @@ -840,7 +845,7 @@ class Meta:

def clean(self) -> None:
if (
self.team.team_organization.members.filter(member=self.member).count() == 0
not self.team.team_organization.members.filter(member=self.member).exists()
and self.team.team_organization.organization_owner != self.member
):
raise ValidationError(
Expand Down Expand Up @@ -898,11 +903,11 @@ def for_user(self, user: "User", skip_invalid: bool = False):
.filter(id=OuterRef("owner"))
)
max_premium_collaborators_per_private_project_q = Q(
owner__useraccount__current_subscription__plan__max_premium_collaborators_per_private_project=V(
owner__useraccount__current_subscription_vw__plan__max_premium_collaborators_per_private_project=V(
-1
)
) | Q(
owner__useraccount__current_subscription__plan__max_premium_collaborators_per_private_project__gte=count
owner__useraccount__current_subscription_vw__plan__max_premium_collaborators_per_private_project__gte=count
)

# Assemble the condition
Expand Down Expand Up @@ -1096,22 +1101,22 @@ def users(self):
def has_online_vector_data(self) -> Optional[bool]:
"""Returns None if project details or layers details are not available"""

# it's safer to assume there is an online vector layer
if not self.project_details:
return None

layers_by_id = self.project_details.get("layers_by_id")

# it's safer to assume there is an online vector layer
if layers_by_id is None:
return None

has_online_vector_layers = False

for layer_data in layers_by_id.values():
if layer_data.get("type_name") == "VectorLayer" and not layer_data.get(
"filename", ""
):
# NOTE QGIS 3.30.x returns "Vector", while previous versions return "VectorLayer"
if layer_data.get("type_name") in (
"VectorLayer",
"Vector",
) and not layer_data.get("filename", ""):
has_online_vector_layers = True
break

Expand Down Expand Up @@ -1151,7 +1156,7 @@ def status(self) -> Status:
status = Project.Status.OK
status_code = Project.StatusCode.OK
max_premium_collaborators_per_private_project = (
self.owner.useraccount.active_subscription.plan.max_premium_collaborators_per_private_project
self.owner.useraccount.current_subscription.plan.max_premium_collaborators_per_private_project
)

if not self.project_filename:
Expand All @@ -1175,10 +1180,10 @@ def status_code(self) -> StatusCode:

@property
def storage_size_perc(self) -> float:
if self.owner.useraccount.active_subscription.active_storage_total_bytes > 0:
if self.owner.useraccount.current_subscription.active_storage_total_bytes > 0:
return (
self.file_storage_bytes
/ self.owner.useraccount.active_subscription.active_storage_total_bytes
/ self.owner.useraccount.current_subscription.active_storage_total_bytes
* 100
)
else:
Expand Down Expand Up @@ -1206,7 +1211,34 @@ def delete(self, *args, **kwargs):
storage.delete_project_thumbnail(self)
super().delete(*args, **kwargs)

@property
def owner_can_create_job(self):
# NOTE consider including in status refactoring

from qfieldcloud.core.permissions_utils import (
is_supported_regarding_owner_account,
)

return is_supported_regarding_owner_account(self)

def check_can_be_created(self):
from qfieldcloud.core.permissions_utils import (
check_supported_regarding_owner_account,
)

check_supported_regarding_owner_account(self, ignore_online_layers=True)

def clean(self) -> None:
"""
Prevent creating new projects if the user is inactive or over quota
"""
if self._state.adding:
self.check_can_be_created()

return super().clean()

def save(self, recompute_storage=False, *args, **kwargs):
self.clean()
logger.info(f"Saving project {self}...")

if recompute_storage:
Expand Down Expand Up @@ -1235,13 +1267,13 @@ def validated(self, skip_invalid=False):
# Build the conditions with Q objects
is_public_q = Q(project__is_public=True)
is_team_collaborator = Q(collaborator__type=User.Type.TEAM)
# max_premium_collaborators_per_private_project_q = active_subscription_q & (
# max_premium_collaborators_per_private_project_q = current_subscription_q & (
max_premium_collaborators_per_private_project_q = Q(
project__owner__useraccount__current_subscription__plan__max_premium_collaborators_per_private_project=V(
project__owner__useraccount__current_subscription_vw__plan__max_premium_collaborators_per_private_project=V(
-1
)
) | Q(
project__owner__useraccount__current_subscription__plan__max_premium_collaborators_per_private_project__gte=count
project__owner__useraccount__current_subscription_vw__plan__max_premium_collaborators_per_private_project__gte=count
)

# Assemble the condition
Expand Down Expand Up @@ -1344,15 +1376,15 @@ def clean(self) -> None:
if self.collaborator.is_person:
members_qs = organization.members.filter(member=self.collaborator)

if members_qs.count() == 0:
if not members_qs.exists():
raise ValidationError(
_(
"Cannot add a user who is not a member of the organization as a project collaborator."
)
)
elif self.collaborator.is_team:
team_qs = organization.teams.filter(pk=self.collaborator)
if team_qs.count() == 0:
if not team_qs.exists():

raise ValidationError(_("Team does not exist."))

Expand Down Expand Up @@ -1403,6 +1435,7 @@ class Status(models.TextChoices):

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
deltafile_id = models.UUIDField(db_index=True)
client_id = models.UUIDField(null=False, db_index=True, editable=False)
project = models.ForeignKey(
Project,
on_delete=models.CASCADE,
Expand Down Expand Up @@ -1468,13 +1501,6 @@ def short_id(self):
def method(self):
return self.content.get("method")

@property
def is_supported_regarding_owner_account(self):
return (
not self.project.has_online_vector_data
or self.project.owner.useraccount.active_subscription.plan.is_external_db_supported
)


class Job(models.Model):

Expand Down Expand Up @@ -1510,6 +1536,8 @@ class Status(models.TextChoices):
updated_at = models.DateTimeField(auto_now=True, db_index=True)
started_at = models.DateTimeField(blank=True, null=True, editable=False)
finished_at = models.DateTimeField(blank=True, null=True, editable=False)
docker_started_at = models.DateTimeField(blank=True, null=True, editable=False)
docker_finished_at = models.DateTimeField(blank=True, null=True, editable=False)

@property
def short_id(self) -> str:
Expand Down Expand Up @@ -1546,6 +1574,23 @@ def fallback_output(self) -> str:
"The job ended in unknown state. Please verify the project is configured properly, try again and contact QFieldCloud support for more information."
)

def check_can_be_created(self):
from qfieldcloud.core.permissions_utils import (
check_supported_regarding_owner_account,
)

check_supported_regarding_owner_account(self.project)

def clean(self):
if self._state.adding:
self.check_can_be_created()

return super().clean()

def save(self, *args, **kwargs):
self.clean()
return super().save(*args, **kwargs)


class PackageJob(Job):
def save(self, *args, **kwargs):
Expand All @@ -1558,6 +1603,11 @@ class Meta:


class ProcessProjectfileJob(Job):
def check_can_be_created(self):
# Alsways create jobs because they are cheap
# and is always good to have updated metadata
pass

def save(self, *args, **kwargs):
self.type = self.Type.PROCESS_PROJECTFILE
return super().save(*args, **kwargs)
Expand Down
Loading

0 comments on commit f38ef4a

Please sign in to comment.