core.menu.MenuItem.on_get()   F
last analyzed

Complexity

Conditions 14

Size

Total Lines 87
Code Lines 51

Duplication

Lines 87
Ratio 100 %

Importance

Changes 0
Metric Value
eloc 51
dl 87
loc 87
rs 3.6
c 0
b 0
f 0
cc 14
nop 3

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 core.menu.MenuItem.on_get() 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
import falcon
2
import mysql.connector
3
import simplejson as json
4
import redis
5
from core.useractivity import user_logger, admin_control, access_control, api_key_control
6
import config
7
8
9
def clear_menu_cache(menu_id=None):
10
    """
11
    Clear menu-related cache after data modification
12
13
    Args:
14
        menu_id: Menu ID (optional, for specific item cache)
15
    """
16
    # Check if Redis is enabled
17
    if not config.redis.get('is_enabled', False):
18
        return
19
20
    redis_client = None
21
    try:
22
        redis_client = redis.Redis(
23
            host=config.redis['host'],
24
            port=config.redis['port'],
25
            password=config.redis['password'] if config.redis['password'] else None,
26
            db=config.redis['db'],
27
            decode_responses=True,
28
            socket_connect_timeout=2,
29
            socket_timeout=2
30
        )
31
        redis_client.ping()
32
33
        # Clear menu list cache
34
        list_cache_key_pattern = 'menu:list*'
35
        matching_keys = redis_client.keys(list_cache_key_pattern)
36
        if matching_keys:
37
            redis_client.delete(*matching_keys)
38
39
        # Clear menu web cache
40
        web_cache_key_pattern = 'menu:web*'
41
        matching_keys = redis_client.keys(web_cache_key_pattern)
42
        if matching_keys:
43
            redis_client.delete(*matching_keys)
44
45
        # Clear specific menu item cache if menu_id is provided
46
        if menu_id:
47
            item_cache_key = f'menu:item:{menu_id}'
48
            redis_client.delete(item_cache_key)
49
50
        # Clear menu children cache if menu_id is provided
51
        if menu_id:
52
            children_cache_key = f'menu:children:{menu_id}'
53
            redis_client.delete(children_cache_key)
54
55
    except Exception:
56
        # If cache clear fails, ignore and continue
57
        pass
58
59
60 View Code Duplication
class MenuCollection:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
61
    """
62
    Menu Collection Resource
63
64
    This class handles menu operations for the MyEMS system.
65
    It provides functionality to retrieve all menus and their hierarchical structure
66
    used for navigation in the MyEMS web interface.
67
    """
68
69
    def __init__(self):
70
        pass
71
72
    @staticmethod
73
    def on_options(req, resp):
74
        """
75
        Handle OPTIONS request for CORS preflight
76
77
        Args:
78
            req: Falcon request object
79
            resp: Falcon response object
80
        """
81
        _ = req
82
        resp.status = falcon.HTTP_200
83
84
    @staticmethod
85
    def on_get(req, resp):
86
        """
87
        Handle GET requests to retrieve all menus
88
89
        Returns a list of all menus with their metadata including:
90
        - Menu ID and name
91
        - Route path
92
        - Parent menu ID (for hierarchical structure)
93
        - Hidden status
94
95
        Args:
96
            req: Falcon request object
97
            resp: Falcon response object
98
        """
99
        # Check authentication method (API key or session)
100
        if 'API-KEY' not in req.headers or \
101
                not isinstance(req.headers['API-KEY'], str) or \
102
                len(str.strip(req.headers['API-KEY'])) == 0:
103
            access_control(req)
104
        else:
105
            api_key_control(req)
106
107
        # Redis cache key
108
        cache_key = 'menu:list'
109
        cache_expire = 28800  # 8 hours in seconds (long-term cache)
110
111
        # Try to get from Redis cache (only if Redis is enabled)
112
        redis_client = None
113
        if config.redis.get('is_enabled', False):
114
            try:
115
                redis_client = redis.Redis(
116
                    host=config.redis['host'],
117
                    port=config.redis['port'],
118
                    password=config.redis['password'] if config.redis['password'] else None,
119
                    db=config.redis['db'],
120
                    decode_responses=True,
121
                    socket_connect_timeout=2,
122
                    socket_timeout=2
123
                )
