customerum 1.0.0__tar.gz
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.
- customerum-1.0.0/LICENSE +21 -0
- customerum-1.0.0/PKG-INFO +65 -0
- customerum-1.0.0/README.md +35 -0
- customerum-1.0.0/pyproject.toml +3 -0
- customerum-1.0.0/setup.cfg +43 -0
- customerum-1.0.0/src/customerum/__init__.py +3 -0
- customerum-1.0.0/src/customerum/customer.py +295 -0
- customerum-1.0.0/src/customerum/data.py +123 -0
- customerum-1.0.0/src/customerum/environ.py +34 -0
- customerum-1.0.0/src/customerum/param.py +40 -0
- customerum-1.0.0/src/customerum/schema.py +82 -0
- customerum-1.0.0/src/customerum/syslogin.py +82 -0
- customerum-1.0.0/src/customerum/task.py +289 -0
- customerum-1.0.0/src/customerum/template.py +235 -0
- customerum-1.0.0/src/customerum/title.py +75 -0
- customerum-1.0.0/src/customerum.egg-info/PKG-INFO +65 -0
- customerum-1.0.0/src/customerum.egg-info/SOURCES.txt +19 -0
- customerum-1.0.0/src/customerum.egg-info/dependency_links.txt +1 -0
- customerum-1.0.0/src/customerum.egg-info/requires.txt +14 -0
- customerum-1.0.0/src/customerum.egg-info/top_level.txt +1 -0
customerum-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Asinerum Conlang Project
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: customerum
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Literp Customer Service Web Tool
|
|
5
|
+
Home-page: https://github.com/asinerum/customerum
|
|
6
|
+
Author: Asinerum Conlang Project
|
|
7
|
+
Author-email: asinerum.com@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.7
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: python-dotenv>=1.2.1
|
|
16
|
+
Requires-Dist: python-multipart>=0.0.29
|
|
17
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
18
|
+
Requires-Dist: starlette>=0.50.0
|
|
19
|
+
Requires-Dist: uvicorn>=0.38.0
|
|
20
|
+
Requires-Dist: jinja2>=3.1.6
|
|
21
|
+
Requires-Dist: tagrepo>=1.0.7
|
|
22
|
+
Requires-Dist: formatize>=1.0.19
|
|
23
|
+
Requires-Dist: pavro>=1.0.25
|
|
24
|
+
Requires-Dist: tier2>=1.0.10
|
|
25
|
+
Requires-Dist: skadd>=1.0.8
|
|
26
|
+
Requires-Dist: skgen>=1.0.4
|
|
27
|
+
Requires-Dist: vapp>=1.0.5
|
|
28
|
+
Requires-Dist: uvia>=1.0.2
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Literp Customer Service Web Tool [June 27, 2026]
|
|
32
|
+
|
|
33
|
+
## Purpose
|
|
34
|
+
- This tiny tool (working as a web daemon) helps people to manage their small community of users.
|
|
35
|
+
- The users are created manually by an administrator, and about to receive emails with their secret setup keys which can be added to modern authenticator apps.
|
|
36
|
+
- After being created, a new user can log in the community portal with TOTP authenticator code.
|
|
37
|
+
- The tool includes: user listing, creating, deleting routines. The user-creating routine allows administrators add new user name and email, then automatically send notification email.
|
|
38
|
+
- The automatic email sending uses Google mail API under administrators Gmail account.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
```bash
|
|
42
|
+
pip install customerum
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Sample Usage
|
|
46
|
+
```bash
|
|
47
|
+
## nano customerum.sh
|
|
48
|
+
export JWT_SECRET="very-strong-password"
|
|
49
|
+
export TOKEN_EXPIRE_MINUTES=30
|
|
50
|
+
export ADMIN_AUTHENTICATOR_EMAIL_LIST="EMAIL_LIST_SEPARATED_BY_COMMA"
|
|
51
|
+
export SENDER_GMAIL_ADDRESS="YOUR_WORKING_GMAIL_ADDRESS"
|
|
52
|
+
export SENDER_GMAIL_APP_PWD="YOUR_WORKING_GMAIL_APP_PWD"
|
|
53
|
+
export FILE_AVRO_AUTHORS="./users/roleauthors.avro"
|
|
54
|
+
export FILE_AVRO_DATA="./users/setupkeys.avro"
|
|
55
|
+
uvia -a vapp -m customerum [-p <PORT> [-H <HOST>]]
|
|
56
|
+
## The <PORT> is 11718 by default, but can be edited
|
|
57
|
+
## The <HOST> should be 127.0.0.1 for security reason
|
|
58
|
+
## chmod +x customerum.sh
|
|
59
|
+
## ./customerum.sh
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Detailed tips, tricks, and examples, can be found at project's repository
|
|
63
|
+
https://github.com/asinerum/customerum
|
|
64
|
+
|
|
65
|
+
(C) 2026 Asinerum Conlang Project
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Literp Customer Service Web Tool [June 27, 2026]
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
- This tiny tool (working as a web daemon) helps people to manage their small community of users.
|
|
5
|
+
- The users are created manually by an administrator, and about to receive emails with their secret setup keys which can be added to modern authenticator apps.
|
|
6
|
+
- After being created, a new user can log in the community portal with TOTP authenticator code.
|
|
7
|
+
- The tool includes: user listing, creating, deleting routines. The user-creating routine allows administrators add new user name and email, then automatically send notification email.
|
|
8
|
+
- The automatic email sending uses Google mail API under administrators Gmail account.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
```bash
|
|
12
|
+
pip install customerum
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Sample Usage
|
|
16
|
+
```bash
|
|
17
|
+
## nano customerum.sh
|
|
18
|
+
export JWT_SECRET="very-strong-password"
|
|
19
|
+
export TOKEN_EXPIRE_MINUTES=30
|
|
20
|
+
export ADMIN_AUTHENTICATOR_EMAIL_LIST="EMAIL_LIST_SEPARATED_BY_COMMA"
|
|
21
|
+
export SENDER_GMAIL_ADDRESS="YOUR_WORKING_GMAIL_ADDRESS"
|
|
22
|
+
export SENDER_GMAIL_APP_PWD="YOUR_WORKING_GMAIL_APP_PWD"
|
|
23
|
+
export FILE_AVRO_AUTHORS="./users/roleauthors.avro"
|
|
24
|
+
export FILE_AVRO_DATA="./users/setupkeys.avro"
|
|
25
|
+
uvia -a vapp -m customerum [-p <PORT> [-H <HOST>]]
|
|
26
|
+
## The <PORT> is 11718 by default, but can be edited
|
|
27
|
+
## The <HOST> should be 127.0.0.1 for security reason
|
|
28
|
+
## chmod +x customerum.sh
|
|
29
|
+
## ./customerum.sh
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Detailed tips, tricks, and examples, can be found at project's repository
|
|
33
|
+
https://github.com/asinerum/customerum
|
|
34
|
+
|
|
35
|
+
(C) 2026 Asinerum Conlang Project
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[metadata]
|
|
2
|
+
name = customerum
|
|
3
|
+
version = 1.0.0
|
|
4
|
+
author = Asinerum Conlang Project
|
|
5
|
+
author_email = asinerum.com@gmail.com
|
|
6
|
+
description = Literp Customer Service Web Tool
|
|
7
|
+
long_description = file: README.md
|
|
8
|
+
long_description_content_type = text/markdown
|
|
9
|
+
url = https://github.com/asinerum/customerum
|
|
10
|
+
license = MIT
|
|
11
|
+
classifiers =
|
|
12
|
+
Programming Language :: Python :: 3
|
|
13
|
+
License :: OSI Approved :: MIT License
|
|
14
|
+
Operating System :: OS Independent
|
|
15
|
+
|
|
16
|
+
[options]
|
|
17
|
+
package_dir =
|
|
18
|
+
= src
|
|
19
|
+
packages = find:
|
|
20
|
+
python_requires = >=3.7
|
|
21
|
+
install_requires =
|
|
22
|
+
python-dotenv >= 1.2.1
|
|
23
|
+
python-multipart >= 0.0.29
|
|
24
|
+
typing-extensions >= 4.15.0
|
|
25
|
+
starlette >= 0.50.0
|
|
26
|
+
uvicorn >= 0.38.0
|
|
27
|
+
jinja2 >= 3.1.6
|
|
28
|
+
tagrepo >= 1.0.7
|
|
29
|
+
formatize >= 1.0.19
|
|
30
|
+
pavro >= 1.0.25
|
|
31
|
+
tier2 >= 1.0.10
|
|
32
|
+
skadd >= 1.0.8
|
|
33
|
+
skgen >= 1.0.4
|
|
34
|
+
vapp >= 1.0.5
|
|
35
|
+
uvia >= 1.0.2
|
|
36
|
+
|
|
37
|
+
[options.packages.find]
|
|
38
|
+
where = src
|
|
39
|
+
|
|
40
|
+
[egg_info]
|
|
41
|
+
tag_build =
|
|
42
|
+
tag_date = 0
|
|
43
|
+
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Literp Customers and Contacts Management Tool
|
|
3
|
+
Dependencies:
|
|
4
|
+
pip install [tagrepo] uvia vapp customerum
|
|
5
|
+
Start daemon:
|
|
6
|
+
export LLM_TRANSFORMER_MODEL="./all-MiniLM-L6-v2"
|
|
7
|
+
export SETUPKEY_ENCRYPT_SECRET="very-strong-password"
|
|
8
|
+
export JWT_SECRET="very-strong-password"
|
|
9
|
+
export TOKEN_EXPIRE_MINUTES=30
|
|
10
|
+
export ADMIN_AUTHENTICATOR_EMAIL_LIST=EMAIL_LIST_SEPARATED_BY_COMMA
|
|
11
|
+
export SUPERVISOR_AUTHENTICATOR_EMAIL_LIST=EMAIL_LIST_SEPARATED_BY_COMMA
|
|
12
|
+
export STAFF_AUTHENTICATOR_EMAIL_LIST=EMAIL_LIST_SEPARATED_BY_COMMA
|
|
13
|
+
export FILE_AVRO_CRM_SUPERVISORS="../users/rolecrmsupervisors.avro"
|
|
14
|
+
export FILE_AVRO_CRM_STAFF="../users/rolecrmstaff.avro"
|
|
15
|
+
export FILE_AVRO_DATA="../users/setupkeys.avro"
|
|
16
|
+
export FILE_DUCKDB_VECTOR="./temp.db"
|
|
17
|
+
uvia -a vapp -m customerum [-p <PORT>] [-H <HOST>]
|
|
18
|
+
VAPP routes:
|
|
19
|
+
Route: /callback -> callbackpage()
|
|
20
|
+
Route: /login -> loginpage()
|
|
21
|
+
Route: /call -> callpage()
|
|
22
|
+
GET -> request.query_params = {function, access_token, jsondata, module}
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
APP_PORT = 11718 ## for <uvia> and <vapp>
|
|
26
|
+
|
|
27
|
+
## import os
|
|
28
|
+
## import shutil
|
|
29
|
+
import uvicorn ## for <uvia>
|
|
30
|
+
from starlette.responses import HTMLResponse
|
|
31
|
+
from starlette.responses import RedirectResponse
|
|
32
|
+
from starlette.requests import Request
|
|
33
|
+
|
|
34
|
+
from skadd import login_page, clear_session, modify_session, generate_access_token
|
|
35
|
+
|
|
36
|
+
from tagrepo.format import dstrip, boolean
|
|
37
|
+
from tagrepo.filter import inputfilter
|
|
38
|
+
from tagrepo.ware import middlewares ## for <vapp>
|
|
39
|
+
|
|
40
|
+
from .param import get_request_opportunity_options, get_request_interaction_options, get_request_contact_options
|
|
41
|
+
from .syslogin import *
|
|
42
|
+
from .task import *
|
|
43
|
+
|
|
44
|
+
def RedirectResponseGet(url: str):
|
|
45
|
+
return RedirectResponse(url=url, status_code=303)
|
|
46
|
+
|
|
47
|
+
def RedirectResponsePost(url: str):
|
|
48
|
+
return RedirectResponse(url=url, status_code=307)
|
|
49
|
+
|
|
50
|
+
## core <uvia> functions
|
|
51
|
+
|
|
52
|
+
def homepage():
|
|
53
|
+
async def submit(request: Request):
|
|
54
|
+
return HTMLResponse(Template(APP_TMP_DIRECTORY_MENU).render(
|
|
55
|
+
APP_CHARSET=APP_CHARSET,
|
|
56
|
+
APP_VIEWPORT=APP_VIEWPORT,
|
|
57
|
+
APP_TITLE=APP_DESC,
|
|
58
|
+
FORM_TITLE=FORM_TITLE,
|
|
59
|
+
directory_items=[
|
|
60
|
+
{"href": "/login", "name": LABEL_MENU_LOGIN},
|
|
61
|
+
{"href": call_function(FUNCTION_CUSTOMER_INSERT), "name": LABEL_MENU_CUSTOMER_INSERT},
|
|
62
|
+
{"href": call_function(FUNCTION_CUSTOMERS_SELECT), "name": LABEL_MENU_CUSTOMER_SELECT},
|
|
63
|
+
{"href": call_function(FUNCTION_OPPORTUNITIES_GLOBAL_SELECT), "name": LABEL_MENU_OPPORTUNITY_GLOBAL_SELECT},
|
|
64
|
+
{"href": call_function(FUNCTION_INTERACTIONS_GLOBAL_SELECT), "name": LABEL_MENU_INTERACTION_GLOBAL_SELECT},
|
|
65
|
+
{"href": call_function(FUNCTION_CUSTOMERS_SEARCH), "name": LABEL_MENU_CUSTOMER_SEARCH},
|
|
66
|
+
{"href": call_function(FUNCTION_CONTACTS_SEARCH), "name": LABEL_MENU_CONTACT_SEARCH},
|
|
67
|
+
{"href": call_function(FUNCTION_OPPORTUNITIES_SEARCH), "name": LABEL_MENU_OPPORTUNITY_SELECT},
|
|
68
|
+
{"href": call_function(FUNCTION_INTERACTIONS_SEARCH), "name": LABEL_MENU_INTERACTION_SELECT},
|
|
69
|
+
{"href": call_function("logout"), "name": LABEL_MENU_LOGOUT},
|
|
70
|
+
]
|
|
71
|
+
))
|
|
72
|
+
return submit
|
|
73
|
+
|
|
74
|
+
def loginpage():
|
|
75
|
+
async def submit(request: Request):
|
|
76
|
+
if request.method == 'GET':
|
|
77
|
+
return HTMLResponse(login_page())
|
|
78
|
+
else:
|
|
79
|
+
form = await request.form()
|
|
80
|
+
email = form.get('email').strip()
|
|
81
|
+
code = form.get('code').strip()
|
|
82
|
+
if is_authorized(email, code):
|
|
83
|
+
modify_session(request, at=generate_access_token(email), su=email)
|
|
84
|
+
return RedirectResponse(url='/')
|
|
85
|
+
else:
|
|
86
|
+
clear_session(request)
|
|
87
|
+
return HTMLResponse(YOU_COULD_NOT_LOG_IN)
|
|
88
|
+
return submit
|
|
89
|
+
|
|
90
|
+
def callpage():
|
|
91
|
+
async def submit(request: Request):
|
|
92
|
+
func = request.query_params.get(QUERY_FUNCTION)
|
|
93
|
+
if func == FUNCTION_LOGOUT:
|
|
94
|
+
clear_session(request)
|
|
95
|
+
return HTMLResponse(YOU_ARE_LOGGED_OUT)
|
|
96
|
+
failed = exec_auth_function(request, func)
|
|
97
|
+
if failed: return HTMLResponse(failed)
|
|
98
|
+
return await _callpage(request, func)
|
|
99
|
+
return submit
|
|
100
|
+
|
|
101
|
+
async def _callpage(request, func: str):
|
|
102
|
+
if func == FUNCTION_CUSTOMER_INSERT:
|
|
103
|
+
if request.method == 'GET':
|
|
104
|
+
return HTMLResponse(insert_customer_page(request))
|
|
105
|
+
else:
|
|
106
|
+
form = await request.form()
|
|
107
|
+
form = inputfilter(dstrip(form), Customers, excludes=[DATED])
|
|
108
|
+
insert = insert_customer_page(request, **form)
|
|
109
|
+
if insert == CALLBACK: return RedirectResponseGet(url=call_function("select-customers"))
|
|
110
|
+
return HTMLResponse(insert)
|
|
111
|
+
elif func == FUNCTION_CUSTOMERS_SELECT:
|
|
112
|
+
if request.method == 'GET':
|
|
113
|
+
customer_id = request.query_params.get("id")
|
|
114
|
+
if not customer_id: return HTMLResponse(select_customers_page(request))
|
|
115
|
+
else:
|
|
116
|
+
form = await request.form()
|
|
117
|
+
customer_id = form.get("id") ## html form radio options
|
|
118
|
+
return HTMLResponse(select_contacts_page(request, customer_id))
|
|
119
|
+
elif func == FUNCTION_CONTACT_INSERT: ## <post> only
|
|
120
|
+
customer_id = request.query_params.get("customer_id")
|
|
121
|
+
form = await request.form()
|
|
122
|
+
if not form.get(NAME):
|
|
123
|
+
return HTMLResponse(insert_contact_page(request, customer_id, name=""))
|
|
124
|
+
else:
|
|
125
|
+
form = inputfilter(dstrip(form), Contacts, excludes=[DATED])
|
|
126
|
+
insert = insert_contact_page(request, customer_id, **form)
|
|
127
|
+
if insert == CALLBACK: return RedirectResponseGet(url=call_function(f"select-customers&id={customer_id}"))
|
|
128
|
+
return HTMLResponse(insert)
|
|
129
|
+
elif func == FUNCTION_CUSTOMERS_REVIEW: ## <get> only
|
|
130
|
+
## hidden task: simple customers listing to review
|
|
131
|
+
return HTMLResponse(review_customers_page(request))
|
|
132
|
+
elif func == FUNCTION_CUSTOMERS_UPDATE: ## <get> only
|
|
133
|
+
return HTMLResponse(update_customers_page(request))
|
|
134
|
+
elif func == FUNCTION_CUSTOMER_SELECT: ## <get> only
|
|
135
|
+
customer_id = request.query_params.get("id")
|
|
136
|
+
return HTMLResponse(select_customer_page(request, customer_id))
|
|
137
|
+
elif func == FUNCTION_CUSTOMER_UPDATE:
|
|
138
|
+
customer_id = request.query_params.get("id")
|
|
139
|
+
if request.method == 'GET':
|
|
140
|
+
return HTMLResponse(update_customer_page(request, customer_id))
|
|
141
|
+
else:
|
|
142
|
+
form = await request.form()
|
|
143
|
+
form = inputfilter(dstrip(form), Customers, includes=UPDATE_INPUTS[Customers])
|
|
144
|
+
update = update_customer_page(request, customer_id, **form)
|
|
145
|
+
if update == CALLBACK: return RedirectResponseGet(url=call_function(f"select-customer&id={customer_id}"))
|
|
146
|
+
return HTMLResponse(update)
|
|
147
|
+
elif func == FUNCTION_CUSTOMERS_SEARCH:
|
|
148
|
+
if request.method == 'GET':
|
|
149
|
+
return HTMLResponse(search_customers_page(request))
|
|
150
|
+
else:
|
|
151
|
+
form = await request.form()
|
|
152
|
+
search = form.get("search")
|
|
153
|
+
seek = form.get("seek")
|
|
154
|
+
reset = form.get("reset")
|
|
155
|
+
search = search_customers_page(request, search=search, reset=boolean(reset), seek=seek)
|
|
156
|
+
return HTMLResponse(search)
|
|
157
|
+
elif func == FUNCTION_CONTACTS_SELECT:
|
|
158
|
+
customer_id = request.query_params.get("customer_id")
|
|
159
|
+
return HTMLResponse(select_contacts_page(request, customer_id))
|
|
160
|
+
elif func == FUNCTION_CONTACTS_SEARCH:
|
|
161
|
+
if request.method == 'GET':
|
|
162
|
+
return HTMLResponse(search_contacts_page(request))
|
|
163
|
+
else:
|
|
164
|
+
form = await request.form()
|
|
165
|
+
search = form.get("search")
|
|
166
|
+
seek = form.get("seek")
|
|
167
|
+
reset = form.get("reset")
|
|
168
|
+
search = search_contacts_page(request, search=search, reset=boolean(reset), seek=seek)
|
|
169
|
+
return HTMLResponse(search)
|
|
170
|
+
elif func == FUNCTION_CONTACT_SELECT:
|
|
171
|
+
contact_id = request.query_params.get("id")
|
|
172
|
+
return HTMLResponse(select_contact_page(request, contact_id))
|
|
173
|
+
elif func == FUNCTION_CONTACT_UPDATE:
|
|
174
|
+
contact_id = request.query_params.get("id")
|
|
175
|
+
if request.method == 'GET':
|
|
176
|
+
return HTMLResponse(update_contact_page(request, contact_id))
|
|
177
|
+
else:
|
|
178
|
+
form = await request.form()
|
|
179
|
+
form = inputfilter(dstrip(form), Contacts, includes=UPDATE_INPUTS[Contacts])
|
|
180
|
+
update = update_contact_page(request, contact_id, **form)
|
|
181
|
+
if update == CALLBACK: return RedirectResponseGet(url=call_function(f"select-contact&id={contact_id}"))
|
|
182
|
+
return HTMLResponse(update)
|
|
183
|
+
elif func == FUNCTION_OPPORTUNITY_INSERT:
|
|
184
|
+
contact_id = request.query_params.get("contact_id")
|
|
185
|
+
if request.method == 'GET':
|
|
186
|
+
return HTMLResponse(insert_opportunity_page(request, contact_id))
|
|
187
|
+
else:
|
|
188
|
+
form = await request.form()
|
|
189
|
+
form = inputfilter(dstrip(form), Opportunities, excludes=[DATED])
|
|
190
|
+
insert = insert_opportunity_page(request, contact_id, **form)
|
|
191
|
+
if insert == CALLBACK: return RedirectResponseGet(url=call_function(f"select-opportunities&contact_id={contact_id}"))
|
|
192
|
+
return HTMLResponse(insert)
|
|
193
|
+
elif func == FUNCTION_OPPORTUNITIES_SELECT:
|
|
194
|
+
contact_id = request.query_params.get("contact_id")
|
|
195
|
+
return HTMLResponse(select_opportunities_page(request, contact_id=contact_id, **get_request_opportunity_options(request)))
|
|
196
|
+
elif func == FUNCTION_OPPORTUNITY_SELECT:
|
|
197
|
+
opportunity_id = request.query_params.get("id")
|
|
198
|
+
return HTMLResponse(select_opportunity_page(request, opportunity_id))
|
|
199
|
+
elif func == FUNCTION_OPPORTUNITY_UPDATE:
|
|
200
|
+
opportunity_id = request.query_params.get("id")
|
|
201
|
+
if request.method == 'GET':
|
|
202
|
+
return HTMLResponse(update_opportunity_page(request, opportunity_id))
|
|
203
|
+
else:
|
|
204
|
+
form = await request.form()
|
|
205
|
+
form = inputfilter(dstrip(form), Opportunities, includes=UPDATE_INPUTS[Opportunities])
|
|
206
|
+
update = update_opportunity_page(request, opportunity_id, **form)
|
|
207
|
+
if update == CALLBACK: return RedirectResponseGet(url=call_function(f"select-opportunity&id={opportunity_id}"))
|
|
208
|
+
return HTMLResponse(update)
|
|
209
|
+
elif func == FUNCTION_INTERACTION_INSERT:
|
|
210
|
+
opportunity_id = request.query_params.get("opportunity_id")
|
|
211
|
+
if request.method == 'GET':
|
|
212
|
+
return HTMLResponse(insert_interaction_page(request, opportunity_id))
|
|
213
|
+
else:
|
|
214
|
+
form = await request.form()
|
|
215
|
+
form = inputfilter(dstrip(form), Interactions, excludes=[DATED])
|
|
216
|
+
insert = insert_interaction_page(request, opportunity_id, **form)
|
|
217
|
+
if insert == CALLBACK: return RedirectResponseGet(url=call_function(f"select-interactions&opportunity_id={opportunity_id}"))
|
|
218
|
+
return HTMLResponse(insert)
|
|
219
|
+
elif func == FUNCTION_INTERACTIONS_SELECT:
|
|
220
|
+
opportunity_id = request.query_params.get("opportunity_id")
|
|
221
|
+
return HTMLResponse(select_interactions_page(request, opportunity_id=opportunity_id, **get_request_interaction_options(request)))
|
|
222
|
+
elif func == FUNCTION_INTERACTION_SELECT:
|
|
223
|
+
interaction_id = request.query_params.get("id")
|
|
224
|
+
return HTMLResponse(select_interaction_page(request, interaction_id))
|
|
225
|
+
elif func == FUNCTION_INTERACTION_UPDATE:
|
|
226
|
+
interaction_id = request.query_params.get("id")
|
|
227
|
+
if request.method == 'GET':
|
|
228
|
+
return HTMLResponse(update_interaction_page(request, interaction_id))
|
|
229
|
+
else:
|
|
230
|
+
form = await request.form()
|
|
231
|
+
form = inputfilter(dstrip(form), Interactions, includes=UPDATE_INPUTS[Interactions])
|
|
232
|
+
update = update_interaction_page(request, interaction_id, **form)
|
|
233
|
+
if update == CALLBACK: return RedirectResponseGet(url=call_function(f"select-interaction&id={interaction_id}"))
|
|
234
|
+
return HTMLResponse(update)
|
|
235
|
+
elif func == FUNCTION_CONTACTS_UPDATE: ## <get> only
|
|
236
|
+
customer_id = request.query_params.get("customer_id")
|
|
237
|
+
return HTMLResponse(update_contacts_page(request, customer_id))
|
|
238
|
+
elif func == FUNCTION_CONTACTS_REVIEW: ## <get> only
|
|
239
|
+
## hidden task: simple contacts listing to review
|
|
240
|
+
customer_id = request.query_params.get("customer_id")
|
|
241
|
+
return HTMLResponse(review_contacts_page(request, customer_id))
|
|
242
|
+
elif func == FUNCTION_OPPORTUNITIES_UPDATE: ## <get> only
|
|
243
|
+
## hidden task: simple opportunities listing to update
|
|
244
|
+
contact_id = request.query_params.get("contact_id")
|
|
245
|
+
return HTMLResponse(update_opportunities_page(request, contact_id))
|
|
246
|
+
elif func == FUNCTION_OPPORTUNITY_CLOSE: ## <get> or <post>
|
|
247
|
+
opportunity_id = request.query_params.get("id")
|
|
248
|
+
won_status = request.query_params.get("won")
|
|
249
|
+
return HTMLResponse(update_close_opportunity_page(request, opportunity_id, won_status))
|
|
250
|
+
elif func == FUNCTION_OPPORTUNITIES_GLOBAL_SELECT:
|
|
251
|
+
return HTMLResponse(select_opportunities_page(request, contact_id="", **get_request_opportunity_options(request)))
|
|
252
|
+
elif func == FUNCTION_INTERACTIONS_GLOBAL_SELECT:
|
|
253
|
+
return HTMLResponse(select_interactions_page(request, opportunity_id="", **get_request_interaction_options(request)))
|
|
254
|
+
elif func == FUNCTION_OPPORTUNITIES_SEARCH:
|
|
255
|
+
author = request.query_params.get("author")
|
|
256
|
+
if request.method == 'GET':
|
|
257
|
+
return HTMLResponse(search_opportunities_page(request))
|
|
258
|
+
else:
|
|
259
|
+
form = await request.form()
|
|
260
|
+
search = form.get("search")
|
|
261
|
+
seek = form.get("seek")
|
|
262
|
+
reset = form.get("reset")
|
|
263
|
+
search = search_opportunities_page(request, search=search, reset=boolean(reset), seek=seek, author=author)
|
|
264
|
+
return HTMLResponse(search)
|
|
265
|
+
elif func == FUNCTION_INTERACTIONS_SEARCH:
|
|
266
|
+
author = request.query_params.get("author")
|
|
267
|
+
if request.method == 'GET':
|
|
268
|
+
return HTMLResponse(search_interactions_page(request))
|
|
269
|
+
else:
|
|
270
|
+
form = await request.form()
|
|
271
|
+
search = form.get("search")
|
|
272
|
+
seek = form.get("seek")
|
|
273
|
+
reset = form.get("reset")
|
|
274
|
+
search = search_interactions_page(request, search=search, reset=boolean(reset), seek=seek, author=author)
|
|
275
|
+
return HTMLResponse(search)
|
|
276
|
+
elif func == FUNCTION_OPPORTUNITIES_AUTHOR_SEARCH:
|
|
277
|
+
if request.method == 'GET':
|
|
278
|
+
return HTMLResponse(search_opportunities_author_page(request))
|
|
279
|
+
else:
|
|
280
|
+
form = await request.form()
|
|
281
|
+
search = form.get("search")
|
|
282
|
+
seek = form.get("seek")
|
|
283
|
+
reset = form.get("reset")
|
|
284
|
+
search = search_opportunities_author_page(request, search=search, reset=boolean(reset), seek=seek)
|
|
285
|
+
return HTMLResponse(search)
|
|
286
|
+
elif func == FUNCTION_OPPORTUNITY_UPGRADE:
|
|
287
|
+
opportunity_id = request.query_params.get("id")
|
|
288
|
+
if request.method == 'GET':
|
|
289
|
+
return HTMLResponse(upgrade_opportunity_page(request, opportunity_id))
|
|
290
|
+
else:
|
|
291
|
+
form = await request.form()
|
|
292
|
+
status = form.get(STATUS)
|
|
293
|
+
update = upgrade_opportunity_page(request, opportunity_id, status=status)
|
|
294
|
+
if update == CALLBACK: return RedirectResponseGet(url=call_function(f"select-opportunity&id={opportunity_id}"))
|
|
295
|
+
return HTMLResponse(update)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from pavro.sql import sql_simple_query_model
|
|
2
|
+
from pavro.vss import LLM_TRANSFORMER_MODEL, SentenceTransformer
|
|
3
|
+
|
|
4
|
+
from tagrepo.filter import arrayfilter
|
|
5
|
+
from tagrepo.dataframe import *
|
|
6
|
+
from tagrepo.frame import *
|
|
7
|
+
|
|
8
|
+
from .param import valid_author_id
|
|
9
|
+
from .environ import *
|
|
10
|
+
from .schema import *
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
llm_model = SentenceTransformer(LLM_TRANSFORMER_MODEL)
|
|
14
|
+
except:
|
|
15
|
+
print("LLM model not loaded")
|
|
16
|
+
|
|
17
|
+
## data constants
|
|
18
|
+
|
|
19
|
+
CUSTOMER_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Customers], excludes=[DATED])
|
|
20
|
+
CONTACT_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Contacts], excludes=[CUSTOMER_ID, DATED])
|
|
21
|
+
OPPORTUNITY_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Opportunities], excludes=[CUSTOMER_ID, CONTACT_ID, AUTHOR_ID, DATED])
|
|
22
|
+
INTERACTION_INSERT_FIELDS = arrayfilter(INSERT_INPUTS[Interactions], excludes=[CUSTOMER_ID, CONTACT_ID, OPPORTUNITY_ID, DATED])
|
|
23
|
+
|
|
24
|
+
## derivatives
|
|
25
|
+
|
|
26
|
+
def list_review_interactions(**kwargs) -> str:
|
|
27
|
+
model = "Interactions"
|
|
28
|
+
show = REVIEW_LISTING_FIELDS[Interactions]
|
|
29
|
+
df = sql_simple_query_model(model, **kwargs)
|
|
30
|
+
return df_list_review_rows(df, show, header=LISTING_HEADERS[Interactions], callfunction="select-interaction")
|
|
31
|
+
|
|
32
|
+
def list_insert_interactions(**kwargs) -> str:
|
|
33
|
+
model = "Opportunities"
|
|
34
|
+
show = INSERT_LISTING_FIELDS[Opportunities]
|
|
35
|
+
df = sql_simple_query_model(model, **kwargs)
|
|
36
|
+
return df_list_insert_rows(df, show, header=LISTING_HEADERS[Opportunities], open_func=[add_button, view_button], callfunction=["insert-interaction", "select-opportunity"], idfield=[ID, ID], linkfield=INSERT_LINK_FIELDS[Opportunities])
|
|
37
|
+
|
|
38
|
+
list_opportunities_to_insert_interaction = list_insert_interactions
|
|
39
|
+
|
|
40
|
+
def list_insert_opportunities(**kwargs) -> str:
|
|
41
|
+
model = "Contacts"
|
|
42
|
+
show = INSERT_LISTING_FIELDS[Contacts]
|
|
43
|
+
df = sql_simple_query_model(model, **kwargs)
|
|
44
|
+
return df_list_insert_rows(df, show, header=LISTING_HEADERS[Contacts], open_func=[add_button, view_button], callfunction=["insert-opportunity", "select-contact"], idfield=[ID, ID], linkfield=INSERT_LINK_FIELDS[Contacts])
|
|
45
|
+
|
|
46
|
+
list_contacts_to_insert_opportunity = list_insert_opportunities
|
|
47
|
+
|
|
48
|
+
def list_update_opportunities(**kwargs) -> str:
|
|
49
|
+
return list_update_rows(model="Opportunities", show=UPDATE_LISTING_FIELDS[Opportunities], callfunction="update-opportunity", **kwargs)
|
|
50
|
+
|
|
51
|
+
def list_update_contacts(**kwargs) -> str:
|
|
52
|
+
return list_update_rows(model="Contacts", show=UPDATE_LISTING_FIELDS[Contacts], callfunction="update-contact", **kwargs)
|
|
53
|
+
|
|
54
|
+
def list_update_customers(**kwargs) -> str:
|
|
55
|
+
return list_update_rows(model="Customers", show=UPDATE_LISTING_FIELDS[Customers], callfunction="update-customer", **kwargs)
|
|
56
|
+
|
|
57
|
+
def list_review_contacts(**kwargs) -> str:
|
|
58
|
+
return list_review_rows(model="Contacts", show=REVIEW_LISTING_FIELDS[Contacts], callfunction="select-contact", **kwargs)
|
|
59
|
+
|
|
60
|
+
def list_review_customers(**kwargs) -> str:
|
|
61
|
+
return list_review_rows(model="Customers", show=REVIEW_LISTING_FIELDS[Customers], **kwargs)
|
|
62
|
+
|
|
63
|
+
def list_select_customers(**kwargs) -> str:
|
|
64
|
+
return list_select_rows("Customers", SELECT_LISTING_FIELDS[Customers], header=LISTING_HEADERS[Customers], **kwargs)
|
|
65
|
+
|
|
66
|
+
def select_ref_customercontact_id(opportunity_id: str) -> list:
|
|
67
|
+
return select_fields_by_id(id=opportunity_id, model="Opportunities", select=[CUSTOMER_ID, CONTACT_ID])
|
|
68
|
+
|
|
69
|
+
def select_ref_contact_id(opportunity_id: str):
|
|
70
|
+
return select_name_by_id(id=opportunity_id, model="Opportunities", select=CONTACT_ID)
|
|
71
|
+
|
|
72
|
+
def select_ref_customer_id(contact_id: str):
|
|
73
|
+
return select_name_by_id(id=contact_id, model="Contacts", select=CUSTOMER_ID)
|
|
74
|
+
|
|
75
|
+
def select_interaction(interaction_id: str):
|
|
76
|
+
return select_pure_row("Interactions",
|
|
77
|
+
linkfields=SELECT_LINK_FIELDS[Interactions],
|
|
78
|
+
linkrefs={CUSTOMER_ID: call_function("select-customer&id="), CONTACT_ID: call_function("select-contact&id="), OPPORTUNITY_ID: call_function("select-opportunity&id=")},
|
|
79
|
+
where=f"id='{interaction_id}'", limit=1)
|
|
80
|
+
|
|
81
|
+
def select_opportunity(opportunity_id: str):
|
|
82
|
+
return select_pure_row("Opportunities",
|
|
83
|
+
linkfields=SELECT_LINK_FIELDS[Opportunities],
|
|
84
|
+
linkrefs={CUSTOMER_ID: call_function("select-customer&id="), CONTACT_ID: call_function("select-contact&id=")},
|
|
85
|
+
where=f"id='{opportunity_id}'", limit=1)
|
|
86
|
+
|
|
87
|
+
def select_contact(contact_id: str):
|
|
88
|
+
return select_pure_row("Contacts",
|
|
89
|
+
linkfields=SELECT_LINK_FIELDS[Contacts],
|
|
90
|
+
linkrefs={CUSTOMER_ID: call_function("select-customer&id=")},
|
|
91
|
+
where=f"id='{contact_id}'", limit=1)
|
|
92
|
+
|
|
93
|
+
def select_customer(customer_id: str):
|
|
94
|
+
return select_pure_row("Customers", where=f"id='{customer_id}'", limit=1)
|
|
95
|
+
|
|
96
|
+
def get_interaction(interaction_id: str) -> dict:
|
|
97
|
+
return select_dict_row("Interactions", where=f"id='{interaction_id}'", limit=1)
|
|
98
|
+
|
|
99
|
+
def get_opportunity(opportunity_id: str) -> dict:
|
|
100
|
+
return select_dict_row("Opportunities", where=f"id='{opportunity_id}'", limit=1)
|
|
101
|
+
|
|
102
|
+
def get_contact(contact_id: str) -> dict:
|
|
103
|
+
return select_dict_row("Contacts", where=f"id='{contact_id}'", limit=1)
|
|
104
|
+
|
|
105
|
+
def get_customer(customer_id: str) -> dict:
|
|
106
|
+
return select_dict_row("Customers", where=f"id='{customer_id}'", limit=1)
|
|
107
|
+
|
|
108
|
+
def get_search_author_option(request, seek: str="") -> dict:
|
|
109
|
+
if seek == AUTHOR_EXC:
|
|
110
|
+
includes = {"func": record_fit_pro, "field": {AUTHOR_ID: request.session.get(SESSION_USER)}}
|
|
111
|
+
elif seek == AUTHOR_INC:
|
|
112
|
+
includes = {"func": record_fit_pro, "field": {AUTHOR_ID: ""}}
|
|
113
|
+
else:
|
|
114
|
+
includes = {}
|
|
115
|
+
return includes
|
|
116
|
+
|
|
117
|
+
def get_search_opportunity_option(request, seek: str="", author: str="") -> dict:
|
|
118
|
+
## <request> is reserved
|
|
119
|
+
includes = {"func": record_fit_pro}
|
|
120
|
+
includes["field"] = {}
|
|
121
|
+
if seek in OPPORTUNITY_STATUS_LIST: includes["field"][STATUS] = seek
|
|
122
|
+
if valid_author_id(author): includes["field"][AUTHOR_ID] = author
|
|
123
|
+
return includes if includes["field"] else {}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from tagrepo.environ import set_environ
|
|
2
|
+
|
|
3
|
+
from skadd import SESSION_USER, ACCESS_TOKEN
|
|
4
|
+
from skadd import MISSING_INPUT_DATA, FAILED_SAVE_DATA
|
|
5
|
+
from skadd import QUERY_FUNCTION, FUNCTION_LOGOUT, CALLBACK
|
|
6
|
+
from skadd import YOU_ARE_LOGGED_OUT, YOU_HAVE_TO_LOG_IN, YOU_COULD_NOT_LOG_IN
|
|
7
|
+
|
|
8
|
+
## data configs
|
|
9
|
+
|
|
10
|
+
LIST_MAX_ROWS = int(set_environ('LIST_MAX_ROWS', 1000))
|
|
11
|
+
SEARCH_RESULTS = int(set_environ('SEARCH_RESULTS', 10))
|
|
12
|
+
|
|
13
|
+
## data messages
|
|
14
|
+
|
|
15
|
+
set_environ('ARE_YOU_SURE', 'Are you sure?')
|
|
16
|
+
set_environ('CONFIRM_OPPORTUNITY_WON', 'Confirm: this opportunity was won?')
|
|
17
|
+
set_environ('CONFIRM_OPPORTUNITY_LOST', 'Confirm: this opportunity was lost')
|
|
18
|
+
|
|
19
|
+
set_environ('DONE_SAVE_DATA', 'Success: data saved')
|
|
20
|
+
set_environ('NO_DATA_AVAILABLE', 'Warning: no data available')
|
|
21
|
+
set_environ('FAILED_COMMIT_TRANSACTION', 'Warning: transaction commit failed')
|
|
22
|
+
|
|
23
|
+
set_environ('WRONG_CUSTOMER_ID', 'Error: invalid customer id')
|
|
24
|
+
set_environ('WRONG_CONTACT_ID', 'Error: invalid contact id')
|
|
25
|
+
set_environ('WRONG_OPPORTUNITY_ID', 'Error: invalid opportunity id')
|
|
26
|
+
|
|
27
|
+
set_environ('CUSTOMER_NOT_EXIST', 'Warning: customer does not exist')
|
|
28
|
+
set_environ('CUSTOMER_ALREADY_EXISTS', 'Warning: customer with this name already exists')
|
|
29
|
+
set_environ('CONTACT_NOT_EXIST', 'Warning: contact does not exist')
|
|
30
|
+
set_environ('CONTACT_ALREADY_EXISTS', 'Warning: contact with this name already exists')
|
|
31
|
+
set_environ('OPPORTUNITY_NOT_EXIST', 'Warning: opportunity does not exist')
|
|
32
|
+
set_environ('OPPORTUNITY_NOT_EXIST_OR_CLOSED', 'Warning: opportunity does not exist or was closed')
|
|
33
|
+
set_environ('OPPORTUNITY_ALREADY_EXISTS', 'Warning: opportunity with this name already exists')
|
|
34
|
+
set_environ('INTERACTION_ALREADY_EXISTS', 'Warning: interaction with this name already exists')
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from tagrepo.format import boolean, integer, emailing, telegraming
|
|
2
|
+
from tagrepo.auth import _is_supervisor
|
|
3
|
+
|
|
4
|
+
from .environ import *
|
|
5
|
+
from .schema import *
|
|
6
|
+
|
|
7
|
+
valid_author_id = lambda id: emailing(id) or telegraming(id)
|
|
8
|
+
|
|
9
|
+
def adjust_listing_options(where: str, author: str, sort: str, asc: str, limit: str, sort_fields: list=OPPORTUNITY_SORT_FIELDS, sort_default: str=DATED, limit_default: int=LIST_MAX_ROWS, request=None) -> tuple:
|
|
10
|
+
if valid_author_id(author):
|
|
11
|
+
if request:
|
|
12
|
+
user = request.session.get(SESSION_USER)
|
|
13
|
+
if author != user and not _is_supervisor(user):
|
|
14
|
+
where = "FALSE" ## disable unwanted accessing
|
|
15
|
+
where = f"{where} AND {AUTHOR_ID} IN ('{author}')"
|
|
16
|
+
sort = sort if sort in sort_fields else sort_default
|
|
17
|
+
asc = boolean(asc)
|
|
18
|
+
limit = integer(limit) or limit_default
|
|
19
|
+
return where, sort, asc, limit
|
|
20
|
+
|
|
21
|
+
def get_request_opportunity_options(request) -> dict:
|
|
22
|
+
status = request.query_params.get("status") or ""
|
|
23
|
+
author = request.query_params.get("author") or ""
|
|
24
|
+
sort = request.query_params.get("sort") or DATED
|
|
25
|
+
asc = request.query_params.get("asc") or False
|
|
26
|
+
limit = request.query_params.get("limit") or LIST_MAX_ROWS
|
|
27
|
+
values = locals()
|
|
28
|
+
values.pop("request")
|
|
29
|
+
return values
|
|
30
|
+
|
|
31
|
+
def get_request_interaction_options(request) -> dict:
|
|
32
|
+
values = get_request_opportunity_options(request)
|
|
33
|
+
values["form"] = request.query_params.get("form") or ""
|
|
34
|
+
values.pop("status")
|
|
35
|
+
return values
|
|
36
|
+
|
|
37
|
+
def get_request_contact_options(request) -> dict:
|
|
38
|
+
values = get_request_opportunity_options(request)
|
|
39
|
+
values.pop("status")
|
|
40
|
+
return values
|