finish flask admin customization

This commit is contained in:
SvyatoslavArtymovych 2023-06-09 16:36:09 +03:00
parent 298096e126
commit d8a85b37fb
23 changed files with 153097 additions and 205 deletions

View File

@ -100,6 +100,7 @@ def create_app(environment="development"):
InterpretationView,
CommentView,
TagView,
BookContributorView,
)
app.config["FLASK_ADMIN_SWATCH"] = "Flatly"
@ -125,6 +126,12 @@ def create_app(environment="development"):
),
CommentView(m.Comment, db.session, name="Comment", endpoint="/comment_"),
TagView(m.Tag, db.session, name="Tag", endpoint="/tag_"),
BookContributorView(
m.BookContributor,
db.session,
name="BookContributor",
endpoint="/book_contributor_",
),
]:
admin.add_view(view)

View File

@ -8,3 +8,4 @@ from .section import SectionsView
from .interpretation import InterpretationView
from .comment import CommentView
from .tag import TagView
from .book_contributors import BookContributorView

View File

@ -8,8 +8,13 @@ from .protected_model_view import ProtectedModelView
class BooksView(ProtectedModelView):
column_list = ("id", "label", "about", "is_deleted", "user_id", "created_at")
column_labels = dict(user_id="Created by")
column_list = ("id", "label", "about", "is_deleted", "owner", "created_at")
form_edit_rules = (
"label",
"about",
"is_deleted",
"created_at",
)
@expose("/delete/", methods=("POST",))
def delete_view(self):

View File

@ -0,0 +1,116 @@
from flask_admin.base import expose
from flask_admin.helpers import (
get_redirect_target,
flash_errors,
)
from flask import redirect, flash, request
from flask_admin.babel import gettext
from wtforms import SelectField
from flask_admin.contrib.sqla.fields import QuerySelectField
from flask_admin.form import form
from flask_admin.model.template import EndpointLinkRowAction
from .protected_model_view import ProtectedModelView
from app import models as m, forms as f
from app.controllers.contributor import (
add_contributor_to_book,
delete_contributor_from_book,
)
from app.controllers.permission import set_access_level
from app.logger import log
class BookContributorViewCreateForm(form.Form):
book = QuerySelectField("Book", get_label="label")
user = QuerySelectField("User", get_label="username")
role = SelectField(
"Role",
choices=[
(role.value, role.name)
for role in m.BookContributor.Roles
if role.value > 0
],
)
class BookContributorView(ProtectedModelView):
column_list = ("id", "created_at", "role", "user", "book")
can_create = True
can_edit = False
column_extra_row_actions = [ # Add a new action button
EndpointLinkRowAction("glyphicon glyphicon-list-alt", ".edit_access_level"),
]
@expose("/edit_access_level/<string:id>", methods=("GET", "POST"))
def edit_access_level(self, id):
model: m.BookContributor = self.get_one((id,))
user = model.user
book = model.book
form: f.EditPermissionForm = f.EditPermissionForm()
if form.validate_on_submit():
set_access_level(form, book)
return self.render(
"admin/contributor_access_level.html", user=user, book=book, id=id
)
def create_form(self):
form = BookContributorViewCreateForm(request.form)
form.book.query = m.Book.query.filter_by(is_deleted=False)
form.user.query = m.User.query
return form
@expose("/new/", methods=("GET", "POST"))
def create_view(self):
return_url = get_redirect_target() or self.get_url(".index_view")
if not self.can_delete:
return redirect(return_url)
form: BookContributorViewCreateForm = self.create_form()
if (
form.user.data
and form.book.data
and form.book.data.user_id == form.user.data.id
):
flash("This user is owner of this book", "danger")
elif self.validate_form(form):
add_contributor_to_book(form, form.book.data.id, user_id=form.user.data.id)
return self.render("admin/model/create.html", form=form)
@expose("/delete/", methods=("POST",))
def delete_view(self):
return_url = get_redirect_target() or self.get_url(".index_view")
if not self.can_delete:
return redirect(return_url)
form = self.delete_form()
if self.validate_form(form):
# id is InputRequired()
id = form.id.data
model = self.get_one(id)
if model is None:
flash(gettext("Record does not exist."), "error")
return redirect(return_url)
try:
delete_contributor_from_book(form, model.book_id, user_id=model.user_id)
except Exception as e:
log(
log.EXCEPTION,
"AdminPanel delete contributor unexpected error [%s]",
str(e),
)
return redirect(return_url)
else:
flash_errors(form, message="Failed to delete record. %(error)s")
return redirect(return_url)