124
                redis_client.ping()
125
                cached_result = redis_client.get(cache_key)
126
                if cached_result:
127
                    resp.text = cached_result
128
                    return
129
            except Exception:
130
                # If Redis connection fails, continue to database query
131
                pass
132
133
        # Cache miss or Redis error - query database
134
        cnx = mysql.connector.connect(**config.myems_system_db)
135
        cursor = cnx.cursor()
136
137
        # Query to retrieve all menus ordered by ID
138
        query = (" SELECT id, name, route, parent_menu_id, is_hidden "
139
                 " FROM tbl_menus "
140
                 " ORDER BY id ")
141
        cursor.execute(query)
142
        rows_menus = cursor.fetchall()
143
144
        # Build result list
145
        result = list()
146
        if rows_menus is not None and len(rows_menus) > 0:
147
            for row in rows_menus:
148
                temp = {"id": row[0],
149
                        "name": row[1],
150
                        "route": row[2],
151
                        "parent_menu_id": row[3],
152
                        "is_hidden": bool(row[4])}
153
154
                result.append(temp)
155
156
        cursor.close()
157
        cnx.close()
158
159
        # Store result in Redis cache
160
        result_json = json.dumps(result)
161
        if redis_client:
162
            try:
163
                redis_client.setex(cache_key, cache_expire, result_json)
164
            except Exception:
165
                # If cache set fails, ignore and continue
166
                pass
167
168
        resp.text = result_json
169
170
171
class MenuItem:
172
    """
173
    Menu Item Resource
174
175
    This class handles individual menu operations including:
176
    - Retrieving a specific menu by ID
177
    - Updating menu visibility status
178
    """
179
180
    def __init__(self):
181
        pass
182
183
    @staticmethod
184
    def on_options(req, resp, id_):
185
        """
186
        Handle OPTIONS request for CORS preflight
187
188
        Args:
189
            req: Falcon request object
190
            resp: Falcon response object
191
            id_: Menu ID parameter
192
        """
193
        _ = req
194
        resp.status = falcon.HTTP_200
195
        _ = id_
196
197 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
198
    def on_get(req, resp, id_):
199
        """
200
        Handle GET requests to retrieve a specific menu by ID
201
202
        Retrieves a single menu with its metadata including:
203
        - Menu ID and name
204
        - Route path
205
        - Parent menu ID
206
        - Hidden status
207
208
        Args:
209
            req: Falcon request object
210
            resp: Falcon response object
211
            id_: Menu ID to retrieve
212
        """
213
        # Check authentication method (API key or session)
214
        if 'API-KEY' not in req.headers or \
215
                not isinstance(req.headers['API-KEY'], str) or \
216
                len(str.strip(req.headers['API-KEY'])) == 0:
217
            access_control(req)
218
        else:
219
            api_key_control(req)
220
221
        if not id_.isdigit() or int(id_) <= 0:
222
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
223
                                   description='API.INVALID_MENU_ID')
224
225
        # Redis cache key
226
        cache_key = f'menu:item:{id_}'
227
        cache_expire = 28800  # 8 hours in seconds (long-term cache)
228
229
        # Try to get from Redis cache (only if Redis is enabled)
230
        redis_client = None
231
        if config.redis.get('is_enabled', False):
232
            try:
233
                redis_client = redis.Redis(
234
                    host=config.redis['host'],
235
                    port=config.redis['port'],
236
                    password=config.redis['password'] if config.redis['password'] else None,
237
                    db=config.redis['db'],
238
                    decode_responses=True,
239
                    socket_connect_timeout=2,
240
                    socket_timeout=2
241
                )
242
                redis_client.ping()
243
                cached_result = redis_client.get(cache_key)
244
                if cached_result:
245
                    resp.text = cached_result
246
                    return
247
            except Exception:
248
                # If Redis connection fails, continue to database query
249
                pass
250
251
        # Cache miss or Redis error - query database
252
        cnx = mysql.connector.connect(**config.myems_system_db)
