girder-sample-tracker 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ import os
2
+ from girder.plugin import GirderPlugin, registerPluginStaticContent
3
+ from girder.utility.model_importer import ModelImporter
4
+ from .rest.sample import Sample
5
+ from .models.sample import Sample as SampleModel
6
+
7
+
8
+ class SampleTrackerPlugin(GirderPlugin):
9
+ DISPLAY_NAME = "Sample Tracker"
10
+
11
+ def load(self, info):
12
+ ModelImporter.registerModel("sample", SampleModel, plugin="sample_tracker")
13
+ info["apiRoot"].sample = Sample()
14
+ registerPluginStaticContent(
15
+ plugin="sample_tracker",
16
+ css=["/style.css"],
17
+ js=["/girder-plugin-sample-tracker.umd.cjs"],
18
+ staticDir=os.path.join(os.path.dirname(__file__), "web_client", "dist"),
19
+ tree=info["serverRoot"],
20
+ )
File without changes
@@ -0,0 +1,96 @@
1
+ import datetime
2
+ import io
3
+
4
+ import cairosvg
5
+ import qrcode
6
+ from girder.constants import AccessType
7
+ from girder.models.model_base import AccessControlledModel
8
+ from qrcode.compat.etree import ET
9
+ from qrcode.image.svg import SvgPathFillImage
10
+
11
+
12
+ class Sample(AccessControlledModel):
13
+ def initialize(self):
14
+ self.name = "sample"
15
+ self.ensureIndices(["name"])
16
+
17
+ self.exposeFields(
18
+ level=AccessType.READ,
19
+ fields=(
20
+ "_id",
21
+ "created",
22
+ "creator",
23
+ "description",
24
+ "eventTypes",
25
+ "updated",
26
+ "name",
27
+ "events",
28
+ ),
29
+ )
30
+
31
+ def validate(self, doc):
32
+ return doc
33
+
34
+ def create(
35
+ self, name, creator, description=None, eventTypes=None, access=None, save=True
36
+ ):
37
+ now = datetime.datetime.utcnow()
38
+
39
+ sample = {
40
+ "name": name,
41
+ "creator": creator["_id"],
42
+ "created": now,
43
+ "description": description,
44
+ "eventTypes": eventTypes or [],
45
+ "updated": now,
46
+ "events": [],
47
+ }
48
+
49
+ if access is not None:
50
+ self.setAccessList(sample, access, save=False, user=creator)
51
+ else:
52
+ self.setUserAccess(sample, user=creator, level=AccessType.ADMIN, save=False)
53
+ if save:
54
+ sample = self.save(sample)
55
+
56
+ return sample
57
+
58
+ def add_event(self, sample, event, save=True):
59
+ sample["events"].insert(0, event)
60
+ sample["updated"] = event["created"]
61
+
62
+ if save:
63
+ sample = self.save(sample)
64
+
65
+ return sample
66
+
67
+ def qr_code(self, sample, url):
68
+ buf = io.BytesIO()
69
+ qr = qrcode.QRCode(
70
+ version=6,
71
+ error_correction=qrcode.constants.ERROR_CORRECT_H,
72
+ border=10,
73
+ image_factory=SvgPathFillImage,
74
+ )
75
+ qr.add_data(f"{url}/#sample/{sample['_id']}/add")
76
+ qr.make(fit=True)
77
+
78
+ img = qr.make_image(fill_color="black", back_color="white")
79
+ text = ET.SubElement(
80
+ img._img,
81
+ "text",
82
+ {
83
+ "x": "50%",
84
+ "y": "93%",
85
+ "dominant-baseline": "middle",
86
+ "text-anchor": "middle",
87
+ "font-size": "5",
88
+ "fill": "black",
89
+ },
90
+ )
91
+ text.text = sample["name"]
92
+ cairosvg.svg2png(
93
+ bytestring=img.to_string(encoding="unicode"), write_to=buf, dpi=300
94
+ )
95
+ buf.seek(0)
96
+ return buf
File without changes
@@ -0,0 +1,348 @@
1
+ import datetime
2
+ import math
3
+ from urllib.parse import urlparse
4
+
5
+ import cherrypy
6
+ from girder.api import access
7
+ from girder.api.describe import Description, autoDescribeRoute
8
+ from girder.api.rest import (
9
+ Resource,
10
+ filtermodel,
11
+ setContentDisposition,
12
+ setResponseHeader,
13
+ )
14
+ from girder.constants import AccessType, SortDir, TokenScope
15
+ from girder.exceptions import RestException, ValidationException
16
+ from girder.utility import ziputil
17
+ from girder.utility.progress import ProgressContext
18
+
19
+ from ..models.sample import Sample as SampleModel
20
+
21
+
22
+ def bytesio_iterator(bio, chunk_size=4096):
23
+ """
24
+ Iterates over the content of a BytesIO object in chunks.
25
+ """
26
+ while True:
27
+ chunk = bio.read(chunk_size)
28
+ if not chunk:
29
+ break
30
+ yield chunk
31
+
32
+
33
+ class Sample(Resource):
34
+ def __init__(self):
35
+ super(Sample, self).__init__()
36
+ self.resourceName = "sample"
37
+ self.route("GET", (), self.list_samples)
38
+ self.route("DELETE", (), self.delete_samples)
39
+ self.route("PUT", ("access",), self.bulk_update_access)
40
+ self.route("GET", (":id", "download"), self.download_sample)
41
+ self.route("POST", ("download",), self.download_samples)
42
+ self.route("GET", (":id",), self.get_sample)
43
+ self.route("PUT", (":id",), self.update_sample)
44
+ self.route("POST", (), self.create_sample)
45
+ self.route("DELETE", (":id",), self.delete_sample)
46
+ self.route("GET", (":id", "access"), self.get_access)
47
+ self.route("PUT", (":id", "access"), self.update_access)
48
+ self.route("POST", (":id", "event"), self.create_event)
49
+
50
+ @access.public
51
+ @autoDescribeRoute(
52
+ Description("List samples").pagingParams(
53
+ defaultSort="name", defaultSortDir=SortDir.DESCENDING
54
+ )
55
+ )
56
+ @filtermodel(model="sample", plugin="sample_tracker")
57
+ def list_samples(self, limit, offset, sort):
58
+ return SampleModel().findWithPermissions(
59
+ query={},
60
+ offset=offset,
61
+ limit=limit,
62
+ sort=sort,
63
+ user=self.getCurrentUser(),
64
+ level=AccessType.READ,
65
+ fields={"events": 0},
66
+ )
67
+
68
+ @access.public
69
+ @autoDescribeRoute(
70
+ Description("Get a sample by ID").modelParam(
71
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.READ
72
+ )
73
+ )
74
+ @filtermodel(model="sample", plugin="sample_tracker")
75
+ def get_sample(self, sample):
76
+ return sample
77
+
78
+ @access.user(scope=TokenScope.DATA_OWN)
79
+ @autoDescribeRoute(
80
+ Description("Update a sample")
81
+ .modelParam(
82
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.ADMIN
83
+ )
84
+ .param("name", "The name of the sample", required=False)
85
+ .param("description", "The description of the sample", required=False)
86
+ .jsonParam(
87
+ "eventTypes",
88
+ "The event types for the sample",
89
+ required=False,
90
+ requireArray=True,
91
+ )
92
+ )
93
+ @filtermodel(model="sample", plugin="sample_tracker")
94
+ def update_sample(self, sample, name, description, eventTypes):
95
+ if name:
96
+ sample["name"] = name
97
+ if description:
98
+ sample["description"] = description
99
+ if eventTypes != sample.get("eventTypes", []):
100
+ sample["eventTypes"] = eventTypes
101
+ sample["updated"] = datetime.datetime.utcnow()
102
+ return SampleModel().save(sample)
103
+
104
+ @access.user
105
+ @autoDescribeRoute(
106
+ Description("Create a sample")
107
+ .param("name", "The name of the sample", required=True)
108
+ .param("description", "The description of the sample", required=False)
109
+ .jsonParam(
110
+ "eventTypes",
111
+ "The event types for the sample",
112
+ required=False,
113
+ requireArray=True,
114
+ )
115
+ .param(
116
+ "batchSize",
117
+ "The size of the batch. Default 1. Cannot be less than 1 and greater than 64.",
118
+ required=False,
119
+ dataType="integer",
120
+ )
121
+ .jsonParam(
122
+ "access", "The access control list as a JSON object", requireObject=False
123
+ )
124
+ )
125
+ @filtermodel(model="sample", plugin="sample_tracker")
126
+ def create_sample(self, name, description, eventTypes, batchSize, access):
127
+ if batchSize is None:
128
+ batchSize = 1
129
+ if batchSize < 1 or batchSize > 64:
130
+ raise ValidationException(
131
+ "Batch size must be at least 1, but no more than 64."
132
+ )
133
+ if not eventTypes:
134
+ eventTypes = []
135
+ user = self.getCurrentUser()
136
+ samples = []
137
+ if batchSize > 1:
138
+ format_str = "{name}{i:0" + str(math.ceil(math.log10(batchSize))) + "d}"
139
+ for i in range(batchSize):
140
+ sample = SampleModel().create(
141
+ format_str.format(name=name, i=i + 1),
142
+ user,
143
+ description=description,
144
+ eventTypes=eventTypes,
145
+ access=access,
146
+ )
147
+ samples.append(sample)
148
+ else:
149
+ samples.append(
150
+ SampleModel().create(
151
+ name,
152
+ user,
153
+ description=description,
154
+ eventTypes=eventTypes,
155
+ access=access,
156
+ )
157
+ )
158
+ return samples[0]
159
+
160
+ @access.user
161
+ @autoDescribeRoute(
162
+ Description("Delete a sample").modelParam(
163
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.WRITE
164
+ )
165
+ )
166
+ @filtermodel(model="sample", plugin="sample_tracker")
167
+ def delete_sample(self, sample):
168
+ SampleModel().remove(sample)
169
+
170
+ @access.user
171
+ @autoDescribeRoute(
172
+ Description("Delete multiple samples")
173
+ .jsonParam(
174
+ "ids",
175
+ "The IDs of the samples to delete",
176
+ requireArray=True,
177
+ )
178
+ .param(
179
+ "progress",
180
+ "Whether to record progress on this task.",
181
+ default=False,
182
+ required=False,
183
+ dataType="boolean",
184
+ )
185
+ )
186
+ def delete_samples(self, ids, progress):
187
+ user = self.getCurrentUser()
188
+ total = len(ids)
189
+ with ProgressContext(
190
+ progress,
191
+ user=user,
192
+ title="Deleting resources",
193
+ message="Calculating size...",
194
+ ) as ctx:
195
+ ctx.update(total=total)
196
+ current = 0
197
+ for sample_id in ids:
198
+ doc = SampleModel().load(
199
+ sample_id, user=user, level=AccessType.ADMIN, exc=True
200
+ )
201
+ SampleModel().remove(doc, progress=ctx)
202
+ if progress:
203
+ current += 1
204
+ if ctx.progress["data"]["current"] != current:
205
+ ctx.update(current=current, message="Deleted sample")
206
+
207
+ @access.user
208
+ @autoDescribeRoute(
209
+ Description("Get the access control list for a sample").modelParam(
210
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.ADMIN
211
+ )
212
+ )
213
+ def get_access(self, sample):
214
+ return SampleModel().getFullAccessList(sample)
215
+
216
+ @access.user(scope=TokenScope.DATA_OWN)
217
+ @autoDescribeRoute(
218
+ Description("Update the access control list for a sample")
219
+ .modelParam(
220
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.ADMIN
221
+ )
222
+ .jsonParam(
223
+ "access", "The access control list as a JSON object", requireObject=True
224
+ )
225
+ .jsonParam(
226
+ "publicFlags",
227
+ "Public access control flags",
228
+ requireArray=True,
229
+ required=False,
230
+ )
231
+ .param(
232
+ "public",
233
+ "Whether the resource should be publicly visible",
234
+ dataType="boolean",
235
+ required=False,
236
+ )
237
+ .errorResponse("ID was invalid.")
238
+ .errorResponse("Admin access was denied for the sample.", 403)
239
+ )
240
+ def update_access(self, sample, access, publicFlags, public):
241
+ return SampleModel().setAccessList(
242
+ sample, access, save=True, user=self.getCurrentUser()
243
+ )
244
+
245
+ @access.user(scope=TokenScope.DATA_OWN)
246
+ @autoDescribeRoute(
247
+ Description("Update the access control list for multiple samples")
248
+ .jsonParam(
249
+ "ids",
250
+ "The IDs of the samples to update",
251
+ requireArray=True,
252
+ )
253
+ .jsonParam(
254
+ "access", "The access control list as a JSON object", requireObject=True
255
+ )
256
+ .jsonParam(
257
+ "publicFlags",
258
+ "Public access control flags",
259
+ requireArray=True,
260
+ required=False,
261
+ )
262
+ .param(
263
+ "public",
264
+ "Whether the resource should be publicly visible",
265
+ dataType="boolean",
266
+ required=False,
267
+ )
268
+ .errorResponse("ID was invalid.")
269
+ .errorResponse("Admin access was denied for the sample.", 403)
270
+ )
271
+ def bulk_update_access(self, ids, access, publicFlags, public):
272
+ user = self.getCurrentUser()
273
+ for sample_id in ids:
274
+ doc = SampleModel().load(
275
+ sample_id, user=user, level=AccessType.ADMIN, exc=True
276
+ )
277
+ sample = SampleModel().setAccessList(doc, access, save=True, user=user)
278
+ return sample
279
+
280
+ @access.user
281
+ @autoDescribeRoute(
282
+ Description("Create an event for a sample")
283
+ .modelParam(
284
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.WRITE
285
+ )
286
+ .param("eventType", "The type of the event", required=True)
287
+ .param("location", "The location of the event", required=False)
288
+ .param("comment", "Extra comment about the event", required=False)
289
+ )
290
+ @filtermodel(model="sample", plugin="sample_tracker")
291
+ def create_event(self, sample, eventType, location, comment):
292
+ user = self.getCurrentUser()
293
+ event = {
294
+ "comment": comment,
295
+ "created": datetime.datetime.utcnow(),
296
+ "creator": user["_id"],
297
+ "creatorName": f"{user['firstName']} {user['lastName']}",
298
+ "eventType": eventType,
299
+ "location": location,
300
+ }
301
+ return SampleModel().add_event(sample, event)
302
+
303
+ @access.public(scope=TokenScope.DATA_READ, cookie=True)
304
+ @autoDescribeRoute(
305
+ Description("Download a sample").modelParam(
306
+ "id", "The ID of the sample", model=SampleModel, level=AccessType.READ
307
+ )
308
+ )
309
+ def download_sample(self, sample):
310
+ url = urlparse(cherrypy.request.headers["Referer"])
311
+ girder_base = f"{url.scheme}://{url.netloc}"
312
+ qr_img = SampleModel().qr_code(sample, girder_base)
313
+ setResponseHeader("Content-Type", "image/png")
314
+ setContentDisposition(f"{sample['name']}.png")
315
+ return bytesio_iterator(qr_img)
316
+
317
+ @access.public(scope=TokenScope.DATA_READ, cookie=True)
318
+ @autoDescribeRoute(
319
+ Description("Download QR codes for a list of samples").jsonParam(
320
+ "ids",
321
+ "The IDs of the samples to download",
322
+ requireArray=True,
323
+ )
324
+ )
325
+ def download_samples(self, ids):
326
+ user = self.getCurrentUser()
327
+ url = urlparse(cherrypy.request.headers["Referer"])
328
+ girder_base = f"{url.scheme}://{url.netloc}"
329
+ for sample_id in ids:
330
+ if not SampleModel().load(sample_id, user=user, level=AccessType.READ):
331
+ raise RestException(f"Sample {sample_id} not found or access denied.")
332
+ setResponseHeader("Content-Type", "application/zip")
333
+ setContentDisposition("samples.zip")
334
+
335
+ def stream():
336
+ _zip = ziputil.ZipGenerator()
337
+ for sample_id in ids:
338
+ doc = SampleModel().load(sample_id, user=user, level=AccessType.READ)
339
+ qr_img = SampleModel().qr_code(doc, girder_base)
340
+
341
+ def qr_stream():
342
+ yield qr_img.getvalue()
343
+
344
+ for data in _zip.addFile(qr_stream, f"{doc['name']}.png"):
345
+ yield data
346
+ yield _zip.footer()
347
+
348
+ return stream