zrb 1.0.0b9__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/.coveragerc +11 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/.gitignore +4 -0
- 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/config.py +5 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_task.py +131 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/add_entity_util.py +128 -5
- 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/app_template/test/my_module/my_entity/test_create_my_entity.py +53 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_delete_my_entity.py +62 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_read_my_entity.py +65 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/app_template/test/my_module/my_entity/test_update_my_entity.py +61 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/entity/template/gateway_subroute.py +81 -13
- 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 +42 -3
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/app_template/module/gateway/subroute/my_module.py +8 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/module/template/module_task_definition.py +10 -6
- 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/_zrb/task.py +56 -12
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/_zrb/task_util.py +10 -4
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/base_service.py +136 -52
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/common/util/parser.py +3 -3
- 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 +19 -8
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/3093c7336477_add_auth_tables.py +46 -43
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/migration/versions/8ed025bcc845_create_permissions.py +69 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/repository/user_db_repository.py +5 -2
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/auth/service/user/user_service.py +16 -21
- 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 +277 -44
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/util/auth.py +66 -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 +6 -1
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/permission.py +1 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/schema/user.py +9 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/_util/access_token.py +19 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_create_permission.py +59 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_delete_permission.py +68 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_read_permission.py +71 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/permission/test_update_permission.py +66 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/auth/test_user_session.py +195 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_health_and_readiness.py +28 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_homepage.py +15 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test/test_not_found_error.py +16 -0
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/test.sh +7 -0
- 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/task/base_task.py +10 -10
- zrb/util/codemod/modification_mode.py +3 -0
- zrb/util/codemod/modify_class.py +58 -0
- zrb/util/codemod/modify_class_parent.py +68 -0
- zrb/util/codemod/modify_class_property.py +128 -0
- zrb/util/codemod/modify_dict.py +75 -0
- zrb/util/codemod/modify_function.py +65 -0
- zrb/util/codemod/modify_function_call.py +68 -0
- zrb/util/codemod/modify_method.py +88 -0
- zrb/util/codemod/{prepend_code_to_module.py → modify_module.py} +2 -3
- zrb/util/file.py +3 -2
- zrb/util/load.py +13 -7
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/METADATA +2 -2
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/RECORD +80 -46
- zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/migrate.py +0 -3
- zrb/util/codemod/append_code_to_class.py +0 -35
- zrb/util/codemod/append_code_to_function.py +0 -38
- zrb/util/codemod/append_code_to_method.py +0 -55
- zrb/util/codemod/append_key_to_dict.py +0 -51
- zrb/util/codemod/append_param_to_function_call.py +0 -39
- zrb/util/codemod/prepend_parent_to_class.py +0 -38
- zrb/util/codemod/prepend_property_to_class.py +0 -55
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/WHEEL +0 -0
- {zrb-1.0.0b9.dist-info → zrb-1.1.0.dist-info}/entry_points.txt +0 -0
zrb/builtin/project/add/fastapp/fastapp_template/my_app_name/module/gateway/view/static/crud/util.js
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
const CRUD_UTIL = {
|
2
|
+
|
3
|
+
renderPagination(paginationComponent, crudState, total, fetchFunction = "fetchRows") {
|
4
|
+
const totalPages = Math.ceil(total / crudState.pageSize);
|
5
|
+
paginationComponent.innerHTML = "";
|
6
|
+
// Ensure left alignment (if not already handled by PicoCSS or external CSS)
|
7
|
+
paginationComponent.style.textAlign = "left";
|
8
|
+
let paginationHTML = "";
|
9
|
+
// Only show "First" and "Previous" if we're not on page 1
|
10
|
+
if (crudState.currentPage > 1) {
|
11
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(1)">«</button>`;
|
12
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${crudState.currentPage - 1})"><</button>`;
|
13
|
+
}
|
14
|
+
if (totalPages <= 5) {
|
15
|
+
// If total pages are few, simply list them all
|
16
|
+
for (let i = 1; i <= totalPages; i++) {
|
17
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${i})" ${i === crudState.currentPage ? "disabled" : ""}>${i}</button>`;
|
18
|
+
}
|
19
|
+
} else {
|
20
|
+
// Always show first page
|
21
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(1)" ${crudState.currentPage === 1 ? "disabled" : ""}>1</button>`;
|
22
|
+
// Determine start and end for the page range around current page
|
23
|
+
const start = Math.max(2, crudState.currentPage - 1);
|
24
|
+
const end = Math.min(totalPages - 1, crudState.currentPage + 1);
|
25
|
+
// Add ellipsis if there's a gap between first page and the start of the range
|
26
|
+
if (start > 2) {
|
27
|
+
paginationHTML += `<span style="padding: 0 5px;">...</span>`;
|
28
|
+
}
|
29
|
+
// Render the range around the current page
|
30
|
+
for (let i = start; i <= end; i++) {
|
31
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${i})" ${i === crudState.currentPage ? "disabled" : ""}>${i}</button>`;
|
32
|
+
}
|
33
|
+
// Add ellipsis if there's a gap between the end of the range and the last page
|
34
|
+
if (end < totalPages - 1) {
|
35
|
+
paginationHTML += `<span style="padding: 0 5px;">...</span>`;
|
36
|
+
}
|
37
|
+
// Always show last page
|
38
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${totalPages})" ${crudState.currentPage === totalPages ? "disabled" : ""}>${totalPages}</button>`;
|
39
|
+
}
|
40
|
+
// Only show "Next" and "Last" if we're not on the last page
|
41
|
+
if (crudState.currentPage < totalPages) {
|
42
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${crudState.currentPage + 1})">></button>`;
|
43
|
+
paginationHTML += `<button class="secondary" onclick="${fetchFunction}(${totalPages})">»</button>`;
|
44
|
+
}
|
45
|
+
paginationComponent.innerHTML = paginationHTML;
|
46
|
+
},
|
47
|
+
|
48
|
+
splitUnescaped(query, delimiter=",") {
|
49
|
+
const parts = [];
|
50
|
+
let current = "";
|
51
|
+
let escaped = false;
|
52
|
+
for (let i = 0; i < query.length; i++) {
|
53
|
+
const char = query[i];
|
54
|
+
if (escaped) {
|
55
|
+
current += char;
|
56
|
+
escaped = false;
|
57
|
+
} else if (char === "\\") {
|
58
|
+
escaped = true;
|
59
|
+
} else if (char === delimiter) {
|
60
|
+
parts.push(current);
|
61
|
+
current = "";
|
62
|
+
} else {
|
63
|
+
current += char;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
if (current != "") {
|
67
|
+
parts.push(current);
|
68
|
+
}
|
69
|
+
return parts;
|
70
|
+
},
|
71
|
+
|
72
|
+
isValidFilterQuery(query) {
|
73
|
+
const filterPattern = /^([\w]+):(eq|ne|gt|gte|lt|lte|like|in):(.+)$/;
|
74
|
+
const parts = this.splitUnescaped(query);
|
75
|
+
return parts.every(part => filterPattern.test(part));
|
76
|
+
},
|
77
|
+
|
78
|
+
getSearchParam(crudState, defaultSearchColumn, apiMode = false) {
|
79
|
+
return new URLSearchParams({
|
80
|
+
page: crudState.currentPage || 1,
|
81
|
+
page_size: crudState.pageSize || 10,
|
82
|
+
filter: this._getFilterSearchParamValue(crudState, defaultSearchColumn, apiMode),
|
83
|
+
}).toString();
|
84
|
+
},
|
85
|
+
|
86
|
+
_getFilterSearchParamValue(crudState, defaultSearchColumn, apiMode = false) {
|
87
|
+
const filter = crudState.filter || "";
|
88
|
+
if (!apiMode) {
|
89
|
+
return filter;
|
90
|
+
}
|
91
|
+
return this.isValidFilterQuery(filter) ? filter : `${defaultSearchColumn}:like:%${filter}%`;
|
92
|
+
}
|
93
|
+
|
94
|
+
}
|
@@ -0,0 +1,23 @@
|
|
1
|
+
h1 {
|
2
|
+
color: var(--pico-primary)
|
3
|
+
}
|
4
|
+
|
5
|
+
h2 {
|
6
|
+
color: var(--pico-primary)
|
7
|
+
}
|
8
|
+
|
9
|
+
h3 {
|
10
|
+
color: var(--pico-primary)
|
11
|
+
}
|
12
|
+
|
13
|
+
h4 {
|
14
|
+
color: var(--pico-primary)
|
15
|
+
}
|
16
|
+
|
17
|
+
h5 {
|
18
|
+
color: var(--pico-primary)
|
19
|
+
}
|
20
|
+
|
21
|
+
h6 {
|
22
|
+
color: var(--pico-primary)
|
23
|
+
}
|
@@ -0,0 +1,44 @@
|
|
1
|
+
// Hamburger menu functionality
|
2
|
+
const hamburgerMenu = document.querySelector('.hamburger-menu');
|
3
|
+
const layoutContainer = document.querySelector('.layout-container');
|
4
|
+
|
5
|
+
hamburgerMenu.addEventListener('click', function() {
|
6
|
+
layoutContainer.classList.toggle('menu-active');
|
7
|
+
});
|
8
|
+
|
9
|
+
// Close menu when clicking outside
|
10
|
+
document.addEventListener('click', function(event) {
|
11
|
+
if (!layoutContainer.contains(event.target) && !hamburgerMenu.contains(event.target)) {
|
12
|
+
layoutContainer.classList.remove('menu-active');
|
13
|
+
}
|
14
|
+
});
|
15
|
+
|
16
|
+
// Theme switcher functionality
|
17
|
+
const themeSelect = document.getElementById('theme-select');
|
18
|
+
|
19
|
+
function setTheme(theme) {
|
20
|
+
document.documentElement.setAttribute('data-theme', theme);
|
21
|
+
localStorage.setItem('theme', theme);
|
22
|
+
}
|
23
|
+
|
24
|
+
function getSavedTheme() {
|
25
|
+
return localStorage.getItem('theme') || 'auto';
|
26
|
+
}
|
27
|
+
|
28
|
+
const savedTheme = getSavedTheme();
|
29
|
+
setTheme(savedTheme);
|
30
|
+
themeSelect.value = savedTheme;
|
31
|
+
|
32
|
+
themeSelect.addEventListener('change', (e) => {
|
33
|
+
setTheme(e.target.value);
|
34
|
+
});
|
35
|
+
|
36
|
+
function updateAutoTheme() {
|
37
|
+
if (getSavedTheme() === 'auto') {
|
38
|
+
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
39
|
+
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
updateAutoTheme();
|
44
|
+
window.matchMedia('(prefers-color-scheme: dark)').addListener(updateAutoTheme);
|
@@ -0,0 +1,102 @@
|
|
1
|
+
body {
|
2
|
+
overflow-x: hidden;
|
3
|
+
margin: 0;
|
4
|
+
padding: 0;
|
5
|
+
min-height: 100vh;
|
6
|
+
}
|
7
|
+
.layout-container {
|
8
|
+
display: flex;
|
9
|
+
min-height: 100vh;
|
10
|
+
}
|
11
|
+
aside {
|
12
|
+
position: fixed;
|
13
|
+
top: 0;
|
14
|
+
left: 0;
|
15
|
+
bottom: 0;
|
16
|
+
width: 300px;
|
17
|
+
padding: 1rem;
|
18
|
+
border-right: 1px solid var(--muted-border-color);
|
19
|
+
height: 100vh;
|
20
|
+
background: var(--background-color);
|
21
|
+
display: flex;
|
22
|
+
flex-direction: column;
|
23
|
+
}
|
24
|
+
aside nav {
|
25
|
+
flex-grow: 1; /* Pushes theme-switcher to the bottom */
|
26
|
+
overflow-y: auto;
|
27
|
+
overflow-x: hidden;
|
28
|
+
}
|
29
|
+
aside ul {
|
30
|
+
padding: 0;
|
31
|
+
list-style: none;
|
32
|
+
}
|
33
|
+
aside li {
|
34
|
+
margin-bottom: 0.5rem;
|
35
|
+
}
|
36
|
+
aside li > ul {
|
37
|
+
padding-left: 1rem;
|
38
|
+
}
|
39
|
+
.content-wrapper {
|
40
|
+
flex: 1;
|
41
|
+
overflow-x: hidden;
|
42
|
+
}
|
43
|
+
.content {
|
44
|
+
padding: 1rem;
|
45
|
+
}
|
46
|
+
.hamburger-menu {
|
47
|
+
display: none;
|
48
|
+
background: var(--pico-primary);
|
49
|
+
border: none;
|
50
|
+
font-size: 1.5rem;
|
51
|
+
cursor: pointer;
|
52
|
+
position: fixed;
|
53
|
+
top: 1rem;
|
54
|
+
left: 1rem;
|
55
|
+
z-index: 1001;
|
56
|
+
color: var(--contrast);
|
57
|
+
}
|
58
|
+
.theme-switcher {
|
59
|
+
display: flex;
|
60
|
+
align-items: center;
|
61
|
+
gap: 0.5rem;
|
62
|
+
margin-top: auto;
|
63
|
+
}
|
64
|
+
.active-link {
|
65
|
+
font-weight: bold;
|
66
|
+
color: var(--primary);
|
67
|
+
}
|
68
|
+
|
69
|
+
@media (max-width: 768px) {
|
70
|
+
.layout-container {
|
71
|
+
position: relative;
|
72
|
+
left: 0;
|
73
|
+
transition: left 0.3s ease;
|
74
|
+
}
|
75
|
+
.hamburger-menu {
|
76
|
+
display: block;
|
77
|
+
}
|
78
|
+
aside {
|
79
|
+
left: -300px;
|
80
|
+
top: 0;
|
81
|
+
bottom: 0;
|
82
|
+
z-index: 1000;
|
83
|
+
transition: left 0.3s ease;
|
84
|
+
padding-top: 4rem;
|
85
|
+
}
|
86
|
+
.layout-container.menu-active {
|
87
|
+
margin-left: 300px;
|
88
|
+
/*left: 300px;*/
|
89
|
+
}
|
90
|
+
.layout-container.menu-active aside {
|
91
|
+
left: 0;
|
92
|
+
}
|
93
|
+
.content {
|
94
|
+
padding-top: 4rem;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
98
|
+
@media (min-width: 769px) {
|
99
|
+
.content-wrapper {
|
100
|
+
margin-left: 300px;
|
101
|
+
}
|
102
|
+
}
|
@@ -4,31 +4,86 @@
|
|
4
4
|
<meta charset="utf-8">
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
6
6
|
<meta name="color-scheme" content="light dark">
|
7
|
-
|
7
|
+
<link rel="stylesheet" href="{{pico_css_path}}">
|
8
|
+
<link rel="stylesheet" href="/static/default/pico-style.css">
|
9
|
+
<link rel="stylesheet" href="/static/default/style.css">
|
10
|
+
{% for css_path in css_path_list -%}
|
8
11
|
<link rel="stylesheet" href="{{css_path}}">
|
9
12
|
{% endfor %}
|
10
|
-
<link rel="icon" href="{{
|
11
|
-
<title>{{
|
13
|
+
<link rel="icon" href="{{favicon_path}}" sizes="32x32" type="image/png">
|
14
|
+
<title>{{title}}</title>
|
12
15
|
</head>
|
13
16
|
<body>
|
14
|
-
<
|
15
|
-
|
16
|
-
|
17
|
-
<p>{{partials.subtitle}}</p>
|
17
|
+
<button class="hamburger-menu" aria-label="Menu">☰</button>
|
18
|
+
<div class="layout-container">
|
19
|
+
<aside>
|
18
20
|
<nav>
|
19
21
|
<ul>
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
{% for navigation in navigations %}
|
23
|
+
{% if navigation|attr("pages") %}
|
24
|
+
<li>
|
25
|
+
<details {% if navigation.active %}open{% endif %}>
|
26
|
+
<summary>{{navigation.caption}}</summary>
|
27
|
+
<ul>
|
28
|
+
{% for page in navigation.pages %}
|
29
|
+
<li><a href="{{page.url}}" class="{{ 'active-link' if page.active else '' }}">{{page.caption}}</a></li>
|
30
|
+
{% endfor %}
|
31
|
+
</ul>
|
32
|
+
</details>
|
33
|
+
</li>
|
34
|
+
{% else %}
|
35
|
+
<li><a href="{{navigation.url}}" class="{{ 'active-link' if navigation.active else '' }}">{{navigation.caption}}</a></li>
|
36
|
+
{% endif %}
|
37
|
+
{% endfor %}
|
25
38
|
</ul>
|
26
39
|
</nav>
|
27
|
-
|
28
|
-
|
29
|
-
|
40
|
+
<div class="theme-switcher">
|
41
|
+
<label for="theme-select">Theme:</label>
|
42
|
+
<select id="theme-select">
|
43
|
+
<option value="auto">🌗 Auto</option>
|
44
|
+
<option value="light">☀️ Light</option>
|
45
|
+
<option value="dark">🌙 Dark</option>
|
46
|
+
</select>
|
47
|
+
</div>
|
48
|
+
</aside>
|
49
|
+
<div class="content-wrapper">
|
50
|
+
<main class="content">
|
51
|
+
<header>
|
52
|
+
<hgroup>
|
53
|
+
<h1>{{title}}</h1>
|
54
|
+
<p>{{subtitle}}</p>
|
55
|
+
</hgroup>
|
56
|
+
{% if show_user_info %}
|
57
|
+
<nav>
|
58
|
+
<ul></ul>
|
59
|
+
<ul>
|
60
|
+
<li>
|
61
|
+
{% if current_user is none %}
|
62
|
+
<p>Hi Visitor <a href="/login">🔓</a></p>
|
63
|
+
{% elif current_user.is_guest %}
|
64
|
+
<p>Hi {{current_user.username}} <a href="/login">🔓</a></p>
|
65
|
+
{% else %}
|
66
|
+
<p>Hi {{current_user.username}} <a href="/logout">🔒</a></p>
|
67
|
+
{% endif %}
|
68
|
+
</li>
|
69
|
+
</ul>
|
70
|
+
</nav>
|
71
|
+
{% endif %}
|
72
|
+
</header>
|
73
|
+
<script src="/static/common/util.js"></script>
|
74
|
+
{{content}}
|
75
|
+
<footer>{{footer}}</footer>
|
76
|
+
</main>
|
77
|
+
</div>
|
78
|
+
</div>
|
79
|
+
{% for js_path in js_path_list -%}
|
80
|
+
<script src="{{js_path}}"></script>
|
81
|
+
{% endfor %}
|
82
|
+
<script src="/static/default/script.js"></script>
|
83
|
+
<script>
|
84
|
+
{% if should_refresh_session %}
|
85
|
+
UTIL.refreshAccessTokenPeriodically({{refresh_session_interval_seconds}});
|
86
|
+
{% endif %}
|
87
|
+
</script>
|
30
88
|
</body>
|
31
|
-
{% for js_path in partials.js_path_list -%}
|
32
|
-
<script src="{{js_path}}"></script>
|
33
|
-
{% endfor %}
|
34
89
|
</html>
|
@@ -90,6 +90,12 @@ class AuthUserResponse(UserResponse):
|
|
90
90
|
is_super_user: bool
|
91
91
|
is_guest: bool
|
92
92
|
|
93
|
+
def has_permission(self, permission_name: str):
|
94
|
+
return self.is_super_user or permission_name in self.permission_names
|
95
|
+
|
96
|
+
def has_role(self, role_name: str):
|
97
|
+
return self.is_super_user or role_name in self.role_names
|
98
|
+
|
93
99
|
|
94
100
|
class MultipleUserResponse(BaseModel):
|
95
101
|
data: list[UserResponse]
|
@@ -111,6 +117,9 @@ class UserTokenData(SQLModel):
|
|
111
117
|
class UserSessionResponse(SQLModel):
|
112
118
|
id: str
|
113
119
|
user_id: str
|
120
|
+
access_token: str
|
121
|
+
refresh_token: str
|
122
|
+
token_type: str
|
114
123
|
access_token_expired_at: datetime.datetime
|
115
124
|
refresh_token_expired_at: datetime.datetime
|
116
125
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
import time
|
2
|
+
|
3
|
+
from fastapi.testclient import TestClient
|
4
|
+
from my_app_name.config import APP_AUTH_SUPER_USER, APP_AUTH_SUPER_USER_PASSWORD
|
5
|
+
from my_app_name.main import app
|
6
|
+
|
7
|
+
|
8
|
+
def get_admin_access_token():
|
9
|
+
client = TestClient(app, base_url="http://localhost")
|
10
|
+
# Create new admin user session and check the response
|
11
|
+
session_response = client.post(
|
12
|
+
"/api/v1/user-sessions",
|
13
|
+
data={
|
14
|
+
"username": APP_AUTH_SUPER_USER,
|
15
|
+
"password": APP_AUTH_SUPER_USER_PASSWORD,
|
16
|
+
},
|
17
|
+
)
|
18
|
+
session_data = session_response.json()
|
19
|
+
return session_data.get("access_token")
|
@@ -0,0 +1,59 @@
|
|
1
|
+
from fastapi.testclient import TestClient
|
2
|
+
from my_app_name.main import app
|
3
|
+
from my_app_name.test._util.access_token import get_admin_access_token
|
4
|
+
|
5
|
+
|
6
|
+
def test_create_permission():
|
7
|
+
client = TestClient(app, base_url="http://localhost")
|
8
|
+
access_token = get_admin_access_token()
|
9
|
+
new_permission_data = {
|
10
|
+
"name": "new-permission",
|
11
|
+
"description": "new-permission-description",
|
12
|
+
}
|
13
|
+
response = client.post(
|
14
|
+
"/api/v1/permissions",
|
15
|
+
json=new_permission_data,
|
16
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
17
|
+
)
|
18
|
+
assert response.status_code == 200
|
19
|
+
response_data = response.json()
|
20
|
+
assert response_data.get("id") is not None
|
21
|
+
assert response_data.get("id") != ""
|
22
|
+
assert response_data.get("name") == "new-permission"
|
23
|
+
assert response_data.get("description") == "new-permission-description"
|
24
|
+
|
25
|
+
|
26
|
+
def test_create_permission_bulk():
|
27
|
+
client = TestClient(app, base_url="http://localhost")
|
28
|
+
access_token = get_admin_access_token()
|
29
|
+
new_first_permission_data = {
|
30
|
+
"name": "new-permission-bulk-1",
|
31
|
+
"description": "new-permission-bulk-description-1",
|
32
|
+
}
|
33
|
+
new_second_permission_data = {
|
34
|
+
"name": "new-permission-bulk-2",
|
35
|
+
"description": "new-permission-bulk-description-2",
|
36
|
+
}
|
37
|
+
new_permission_data = [new_first_permission_data, new_second_permission_data]
|
38
|
+
response = client.post(
|
39
|
+
"/api/v1/permissions/bulk",
|
40
|
+
json=new_permission_data,
|
41
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
42
|
+
)
|
43
|
+
assert response.status_code == 200
|
44
|
+
response_data = response.json()
|
45
|
+
assert len(response_data) == 2
|
46
|
+
# Id should not be empty
|
47
|
+
assert response_data[0] is not None
|
48
|
+
assert response_data[0] != ""
|
49
|
+
assert response_data[1] is not None
|
50
|
+
assert response_data[1] != ""
|
51
|
+
# Data should match
|
52
|
+
assert new_first_permission_data["name"] in [row["name"] for row in response_data]
|
53
|
+
assert new_second_permission_data["name"] in [row["name"] for row in response_data]
|
54
|
+
assert new_first_permission_data["description"] in [
|
55
|
+
row["description"] for row in response_data
|
56
|
+
]
|
57
|
+
assert new_second_permission_data["description"] in [
|
58
|
+
row["description"] for row in response_data
|
59
|
+
]
|
@@ -0,0 +1,68 @@
|
|
1
|
+
from fastapi.testclient import TestClient
|
2
|
+
from my_app_name.main import app
|
3
|
+
from my_app_name.test._util.access_token import get_admin_access_token
|
4
|
+
|
5
|
+
|
6
|
+
def test_delete_permission():
|
7
|
+
client = TestClient(app, base_url="http://localhost")
|
8
|
+
access_token = get_admin_access_token()
|
9
|
+
new_permission_data = {
|
10
|
+
"name": "to-be-deleted-permission",
|
11
|
+
"description": "to-be-deleted-permission-description",
|
12
|
+
}
|
13
|
+
insert_response = client.post(
|
14
|
+
"/api/v1/permissions",
|
15
|
+
json=new_permission_data,
|
16
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
17
|
+
)
|
18
|
+
assert insert_response.status_code == 200
|
19
|
+
id = insert_response.json().get("id")
|
20
|
+
# deleting
|
21
|
+
response = client.delete(
|
22
|
+
f"/api/v1/permissions/{id}", headers={"Authorization": f"Bearer {access_token}"}
|
23
|
+
)
|
24
|
+
assert response.status_code == 200
|
25
|
+
response_data = response.json()
|
26
|
+
assert response_data.get("id") == id
|
27
|
+
assert response_data.get("name") == "to-be-deleted-permission"
|
28
|
+
assert response_data.get("description") == "to-be-deleted-permission-description"
|
29
|
+
|
30
|
+
|
31
|
+
def test_delete_permission_bulk():
|
32
|
+
client = TestClient(app, base_url="http://localhost")
|
33
|
+
access_token = get_admin_access_token()
|
34
|
+
new_first_permission_data = {
|
35
|
+
"name": "to-be-deleted-permission-bulk-1",
|
36
|
+
"description": "to-be-deleted-permission-bulk-description-1",
|
37
|
+
}
|
38
|
+
new_second_permission_data = {
|
39
|
+
"name": "to-be-deleted-permission-bulk-2",
|
40
|
+
"description": "to-be-deleted-permission-bulk-description-2",
|
41
|
+
}
|
42
|
+
new_permission_data = [new_first_permission_data, new_second_permission_data]
|
43
|
+
insert_response = client.post(
|
44
|
+
"/api/v1/permissions/bulk",
|
45
|
+
json=new_permission_data,
|
46
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
47
|
+
)
|
48
|
+
assert insert_response.status_code == 200
|
49
|
+
ids = [row["id"] for row in insert_response.json()]
|
50
|
+
# deleting (use client.request since client.delete doesn't support json param)
|
51
|
+
response = client.request(
|
52
|
+
"DELETE",
|
53
|
+
f"/api/v1/permissions/bulk",
|
54
|
+
json=ids,
|
55
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
56
|
+
)
|
57
|
+
assert response.status_code == 200
|
58
|
+
response_data = response.json()
|
59
|
+
# Data should match
|
60
|
+
assert len([row["id"] for row in response_data if row["id"] in ids]) == 2
|
61
|
+
assert new_first_permission_data["name"] in [row["name"] for row in response_data]
|
62
|
+
assert new_second_permission_data["name"] in [row["name"] for row in response_data]
|
63
|
+
assert new_first_permission_data["description"] in [
|
64
|
+
row["description"] for row in response_data
|
65
|
+
]
|
66
|
+
assert new_second_permission_data["description"] in [
|
67
|
+
row["description"] for row in response_data
|
68
|
+
]
|
@@ -0,0 +1,71 @@
|
|
1
|
+
from fastapi.testclient import TestClient
|
2
|
+
from my_app_name.main import app
|
3
|
+
from my_app_name.test._util.access_token import get_admin_access_token
|
4
|
+
|
5
|
+
|
6
|
+
def test_read_permission_by_id():
|
7
|
+
client = TestClient(app, base_url="http://localhost")
|
8
|
+
access_token = get_admin_access_token()
|
9
|
+
new_permission_data = {
|
10
|
+
"name": "to-be-read-permission",
|
11
|
+
"description": "to-be-read-permission-description",
|
12
|
+
}
|
13
|
+
insert_response = client.post(
|
14
|
+
"/api/v1/permissions",
|
15
|
+
json=new_permission_data,
|
16
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
17
|
+
)
|
18
|
+
assert insert_response.status_code == 200
|
19
|
+
id = insert_response.json().get("id")
|
20
|
+
# fetching
|
21
|
+
response = client.get(
|
22
|
+
f"/api/v1/permissions/{id}", headers={"Authorization": f"Bearer {access_token}"}
|
23
|
+
)
|
24
|
+
assert response.status_code == 200
|
25
|
+
response_data = response.json()
|
26
|
+
assert response_data.get("id") == id
|
27
|
+
assert response_data.get("name") == "to-be-read-permission"
|
28
|
+
assert response_data.get("description") == "to-be-read-permission-description"
|
29
|
+
|
30
|
+
|
31
|
+
def test_read_permission_bulk():
|
32
|
+
client = TestClient(app, base_url="http://localhost")
|
33
|
+
access_token = get_admin_access_token()
|
34
|
+
new_first_permission_data = {
|
35
|
+
"name": "to-be-read-permission-bulk-1",
|
36
|
+
"description": "to-be-read-permission-bulk-description-1",
|
37
|
+
}
|
38
|
+
new_second_permission_data = {
|
39
|
+
"name": "to-be-read-permission-bulk-2",
|
40
|
+
"description": "to-be-read-permission-bulk-description-2",
|
41
|
+
}
|
42
|
+
new_permission_data = [new_first_permission_data, new_second_permission_data]
|
43
|
+
insert_response = client.post(
|
44
|
+
"/api/v1/permissions/bulk",
|
45
|
+
json=new_permission_data,
|
46
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
47
|
+
)
|
48
|
+
assert insert_response.status_code == 200
|
49
|
+
ids = [row["id"] for row in insert_response.json()]
|
50
|
+
# fetching
|
51
|
+
response = client.get(
|
52
|
+
f"/api/v1/permissions",
|
53
|
+
params={
|
54
|
+
"filter": "name:like:to-be-read-permission-bulk-%",
|
55
|
+
},
|
56
|
+
headers={"Authorization": f"Bearer {access_token}"},
|
57
|
+
)
|
58
|
+
assert response.status_code == 200
|
59
|
+
response_data_count = response.json()["count"]
|
60
|
+
assert response_data_count == 2
|
61
|
+
response_data = response.json()["data"]
|
62
|
+
# Data should match
|
63
|
+
assert len([row["id"] for row in response_data if row["id"] in ids]) == 2
|
64
|
+
assert new_first_permission_data["name"] in [row["name"] for row in response_data]
|
65
|
+
assert new_second_permission_data["name"] in [row["name"] for row in response_data]
|
66
|
+
assert new_first_permission_data["description"] in [
|
67
|
+
row["description"] for row in response_data
|
68
|
+
]
|
69
|
+
assert new_second_permission_data["description"] in [
|
70
|
+
row["description"] for row in response_data
|
71
|
+
]
|