work on permissions ui and base backend

This commit is contained in:
SvyatoslavArtymovych 2023-05-23 12:40:33 +03:00
parent 5c21cc8d0f
commit f60a9ad1c6
12 changed files with 147562 additions and 4 deletions

View File

@ -26,6 +26,7 @@ def create_app(environment="development"):
vote_blueprint,
approve_blueprint,
star_blueprint,
permissions_blueprint,
)
from app.models import (
User,
@ -57,6 +58,7 @@ def create_app(environment="development"):
app.register_blueprint(vote_blueprint)
app.register_blueprint(approve_blueprint)
app.register_blueprint(star_blueprint)
app.register_blueprint(permissions_blueprint)
# Set up flask login.
@login_manager.user_loader

View File

@ -13,3 +13,4 @@ from .interpretation import CreateInterpretationForm, EditInterpretationForm
from .comment import CreateCommentForm
from .vote import VoteForm
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm
from .permission import EditPermissionForm

10
app/forms/permission.py Normal file
View File

@ -0,0 +1,10 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, IntegerField
from wtforms.validators import DataRequired
class EditPermissionForm(FlaskForm):
book_id = IntegerField("Book ID", [DataRequired()])
user_id = IntegerField("User ID", [DataRequired()])
permissions = StringField("Permissions JSON", [DataRequired()])
submit = SubmitField("Edit")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,82 @@
<!-- prettier-ignore-->
<div id="access-level-modal" tabindex="-1" aria-hidden="true"
class="fixed top-0 left-0 right-0 z-[150] hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
<div class="relative w-full max-w-2xl max-h-full">
<!-- Modal content -->
<form action="{{ url_for('permission.set') }}" method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
{{ form_hidden_tag() }}
<input type="hidden" name="book_id" id="permission_modal_book_id"/>
<input type="hidden" name="user_id" id="permission_modal_user_id"/>
<input type="hidden" name="permissions" id="permissions_json"/>
<!-- Modal header -->
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
Access Level
</h3>
<button id="modalAddCloseButton" data-modal-hide="access-level-modal" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"> </path> </svg>
</button>
</div>
<!-- Modal body -->
<div class="p-6 space-y-6">
<div class="checkbox-tree">
<ul class="ml-4">
<li>
<div class="flex items-center space-x-2">
<input type="checkbox" data-root="true" data-access-to="book" data-access-to-id="{{ book.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
<span class="text-center dark:text-gray-300">{{ book.label }}</span>
</div>
{% for collection in book.last_version.children_collections %}
<ul class="ml-4">
<li>
<div class="flex items-center space-x-2">
<input type="checkbox" data-access-to="collection" data-access-to-id="{{ collection.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
<span class="text-center dark:text-gray-300">{{ collection.label }}</span>
</div>
{% for sub_collection in collection.children %}
<ul class="ml-4">
<li>
<div class="flex items-center space-x-2">
<input type="checkbox" data-access-to="sub_collection" data-access-to-id="{{ sub_collection.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
<span class="text-center dark:text-gray-300">{{ sub_collection.label }}</span>
</div>
{% for section in sub_collection.sections %}
<ul class="ml-4">
<li>
<div class="flex items-center space-x-2">
<input type="checkbox" data-access-to="section" data-access-to-id="{{ section.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
<span class="text-center dark:text-gray-300">{{ section.label }}</span>
</div>
</li>
</ul>
{% endfor %}
</li>
</ul>
{% endfor %}
{% for section in collection.sections %}
<ul class="ml-4">
<li>
<div class="flex items-center space-x-2">
<input type="checkbox" data-access-to="section" data-access-to-id="{{ section.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
<span class="text-center dark:text-gray-300">{{ section.label }}</span>
</div>
</li>
</ul>
{% endfor %}
</li>
</ul>
{% endfor %}
</li>
</ul>
</div>
</div>
<!-- Modal footer -->
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
<button name="submit" type="submit"
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Save</button>
</div>
</form>
</div>
</div>

View File

@ -2,6 +2,7 @@
{% extends 'base.html' %}
{% include 'book/add_contributor_modal.html' %}
{% include 'book/delete_book_modal.html' %}
{% include 'book/access_level_modal.html' %}
{% block content %}
<!-- Hide right_sidebar -->
@ -108,7 +109,7 @@
</form>
</td>
<td class="px-4 py-4 flex justify-center">
<button type="button" class="text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-1 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
<button type="button" data-book-id="{{book.id}}" data-user-id="{{contributor.user.id}}" data-modal-target="access-level-modal" data-modal-toggle="access-level-modal" class="edit-permissions-btn text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-lg text-sm px-5 py-1 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
Edit
</button>
</td>

View File

@ -8,3 +8,4 @@ from .section import bp as section_blueprint
from .vote import bp as vote_blueprint
from .approve import bp as approve_blueprint
from .star import bp as star_blueprint
from .permission import bp as permissions_blueprint

40
app/views/permission.py Normal file
View File

@ -0,0 +1,40 @@
import json
from flask import redirect, url_for, Blueprint, request, flash
from flask_login import current_user
from app import forms as f, models as m, db
from app.logger import log
bp = Blueprint("permission", __name__, "/permission")
@bp.route("/set", methods=["POST"])
def set():
form: f.EditPermissionForm = f.EditPermissionForm()
if form.validate_on_submit():
book_id = form.book_id.data
book: m.Book = db.session.get(m.Book, book_id)
if not book or book.is_deleted or book.owner != current_user:
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
flash("You are not owner of this book!", "danger")
return redirect(url_for("book.my_library"))
user_id = form.user_id.data
contributor: m.BookContributor = m.BookContributor.query.filter_by(
user_id=user_id, book_id=book_id
).first()
if not contributor:
log(
log.INFO,
"User: [%s] is not contributor of book: [%s]",
current_user,
book,
)
flash("User are not contributor of this book!", "danger")
return redirect(url_for("book.my_library"))
permissions = json.loads(form.permissions.data)
return {"status": "ok"}

81
src/checkBoxTree.ts Normal file
View File

@ -0,0 +1,81 @@
interface Permissions {
[key: string]: number[];
}
const updatePermissionsJSON = (
permissionsJSON: Permissions,
checkBoxTrees: Element,
) => {
const inputs: NodeListOf<HTMLInputElement> = checkBoxTrees.querySelectorAll(
'input[type=checkbox]',
);
inputs.forEach(element => {
const accessTo: string = `${element.getAttribute('data-access-to')}`;
const accessToId: number = parseInt(
element.getAttribute('data-access-to-id'),
);
const checked = element.checked;
if (checked && !permissionsJSON[accessTo].includes(accessToId)) {
permissionsJSON[accessTo].push(accessToId);
} else if (!checked && permissionsJSON[accessTo].includes(accessToId)) {
permissionsJSON[accessTo] = permissionsJSON[accessTo].filter(
el => el != accessToId,
);
}
});
};
const uncheckParentInputs = (checkbox: HTMLElement) => {
const parentLiElement: HTMLElement =
checkbox.parentElement.parentElement.parentElement.parentElement;
const parentInputElement: HTMLInputElement = parentLiElement.querySelector(
'input[type=checkbox]',
);
parentInputElement.checked = false;
if (parentInputElement.getAttribute('data-root') != 'true') {
uncheckParentInputs(parentInputElement);
}
};
const handleCheckboxClick = (checkbox: HTMLInputElement) => {
const parentLiElement: HTMLElement = checkbox.parentElement.parentElement;
const checked = checkbox.checked;
if (!checked) {
uncheckParentInputs(checkbox);
}
const checkboxes = parentLiElement.querySelectorAll('input[type=checkbox]');
checkboxes.forEach((checkbox: HTMLInputElement) => {
checkbox.checked = checked;
});
};
export const initCheckBoxTree = () => {
const permissionsJSON: Permissions = {
book: [],
sub_collection: [],
collection: [],
section: [],
};
const permissionsJsonInput: HTMLInputElement =
document.querySelector('#permissions_json');
const checkBoxTrees = document.querySelectorAll('.checkbox-tree');
checkBoxTrees.forEach((checkBoxTree: Element) => {
const checkboxes = checkBoxTree.querySelectorAll('input[type=checkbox]');
checkboxes.forEach((checkbox: HTMLInputElement) => {
checkbox.addEventListener('click', () => {
handleCheckboxClick(checkbox);
updatePermissionsJSON(permissionsJSON, checkBoxTree);
permissionsJsonInput.value = JSON.stringify(permissionsJSON);
});
});
});
};

View File

@ -20,6 +20,8 @@ import {renameSubCollection} from './renameSubCollection';
import {initQuillReadOnly} from './initQuillReadOnly';
import {initGoBack} from './tabGoBackBtn';
import {scroll} from './scroll';
import {initCheckBoxTree} from './checkBoxTree';
import {initPermissions} from './permissions';
initQuillReadOnly();
initBooks();
@ -43,3 +45,5 @@ deleteSubCollection();
renameSubCollection();
initGoBack();
scroll();
initCheckBoxTree();
initPermissions();

19
src/permissions.ts Normal file
View File

@ -0,0 +1,19 @@
export const initPermissions = () => {
const editBtns = document.querySelectorAll('.edit-permissions-btn');
editBtns.forEach(element => {
const bookIdInput: HTMLInputElement = document.querySelector(
'#permission_modal_book_id',
);
const userIdInput: HTMLInputElement = document.querySelector(
'#permission_modal_user_id',
);
element.addEventListener('click', () => {
const book_id = element.getAttribute('data-book-id');
const user_id = element.getAttribute('data-user-id');
bookIdInput.value = book_id;
userIdInput.value = user_id;
});
});
};