Coverage for gws-app/gws/plugin/model_field/file/__init__.py: 0%
153 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""File field."""
3from typing import Optional, cast
5import gws
6import gws.base.database.model
7import gws.base.model.field
8import gws.lib.mime
9import gws.lib.osx
10import gws.lib.sa as sa
12gws.ext.new.modelField('file')
15class Config(gws.base.model.field.Config):
16 """Configuration for the file field."""
18 contentColumn: str = ''
19 """Column name for the file content, if stored in the database."""
20 pathColumn: str = ''
21 """Column name for the file path, if stored in the filesystem."""
22 nameColumn: str = ''
23 """Column name for the file name, if stored in the database or filesystem."""
26class Props(gws.base.model.field.Props):
27 pass
30class Cols(gws.Data):
31 content: Optional[sa.Column]
32 path: Optional[sa.Column]
33 name: Optional[sa.Column]
36class FileInputProps(gws.Data):
37 content: bytes
38 name: str
41class ServerFileProps(gws.Data):
42 downloadUrl: str
43 extension: str
44 label: str
45 previewUrl: str
46 size: int
49class ClientFileProps(gws.Data):
50 name: str
51 content: bytes
54class FileValue(gws.Data):
55 content: bytes
56 name: str
57 path: str
58 size: int
61class Object(gws.base.model.field.Object):
62 model: gws.DatabaseModel
64 attributeType = gws.AttributeType.file
65 cols: Cols
67 def __getstate__(self):
68 return gws.u.omit(vars(self), 'cols')
70 def post_configure(self):
71 self.configure_columns()
73 def activate(self):
74 self.configure_columns()
76 def configure_columns(self):
77 model = cast(gws.base.database.model.Object, self.model)
79 self.cols = Cols()
81 p = self.cfg('contentColumn')
82 self.cols.content = model.column(p) if p else None
84 p = self.cfg('pathColumn')
85 self.cols.path = model.column(p) if p else None
87 p = self.cfg('nameColumn')
88 self.cols.name = model.column(p) if p else None
90 if self.cols.content is None and self.cols.path is None:
91 raise gws.ConfigurationError('contentColumn or pathColumn must be set')
93 if not self.model.uidName:
94 raise gws.ConfigurationError('file fields require a primary key')
96 def configure_widget(self):
97 if not super().configure_widget():
98 self.widget = self.root.create_shared(gws.ext.object.modelWidget, type='file')
99 return True
101 ##
103 def before_select(self, mc):
104 mc.dbSelect.columns.extend(self.select_columns(False, mc))
106 def after_select(self, features, mc):
107 for feature in features:
108 self.from_record(feature, mc)
110 def before_create(self, feature, mc):
111 self.to_record(feature, mc)
113 def before_update(self, feature, mc):
114 self.to_record(feature, mc)
116 def from_record(self, feature, mc):
117 feature.set(self.name, self.load_value(feature.record.attributes, mc))
119 def to_record(self, feature, mc):
120 if not mc.user.can_write(self):
121 return
123 # @TODO store in the filesystem
125 fv = cast(FileValue, feature.get(self.name))
126 if fv:
127 if self.cols.content is not None:
128 feature.record.attributes[self.cols.content.name] = fv.content
129 if self.cols.name is not None:
130 feature.record.attributes[self.cols.name.name] = fv.name
132 # @TODO merge with scalar_field?
134 def from_props(self, feature, mc):
135 value = feature.props.attributes.get(self.name)
136 if value is not None:
137 value = self.prop_to_python(feature, value, mc)
138 if value is not None:
139 feature.set(self.name, value)
141 def to_props(self, feature, mc):
142 if not mc.user.can_read(self):
143 return
144 value = feature.get(self.name)
145 if value is not None:
146 value = self.python_to_prop(feature, value, mc)
147 if value is not None:
148 feature.props.attributes[self.name] = value
150 ##
152 def prop_to_python(self, feature, value, mc) -> FileValue:
153 try:
154 return FileValue(
155 content=gws.u.get(value, 'content'),
156 name=gws.u.get(value, 'name'),
157 )
158 except ValueError:
159 return gws.ErrorValue
161 def python_to_prop(self, feature, value, mc) -> ServerFileProps:
162 fv = cast(FileValue, value)
164 mime = self.get_mime_type(fv)
165 ext = gws.lib.mime.extension_for(mime)
167 p = ServerFileProps(
168 # @TODO use a template
169 label=fv.name or '',
170 extension=ext,
171 size=fv.size or 0,
172 previewUrl='',
173 downloadUrl='',
174 )
176 name = fv.name or f'gws.{ext}'
178 url_args = dict(
179 projectUid=mc.project.uid,
180 modelUid=self.model.uid,
181 fieldName=self.name,
182 featureUid=feature.uid(),
183 )
185 if mime.startswith('image'):
186 p.previewUrl = gws.u.action_url_path('webFile', preview=1, **url_args) + '/' + name
188 p.downloadUrl = gws.u.action_url_path('webFile', **url_args) + '/' + name
190 return p
192 ##
194 def get_mime_type(self, fv: FileValue) -> str:
195 if fv.path:
196 return gws.lib.mime.for_path(fv.path)
197 if fv.name:
198 return gws.lib.mime.for_path(fv.name)
199 # @TODO guess mime from content?
200 return gws.lib.mime.TXT
202 def handle_web_file_request(self, feature_uid: str, preview: bool, mc: gws.ModelContext) -> Optional[gws.ContentResponse]:
203 model = cast(gws.DatabaseModel, self.model)
205 sql = sa.select(*self.select_columns(True, mc)).where(model.uid_column().__eq__(feature_uid))
207 with self.model.db.connect() as conn:
208 rs = list(conn.execute(sql))
209 if not rs:
210 return
212 for row in rs:
213 fv = self.load_value(gws.u.to_dict(row), mc)
214 return gws.ContentResponse(
215 asAttachment=not preview,
216 attachmentName=fv.name,
217 content=fv.content,
218 mime=self.get_mime_type(fv),
219 )
221 ##
223 def select_columns(self, with_content, mc):
224 cs = []
226 if self.cols.content is not None:
227 cs.append(sa.func.length(self.cols.content).label(f'{self.name}_length'))
228 if with_content:
229 cs.append(self.cols.content)
231 if self.cols.path is not None:
232 cs.append(self.cols.path)
234 if self.cols.name is not None:
235 cs.append(self.cols.name)
237 return cs
239 def load_value(self, attributes: dict, mc) -> FileValue:
240 d = {}
242 if self.cols.content is not None:
243 d['size'] = attributes.get(f'{self.name}_length')
244 d['content'] = attributes.get(self.cols.content.name)
245 if self.cols.path is not None:
246 d['path'] = attributes.get(self.cols.path.name)
247 if self.cols.name is not None:
248 d['name'] = attributes.get(self.cols.name.name)
250 if d:
251 return FileValue(**d)