253
        cursor = cnx.cursor()
254
255
        # Query to retrieve specific menu by ID
256
        query = (" SELECT id, name, route, parent_menu_id, is_hidden "
257
                 " FROM tbl_menus "
258
                 " WHERE id= %s ")
259
        cursor.execute(query, (id_,))
260
        rows_menu = cursor.fetchone()
261
262
        # Build result object
263
        result = None
264
        if rows_menu is not None and len(rows_menu) > 0:
265
            result = {"id": rows_menu[0],
266
                      "name": rows_menu[1],
267
                      "route": rows_menu[2],
268
                      "parent_menu_id": rows_menu[3],
269
                      "is_hidden": bool(rows_menu[4])}
270
271
        cursor.close()
272
        cnx.close()
273
274
        # Store result in Redis cache
275
        result_json = json.dumps(result)
276
        if redis_client:
277
            try:
278
                redis_client.setex(cache_key, cache_expire, result_json)
279
            except Exception:
280
                # If cache set fails, ignore and continue
281
                pass
282
283
        resp.text = result_json
284
285 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
286
    @user_logger
287
    def on_put(req, resp, id_):
288
        """
289
        Handle PUT requests to update menu visibility
290
291
        Updates the hidden status of a specific menu.
292
        Requires admin privileges.
293
294
        Args:
295
            req: Falcon request object containing update data:
296
                - is_hidden: Boolean value for menu visibility
297
            resp: Falcon response object
298
            id_: Menu ID to update
299
        """
300
        admin_control(req)
301
        try:
302
            raw_json = req.stream.read().decode('utf-8')
303
        except UnicodeDecodeError as ex:
304
            print("Failed to decode request")
305
            raise falcon.HTTPError(status=falcon.HTTP_400,
306
                                   title='API.BAD_REQUEST',
307
                                   description='API.INVALID_ENCODING')
308
        except Exception as ex:
309
            print("Unexpected error reading request stream")
310
            raise falcon.HTTPError(status=falcon.HTTP_400,
311
                                   title='API.BAD_REQUEST',
312
                                   description='API.FAILED_TO_READ_REQUEST_STREAM')
313
314
        if not id_.isdigit() or int(id_) <= 0:
315
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
316
                                   description='API.INVALID_MENU_ID')
317
318
        new_values = json.loads(raw_json)
319
320
        # Validate hidden status
321
        if 'is_hidden' not in new_values['data'].keys() or \
322
                not isinstance(new_values['data']['is_hidden'], bool):
323
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
324
                                   description='API.INVALID_IS_HIDDEN')
325
        is_hidden = new_values['data']['is_hidden']
326
327
        cnx = mysql.connector.connect(**config.myems_system_db)
328
        cursor = cnx.cursor()
329
330
        # Update menu visibility status
331
        update_row = (" UPDATE tbl_menus "
332
                      " SET is_hidden = %s "
333
                      " WHERE id = %s ")
334
        cursor.execute(update_row, (is_hidden,
335
                                    id_))
336
        cnx.commit()
337
338
        cursor.close()
339
        cnx.close()
340
341
        # Clear cache after updating menu
342
        clear_menu_cache(menu_id=id_)
343
344
        resp.status = falcon.HTTP_200
345
346
347
class MenuChildrenCollection:
348
    """
349
    Menu Children Collection Resource
350
351
    This class handles retrieval of menu hierarchy including:
352
    - Current menu information
353
    - Parent menu information
354
    - Child menus
355
    """
356
357
    def __init__(self):
358
        pass
359
360
    @staticmethod
361
    def on_options(req, resp, id_):
362
        """
363
        Handle OPTIONS request for CORS preflight
364
365
        Args:
366
            req: Falcon request object
367
            resp: Falcon response object
368
            id_: Menu ID parameter
369
        """
370
        _ = req
371
        resp.status = falcon.HTTP_200
372
        _ = id_
373
374
    @staticmethod
375
    def on_get(req, resp, id_):