View File

@ -20,6 +20,13 @@ class CollectionsView(ProtectedModelView):
"is_deleted",
"created_at",
)
form_edit_rules = (
"label",
"about",
"position",
"is_deleted",
"created_at",
)
@expose("/delete/", methods=("POST",))
def delete_view(self):

View File

@ -12,6 +12,14 @@ from .protected_model_view import ProtectedModelView
class CommentView(ProtectedModelView):
column_list = (
"id",
"text",
"approved",
"edited",
"is_deleted",
"user",
"created_at",
)
form_edit_rules = (
"text",
"approved",
"edited",

View File

@ -15,6 +15,14 @@ class InterpretationView(ProtectedModelView):
"plain_text",
"approved",
"is_deleted",
"user",
"created_at",
)
form_edit_rules = (
"text",
"plain_text",
"approved",
"is_deleted",
"created_at",
)

View File

@ -12,6 +12,13 @@ from .protected_model_view import ProtectedModelView
class SectionsView(ProtectedModelView):
column_list = (
"id",
"label",
"position",
"is_deleted",
"user",
"created_at",
)
form_edit_rules = (
"label",
"position",
"is_deleted",

View File

@ -15,6 +15,11 @@ class TagView(ProtectedModelView):
"name",
"created_at",
)
form_edit_rules = (
"id",
"name",
"created_at",
)
@expose("/delete/", methods=("POST",))
def delete_view(self):

View File

@ -0,0 +1,96 @@
from flask import flash, redirect, url_for
from app import forms as f, models as m, db
from app.logger import log
def add_contributor_to_book(
form: f.AddContributorForm,
book_id: int,
selected_tab: str = "",
user_id: int = None,
):
if not user_id:
user_id = form.user_id.data
book_contributor = m.BookContributor.query.filter_by(
user_id=user_id, book_id=book_id
).first()
if book_contributor:
log(log.INFO, "Contributor: [%s] already exists", book_contributor)
flash("Already exists!", "danger")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
role = m.BookContributor.Roles(int(form.role.data))
contributor = m.BookContributor(user_id=user_id, book_id=book_id, role=role)
log(log.INFO, "New contributor [%s]", contributor)
contributor.save()
groups = (
db.session.query(m.AccessGroup)
.filter(
m.BookAccessGroups.book_id == book_id,
m.AccessGroup.id == m.BookAccessGroups.access_group_id,
m.AccessGroup.name == role.name.lower(),
)
.all()
)
for group in groups:
m.UserAccessGroups(user_id=user_id, access_group_id=group.id).save()
flash("Contributor was added!", "success")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
def delete_contributor_from_book(
form: f.DeleteContributorForm,
book_id: int,
selected_tab: str = "",
user_id: int = None,
):
if not user_id:
user_id = form.user_id.data
book_contributor = m.BookContributor.query.filter_by(
user_id=user_id, book_id=book_id
).first()
if not book_contributor:
log(
log.INFO,
"BookContributor does not exists user: [%s], book: [%s]",
user_id,
book_id,
)
flash("Does not exists!", "success")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
book: m.Book = db.session.get(m.Book, book_id)
user: m.User = db.session.get(m.User, user_id)
if book:
for access_group in book.access_groups:
access_group: m.AccessGroup
if user in access_group.users:
log(
log.INFO,
"Delete user [%s] from AccessGroup [%s]",
user,
access_group,
)
relationships_to_delete = m.UserAccessGroups.query.filter_by(
user_id=user_id, access_group_id=access_group.id
).all()
for relationship in relationships_to_delete:
db.session.delete(relationship)
log(log.INFO, "Delete BookContributor [%s]", book_contributor)
db.session.delete(book_contributor)
db.session.commit()
flash("Success!", "success")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)

View File

