Total Complexity | 96 |
Total Lines | 614 |
Duplicated Lines | 5.54 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ui.objectmanager.crud often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | #!/usr/bin/env python |
||
2 | # -*- coding: UTF-8 -*- |
||
3 | |||
4 | # Isomer - The distributed application framework |
||
5 | # ============================================== |
||
6 | # Copyright (C) 2011-2020 Heiko 'riot' Weinen <[email protected]> and others. |
||
7 | # |
||
8 | # This program is free software: you can redistribute it and/or modify |
||
9 | # it under the terms of the GNU Affero General Public License as published by |
||
10 | # the Free Software Foundation, either version 3 of the License, or |
||
11 | # (at your option) any later version. |
||
12 | # |
||
13 | # This program is distributed in the hope that it will be useful, |
||
14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
16 | # GNU Affero General Public License for more details. |
||
17 | # |
||
18 | # You should have received a copy of the GNU Affero General Public License |
||
19 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
20 | |||
21 | """ |
||
22 | |||
23 | Module: objectmanager.crud |
||
24 | ========================== |
||
25 | |||
26 | CRUD operations for objects. CRUD stands for |
||
27 | |||
28 | * Create |
||
29 | * Read |
||
30 | * Update |
||
31 | * Delete |
||
32 | |||
33 | |||
34 | """ |
||
35 | |||
36 | from uuid import uuid4 |
||
37 | from ast import literal_eval |
||
38 | |||
39 | from isomer.component import handler |
||
40 | from isomer.database import objectmodels, ValidationError |
||
41 | from isomer.schemastore import schemastore |
||
42 | from isomer.events.client import send |
||
43 | from isomer.events.objectmanager import ( |
||
44 | get, |
||
45 | search, |
||
46 | getlist, |
||
47 | change, |
||
48 | put, |
||
49 | objectcreation, |
||
50 | objectchange, |
||
51 | delete, |
||
52 | objectdeletion, |
||
53 | ) |
||
54 | from isomer.logger import warn, verbose, error, debug, critical |
||
55 | from isomer.misc import nested_map_find, nested_map_update |
||
56 | from isomer.misc.std import std_uuid |
||
57 | from pymongo import ASCENDING, DESCENDING |
||
58 | |||
59 | from isomer.ui.objectmanager.cli import CliManager |
||
60 | |||
61 | WARN_SIZE = 500 |
||
62 | |||
63 | |||
64 | class CrudOperations(CliManager): |
||
65 | """Adds CRUD (create, read, update, delete) functionality""" |
||
66 | |||
67 | @handler(get) |
||
68 | def get(self, event): |
||
69 | """Get a specified object""" |
||
70 | |||
71 | try: |
||
72 | data, schema, user, client = self._get_args(event) |
||
73 | except AttributeError: |
||
74 | return |
||
75 | |||
76 | object_filter = self._get_filter(event) |
||
77 | |||
78 | if "subscribe" in data: |
||
79 | do_subscribe = data["subscribe"] is True |
||
80 | else: |
||
81 | do_subscribe = False |
||
82 | |||
83 | try: |
||
84 | uuid = str(data["uuid"]) |
||
85 | except (KeyError, TypeError): |
||
86 | uuid = "" |
||
87 | |||
88 | opts = schemastore[schema].get("options", {}) |
||
89 | hidden = opts.get("hidden", []) |
||
90 | |||
91 | if object_filter == {}: |
||
92 | if uuid == "": |
||
93 | self.log( |
||
94 | "Object with no filter/uuid requested:", schema, data, lvl=warn |
||
95 | ) |
||
96 | return |
||
97 | object_filter = {"uuid": uuid} |
||
98 | |||
99 | storage_object = None |
||
100 | storage_object = objectmodels[schema].find_one(object_filter) |
||
101 | |||
102 | if not storage_object: |
||
103 | self._cancel_by_error( |
||
104 | event, |
||
105 | uuid + "(" + str(object_filter) + ") of " + schema + " unavailable", |
||
106 | ) |
||
107 | return |
||
108 | |||
109 | if storage_object: |
||
110 | self.log("Object found, checking permissions: ", data, lvl=verbose) |
||
111 | |||
112 | if not self._check_permissions(schema, user, "read", storage_object): |
||
113 | self._cancel_by_permission(schema, data, event) |
||
114 | return |
||
115 | |||
116 | for field in hidden: |
||
117 | storage_object._fields.pop(field, None) |
||
118 | |||
119 | if do_subscribe and uuid != "": |
||
120 | self._add_subscription(uuid, event) |
||
121 | |||
122 | result = { |
||
123 | "component": "isomer.events.objectmanager", |
||
124 | "action": "get", |
||
125 | "data": { |
||
126 | "schema": schema, |
||
127 | "uuid": uuid, |
||
128 | "object": storage_object.serializablefields(), |
||
129 | }, |
||
130 | } |
||
131 | self._respond(None, result, event) |
||
132 | |||
133 | @handler(search) |
||
134 | def search(self, event): |
||
135 | """Search for an object""" |
||
136 | |||
137 | try: |
||
138 | data, schema, user, client = self._get_args(event) |
||
139 | except AttributeError: |
||
140 | return |
||
141 | |||
142 | def get_filter(filter_request): |
||
143 | # result['$text'] = {'$search': str(data['search'])} |
||
144 | request_search = filter_request["search"] |
||
145 | |||
146 | if filter_request.get("fulltext", False) is True: |
||
147 | search_filter = { |
||
148 | "name": {"$regex": str(request_search), "$options": "$i"} |
||
149 | } |
||
150 | else: |
||
151 | search_filter = {} |
||
152 | |||
153 | if isinstance(request_search, dict): |
||
154 | search_filter = request_search |
||
155 | elif isinstance(request_search, str) and len(request_search) > 0: |
||
156 | if request_search != "*": |
||
157 | self.log(request_search, lvl=warn) |
||
158 | request_search = request_search.replace(r"\\\\", r"") |
||
159 | search_filter = literal_eval(request_search) |
||
160 | |||
161 | self.log("Final filter:", search_filter, lvl=debug) |
||
162 | |||
163 | return search_filter |
||
164 | |||
165 | object_filter = get_filter(data) |
||
166 | |||
167 | if "fields" in data: |
||
168 | fields = data["fields"] |
||
169 | else: |
||
170 | fields = [] |
||
171 | |||
172 | skip = data.get("skip", 0) |
||
173 | limit = data.get("limit", 0) |
||
174 | sort = data.get("sort", None) |
||
175 | # page = data.get('page', 0) |
||
176 | # count = data.get('count', 0) |
||
177 | # |
||
178 | # if page > 0 and count > 0: |
||
179 | # skip = page * count |
||
180 | # limit = count |
||
181 | |||
182 | if "subscribe" in data: |
||
183 | self.log("Subscription:", data["subscribe"], lvl=verbose) |
||
184 | do_subscribe = data["subscribe"] is True |
||
185 | else: |
||
186 | do_subscribe = False |
||
187 | |||
188 | object_list = [] |
||
189 | |||
190 | size = objectmodels[schema].count(object_filter) |
||
191 | |||
192 | if size > WARN_SIZE and (limit > 0 and limit > WARN_SIZE): |
||
193 | self.log( |
||
194 | "Getting a very long (", size, ") list of items for ", schema, lvl=warn |
||
195 | ) |
||
196 | |||
197 | opts = schemastore[schema].get("options", {}) |
||
198 | hidden = opts.get("hidden", []) |
||
199 | |||
200 | self.log( |
||
201 | "result: ", |
||
202 | object_filter, |
||
203 | " Schema: ", |
||
204 | schema, |
||
205 | "Fields: ", |
||
206 | fields, |
||
207 | lvl=verbose, |
||
208 | ) |
||
209 | |||
210 | def get_options(): |
||
211 | options = {} |
||
212 | |||
213 | if skip > 0: |
||
214 | options["skip"] = skip |
||
215 | if limit > 0: |
||
216 | options["limit"] = limit |
||
217 | if sort is not None: |
||
218 | options["sort"] = [] |
||
219 | for thing in sort: |
||
220 | key = thing[0] |
||
221 | direction = thing[1] |
||
222 | direction = ASCENDING if direction == "asc" else DESCENDING |
||
223 | options["sort"].append([key, direction]) |
||
224 | |||
225 | return options |
||
226 | |||
227 | cursor = objectmodels[schema].find(object_filter, **get_options()) |
||
228 | |||
229 | for item in cursor: |
||
230 | if not self._check_permissions(schema, user, "list", item): |
||
231 | continue |
||
232 | self.log("Search found item: ", item, lvl=verbose) |
||
233 | |||
234 | try: |
||
235 | list_item = {"uuid": item.uuid} |
||
236 | View Code Duplication | if fields in ("*", ["*"]): |
|
|
|||
237 | item_fields = item.serializablefields() |
||
238 | for field in hidden: |
||
239 | item_fields.pop(field, None) |
||
240 | object_list.append(item_fields) |
||
241 | else: |
||
242 | if "name" in item._fields: |
||
243 | list_item["name"] = item.name |
||
244 | |||
245 | for field in fields: |
||
246 | if field in item._fields and field not in hidden: |
||
247 | list_item[field] = item._fields[field] |
||
248 | else: |
||
249 | list_item[field] = None |
||
250 | |||
251 | object_list.append(list_item) |
||
252 | |||
253 | if do_subscribe: |
||
254 | self._add_subscription(item.uuid, event) |
||
255 | except Exception as e: |
||
256 | self.log( |
||
257 | "Faulty object or field: ", |
||
258 | e, |
||
259 | type(e), |
||
260 | item._fields, |
||
261 | fields, |
||
262 | lvl=error, |
||
263 | exc=True, |
||
264 | ) |
||
265 | # self.log("Generated object search list: ", object_list) |
||
266 | |||
267 | result = { |
||
268 | "component": "isomer.events.objectmanager", |
||
269 | "action": "search", |
||
270 | "data": {"schema": schema, "list": object_list, "size": size}, |
||
271 | } |
||
272 | |||
273 | self._respond(None, result, event) |
||
274 | |||
275 | @handler(getlist) |
||
276 | def objectlist(self, event): |
||
277 | """Get a list of objects""" |
||
278 | |||
279 | self.log("LEGACY LIST FUNCTION CALLED!", lvl=warn) |
||
280 | try: |
||
281 | data, schema, user, client = self._get_args(event) |
||
282 | except AttributeError: |
||
283 | return |
||
284 | |||
285 | object_filter = self._get_filter(event) |
||
286 | self.log( |
||
287 | "Object list for", schema, "requested from", user.account.name, lvl=debug |
||
288 | ) |
||
289 | |||
290 | if "fields" in data: |
||
291 | fields = data["fields"] |
||
292 | else: |
||
293 | fields = [] |
||
294 | |||
295 | object_list = [] |
||
296 | |||
297 | opts = schemastore[schema].get("options", {}) |
||
298 | hidden = opts.get("hidden", []) |
||
299 | |||
300 | if objectmodels[schema].count(object_filter) > WARN_SIZE: |
||
301 | self.log("Getting a very long list of items for ", schema, lvl=warn) |
||
302 | |||
303 | try: |
||
304 | for item in objectmodels[schema].find(object_filter): |
||
305 | try: |
||
306 | if not self._check_permissions(schema, user, "list", item): |
||
307 | continue |
||
308 | View Code Duplication | if fields in ("*", ["*"]): |
|
309 | item_fields = item.serializablefields() |
||
310 | for field in hidden: |
||
311 | item_fields.pop(field, None) |
||
312 | object_list.append(item_fields) |
||
313 | else: |
||
314 | list_item = {"uuid": item.uuid} |
||
315 | |||
316 | if "name" in item._fields: |
||
317 | list_item["name"] = item._fields["name"] |
||
318 | |||
319 | for field in fields: |
||
320 | if field in item._fields and field not in hidden: |
||
321 | list_item[field] = item._fields[field] |
||
322 | else: |
||
323 | list_item[field] = None |
||
324 | |||
325 | object_list.append(list_item) |
||
326 | except Exception as e: |
||
327 | self.log( |
||
328 | "Faulty object or field: ", |
||
329 | e, |
||
330 | type(e), |
||
331 | item._fields, |
||
332 | fields, |
||
333 | lvl=error, |
||
334 | exc=True, |
||
335 | ) |
||
336 | except ValidationError as e: |
||
337 | self.log("Invalid object in database encountered!", e, exc=True, lvl=warn) |
||
338 | # self.log("Generated object list: ", object_list) |
||
339 | |||
340 | result = { |
||
341 | "component": "isomer.events.objectmanager", |
||
342 | "action": "getlist", |
||
343 | "data": {"schema": schema, "list": object_list}, |
||
344 | } |
||
345 | |||
346 | self._respond(None, result, event) |
||
347 | |||
348 | @handler(change) |
||
349 | def change(self, event): |
||
350 | """Change an existing object""" |
||
351 | |||
352 | try: |
||
353 | data, schema, user, client = self._get_args(event) |
||
354 | except AttributeError: |
||
355 | return |
||
356 | |||
357 | try: |
||
358 | uuid = data["uuid"] |
||
359 | object_change = data["change"] |
||
360 | field = object_change["field"] |
||
361 | new_data = object_change["value"] |
||
362 | except KeyError as e: |
||
363 | self.log("Update request with missing arguments!", data, e, lvl=critical) |
||
364 | self._cancel_by_error(event, "missing_args") |
||
365 | return |
||
366 | |||
367 | storage_object = None |
||
368 | |||
369 | try: |
||
370 | storage_object = objectmodels[schema].find_one({"uuid": uuid}) |
||
371 | except Exception as e: |
||
372 | self.log("Change for unknown object requested:", e, schema, data, lvl=warn) |
||
373 | |||
374 | if storage_object is None: |
||
375 | self._cancel_by_error(event, "not_found") |
||
376 | return |
||
377 | |||
378 | if not self._check_permissions(schema, user, "write", storage_object): |
||
379 | self._cancel_by_permission(schema, data, event) |
||
380 | return |
||
381 | |||
382 | self.log("Changing object:", storage_object._fields, lvl=debug) |
||
383 | storage_object._fields[field] = new_data |
||
384 | |||
385 | self.log("Storing object:", storage_object._fields, lvl=debug) |
||
386 | try: |
||
387 | storage_object.validate() |
||
388 | except ValidationError: |
||
389 | self.log("Validation of changed object failed!", storage_object, lvl=warn) |
||
390 | self._cancel_by_error(event, "invalid_object") |
||
391 | return |
||
392 | |||
393 | storage_object.save() |
||
394 | |||
395 | self.log("Object stored.") |
||
396 | |||
397 | notification = objectchange(storage_object.uuid, schema, client) |
||
398 | |||
399 | self._update_subscribers(schema, storage_object) |
||
400 | |||
401 | result = { |
||
402 | "component": "isomer.events.objectmanager", |
||
403 | "action": "change", |
||
404 | "data": {"schema": schema, "uuid": uuid}, |
||
405 | } |
||
406 | |||
407 | self._respond(notification, result, event) |
||
408 | |||
409 | def _validate(self, schema_name, model, client_data): |
||
410 | """Validates and tries to fix up to 10 errors in client model data..""" |
||
411 | # TODO: This should probably move to Formal. |
||
412 | # Also i don't like artificially limiting this. |
||
413 | # Alas, never giving it up is even worse :) |
||
414 | |||
415 | give_up = 10 |
||
416 | validated = False |
||
417 | |||
418 | while give_up > 0 and validated is False: |
||
419 | try: |
||
420 | validated = model(client_data) |
||
421 | except ValidationError as e: |
||
422 | self.log("Validation Error:", e, e.__dict__, pretty=True) |
||
423 | give_up -= 1 |
||
424 | if e.validator == "type": |
||
425 | schema_data = schemastore[schema_name]["schema"] |
||
426 | if e.validator_value == "number": |
||
427 | definition = nested_map_find( |
||
428 | schema_data, list(e.schema_path)[:-1] |
||
429 | ) |
||
430 | |||
431 | if "default" in definition: |
||
432 | client_data = nested_map_update( |
||
433 | client_data, definition["default"], list(e.path) |
||
434 | ) |
||
435 | else: |
||
436 | client_data = nested_map_update( |
||
437 | client_data, None, list(e.path) |
||
438 | ) |
||
439 | if ( |
||
440 | e.validator == "pattern" |
||
441 | and "uuid" == e.path[0] |
||
442 | and client_data["uuid"] == "create" |
||
443 | ): |
||
444 | client_data["uuid"] = std_uuid() |
||
445 | |||
446 | if validated is False: |
||
447 | raise ValidationError("Could not validate object") |
||
448 | |||
449 | return client_data |
||
450 | |||
451 | @handler(put) |
||
452 | def put(self, event): |
||
453 | """Put an object""" |
||
454 | |||
455 | try: |
||
456 | data, schema, user, client = self._get_args(event) |
||
457 | except AttributeError: |
||
458 | return |
||
459 | |||
460 | try: |
||
461 | client_object = data["obj"] |
||
462 | uuid = client_object["uuid"] |
||
463 | except KeyError as e: |
||
464 | self.log("Put request with missing arguments!", e, data, lvl=critical) |
||
465 | return |
||
466 | |||
467 | try: |
||
468 | model = objectmodels[schema] |
||
469 | created = False |
||
470 | storage_object = None |
||
471 | |||
472 | try: |
||
473 | client_object = self._validate(schema, model, client_object) |
||
474 | except ValidationError: |
||
475 | self._cancel_by_error(event, "Invalid data") |
||
476 | return |
||
477 | |||
478 | if uuid != "create": |
||
479 | storage_object = model.find_one({"uuid": uuid}) |
||
480 | |||
481 | if uuid == "create" or model.count({"uuid": uuid}) == 0: |
||
482 | if uuid == "create": |
||
483 | uuid = str(uuid4()) |
||
484 | created = True |
||
485 | client_object["uuid"] = uuid |
||
486 | client_object["owner"] = user.uuid |
||
487 | storage_object = model(client_object) |
||
488 | |||
489 | if not self._check_create_permission(user, schema): |
||
490 | self._cancel_by_permission(schema, data, event) |
||
491 | return |
||
492 | |||
493 | if storage_object is not None: |
||
494 | if not self._check_permissions(schema, user, "write", storage_object): |
||
495 | self._cancel_by_permission(schema, data, event) |
||
496 | return |
||
497 | |||
498 | self.log("Updating object:", storage_object._fields, lvl=debug) |
||
499 | storage_object.update(client_object) |
||
500 | |||
501 | else: |
||
502 | storage_object = model(client_object) |
||
503 | if not self._check_permissions(schema, user, "write", storage_object): |
||
504 | self._cancel_by_permission(schema, data, event) |
||
505 | return |
||
506 | |||
507 | self.log("Storing object:", storage_object._fields, lvl=debug) |
||
508 | try: |
||
509 | storage_object.validate() |
||
510 | except ValidationError: |
||
511 | self.log( |
||
512 | "Validation of new object failed!", client_object, lvl=warn |
||
513 | ) |
||
514 | |||
515 | storage_object.save() |
||
516 | |||
517 | self.log("Object %s stored." % schema) |
||
518 | |||
519 | # Notify backend listeners |
||
520 | |||
521 | if created: |
||
522 | notification = objectcreation(storage_object.uuid, schema, client) |
||
523 | else: |
||
524 | notification = objectchange(storage_object.uuid, schema, client) |
||
525 | |||
526 | self._update_subscribers(schema, storage_object) |
||
527 | |||
528 | result = { |
||
529 | "component": "isomer.events.objectmanager", |
||
530 | "action": "put", |
||
531 | "data": { |
||
532 | "schema": schema, |
||
533 | "object": storage_object.serializablefields(), |
||
534 | "uuid": storage_object.uuid, |
||
535 | }, |
||
536 | } |
||
537 | |||
538 | self._respond(notification, result, event) |
||
539 | |||
540 | except Exception as e: |
||
541 | self.log( |
||
542 | "Error during object storage:", |
||
543 | e, |
||
544 | e.__dict__, |
||
545 | type(e), |
||
546 | data, |
||
547 | lvl=error, |
||
548 | exc=True, |
||
549 | pretty=True, |
||
550 | ) |
||
551 | |||
552 | @handler(delete) |
||
553 | def delete(self, event): |
||
554 | """Delete an existing object""" |
||
555 | |||
556 | try: |
||
557 | data, schema, user, client = self._get_args(event) |
||
558 | except AttributeError: |
||
559 | return |
||
560 | |||
561 | try: |
||
562 | uuids = data["uuid"] |
||
563 | |||
564 | if not isinstance(uuids, list): |
||
565 | uuids = [uuids] |
||
566 | |||
567 | if schema not in objectmodels.keys(): |
||
568 | self.log("Unknown schema encountered: ", schema, lvl=warn) |
||
569 | return |
||
570 | |||
571 | for uuid in uuids: |
||
572 | self.log("Looking for object to be deleted:", uuid, lvl=debug) |
||
573 | storage_object = objectmodels[schema].find_one({"uuid": uuid}) |
||
574 | |||
575 | if not storage_object: |
||
576 | self._cancel_by_error(event, "not found") |
||
577 | return |
||
578 | |||
579 | self.log("Found object.", lvl=debug) |
||
580 | |||
581 | if not self._check_permissions(schema, user, "write", storage_object): |
||
582 | self._cancel_by_permission(schema, data, event) |
||
583 | return |
||
584 | |||
585 | # self.log("Fields:", storage_object._fields, "\n\n\n", |
||
586 | # storage_object.__dict__) |
||
587 | |||
588 | storage_object.delete() |
||
589 | |||
590 | self.log("Deleted. Preparing notification.", lvl=debug) |
||
591 | notification = objectdeletion(uuid, schema, client) |
||
592 | |||
593 | if uuid in self.subscriptions: |
||
594 | deletion = { |
||
595 | "component": "isomer.events.objectmanager", |
||
596 | "action": "deletion", |
||
597 | "data": {"schema": schema, "uuid": uuid}, |
||
598 | } |
||
599 | for recipient in self.subscriptions[uuid]: |
||
600 | self.fireEvent(send(recipient, deletion)) |
||
601 | |||
602 | del self.subscriptions[uuid] |
||
603 | |||
604 | result = { |
||
605 | "component": "isomer.events.objectmanager", |
||
606 | "action": "delete", |
||
607 | "data": {"schema": schema, "uuid": storage_object.uuid}, |
||
608 | } |
||
609 | |||
610 | self._respond(notification, result, event) |
||
611 | |||
612 | except Exception as e: |
||
613 | self.log("Error during delete request: ", e, type(e), lvl=error) |
||
614 |