376
        """
377
        Handle GET requests to retrieve menu hierarchy
378
379
        Returns detailed menu information including:
380
        - Current menu details
381
        - Parent menu information
382
        - List of child menus
383
384
        Args:
385
            req: Falcon request object
386
            resp: Falcon response object
387
            id_: Menu ID to retrieve hierarchy for
388
        """
389
        # Check authentication method (API key or session)
390
        if 'API-KEY' not in req.headers or \
391
                not isinstance(req.headers['API-KEY'], str) or \
392
                len(str.strip(req.headers['API-KEY'])) == 0:
393
            access_control(req)
394
        else:
395
            api_key_control(req)
396
397
        if not id_.isdigit() or int(id_) <= 0:
398
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
399
                                   description='API.INVALID_MENU_ID')
400
401
        # Redis cache key
402
        cache_key = f'menu:children:{id_}'
403
        cache_expire = 28800  # 8 hours in seconds (long-term cache)
404
405
        # Try to get from Redis cache (only if Redis is enabled)
406
        redis_client = None
407
        if config.redis.get('is_enabled', False):
408
            try:
409
                redis_client = redis.Redis(
410
                    host=config.redis['host'],
411
                    port=config.redis['port'],
412
                    password=config.redis['password'] if config.redis['password'] else None,
413
                    db=config.redis['db'],
414
                    decode_responses=True,
415
                    socket_connect_timeout=2,
416
                    socket_timeout=2
417
                )
418
                redis_client.ping()
419
                cached_result = redis_client.get(cache_key)
420
                if cached_result:
421
                    resp.text = cached_result
422
                    return
423
            except Exception:
424
                # If Redis connection fails, continue to database query
425
                pass
426
427
        # Cache miss or Redis error - query database
428
        cnx = mysql.connector.connect(**config.myems_system_db)
429
        cursor = cnx.cursor()
430
431
        # Query to retrieve current menu
432
        query = (" SELECT id, name, route, parent_menu_id, is_hidden "
433
                 " FROM tbl_menus "
434
                 " WHERE id = %s ")
435
        cursor.execute(query, (id_,))
436
        row_current_menu = cursor.fetchone()
437
        if row_current_menu is None:
438
            cursor.close()
439
            cnx.close()
440
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND',
441
                                   description='API.MENU_NOT_FOUND')
442
443
        # Query to retrieve all menus for parent lookup
444
        query = (" SELECT id, name "
445
                 " FROM tbl_menus "
446
                 " ORDER BY id ")
447
        cursor.execute(query)
448
        rows_menus = cursor.fetchall()
449
450
        # Build menu dictionary for parent lookup
451
        menu_dict = dict()
452
        if rows_menus is not None and len(rows_menus) > 0:
453
            for row in rows_menus:
454
                menu_dict[row[0]] = {"id": row[0],
455
                                     "name": row[1]}
456
457
        # Build result structure
458
        result = dict()
459
        result['current'] = dict()
460
        result['current']['id'] = row_current_menu[0]
461
        result['current']['name'] = row_current_menu[1]
462
        result['current']['parent_menu'] = menu_dict.get(row_current_menu[3], None)
463
        result['current']['is_hidden'] = bool(row_current_menu[4])
464
465
        result['children'] = list()
466
467
        # Query to retrieve child menus
468
        query = (" SELECT id, name, route, parent_menu_id, is_hidden "
469
                 " FROM tbl_menus "
470
                 " WHERE parent_menu_id = %s "
471
                 " ORDER BY id ")
472
        cursor.execute(query, (id_, ))
473
        rows_menus = cursor.fetchall()
474
475
        # Build children list
476
        if rows_menus is not None and len(rows_menus) > 0:
477
            for row in rows_menus:
478
                meta_result = {"id": row[0],
479
                               "name": row[1],
480
                               "parent_menu": menu_dict.get(row[3], None),
481
                               "is_hidden": bool(row[4])}
482
                result['children'].append(meta_result)
483
484
        cursor.close()
485
        cnx.close()
486
487
        # Store result in Redis cache
488
        result_json = json.dumps(result)
489
        if redis_client:
490
            try:
491
                redis_client.setex(cache_key, cache_expire, result_json)
492
            except Exception:
493
                # If cache set fails, ignore and continue
