ui.objectmanager.crud.CrudOperations.objectlist()   F
last analyzed

Complexity

Conditions 14

Size

Total Lines 72
Code Lines 52

Duplication

Lines 18
Ratio 25 %

Importance

Changes 0
Metric Value
cc 14
eloc 52
nop 2
dl 18
loc 72
rs 3.6
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like ui.objectmanager.crud.CrudOperations.objectlist() 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 ("*", ["*"]):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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 ("*", ["*"]):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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