Coverage for gws-app/gws/plugin/qfieldcloud/__init__.py: 100%

2 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""QField Cloud plugin. 

2 

3This plugin emulates the QFieldCloud API, so that the QField mobile app can 

4download projects from GWS and synchronize edits back. The QGIS projects are 

5packaged according to the settings made with the QFieldSync QGIS plugin. 

6 

7.. rubric:: Submodules 

8 

9- ``action``: the ``qfieldcloud`` action. It dispatches API requests to route handlers, authenticates clients by token, runs packaging jobs, stores incoming deltas and manages the package and cache directories. 

10- ``api``: data classes and enums mirroring the QFieldCloud API objects (``swagger.yaml``), plus a few objects used by the client that are not in the specification. 

11- ``auth``: the ``qfieldcloud`` authorization method, created and registered by the action. It has the fixed uid ``gws.plugin.qfieldcloud.auth``. 

12- ``caps``: reads the QFieldSync project and layer properties from the QGIS project and decides, per layer, whether it is packaged for editing, packaged as a base map or removed. 

13- ``cli``: the ``qfieldcloudPackage`` command, which creates a package into a directory. 

14- ``core``: the configuration and object of a single QField project. 

15- ``packager``: writes a package (GeoPackage data, base maps, media files, modified QGIS project). 

16- ``patcher``: applies changes ("deltas") and file uploads from QField to the database. 

17 

18.. rubric:: Design 

19 

20The action holds a list of QField projects (``core.QfcProject``), each built on 

21a QGIS project. For each QField project, ``caps.Parser`` creates a ``caps.Caps`` 

22object with the layer and model map; the action caches it until the QGIS project 

23source changes. Packaging and patching both work on these capabilities. 

24 

25The action answers requests under the raw command ``qfieldcloudApi``. A rewrite 

26rule maps a regular address to that command and names the GWS project:: 

27 

28 web.sites+ { 

29 rewriteRules+ { 

30 pattern "^/qfc/(.*)" 

31 target "/_/qfieldcloudApi/projectUid/my_project/$1" 

32 } 

33 } 

34 

35QField users enter the resulting address (``https://example.com/qfc``) as the 

36server and log in with their GWS credentials. The action brings its own 

37authorization method; an auth provider and a session manager must be configured. 

38Requests other than the public status and login routes require an 

39``Authorization: Token ...`` header with the session token. 

40 

41.. rubric:: Configuration 

42 

43The plugin exposes an action of type ``qfieldcloud``. In the action config, you can define multiple ``projects``, each representing a QField project:: 

44 

45 actions+ { 

46 type "qfieldcloud" 

47 

48 projects+ { 

49 title "My Project" 

50 provider.path "/path/to/file.qgs" 

51 } 

52 } 

53 

54.. rubric:: Downloading data 

55 

56Data flow GWS -> QField, also called "packaging". 

57 

58When QField requests a package job, the action runs the packager in a background job. 

59In the given QGIS project, the plugin looks for Postgres layers marked as "offline editable", fetches their data and writes each table into a GeoPackage file. A modified QGIS project file is also created, pointing to the GeoPackage layers instead of the original Postgres layers. Layers marked "remove" are removed from the project, and empty layer groups are dropped. 

60 

61For each offline table, a Model can be configured to customize field selection and data filtering. Models are defined in the project configuration and matched by table name. By default, the plugin uses a generic Postgres model that includes all fields and all features. 

62 

63Base map layers (a map theme or a single layer, as selected in QFieldSync) are rendered through QGIS into a single raster image covering the area of interest at the configured maximum zoom level. The rendered images are cached in the project cache directory for ``mapCacheLifeTime``. 

64 

65Directories listed in the QFieldSync attachment, data and copy settings are added to the package as media files. 

66 

67.. rubric:: Uploading data 

68 

69Data flow QField -> GWS, also called "patching". 

70 

71For each incoming "delta" payload from QField, the plugin extracts the created, updated and deleted features and passes them to the respective Model. The Model is responsible for applying the changes to the Postgres database. The payload is stored for an hour, so that QField can poll its status. 

72 

73.. rubric:: File uploads 

74 

75If a Model supports file uploads, it should contain a virtual file field with ``nameColumn`` and ``contentColumn``:: 

76 

77 actions+ { 

78 type "qfieldcloud" 

79 

80 projects+ { 

81 title "My Project" 

82 provider.path "/path/to/file.qgs" 

83 

84 models+ { 

85 type "postgres" 

86 ... 

87 fields+ { 

88 type "file" 

89 name "virtual_file_field" 

90 contentColumn "file_content" 

91 nameColumn "file_name" 

92 } 

93 } 

94 } 

95 } 

96 

97QField sends uploads in two steps: first, the file path is included along with the feature changes in the delta payload. Later, the actual file content is uploaded in a separate request. The plugin matches the file content to the respective feature based on the ``nameColumn`` value and writes it into ``contentColumn``. 

98 

99.. rubric:: Extending 

100 

101Override the packager and patcher classes to customize packaging and patching behavior. 

102In your custom action class, override ``get_packager()`` and ``get_patcher()`` methods to return your custom classes. 

103 

104Example:: 

105 

106 import gws.plugin.qfieldcloud.action 

107 import gws.plugin.qfieldcloud.patcher 

108 

109 class MyPatcher(gws.plugin.qfieldcloud.patcher.Object): 

110 def commit_operations_for_model(self, me, ops): 

111 ... 

112 super().commit_operations_for_model(me, ops) 

113 

114 class MyAction(gws.plugin.qfieldcloud.action.Object): 

115 def get_patcher(self): 

116 return MyPatcher() 

117""" 

118 

119from . import ( 

120 action, 

121 packager, 

122 patcher, 

123 caps, 

124) 

125 

126__all__ = [ 

127 'action', 

128 'packager', 

129 'patcher', 

130 'caps', 

131]