Coverage for gws-app/gws/plugin/qfieldcloud/core.py: 100%
65 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-23 20:06 +0200
1"""QField project configuration and object."""
3from typing import Optional
4import gws
5import gws.plugin.qgis.provider
6import gws.plugin.model_field.file as file_field
8from . import api
11class DeltaApplyError(gws.Error):
12 """Raised when a delta payload cannot be applied."""
15class DeltaStatus(gws.Data):
16 """Status of a delta, as returned to QField."""
18 id: str
19 """Delta id."""
20 deltafile_id: str
21 """Id of the delta payload."""
22 created_by: str
23 """Login name of the user who sent the delta."""
24 created_at: str
25 """Creation time."""
26 updated_at: str
27 """Last update time."""
28 status: api.DeltaStatusType
29 """Delta status."""
30 output: str
31 """Error message, if the delta could not be applied."""
32 content: api.Delta
33 """Delta as sent by QField."""
36class SyncFileConfig(gws.Config):
37 """File field whose content is sent to QField. (added in 8.5)"""
39 field: str
40 """Name of a file field in the project models."""
41 useBlob: bool = False
42 """Send the content as a binary GeoPackage column instead of a file."""
45class SyncFile(gws.Data):
46 """File field whose content is sent to QField."""
48 model: gws.Model
49 """Model the file field belongs to."""
50 field: file_field.Object
51 """File field."""
52 useBlob: bool
53 """Send the content as a binary GeoPackage column, named after the ``contentColumn``, instead of a file under the path in the ``nameColumn``."""
56class ProjectConfig(gws.ConfigWithAccess):
57 """Project offered to QField, packaged from a QGIS project."""
59 title: str = ''
60 """Project title shown in QField."""
61 provider: gws.plugin.qgis.provider.Config
62 """QGIS project the QField package is built from."""
63 models: Optional[list[gws.ext.config.model]]
64 """Data models for editable layers, matched by table name."""
65 mapCacheLifeTime: gws.Duration = '0'
66 """How long rendered offline base maps are reused."""
67 thumbnail: Optional[gws.FilePath]
68 """Thumbnail image shown in the project details. (added in 8.5)"""
69 syncFiles: Optional[list[SyncFileConfig]]
70 """File fields whose content is sent to QField. (added in 8.5)"""
73class QfcProject(gws.Node):
74 """Project offered to QField.
76 Holds the QGIS provider of the source project, the models for editable
77 layers and the base map settings used when the project is packaged.
78 """
80 title: str
81 """Project title, the uid if no title is configured."""
82 qgisProvider: gws.plugin.qgis.provider.Object
83 """QGIS provider for the source project."""
84 models: list[gws.DatabaseModel]
85 """Configured models for editable layers."""
86 mapCacheLifeTime: int
87 """How long rendered base maps are reused, in seconds. ``0`` disables the cache."""
88 thumbnail: str
89 """Path to the thumbnail image, or an empty string."""
90 syncFiles: list[SyncFile]
91 """File fields whose content is sent to QField."""
93 def configure(self):
94 self.title = self.cfg('title', '') or self.uid
95 self.qgisProvider = self.create_child(gws.plugin.qgis.provider.Object, self.cfg('provider'))
96 self.root.app.register_supported_crs(self.qgisProvider.forceCrs)
97 self.models = self.create_children(gws.ext.object.model, self.cfg('models'))
98 self.mapCacheLifeTime = self.cfg('mapCacheLifeTime') or 0
99 self.thumbnail = self.cfg('thumbnail') or ''
100 self.syncFiles = [self.configure_sync_file(c) for c in self.cfg('syncFiles', default=[])]
102 def configure_sync_file(self, cfg: SyncFileConfig) -> SyncFile:
103 """Find the file field for a ``syncFiles`` entry.
105 The columns are checked in the field config, because file fields set their column names in ``post_configure``.
107 Args:
108 cfg: ``syncFiles`` entry.
110 Returns:
111 The file field and the blob flag.
113 Raises:
114 ``gws.ConfigurationError``: If the name does not match exactly one file field in the project models,
115 the field has no ``contentColumn``, or has no ``nameColumn`` and ``useBlob`` is not set.
116 """
117 found = [f for m in self.models for f in m.fields if f.name == cfg.field and isinstance(f, file_field.Object)]
118 if len(found) != 1:
119 raise gws.ConfigurationError(f'syncFiles: expected one file field {cfg.field!r}, found {len(found)}')
120 fld = found[0]
121 if not fld.cfg('contentColumn'):
122 raise gws.ConfigurationError(f'syncFiles: file field {cfg.field!r} has no contentColumn')
123 if not fld.cfg('nameColumn') and not cfg.useBlob:
124 raise gws.ConfigurationError(f'syncFiles: file field {cfg.field!r} has no nameColumn')
125 return SyncFile(
126 model=fld.model,
127 field=fld,
128 useBlob=bool(cfg.useBlob),
129 )
132def gp_field_name(name: str) -> str:
133 """GeoPackage column name for a model field name. ``fid`` is renamed to ``fid_gws``, because GDAL uses it internally."""
134 if name.lower() == 'fid':
135 return 'fid_gws'
136 return name
139def db_field_name(name: str) -> str:
140 """Model field name for a GeoPackage column name, the reverse of ``gp_field_name``."""
141 if name == 'fid_gws':
142 return 'fid'
143 return name