494
                pass
495
496
        resp.text = result_json
497
498
499
class MenuWebCollection:
500
    """
501
    Menu Web Collection Resource
502
503
    This class provides menu data specifically formatted for web interface.
504
    It returns a hierarchical structure of routes organized by parent-child relationships.
505
    """
506
507
    def __init__(self):
508
        pass
509
510
    @staticmethod
511
    def on_options(req, resp):
512
        """
513
        Handle OPTIONS request for CORS preflight
514
515
        Args:
516
            req: Falcon request object
517
            resp: Falcon response object
518
        """
519
        _ = req
520
        resp.status = falcon.HTTP_200
521
522
    @staticmethod
523
    def on_get(req, resp):
524
        """
525
        Handle GET requests to retrieve web menu structure
526
527
        Returns a hierarchical menu structure formatted for web interface:
528
        - First level routes (parent menus)
529
        - Child routes organized under their parents
530
        - Only non-hidden menus are included
531
532
        Args:
533
            req: Falcon request object
534
            resp: Falcon response object
535
        """
536
        # Check authentication method (API key or session)
537
        if 'API-KEY' not in req.headers or \
538
                not isinstance(req.headers['API-KEY'], str) or \
539
                len(str.strip(req.headers['API-KEY'])) == 0:
540
            access_control(req)
541
        else:
542
            api_key_control(req)
543
544
        # Redis cache key
545
        cache_key = 'menu:web'
546
        cache_expire = 28800  # 8 hours in seconds (long-term cache)
547
548
        # Try to get from Redis cache (only if Redis is enabled)
549
        redis_client = None
550
        if config.redis.get('is_enabled', False):
551
            try:
552
                redis_client = redis.Redis(
553
                    host=config.redis['host'],
554
                    port=config.redis['port'],
555
                    password=config.redis['password'] if config.redis['password'] else None,
556
                    db=config.redis['db'],
557
                    decode_responses=True,
558
                    socket_connect_timeout=2,
559
                    socket_timeout=2
560
                )
561
                redis_client.ping()
562
                cached_result = redis_client.get(cache_key)
563
                if cached_result:
564
                    resp.text = cached_result
565
                    return
566
            except Exception:
567
                # If Redis connection fails, continue to database query
568
                pass
569
570
        # Cache miss or Redis error - query database
571
        cnx = mysql.connector.connect(**config.myems_system_db)
572
        cursor = cnx.cursor()
573
574
        # Optimized: Single query to retrieve all non-hidden menus
575
        # This reduces database round trips from 2 to 1
576
        # MySQL compatible: parent_menu_id IS NULL comes first in ORDER BY
577
        query = (" SELECT id, route, parent_menu_id "
578
                 " FROM tbl_menus "
579
                 " WHERE is_hidden = 0 "
580
                 " ORDER BY (parent_menu_id IS NULL) DESC, id ")
581
        cursor.execute(query)
582
        rows_menus = cursor.fetchall()
583
584
        # Build first level routes dictionary and result structure in one pass
585
        first_level_routes = {}
586
        result = {}
587
588
        if rows_menus:
589
            for row in rows_menus:
590
                menu_id, route, parent_menu_id = row
591
592
                if parent_menu_id is None:
593
                    # First level menu (parent)
594
                    first_level_routes[menu_id] = {
595
                        'route': route,
596
                        'children': []
597
                    }
598
                    result[route] = []
599
                else:
600
                    # Child menu - optimized: direct dictionary lookup instead of .keys()
601
                    if parent_menu_id in first_level_routes:
602
                        first_level_routes[parent_menu_id]['children'].append(route)
603
                        # Update result directly
604
                        parent_route = first_level_routes[parent_menu_id]['route']
605
                        result[parent_route].append(route)
606
607
        cursor.close()
608
        cnx.close()
609
610
        # Store result in Redis cache
611
        result_json = json.dumps(result)
612
        if redis_client:
613
            try:
614
                redis_client.setex(cache_key, cache_expire, result_json)
615
            except Exception:
616
                # If cache set fails, ignore and continue
617
                pass
618
619
        resp.text = result_json
620