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 Aug 25, 2023
2 parents be7cbfc + 5885c49 commit 9ebe709
Show file tree
Hide file tree
Showing 15 changed files with 164 additions and 178 deletions.
10 changes: 0 additions & 10 deletions docker-app/qfieldcloud/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from deprecated import deprecated
from rest_framework import status


Expand Down Expand Up @@ -177,12 +176,3 @@ class ProjectAlreadyExistsError(QFieldCloudException):
code = "project_already_exists"
message = "This user already owns a project with the same name."
status_code = status.HTTP_400_BAD_REQUEST


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

code = "organization_has_max_number_of_members"
message = "Cannot add new organization members, account limit has been reached."
status_code = status.HTTP_403_FORBIDDEN
35 changes: 31 additions & 4 deletions docker-app/qfieldcloud/core/middleware/requests.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,44 @@
import io
import logging
import shutil

from constance import config
from django.conf import settings

logger = logging.getLogger(__name__)


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.
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;
- a byte-for-byte, non stealing copy of the raw body to inspect multipart boundaries.
"""

def middleware(request):
# add a copy of the request body to the request
if (
settings.SENTRY_DSN
and request.method == "POST"
and "Content-Length" in request.headers
and (
int(request.headers["Content-Length"])
< config.SENTRY_REQUEST_MAX_SIZE_TO_SEND
)
):
logger.info("Making a temporary copy for request body.")

input_stream = io.BytesIO(request.body)
output_stream = io.BytesIO()
shutil.copyfileobj(input_stream, output_stream)
request.body_stream = output_stream

request_attributes = {
"file_key": str(request.FILES.keys()),
"meta": str(request.META),
"files": request.FILES.getlist("file"),
}
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
Expand Down
27 changes: 6 additions & 21 deletions docker-app/qfieldcloud/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,6 @@ def current_subscription(self):
Subscription = get_subscription_model()
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):
from qfieldcloud.subscription.models import get_subscription_model
Expand All @@ -437,13 +432,6 @@ def avatar_url(self):
else:
return None

@property
@deprecated("Use `UserAccount().storage_used_bytes` instead")
# TODO delete this method after refactoring tests so it's no longer used there
def storage_used_mb(self) -> float:
"""Returns the storage used in MB"""
return self.storage_used_bytes / 1000 / 1000

@property
def storage_used_bytes(self) -> float:
"""Returns the storage used in bytes"""
Expand All @@ -457,14 +445,6 @@ def storage_used_bytes(self) -> float:

return used_quota

@property
@deprecated("Use `UserAccount().storage_free_bytes` instead")
# TODO delete this method after refactoring tests so it's no longer used there
def storage_free_mb(self) -> float:
"""Returns the storage quota left in MB (quota from account and packages minus storage of all owned projects)"""

return self.storage_free_bytes / 1000 / 1000

@property
def storage_free_bytes(self) -> float:
"""Returns the storage quota left in bytes (quota from account and packages minus storage of all owned projects)"""
Expand Down Expand Up @@ -1498,7 +1478,12 @@ def clean(self) -> None:
if self.collaborator.is_person:
members_qs = organization.members.filter(member=self.collaborator)

if not members_qs.exists():
# for organizations-owned projects, the candidate collaborator
# must be a member of the organization or the organization's owner
if not (
members_qs.exists()
or self.collaborator == organization.organization_owner
):
raise ValidationError(
_(
"Cannot add a user who is not a member of the organization as a project collaborator."
Expand Down
69 changes: 0 additions & 69 deletions docker-app/qfieldcloud/core/permissions_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import List, Literal, Union

from deprecated import deprecated
from django.utils.translation import gettext as _
from qfieldcloud.authentication.models import AuthToken
from qfieldcloud.core.models import (
Expand Down Expand Up @@ -336,33 +335,6 @@ def can_apply_pending_deltas_for_project(user: QfcUser, project: Project) -> boo
)


@deprecated("Use `can_set_delta_status_for_project` instead")
def can_apply_deltas(user: QfcUser, project: Project) -> bool:
return user_has_project_roles(
user,
project,
[
ProjectCollaborator.Roles.ADMIN,
ProjectCollaborator.Roles.MANAGER,
ProjectCollaborator.Roles.EDITOR,
ProjectCollaborator.Roles.REPORTER,
],
)


@deprecated("Use `can_set_delta_status_for_project` instead")
def can_overwrite_deltas(user: QfcUser, project: Project) -> bool:
return user_has_project_roles(
user,
project,
[
ProjectCollaborator.Roles.ADMIN,
ProjectCollaborator.Roles.MANAGER,
ProjectCollaborator.Roles.EDITOR,
],
)


def can_set_delta_status_for_project(user: QfcUser, project: Project) -> bool:
return user_has_project_roles(
user,
Expand Down Expand Up @@ -414,47 +386,6 @@ def can_create_delta(user: QfcUser, delta: Delta) -> bool:
return False


@deprecated("Use `can_set_delta_status` instead")
def can_retry_delta(user: QfcUser, delta: Delta) -> bool:
if not can_apply_deltas(user, delta.project):
return False

if delta.last_status not in (
Delta.Status.CONFLICT,
Delta.Status.NOT_APPLIED,
Delta.Status.ERROR,
):
return False

return True


@deprecated("Use `can_set_delta_status` instead")
def can_overwrite_delta(user: QfcUser, delta: Delta) -> bool:
if not can_overwrite_deltas(user, delta.project):
return False

if delta.last_status not in (Delta.Status.CONFLICT):
return False

return True


@deprecated("Use `can_set_delta_status` instead")
def can_ignore_delta(user: QfcUser, delta: Delta) -> bool:
if not can_apply_deltas(user, delta.project):
return False

if delta.last_status not in (
Delta.Status.CONFLICT,
Delta.Status.NOT_APPLIED,
Delta.Status.ERROR,
):
return False

return True


def can_read_jobs(user: QfcUser, project: Project) -> bool:
return user_has_project_roles(
user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@
});

$deleteBtn.addEventListener('click', () => {
const passPhrase = 'Here be dragons!';
const confirmation = prompt(`Are you sure you want to delete file "${file.name}"? This operation is irreversible, the file is deleted forever and the project may be damaged forever! Type "${passPhrase}" to confirm your destructive action!`);
const passPhrase = file.name;
const confirmation = prompt(`Are you sure you want to delete file "${passPhrase}"? This operation is irreversible, the file is deleted forever and the project may be damaged forever! Type "${passPhrase}" to confirm your destructive action!`);

if (confirmation !== passPhrase) {
$dialog.close();
Expand Down
25 changes: 19 additions & 6 deletions docker-app/qfieldcloud/core/tests/test_sentry.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
from io import StringIO
from os import environ
from io import BytesIO, StringIO
from unittest import skipIf

from rest_framework.test import APITestCase
from django.conf import settings
from django.test import Client, TestCase

from ..utils2.sentry import report_serialization_diff_to_sentry


class QfcTestCase(APITestCase):
class QfcTestCase(TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
# Let's set up a WSGI request the body of which we'll extract
response = Client().post(
"test123",
data={"file": BytesIO(b"Hello World")},
format="multipart",
)
request = response.wsgi_request
cls.body_stream = BytesIO(request.read())

@skipIf(
environ.get("SENTRY_DSN", False),
not settings.SENTRY_DSN,
"Do not run this test when Sentry's DSN is not set.",
)
def test_logging_with_sentry(self):
Expand Down Expand Up @@ -42,7 +54,8 @@ 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
"body_stream": self.body_stream,
"capture_message": True, # so that sentry receives attachments even when there's no exception/event,
}
will_be_sent = report_serialization_diff_to_sentry(**mock_payload)
self.assertTrue(will_be_sent)
23 changes: 12 additions & 11 deletions docker-app/qfieldcloud/core/utils2/projects.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
from typing import Tuple

from django.db.models import Q
from django.utils.translation import gettext as _
from qfieldcloud.core import invitations_utils as invitation
from qfieldcloud.core import permissions_utils as perms
from qfieldcloud.core.models import Person, Project, ProjectCollaborator, Team, User
from qfieldcloud.core.models import Person, Project, ProjectCollaborator, Team


def create_collaborator(
project: Project, user: User, created_by: Person
) -> Tuple[bool, str]:
project: Project, user: Person | Team, created_by: Person
) -> tuple[bool, str]:
"""Creates a new collaborator (qfieldcloud.core.ProjectCollaborator) if possible
Args:
project (Project): the project to add collaborator to
user (User): the user to be added as collaborator
user (Person | Team): the user to be added as collaborator
created_by (Person): the user that initiated the collaborator creation
Returns:
Tuple[bool, str]: success, message - whether the collaborator creation was success and explanation message of the outcome
tuple[bool, str]: success, message - whether the collaborator creation was success and explanation message of the outcome
"""
success, message = False, ""
user_type_name = "Team" if isinstance(user, Team) else "User"

try:
perms.check_can_become_collaborator(user, project)
Expand All @@ -34,12 +33,14 @@ def create_collaborator(
updated_by=created_by,
)
success = True
message = _('User "{}" has been invited to the project.').format(user.username)
message = _(
f'{user_type_name} "{user.username}" has been invited to the project.'
)
except perms.UserOrganizationRoleError:
message = _(
"User '{}' is not a member of the organization that owns the project. "
"Please add this user to the organization first."
).format(user.username)
f"{user_type_name} '{user.username}' is not a member of the organization that owns the project. "
f"Please add this {user_type_name.lower()} to the organization first."
)
except (
perms.AlreadyCollaboratorError,
perms.ReachedCollaboratorLimitError,
Expand Down
16 changes: 13 additions & 3 deletions docker-app/qfieldcloud/core/utils2/sentry.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from io import StringIO
from io import BytesIO, StringIO

import sentry_sdk

Expand All @@ -11,6 +11,7 @@ def report_serialization_diff_to_sentry(
pre_serialization: str,
post_serialization: str,
buffer: StringIO,
body_stream: BytesIO | None,
capture_message=False,
) -> bool:
"""
Expand All @@ -20,10 +21,13 @@ 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.
bodystream: BytesIO buffer capturing the request's raw body.
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:
logger.info("Sending explicit sentry report!")

filename = f"{name}_contents.txt"
scope.add_attachment(
bytes=bytes(
Expand All @@ -38,10 +42,16 @@ def report_serialization_diff_to_sentry(
bytes=bytes(buffer.getvalue(), encoding="utf8"),
filename=filename,
)

if body_stream:
filename = f"{name}_rawbody.txt"
scope.add_attachment(bytes=body_stream.getvalue(), filename=filename)

if capture_message:
sentry_sdk.capture_message("Sending to Sentry...", scope=scope)
sentry_sdk.capture_message("Explicit Sentry report!", scope=scope)
return True

except Exception as error:
logger.error(f"Unable to send file to Sentry: failed on {error}")
sentry_sdk.capture_exception(error)
logging.error(f"Unable to send file to Sentry: failed on {error}")
return False
Loading

0 comments on commit 9ebe709

Please sign in to comment.