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.
@@ -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,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -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,3 @@
1
+ from .customer import *
2
+
3
+ __version__ = "1.0.0"
@@ -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