mirror of
https://github.com/logos-co/open-law.git
synced 2025-02-10 13:57:17 +00:00
Merge pull request #69 from Simple2B/svyat/feat/flask_admin
Flask admin
This commit is contained in:
commit
0d1c0142f8
@ -5,6 +5,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from flask_migrate import Migrate
|
||||
from flask_admin import Admin
|
||||
|
||||
from app.logger import log
|
||||
|
||||
@ -28,7 +29,7 @@ def create_app(environment="development"):
|
||||
permissions_blueprint,
|
||||
search_blueprint,
|
||||
)
|
||||
from app.models import User, AnonymousUser, Permission
|
||||
from app import models as m
|
||||
|
||||
# Instantiate app.
|
||||
app = Flask(__name__)
|
||||
@ -60,11 +61,11 @@ def create_app(environment="development"):
|
||||
# Set up flask login.
|
||||
@login_manager.user_loader
|
||||
def get_user(id):
|
||||
return User.query.get(int(id))
|
||||
return m.User.query.get(int(id))
|
||||
|
||||
login_manager.login_view = "auth.login"
|
||||
login_manager.login_message_category = "info"
|
||||
login_manager.anonymous_user = AnonymousUser
|
||||
login_manager.anonymous_user = m.AnonymousUser
|
||||
|
||||
# Jinja globals
|
||||
from app.controllers.jinja_globals import (
|
||||
@ -75,8 +76,8 @@ def create_app(environment="development"):
|
||||
has_permission,
|
||||
)
|
||||
|
||||
app.jinja_env.globals["Access"] = Permission.Access
|
||||
app.jinja_env.globals["EntityType"] = Permission.Entity
|
||||
app.jinja_env.globals["Access"] = m.Permission.Access
|
||||
app.jinja_env.globals["EntityType"] = m.Permission.Entity
|
||||
|
||||
app.jinja_env.globals["form_hidden_tag"] = form_hidden_tag
|
||||
app.jinja_env.globals["display_inline_elements"] = display_inline_elements
|
||||
@ -89,4 +90,49 @@ def create_app(environment="development"):
|
||||
def handle_http_error(exc):
|
||||
return render_template("error.html", error=exc), exc.code
|
||||
|
||||
# flask admin
|
||||
from app.controllers.admin import (
|
||||
CustomAdminIndexView,
|
||||
UsersView,
|
||||
BooksView,
|
||||
CollectionsView,
|
||||
SectionsView,
|
||||
InterpretationView,
|
||||
CommentView,
|
||||
TagView,
|
||||
BookContributorView,
|
||||
)
|
||||
|
||||
app.config["FLASK_ADMIN_SWATCH"] = "Flatly"
|
||||
admin = Admin(
|
||||
app,
|
||||
name="Open Law Admin",
|
||||
template_mode="bootstrap3",
|
||||
index_view=CustomAdminIndexView(),
|
||||
)
|
||||
|
||||
for view in [
|
||||
UsersView(m.User, db.session, name="User", endpoint="/user_"),
|
||||
BooksView(m.Book, db.session, name="Book", endpoint="/book_"),
|
||||
CollectionsView(
|
||||
m.Collection, db.session, name="Collection", endpoint="/collection_"
|
||||
),
|
||||
SectionsView(m.Section, db.session, name="Section", endpoint="/section_"),
|
||||
InterpretationView(
|
||||
m.Interpretation,
|
||||
db.session,
|
||||
name="Interpretation",
|
||||
endpoint="/interpretation_",
|
||||
),
|
||||
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)
|
||||
|
||||
return app
|
||||
|
11
app/controllers/admin/__init__.py
Normal file
11
app/controllers/admin/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
# flake8: noqa F401
|
||||
from .custom_admin_index_view import CustomAdminIndexView
|
||||
from .protected_model_view import ProtectedModelView
|
||||
from .user import UsersView
|
||||
from .book import BooksView
|
||||
from .collection import CollectionsView
|
||||
from .section import SectionsView
|
||||
from .interpretation import InterpretationView
|
||||
from .comment import CommentView
|
||||
from .tag import TagView
|
||||
from .book_contributors import BookContributorView
|
50
app/controllers/admin/book.py
Normal file
50
app/controllers/admin/book.py
Normal file
@ -0,0 +1,50 @@
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import get_redirect_target, flash_errors
|
||||
from flask import redirect, flash
|
||||
from flask_admin.babel import gettext
|
||||
|
||||
from app.controllers.delete_nested_book_entities import delete_nested_book_entities
|
||||
from .protected_model_view import ProtectedModelView
|
||||
|
||||
|
||||
class BooksView(ProtectedModelView):
|
||||
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):
|
||||
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 = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
flash(gettext("Record does not exist."), "error")
|
||||
return redirect(return_url)
|
||||
|
||||
model.is_deleted = True
|
||||
delete_nested_book_entities(model)
|
||||
model.save()
|
||||
flash(
|
||||
gettext(
|
||||
"Book and nested entities were successfully deleted.",
|
||||
),
|
||||
"success",
|
||||
)
|
||||
return redirect(return_url)
|
||||
else:
|
||||
flash_errors(form, message="Failed to delete record. %(error)s")
|
||||
|
||||
return redirect(return_url)
|
115
app/controllers/admin/book_contributors.py
Normal file
115
app/controllers/admin/book_contributors.py
Normal file
@ -0,0 +1,115 @@
|
||||
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 = 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)
|
62
app/controllers/admin/collection.py
Normal file
62
app/controllers/admin/collection.py
Normal file
@ -0,0 +1,62 @@
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import get_redirect_target, flash_errors
|
||||
from flask import redirect, flash
|
||||
from flask_admin.babel import gettext
|
||||
|
||||
from app.controllers.delete_nested_book_entities import (
|
||||
delete_nested_collection_entities,
|
||||
)
|
||||
from .protected_model_view import ProtectedModelView
|
||||
|
||||
|
||||
class CollectionsView(ProtectedModelView):
|
||||
column_list = (
|
||||
"id",
|
||||
"label",
|
||||
"about",
|
||||
"is_root",
|
||||
"is_leaf",
|
||||
"position",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
)
|
||||
form_edit_rules = (
|
||||
"label",
|
||||
"about",
|
||||
"position",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
@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 = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
flash(gettext("Record does not exist."), "error")
|
||||
return redirect(return_url)
|
||||
|
||||
model.is_deleted = True
|
||||
delete_nested_collection_entities(model)
|
||||
model.save()
|
||||
flash(
|
||||
gettext(
|
||||
"Collection and nested entities were successfully deleted.",
|
||||
),
|
||||
"success",
|
||||
)
|
||||
return redirect(return_url)
|
||||
else:
|
||||
flash_errors(form, message="Failed to delete record. %(error)s")
|
||||
|
||||
return redirect(return_url)
|
61
app/controllers/admin/comment.py
Normal file
61
app/controllers/admin/comment.py
Normal file
@ -0,0 +1,61 @@
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import get_redirect_target, flash_errors
|
||||
from flask import redirect, flash
|
||||
from flask_admin.babel import gettext
|
||||
|
||||
from app.controllers.delete_nested_book_entities import (
|
||||
delete_nested_comment_entities,
|
||||
)
|
||||
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",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
@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 = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
flash(gettext("Record does not exist."), "error")
|
||||
return redirect(return_url)
|
||||
|
||||
model.is_deleted = True
|
||||
delete_nested_comment_entities(model)
|
||||
model.save()
|
||||
flash(
|
||||
gettext(
|
||||
"Section and nested entities were successfully deleted.",
|
||||
),
|
||||
"success",
|
||||
)
|
||||
return redirect(return_url)
|
||||
else:
|
||||
flash_errors(form, message="Failed to delete record. %(error)s")
|
||||
|
||||
return redirect(return_url)
|
16
app/controllers/admin/custom_admin_index_view.py
Normal file
16
app/controllers/admin/custom_admin_index_view.py
Normal file
@ -0,0 +1,16 @@
|
||||
from flask_admin import AdminIndexView
|
||||
from flask_login import current_user
|
||||
from flask import redirect, url_for
|
||||
|
||||
|
||||
class CustomAdminIndexView(AdminIndexView):
|
||||
def is_accessible(self):
|
||||
return current_user.is_super_user
|
||||
|
||||
def is_visible(self):
|
||||
# This view won't appear in the menu structure
|
||||
return False
|
||||
|
||||
def inaccessible_callback(self, name, **kwargs):
|
||||
# redirect to login page if user doesn't have access
|
||||
return redirect(url_for("main.index"))
|
60
app/controllers/admin/interpretation.py
Normal file
60
app/controllers/admin/interpretation.py
Normal file
@ -0,0 +1,60 @@
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import get_redirect_target, flash_errors
|
||||
from flask import redirect, flash
|
||||
from flask_admin.babel import gettext
|
||||
|
||||
from app.controllers.delete_nested_book_entities import (
|
||||
delete_nested_interpretation_entities,
|
||||
)
|
||||
from .protected_model_view import ProtectedModelView
|
||||
|
||||
|
||||
class InterpretationView(ProtectedModelView):
|
||||
column_list = (
|
||||
"id",
|
||||
"plain_text",
|
||||
"approved",
|
||||
"is_deleted",
|
||||
"user",
|
||||
"created_at",
|
||||
)
|
||||
form_edit_rules = (
|
||||
"text",
|
||||
"plain_text",
|
||||
"approved",
|
||||
"is_deleted",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
@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 = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
flash(gettext("Record does not exist."), "error")
|
||||
return redirect(return_url)
|
||||
|
||||
model.is_deleted = True
|
||||
delete_nested_interpretation_entities(model)
|
||||
model.save()
|
||||
flash(
|
||||
gettext(
|
||||
"Section and nested entities were successfully deleted.",
|
||||
),
|
||||
"success",
|
||||
)
|
||||
return redirect(return_url)
|
||||
else:
|
||||
flash_errors(form, message="Failed to delete record. %(error)s")
|
||||
|
||||
return redirect(return_url)
|
11
app/controllers/admin/protected_model_view.py
Normal file
11
app/controllers/admin/protected_model_view.py
Normal file
@ -0,0 +1,11 @@
|
||||
from flask_admin.contrib.sqla import ModelView
|
||||
from flask_login import current_user
|
||||
|
||||
|
||||
class ProtectedModelView(ModelView):
|
||||
action_disallowed_list = ["delete"]
|
||||
column_default_sort = "id"
|
||||
can_create = False
|
||||
|
||||
def is_accessible(self):
|
||||
return current_user.is_super_user
|
59
app/controllers/admin/section.py
Normal file
59
app/controllers/admin/section.py
Normal file
@ -0,0 +1,59 @@
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import get_redirect_target, flash_errors
|
||||
from flask import redirect, flash
|
||||
from flask_admin.babel import gettext
|
||||
|
||||
from app.controllers.delete_nested_book_entities import (
|
||||
delete_nested_section_entities,
|
||||
)
|
||||
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",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
@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 = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
flash(gettext("Record does not exist."), "error")
|
||||
return redirect(return_url)
|
||||
|
||||
model.is_deleted = True
|
||||
delete_nested_section_entities(model)
|
||||
model.save()
|
||||
flash(
|
||||
gettext(
|
||||
"Section and nested entities were successfully deleted.",
|
||||
),
|
||||
"success",
|
||||
)
|
||||
return redirect(return_url)
|
||||
else:
|
||||
flash_errors(form, message="Failed to delete record. %(error)s")
|
||||
|
||||
return redirect(return_url)
|
55
app/controllers/admin/tag.py
Normal file
55
app/controllers/admin/tag.py
Normal file
@ -0,0 +1,55 @@
|
||||
from flask_admin.base import expose
|
||||
from flask_admin.helpers import get_redirect_target, flash_errors
|
||||
from flask import redirect, flash
|
||||
from flask_admin.babel import gettext
|
||||
|
||||
from app.controllers.delete_nested_book_entities import (
|
||||
delete_nested_comment_entities,
|
||||
)
|
||||
from .protected_model_view import ProtectedModelView
|
||||
|
||||
|
||||
class TagView(ProtectedModelView):
|
||||
column_list = (
|
||||
"id",
|
||||
"name",
|
||||
"created_at",
|
||||
)
|
||||
form_edit_rules = (
|
||||
"id",
|
||||
"name",
|
||||
"created_at",
|
||||
)
|
||||
|
||||
@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 = form.id.data
|
||||
|
||||
model = self.get_one(id)
|
||||
|
||||
if model is None:
|
||||
flash(gettext("Record does not exist."), "error")
|
||||
return redirect(return_url)
|
||||
|
||||
model.is_deleted = True
|
||||
delete_nested_comment_entities(model)
|
||||
model.save()
|
||||
flash(
|
||||
gettext(
|
||||
"Section and nested entities were successfully deleted.",
|
||||
),
|
||||
"success",
|
||||
)
|
||||
return redirect(return_url)
|
||||
else:
|
||||
flash_errors(form, message="Failed to delete record. %(error)s")
|
||||
|
||||
return redirect(return_url)
|
11
app/controllers/admin/user.py
Normal file
11
app/controllers/admin/user.py
Normal file
@ -0,0 +1,11 @@
|
||||
from .protected_model_view import ProtectedModelView
|
||||
|
||||
|
||||
class UsersView(ProtectedModelView):
|
||||
column_list = (
|
||||
"id",
|
||||
"username",
|
||||
"is_activated",
|
||||
"wallet_id",
|
||||
"is_super_user",
|
||||
)
|
96
app/controllers/contributor.py
Normal file
96
app/controllers/contributor.py
Normal 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)
|
||||
)
|
89
app/controllers/permission.py
Normal file
89
app/controllers/permission.py
Normal 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))
|
@ -11,7 +11,6 @@ class Comment(BaseModel):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
text = db.Column(db.Text, unique=False, nullable=False)
|
||||
approved = db.Column(db.Boolean, default=False)
|
||||
marked = db.Column(db.Boolean, default=False)
|
||||
edited = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Foreign keys
|
||||
|
@ -10,7 +10,6 @@ class Interpretation(BaseModel):
|
||||
text = db.Column(db.Text, unique=False, nullable=False)
|
||||
plain_text = db.Column(db.Text, unique=False)
|
||||
approved = db.Column(db.Boolean, default=False)
|
||||
marked = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Foreign keys
|
||||
user_id = db.Column(db.ForeignKey("users.id"))
|
||||
|
@ -18,7 +18,6 @@ class Section(BaseModel):
|
||||
collection_id = db.Column(db.ForeignKey("collections.id"))
|
||||
user_id = db.Column(db.ForeignKey("users.id"))
|
||||
version_id = db.Column(db.ForeignKey("book_versions.id"))
|
||||
selected_interpretation_id = db.Column(db.Integer, nullable=True)
|
||||
position = db.Column(db.Integer, default=-1, nullable=True)
|
||||
|
||||
# Relationships
|
||||
|
@ -23,6 +23,7 @@ class User(BaseModel, UserMixin):
|
||||
is_activated = db.Column(db.Boolean, default=False)
|
||||
wallet_id = db.Column(db.String(64), nullable=True)
|
||||
avatar_img = db.Column(db.Text, nullable=True)
|
||||
is_super_user = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Relationships
|
||||
access_groups = db.relationship(
|
||||
|
File diff suppressed because one or more lines are too long
70
app/templates/admin/contributor_access_level.html
Normal file
70
app/templates/admin/contributor_access_level.html
Normal 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 %}
|
@ -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 %}
|
||||
|
@ -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">
|
||||
|
@ -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():
|
||||
|
@ -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():
|
||||
|
@ -0,0 +1,39 @@
|
||||
"""remove selected_interpretation_id
|
||||
|
||||
Revision ID: 776fd9579f1f
|
||||
Revises: 7c8a5aefe801
|
||||
Create Date: 2023-05-26 14:38:44.858319
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "776fd9579f1f"
|
||||
down_revision = "96995454b90d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("sections", schema=None) as batch_op:
|
||||
batch_op.drop_column("selected_interpretation_id")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("sections", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"selected_interpretation_id",
|
||||
sa.INTEGER(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
42
migrations/versions/7c8a5aefe801_remove_mark_fields.py
Normal file
42
migrations/versions/7c8a5aefe801_remove_mark_fields.py
Normal file
@ -0,0 +1,42 @@
|
||||
"""remove mark fields
|
||||
|
||||
Revision ID: 7c8a5aefe801
|
||||
Revises: 776fd9579f1f
|
||||
Create Date: 2023-05-25 15:44:06.072076
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7c8a5aefe801"
|
||||
down_revision = "776fd9579f1f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
||||
batch_op.drop_column("marked")
|
||||
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.drop_column("marked")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("marked", sa.BOOLEAN(), autoincrement=False, nullable=True)
|
||||
)
|
||||
|
||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("marked", sa.BOOLEAN(), autoincrement=False, nullable=True)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
32
migrations/versions/a41f004cad1a_user_is_super_user.py
Normal file
32
migrations/versions/a41f004cad1a_user_is_super_user.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""user.is_super_user
|
||||
|
||||
Revision ID: a41f004cad1a
|
||||
Revises: 7c8a5aefe801
|
||||
Create Date: 2023-05-25 14:31:14.046066
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a41f004cad1a"
|
||||
down_revision = "7c8a5aefe801"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("is_super_user", sa.Boolean(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.drop_column("is_super_user")
|
||||
|
||||
# ### end Alembic commands ###
|
2500
poetry.lock
generated
2500
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@ -2,13 +2,12 @@
|
||||
name = "flask.app"
|
||||
version = "0.1.0"
|
||||
description = ""
|
||||
authors = ["denys <denysburimov@gmail.com>"]
|
||||
authors = ["Simple2b https://www.simple2b.net/"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
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"
|
||||
@ -17,6 +16,9 @@ email-validator = "^1.3.1"
|
||||
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"
|
||||
|
@ -1,3 +0,0 @@
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -119,14 +119,12 @@ def create_dummy_data():
|
||||
text="Dummy Interpretation 2 About",
|
||||
section_id=section_2_1_1.id,
|
||||
user_id=user.id,
|
||||
marked=True,
|
||||
).save()
|
||||
|
||||
interpretation_3 = m.Interpretation(
|
||||
text="Dummy Interpretation 3 About",
|
||||
section_id=section_2_1_2.id,
|
||||
user_id=user.id,
|
||||
marked=True,
|
||||
).save()
|
||||
|
||||
m.Interpretation(
|
||||
@ -163,7 +161,6 @@ def create_dummy_data():
|
||||
text="Dummy Comment 1.2 Text",
|
||||
user_id=user_2.id,
|
||||
parent_id=comment_1.id,
|
||||
marked=True,
|
||||
interpretation_id=interpretation_2.id,
|
||||
).save()
|
||||
|
||||
@ -182,7 +179,6 @@ def create_dummy_data():
|
||||
comment_3_1 = m.Comment(
|
||||
text="Dummy Comment 3.1 Text",
|
||||
user_id=user.id,
|
||||
marked=True,
|
||||
parent_id=comment_3.id,
|
||||
interpretation_id=interpretation_2.id,
|
||||
).save()
|
||||
|
@ -161,7 +161,6 @@ def test_dummy_data(runner: FlaskCliRunner):
|
||||
).first()
|
||||
|
||||
assert interpretation_2
|
||||
assert interpretation_2.marked
|
||||
assert interpretation_2.user == user
|
||||
assert interpretation_2.section == section_2_1_1
|
||||
|
||||
@ -170,7 +169,6 @@ def test_dummy_data(runner: FlaskCliRunner):
|
||||
).first()
|
||||
|
||||
assert interpretation_3
|
||||
assert interpretation_3.marked
|
||||
assert interpretation_3.user == user
|
||||
assert interpretation_3.section == section_2_1_2
|
||||
|
||||
@ -211,7 +209,6 @@ def test_dummy_data(runner: FlaskCliRunner):
|
||||
assert comment_1_1 in comment_1.children
|
||||
assert comment_1_2.parent == comment_1
|
||||
assert comment_1_2.parent == comment_1
|
||||
assert comment_1_2.marked
|
||||
|
||||
comment_2: m.Comment = m.Comment.query.filter_by(
|
||||
text="Dummy Comment 2 Text"
|
||||
@ -242,7 +239,6 @@ def test_dummy_data(runner: FlaskCliRunner):
|
||||
assert comment_3_1 in comment_3.children
|
||||
assert comment_3_2 in comment_3.children
|
||||
assert comment_3_3 in comment_3.children
|
||||
assert comment_3_1.marked
|
||||
assert comment_3_2.approved
|
||||
|
||||
assert comment_1 in interpretation_2.comments
|
||||
|
Loading…
x
Reference in New Issue
Block a user