zrb 1.0.0b10__py3-none-any.whl → 1.1.0__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.
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_task.py +99 -55
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/column/add_column_util.py +301 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +24 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +61 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/module/gateway/view/content/my-module/my-entity.html +297 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +24 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/navigation_config_file.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_task.py +8 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/add_module_util.py +40 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +2 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/navigation_config_file.py +6 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +2 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/view.py +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/config.py +18 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py +39 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/route.py +52 -11
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/schema/navigation.py +95 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/subroute/auth.py +91 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +9 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/view.py +33 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/permission.html +311 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/role.html +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/auth/user.html +0 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/error.html +4 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/login.html +67 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/content/logout.html +49 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/common/util.js +160 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/style.css +14 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js +94 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/pico-style.css +23 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/script.js +44 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/default/style.css +102 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/template/default.html +73 -18
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/requirements.txt +1 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +2 -4
- zrb/runner/web_route/refresh_token_api_route.py +1 -1
- zrb/runner/web_route/static/refresh-token.template.js +9 -0
- zrb/runner/web_route/static/static_route.py +1 -1
- zrb/util/load.py +13 -7
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/RECORD +43 -26
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b10.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,297 @@
|
|
1
|
+
<link rel="stylesheet" href="/static/crud/style.css">
|
2
|
+
|
3
|
+
<main class="container">
|
4
|
+
<article>
|
5
|
+
<h1>My Entity</h1>
|
6
|
+
|
7
|
+
<fieldset id="crud-table-fieldset" role="group" class="grid">
|
8
|
+
<input id="crud-filter" onchange="applySearch()" placeholder="🔍 Filter" aria-label="Search" />
|
9
|
+
<button onclick="applySearch()">🔍 Search</button>
|
10
|
+
{% if allow_create %}
|
11
|
+
<button class="contrast" onclick="showCreateForm(event)">➕ Add</button>
|
12
|
+
{% endif %}
|
13
|
+
</fieldset>
|
14
|
+
<div id="crud-table-container">
|
15
|
+
<table id="crud-table" class="striped">
|
16
|
+
<thead>
|
17
|
+
<tr>
|
18
|
+
<th scope="col">ID</th>
|
19
|
+
<th scope="col">My Column</th>
|
20
|
+
<!-- Update this -->
|
21
|
+
{% if allow_update or allow_delete %}
|
22
|
+
<th scope="col">Actions</th>
|
23
|
+
{% endif %}
|
24
|
+
</tr>
|
25
|
+
</thead>
|
26
|
+
<tbody></tbody>
|
27
|
+
</table>
|
28
|
+
</div>
|
29
|
+
<div id="crud-pagination"></div>
|
30
|
+
|
31
|
+
{% if allow_create %}
|
32
|
+
<dialog id="crud-create-form-dialog">
|
33
|
+
<article>
|
34
|
+
<h2>New My Entity</h2>
|
35
|
+
<form id="crud-create-form">
|
36
|
+
<label>
|
37
|
+
My Column:
|
38
|
+
<input type="text" name="my_column" required>
|
39
|
+
</label>
|
40
|
+
<!-- Update this -->
|
41
|
+
<footer>
|
42
|
+
<button onclick="createRow(event)">➕ Save</button>
|
43
|
+
<button class="secondary" onclick="hideCreateForm(event)">❌ Cancel</button>
|
44
|
+
</footer>
|
45
|
+
</form>
|
46
|
+
</article>
|
47
|
+
</dialog>
|
48
|
+
{% endif %}
|
49
|
+
|
50
|
+
{% if allow_update %}
|
51
|
+
<dialog id="crud-update-form-dialog">
|
52
|
+
<article>
|
53
|
+
<h2>Update My Entity</h2>
|
54
|
+
<form id="crud-update-form">
|
55
|
+
<label>
|
56
|
+
My Column:
|
57
|
+
<input type="text" name="my_column" required>
|
58
|
+
</label>
|
59
|
+
<!-- Update this -->
|
60
|
+
<footer>
|
61
|
+
<button onclick="updateRow(event)">✏️ Save</button>
|
62
|
+
<button class="secondary" onclick="hideUpdateForm(event)">❌ Cancel</button>
|
63
|
+
</footer>
|
64
|
+
</form>
|
65
|
+
</article>
|
66
|
+
</dialog>
|
67
|
+
{% endif %}
|
68
|
+
|
69
|
+
{% if allow_delete %}
|
70
|
+
<dialog id="crud-delete-form-dialog">
|
71
|
+
<article>
|
72
|
+
<h2>Delete My Entity</h2>
|
73
|
+
<form id="crud-delete-form">
|
74
|
+
<label>
|
75
|
+
My Column:
|
76
|
+
<input type="text" name="my_column" readonly>
|
77
|
+
</label>
|
78
|
+
<!-- Update this -->
|
79
|
+
<footer>
|
80
|
+
<button class="secondary" onclick="hideDeleteForm()">❌ Cancel</button>
|
81
|
+
<button onclick="deleteRow()">🗑️ Delete</button>
|
82
|
+
</footer>
|
83
|
+
</form>
|
84
|
+
</article>
|
85
|
+
</dialog>
|
86
|
+
{% endif %}
|
87
|
+
|
88
|
+
<dialog id="crud-alert-dialog">
|
89
|
+
<article>
|
90
|
+
<h2 id="crud-alert-title">Error</h2>
|
91
|
+
<pre id="crud-alert-message"></pre>
|
92
|
+
<footer>
|
93
|
+
<button onclick="hideAlert(event)">Close</button>
|
94
|
+
</footer>
|
95
|
+
</article>
|
96
|
+
</dialog>
|
97
|
+
|
98
|
+
</article>
|
99
|
+
</main>
|
100
|
+
|
101
|
+
<script src="/static/crud/util.js"></script>
|
102
|
+
<script>
|
103
|
+
const apiUrl = "/api/v1/my-entities";
|
104
|
+
const crudState = {
|
105
|
+
pageSize: {{page_size | tojson}},
|
106
|
+
currentPage: {{page | tojson}},
|
107
|
+
sort: {{sort | tojson}},
|
108
|
+
filter: {{filter | tojson}},
|
109
|
+
allowCreate: {{allow_create | tojson}},
|
110
|
+
allowUpdate: {{allow_update | tojson}},
|
111
|
+
allowDelete: {{allow_delete | tojson}},
|
112
|
+
updatedRowId: null,
|
113
|
+
deletedRowId: null,
|
114
|
+
};
|
115
|
+
|
116
|
+
async function applySearch() {
|
117
|
+
const filterInput = document.getElementById("crud-filter");
|
118
|
+
crudState.filter = filterInput.value;
|
119
|
+
return await fetchRows(crudState.currentPage);
|
120
|
+
}
|
121
|
+
|
122
|
+
async function fetchRows(page = null) {
|
123
|
+
try {
|
124
|
+
if (typeof page !== 'undefined' && page !== null) {
|
125
|
+
crudState.currentPage = page;
|
126
|
+
}
|
127
|
+
const defaultSearchColumn = "my_column"
|
128
|
+
// update address bar
|
129
|
+
const searchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, false);
|
130
|
+
const newUrl = `${window.location.pathname}?${searchParam}`;
|
131
|
+
window.history.pushState({ path: newUrl }, "", newUrl);
|
132
|
+
// update table and pagination
|
133
|
+
const apiSearchParam = CRUD_UTIL.getSearchParam(crudState, defaultSearchColumn, true);
|
134
|
+
const result = await UTIL.fetchAPI(
|
135
|
+
`${apiUrl}?${apiSearchParam}`, { method: "GET" }
|
136
|
+
);
|
137
|
+
renderRows(result.data);
|
138
|
+
const crudPagination = document.getElementById("crud-pagination");
|
139
|
+
CRUD_UTIL.renderPagination(
|
140
|
+
crudPagination, crudState, result.count, "fetchRows"
|
141
|
+
);
|
142
|
+
} catch (error) {
|
143
|
+
console.error("Error fetching items:", error);
|
144
|
+
}
|
145
|
+
}
|
146
|
+
|
147
|
+
function renderRows(rows) {
|
148
|
+
const tableBody = document.querySelector("#crud-table tbody");
|
149
|
+
tableBody.innerHTML = "";
|
150
|
+
rows.forEach(row => {
|
151
|
+
let rowComponent = getRowComponents(row);
|
152
|
+
actionColumn = "";
|
153
|
+
if (crudState.allowUpdate) {
|
154
|
+
actionColumn += `<button class="contrast" onclick="showUpdateForm('${row.id}')">✏️ Edit</button>`;
|
155
|
+
}
|
156
|
+
if (crudState.allowDelete) {
|
157
|
+
actionColumn += `<button class="secondary" onclick="showDeleteForm('${row.id}')">🗑️ Delete</button>`;
|
158
|
+
}
|
159
|
+
if (crudState.allowUpdate || crudState.allowDelete) {
|
160
|
+
actionColumn = `<td><fieldset class="grid" role="group">${actionColumn}</fieldset></td>`;
|
161
|
+
}
|
162
|
+
tableBody.innerHTML += `<tr>${rowComponent.join('')}${actionColumn}</tr>`;
|
163
|
+
});
|
164
|
+
}
|
165
|
+
|
166
|
+
function getRowComponents(row) {
|
167
|
+
let rowComponents = [];
|
168
|
+
rowComponents.push(`<td>${row.id}</td>`);
|
169
|
+
rowComponents.push(`<td>${row.my_column}</td>`);
|
170
|
+
// Update this
|
171
|
+
return rowComponents;
|
172
|
+
}
|
173
|
+
|
174
|
+
{% if allow_create %}
|
175
|
+
async function showCreateForm(id) {
|
176
|
+
const createFormDialog = document.getElementById("crud-create-form-dialog");
|
177
|
+
const createForm = document.getElementById("crud-create-form");
|
178
|
+
UTIL.clearFormData(createForm);
|
179
|
+
createFormDialog.showModal();
|
180
|
+
}
|
181
|
+
|
182
|
+
async function createRow(event = null) {
|
183
|
+
if (event != null) {
|
184
|
+
event.preventDefault();
|
185
|
+
}
|
186
|
+
try {
|
187
|
+
const createForm = document.getElementById("crud-create-form");
|
188
|
+
const formData = JSON.stringify(UTIL.getFormData(createForm));
|
189
|
+
await UTIL.fetchAPI(apiUrl, {method: "POST", body: formData});
|
190
|
+
await fetchRows();
|
191
|
+
hideCreateForm();
|
192
|
+
} catch(error) {
|
193
|
+
showAlert("Create My Entity Error", error);
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
function hideCreateForm(event = null) {
|
198
|
+
if (event != null) {
|
199
|
+
event.preventDefault();
|
200
|
+
}
|
201
|
+
const createFormDialog = document.getElementById("crud-create-form-dialog");
|
202
|
+
createFormDialog.close();
|
203
|
+
}
|
204
|
+
{% endif %}
|
205
|
+
|
206
|
+
{% if allow_update %}
|
207
|
+
async function showUpdateForm(id) {
|
208
|
+
crudState.updatedRowId = id;
|
209
|
+
const updateFormDialog = document.getElementById("crud-update-form-dialog");
|
210
|
+
const updateForm = document.getElementById("crud-update-form");
|
211
|
+
result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
|
212
|
+
UTIL.setFormData(updateForm, result);
|
213
|
+
updateFormDialog.showModal();
|
214
|
+
}
|
215
|
+
|
216
|
+
async function updateRow(event = null) {
|
217
|
+
if (event != null) {
|
218
|
+
event.preventDefault();
|
219
|
+
}
|
220
|
+
try {
|
221
|
+
const updateForm = document.getElementById("crud-update-form");
|
222
|
+
const formData = JSON.stringify(UTIL.getFormData(updateForm));
|
223
|
+
await UTIL.fetchAPI(
|
224
|
+
`${apiUrl}/${crudState.updatedRowId}`, {method: "PUT", body: formData},
|
225
|
+
);
|
226
|
+
await fetchRows();
|
227
|
+
hideUpdateForm();
|
228
|
+
} catch(error) {
|
229
|
+
showAlert("Update My Entity Error", error);
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
function hideUpdateForm(event = null) {
|
234
|
+
if (event != null) {
|
235
|
+
event.preventDefault();
|
236
|
+
}
|
237
|
+
const updateFormDialog = document.getElementById("crud-update-form-dialog");
|
238
|
+
updateFormDialog.close();
|
239
|
+
}
|
240
|
+
{% endif %}
|
241
|
+
|
242
|
+
{% if allow_delete %}
|
243
|
+
async function showDeleteForm(id) {
|
244
|
+
crudState.deletedRowId = id;
|
245
|
+
const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
|
246
|
+
const deleteForm = document.getElementById("crud-delete-form");
|
247
|
+
result = await UTIL.fetchAPI(`${apiUrl}/${id}`, { method: "GET" });
|
248
|
+
UTIL.setFormData(deleteForm, result);
|
249
|
+
deleteFormDialog.showModal();
|
250
|
+
}
|
251
|
+
|
252
|
+
async function deleteRow(event = null) {
|
253
|
+
if (event != null) {
|
254
|
+
event.preventDefault();
|
255
|
+
}
|
256
|
+
try {
|
257
|
+
await UTIL.fetchAPI(`${apiUrl}/${crudState.deletedRowId}`, {method: "DELETE",});
|
258
|
+
await fetchRows();
|
259
|
+
hideDeleteForm();
|
260
|
+
} catch(error) {
|
261
|
+
showAlert("Delete My Entity Error", error);
|
262
|
+
}
|
263
|
+
}
|
264
|
+
|
265
|
+
function hideDeleteForm(event = null) {
|
266
|
+
if (event != null) {
|
267
|
+
event.preventDefault();
|
268
|
+
}
|
269
|
+
const deleteFormDialog = document.getElementById("crud-delete-form-dialog");
|
270
|
+
deleteFormDialog.close();
|
271
|
+
}
|
272
|
+
{% endif %}
|
273
|
+
|
274
|
+
function showAlert(title, error) {
|
275
|
+
const alertDialog = document.getElementById("crud-alert-dialog");
|
276
|
+
const alertTitle = document.getElementById("crud-alert-title");
|
277
|
+
const alertMessage = document.getElementById("crud-alert-message");
|
278
|
+
const errorMessage = error.message ? error.message : String(error);
|
279
|
+
alertTitle.textContent = title;
|
280
|
+
alertMessage.textContent = errorMessage;
|
281
|
+
alertDialog.showModal();
|
282
|
+
}
|
283
|
+
|
284
|
+
function hideAlert(event = null) {
|
285
|
+
if (event != null) {
|
286
|
+
event.preventDefault();
|
287
|
+
}
|
288
|
+
const alertDialog = document.getElementById("crud-alert-dialog");
|
289
|
+
alertDialog.close();
|
290
|
+
}
|
291
|
+
|
292
|
+
document.addEventListener("DOMContentLoaded", () => {
|
293
|
+
const filterInput = document.getElementById("crud-filter");
|
294
|
+
filterInput.value = crudState.filter;
|
295
|
+
fetchRows(crudState.currentPage);
|
296
|
+
});
|
297
|
+
</script>
|
@@ -1,6 +1,30 @@
|
|
1
1
|
# MyEntity routes
|
2
2
|
|
3
3
|
|
4
|
+
@app.get("/my-module/my-entities", include_in_schema=False)
|
5
|
+
def my_entities_crud_ui(
|
6
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
7
|
+
page: int = 1,
|
8
|
+
page_size: int = 10,
|
9
|
+
sort: str | None = None,
|
10
|
+
filter: str | None = None,
|
11
|
+
):
|
12
|
+
if not current_user.has_permission("my-entity:read"):
|
13
|
+
return render_error(error_message="Access denied", status_code=403)
|
14
|
+
return render_content(
|
15
|
+
view_path=os.path.join("my-module", "my-entity.html"),
|
16
|
+
current_user=current_user,
|
17
|
+
page_name="my-module.my-entity",
|
18
|
+
page=page,
|
19
|
+
page_size=page_size,
|
20
|
+
sort=sort,
|
21
|
+
filter=filter,
|
22
|
+
allow_create=current_user.has_permission("my-entity:create"),
|
23
|
+
allow_update=current_user.has_permission("my-entity:update"),
|
24
|
+
allow_delete=current_user.has_permission("my-entity:delete"),
|
25
|
+
)
|
26
|
+
|
27
|
+
|
4
28
|
@app.get("/api/v1/my-entities", response_model=MultipleMyEntityResponse)
|
5
29
|
async def get_my_entities(
|
6
30
|
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
@@ -10,12 +10,14 @@ from my_app_name._zrb.module.add_module_util import (
|
|
10
10
|
is_app_zrb_config_file,
|
11
11
|
is_app_zrb_task_file,
|
12
12
|
is_gateway_module_subroute_file,
|
13
|
+
is_gateway_navigation_config_file,
|
13
14
|
is_gateway_route_file,
|
14
15
|
is_in_module_dir,
|
15
16
|
update_app_config_file,
|
16
17
|
update_app_main_file,
|
17
18
|
update_app_zrb_config_file,
|
18
19
|
update_app_zrb_task_file,
|
20
|
+
update_gateway_navigation_config_file,
|
19
21
|
update_gateway_route_file,
|
20
22
|
)
|
21
23
|
from my_app_name._zrb.util import get_existing_module_names
|
@@ -91,6 +93,12 @@ scaffold_my_app_name_module = Scaffolder(
|
|
91
93
|
match=is_gateway_route_file,
|
92
94
|
transform=update_gateway_route_file,
|
93
95
|
),
|
96
|
+
# Register module's page to my_app_name/gateway/config/navigation.py
|
97
|
+
ContentTransformer(
|
98
|
+
name="transform-gateway-navigation-config",
|
99
|
+
match=is_gateway_navigation_config_file,
|
100
|
+
transform=update_gateway_navigation_config_file,
|
101
|
+
),
|
94
102
|
],
|
95
103
|
retries=0,
|
96
104
|
upstream=validate_add_my_app_name_module,
|
@@ -7,7 +7,12 @@ from zrb.context.any_context import AnyContext
|
|
7
7
|
from zrb.util.codemod.modify_dict import append_key_to_dict
|
8
8
|
from zrb.util.codemod.modify_function import append_code_to_function
|
9
9
|
from zrb.util.file import read_file, write_file
|
10
|
-
from zrb.util.string.conversion import
|
10
|
+
from zrb.util.string.conversion import (
|
11
|
+
to_human_case,
|
12
|
+
to_kebab_case,
|
13
|
+
to_pascal_case,
|
14
|
+
to_snake_case,
|
15
|
+
)
|
11
16
|
|
12
17
|
|
13
18
|
def is_app_config_file(ctx: AnyContext, file_path: str) -> bool:
|
@@ -22,6 +27,12 @@ def is_gateway_route_file(ctx: AnyContext, file_path: str) -> bool:
|
|
22
27
|
return file_path == os.path.join(APP_DIR, "module", "gateway", "route.py")
|
23
28
|
|
24
29
|
|
30
|
+
def is_gateway_navigation_config_file(ctx: AnyContext, file_path: str) -> bool:
|
31
|
+
return file_path == os.path.join(
|
32
|
+
APP_DIR, "module", "gateway", "config", "navigation.py"
|
33
|
+
)
|
34
|
+
|
35
|
+
|
25
36
|
def is_app_zrb_task_file(ctx: AnyContext, file_path: str) -> bool:
|
26
37
|
return file_path == os.path.join(APP_DIR, "_zrb", "task.py")
|
27
38
|
|
@@ -194,3 +205,31 @@ def _get_module_subroute_import(existing_code: str, module_name: str) -> str | N
|
|
194
205
|
if import_code in existing_code:
|
195
206
|
return None
|
196
207
|
return import_code
|
208
|
+
|
209
|
+
|
210
|
+
def update_gateway_navigation_config_file(
|
211
|
+
ctx: AnyContext, gateway_navigation_config_file_path: str
|
212
|
+
):
|
213
|
+
existing_gateway_navigation_config_code = read_file(
|
214
|
+
gateway_navigation_config_file_path
|
215
|
+
)
|
216
|
+
snake_module_name = to_snake_case(ctx.input.module)
|
217
|
+
kebab_module_name = to_kebab_case(ctx.input.module)
|
218
|
+
human_module_name = to_human_case(ctx.input.module)
|
219
|
+
new_navigation_config_code = read_file(
|
220
|
+
file_path=os.path.join(
|
221
|
+
os.path.dirname(__file__), "template", "navigation_config_file.py"
|
222
|
+
),
|
223
|
+
replace_map={
|
224
|
+
"my_module": snake_module_name,
|
225
|
+
"my-module": kebab_module_name,
|
226
|
+
"My Module": human_module_name.title(),
|
227
|
+
},
|
228
|
+
).strip()
|
229
|
+
write_file(
|
230
|
+
file_path=gateway_navigation_config_file_path,
|
231
|
+
content=[
|
232
|
+
existing_gateway_navigation_config_code,
|
233
|
+
new_navigation_config_code,
|
234
|
+
],
|
235
|
+
)
|
@@ -1,8 +1,10 @@
|
|
1
|
+
import os
|
1
2
|
from typing import Annotated
|
2
3
|
|
3
4
|
from fastapi import Depends, FastAPI
|
4
5
|
from my_app_name.common.error import ForbiddenError
|
5
6
|
from my_app_name.module.gateway.util.auth import get_current_user
|
7
|
+
from my_app_name.module.gateway.util.view import render_content, render_error
|
6
8
|
from my_app_name.schema.user import AuthUserResponse
|
7
9
|
|
8
10
|
|
@@ -72,7 +72,7 @@ def create_default_filter_param_parser() -> (
|
|
72
72
|
# Returns [UserModel.age >= 18, UserModel.name.like("John%"), UserModel.role.in_(["admin", "user"]), UserModel.address == "123, Main St."]
|
73
73
|
"""
|
74
74
|
filters: list[ClauseElement] = []
|
75
|
-
filter_parts =
|
75
|
+
filter_parts = split_unescaped(query)
|
76
76
|
for part in filter_parts:
|
77
77
|
match = re.match(r"(.+):(.+):(.+)", part)
|
78
78
|
if match:
|
@@ -101,5 +101,5 @@ def create_default_filter_param_parser() -> (
|
|
101
101
|
return parse_filter_param
|
102
102
|
|
103
103
|
|
104
|
-
def
|
104
|
+
def split_unescaped(s: str, delimiter: str = ",") -> list[str]:
|
105
105
|
return re.split(r"(?<!\\)" + re.escape(delimiter), s)
|
@@ -27,7 +27,7 @@ def render_str(template_path: str, **data: Any) -> str:
|
|
27
27
|
def render_page(
|
28
28
|
template_path: str,
|
29
29
|
status_code: int = 200,
|
30
|
-
headers: dict[str, str] = None,
|
30
|
+
headers: dict[str, str] | None = None,
|
31
31
|
media_type: str | None = None,
|
32
32
|
**data: Any
|
33
33
|
) -> HTMLResponse:
|
@@ -8,25 +8,35 @@ APP_VERSION = "0.1.0"
|
|
8
8
|
|
9
9
|
APP_GATEWAY_VIEW_PATH = os.path.join(APP_PATH, "module", "gateway", "view")
|
10
10
|
APP_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH = os.getenv(
|
11
|
-
"
|
11
|
+
"MY_APP_NAME_GATEWAY_VIEW_DEFAULT_TEMPLATE_PATH",
|
12
12
|
os.path.join("template", "default.html"),
|
13
13
|
)
|
14
|
-
|
14
|
+
APP_GATEWAY_PICO_CSS_COLOR = os.getenv("MY_APP_NAME_GATEWAY_PICO_CSS_COLOR", "")
|
15
|
+
APP_GATEWAY_PICO_CSS_PATH = (
|
16
|
+
"/static/pico-css/pico.min.css"
|
17
|
+
if APP_GATEWAY_PICO_CSS_COLOR == ""
|
18
|
+
else f"/static/pico-css/pico.{APP_GATEWAY_PICO_CSS_COLOR}.min.css"
|
19
|
+
)
|
15
20
|
APP_GATEWAY_CSS_PATH_LIST = [
|
16
21
|
path
|
17
|
-
for path in os.getenv("
|
22
|
+
for path in os.getenv("MY_APP_NAME_GATEWAY_CSS_PATH", "").split(":")
|
18
23
|
if path != ""
|
19
24
|
]
|
20
25
|
APP_GATEWAY_JS_PATH_LIST = [
|
21
|
-
path
|
26
|
+
path
|
27
|
+
for path in os.getenv("MY_APP_NAME_GATEWAY_JS_PATH", "").split(":")
|
28
|
+
if path != ""
|
22
29
|
]
|
23
|
-
APP_GATEWAY_TITLE = os.getenv("
|
24
|
-
APP_GATEWAY_SUBTITLE = os.getenv("
|
30
|
+
APP_GATEWAY_TITLE = os.getenv("MY_APP_NAME_GATEWAY_TITLE", "My App Name")
|
31
|
+
APP_GATEWAY_SUBTITLE = os.getenv("MY_APP_NAME_GATEWAY_SUBTITLE", "Just Another App")
|
25
32
|
APP_GATEWAY_LOGO_PATH = os.getenv(
|
26
|
-
"
|
33
|
+
"MY_APP_NAME_GATEWAY_LOGO", "/static/images/android-chrome-192x192.png"
|
34
|
+
)
|
35
|
+
APP_GATEWAY_FOOTER = os.getenv(
|
36
|
+
"MY_APP_NAME_GATEWAY_FOOTER", f"{APP_GATEWAY_TITLE} © 2025"
|
27
37
|
)
|
28
38
|
APP_GATEWAY_FAVICON_PATH = os.getenv(
|
29
|
-
"
|
39
|
+
"MY_APP_NAME_GATEWAY_FAVICON", "/static/images/favicon-32x32.png"
|
30
40
|
)
|
31
41
|
|
32
42
|
APP_MODE = os.getenv("MY_APP_NAME_MODE", "monolith")
|
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/config/navigation.py
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
from my_app_name.module.gateway.schema.navigation import Navigation, Page, PageGroup
|
2
|
+
|
3
|
+
APP_NAVIGATION = Navigation()
|
4
|
+
|
5
|
+
APP_NAVIGATION.append_page(Page(name="gateway.home", caption="Home", url="/"))
|
6
|
+
|
7
|
+
auth_menu = APP_NAVIGATION.append_page_group(
|
8
|
+
PageGroup(
|
9
|
+
name="auth",
|
10
|
+
caption="Authorization",
|
11
|
+
)
|
12
|
+
)
|
13
|
+
|
14
|
+
auth_menu.append_page(
|
15
|
+
Page(
|
16
|
+
name="auth.permission",
|
17
|
+
caption="Permission",
|
18
|
+
url="/auth/permissions",
|
19
|
+
permission="permission:read",
|
20
|
+
)
|
21
|
+
)
|
22
|
+
|
23
|
+
auth_menu.append_page(
|
24
|
+
Page(
|
25
|
+
name="auth.role",
|
26
|
+
caption="Role",
|
27
|
+
url="/auth/roles",
|
28
|
+
permission="role:read",
|
29
|
+
)
|
30
|
+
)
|
31
|
+
|
32
|
+
auth_menu.append_page(
|
33
|
+
Page(
|
34
|
+
name="auth.user",
|
35
|
+
caption="User",
|
36
|
+
url="/auth/users",
|
37
|
+
permission="user:read",
|
38
|
+
)
|
39
|
+
)
|
@@ -1,18 +1,21 @@
|
|
1
|
-
import
|
1
|
+
from typing import Annotated
|
2
2
|
|
3
|
-
from fastapi import FastAPI, HTTPException, Request
|
3
|
+
from fastapi import Depends, FastAPI, HTTPException, Request
|
4
4
|
from fastapi.exception_handlers import http_exception_handler
|
5
|
-
from fastapi.responses import HTMLResponse
|
5
|
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
6
6
|
from my_app_name.common.app_factory import app
|
7
7
|
from my_app_name.common.schema import BasicResponse
|
8
8
|
from my_app_name.config import (
|
9
|
-
|
9
|
+
APP_AUTH_ACCESS_TOKEN_COOKIE_NAME,
|
10
10
|
APP_MAIN_MODULE,
|
11
11
|
APP_MODE,
|
12
12
|
APP_MODULES,
|
13
13
|
)
|
14
|
+
from my_app_name.module.auth.client.auth_client_factory import auth_client
|
14
15
|
from my_app_name.module.gateway.subroute.auth import serve_auth_route
|
15
|
-
from my_app_name.module.gateway.util.
|
16
|
+
from my_app_name.module.gateway.util.auth import get_current_user
|
17
|
+
from my_app_name.module.gateway.util.view import render_content, render_error
|
18
|
+
from my_app_name.schema.user import AuthUserResponse
|
16
19
|
|
17
20
|
|
18
21
|
def serve_route(app: FastAPI):
|
@@ -21,18 +24,48 @@ def serve_route(app: FastAPI):
|
|
21
24
|
if APP_MODE == "monolith" or APP_MAIN_MODULE == "gateway":
|
22
25
|
_serve_health_check(app)
|
23
26
|
_serve_readiness_check(app)
|
24
|
-
|
27
|
+
_serve_common_pages(app)
|
25
28
|
_handle_404(app)
|
26
29
|
|
27
30
|
# Serve auth routes
|
28
31
|
serve_auth_route(app)
|
29
32
|
|
30
33
|
|
31
|
-
def
|
34
|
+
def _serve_common_pages(app: FastAPI):
|
32
35
|
@app.get("/", include_in_schema=False)
|
33
|
-
def home_page(
|
34
|
-
|
35
|
-
|
36
|
+
def home_page(
|
37
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
38
|
+
):
|
39
|
+
return render_content(
|
40
|
+
view_path="homepage.html",
|
41
|
+
current_user=current_user,
|
42
|
+
page_name="gateway.home",
|
43
|
+
)
|
44
|
+
|
45
|
+
@app.get("/login", include_in_schema=False)
|
46
|
+
def login_page(
|
47
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
48
|
+
):
|
49
|
+
if not current_user.is_guest:
|
50
|
+
return RedirectResponse("/")
|
51
|
+
return render_content(
|
52
|
+
view_path="login.html",
|
53
|
+
current_user=current_user,
|
54
|
+
page_name="gateway.home",
|
55
|
+
partials={"show_user_info": False},
|
56
|
+
)
|
57
|
+
|
58
|
+
@app.get("/logout", include_in_schema=False)
|
59
|
+
def logout_page(
|
60
|
+
current_user: Annotated[AuthUserResponse, Depends(get_current_user)],
|
61
|
+
):
|
62
|
+
if current_user is None:
|
63
|
+
return RedirectResponse("/")
|
64
|
+
return render_content(
|
65
|
+
view_path="logout.html",
|
66
|
+
current_user=current_user,
|
67
|
+
page_name="gateway.home",
|
68
|
+
partials={"show_user_info": False},
|
36
69
|
)
|
37
70
|
|
38
71
|
|
@@ -60,7 +93,15 @@ def _handle_404(app: FastAPI):
|
|
60
93
|
if request.url.path.startswith("/api"):
|
61
94
|
# Re-raise the exception to let FastAPI handle it
|
62
95
|
return await http_exception_handler(request, exc)
|
63
|
-
|
96
|
+
# Get current user by cookies
|
97
|
+
current_user = None
|
98
|
+
cookie_access_token = request.cookies.get(APP_AUTH_ACCESS_TOKEN_COOKIE_NAME)
|
99
|
+
if cookie_access_token is not None and cookie_access_token != "":
|
100
|
+
current_user = await auth_client.get_current_user(cookie_access_token)
|
101
|
+
# Show error page
|
102
|
+
return render_error(
|
103
|
+
error_message="Not found", status_code=404, current_user=current_user
|
104
|
+
)
|
64
105
|
|
65
106
|
|
66
107
|
serve_route(app)
|