Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Mergin/projects_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
LoginError,
find_qgis_files,
login_error_message,
lost_project_access,
mergin_project_local_path,
same_dir,
send_logs,
Expand Down Expand Up @@ -234,14 +235,15 @@ def create_project(self, project_name, project_dir, is_public, namespace):

return True

def refresh_project_role_variable(self, project_dir):
def refresh_project_role_variable(self, project_dir, lost_access=False):
"""Re-read the role the client just refreshed, unless this is not the project open in QGIS.

Both sync and the status dialog can be started from the Browser panel for any downloaded
project, while the variable only ever describes the open one.
"""
if same_dir(project_dir, mergin_project_local_path()):
write_project_role_variable(MerginProject(project_dir).project_role())
role = None if lost_access else MerginProject(project_dir).project_role()
write_project_role_variable(role)

def project_status(self, project_dir):
if project_dir is None:
Expand Down Expand Up @@ -293,6 +295,8 @@ def project_status(self, project_dir):
self.reset_local_changes(project_dir, dlg.file_to_reset)

except (URLError, ClientError, InvalidProject) as e:
if lost_project_access(e):
self.refresh_project_role_variable(project_dir, lost_access=True)
msg = f"Failed to get status for project {project_name}:\n\n{str(e)}"
QMessageBox.critical(None, "Project status", msg, QMessageBox.StandardButton.Close)
except AuthTokenExpiredError:
Expand Down
11 changes: 8 additions & 3 deletions Mergin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,11 @@ def get_local_mergin_projects_info(workspace=None):
return local_projects_info


def lost_project_access(error):
"""Whether the server refused to read the project because the user's access to it was removed."""
return isinstance(error, ClientError) and error.http_error == 403


def refresh_project_role(mc, project_dir):
"""Ask the server for the user's current role on the project."""
try:
Expand All @@ -1034,9 +1039,9 @@ def refresh_project_role(mc, project_dir):
# servers below 2025.8.2 have no v2 project info, and old projects have no id in their metadata
role = mc.project_info(mp.project_full_name())["role"]
write_project_role_variable(role)
except (InvalidProject, ClientError, URLError):
# keep the role already read from the metadata, the variable has to stay usable offline
pass
except (InvalidProject, ClientError, URLError, LoginError, AuthTokenExpiredError) as e:
if lost_project_access(e):
write_project_role_variable(None)


def set_qgis_project_mergin_variables(project_dir):
Expand Down
31 changes: 31 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
import tempfile
from pathlib import Path
from typing import Dict
from unittest.mock import MagicMock
from urllib.error import URLError

import pytest

from qgis.core import (
QgsCoordinateReferenceSystem,
Expand All @@ -23,10 +27,14 @@
from qgis.PyQt.QtCore import QVariant

from Mergin.utils import (
AuthTokenExpiredError,
ClientError,
create_map_sketches_layer,
create_tracking_layer,
get_datum_shift_grids,
is_valid_name,
LoginError,
refresh_project_role,
remove_project_role_variable,
same_schema,
set_qgis_project_mergin_variables,
Expand Down Expand Up @@ -259,3 +267,26 @@ def test_project_role_variable_missing_in_metadata(mergin_project_dir: Path):
set_qgis_project_mergin_variables(str(mergin_project_dir))
assert QgsExpressionContextUtils.globalScope().variable("mm_project_role") == ""
remove_project_role_variable()


@pytest.mark.parametrize(
"error, expected_role",
[
(ClientError("Forbidden", http_error=403), ""),
(ClientError("Internal server error", http_error=500), "editor"),
(URLError("offline"), "editor"),
(AuthTokenExpiredError("Token has expired - please re-login"), "editor"),
(LoginError("Invalid username or password"), "editor"),
],
)
def test_refresh_project_role_on_server_error(mergin_project_dir: Path, error: Exception, expected_role: str):
"""Only removed access clears the role, any other failure keeps the one from the last sync."""
set_qgis_project_mergin_variables(str(mergin_project_dir))
mc = MagicMock()
mc.project_info_v2.side_effect = error
mc.project_info.side_effect = error

refresh_project_role(mc, str(mergin_project_dir))

assert QgsExpressionContextUtils.globalScope().variable("mm_project_role") == expected_role
remove_project_role_variable()
Loading