django-nativemojo 0.1.15__py3-none-any.whl → 0.1.16__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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
  2. django_nativemojo-0.1.16.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +281 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -1,132 +0,0 @@
1
- from mojo.helpers import logit
2
- import time
3
-
4
- logger = logit.get_logger("ti_example", "ti_example.log")
5
-
6
- def run_example_task(task):
7
- logger.info("Running example task with data", task)
8
- time.sleep(task.data.get("duration", 5))
9
-
10
-
11
- def run_error_task(task):
12
- logger.info("Running error task with data", task)
13
- time.sleep(2)
14
- raise Exception("Example error")
15
-
16
-
17
- def run_quick_task(task):
18
- """Quick task for testing - completes immediately"""
19
- logger.info("Running quick task with data", task)
20
- return {"status": "completed", "data": task.data}
21
-
22
-
23
- def run_slow_task(task):
24
- """Slow task for testing - takes 10 seconds"""
25
- logger.info("Running slow task with data", task)
26
- time.sleep(10)
27
- return {"status": "completed", "duration": 10}
28
-
29
-
30
- def run_args_kwargs_task(*args, **kwargs):
31
- """Task that receives args and kwargs directly"""
32
- logger.info(f"Running args/kwargs task with args: {args}, kwargs: {kwargs}")
33
- return {"args": args, "kwargs": kwargs}
34
-
35
-
36
- def run_data_processing_task(task):
37
- """Task that processes data and returns results"""
38
- logger.info("Running data processing task")
39
- data = task.data
40
- if not isinstance(data, dict):
41
- raise ValueError("Data must be a dictionary")
42
-
43
- result = {
44
- "processed": True,
45
- "input_keys": list(data.keys()),
46
- "total_items": len(data)
47
- }
48
- return result
49
-
50
-
51
- def run_counter_task(task):
52
- """Task that increments a counter - for testing state changes"""
53
- logger.info("Running counter task")
54
- count = task.data.get("count", 0)
55
- new_count = count + 1
56
- logger.info(f"Counter incremented from {count} to {new_count}")
57
- return {"count": new_count}
58
-
59
-
60
- def run_timeout_task(task):
61
- """Task that times out - for testing timeout scenarios"""
62
- duration = task.data.get("duration", 60)
63
- logger.info(f"Running timeout task for {duration} seconds")
64
- time.sleep(duration)
65
- return {"completed": True}
66
-
67
-
68
- def run_memory_task(task):
69
- """Task that uses memory - for testing resource usage"""
70
- logger.info("Running memory task")
71
- size = task.data.get("size", 1000000) # 1MB default
72
- data = bytearray(size)
73
- logger.info(f"Allocated {size} bytes")
74
- return {"allocated_bytes": size}
75
-
76
-
77
- def run_conditional_error_task(task):
78
- """Task that conditionally raises an error based on input"""
79
- logger.info("Running conditional error task")
80
- should_error = task.data.get("should_error", False)
81
- error_message = task.data.get("error_message", "Conditional error occurred")
82
-
83
- if should_error:
84
- raise Exception(error_message)
85
-
86
- return {"status": "success", "should_error": should_error}
87
-
88
-
89
- def run_nested_data_task(task):
90
- """Task that works with nested data structures"""
91
- logger.info("Running nested data task")
92
- data = task.data
93
-
94
- if "nested" not in data:
95
- raise ValueError("Missing 'nested' key in data")
96
-
97
- nested = data["nested"]
98
- result = {
99
- "original": nested,
100
- "keys": list(nested.keys()) if isinstance(nested, dict) else None,
101
- "length": len(nested) if hasattr(nested, '__len__') else None
102
- }
103
-
104
- return result
105
-
106
-
107
- # Test async task handlers
108
- def async_quick_task(message="Hello"):
109
- """Async task handler for testing decorator"""
110
- logger.info(f"Async quick task: {message}")
111
- return f"Processed: {message}"
112
-
113
-
114
- def async_slow_task(duration=5):
115
- """Async slow task handler for testing decorator"""
116
- logger.info(f"Async slow task sleeping for {duration} seconds")
117
- time.sleep(duration)
118
- return f"Completed after {duration} seconds"
119
-
120
-
121
- def async_error_task(should_error=True, message="Async error"):
122
- """Async error task handler for testing decorator"""
123
- logger.info(f"Async error task - should_error: {should_error}")
124
- if should_error:
125
- raise Exception(message)
126
- return "No error raised"
127
-
128
-
129
- def async_args_task(*args, **kwargs):
130
- """Async task that tests args and kwargs handling"""
131
- logger.info(f"Async args task - args: {args}, kwargs: {kwargs}")
132
- return {"received_args": args, "received_kwargs": kwargs}
mojo/helpers/redis.py DELETED
@@ -1,10 +0,0 @@
1
- from redis import ConnectionPool, StrictRedis
2
- from mojo.helpers.settings import settings
3
- REDIS_POOL = None
4
-
5
-
6
- def get_connection():
7
- global REDIS_POOL
8
- if REDIS_POOL is None:
9
- REDIS_POOL = ConnectionPool(**settings.REDIS_DB)
10
- return StrictRedis(connection_pool=REDIS_POOL)
mojo/models/meta.py DELETED
@@ -1,262 +0,0 @@
1
- from objict import objict
2
- from django.db import models as dm
3
- import string
4
- from rest.encryption import ENCRYPTER, DECRYPTER
5
- from datetime import datetime, date
6
-
7
- class MetaDataBase(dm.Model):
8
- class Meta:
9
- abstract = True
10
-
11
- category = dm.CharField(db_index=True, max_length=32, default=None, null=True, blank=True)
12
- key = dm.CharField(db_index=True, max_length=80)
13
- value_format = dm.CharField(max_length=16)
14
- value = dm.TextField()
15
- int_value = dm.IntegerField(default=None, null=True, blank=True)
16
- float_value = dm.FloatField(default=None, null=True, blank=True)
17
-
18
- def set_value(self, value):
19
- self.value = str(value)
20
- value_type = type(value)
21
-
22
- if value_type is int or self.value in ["0", "1"]:
23
- if value_type is int and value > 2147483647:
24
- self.value_format = "S"
25
- return
26
- self.value_format = "I"
27
- self.int_value = value
28
- elif value_type is float:
29
- self.value_format = "F"
30
- self.float_value = value
31
- elif isinstance(value, list):
32
- self.value_format = "L"
33
- elif isinstance(value, dict):
34
- self.value_format = "O"
35
- elif isinstance(value, str) and len(value) < 9 and value.isdigit():
36
- self.value_format = "I"
37
- self.int_value = int(value)
38
- elif value in ["True", "true", "False", "false"]:
39
- self.value_format = "I"
40
- self.int_value = 1 if value.lower() == "true" else 0
41
- elif isinstance(value, bool):
42
- self.value_format = "I"
43
- self.int_value = 1 if value else 0
44
- else:
45
- self.value_format = "S"
46
-
47
- def get_strict_type(self, field_type):
48
- try:
49
- return field_type(self.value)
50
- except (ValueError, TypeError):
51
- if field_type is bool:
52
- return self.int_value != 0 if self.value_format == 'I' else self.value.lower() in ['true', '1', 'y', 'yes']
53
- elif field_type in [date, datetime]:
54
- return rh.parseDate(self.value)
55
- return self.value
56
-
57
- def get_value(self, field_type=None):
58
- if field_type:
59
- return self.get_strict_type(field_type)
60
- if self.value_format == 'I':
61
- return self.int_value
62
- elif self.value_format == 'F':
63
- return self.float_value
64
- elif self.value_format in ["L", "O"] and self.value:
65
- try:
66
- return eval(self.value)
67
- except Exception:
68
- pass
69
- return self.value
70
-
71
- def __str__(self):
72
- return f"{self.category}.{self.key}={self.value}" if self.category else f"{self.key}={self.value}"
73
-
74
- class MetaDataModel:
75
- def set_metadata(self, request, values=None):
76
- if not self.id:
77
- self.save()
78
-
79
- values = values or request
80
- if isinstance(values, list):
81
- values = objict({k: v for item in values if isinstance(item, dict) for k, v in item.items()})
82
-
83
- if not isinstance(values, dict):
84
- raise ValueError(f"invalid metadata: {values}")
85
-
86
- for key, value in values.items():
87
- cat, key = key.split('.', 1) if '.' in key else (None, key)
88
- self.set_property(key, value, cat, request=request)
89
-
90
- def metadata(self):
91
- return self.get_properties()
92
-
93
- def remove_properties(self, category=None):
94
- self.properties.filter(category=category).delete()
95
-
96
- def get_properties(self, category=None):
97
- result = {}
98
- for prop in self.properties.all():
99
- category_ = prop.category
100
- key = prop.key
101
-
102
- if not category_:
103
- self._add_property_to_result(result, prop)
104
- continue
105
-
106
- props = self.get_field_props(category_)
107
- if props.hidden:
108
- continue
109
-
110
- if category_ not in result:
111
- result[category_] = {}
112
-
113
- if category_ == "secrets":
114
- masked_value = "*" * prop.int_value if prop.int_value else "******"
115
- result[category_][key] = masked_value
116
- else:
117
- self._add_property_to_result(result[category_], prop)
118
-
119
- return result.get(category, {}) if category else result
120
-
121
- def _add_property_to_result(self, result_dict, prop):
122
- props = self.get_field_props(prop.key)
123
- if not props.hidden:
124
- result_dict[prop.key] = prop.get_value()
125
-
126
- def get_field_props(self, key):
127
- self._init_field_props()
128
- category, key = key.split('.', 1) if '.' in key else (None, key)
129
- props = objict()
130
-
131
- if self.__field_props:
132
- cat_props = self.__field_props.get(category, {})
133
- self._update_props_with_category(props, cat_props)
134
-
135
- field_props = self.__field_props.get(key, {})
136
- self._update_props_with_field(props, field_props)
137
-
138
- return props
139
-
140
- def _update_props_with_category(self, props, cat_props):
141
- if cat_props:
142
- props.notify = cat_props.get("notify")
143
- props.requires = cat_props.get("requires")
144
- props.hidden = cat_props.get("hidden", False)
145
- on_change_name = cat_props.get("on_change")
146
- if on_change_name:
147
- props.on_change = getattr(self, on_change_name, None)
148
-
149
- def _update_props_with_field(self, props, field_props):
150
- props.notify = field_props.get("notify", props.notify)
151
- props.requires = field_props.get("requires", props.requires)
152
- props.hidden = field_props.get("hidden", props.hidden)
153
- on_change_name = field_props.get("on_change")
154
- if on_change_name:
155
- props.on_change = getattr(self, on_change_name, None)
156
-
157
- def check_field_perms(self, full_key, props, request=None):
158
- if not props.requires:
159
- return True
160
- if not request or not request.member:
161
- return False
162
- if request.member.hasPermission(props.requires) or request.user.is_superuser:
163
- return True
164
-
165
- if props.notify and request.member:
166
- subject = f"permission denied changing protected '{full_key}' field"
167
- msg = f"permission denied changing protected field '{full_key}'\nby user: {request.user.username}\nfor: {self}"
168
- request.member.notifyWithPermission(props.notify, subject, msg, email_only=True)
169
- raise re.PermissionDeniedException(subject, 481)
170
-
171
- def set_properties(self, data, category=None, request=None, using=None):
172
- for k, v in data.items():
173
- self.set_property(k, v, category, request=request, using=using)
174
-
175
- def set_property(self, key, value, category=None, request=None, using=None, ascii_only=False, encrypted=False):
176
- if ascii_only and isinstance(value, str):
177
- value = ''.join(filter(lambda x: x in string.printable, value))
178
-
179
- if using is None:
180
- using = getattr(self.RestMeta, "DATABASE", None)
181
-
182
- if request is None:
183
- request = rh.getActiveRequest()
184
-
185
- self._init_field_props()
186
-
187
- if '.' in key:
188
- category, key = key.split('.', 1)
189
-
190
- full_key = f"{category}.{key}" if category else key
191
- field_props = self.get_field_props(full_key)
192
-
193
- if not self.check_field_perms(full_key, field_props, request):
194
- return False
195
-
196
- prop = self.properties.filter(category=category, key=key).last()
197
- if not prop and (value is None or value == ""):
198
- return False
199
-
200
- has_changed, old_value = self._update_or_create_property(prop, category, key, value, encrypted, using)
201
-
202
- if has_changed and field_props.on_change:
203
- field_props.on_change(key, value, old_value, category)
204
-
205
- self._notify_change_if_required(field_props, full_key, value, request)
206
-
207
- if hasattr(self, "_recordRestChange"):
208
- self._recordRestChange(f"metadata.{full_key}", old_value)
209
-
210
- return has_changed
211
-
212
- def _update_or_create_property(self, prop, category, key, value, encrypted, using):
213
- has_changed = False
214
- old_value = None
215
-
216
- value_len = len(value) if encrypted else 0
217
- if encrypted:
218
- value = ENCRYPTER.encrypt(value)
219
-
220
- if prop:
221
- old_value = prop.get_value()
222
- if value is None or value == "":
223
- self.properties.filter(category=category, key=key).delete()
224
- has_changed = True
225
- else:
226
- has_changed = str(value) != prop.value
227
- if has_changed:
228
- prop.set_value(value)
229
- if encrypted:
230
- prop.int_value = value_len
231
- prop.save(using=using)
232
- else:
233
- has_changed = True
234
- PropClass = self.get_fk_model("properties")
235
- prop = PropClass(parent=self, key=key, category=category)
236
- prop.set_value(value)
237
- prop.save(using=using)
238
-
239
- return has_changed, old_value
240
-
241
- def _notify_change_if_required(self, field_props, full_key, value, request):
242
- if field_props.notify and request and request.member:
243
- username = request.member.username if request and request.member else "root"
244
- truncated_value = "***" if value and len(str(value)) > 5 else value
245
- msg = (f"protected field '{full_key}' changed to '{truncated_value}'\n"
246
- f"by user: {username}\nfor: {self}")
247
- request.member.notifyWithPermission(field_props.notify, f"protected '{full_key}' field changed", msg, email_only=True)
248
-
249
- def get_property(self, key, default=None, category=None, field_type=None, decrypted=False):
250
- category, key = key.split('.', 1) if '.' in key else (category, key)
251
-
252
- try:
253
- prop_value = self.properties.get(category=category, key=key).get_value(field_type)
254
- return DECRYPTER.decrypt(prop_value) if decrypted and prop_value else prop_value
255
- except Exception:
256
- return default
257
-
258
- def set_secret_property(self, key, value):
259
- return self.set_property(key, value, category="secrets", encrypted=True)
260
-
261
- def get_secret_property(self, key, default=None):
262
- return self.get_property(key, default, "secrets", decrypted=True)