dmart 0.1.9__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.
- alembic/__init__.py +0 -0
- alembic/env.py +91 -0
- alembic/scripts/__init__.py +0 -0
- alembic/scripts/calculate_checksums.py +77 -0
- alembic/scripts/migration_f7a4949eed19.py +28 -0
- alembic/versions/0f3d2b1a7c21_add_authz_materialized_views.py +87 -0
- alembic/versions/10d2041b94d4_last_checksum_history.py +62 -0
- alembic/versions/1cf4e1ee3cb8_ext_permission_with_filter_fields_values.py +33 -0
- alembic/versions/26bfe19b49d4_rm_failedloginattempts.py +42 -0
- alembic/versions/3c8bca2219cc_add_otp_table.py +38 -0
- alembic/versions/6675fd9dfe42_remove_unique_from_sessions_table.py +36 -0
- alembic/versions/71bc1df82e6a_adding_user_last_login_at.py +43 -0
- alembic/versions/74288ccbd3b5_initial.py +264 -0
- alembic/versions/7520a89a8467_rm_activesession_table.py +39 -0
- alembic/versions/848b623755a4_make_created_nd_updated_at_required.py +138 -0
- alembic/versions/8640dcbebf85_add_notes_to_users.py +32 -0
- alembic/versions/91c94250232a_adding_fk_on_owner_shortname.py +104 -0
- alembic/versions/98ecd6f56f9a_ext_meta_with_owner_group_shortname.py +66 -0
- alembic/versions/9aae9138c4ef_indexing_created_at_updated_at.py +80 -0
- alembic/versions/__init__.py +0 -0
- alembic/versions/b53f916b3f6d_json_to_jsonb.py +492 -0
- alembic/versions/eb5f1ec65156_adding_user_locked_to_device.py +36 -0
- alembic/versions/f7a4949eed19_adding_query_policies_to_meta.py +60 -0
- api/__init__.py +0 -0
- api/info/__init__.py +0 -0
- api/info/router.py +109 -0
- api/managed/__init__.py +0 -0
- api/managed/router.py +1541 -0
- api/managed/utils.py +1850 -0
- api/public/__init__.py +0 -0
- api/public/router.py +758 -0
- api/qr/__init__.py +0 -0
- api/qr/router.py +108 -0
- api/user/__init__.py +0 -0
- api/user/model/__init__.py +0 -0
- api/user/model/errors.py +14 -0
- api/user/model/requests.py +165 -0
- api/user/model/responses.py +11 -0
- api/user/router.py +1401 -0
- api/user/service.py +270 -0
- bundler.py +44 -0
- config/__init__.py +0 -0
- config/channels.json +11 -0
- config/notification.json +17 -0
- data_adapters/__init__.py +0 -0
- data_adapters/adapter.py +16 -0
- data_adapters/base_data_adapter.py +467 -0
- data_adapters/file/__init__.py +0 -0
- data_adapters/file/adapter.py +2043 -0
- data_adapters/file/adapter_helpers.py +1013 -0
- data_adapters/file/archive.py +150 -0
- data_adapters/file/create_index.py +331 -0
- data_adapters/file/create_users_folders.py +52 -0
- data_adapters/file/custom_validations.py +68 -0
- data_adapters/file/drop_index.py +40 -0
- data_adapters/file/health_check.py +560 -0
- data_adapters/file/redis_services.py +1110 -0
- data_adapters/helpers.py +27 -0
- data_adapters/sql/__init__.py +0 -0
- data_adapters/sql/adapter.py +3210 -0
- data_adapters/sql/adapter_helpers.py +491 -0
- data_adapters/sql/create_tables.py +451 -0
- data_adapters/sql/create_users_folders.py +53 -0
- data_adapters/sql/db_to_json_migration.py +482 -0
- data_adapters/sql/health_check_sql.py +232 -0
- data_adapters/sql/json_to_db_migration.py +454 -0
- data_adapters/sql/update_query_policies.py +101 -0
- data_generator.py +81 -0
- dmart-0.1.9.dist-info/METADATA +64 -0
- dmart-0.1.9.dist-info/RECORD +149 -0
- dmart-0.1.9.dist-info/WHEEL +5 -0
- dmart-0.1.9.dist-info/entry_points.txt +2 -0
- dmart-0.1.9.dist-info/top_level.txt +23 -0
- dmart.py +513 -0
- get_settings.py +7 -0
- languages/__init__.py +0 -0
- languages/arabic.json +15 -0
- languages/english.json +16 -0
- languages/kurdish.json +14 -0
- languages/loader.py +13 -0
- main.py +506 -0
- migrate.py +24 -0
- models/__init__.py +0 -0
- models/api.py +203 -0
- models/core.py +597 -0
- models/enums.py +255 -0
- password_gen.py +8 -0
- plugins/__init__.py +0 -0
- plugins/action_log/__init__.py +0 -0
- plugins/action_log/plugin.py +121 -0
- plugins/admin_notification_sender/__init__.py +0 -0
- plugins/admin_notification_sender/plugin.py +124 -0
- plugins/ldap_manager/__init__.py +0 -0
- plugins/ldap_manager/plugin.py +100 -0
- plugins/local_notification/__init__.py +0 -0
- plugins/local_notification/plugin.py +123 -0
- plugins/realtime_updates_notifier/__init__.py +0 -0
- plugins/realtime_updates_notifier/plugin.py +58 -0
- plugins/redis_db_update/__init__.py +0 -0
- plugins/redis_db_update/plugin.py +188 -0
- plugins/resource_folders_creation/__init__.py +0 -0
- plugins/resource_folders_creation/plugin.py +81 -0
- plugins/system_notification_sender/__init__.py +0 -0
- plugins/system_notification_sender/plugin.py +188 -0
- plugins/update_access_controls/__init__.py +0 -0
- plugins/update_access_controls/plugin.py +9 -0
- pytests/__init__.py +0 -0
- pytests/api_user_models_erros_test.py +16 -0
- pytests/api_user_models_requests_test.py +98 -0
- pytests/archive_test.py +72 -0
- pytests/base_test.py +300 -0
- pytests/get_settings_test.py +14 -0
- pytests/json_to_db_migration_test.py +237 -0
- pytests/service_test.py +26 -0
- pytests/test_info.py +55 -0
- pytests/test_status.py +15 -0
- run_notification_campaign.py +98 -0
- scheduled_notification_handler.py +121 -0
- schema_migration.py +208 -0
- schema_modulate.py +192 -0
- set_admin_passwd.py +55 -0
- sync.py +202 -0
- utils/__init__.py +0 -0
- utils/access_control.py +306 -0
- utils/async_request.py +8 -0
- utils/exporter.py +309 -0
- utils/firebase_notifier.py +57 -0
- utils/generate_email.py +38 -0
- utils/helpers.py +352 -0
- utils/hypercorn_config.py +12 -0
- utils/internal_error_code.py +60 -0
- utils/jwt.py +124 -0
- utils/logger.py +167 -0
- utils/middleware.py +99 -0
- utils/notification.py +75 -0
- utils/password_hashing.py +16 -0
- utils/plugin_manager.py +215 -0
- utils/query_policies_helper.py +112 -0
- utils/regex.py +44 -0
- utils/repository.py +529 -0
- utils/router_helper.py +19 -0
- utils/settings.py +165 -0
- utils/sms_notifier.py +21 -0
- utils/social_sso.py +67 -0
- utils/templates/activation.html.j2 +26 -0
- utils/templates/reminder.html.j2 +17 -0
- utils/ticket_sys_utils.py +203 -0
- utils/web_notifier.py +29 -0
- websocket.py +231 -0
utils/settings.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
""" Application Settings """
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import string
|
|
7
|
+
import random
|
|
8
|
+
import sys
|
|
9
|
+
from venv import logger
|
|
10
|
+
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Settings(BaseSettings):
|
|
17
|
+
"""Main settings class"""
|
|
18
|
+
|
|
19
|
+
app_url: str = ""
|
|
20
|
+
public_app_url: str = ""
|
|
21
|
+
app_name: str = "dmart"
|
|
22
|
+
websocket_url: str = "" #"http://127.0.0.1:8484"
|
|
23
|
+
websocket_port: int = 8484
|
|
24
|
+
base_path: str = ""
|
|
25
|
+
debug_enabled: bool = True
|
|
26
|
+
log_handlers: list[str] = ['file']
|
|
27
|
+
log_file: str = "../logs/dmart.ljson.log"
|
|
28
|
+
ws_log_file: str = "../logs/websocket.ljson.log"
|
|
29
|
+
jwt_secret: str = "".join(random.sample(string.ascii_letters + string.digits,12))
|
|
30
|
+
jwt_algorithm: str = "HS256"
|
|
31
|
+
jwt_access_expires: int = 30 * 86400 # 30 days
|
|
32
|
+
listening_host: str = "0.0.0.0"
|
|
33
|
+
listening_port: int = 8282
|
|
34
|
+
redis_host: str = "127.0.0.1"
|
|
35
|
+
redis_password: str = ""
|
|
36
|
+
redis_port: int = 6379
|
|
37
|
+
redis_pool_max_connections: int = 20
|
|
38
|
+
max_sessions_per_user: int = 5
|
|
39
|
+
management_space: str = "management"
|
|
40
|
+
users_subpath: str = "users"
|
|
41
|
+
spaces_folder: Path = Path("../sample/spaces/")
|
|
42
|
+
lock_period: int = 300
|
|
43
|
+
auto_uuid_rule: str = "auto" # Used to generate a shortname from UUID
|
|
44
|
+
google_application_credentials: str = ""
|
|
45
|
+
is_registrable: bool = True
|
|
46
|
+
is_otp_for_create_required: bool = True
|
|
47
|
+
social_login_allowed: bool = True
|
|
48
|
+
all_spaces_mw: str = (
|
|
49
|
+
"__all_spaces__" # magic word used in access control refers to any space
|
|
50
|
+
)
|
|
51
|
+
all_subpaths_mw: str = (
|
|
52
|
+
"__all_subpaths__" # used in access control refers to all subpaths
|
|
53
|
+
)
|
|
54
|
+
current_user_mw: str = (
|
|
55
|
+
"__current_user__" # used in access control refers to current logged-in user
|
|
56
|
+
)
|
|
57
|
+
root_subpath_mw : str = "__root__"
|
|
58
|
+
email_sender: str = "dmart@dmart.com"
|
|
59
|
+
|
|
60
|
+
otp_token_ttl: int = 60 * 5
|
|
61
|
+
allow_otp_resend_after: int = 60
|
|
62
|
+
comms_api: str = ""
|
|
63
|
+
send_sms_otp_api: str = ""
|
|
64
|
+
smpp_auth_key: str = ""
|
|
65
|
+
send_email_otp_api: str = ""
|
|
66
|
+
send_sms_api: str = ""
|
|
67
|
+
send_email_api: str = ""
|
|
68
|
+
mock_smtp_api: bool = True
|
|
69
|
+
files_query: str = "scandir"
|
|
70
|
+
mock_smpp_api: bool = True
|
|
71
|
+
mock_otp_code: str = "123456"
|
|
72
|
+
invitation_link: str = ""
|
|
73
|
+
ldap_url: str = "ldap://"
|
|
74
|
+
ldap_admin_dn: str = ""
|
|
75
|
+
ldap_root_dn: str = ""
|
|
76
|
+
ldap_pass: str = ""
|
|
77
|
+
max_query_limit: int = 10000
|
|
78
|
+
session_inactivity_ttl: int = 0 # Set initially to 0 to disable session timeout. Possible value : 60 * 60 * 24 * 7 # 7 days
|
|
79
|
+
request_timeout: int = 35 # In seconds the time of dmart requests.
|
|
80
|
+
jq_timeout: int = 2 # secs
|
|
81
|
+
|
|
82
|
+
url_shorter_expires: int = 60 * 60 * 48 # 48 hours
|
|
83
|
+
|
|
84
|
+
google_client_id: str = ""
|
|
85
|
+
google_client_secret: str = ""
|
|
86
|
+
apple_client_id: str = ""
|
|
87
|
+
apple_client_secret: str = ""
|
|
88
|
+
|
|
89
|
+
facebook_client_id: str = ""
|
|
90
|
+
facebook_client_secret: str = ""
|
|
91
|
+
|
|
92
|
+
enable_channel_auth: bool = False
|
|
93
|
+
channels: list = []
|
|
94
|
+
store_payload_string: bool = True
|
|
95
|
+
|
|
96
|
+
active_operational_db: str = "redis" # allowed values: redis, manticore
|
|
97
|
+
active_data_db: str = "file" # allowed values: file, sql
|
|
98
|
+
|
|
99
|
+
database_driver: str = 'postgresql+psycopg'
|
|
100
|
+
database_username: str = 'postgres'
|
|
101
|
+
database_password: str = ''
|
|
102
|
+
database_host: str = 'localhost'
|
|
103
|
+
database_port: int = 5432
|
|
104
|
+
database_name: str = 'dmart'
|
|
105
|
+
database_pool_size: int = 2
|
|
106
|
+
database_max_overflow: int = 2
|
|
107
|
+
database_pool_timeout: int = 30
|
|
108
|
+
database_pool_recycle: int = 30
|
|
109
|
+
|
|
110
|
+
hide_stack_trace: bool = False
|
|
111
|
+
max_failed_login_attempts: int = 5
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
model_config = SettingsConfigDict(
|
|
115
|
+
env_file=os.getenv(
|
|
116
|
+
"BACKEND_ENV",
|
|
117
|
+
str(Path(__file__).resolve().parent.parent.parent / "config.env") if __file__.endswith(".pyc") else "config.env"
|
|
118
|
+
), env_file_encoding="utf-8"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def load_config_files(self) -> None:
|
|
122
|
+
channels_config_file = Path(__file__).resolve().parent.parent / 'config/channels.json'
|
|
123
|
+
if channels_config_file.exists():
|
|
124
|
+
try:
|
|
125
|
+
with open(channels_config_file, 'r') as file:
|
|
126
|
+
self.channels = json.load(file)
|
|
127
|
+
|
|
128
|
+
# Compile the patterns for better performance
|
|
129
|
+
for idx, channel in enumerate(self.channels):
|
|
130
|
+
compiled_patterns: list[re.Pattern] = []
|
|
131
|
+
for pattern in channel.get("allowed_api_patterns", []):
|
|
132
|
+
compiled_patterns.append(re.compile(pattern))
|
|
133
|
+
self.channels[idx]["allowed_api_patterns"] = compiled_patterns
|
|
134
|
+
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.error(f"Failed to open the channel config file at {channels_config_file}. Error: {e}")
|
|
137
|
+
|
|
138
|
+
raw_allowed_submit_models: str = Field(default="",alias="allowed_submit_models")
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def allowed_submit_models(self) -> dict[str, list[str]]:
|
|
142
|
+
allowed_models_str = self.raw_allowed_submit_models
|
|
143
|
+
result: dict = {}
|
|
144
|
+
if allowed_models_str:
|
|
145
|
+
entries = allowed_models_str.split(",")
|
|
146
|
+
for entry in entries:
|
|
147
|
+
entry = entry.strip()
|
|
148
|
+
if "." in entry:
|
|
149
|
+
space, schema = entry.split(".", 1)
|
|
150
|
+
if space not in result:
|
|
151
|
+
result[space] = []
|
|
152
|
+
result[space].append(schema)
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
Settings.model_validate(
|
|
157
|
+
Settings()
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
# logger.error(f"Failed to load settings.\nError: {e}")
|
|
161
|
+
# sys.exit(1)
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
settings = Settings()
|
|
165
|
+
settings.load_config_files()
|
utils/sms_notifier.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from api.user.service import send_sms
|
|
2
|
+
from models.core import NotificationData
|
|
3
|
+
from utils.helpers import lang_code
|
|
4
|
+
from utils.notification import Notifier
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SMSNotifier(Notifier):
|
|
8
|
+
|
|
9
|
+
async def send(
|
|
10
|
+
self,
|
|
11
|
+
data: NotificationData
|
|
12
|
+
) -> bool:
|
|
13
|
+
user_lang = lang_code(data.receiver.get("language", "ar"))
|
|
14
|
+
if "msisdn" not in data.receiver:
|
|
15
|
+
return False
|
|
16
|
+
await send_sms(
|
|
17
|
+
data.receiver["msisdn"],
|
|
18
|
+
data.title.__getattribute__(user_lang)
|
|
19
|
+
)
|
|
20
|
+
return True
|
|
21
|
+
|
utils/social_sso.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from fastapi_sso.sso.facebook import FacebookSSO
|
|
3
|
+
from fastapi_sso.sso.google import GoogleSSO
|
|
4
|
+
from httpx import AsyncClient
|
|
5
|
+
from utils.settings import settings
|
|
6
|
+
from fastapi_sso.sso.generic import create_provider
|
|
7
|
+
from fastapi_sso.sso.base import OpenID, SSOBase, DiscoveryDocument
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
FACEBOOK_CLIENT_ID = settings.facebook_client_id
|
|
11
|
+
FACEBOOK_CLIENT_SECRET = settings.facebook_client_secret
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_facebook_sso() -> FacebookSSO:
|
|
15
|
+
return FacebookSSO(
|
|
16
|
+
FACEBOOK_CLIENT_ID,
|
|
17
|
+
FACEBOOK_CLIENT_SECRET,
|
|
18
|
+
redirect_uri=f"{settings.app_url}/user/facebook/callback",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
GOOGLE_CLIENT_ID = settings.google_client_id
|
|
23
|
+
GOOGLE_CLIENT_SECRET = settings.google_client_secret
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_google_sso() -> GoogleSSO:
|
|
27
|
+
return GoogleSSO(
|
|
28
|
+
GOOGLE_CLIENT_ID,
|
|
29
|
+
GOOGLE_CLIENT_SECRET,
|
|
30
|
+
redirect_uri=f"{settings.app_url}/user/google/callback",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Apple SSO
|
|
34
|
+
APPLE_CLIENT_ID = settings.apple_client_id # Your Apple Services ID
|
|
35
|
+
APPLE_CLIENT_SECRET = settings.apple_client_secret
|
|
36
|
+
|
|
37
|
+
def apple_response_convertor(response: dict[str, Any], client: Optional[AsyncClient] = None) -> OpenID:
|
|
38
|
+
return OpenID(
|
|
39
|
+
id=response.get("sub"),
|
|
40
|
+
email=response.get("email"),
|
|
41
|
+
first_name=response.get("fullName", []).split(" ")[0],
|
|
42
|
+
last_name=response.get("fullName", []).split(" ")[-1],
|
|
43
|
+
picture=response.get("picture"),
|
|
44
|
+
provider="apple",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def get_apple_sso() -> SSOBase:
|
|
48
|
+
apple_discovery = DiscoveryDocument({
|
|
49
|
+
"authorization_endpoint":"https://appleid.apple.com/auth/authorize",
|
|
50
|
+
"token_endpoint":"https://appleid.apple.com/auth/token",
|
|
51
|
+
#"jwks_uri":"https://appleid.apple.com/auth/keys",
|
|
52
|
+
"userinfo_endpoint": "http://localhost:9090/me",
|
|
53
|
+
|
|
54
|
+
})
|
|
55
|
+
AppleProvider: type[SSOBase] = create_provider(
|
|
56
|
+
name="apple",
|
|
57
|
+
discovery_document=apple_discovery,
|
|
58
|
+
default_scope=["fullName", "email"],
|
|
59
|
+
response_convertor=apple_response_convertor
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return AppleProvider(
|
|
63
|
+
client_id=APPLE_CLIENT_ID,
|
|
64
|
+
client_secret=APPLE_CLIENT_SECRET,
|
|
65
|
+
redirect_uri=f"{settings.app_url}/user/apple/callback",
|
|
66
|
+
allow_insecure_http=True
|
|
67
|
+
)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Email</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
8
|
+
</head>
|
|
9
|
+
<body style="margin: 0; padding: 0">
|
|
10
|
+
|
|
11
|
+
<p>Hi {{name}}</p>
|
|
12
|
+
<p>MSISDN: {{msisdn}}</p>
|
|
13
|
+
<p>Username: {{shortname}}</p>
|
|
14
|
+
|
|
15
|
+
<p>Welcome, we're happy to see you on board!</p>
|
|
16
|
+
|
|
17
|
+
<p>Only Few steps are left to activate your account, please use the below account activation link.</p>
|
|
18
|
+
|
|
19
|
+
<p>Activation Link:</p>
|
|
20
|
+
|
|
21
|
+
<a href="{{ link }}">{{ link }}</a>
|
|
22
|
+
|
|
23
|
+
<p>Regards,</p>
|
|
24
|
+
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<title>Email</title>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
8
|
+
</head>
|
|
9
|
+
<body style="margin: 0; padding: 0">
|
|
10
|
+
<p>Hi {{ name }},</p>
|
|
11
|
+
<p>We would like to remind you to activate your account. We will be happy to see you on board!</p>
|
|
12
|
+
<p>Few steps are left to activate your account, please use the below account activation link.</p>
|
|
13
|
+
<p>Activation Link:</p>
|
|
14
|
+
{{ link }}
|
|
15
|
+
<p>Regards,</p>
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import models.api as api
|
|
2
|
+
from fastapi import status
|
|
3
|
+
from data_adapters.adapter import data_adapter as db
|
|
4
|
+
import models.core as core
|
|
5
|
+
from utils.internal_error_code import InternalErrorCode
|
|
6
|
+
from utils.settings import settings
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def get_init_state_from_workflow(space_name: str, workflow_shortname: str):
|
|
11
|
+
payload = await db.load_resource_payload(
|
|
12
|
+
space_name=space_name,
|
|
13
|
+
subpath="workflows",
|
|
14
|
+
filename=workflow_shortname,
|
|
15
|
+
class_type=core.Content,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if payload is None:
|
|
19
|
+
raise api.Exception(
|
|
20
|
+
status.HTTP_400_BAD_REQUEST,
|
|
21
|
+
api.Error(
|
|
22
|
+
type="request",
|
|
23
|
+
code=InternalErrorCode.SHORTNAME_ALREADY_EXIST,
|
|
24
|
+
message="Workflow not found",
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return payload['initial_state'][0]['name']
|
|
29
|
+
|
|
30
|
+
async def set_init_state_for_record(record: core.Record, space_name: str, logged_in_user: str):
|
|
31
|
+
workflow_attr = record.attributes
|
|
32
|
+
workflow_shortname = workflow_attr["workflow_shortname"]
|
|
33
|
+
|
|
34
|
+
_user_roles = await db.get_user_roles(logged_in_user)
|
|
35
|
+
user_roles = _user_roles.keys()
|
|
36
|
+
|
|
37
|
+
workflows_data: core.Content = await db.load(
|
|
38
|
+
space_name=space_name,
|
|
39
|
+
subpath="workflows",
|
|
40
|
+
shortname=workflow_shortname,
|
|
41
|
+
class_type=core.Content,
|
|
42
|
+
user_shortname=logged_in_user,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if workflows_data is not None and workflows_data.payload is not None:
|
|
46
|
+
workflows_payload: dict[str,Any]
|
|
47
|
+
if isinstance(workflows_data.payload.body, dict):
|
|
48
|
+
workflows_payload = workflows_data.payload.body
|
|
49
|
+
else:
|
|
50
|
+
payload = await db.load_resource_payload(
|
|
51
|
+
space_name=space_name,
|
|
52
|
+
subpath="workflows",
|
|
53
|
+
filename=str(workflows_data.payload.body),
|
|
54
|
+
class_type=core.Content,
|
|
55
|
+
)
|
|
56
|
+
workflows_payload = payload if payload else {}
|
|
57
|
+
|
|
58
|
+
initial_state = None
|
|
59
|
+
|
|
60
|
+
for state in workflows_payload["initial_state"]:
|
|
61
|
+
if initial_state is None and "default" in state["roles"]:
|
|
62
|
+
initial_state = state["name"]
|
|
63
|
+
elif [role in user_roles for role in state["roles"]].count(True):
|
|
64
|
+
initial_state = state["name"]
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
if initial_state is None:
|
|
68
|
+
raise api.Exception(
|
|
69
|
+
status.HTTP_400_BAD_REQUEST,
|
|
70
|
+
api.Error(
|
|
71
|
+
type="request",
|
|
72
|
+
code=InternalErrorCode.NOT_ALLOWED,
|
|
73
|
+
message="The user does not have the required roles to create this ticket",
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
record.attributes = {
|
|
78
|
+
**workflow_attr,
|
|
79
|
+
"state": initial_state,
|
|
80
|
+
"is_open": True,
|
|
81
|
+
}
|
|
82
|
+
return record
|
|
83
|
+
|
|
84
|
+
raise api.Exception(
|
|
85
|
+
status.HTTP_400_BAD_REQUEST,
|
|
86
|
+
api.Error(
|
|
87
|
+
type="request",
|
|
88
|
+
code=InternalErrorCode.SHORTNAME_ALREADY_EXIST,
|
|
89
|
+
message="This shortname already exists",
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def set_init_state_from_record(
|
|
95
|
+
ticket: core.Record, logged_in_user, space_name
|
|
96
|
+
):
|
|
97
|
+
workflow_attr = ticket.attributes
|
|
98
|
+
workflow_shortname = workflow_attr["workflow_shortname"]
|
|
99
|
+
|
|
100
|
+
workflows_data = await db.load(
|
|
101
|
+
space_name=space_name,
|
|
102
|
+
subpath="workflows",
|
|
103
|
+
shortname=workflow_shortname,
|
|
104
|
+
class_type=core.Content,
|
|
105
|
+
user_shortname=logged_in_user,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if workflows_data is not None and workflows_data.payload is not None:
|
|
109
|
+
# file: fetch payload from file
|
|
110
|
+
# sql: payload is already within the entry
|
|
111
|
+
workflows_payload : dict[str, Any] = {}
|
|
112
|
+
mypayload = workflows_data.payload.body
|
|
113
|
+
if settings.active_data_db == "file" and isinstance(mypayload, str):
|
|
114
|
+
payload = await db.load_resource_payload(
|
|
115
|
+
space_name=space_name,
|
|
116
|
+
subpath="workflows",
|
|
117
|
+
filename=mypayload,
|
|
118
|
+
class_type=core.Content,
|
|
119
|
+
)
|
|
120
|
+
workflows_payload = payload if payload else {}
|
|
121
|
+
elif isinstance(mypayload, dict):
|
|
122
|
+
workflows_payload = mypayload
|
|
123
|
+
else:
|
|
124
|
+
raise api.Exception(
|
|
125
|
+
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
126
|
+
api.Error(
|
|
127
|
+
type="request",
|
|
128
|
+
code=InternalErrorCode.INVALID_DATA,
|
|
129
|
+
message=f"Invalid payload data {mypayload}",
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
ticket.attributes = {
|
|
136
|
+
**workflow_attr,
|
|
137
|
+
"state": workflows_payload["initial_state"],
|
|
138
|
+
}
|
|
139
|
+
return ticket
|
|
140
|
+
|
|
141
|
+
raise api.Exception(
|
|
142
|
+
status.HTTP_400_BAD_REQUEST,
|
|
143
|
+
api.Error(
|
|
144
|
+
type="request",
|
|
145
|
+
code=InternalErrorCode.SHORTNAME_ALREADY_EXIST,
|
|
146
|
+
message="This shortname already exists",
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def transite(states, current_state: str, action: str, user_roles):
|
|
152
|
+
for state in states:
|
|
153
|
+
if state["state"] == current_state:
|
|
154
|
+
for next_state in state["next"]:
|
|
155
|
+
if next_state["action"] == action:
|
|
156
|
+
for role in next_state["roles"]:
|
|
157
|
+
if role in user_roles:
|
|
158
|
+
return {"status": True, "message": next_state["state"]}
|
|
159
|
+
return {
|
|
160
|
+
"status": False,
|
|
161
|
+
"message": f"You don't have the permission to progress this ticket with action {action}",
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
"status": False,
|
|
166
|
+
"message": f"You can't progress from {current_state} using {action}",
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def post_transite(states, next_state: str, resolution: str):
|
|
171
|
+
for state in states:
|
|
172
|
+
if state["state"] == next_state:
|
|
173
|
+
if "resolutions" in state:
|
|
174
|
+
available_resolutions = [one for one in state["resolutions"]]
|
|
175
|
+
if len(available_resolutions) == 0:
|
|
176
|
+
return {
|
|
177
|
+
"status": False,
|
|
178
|
+
"message": f"The state {next_state} does not have any resolutions defined",
|
|
179
|
+
}
|
|
180
|
+
else:
|
|
181
|
+
if isinstance(available_resolutions[0], str):
|
|
182
|
+
if resolution in available_resolutions:
|
|
183
|
+
return {"status": True, "message": resolution}
|
|
184
|
+
else:
|
|
185
|
+
if resolution in [item['key'] for item in available_resolutions]:
|
|
186
|
+
return {"status": True, "message": resolution}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
"status": False,
|
|
190
|
+
"message": f"The resolution {resolution} provided is not acceptable in state {next_state}",
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
"status": False,
|
|
194
|
+
"message": f"Cannot fetch the next state {next_state} with resolution {resolution}",
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def check_open_state(states, current_state: str):
|
|
199
|
+
for state in states:
|
|
200
|
+
if state["state"] == current_state:
|
|
201
|
+
return "next" in state
|
|
202
|
+
|
|
203
|
+
return True
|
utils/web_notifier.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from models.core import NotificationData
|
|
2
|
+
from utils.async_request import AsyncRequest
|
|
3
|
+
from utils.notification import Notifier
|
|
4
|
+
from utils.helpers import lang_code
|
|
5
|
+
from utils.settings import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class WebNotifier(Notifier):
|
|
11
|
+
|
|
12
|
+
async def send(
|
|
13
|
+
self,
|
|
14
|
+
data: NotificationData
|
|
15
|
+
) -> bool:
|
|
16
|
+
if not settings.websocket_url:
|
|
17
|
+
return False
|
|
18
|
+
user_lang = lang_code(data.receiver.get("language", "ar"))
|
|
19
|
+
async with AsyncRequest() as client:
|
|
20
|
+
await client.post(
|
|
21
|
+
f"{settings.websocket_url}/send-message/{data.receiver.get('shortname')}",
|
|
22
|
+
json={
|
|
23
|
+
"title": data.title.__getattribute__(user_lang),
|
|
24
|
+
"description": data.body.__getattribute__(user_lang),
|
|
25
|
+
}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return True
|
|
29
|
+
|