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.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/METADATA +3 -1
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -0,0 +1,99 @@
|
|
1
|
+
from django.db import models
|
2
|
+
from mojo.models import MojoModel
|
3
|
+
|
4
|
+
|
5
|
+
class NotificationTemplate(models.Model, MojoModel):
|
6
|
+
"""
|
7
|
+
Reusable notification templates with variable substitution support.
|
8
|
+
"""
|
9
|
+
created = models.DateTimeField(auto_now_add=True, editable=False, db_index=True)
|
10
|
+
modified = models.DateTimeField(auto_now=True, db_index=True)
|
11
|
+
|
12
|
+
group = models.ForeignKey("account.Group", on_delete=models.CASCADE,
|
13
|
+
related_name="notification_templates", null=True, blank=True,
|
14
|
+
help_text="Organization for this template. Null = system template")
|
15
|
+
|
16
|
+
name = models.CharField(max_length=100, db_index=True)
|
17
|
+
title_template = models.CharField(max_length=200, blank=True, null=True)
|
18
|
+
body_template = models.TextField(blank=True, null=True)
|
19
|
+
action_url = models.URLField(blank=True, null=True, help_text="Template URL with variable support")
|
20
|
+
data_template = models.JSONField(default=dict, blank=True,
|
21
|
+
help_text="Template data payload with variable support")
|
22
|
+
|
23
|
+
# Delivery preferences
|
24
|
+
category = models.CharField(max_length=50, default="general", db_index=True)
|
25
|
+
priority = models.CharField(max_length=20, choices=[
|
26
|
+
('low', 'Low'),
|
27
|
+
('normal', 'Normal'),
|
28
|
+
('high', 'High')
|
29
|
+
], default='normal', db_index=True)
|
30
|
+
|
31
|
+
# Template variables documentation
|
32
|
+
variables = models.JSONField(default=dict, blank=True,
|
33
|
+
help_text="Expected template variables and descriptions for title, body, action_url, and data_template")
|
34
|
+
|
35
|
+
is_active = models.BooleanField(default=True, db_index=True)
|
36
|
+
|
37
|
+
class Meta:
|
38
|
+
ordering = ['group__name', 'name']
|
39
|
+
unique_together = [('group', 'name')]
|
40
|
+
|
41
|
+
class RestMeta:
|
42
|
+
VIEW_PERMS = ["manage_notifications", "manage_groups", "owner", "manage_users"]
|
43
|
+
SAVE_PERMS = ["manage_notifications", "manage_groups"]
|
44
|
+
SEARCH_FIELDS = ["name", "category"]
|
45
|
+
LIST_DEFAULT_FILTERS = {"is_active": True}
|
46
|
+
GRAPHS = {
|
47
|
+
"basic": {
|
48
|
+
"fields": ["id", "name", "category", "priority", "is_active"]
|
49
|
+
},
|
50
|
+
"default": {
|
51
|
+
"fields": ["id", "name", "title_template", "body_template", "action_url",
|
52
|
+
"data_template", "category", "priority", "variables", "is_active"],
|
53
|
+
"graphs": {
|
54
|
+
"group": "basic"
|
55
|
+
}
|
56
|
+
},
|
57
|
+
"full": {
|
58
|
+
"graphs": {
|
59
|
+
"group": "default"
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
|
64
|
+
def __str__(self):
|
65
|
+
org = self.group.name if self.group else "System"
|
66
|
+
return f"{self.name} ({org})"
|
67
|
+
|
68
|
+
def clean(self):
|
69
|
+
"""Validate that at least one template field is provided."""
|
70
|
+
from django.core.exceptions import ValidationError
|
71
|
+
|
72
|
+
has_title = self.title_template and self.title_template.strip()
|
73
|
+
has_body = self.body_template and self.body_template.strip()
|
74
|
+
has_data = self.data_template and bool(self.data_template)
|
75
|
+
|
76
|
+
if not (has_title or has_body or has_data):
|
77
|
+
raise ValidationError(
|
78
|
+
"Template must have at least one of: title_template, body_template, or data_template"
|
79
|
+
)
|
80
|
+
|
81
|
+
def render(self, context):
|
82
|
+
"""
|
83
|
+
Render template with provided context variables.
|
84
|
+
Returns tuple of (title, body, action_url, data)
|
85
|
+
"""
|
86
|
+
title = self.title_template.format(**context) if self.title_template else None
|
87
|
+
body = self.body_template.format(**context) if self.body_template else None
|
88
|
+
action_url = self.action_url.format(**context) if self.action_url else None
|
89
|
+
|
90
|
+
# Render data template with context
|
91
|
+
data = {}
|
92
|
+
if self.data_template:
|
93
|
+
for key, value in self.data_template.items():
|
94
|
+
if isinstance(value, str):
|
95
|
+
data[key] = value.format(**context)
|
96
|
+
else:
|
97
|
+
data[key] = value
|
98
|
+
|
99
|
+
return title, body, action_url, data
|
mojo/apps/account/models/user.py
CHANGED
@@ -6,6 +6,7 @@ from mojo import errors as merrors
|
|
6
6
|
from mojo.helpers import dates
|
7
7
|
from mojo.apps.account.utils.jwtoken import JWToken
|
8
8
|
from mojo.apps import metrics
|
9
|
+
from .device import UserDevice
|
9
10
|
import uuid
|
10
11
|
|
11
12
|
SYS_USER_PERMS_PROTECTION = {
|
@@ -28,6 +29,7 @@ USER_PERMS_PROTECTION.update(SYS_USER_PERMS_PROTECTION)
|
|
28
29
|
|
29
30
|
USER_LAST_ACTIVITY_FREQ = settings.get("USER_LAST_ACTIVITY_FREQ", 300)
|
30
31
|
METRICS_TIMEZONE = settings.get("METRICS_TIMEZONE", "America/Los_Angeles")
|
32
|
+
METRICS_TRACK_USER_ACTIVITY = settings.get("METRICS_TRACK_USER_ACTIVITY", False)
|
31
33
|
|
32
34
|
class CustomUserManager(BaseUserManager):
|
33
35
|
def create_user(self, email, password=None, **extra_fields):
|
@@ -62,6 +64,11 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
62
64
|
phone_number = models.CharField(max_length=32, blank=True, null=True, default=None)
|
63
65
|
is_active = models.BooleanField(default=True, db_index=True)
|
64
66
|
display_name = models.CharField(max_length=80, blank=True, null=True, default=None)
|
67
|
+
|
68
|
+
# Organization relationship for push config resolution
|
69
|
+
org = models.ForeignKey("account.Group", on_delete=models.SET_NULL,
|
70
|
+
null=True, blank=True, related_name="org_users",
|
71
|
+
help_text="Default organization for this user")
|
65
72
|
# key used for sessions and general authentication algs
|
66
73
|
auth_key = models.TextField(null=True, default=None)
|
67
74
|
onetime_code = models.TextField(null=True, default=None)
|
@@ -88,6 +95,8 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
88
95
|
objects = CustomUserManager()
|
89
96
|
|
90
97
|
class RestMeta:
|
98
|
+
LOG_CHANGES = True
|
99
|
+
POST_SAVE_ACTIONS = ['send_invite']
|
91
100
|
NO_SHOW_FIELDS = ["password", "auth_key", "onetime_code"]
|
92
101
|
SEARCH_FIELDS = ["username", "email", "display_name"]
|
93
102
|
VIEW_PERMS = ["view_users", "manage_users", "owner"]
|
@@ -124,7 +133,8 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
124
133
|
'is_active'
|
125
134
|
],
|
126
135
|
"graphs": {
|
127
|
-
"avatar": "basic"
|
136
|
+
"avatar": "basic",
|
137
|
+
"org": "basic"
|
128
138
|
}
|
129
139
|
},
|
130
140
|
"full": {
|
@@ -151,6 +161,12 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
151
161
|
if self.last_activity is None or dates.has_time_elsapsed(self.last_activity, seconds=USER_LAST_ACTIVITY_FREQ):
|
152
162
|
self.last_activity = dates.utcnow()
|
153
163
|
self.atomic_save()
|
164
|
+
if METRICS_TRACK_USER_ACTIVITY:
|
165
|
+
metrics.record(f"user_activity:{self.pk}", category="user", min_granularity="minutes")
|
166
|
+
|
167
|
+
def track(self, request):
|
168
|
+
self.touch()
|
169
|
+
UserDevice.track(request)
|
154
170
|
|
155
171
|
def get_auth_key(self):
|
156
172
|
if self.auth_key is None:
|
@@ -323,8 +339,16 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
323
339
|
# Fall back to using the full email as username
|
324
340
|
return self.email.lower()
|
325
341
|
|
342
|
+
def generate_display_name(self):
|
343
|
+
"""Generate a display name from email, falling back to email if username exists."""
|
344
|
+
# Try using the part before @ as display name
|
345
|
+
# generate display name from usernames like "bob.smith", "bob_smith", "bob.smith@example.com"
|
346
|
+
# Extract the base part (before @ if email format)
|
347
|
+
base_username = self.username.split("@")[0] if "@" in self.username else self.username
|
348
|
+
# Replace underscores and dots with spaces, then title case
|
349
|
+
return base_username.replace("_", " ").replace(".", " ").title()
|
350
|
+
|
326
351
|
def on_rest_pre_save(self, changed_fields, created):
|
327
|
-
self.debug("PRE SAVE")
|
328
352
|
creds_changed = False
|
329
353
|
if "email" in changed_fields:
|
330
354
|
creds_changed = True
|
@@ -348,29 +372,178 @@ class User(MojoSecrets, AbstractBaseUser, MojoModel):
|
|
348
372
|
qset = qset.exclude(pk=self.pk)
|
349
373
|
if qset.exists():
|
350
374
|
raise merrors.ValueException("Username already exists")
|
375
|
+
if not self.display_name:
|
376
|
+
self.display_name = self.generate_display_name()
|
351
377
|
if self.pk is not None:
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
378
|
+
self._handle_new_user_pre_save(creds_changed, changed_fields)
|
379
|
+
|
380
|
+
def _handle_new_user_pre_save(self, creds_changed, changed_fields):
|
381
|
+
# only super user can change email or username
|
382
|
+
if creds_changed and not self.active_user.is_superuser:
|
383
|
+
raise merrors.PermissionDeniedException("You are not allowed to change email or username")
|
384
|
+
if "password" in changed_fields:
|
385
|
+
raise merrors.PermissionDeniedException("You are not allowed to change password")
|
386
|
+
if "new_password" in changed_fields:
|
387
|
+
if not self.can_change_password():
|
356
388
|
raise merrors.PermissionDeniedException("You are not allowed to change password")
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
if
|
365
|
-
|
366
|
-
|
367
|
-
|
389
|
+
self.debug("CHANGING PASSWORD")
|
390
|
+
self.log("****", kind="password:changed")
|
391
|
+
if "email" in changed_fields:
|
392
|
+
self.log(kind="email:changed", log=f"{changed_fields['email']} to {self.email}")
|
393
|
+
if "username" in changed_fields:
|
394
|
+
self.log(kind="username:changed", log=f"{changed_fields['username']} to {self.username}")
|
395
|
+
if "is_active" in changed_fields:
|
396
|
+
if not self.is_active:
|
397
|
+
metrics.record("user_deactivated", category="user", min_granularity="hours")
|
398
|
+
metrics.set_value("total_users", User.objects.filter(is_active=True).count(), account="global")
|
368
399
|
|
369
400
|
def check_edit_permission(self, perms, request):
|
370
401
|
if "owner" in perms and self.is_request_user():
|
371
402
|
return True
|
372
403
|
return request.user.has_permission(perms)
|
373
404
|
|
405
|
+
def on_action_send_invite(self, value):
|
406
|
+
self.send_invite()
|
407
|
+
|
408
|
+
def push_notification(self, title=None, body=None, data=None,
|
409
|
+
category="general", action_url=None,
|
410
|
+
devices=None, user_ids=None, delay=None):
|
411
|
+
from mojo.apps.account.services.push import send_direct_notification
|
412
|
+
send_direct_notification(
|
413
|
+
self, title=title, body=body, data=data, category=category,
|
414
|
+
action_url=action_url,
|
415
|
+
devices=devices, user_ids=user_ids, delay=delay)
|
416
|
+
|
417
|
+
def send_invite(self):
|
418
|
+
self.send_template_email(
|
419
|
+
template_name="invite",
|
420
|
+
context={"user": self.to_dict("basic")}
|
421
|
+
)
|
422
|
+
|
423
|
+
def send_email(
|
424
|
+
self,
|
425
|
+
subject=None,
|
426
|
+
body_text=None,
|
427
|
+
body_html=None,
|
428
|
+
cc=None,
|
429
|
+
bcc=None,
|
430
|
+
reply_to=None,
|
431
|
+
**kwargs
|
432
|
+
):
|
433
|
+
"""Send email to this user using mailbox determined by user's org domain or system default
|
434
|
+
|
435
|
+
Args:
|
436
|
+
subject: Email subject
|
437
|
+
body_text: Optional plain text body
|
438
|
+
body_html: Optional HTML body
|
439
|
+
cc, bcc, reply_to: Optional addressing
|
440
|
+
**kwargs: Additional arguments passed to mailbox.send_email()
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
SentMessage instance
|
444
|
+
|
445
|
+
Raises:
|
446
|
+
ValueError: If no mailbox can be found
|
447
|
+
"""
|
448
|
+
from mojo.apps.aws.models import Mailbox
|
449
|
+
|
450
|
+
mailbox = None
|
451
|
+
|
452
|
+
# Try to get mailbox from org domain
|
453
|
+
if self.org and hasattr(self.org, 'metadata'):
|
454
|
+
domain = self.org.metadata.get("domain")
|
455
|
+
if domain:
|
456
|
+
# Try domain default first
|
457
|
+
mailbox = Mailbox.get_domain_default(domain)
|
458
|
+
if not mailbox:
|
459
|
+
# Try any mailbox from that domain
|
460
|
+
mailbox = Mailbox.objects.filter(
|
461
|
+
domain__name__iexact=domain,
|
462
|
+
allow_outbound=True
|
463
|
+
).first()
|
464
|
+
|
465
|
+
# Fall back to system default
|
466
|
+
if not mailbox:
|
467
|
+
mailbox = Mailbox.get_system_default()
|
468
|
+
|
469
|
+
if not mailbox:
|
470
|
+
raise ValueError("No mailbox available for sending email. Please configure a system default mailbox.")
|
471
|
+
|
472
|
+
return mailbox.send_email(
|
473
|
+
to=self.email,
|
474
|
+
subject=subject,
|
475
|
+
body_text=body_text,
|
476
|
+
body_html=body_html,
|
477
|
+
cc=cc,
|
478
|
+
bcc=bcc,
|
479
|
+
reply_to=reply_to,
|
480
|
+
**kwargs
|
481
|
+
)
|
482
|
+
|
483
|
+
def send_template_email(
|
484
|
+
self,
|
485
|
+
template_name,
|
486
|
+
context=None,
|
487
|
+
cc=None,
|
488
|
+
bcc=None,
|
489
|
+
reply_to=None,
|
490
|
+
**kwargs
|
491
|
+
):
|
492
|
+
"""Send template email to this user using mailbox determined by user's org domain or system default
|
493
|
+
|
494
|
+
Args:
|
495
|
+
template_name: Name of the EmailTemplate in database
|
496
|
+
context: Template context variables (user will be added automatically)
|
497
|
+
cc, bcc, reply_to: Optional addressing
|
498
|
+
**kwargs: Additional arguments passed to mailbox.send_template_email()
|
499
|
+
|
500
|
+
Returns:
|
501
|
+
SentMessage instance
|
502
|
+
|
503
|
+
Raises:
|
504
|
+
ValueError: If no mailbox can be found or template not found
|
505
|
+
"""
|
506
|
+
from mojo.apps.aws.models import Mailbox
|
507
|
+
|
508
|
+
mailbox = None
|
509
|
+
|
510
|
+
# Try to get mailbox from org domain
|
511
|
+
if self.org and hasattr(self.org, 'metadata'):
|
512
|
+
domain = self.org.metadata.get("domain")
|
513
|
+
if domain:
|
514
|
+
# Try domain default first
|
515
|
+
mailbox = Mailbox.get_domain_default(domain)
|
516
|
+
if not mailbox:
|
517
|
+
# Try any mailbox from that domain
|
518
|
+
mailbox = Mailbox.objects.filter(
|
519
|
+
domain__name__iexact=domain,
|
520
|
+
allow_outbound=True
|
521
|
+
).first()
|
522
|
+
|
523
|
+
# Fall back to system default
|
524
|
+
if not mailbox:
|
525
|
+
mailbox = Mailbox.get_system_default()
|
526
|
+
|
527
|
+
if not mailbox:
|
528
|
+
raise ValueError("No mailbox available for sending email. Please configure a system default mailbox.")
|
529
|
+
|
530
|
+
# Add user to context if not already present
|
531
|
+
if context is None:
|
532
|
+
context = {}
|
533
|
+
if 'user' not in context:
|
534
|
+
context['user'] = self.to_dict("basic")
|
535
|
+
|
536
|
+
return mailbox.send_template_email(
|
537
|
+
to=self.email,
|
538
|
+
template_name=template_name,
|
539
|
+
context=context,
|
540
|
+
cc=cc,
|
541
|
+
bcc=bcc,
|
542
|
+
reply_to=reply_to,
|
543
|
+
allow_unverified=True,
|
544
|
+
**kwargs
|
545
|
+
)
|
546
|
+
|
374
547
|
@classmethod
|
375
548
|
def validate_jwt(cls, token):
|
376
549
|
token_manager = JWToken()
|
@@ -0,0 +1,39 @@
|
|
1
|
+
from mojo import decorators as md
|
2
|
+
from mojo.apps.account.models.device import UserDevice, UserDeviceLocation, GeoLocatedIP
|
3
|
+
|
4
|
+
|
5
|
+
@md.URL('user/device')
|
6
|
+
@md.URL('user/device/<int:pk>')
|
7
|
+
def on_user_device(request, pk=None):
|
8
|
+
return UserDevice.on_rest_request(request, pk)
|
9
|
+
|
10
|
+
|
11
|
+
@md.GET('user/device/lookup')
|
12
|
+
@md.requires_params('duid')
|
13
|
+
def on_user_device_by_duid(request):
|
14
|
+
duid = request.DATA.get('duid')
|
15
|
+
device = UserDevice.objects.filter(duid=duid).first()
|
16
|
+
if not device:
|
17
|
+
return UserDevice.rest_error_response(request, 404, error="Device not found")
|
18
|
+
return device.on_rest_get(request)
|
19
|
+
|
20
|
+
|
21
|
+
@md.URL('user/device/location')
|
22
|
+
@md.URL('user/device/location/<int:pk>')
|
23
|
+
def on_user_device_location(request, pk=None):
|
24
|
+
return UserDeviceLocation.on_rest_request(request, pk)
|
25
|
+
|
26
|
+
|
27
|
+
@md.URL('system/geoip')
|
28
|
+
@md.URL('system/geoip/<int:pk>')
|
29
|
+
def on_geo_located_ip(request, pk=None):
|
30
|
+
return GeoLocatedIP.on_rest_request(request, pk)
|
31
|
+
|
32
|
+
|
33
|
+
@md.GET('system/geoip/lookup')
|
34
|
+
@md.requires_params('ip')
|
35
|
+
def on_geo_located_ip_lookup(request):
|
36
|
+
ip_address = request.DATA.get('ip')
|
37
|
+
auto_refresh = request.DATA.get('auto_refresh', True)
|
38
|
+
geo_ip = GeoLocatedIP.geolocate(ip_address, auto_refresh=auto_refresh)
|
39
|
+
return geo_ip.on_rest_get(request)
|
mojo/apps/account/rest/group.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from mojo import decorators as md
|
2
2
|
from mojo.apps.account.models import Group, GroupMember
|
3
|
+
from mojo.helpers.response import JsonResponse
|
3
4
|
|
4
5
|
|
5
6
|
@md.URL('group')
|
@@ -14,6 +15,13 @@ def on_group_member(request, pk=None):
|
|
14
15
|
return GroupMember.on_rest_request(request, pk)
|
15
16
|
|
16
17
|
|
18
|
+
@md.POST('group/member/invite')
|
19
|
+
@md.requires_params('email', 'group')
|
20
|
+
def on_group_invite_member(request):
|
21
|
+
ms = request.group.invite(request.DATA['email'])
|
22
|
+
return ms.on_rest_get(request)
|
23
|
+
|
24
|
+
|
17
25
|
@md.GET('group/<int:pk>/member')
|
18
26
|
def on_group_me_member(request, pk=None):
|
19
27
|
request.group = Group.objects.filter(pk=pk).last()
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import mojo.decorators as md
|
2
|
+
from mojo.apps.account.models import (
|
3
|
+
RegisteredDevice, NotificationTemplate, PushConfig,
|
4
|
+
NotificationDelivery
|
5
|
+
)
|
6
|
+
from mojo.apps.account.services.push import (
|
7
|
+
send_push_notification, send_direct_notification
|
8
|
+
)
|
9
|
+
from mojo.helpers import response
|
10
|
+
|
11
|
+
|
12
|
+
@md.POST('account/devices/push/register')
|
13
|
+
@md.requires_auth()
|
14
|
+
@md.requires_params(['device_token', 'device_id', 'platform'])
|
15
|
+
def register_device(request):
|
16
|
+
"""
|
17
|
+
Register device for push notifications.
|
18
|
+
|
19
|
+
POST /api/account/devices/push/register
|
20
|
+
{
|
21
|
+
"device_token": "...",
|
22
|
+
"device_id": "...",
|
23
|
+
"platform": "ios|android|web",
|
24
|
+
"device_name": "...",
|
25
|
+
"app_version": "...",
|
26
|
+
"os_version": "...",
|
27
|
+
"push_preferences": {"orders": true, "marketing": false}
|
28
|
+
}
|
29
|
+
"""
|
30
|
+
device, created = RegisteredDevice.objects.update_or_create(
|
31
|
+
user=request.user,
|
32
|
+
device_id=request.DATA.get('device_id'),
|
33
|
+
defaults={
|
34
|
+
'device_token': request.DATA.get('device_token'),
|
35
|
+
'platform': request.DATA.get('platform'),
|
36
|
+
'device_name': request.DATA.get('device_name', ''),
|
37
|
+
'app_version': request.DATA.get('app_version', ''),
|
38
|
+
'os_version': request.DATA.get('os_version', ''),
|
39
|
+
'push_preferences': request.DATA.get('push_preferences', {}),
|
40
|
+
'is_active': True,
|
41
|
+
'push_enabled': True
|
42
|
+
}
|
43
|
+
)
|
44
|
+
|
45
|
+
return device.on_rest_get(request, 'default')
|
46
|
+
|
47
|
+
|
48
|
+
@md.URL('account/devices/push')
|
49
|
+
@md.URL('account/devices/push/<int:pk>')
|
50
|
+
def on_registered_devices(request, pk=None):
|
51
|
+
"""Standard CRUD for registered devices."""
|
52
|
+
return RegisteredDevice.on_rest_request(request, pk)
|
53
|
+
|
54
|
+
|
55
|
+
@md.URL('account/devices/push/templates')
|
56
|
+
@md.URL('account/devices/push/templates/<int:pk>')
|
57
|
+
def on_notification_templates(request, pk=None):
|
58
|
+
"""Standard CRUD for notification templates."""
|
59
|
+
return NotificationTemplate.on_rest_request(request, pk)
|
60
|
+
|
61
|
+
|
62
|
+
@md.URL('account/devices/push/config')
|
63
|
+
@md.URL('account/devices/push/config/<int:pk>')
|
64
|
+
def on_push_config(request, pk=None):
|
65
|
+
"""Standard CRUD for push configuration."""
|
66
|
+
return PushConfig.on_rest_request(request, pk)
|
67
|
+
|
68
|
+
|
69
|
+
@md.URL('account/devices/push/deliveries')
|
70
|
+
@md.URL('account/devices/push/deliveries/<int:pk>')
|
71
|
+
def on_notification_deliveries(request, pk=None):
|
72
|
+
"""Standard CRUD for notification delivery history."""
|
73
|
+
return NotificationDelivery.on_rest_request(request, pk)
|
74
|
+
|
75
|
+
|
76
|
+
@md.POST('account/devices/push/send')
|
77
|
+
@md.requires_auth()
|
78
|
+
@md.requires_perms("send_notifications")
|
79
|
+
def send_notification(request):
|
80
|
+
"""
|
81
|
+
Send push notification using template or direct content.
|
82
|
+
|
83
|
+
POST /api/account/devices/push/send
|
84
|
+
|
85
|
+
Templated:
|
86
|
+
{
|
87
|
+
"template": "template_name",
|
88
|
+
"context": {"key": "value"},
|
89
|
+
"user_ids": [1, 2, 3] # optional
|
90
|
+
}
|
91
|
+
|
92
|
+
Direct:
|
93
|
+
{
|
94
|
+
"title": "Hello!",
|
95
|
+
"body": "Your order is ready",
|
96
|
+
"category": "orders",
|
97
|
+
"action_url": "myapp://orders/123",
|
98
|
+
"user_ids": [1, 2, 3] # optional
|
99
|
+
}
|
100
|
+
"""
|
101
|
+
template = request.DATA.get('template')
|
102
|
+
title = request.DATA.get('title')
|
103
|
+
body = request.DATA.get('body')
|
104
|
+
|
105
|
+
if template:
|
106
|
+
# Templated sending
|
107
|
+
context = request.DATA.get('context', {})
|
108
|
+
user_ids = request.DATA.get('user_ids')
|
109
|
+
results = send_push_notification(
|
110
|
+
user=request.user,
|
111
|
+
template_name=template,
|
112
|
+
context=context,
|
113
|
+
user_ids=user_ids
|
114
|
+
)
|
115
|
+
elif title and body:
|
116
|
+
# Direct sending
|
117
|
+
category = request.DATA.get('category', 'general')
|
118
|
+
action_url = request.DATA.get('action_url')
|
119
|
+
user_ids = request.DATA.get('user_ids')
|
120
|
+
results = send_direct_notification(
|
121
|
+
user=request.user,
|
122
|
+
title=title,
|
123
|
+
body=body,
|
124
|
+
category=category,
|
125
|
+
action_url=action_url,
|
126
|
+
user_ids=user_ids
|
127
|
+
)
|
128
|
+
else:
|
129
|
+
return response.error('Must provide either template or both title and body')
|
130
|
+
|
131
|
+
return response.success({
|
132
|
+
'success': True,
|
133
|
+
'sent_count': len([r for r in results if r.status == 'sent']),
|
134
|
+
'failed_count': len([r for r in results if r.status == 'failed']),
|
135
|
+
'deliveries': [r.to_dict("basic") for r in results]
|
136
|
+
})
|
137
|
+
|
138
|
+
|
139
|
+
@md.POST('account/devices/push/test')
|
140
|
+
@md.requires_auth()
|
141
|
+
def test_push_config(request):
|
142
|
+
"""
|
143
|
+
Test push configuration by sending a test notification to requesting user's devices.
|
144
|
+
|
145
|
+
POST /api/account/devices/push/test
|
146
|
+
{
|
147
|
+
"message": "Custom test message" # optional
|
148
|
+
}
|
149
|
+
"""
|
150
|
+
test_message = request.DATA.get('message', 'This is a test notification')
|
151
|
+
|
152
|
+
results = send_direct_notification(
|
153
|
+
user=request.user,
|
154
|
+
title="Push Test",
|
155
|
+
body=test_message,
|
156
|
+
category="test"
|
157
|
+
)
|
158
|
+
|
159
|
+
if not results:
|
160
|
+
return response.error('No registered devices found for testing')
|
161
|
+
|
162
|
+
return response.success({
|
163
|
+
'success': True,
|
164
|
+
'message': 'Test notifications sent',
|
165
|
+
'results': [r.to_dict('basic') for r in results]
|
166
|
+
})
|
167
|
+
|
168
|
+
|
169
|
+
@md.GET('account/devices/push/stats')
|
170
|
+
@md.requires_auth()
|
171
|
+
def push_stats(request):
|
172
|
+
"""
|
173
|
+
Get push notification statistics for the requesting user.
|
174
|
+
"""
|
175
|
+
user_deliveries = NotificationDelivery.objects.filter(user=request.user)
|
176
|
+
|
177
|
+
stats = {
|
178
|
+
'total_sent': user_deliveries.filter(status='sent').count(),
|
179
|
+
'total_failed': user_deliveries.filter(status='failed').count(),
|
180
|
+
'total_pending': user_deliveries.filter(status='pending').count(),
|
181
|
+
'registered_devices': request.user.registered_devices.filter(is_active=True).count(),
|
182
|
+
'enabled_devices': request.user.registered_devices.filter(
|
183
|
+
is_active=True, push_enabled=True
|
184
|
+
).count()
|
185
|
+
}
|
186
|
+
|
187
|
+
return response.success(stats)
|