@ -0,0 +1,89 @@
import json
from flask_login import current_user
from flask import flash, redirect, url_for
from app.logger import log
from app import models as m, db, forms as f
from app.controllers.create_access_groups import (
create_editor_group,
create_moderator_group,
)
def set_access_level(form: f.EditPermissionForm, book: m.Book):
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"))
user: m.User = contributor.user
users_access_groups: list[m.AccessGroup] = list(
set(book.list_access_groups).intersection(user.access_groups)
)
if len(users_access_groups) > 1:
log(
log.WARNING,
"User: [%s] has more than 1 access group in book [%s]",
user,
book,
)
for users_access in users_access_groups:
users_access: m.AccessGroup
users_access.users.remove(user)
permissions_json = json.loads(form.permissions.data)
book_ids = permissions_json.get("book", [])
for book_id in book_ids:
entire_boot_access_group = m.AccessGroup.query.filter_by(
book_id=book_id, name=contributor.role.name.lower()
).first()
m.UserAccessGroups(
user_id=user.id, access_group_id=entire_boot_access_group.id
).save(False)
db.session.commit()
flash("Success!", "success")
return redirect(url_for("book.settings", book_id=book.id))
new_access_group = None
match contributor.role:
case m.BookContributor.Roles.EDITOR:
new_access_group = create_editor_group(book.id)
case m.BookContributor.Roles.MODERATOR:
new_access_group = create_moderator_group(book.id)
case _:
log(
log.CRITICAL,
"Unknown contributor's [%s] role: [%s]",
contributor,
contributor.role,
)
flash("Unknown contributor's role", "danger")
return redirect(url_for("book.settings", book_id=book.id))
m.UserAccessGroups(user_id=user.id, access_group_id=new_access_group.id).save(False)
collection_ids = permissions_json.get("collection", [])
for collection_id in collection_ids:
m.CollectionAccessGroups(
collection_id=collection_id, access_group_id=new_access_group.id
).save(False)
section_ids = permissions_json.get("section", [])
for section_id in section_ids:
m.SectionAccessGroups(
section_id=section_id, access_group_id=new_access_group.id
).save(False)
db.session.commit()
flash("Success!", "success")
return redirect(url_for("book.settings", book_id=book.id))

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,70 @@
{% extends 'admin/base.html' %}
{% block head_css %}
{{ super() }}
{% endblock %}
{% block body %}
<span class="hidden trigger-refreshAccessLevelTree"></span>
<div>
<div>Edit {{ user }} access level in book {{ book }}</div>
<form action="{{ url_for('.edit_access_level', id=id) }}" method="post">
{{ form_hidden_tag() }}
<input type="hidden" value="{{ book.id }}" name="book_id" id="permission_modal_book_id"/>
<input type="hidden" value="{{ user.id }}" name="user_id" id="permission_modal_user_id"/>
<input type="hidden" name="permissions" id="permissions_json"/>
<div>
<div class="checkbox-tree">
<ul>
<li>
<div>
<input
type="checkbox"
data-root="true"
data-access-to="book"
data-access-to-id="{{ book.id }}"
/>
<span>{{ book.label }}</span>
</div>
{%- for collection in book.last_version.children_collections recursive %}
<ul>
<li>
<div>
<input type="checkbox" data-access-to="collection" data-access-to-id="{{ collection.id }}"/>
<span>{{ collection.label }}</span>
</div>
{% if collection.active_children %}
{{ loop(collection.active_children)}}
{% else %}
{% for section in collection.sections %}
<ul>
<li>
<div>
<input type="checkbox" data-access-to="section" data-access-to-id="{{ section.id }}"/>
<span>{{ section.label }}</span>
</div>
</li>
</ul>
{% endfor %}
{% endif %}
</li>
</ul>
{%- endfor %}
</li>
</ul>
</div>
</div>
<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>
<script src="{{ url_for('static', filename='js/main.js') }}" type="text/javascript" defer></script>
{% endblock %}

View File

@ -41,47 +41,20 @@
</div>
{% if collection.active_children %}
{{ loop(collection.active_children)}}
{#
{% for sub_collection in collection.active_children %}
<ul class="ml-5">
<li>
<div class="flex items-center space-x-2">
<input type="checkbox" data-access-to="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 %}
#}
{% else %}
{% for section in collection.sections %}
<ul class="ml-5">
<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-purple-600 bg-purple-100 border-purple-400 rounded focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-purple-300 dark:border-purple-500" />
{% for section in collection.sections %}
<ul class="ml-5">
<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-purple-600 bg-purple-100 border-purple-400 rounded focus:ring-purple-500 dark:focus:ring-purple-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-purple-300 dark:border-purple-500" />
<span class="text-center dark:text-gray-300">{{ section.label }}</span>
</div>
</li>
</ul>
{% endfor %}
<span class="text-center dark:text-gray-300">{{ section.label }}</span>
</div>
</li>
</ul>
{% endfor %}
{% endif %}

View File

@ -88,7 +88,6 @@
{% if book.user_id == current_user.id %}
<div class="hidden px-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="permissions" role="tabpanel" aria-labelledby="permissions-tab">
<div class="px-5">
<div class="mb-3 relative overflow-x-auto shadow-md sm:rounded-lg">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">

View File

@ -12,6 +12,10 @@ from app.controllers import (
)
from app import models as m, db, forms as f
from app.controllers.require_permission import require_permission
from app.controllers.contributor import (
add_contributor_to_book,
delete_contributor_from_book,
)
from app.logger import log
from .bp import bp
@ -48,38 +52,8 @@ def add_contributor(book_id: int):
form = f.AddContributorForm()
selected_tab = "user_permissions"
if form.validate_on_submit():
user_id = form.user_id.data
book_contributor = m.BookContributor.query.filter_by(
user_id=user_id, book_id=book_id
).first()
if book_contributor:
log(log.INFO, "Contributor: [%s] already exists", book_contributor)
flash("Already exists!", "danger")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
role = m.BookContributor.Roles(int(form.role.data))
contributor = m.BookContributor(user_id=user_id, book_id=book_id, role=role)
log(log.INFO, "New contributor [%s]", contributor)
contributor.save()
groups = (
db.session.query(m.AccessGroup)
.filter(
m.BookAccessGroups.book_id == book_id,
m.AccessGroup.id == m.BookAccessGroups.access_group_id,
m.AccessGroup.name == role.name.lower(),
)
.all()
)
for group in groups:
m.UserAccessGroups(user_id=user_id, access_group_id=group.id).save()
flash("Contributor was added!", "success")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
response = add_contributor_to_book(form, book_id, selected_tab)
return response
else:
log(log.ERROR, "Book create errors: [%s]", form.errors)
for field, errors in form.errors.items():
@ -104,47 +78,8 @@ def delete_contributor(book_id: int):
selected_tab = "user_permissions"
if form.validate_on_submit():
user_id = int(form.user_id.data)
book_contributor = m.BookContributor.query.filter_by(
user_id=user_id, book_id=book_id
).first()
if not book_contributor:
log(
log.INFO,
"BookContributor does not exists user: [%s], book: [%s]",
user_id,
book_id,
)
flash("Does not exists!", "success")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
book: m.Book = db.session.get(m.Book, book_id)
user: m.User = db.session.get(m.User, user_id)
for access_group in book.access_groups:
access_group: m.AccessGroup
if user in access_group.users:
log(
log.INFO,
"Delete user [%s] from AccessGroup [%s]",
user,
access_group,
)
relationships_to_delete = m.UserAccessGroups.query.filter_by(
user_id=user_id, access_group_id=access_group.id
).all()
for relationship in relationships_to_delete:
db.session.delete(relationship)
log(log.INFO, "Delete BookContributor [%s]", book_contributor)
db.session.delete(book_contributor)
db.session.commit()
flash("Success!", "success")
return redirect(
url_for("book.settings", selected_tab=selected_tab, book_id=book_id)
)
response = delete_contributor_from_book(form, book_id, selected_tab)
return response
else:
log(log.ERROR, "Delete contributor errors: [%s]", form.errors)
for field, errors in form.errors.items():

View File

@ -1,14 +1,9 @@
import json
from flask import redirect, url_for, Blueprint, flash, request
from flask_login import current_user
from app import forms as f, models as m, db
from app.logger import log
from app.controllers.create_access_groups import (
create_editor_group,
create_moderator_group,
)
from app.controllers.permission import set_access_level
bp = Blueprint("permission", __name__, url_prefix="/permission")
@ -24,84 +19,8 @@ def set_permissions():
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"))
user: m.User = contributor.user
users_access_groups: list[m.AccessGroup] = list(
set(book.list_access_groups).intersection(user.access_groups)
)
if len(users_access_groups) > 1:
log(
log.WARNING,
"User: [%s] has more than 1 access group in book [%s]",
user,
book,
)
for users_access in users_access_groups:
users_access: m.AccessGroup
users_access.users.remove(user)
permissions_json = json.loads(form.permissions.data)
book_ids = permissions_json.get("book", [])
for book_id in book_ids:
entire_boot_access_group = m.AccessGroup.query.filter_by(
book_id=book_id, name=contributor.role.name.lower()
).first()
m.UserAccessGroups(
user_id=user.id, access_group_id=entire_boot_access_group.id
).save(False)
db.session.commit()
flash("Success!", "success")
return redirect(url_for("book.settings", book_id=book_id))
new_access_group = None
match contributor.role:
case m.BookContributor.Roles.EDITOR:
new_access_group = create_editor_group(book.id)
case m.BookContributor.Roles.MODERATOR:
new_access_group = create_moderator_group(book.id)
case _:
log(
log.CRITICAL,
"Unknown contributor's [%s] role: [%s]",
contributor,
contributor.role,
)
flash("Unknown contributor's role", "danger")
return redirect(url_for("book.settings", book_id=book_id))
m.UserAccessGroups(user_id=user.id, access_group_id=new_access_group.id).save(
False
)
collection_ids = permissions_json.get("collection", [])
for collection_id in collection_ids:
m.CollectionAccessGroups(
collection_id=collection_id, access_group_id=new_access_group.id
).save(False)
section_ids = permissions_json.get("section", [])
for section_id in section_ids:
m.SectionAccessGroups(
section_id=section_id, access_group_id=new_access_group.id
).save(False)
db.session.commit()
flash("Success!", "success")
return redirect(url_for("book.settings", book_id=book_id))
response = set_access_level(form, book)
return response
log(log.ERROR, "Errors edit contributor access level: [%s]", form.errors)
for field, errors in form.errors.items():

2
poetry.lock generated
View File

@ -998,7 +998,7 @@ multidict = ">=4.0"
[metadata]
lock-version = "1.1"
python-versions = "^3.11"
content-hash = "98f397700dc3b143f24e0b942e8d0ff5b50a6a1a8baacf69472a78856071dff8"
content-hash = "bfd495cb50e26c429a12b53d258d2df31dc074a154445eb4f94f48e8b136252f"
[metadata.files]
abnf = [

View File

@ -8,7 +8,6 @@ authors = ["denys <denysburimov@gmail.com>"]
python = "^3.11"
flask = "^2.2.3"
flask-migrate = "^4.0.4"
flask-wtf = "^1.1.1"
flask-mail = "^0.9.1"
flask-login = "^0.6.2"
python-dotenv = "^1.0.0"
@ -18,6 +17,8 @@ psycopg2-binary = "^2.9.5"
pydantic = "^1.10.7"
siwe = "^2.2.0"
flask-admin = "^1.6.1"
wtforms = "^3.0.1"
flask-wtf = "^1.1.1"
[tool.poetry.dev-dependencies]
pytest = "^7.1.1"

View File

@ -1,3 +0,0 @@
[pytest]
filterwarnings =
ignore::DeprecationWarning

View File

@ -32,4 +32,17 @@ export function initRefreshAccessLevelTree() {
refreshAccessLevelTree(userId, bookId);
});
});
const trigger = document.querySelector('.trigger-refreshAccessLevelTree');
if (trigger) {
const userIdInput: HTMLInputElement = document.querySelector(
'input[name=user_id]',
);
const bookIdInput: HTMLInputElement = document.querySelector(
'input[name=book_id]',
);
const userId = userIdInput.value;
const bookId = bookIdInput.value;
refreshAccessLevelTree(userId, bookId);
}
}

View File

@ -4,3 +4,9 @@ max-line-length = 120
;exclude = tests/*
;max-complexity = 10
exclude = .git,__pycache__,.venv/,migrations/,node_modules/
[pytest]
filterwarnings =
ignore::DeprecationWarning
addopts = -p no:warnings