mirror of https://github.com/logos-co/open-law.git
Merge pull request #107 from Simple2B/svyat/feat/user_permissions
Svyat/feat/user permissions
This commit is contained in:
commit
a4797e5ddb
|
@ -14,6 +14,7 @@
|
||||||
"backref",
|
"backref",
|
||||||
"bookname",
|
"bookname",
|
||||||
"Btns",
|
"Btns",
|
||||||
|
"CUDA",
|
||||||
"CLEANR",
|
"CLEANR",
|
||||||
"Divs",
|
"Divs",
|
||||||
"flowbite",
|
"flowbite",
|
||||||
|
|
|
@ -25,12 +25,10 @@ def create_app(environment="development"):
|
||||||
vote_blueprint,
|
vote_blueprint,
|
||||||
approve_blueprint,
|
approve_blueprint,
|
||||||
star_blueprint,
|
star_blueprint,
|
||||||
|
permissions_blueprint,
|
||||||
search_blueprint,
|
search_blueprint,
|
||||||
)
|
)
|
||||||
from app.models import (
|
from app.models import User, AnonymousUser, Permission
|
||||||
User,
|
|
||||||
AnonymousUser,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Instantiate app.
|
# Instantiate app.
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
@ -56,6 +54,7 @@ def create_app(environment="development"):
|
||||||
app.register_blueprint(vote_blueprint)
|
app.register_blueprint(vote_blueprint)
|
||||||
app.register_blueprint(approve_blueprint)
|
app.register_blueprint(approve_blueprint)
|
||||||
app.register_blueprint(star_blueprint)
|
app.register_blueprint(star_blueprint)
|
||||||
|
app.register_blueprint(permissions_blueprint)
|
||||||
app.register_blueprint(search_blueprint)
|
app.register_blueprint(search_blueprint)
|
||||||
|
|
||||||
# Set up flask login.
|
# Set up flask login.
|
||||||
|
@ -73,12 +72,17 @@ def create_app(environment="development"):
|
||||||
display_inline_elements,
|
display_inline_elements,
|
||||||
build_qa_url_using_interpretation,
|
build_qa_url_using_interpretation,
|
||||||
recursive_render,
|
recursive_render,
|
||||||
|
has_permission,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app.jinja_env.globals["Access"] = Permission.Access
|
||||||
|
app.jinja_env.globals["EntityType"] = Permission.Entity
|
||||||
|
|
||||||
app.jinja_env.globals["form_hidden_tag"] = form_hidden_tag
|
app.jinja_env.globals["form_hidden_tag"] = form_hidden_tag
|
||||||
app.jinja_env.globals["display_inline_elements"] = display_inline_elements
|
app.jinja_env.globals["display_inline_elements"] = display_inline_elements
|
||||||
app.jinja_env.globals["build_qa_url"] = build_qa_url_using_interpretation
|
app.jinja_env.globals["build_qa_url"] = build_qa_url_using_interpretation
|
||||||
app.jinja_env.globals["recursive_render"] = recursive_render
|
app.jinja_env.globals["recursive_render"] = recursive_render
|
||||||
|
app.jinja_env.globals["has_permission"] = has_permission
|
||||||
|
|
||||||
# Error handlers.
|
# Error handlers.
|
||||||
@app.errorhandler(HTTPException)
|
@app.errorhandler(HTTPException)
|
||||||
|
|
|
@ -40,9 +40,9 @@ def book_validator() -> Response | None:
|
||||||
book_id = request_args.get("book_id")
|
book_id = request_args.get("book_id")
|
||||||
if book_id:
|
if book_id:
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
if not book or book.is_deleted or book.owner != current_user:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
||||||
flash("You are not owner of this book!", "danger")
|
flash("Book not found!", "danger")
|
||||||
return redirect(url_for("book.my_library"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
collection_id = request_args.get("collection_id")
|
collection_id = request_args.get("collection_id")
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
from app import models as m
|
||||||
|
from app.logger import log
|
||||||
|
from .get_or_create_permission import get_or_create_permission
|
||||||
|
|
||||||
|
|
||||||
|
def create_moderator_group(book_id: int):
|
||||||
|
log(log.INFO, "Create moderator access group")
|
||||||
|
group: m.AccessGroup = m.AccessGroup(name="moderator", book_id=book_id).save()
|
||||||
|
permissions = []
|
||||||
|
|
||||||
|
comment_DA = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||||
|
entity_type=m.Permission.Entity.COMMENT,
|
||||||
|
)
|
||||||
|
permissions.append(comment_DA)
|
||||||
|
|
||||||
|
interpretation_DA = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||||
|
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||||
|
)
|
||||||
|
permissions.append(interpretation_DA)
|
||||||
|
|
||||||
|
for permission in permissions:
|
||||||
|
log(log.INFO, "Add permission [%d] to group[%d]", permission.id, group.id)
|
||||||
|
m.PermissionAccessGroups(
|
||||||
|
permission_id=permission.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
def create_editor_group(book_id: int):
|
||||||
|
log(log.INFO, "Create editor access group")
|
||||||
|
group: m.AccessGroup = m.AccessGroup(name="editor", book_id=book_id).save()
|
||||||
|
permissions = []
|
||||||
|
|
||||||
|
comment_DA = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||||
|
entity_type=m.Permission.Entity.COMMENT,
|
||||||
|
)
|
||||||
|
permissions.append(comment_DA)
|
||||||
|
|
||||||
|
interpretation_DA = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||||
|
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||||
|
)
|
||||||
|
permissions.append(interpretation_DA)
|
||||||
|
|
||||||
|
section_CUD = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.C | m.Permission.Access.U | m.Permission.Access.D,
|
||||||
|
entity_type=m.Permission.Entity.SECTION,
|
||||||
|
)
|
||||||
|
permissions.append(section_CUD)
|
||||||
|
|
||||||
|
collection_CUD = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.C | m.Permission.Access.U | m.Permission.Access.D,
|
||||||
|
entity_type=m.Permission.Entity.COLLECTION,
|
||||||
|
)
|
||||||
|
permissions.append(collection_CUD)
|
||||||
|
|
||||||
|
book_U = get_or_create_permission(
|
||||||
|
access=m.Permission.Access.U,
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
)
|
||||||
|
permissions.append(book_U)
|
||||||
|
|
||||||
|
for permission in permissions:
|
||||||
|
log(log.INFO, "Add permission [%d] to group[%d]", permission.id, group.id)
|
||||||
|
m.PermissionAccessGroups(
|
||||||
|
permission_id=permission.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
|
return group
|
|
@ -0,0 +1,14 @@
|
||||||
|
from app import models as m
|
||||||
|
from app.logger import log
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_permission(access: int, entity_type: m.Permission.Entity):
|
||||||
|
permission: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access, entity_type=entity_type
|
||||||
|
).first()
|
||||||
|
if not permission:
|
||||||
|
log(log.INFO, "Create permission [%d] for entity [%s]", access, entity_type)
|
||||||
|
permission: m.Permission = m.Permission(
|
||||||
|
access=access, entity_type=entity_type
|
||||||
|
).save()
|
||||||
|
return permission
|
|
@ -3,6 +3,7 @@ import re
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from flask import url_for, render_template
|
from flask import url_for, render_template
|
||||||
|
from flask_login import current_user
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
from app import models as m
|
from app import models as m
|
||||||
|
@ -76,9 +77,58 @@ def build_qa_url_using_interpretation(interpretation: m.Interpretation):
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
# Using: {{ recursive_render("template.html", collection=collection, book=book) }}
|
||||||
def recursive_render(template: str, collection: m.Collection, book: m.Book):
|
def recursive_render(template: str, collection: m.Collection, book: m.Book):
|
||||||
return render_template(
|
return render_template(
|
||||||
template,
|
template,
|
||||||
collection=collection,
|
collection=collection,
|
||||||
book=book,
|
book=book,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Using: {{ has_permission(entity=book, required_permissions=[Access.create]) }}
|
||||||
|
def has_permission(
|
||||||
|
entity: m.Book | m.Collection | m.Section | m.Interpretation,
|
||||||
|
required_permissions: m.Permission.Access | list[m.Permission.Access],
|
||||||
|
entity_type: m.Permission.Entity = None,
|
||||||
|
) -> bool:
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# check if user is owner of book
|
||||||
|
match type(entity):
|
||||||
|
case m.Book:
|
||||||
|
if entity.user_id == current_user.id:
|
||||||
|
return True
|
||||||
|
case m.Collection | m.Section:
|
||||||
|
if entity.version.book.user_id == current_user.id:
|
||||||
|
return True
|
||||||
|
case m.Interpretation:
|
||||||
|
if entity.book.user_id == current_user.id:
|
||||||
|
return True
|
||||||
|
case _:
|
||||||
|
...
|
||||||
|
|
||||||
|
if type(required_permissions) == m.Permission.Access:
|
||||||
|
required_permissions = [required_permissions]
|
||||||
|
|
||||||
|
access_groups: list[m.AccessGroup] = list(
|
||||||
|
set(entity.access_groups).intersection(current_user.access_groups)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not access_groups:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not entity_type:
|
||||||
|
entity_type = m.Permission.Entity[type(entity).__name__.upper()]
|
||||||
|
|
||||||
|
for access_group in access_groups:
|
||||||
|
for permission in access_group.permissions:
|
||||||
|
permission: m.Permission
|
||||||
|
if permission.entity_type != entity_type:
|
||||||
|
continue
|
||||||
|
for required_permission in required_permissions:
|
||||||
|
if permission.access & required_permission:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
from flask_login import current_user
|
||||||
|
from flask import flash, redirect, url_for, request, make_response
|
||||||
|
import functools
|
||||||
|
|
||||||
|
from app import models as m, db
|
||||||
|
from app.logger import log
|
||||||
|
|
||||||
|
|
||||||
|
def check_permissions(
|
||||||
|
entity_type: m.Permission.Entity,
|
||||||
|
access: list[m.Permission.Access],
|
||||||
|
entities: list[dict],
|
||||||
|
):
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
flash("You do not have permission", "danger")
|
||||||
|
return make_response(redirect(url_for("home.get_all")))
|
||||||
|
|
||||||
|
request_args = (
|
||||||
|
{**request.view_args, **request.args} if request.view_args else {**request.args}
|
||||||
|
)
|
||||||
|
entity = None
|
||||||
|
for model in entities:
|
||||||
|
entity_id_field = (model.__name__ + "_id").lower()
|
||||||
|
entity_id = request_args.get(entity_id_field)
|
||||||
|
entity: m.Book | m.Collection | m.Section | m.Interpretation | m.Comment = (
|
||||||
|
db.session.get(model, entity_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
if entity is None:
|
||||||
|
log(log.INFO, "No entity [%s] found", entities)
|
||||||
|
flash("You do not have permission", "danger")
|
||||||
|
return make_response(redirect(url_for("home.get_all")))
|
||||||
|
|
||||||
|
book_id = request_args.get("book_id")
|
||||||
|
if book_id:
|
||||||
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
|
if book and book.user_id == current_user.id:
|
||||||
|
# user has access because he is book owner
|
||||||
|
log(log.INFO, "User [%s] is book owner [%s]", current_user, book)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if type(entity) == m.Comment:
|
||||||
|
log(log.INFO, "Entity is Comment. Replace it by entity.interpretation")
|
||||||
|
entity = entity.interpretation
|
||||||
|
|
||||||
|
if not entity or not entity.access_groups:
|
||||||
|
log(
|
||||||
|
log.INFO,
|
||||||
|
"Entity [%s] of entity.access_groups [%s] not found",
|
||||||
|
access,
|
||||||
|
entity,
|
||||||
|
)
|
||||||
|
flash("You do not have permission", "warning")
|
||||||
|
return make_response(redirect(url_for("home.get_all")))
|
||||||
|
|
||||||
|
# check if user is not owner of book
|
||||||
|
if not book_id and entity.access_groups[0].book.user_id == current_user.id:
|
||||||
|
# user has access because he is book owner
|
||||||
|
log(
|
||||||
|
log.INFO,
|
||||||
|
"User [%s] is book owner [%s]",
|
||||||
|
current_user,
|
||||||
|
entity.access_groups[0].book,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
access_group_query = (
|
||||||
|
m.AccessGroup.query.join(
|
||||||
|
m.PermissionAccessGroups,
|
||||||
|
m.PermissionAccessGroups.access_group_id == m.AccessGroup.id,
|
||||||
|
)
|
||||||
|
.join(m.Permission, m.PermissionAccessGroups.permission_id == m.Permission.id)
|
||||||
|
.filter(
|
||||||
|
m.AccessGroup.id.in_(
|
||||||
|
[access_group.id for access_group in entity.access_groups]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.filter(m.AccessGroup.users.any(id=current_user.id))
|
||||||
|
.filter(m.Permission.entity_type == entity_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
for access in access:
|
||||||
|
access_group_query = access_group_query.filter(
|
||||||
|
m.Permission.access.op("&")(access) > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
access_groups = access_group_query.all()
|
||||||
|
|
||||||
|
if access_groups:
|
||||||
|
log(
|
||||||
|
log.INFO,
|
||||||
|
"User [%s] has permission to [%s] [%s]",
|
||||||
|
access,
|
||||||
|
current_user,
|
||||||
|
entity,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
log(
|
||||||
|
log.INFO,
|
||||||
|
"User [%s] dont have permission to [%s] [%s]",
|
||||||
|
current_user,
|
||||||
|
access,
|
||||||
|
entity,
|
||||||
|
)
|
||||||
|
flash("You do not have permission", "danger")
|
||||||
|
return make_response(redirect(url_for("home.get_all")))
|
||||||
|
|
||||||
|
|
||||||
|
def require_permission(
|
||||||
|
entity_type: m.Permission.Entity,
|
||||||
|
access: list[m.Permission.Access],
|
||||||
|
entities: list[dict],
|
||||||
|
):
|
||||||
|
def decorator(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def permission_checker(*args, **kwargs):
|
||||||
|
if response := check_permissions(
|
||||||
|
entity_type=entity_type,
|
||||||
|
access=access,
|
||||||
|
entities=entities,
|
||||||
|
):
|
||||||
|
return response
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return permission_checker
|
||||||
|
|
||||||
|
return decorator
|
|
@ -17,3 +17,4 @@ from .interpretation import (
|
||||||
from .comment import CreateCommentForm
|
from .comment import CreateCommentForm
|
||||||
from .vote import VoteForm
|
from .vote import VoteForm
|
||||||
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm
|
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm
|
||||||
|
from .permission import EditPermissionForm
|
||||||
|
|
|
@ -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")
|
|
@ -13,5 +13,15 @@ from .interpretation_vote import InterpretationVote
|
||||||
from .tag import Tag
|
from .tag import Tag
|
||||||
from .interpretation_tag import InterpretationTag
|
from .interpretation_tag import InterpretationTag
|
||||||
from .comment_tag import CommentTags
|
from .comment_tag import CommentTags
|
||||||
|
from .permission import (
|
||||||
|
Permission,
|
||||||
|
AccessGroup,
|
||||||
|
UserAccessGroups,
|
||||||
|
PermissionAccessGroups,
|
||||||
|
BookAccessGroups,
|
||||||
|
CollectionAccessGroups,
|
||||||
|
SectionAccessGroups,
|
||||||
|
InterpretationAccessGroups,
|
||||||
|
)
|
||||||
from .book_tag import BookTags
|
from .book_tag import BookTags
|
||||||
from .section_tag import SectionTag
|
from .section_tag import SectionTag
|
||||||
|
|
|
@ -18,7 +18,14 @@ class Book(BaseModel):
|
||||||
owner = db.relationship("User", viewonly=True)
|
owner = db.relationship("User", viewonly=True)
|
||||||
stars = db.relationship("User", secondary="books_stars", back_populates="stars")
|
stars = db.relationship("User", secondary="books_stars", back_populates="stars")
|
||||||
contributors = db.relationship("BookContributor")
|
contributors = db.relationship("BookContributor")
|
||||||
versions = db.relationship("BookVersion")
|
versions = db.relationship("BookVersion", order_by="asc(BookVersion.id)")
|
||||||
|
list_access_groups = db.relationship(
|
||||||
|
"AccessGroup"
|
||||||
|
) # all access_groups in current book(in nested entities)
|
||||||
|
access_groups = db.relationship(
|
||||||
|
"AccessGroup",
|
||||||
|
secondary="books_access_groups",
|
||||||
|
) # access_groups related to current entity
|
||||||
tags = db.relationship(
|
tags = db.relationship(
|
||||||
"Tag",
|
"Tag",
|
||||||
secondary="book_tags",
|
secondary="book_tags",
|
||||||
|
|
|
@ -25,6 +25,10 @@ class Collection(BaseModel):
|
||||||
order_by="asc(Collection.id)",
|
order_by="asc(Collection.id)",
|
||||||
)
|
)
|
||||||
sections = db.relationship("Section")
|
sections = db.relationship("Section")
|
||||||
|
access_groups = db.relationship(
|
||||||
|
"AccessGroup",
|
||||||
|
secondary="collections_access_groups",
|
||||||
|
) # access_groups related to current entity
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"<{self.id}: {self.label}>"
|
return f"<{self.id}: {self.label}>"
|
||||||
|
|
|
@ -26,6 +26,10 @@ class Interpretation(BaseModel):
|
||||||
secondary="interpretation_tags",
|
secondary="interpretation_tags",
|
||||||
back_populates="interpretations",
|
back_populates="interpretations",
|
||||||
)
|
)
|
||||||
|
access_groups = db.relationship(
|
||||||
|
"AccessGroup",
|
||||||
|
secondary="interpretations_access_groups",
|
||||||
|
) # access_groups related to current entity
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def vote_count(self):
|
def vote_count(self):
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# flake8: noqa F401
|
||||||
|
from .access_group import AccessGroup
|
||||||
|
from .permission import Permission
|
||||||
|
from .user_access_groups import UserAccessGroups
|
||||||
|
from .permission_access_groups import PermissionAccessGroups
|
||||||
|
from .book_access_groups import BookAccessGroups
|
||||||
|
from .collection_access_groups import CollectionAccessGroups
|
||||||
|
from .section_access_groups import SectionAccessGroups
|
||||||
|
from .interpretation_access_groups import InterpretationAccessGroups
|
|
@ -0,0 +1,25 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class AccessGroup(BaseModel):
|
||||||
|
__tablename__ = "access_groups"
|
||||||
|
|
||||||
|
name = db.Column(db.String(32), nullable=False)
|
||||||
|
|
||||||
|
# Foreign Keys
|
||||||
|
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
book = db.relationship("Book", viewonly=True)
|
||||||
|
permissions = db.relationship(
|
||||||
|
"Permission",
|
||||||
|
secondary="permissions_access_groups",
|
||||||
|
back_populates="access_groups",
|
||||||
|
)
|
||||||
|
users = db.relationship(
|
||||||
|
"User", secondary="users_access_groups", back_populates="access_groups"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<{self.id}: {self.name} | Book: {self.book_id}>"
|
|
@ -0,0 +1,13 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class BookAccessGroups(BaseModel):
|
||||||
|
__tablename__ = "books_access_groups"
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
|
||||||
|
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<b:{self.book_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class CollectionAccessGroups(BaseModel):
|
||||||
|
__tablename__ = "collections_access_groups"
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
collection_id = db.Column(db.Integer, db.ForeignKey("collections.id"))
|
||||||
|
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<c:{self.collection_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class InterpretationAccessGroups(BaseModel):
|
||||||
|
__tablename__ = "interpretations_access_groups"
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
interpretation_id = db.Column(db.Integer, db.ForeignKey("interpretations.id"))
|
||||||
|
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<c:{self.interpretation_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,33 @@
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class Permission(BaseModel):
|
||||||
|
__tablename__ = "permissions"
|
||||||
|
|
||||||
|
class Access(IntEnum):
|
||||||
|
C = 1 # 0b0001 - Create
|
||||||
|
U = 2 # 0b0010 - Update
|
||||||
|
D = 4 # 0b0100 - Delete
|
||||||
|
A = 8 # 0b1000 - Approve
|
||||||
|
# sum = 0b1111
|
||||||
|
|
||||||
|
class Entity(IntEnum):
|
||||||
|
UNKNOWN = 0
|
||||||
|
BOOK = 1
|
||||||
|
COLLECTION = 2
|
||||||
|
SECTION = 3
|
||||||
|
INTERPRETATION = 4
|
||||||
|
COMMENT = 5
|
||||||
|
|
||||||
|
access = db.Column(db.Integer(), default=Access.C | Access.U | Access.D | Access.A)
|
||||||
|
entity_type = db.Column(db.Enum(Entity), default=Entity.UNKNOWN)
|
||||||
|
|
||||||
|
# Relationships
|
||||||
|
access_groups = db.relationship(
|
||||||
|
"AccessGroup",
|
||||||
|
secondary="permissions_access_groups",
|
||||||
|
back_populates="permissions",
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionAccessGroups(BaseModel):
|
||||||
|
__tablename__ = "permissions_access_groups"
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
permission_id = db.Column(db.Integer, db.ForeignKey("permissions.id"))
|
||||||
|
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<p:{self.permission_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class SectionAccessGroups(BaseModel):
|
||||||
|
__tablename__ = "sections_access_groups"
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
section_id = db.Column(db.Integer, db.ForeignKey("sections.id"))
|
||||||
|
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<s:{self.section_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
||||||
|
from app import db
|
||||||
|
from app.models.utils import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class UserAccessGroups(BaseModel):
|
||||||
|
__tablename__ = "users_access_groups"
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||||
|
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<u:{self.user_id} to a_g:{self.access_group_id}"
|
|
@ -26,6 +26,10 @@ class Section(BaseModel):
|
||||||
interpretations = db.relationship(
|
interpretations = db.relationship(
|
||||||
"Interpretation", viewonly=True, order_by="desc(Interpretation.id)"
|
"Interpretation", viewonly=True, order_by="desc(Interpretation.id)"
|
||||||
)
|
)
|
||||||
|
access_groups = db.relationship(
|
||||||
|
"AccessGroup",
|
||||||
|
secondary="sections_access_groups",
|
||||||
|
) # access_groups related to current entity
|
||||||
tags = db.relationship(
|
tags = db.relationship(
|
||||||
"Tag",
|
"Tag",
|
||||||
secondary="section_tags",
|
secondary="section_tags",
|
||||||
|
|
|
@ -23,7 +23,11 @@ class User(BaseModel, UserMixin):
|
||||||
is_activated = db.Column(db.Boolean, default=False)
|
is_activated = db.Column(db.Boolean, default=False)
|
||||||
wallet_id = db.Column(db.String(64), nullable=True)
|
wallet_id = db.Column(db.String(64), nullable=True)
|
||||||
avatar_img = db.Column(db.Text, nullable=True)
|
avatar_img = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
|
access_groups = db.relationship(
|
||||||
|
"AccessGroup", secondary="users_access_groups", back_populates="users"
|
||||||
|
)
|
||||||
stars = db.relationship("Book", secondary="books_stars", back_populates="stars")
|
stars = db.relationship("Book", secondary="books_stars", back_populates="stars")
|
||||||
books = db.relationship("Book")
|
books = db.relationship("Book")
|
||||||
|
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -28,17 +28,20 @@
|
||||||
Table of contents
|
Table of contents
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
{% if has_permission(book, Access.U) %}
|
||||||
<div class="flex text-black dark:text-white">
|
<div class="flex text-black dark:text-white">
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<div>
|
<div>
|
||||||
{% if not book.versions[-1].children_collections and current_user.is_authenticated %}
|
{% if not book.versions[-1].children_collections and current_user.is_authenticated %}
|
||||||
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" ><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> </button>
|
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" ><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> </button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ url_for("book.settings", book_id=book.id) }}" type="button" class="ml-2" >
|
<a href="{{ url_for('book.settings', book_id=book.id) }}" type="button" class="ml-2" >
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
{% for collection in book.versions[-1].children_collections if not collection.is_root and not collection.is_deleted %}
|
{% for collection in book.versions[-1].children_collections if not collection.is_root and not collection.is_deleted %}
|
||||||
|
@ -60,7 +63,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div data="collection-context-menu-{{collection.id}}" id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-800 border border-gray-800 dark:border-none dark:divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
<div data="collection-context-menu-{{collection.id}}" id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-800 border border-gray-800 dark:border-none dark:divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
{% set access_to_create_collections = has_permission(collection, Access.C) %}
|
||||||
|
{% set access_to_update_collections = has_permission(collection, Access.U) %}
|
||||||
|
{% set access_to_delete_collections = has_permission(collection, Access.D) %}
|
||||||
|
{% set access_to_create_section = has_permission(collection, Access.C, EntityType.SECTION) %}
|
||||||
|
|
||||||
|
{% if access_to_create_collections or access_to_update_collections %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{% if access_to_create_collections %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Collection</button>
|
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Collection</button>
|
||||||
</li>
|
</li>
|
||||||
|
@ -69,20 +79,31 @@
|
||||||
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Subcollection</button>
|
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Subcollection</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if access_to_create_section %}
|
||||||
{% if collection.children|length ==0 or collection.children|length ==0 and collection.is_leaf %}
|
{% if collection.children|length ==0 or collection.children|length ==0 and collection.is_leaf %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="callAddSectionModal" data-modal-target="add-section-modal" data-modal-toggle="add-section-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="_" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Section</button>
|
<button type="button" id="callAddSectionModal" data-modal-target="add-section-modal" data-modal-toggle="add-section-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="_" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Section</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if access_to_update_collections or access_to_delete_collections %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{% if access_to_update_collections %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="rename-collection-button-{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Rename Collection</button>
|
<button type="button" id="rename-collection-button-{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Rename Collection</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if access_to_delete_collections %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="callDeleteCollectionModal" data-modal-target="delete-collection-modal" data-modal-toggle="delete-collection-modal" data-collection-id="{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Delete Collection</button>
|
<button type="button" id="callDeleteCollectionModal" data-modal-target="delete-collection-modal" data-modal-toggle="delete-collection-modal" data-collection-id="{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Delete Collection</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
<li>
|
<li>
|
||||||
<button type="button" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Export Collection</button>
|
<button type="button" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">Export Collection</button>
|
||||||
|
|
|
@ -23,7 +23,8 @@
|
||||||
id="edit-section-label-{{section.id}}"
|
id="edit-section-label-{{section.id}}"
|
||||||
placeholder="Section label"
|
placeholder="Section label"
|
||||||
required
|
required
|
||||||
readonly />
|
readonly
|
||||||
|
/>
|
||||||
<button name="submit" type="submit"></button>
|
<button name="submit" type="submit"></button>
|
||||||
</form>
|
</form>
|
||||||
</button>
|
</button>
|
||||||
|
@ -41,7 +42,13 @@
|
||||||
id="dropdown"
|
id="dropdown"
|
||||||
class="z-10 hidden bg-white divide-y divide-gray-800 border border-gray-800 dark:border-none dark:divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
class="z-10 hidden bg-white divide-y divide-gray-800 border border-gray-800 dark:border-none dark:divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
{% set access_to_create_sections = has_permission(section, Access.C) %}
|
||||||
|
{% set access_to_update_sections = has_permission(section, Access.U) %}
|
||||||
|
{% set access_to_delete_sections = has_permission(section, Access.D) %}
|
||||||
|
|
||||||
|
{% if access_to_update_sections or access_to_delete_sections %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{% if access_to_update_sections %}
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -50,6 +57,9 @@
|
||||||
Rename Section
|
Rename Section
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if access_to_delete_sections %}
|
||||||
<li>
|
<li>
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<button
|
<button
|
||||||
|
@ -66,7 +76,10 @@
|
||||||
Delete Section
|
Delete Section
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -31,32 +31,52 @@
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<div data="sub-collection-context-menu-{{sub_collection.id}}" id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-800 border border-gray-800 dark:border-none dark:divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
<div data="sub-collection-context-menu-{{sub_collection.id}}" id="dropdown" class="z-10 hidden bg-white divide-y divide-gray-800 border border-gray-800 dark:border-none dark:divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
|
{% set access_to_create_collections = has_permission(sub_collection, Access.C) %}
|
||||||
|
{% set access_to_update_collections = has_permission(sub_collection, Access.U) %}
|
||||||
|
{% set access_to_delete_collections = has_permission(sub_collection, Access.D) %}
|
||||||
|
{% set access_to_create_section = has_permission(collection, Access.C, EntityType.SECTION) %}
|
||||||
|
|
||||||
|
{% if access_to_create_collections or access_to_update_collections or access_to_create_section %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
{% if sub_collection.is_leaf and not sub_collection.children %}
|
{% if access_to_create_section and sub_collection.is_leaf and not sub_collection.children %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="callAddSectionModal" data-modal-target="add-section-modal" data-modal-toggle="add-section-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Section </button>
|
<button type="button" id="callAddSectionModal" data-modal-target="add-section-modal" data-modal-toggle="add-section-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Section </button>
|
||||||
</li>
|
</li>
|
||||||
{% elif not sub_collection.is_leaf and not sub_collection.children %}
|
{% elif not sub_collection.is_leaf and not sub_collection.children %}
|
||||||
|
{% if access_to_create_section %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="callAddSectionModal" data-modal-target="add-section-modal" data-modal-toggle="add-section-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Section </button>
|
<button type="button" id="callAddSectionModal" data-modal-target="add-section-modal" data-modal-toggle="add-section-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Section </button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{% endif %}
|
||||||
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Subcollection </button>
|
{% if access_to_create_collections %}
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Subcollection </button>
|
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Subcollection </button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if access_to_create_collections %}
|
||||||
|
<li>
|
||||||
|
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> New Subcollection </button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if access_to_update_collections or access_to_delete_collections %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
|
{% if access_to_update_collections %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="rename-sub-collection-button-{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Rename Sub Collection </button>
|
<button type="button" id="rename-sub-collection-button-{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Rename Sub Collection </button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if access_to_delete_collections %}
|
||||||
<li>
|
<li>
|
||||||
<button type="button" id="callDeleteSubCollectionModal" data-modal-target="delete-sub-collection-modal" data-modal-toggle="delete-sub-collection-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Delete Sub Collection </button>
|
<button type="button" id="callDeleteSubCollectionModal" data-modal-target="delete-sub-collection-modal" data-modal-toggle="delete-sub-collection-modal" data-collection-id="{{collection.id}}" data-sub-collection-id="{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Delete Sub Collection </button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||||
<li>
|
<li>
|
||||||
<button type="button" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Export Sub Collection </button>
|
<button type="button" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Export Sub Collection </button>
|
||||||
|
|
|
@ -80,12 +80,16 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<dl class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700">
|
<dl class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700">
|
||||||
|
{% set access_to_delete_interpretation = has_permission(section, Access.D, EntityType.INTERPRETATION) %}
|
||||||
|
{% set access_to_approve_interpretation = has_permission(section, Access.A, EntityType.INTERPRETATION) %}
|
||||||
|
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
{% for interpretation in section.active_interpretations %}
|
{% for interpretation in section.active_interpretations %}
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<dl class="bg-white dark:bg-gray-900 max-w-full p-3 text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 m-3 border-2 border-gray-200 border-solid rounded-lg dark:border-gray-700">
|
<dl class="bg-white dark:bg-gray-900 max-w-full p-3 text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 m-3 border-2 border-gray-200 border-solid rounded-lg dark:border-gray-700">
|
||||||
<div class="flex flex-row pb-3 p-3 pt-0 w-2/3 md:w-full">
|
<div class="flex flex-row pb-3 p-3 pt-0 w-2/3 md:w-full">
|
||||||
<div class="vote-block flex flex-col m-5 mr-8 justify-center items-center">
|
<div class="vote-block flex flex-col m-5 mr-8 justify-center items-center">
|
||||||
|
|
||||||
<div class="vote-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="true">
|
<div class="vote-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="true">
|
||||||
<svg class="w-6 h-6 select-none
|
<svg class="w-6 h-6 select-none
|
||||||
{% if interpretation.current_user_vote %}
|
{% if interpretation.current_user_vote %}
|
||||||
|
@ -93,8 +97,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" > <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
|
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" > <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="vote-count text-3xl select-none
|
class="vote-count text-3xl select-none
|
||||||
{% if interpretation.vote_count < 0 %}
|
{% if interpretation.vote_count < 0 %}
|
||||||
|
@ -106,7 +108,6 @@
|
||||||
>
|
>
|
||||||
{{ interpretation.vote_count }}
|
{{ interpretation.vote_count }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="vote-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="false">
|
<div class="vote-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="false">
|
||||||
<svg class="w-6 h-6 select-none
|
<svg class="w-6 h-6 select-none
|
||||||
{% if interpretation.current_user_vote == False %}
|
{% if interpretation.current_user_vote == False %}
|
||||||
|
@ -115,8 +116,8 @@
|
||||||
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
|
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO check permissions -->
|
|
||||||
{% if interpretation.book.owner == current_user %}
|
{% if interpretation.book.owner == current_user or access_to_approve_interpretation %}
|
||||||
<div class="approve-button select-none approve-btn mt-3 cursor-pointer" data-approve="interpretation" data-entity-id="{{ interpretation.id }}">
|
<div class="approve-button select-none approve-btn mt-3 cursor-pointer" data-approve="interpretation" data-entity-id="{{ interpretation.id }}">
|
||||||
<!-- outline -->
|
<!-- outline -->
|
||||||
<svg class="not-approved-icon w-6 h-6 {% if interpretation.approved %} hidden {% endif %}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<svg class="not-approved-icon w-6 h-6 {% if interpretation.approved %} hidden {% endif %}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
@ -128,7 +129,9 @@
|
||||||
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if interpretation.user_id == current_user.id %}
|
||||||
<!--Edit & Delete interpretation-->
|
<!--Edit & Delete interpretation-->
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<button id="callEditInterpretationModal" data-popover-target="popover-edit" data-edit-interpretation-id="{{interpretation.id}}" data-edit-interpretation-text="{{interpretation.text}}" type="button" data-modal-target="edit_interpretation_modal" data-modal-toggle="edit_interpretation_modal" class="space-x-0.5 flex items-center">
|
<button id="callEditInterpretationModal" data-popover-target="popover-edit" data-edit-interpretation-id="{{interpretation.id}}" data-edit-interpretation-text="{{interpretation.text}}" type="button" data-modal-target="edit_interpretation_modal" data-modal-toggle="edit_interpretation_modal" class="space-x-0.5 flex items-center">
|
||||||
|
@ -141,6 +144,9 @@
|
||||||
<div data-popper-arrow></div>
|
<div data-popper-arrow></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if interpretation.book.owner == current_user or access_to_delete_interpretation %}
|
||||||
<div class="relative mt-1">
|
<div class="relative mt-1">
|
||||||
<button id="callDeleteInterpretationModal" data-popover-target="popover-delete" data-interpretation-id="{{interpretation.id}}" type="button" data-modal-target="delete_interpretation_modal" data-modal-toggle="delete_interpretation_modal" class="space-x-0.5 flex items-center">
|
<button id="callDeleteInterpretationModal" data-popover-target="popover-delete" data-interpretation-id="{{interpretation.id}}" type="button" data-modal-target="delete_interpretation_modal" data-modal-toggle="delete_interpretation_modal" class="space-x-0.5 flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> </svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> </svg>
|
||||||
|
@ -152,9 +158,10 @@
|
||||||
<div data-popper-arrow></div>
|
<div data-popper-arrow></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<dt class="flex justify-center w-full mb-1 text-gray-500 md:text-lg dark:text-gray-400 flex-col">
|
<dt class="flex justify-center w-full mb-1 text-gray-500 md:text-lg dark:text-gray-400 flex-col">
|
||||||
<div class="ql-snow mb-2 md:max-w-xl">
|
<div class="ql-snow mb-2 md:max-w-xl">
|
||||||
|
|
|
@ -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>
|
|
@ -72,6 +72,9 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<dl class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700">
|
<dl class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700">
|
||||||
|
{% set access_to_delete_comment = has_permission(section, Access.D, EntityType.COMMENT) %}
|
||||||
|
{% set access_to_approve_comment = has_permission(section, Access.A, EntityType.COMMENT) %}
|
||||||
|
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<div class="text-sm dark:text-white p-3">Comments:</div>
|
<div class="text-sm dark:text-white p-3">Comments:</div>
|
||||||
{% for comment in interpretation.comments if not comment.is_deleted and not comment.parent_id %}
|
{% for comment in interpretation.comments if not comment.is_deleted and not comment.parent_id %}
|
||||||
|
@ -81,16 +84,8 @@
|
||||||
<div class="vote-block flex flex-col m-5 mr-8 justify-center items-center">
|
<div class="vote-block flex flex-col m-5 mr-8 justify-center items-center">
|
||||||
|
|
||||||
<div class="vote-button cursor-pointer" data-vote-for="comment" data-entity-id="{{ comment.id }}" data-positive="true">
|
<div class="vote-button cursor-pointer" data-vote-for="comment" data-entity-id="{{ comment.id }}" data-positive="true">
|
||||||
<svg
|
<svg class="w-6 h-6 select-none {% if comment.current_user_vote %} stroke-green-500 {% endif %} " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" > <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
|
||||||
class="w-6 h-6 select-none
|
|
||||||
{% if comment.current_user_vote %}
|
|
||||||
stroke-green-500
|
|
||||||
{% endif %}
|
|
||||||
"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" > <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="vote-count text-3xl select-none
|
class="vote-count text-3xl select-none
|
||||||
{% if comment.vote_count < 0 %}
|
{% if comment.vote_count < 0 %}
|
||||||
|
@ -102,18 +97,12 @@
|
||||||
>
|
>
|
||||||
{{ comment.vote_count }}
|
{{ comment.vote_count }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
||||||
<div class="vote-button cursor-pointer" data-vote-for="comment" data-entity-id="{{ comment.id }}" data-positive="false">
|
<div class="vote-button cursor-pointer" data-vote-for="comment" data-entity-id="{{ comment.id }}" data-positive="false">
|
||||||
<svg class="w-6 h-6 select-none
|
<svg class="w-6 h-6 select-none {% if comment.current_user_vote == False %} stroke-red-500 {% endif %} " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
|
||||||
{% if comment.current_user_vote == False %}
|
|
||||||
stroke-red-500
|
|
||||||
{% endif %}
|
|
||||||
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TODO check permissions -->
|
|
||||||
{% if interpretation.book.owner == current_user %}
|
{% if access_to_approve_comment %}
|
||||||
<div class="approve-button select-none approve-btn mt-3 cursor-pointer" data-approve="comment" data-entity-id="{{ comment.id }}">
|
<div class="approve-button select-none approve-btn mt-3 cursor-pointer" data-approve="comment" data-entity-id="{{ comment.id }}">
|
||||||
<!-- outline -->
|
<!-- outline -->
|
||||||
<svg class="not-approved-icon w-6 h-6 {% if comment.approved %} hidden {% endif %}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<svg class="not-approved-icon w-6 h-6 {% if comment.approved %} hidden {% endif %}" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
@ -162,6 +151,9 @@
|
||||||
<div data-popper-arrow></div>
|
<div data-popper-arrow></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if comment.user_id == current_user.id or access_to_delete_comment %}
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button id="delete_comment_btn" data-popover-target="popover-delete" data-comment-id="{{comment.id}}" type="button" data-modal-target="delete_comment_modal" data-modal-toggle="delete_comment_modal" class="space-x-0.5 flex items-center">
|
<button id="delete_comment_btn" data-popover-target="popover-delete" data-comment-id="{{comment.id}}" type="button" data-modal-target="delete_comment_modal" data-modal-toggle="delete_comment_modal" class="space-x-0.5 flex items-center">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> </svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /> </svg>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% include 'book/modals/access_level_modal.html' %}
|
||||||
{% include 'book/modals/add_contributor_modal.html' %}
|
{% include 'book/modals/add_contributor_modal.html' %}
|
||||||
{% include 'book/modals/delete_book_modal.html' %}
|
{% include 'book/modals/delete_book_modal.html' %}
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
<span>Book settings</span>
|
<span>Book settings</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% if book.user_id == current_user.id %}
|
||||||
<li class="mr-2" role="presentation">
|
<li class="mr-2" role="presentation">
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<button class="flex items-center space-x-2 p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="permissions-tab" data-tabs-target="#permissions" type="button" role="tab" aria-controls="permissions" aria-selected="false">
|
<button class="flex items-center space-x-2 p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="permissions-tab" data-tabs-target="#permissions" type="button" role="tab" aria-controls="permissions" aria-selected="false">
|
||||||
|
@ -33,6 +35,7 @@
|
||||||
<span>User permissions</span>
|
<span>User permissions</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -77,29 +80,49 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="permissions" role="tabpanel" aria-labelledby="permissions-tab">
|
{% if book.user_id == current_user.id %}
|
||||||
<div class="p-5">
|
<div class="hidden px-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="permissions" role="tabpanel" aria-labelledby="permissions-tab">
|
||||||
<div class="flex justify-between ml-4 mb-2">
|
<div class="px-5">
|
||||||
<h1 class="text-2xl font-extrabold dark:text-white">Contributors</h1>
|
|
||||||
<!-- prettier-ignore -->
|
<div class="mb-3 relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||||
<button type="button" data-modal-target="add-contributor-modal" data-modal-toggle="add-contributor-modal" 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-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke"currentColor" class="w-6 h-6 mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>
|
|
||||||
Add
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
|
||||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
<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">
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||||
<tr> <th scope="col" class="px-6 py-3">Username</th> <th scope="col" class="px-6 py-3">Role</th> <th scope="col" class="px-6 py-3">Action</th> </tr>
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
Username
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
Address
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-center">
|
||||||
|
Access Level
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3">
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for contributor in book.contributors %}
|
{% for contributor in book.contributors %}
|
||||||
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700">
|
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700">
|
||||||
<td class="px-6 py-4">{{ contributor.user.username }}</td>
|
<td class="px-6 max-w-[230]">
|
||||||
<td class="px-6 py-4">
|
<div class="flex items-center">
|
||||||
<form action="{{ url_for('book.edit_contributor_role', book_id=book.id) }}" method="post" class="mb-0 flex space-x-2">
|
{% if contributor.user.avatar_img %}
|
||||||
|
<img class="w-6 h-6 rounded-full" src="data:image/jpeg;base64,{{ contributor.user.avatar_img }}" alt="contributor avatar">
|
||||||
|
{% else %}
|
||||||
|
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
|
||||||
|
{% endif %}
|
||||||
|
<span class="ml-2 truncate">{{ contributor.user.username }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 truncate max-w-[280]">{{ contributor.user.wallet_id }}</td>
|
||||||
|
<td class="px-6">
|
||||||
|
<form action="{{ url_for('book.edit_contributor_role', book_id=book.id) }}" method="post" class="mb-0 flex">
|
||||||
{{ form_hidden_tag() }}
|
{{ form_hidden_tag() }}
|
||||||
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
|
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
|
||||||
<select id="role" name="role" data-user-id="{{ contributor.user.id }}" class="contributor-role-select block w-1/2 py-1.5 px-2 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" >
|
<select id="role" name="role" data-user-id="{{ contributor.user.id }}" class="mr-2 contributor-role-select block w-1/2 py-1.5 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" >
|
||||||
{% for role in roles if role.value %}
|
{% for role in roles if role.value %}
|
||||||
<option
|
<option
|
||||||
{% if contributor.role == role %} selected {% endif %}
|
{% if contributor.role == role %} selected {% endif %}
|
||||||
|
@ -111,21 +134,35 @@
|
||||||
<button type="submit" 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">Save</button>
|
<button type="submit" 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">Save</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="px-4 py-4 flex justify-center">
|
||||||
|
<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>
|
||||||
<td class="px-6 py-4">
|
<td class="px-6 py-4">
|
||||||
<!-- prettier-ignore -->
|
<!-- prettier-ignore -->
|
||||||
<form action="{{ url_for('book.delete_contributor', book_id=book.id) }}" method="post" class="mb-0">
|
{% if current_user.id != contributor.user_id %}
|
||||||
|
<form class="mb-0 flex justify-end" action="{{ url_for('book.delete_contributor', book_id=book.id) }}" method="post">
|
||||||
{{ form_hidden_tag() }}
|
{{ form_hidden_tag() }}
|
||||||
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
|
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
|
||||||
<button type="submit" class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-sm rounded-lg text-sm px-5 py-1.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">Delete</button>
|
<button type="submit" class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-sm rounded-lg text-sm px-5 py-1.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" data-modal-target="add-contributor-modal" data-modal-toggle="add-contributor-modal" 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-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> </svg>
|
||||||
|
New Contributor
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -7,4 +7,5 @@ from .home import bp as home_blueprint
|
||||||
from .vote import bp as vote_blueprint
|
from .vote import bp as vote_blueprint
|
||||||
from .approve import bp as approve_blueprint
|
from .approve import bp as approve_blueprint
|
||||||
from .star import bp as star_blueprint
|
from .star import bp as star_blueprint
|
||||||
|
from .permission import bp as permissions_blueprint
|
||||||
from .search import bp as search_blueprint
|
from .search import bp as search_blueprint
|
||||||
|
|
|
@ -5,6 +5,7 @@ from flask import (
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
from app import models as m, db
|
from app import models as m, db
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
|
|
||||||
bp = Blueprint("approve", __name__, url_prefix="/approve")
|
bp = Blueprint("approve", __name__, url_prefix="/approve")
|
||||||
|
@ -14,6 +15,11 @@ bp = Blueprint("approve", __name__, url_prefix="/approve")
|
||||||
"/interpretation/<int:interpretation_id>",
|
"/interpretation/<int:interpretation_id>",
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||||
|
access=[m.Permission.Access.A],
|
||||||
|
entities=[m.Interpretation],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def approve_interpretation(interpretation_id: int):
|
def approve_interpretation(interpretation_id: int):
|
||||||
interpretation: m.Interpretation = db.session.get(
|
interpretation: m.Interpretation = db.session.get(
|
||||||
|
@ -23,16 +29,6 @@ def approve_interpretation(interpretation_id: int):
|
||||||
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
|
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
|
||||||
return jsonify({"message": "Interpretation not found"}), 404
|
return jsonify({"message": "Interpretation not found"}), 404
|
||||||
|
|
||||||
# TODO check permission
|
|
||||||
if interpretation.book.owner != current_user:
|
|
||||||
log(
|
|
||||||
log.WARNING,
|
|
||||||
"User [%s] dont have permission to approve [%s]",
|
|
||||||
current_user,
|
|
||||||
interpretation,
|
|
||||||
)
|
|
||||||
return jsonify({"message": "You dont have permission"}), 404
|
|
||||||
|
|
||||||
already_approved_interpretations = (
|
already_approved_interpretations = (
|
||||||
m.Interpretation.query.filter_by(
|
m.Interpretation.query.filter_by(
|
||||||
approved=True, section_id=interpretation.section_id
|
approved=True, section_id=interpretation.section_id
|
||||||
|
@ -65,25 +61,20 @@ def approve_interpretation(interpretation_id: int):
|
||||||
|
|
||||||
|
|
||||||
@bp.route(
|
@bp.route(
|
||||||
"/comment/<int:interpretation_id>",
|
"/comment/<int:comment_id>",
|
||||||
methods=["POST"],
|
methods=["POST"],
|
||||||
)
|
)
|
||||||
@login_required
|
@require_permission(
|
||||||
def approve_comment(interpretation_id: int):
|
entity_type=m.Permission.Entity.COMMENT,
|
||||||
comment: m.Comment = db.session.get(m.Comment, interpretation_id)
|
access=[m.Permission.Access.A],
|
||||||
if not comment:
|
entities=[m.Comment],
|
||||||
log(log.WARNING, "Comment with id [%s] not found", interpretation_id)
|
|
||||||
return jsonify({"message": "Comment not found"}), 404
|
|
||||||
|
|
||||||
# TODO check permission
|
|
||||||
if comment.interpretation.book.owner != current_user:
|
|
||||||
log(
|
|
||||||
log.WARNING,
|
|
||||||
"User [%s] dont have permission to approve [%s]",
|
|
||||||
current_user,
|
|
||||||
comment,
|
|
||||||
)
|
)
|
||||||
return jsonify({"message": "You dont have permission"}), 404
|
@login_required
|
||||||
|
def approve_comment(comment_id: int):
|
||||||
|
comment: m.Comment = db.session.get(m.Comment, comment_id)
|
||||||
|
if not comment:
|
||||||
|
log(log.WARNING, "Comment with id [%s] not found", comment_id)
|
||||||
|
return jsonify({"message": "Comment not found"}), 404
|
||||||
|
|
||||||
comment.approved = not comment.approved
|
comment.approved = not comment.approved
|
||||||
log(
|
log(
|
||||||
|
|
|
@ -17,6 +17,11 @@ from app.controllers.tags import (
|
||||||
from app.controllers.delete_nested_book_entities import (
|
from app.controllers.delete_nested_book_entities import (
|
||||||
delete_nested_book_entities,
|
delete_nested_book_entities,
|
||||||
)
|
)
|
||||||
|
from app.controllers.create_access_groups import (
|
||||||
|
create_editor_group,
|
||||||
|
create_moderator_group,
|
||||||
|
)
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
|
@ -85,12 +90,24 @@ def create():
|
||||||
log(log.INFO, "Form submitted. Book: [%s]", book)
|
log(log.INFO, "Form submitted. Book: [%s]", book)
|
||||||
book.save()
|
book.save()
|
||||||
version = m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
version = m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
||||||
m.Collection(
|
root_collection = m.Collection(
|
||||||
label="Root Collection", version_id=version.id, is_root=True
|
label="Root Collection", version_id=version.id, is_root=True
|
||||||
).save()
|
).save()
|
||||||
tags = form.tags.data or ""
|
tags = form.tags.data or ""
|
||||||
set_book_tags(book, tags)
|
set_book_tags(book, tags)
|
||||||
|
|
||||||
|
# access groups
|
||||||
|
editor_access_group = create_editor_group(book_id=book.id)
|
||||||
|
moderator_access_group = create_moderator_group(book_id=book.id)
|
||||||
|
access_groups = [editor_access_group, moderator_access_group]
|
||||||
|
|
||||||
|
for access_group in access_groups:
|
||||||
|
m.BookAccessGroups(book_id=book.id, access_group_id=access_group.id).save()
|
||||||
|
m.CollectionAccessGroups(
|
||||||
|
collection_id=root_collection.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
# -------------
|
||||||
|
|
||||||
flash("Book added!", "success")
|
flash("Book added!", "success")
|
||||||
return redirect(url_for("book.my_library"))
|
return redirect(url_for("book.my_library"))
|
||||||
else:
|
else:
|
||||||
|
@ -104,6 +121,11 @@ def create():
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/edit", methods=["POST"])
|
@bp.route("/<int:book_id>/edit", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def edit(book_id: int):
|
def edit(book_id: int):
|
||||||
form = f.EditBookForm()
|
form = f.EditBookForm()
|
||||||
|
@ -130,13 +152,18 @@ def edit(book_id: int):
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/delete", methods=["POST"])
|
@bp.route("/<int:book_id>/delete", methods=["POST"])
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
access=[m.Permission.Access.D],
|
||||||
|
entities=[m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def delete(book_id: int):
|
def delete(book_id: int):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
|
|
||||||
if not book or book.is_deleted:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
||||||
flash("You are not owner of this book!", "danger")
|
flash("Book not found!", "danger")
|
||||||
return redirect(url_for("book.my_library"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
book.is_deleted = True
|
book.is_deleted = True
|
||||||
|
|
|
@ -14,6 +14,7 @@ from app.controllers.delete_nested_book_entities import (
|
||||||
delete_nested_collection_entities,
|
delete_nested_collection_entities,
|
||||||
)
|
)
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
|
|
||||||
|
@ -35,6 +36,11 @@ def collection_view(book_id: int):
|
||||||
@bp.route("/<int:book_id>/create_collection", methods=["POST"])
|
@bp.route("/<int:book_id>/create_collection", methods=["POST"])
|
||||||
@bp.route("/<int:book_id>/<int:collection_id>/create_sub_collection", methods=["POST"])
|
@bp.route("/<int:book_id>/<int:collection_id>/create_sub_collection", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.COLLECTION,
|
||||||
|
access=[m.Permission.Access.C],
|
||||||
|
entities=[m.Collection, m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def collection_create(book_id: int, collection_id: int | None = None):
|
def collection_create(book_id: int, collection_id: int | None = None):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
|
@ -92,6 +98,11 @@ def collection_create(book_id: int, collection_id: int | None = None):
|
||||||
log(log.INFO, "Create collection [%s]. Book: [%s]", collection, book.id)
|
log(log.INFO, "Create collection [%s]. Book: [%s]", collection, book.id)
|
||||||
collection.save()
|
collection.save()
|
||||||
|
|
||||||
|
for access_group in collection.parent.access_groups:
|
||||||
|
m.CollectionAccessGroups(
|
||||||
|
collection_id=collection.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
flash("Success!", "success")
|
flash("Success!", "success")
|
||||||
if collection_id:
|
if collection_id:
|
||||||
redirect_url = url_for("book.collection_view", book_id=book_id)
|
redirect_url = url_for("book.collection_view", book_id=book_id)
|
||||||
|
@ -107,6 +118,11 @@ def collection_create(book_id: int, collection_id: int | None = None):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/<int:collection_id>/edit", methods=["POST"])
|
@bp.route("/<int:book_id>/<int:collection_id>/edit", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.COLLECTION,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Collection],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def collection_edit(book_id: int, collection_id: int):
|
def collection_edit(book_id: int, collection_id: int):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
|
@ -162,6 +178,11 @@ def collection_edit(book_id: int, collection_id: int):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/<int:collection_id>/delete", methods=["POST"])
|
@bp.route("/<int:book_id>/<int:collection_id>/delete", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.COLLECTION,
|
||||||
|
access=[m.Permission.Access.D],
|
||||||
|
entities=[m.Collection],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def collection_delete(book_id: int, collection_id: int):
|
def collection_delete(book_id: int, collection_id: int):
|
||||||
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
||||||
|
|
|
@ -25,7 +25,7 @@ def create_comment(
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
if not book or book.is_deleted:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
||||||
flash("You are not owner of this book!", "danger")
|
flash("Book not found!", "danger")
|
||||||
return redirect(url_for("book.my_library"))
|
return redirect(url_for("book.my_library"))
|
||||||
redirect_url = url_for(
|
redirect_url = url_for(
|
||||||
"book.qa_view", book_id=book_id, interpretation_id=interpretation_id
|
"book.qa_view", book_id=book_id, interpretation_id=interpretation_id
|
||||||
|
|
|
@ -12,6 +12,7 @@ from app.controllers.delete_nested_book_entities import (
|
||||||
delete_nested_interpretation_entities,
|
delete_nested_interpretation_entities,
|
||||||
)
|
)
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app.controllers.tags import set_interpretation_tags
|
from app.controllers.tags import set_interpretation_tags
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
|
@ -86,6 +87,13 @@ def interpretation_create(
|
||||||
)
|
)
|
||||||
interpretation.save()
|
interpretation.save()
|
||||||
|
|
||||||
|
# access groups
|
||||||
|
for access_group in interpretation.section.access_groups:
|
||||||
|
m.InterpretationAccessGroups(
|
||||||
|
interpretation_id=interpretation.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
# -------------
|
||||||
|
|
||||||
tags = current_app.config["TAG_REGEX"].findall(text)
|
tags = current_app.config["TAG_REGEX"].findall(text)
|
||||||
set_interpretation_tags(interpretation, tags)
|
set_interpretation_tags(interpretation, tags)
|
||||||
|
|
||||||
|
@ -109,14 +117,18 @@ def interpretation_edit(
|
||||||
book_id: int,
|
book_id: int,
|
||||||
interpretation_id: int,
|
interpretation_id: int,
|
||||||
):
|
):
|
||||||
|
interpretation: m.Interpretation = db.session.get(
|
||||||
|
m.Interpretation, interpretation_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if interpretation and interpretation.user_id != current_user.id:
|
||||||
|
flash("You dont have permission to edit this interpretation", "danger")
|
||||||
|
return redirect(url_for("book.collection_view", book_id=book_id))
|
||||||
|
|
||||||
form = f.EditInterpretationForm()
|
form = f.EditInterpretationForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
text = form.text.data
|
text = form.text.data
|
||||||
interpretation_id = form.interpretation_id.data
|
|
||||||
interpretation: m.Interpretation = db.session.get(
|
|
||||||
m.Interpretation, interpretation_id
|
|
||||||
)
|
|
||||||
redirect_url = url_for(
|
redirect_url = url_for(
|
||||||
"book.interpretation_view",
|
"book.interpretation_view",
|
||||||
book_id=book_id,
|
book_id=book_id,
|
||||||
|
@ -161,26 +173,24 @@ def interpretation_edit(
|
||||||
"/<int:book_id>/<int:interpretation_id>/delete_interpretation", methods=["POST"]
|
"/<int:book_id>/<int:interpretation_id>/delete_interpretation", methods=["POST"]
|
||||||
)
|
)
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||||
|
access=[m.Permission.Access.D],
|
||||||
|
entities=[m.Interpretation],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def interpretation_delete(
|
def interpretation_delete(
|
||||||
book_id: int,
|
book_id: int,
|
||||||
interpretation_id: int,
|
interpretation_id: int,
|
||||||
):
|
):
|
||||||
form = f.DeleteInterpretationForm()
|
form = f.DeleteInterpretationForm()
|
||||||
interpretation_id = form.interpretation_id.data
|
|
||||||
interpretation: m.Interpretation = db.session.get(
|
interpretation: m.Interpretation = db.session.get(
|
||||||
m.Interpretation, interpretation_id
|
m.Interpretation, interpretation_id
|
||||||
)
|
)
|
||||||
if not interpretation or interpretation.is_deleted:
|
if not interpretation or interpretation.is_deleted:
|
||||||
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
|
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
|
||||||
flash("Interpretation not found", "danger")
|
flash("Interpretation not found", "danger")
|
||||||
return redirect(
|
return redirect(url_for("book.collection_view", book_id=book_id))
|
||||||
url_for(
|
|
||||||
"book.interpretation_view",
|
|
||||||
book_id=book_id,
|
|
||||||
section_id=interpretation.section_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
form = f.DeleteInterpretationForm()
|
form = f.DeleteInterpretationForm()
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
interpretation.is_deleted = True
|
interpretation.is_deleted = True
|
||||||
|
@ -209,7 +219,7 @@ def qa_view(book_id: int, interpretation_id: int):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
if not book or book.is_deleted:
|
if not book or book.is_deleted:
|
||||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
||||||
flash("You are not owner of this book!", "danger")
|
flash("Book not found!", "danger")
|
||||||
return redirect(url_for("book.my_library"))
|
return redirect(url_for("book.my_library"))
|
||||||
|
|
||||||
interpretation: m.Interpretation = db.session.get(
|
interpretation: m.Interpretation = db.session.get(
|
||||||
|
|
|
@ -8,12 +8,18 @@ from flask_login import login_required
|
||||||
from app.controllers import register_book_verify_route
|
from app.controllers import register_book_verify_route
|
||||||
from app.controllers.delete_nested_book_entities import delete_nested_section_entities
|
from app.controllers.delete_nested_book_entities import delete_nested_section_entities
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/<int:collection_id>/create_section", methods=["POST"])
|
@bp.route("/<int:book_id>/<int:collection_id>/create_section", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.SECTION,
|
||||||
|
access=[m.Permission.Access.C],
|
||||||
|
entities=[m.Collection],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def section_create(book_id: int, collection_id: int):
|
def section_create(book_id: int, collection_id: int):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
|
@ -38,6 +44,13 @@ def section_create(book_id: int, collection_id: int):
|
||||||
log(log.INFO, "Create section [%s]. Collection: [%s]", section, collection_id)
|
log(log.INFO, "Create section [%s]. Collection: [%s]", section, collection_id)
|
||||||
section.save()
|
section.save()
|
||||||
|
|
||||||
|
# access groups
|
||||||
|
for access_group in section.collection.access_groups:
|
||||||
|
m.SectionAccessGroups(
|
||||||
|
section_id=section.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
# -------------
|
||||||
|
|
||||||
flash("Success!", "success")
|
flash("Success!", "success")
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
else:
|
else:
|
||||||
|
@ -51,6 +64,11 @@ def section_create(book_id: int, collection_id: int):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/<int:section_id>/edit_section", methods=["POST"])
|
@bp.route("/<int:book_id>/<int:section_id>/edit_section", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.SECTION,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Section],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def section_edit(book_id: int, section_id: int):
|
def section_edit(book_id: int, section_id: int):
|
||||||
section: m.Section = db.session.get(m.Section, section_id)
|
section: m.Section = db.session.get(m.Section, section_id)
|
||||||
|
@ -79,6 +97,11 @@ def section_edit(book_id: int, section_id: int):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/<int:section_id>/delete_section", methods=["POST"])
|
@bp.route("/<int:book_id>/<int:section_id>/delete_section", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.SECTION,
|
||||||
|
access=[m.Permission.Access.D],
|
||||||
|
entities=[m.Section],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def section_delete(
|
def section_delete(
|
||||||
book_id: int,
|
book_id: int,
|
||||||
|
|
|
@ -10,12 +10,18 @@ from app.controllers import (
|
||||||
register_book_verify_route,
|
register_book_verify_route,
|
||||||
)
|
)
|
||||||
from app import models as m, db, forms as f
|
from app import models as m, db, forms as f
|
||||||
|
from app.controllers.require_permission import require_permission
|
||||||
from app.logger import log
|
from app.logger import log
|
||||||
from .bp import bp
|
from .bp import bp
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/settings", methods=["GET"])
|
@bp.route("/<int:book_id>/settings", methods=["GET"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def settings(book_id: int):
|
def settings(book_id: int):
|
||||||
book: m.Book = db.session.get(m.Book, book_id)
|
book: m.Book = db.session.get(m.Book, book_id)
|
||||||
|
@ -27,13 +33,19 @@ def settings(book_id: int):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/add_contributor", methods=["POST"])
|
@bp.route("/<int:book_id>/add_contributor", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def add_contributor(book_id: int):
|
def add_contributor(book_id: int):
|
||||||
form = f.AddContributorForm()
|
form = f.AddContributorForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
|
user_id = form.user_id.data
|
||||||
book_contributor = m.BookContributor.query.filter_by(
|
book_contributor = m.BookContributor.query.filter_by(
|
||||||
user_id=form.user_id.data, book_id=book_id
|
user_id=user_id, book_id=book_id
|
||||||
).first()
|
).first()
|
||||||
if book_contributor:
|
if book_contributor:
|
||||||
log(log.INFO, "Contributor: [%s] already exists", book_contributor)
|
log(log.INFO, "Contributor: [%s] already exists", book_contributor)
|
||||||
|
@ -41,12 +53,22 @@ def add_contributor(book_id: int):
|
||||||
return redirect(url_for("book.settings", book_id=book_id))
|
return redirect(url_for("book.settings", book_id=book_id))
|
||||||
|
|
||||||
role = m.BookContributor.Roles(int(form.role.data))
|
role = m.BookContributor.Roles(int(form.role.data))
|
||||||
contributor = m.BookContributor(
|
contributor = m.BookContributor(user_id=user_id, book_id=book_id, role=role)
|
||||||
user_id=form.user_id.data, book_id=book_id, role=role
|
|
||||||
)
|
|
||||||
log(log.INFO, "New contributor [%s]", contributor)
|
log(log.INFO, "New contributor [%s]", contributor)
|
||||||
contributor.save()
|
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")
|
flash("Contributor was added!", "success")
|
||||||
return redirect(url_for("book.settings", book_id=book_id))
|
return redirect(url_for("book.settings", book_id=book_id))
|
||||||
else:
|
else:
|
||||||
|
@ -60,24 +82,47 @@ def add_contributor(book_id: int):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/delete_contributor", methods=["POST"])
|
@bp.route("/<int:book_id>/delete_contributor", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def delete_contributor(book_id: int):
|
def delete_contributor(book_id: int):
|
||||||
form = f.DeleteContributorForm()
|
form = f.DeleteContributorForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
|
user_id = int(form.user_id.data)
|
||||||
book_contributor = m.BookContributor.query.filter_by(
|
book_contributor = m.BookContributor.query.filter_by(
|
||||||
user_id=int(form.user_id.data), book_id=book_id
|
user_id=user_id, book_id=book_id
|
||||||
).first()
|
).first()
|
||||||
if not book_contributor:
|
if not book_contributor:
|
||||||
log(
|
log(
|
||||||
log.INFO,
|
log.INFO,
|
||||||
"BookContributor does not exists user: [%s], book: [%s]",
|
"BookContributor does not exists user: [%s], book: [%s]",
|
||||||
form.user_id.data,
|
user_id,
|
||||||
book_id,
|
book_id,
|
||||||
)
|
)
|
||||||
flash("Does not exists!", "success")
|
flash("Does not exists!", "success")
|
||||||
return redirect(url_for("book.settings", book_id=book_id))
|
return redirect(url_for("book.settings", 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)
|
log(log.INFO, "Delete BookContributor [%s]", book_contributor)
|
||||||
db.session.delete(book_contributor)
|
db.session.delete(book_contributor)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
@ -95,12 +140,17 @@ def delete_contributor(book_id: int):
|
||||||
|
|
||||||
@bp.route("/<int:book_id>/edit_contributor_role", methods=["POST"])
|
@bp.route("/<int:book_id>/edit_contributor_role", methods=["POST"])
|
||||||
@register_book_verify_route(bp.name)
|
@register_book_verify_route(bp.name)
|
||||||
|
@require_permission(
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
access=[m.Permission.Access.U],
|
||||||
|
entities=[m.Book],
|
||||||
|
)
|
||||||
@login_required
|
@login_required
|
||||||
def edit_contributor_role(book_id: int):
|
def edit_contributor_role(book_id: int):
|
||||||
form = f.EditContributorRoleForm()
|
form = f.EditContributorRoleForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
book_contributor = m.BookContributor.query.filter_by(
|
book_contributor: m.BookContributor = m.BookContributor.query.filter_by(
|
||||||
user_id=int(form.user_id.data), book_id=book_id
|
user_id=int(form.user_id.data), book_id=book_id
|
||||||
).first()
|
).first()
|
||||||
if not book_contributor:
|
if not book_contributor:
|
||||||
|
@ -114,6 +164,24 @@ def edit_contributor_role(book_id: int):
|
||||||
return redirect(url_for("book.settings", book_id=book_id))
|
return redirect(url_for("book.settings", book_id=book_id))
|
||||||
|
|
||||||
role = m.BookContributor.Roles(int(form.role.data))
|
role = m.BookContributor.Roles(int(form.role.data))
|
||||||
|
|
||||||
|
# change access group
|
||||||
|
current_group = m.AccessGroup.query.filter_by(
|
||||||
|
book_id=book_id, name=book_contributor.role.name.lower()
|
||||||
|
).first()
|
||||||
|
user_access_group = m.UserAccessGroups.query.filter_by(
|
||||||
|
user_id=book_contributor.user_id, access_group_id=current_group.id
|
||||||
|
).first()
|
||||||
|
if user_access_group:
|
||||||
|
db.session.delete(user_access_group)
|
||||||
|
|
||||||
|
new_group = m.AccessGroup.query.filter_by(
|
||||||
|
book_id=book_id, name=role.name.lower()
|
||||||
|
).first()
|
||||||
|
m.UserAccessGroups(
|
||||||
|
user_id=book_contributor.user_id, access_group_id=new_group.id
|
||||||
|
).save(False)
|
||||||
|
|
||||||
book_contributor.role = role
|
book_contributor.role = role
|
||||||
|
|
||||||
log(
|
log(
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
from flask import redirect, url_for, Blueprint, 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"))
|
||||||
|
|
||||||
|
# TODO process data from checkbox tree
|
||||||
|
# permissions = json.loads(form.permissions.data)
|
||||||
|
|
||||||
|
return {"status": "ok"}
|
|
@ -1,34 +0,0 @@
|
||||||
"""user wallet_id
|
|
||||||
|
|
||||||
Revision ID: 067a10a531d7
|
|
||||||
Revises: bbc4b55246ba
|
|
||||||
Create Date: 2023-05-03 11:53:14.455999
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "067a10a531d7"
|
|
||||||
down_revision = "bbc4b55246ba"
|
|
||||||
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("wallet_id", sa.String(length=255), 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("wallet_id")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,44 +0,0 @@
|
||||||
"""book-tags
|
|
||||||
|
|
||||||
Revision ID: 0961578f302a
|
|
||||||
Revises: 5df1fabbee7d
|
|
||||||
Create Date: 2023-05-16 10:58:44.518470
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "0961578f302a"
|
|
||||||
down_revision = "a9df3da8cd00"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table(
|
|
||||||
"book_tags",
|
|
||||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["book_id"],
|
|
||||||
["books.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["tag_id"],
|
|
||||||
["tags.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table("book_tags")
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,32 +0,0 @@
|
||||||
"""comment_edited
|
|
||||||
|
|
||||||
Revision ID: 1dfa1f2c208f
|
|
||||||
Revises: 2ec60080de3b
|
|
||||||
Create Date: 2023-05-09 17:22:23.028408
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "1dfa1f2c208f"
|
|
||||||
down_revision = "2ec60080de3b"
|
|
||||||
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.add_column(sa.Column("edited", sa.Boolean(), nullable=True))
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
|
||||||
batch_op.drop_column("edited")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,42 +0,0 @@
|
||||||
"""wallet_id length
|
|
||||||
|
|
||||||
Revision ID: 2ec60080de3b
|
|
||||||
Revises: 4ce4bacc7c06
|
|
||||||
Create Date: 2023-05-05 16:47:55.533205
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "2ec60080de3b"
|
|
||||||
down_revision = "4ce4bacc7c06"
|
|
||||||
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.alter_column(
|
|
||||||
"wallet_id",
|
|
||||||
existing_type=sa.VARCHAR(length=256),
|
|
||||||
type_=sa.String(length=64),
|
|
||||||
existing_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.alter_column(
|
|
||||||
"wallet_id",
|
|
||||||
existing_type=sa.String(length=64),
|
|
||||||
type_=sa.VARCHAR(length=256),
|
|
||||||
existing_nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,38 +0,0 @@
|
||||||
"""user_is_activated
|
|
||||||
|
|
||||||
Revision ID: 377fc0b7e4bb
|
|
||||||
Revises: a1345b416f81
|
|
||||||
Create Date: 2023-05-04 11:14:58.810826
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "377fc0b7e4bb"
|
|
||||||
down_revision = "a1345b416f81"
|
|
||||||
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_activated", sa.Boolean(), nullable=True))
|
|
||||||
batch_op.alter_column(
|
|
||||||
"username", existing_type=sa.VARCHAR(length=60), 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.alter_column(
|
|
||||||
"username", existing_type=sa.VARCHAR(length=60), nullable=False
|
|
||||||
)
|
|
||||||
batch_op.drop_column("is_activated")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,66 +0,0 @@
|
||||||
"""created_at_on_inter
|
|
||||||
|
|
||||||
Revision ID: 4ce4bacc7c06
|
|
||||||
Revises: 377fc0b7e4bb
|
|
||||||
Create Date: 2023-05-05 16:31:52.963720
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "4ce4bacc7c06"
|
|
||||||
down_revision = "377fc0b7e4bb"
|
|
||||||
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.alter_column(
|
|
||||||
"username",
|
|
||||||
existing_type=sa.VARCHAR(length=60),
|
|
||||||
type_=sa.String(length=64),
|
|
||||||
existing_nullable=True,
|
|
||||||
)
|
|
||||||
batch_op.alter_column(
|
|
||||||
"password_hash",
|
|
||||||
existing_type=sa.VARCHAR(length=255),
|
|
||||||
type_=sa.String(length=256),
|
|
||||||
existing_nullable=True,
|
|
||||||
)
|
|
||||||
batch_op.alter_column(
|
|
||||||
"wallet_id",
|
|
||||||
existing_type=sa.VARCHAR(length=255),
|
|
||||||
type_=sa.String(length=256),
|
|
||||||
existing_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.alter_column(
|
|
||||||
"wallet_id",
|
|
||||||
existing_type=sa.String(length=256),
|
|
||||||
type_=sa.VARCHAR(length=255),
|
|
||||||
existing_nullable=True,
|
|
||||||
)
|
|
||||||
batch_op.alter_column(
|
|
||||||
"password_hash",
|
|
||||||
existing_type=sa.String(length=256),
|
|
||||||
type_=sa.VARCHAR(length=255),
|
|
||||||
existing_nullable=True,
|
|
||||||
)
|
|
||||||
batch_op.alter_column(
|
|
||||||
"username",
|
|
||||||
existing_type=sa.String(length=64),
|
|
||||||
type_=sa.VARCHAR(length=60),
|
|
||||||
existing_nullable=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,47 +0,0 @@
|
||||||
"""approved fields
|
|
||||||
|
|
||||||
Revision ID: 5df1fabbee7d
|
|
||||||
Revises: 1dfa1f2c208f
|
|
||||||
Create Date: 2023-05-11 15:06:42.883725
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "5df1fabbee7d"
|
|
||||||
down_revision = "1dfa1f2c208f"
|
|
||||||
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.add_column(sa.Column("approved", sa.Boolean(), nullable=True))
|
|
||||||
batch_op.drop_column("included_with_interpretation")
|
|
||||||
|
|
||||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("approved", sa.Boolean(), nullable=True))
|
|
||||||
|
|
||||||
# ### 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.drop_column("approved")
|
|
||||||
|
|
||||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
|
||||||
batch_op.add_column(
|
|
||||||
sa.Column(
|
|
||||||
"included_with_interpretation",
|
|
||||||
sa.BOOLEAN(),
|
|
||||||
autoincrement=False,
|
|
||||||
nullable=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
batch_op.drop_column("approved")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -0,0 +1,311 @@
|
||||||
|
"""init
|
||||||
|
|
||||||
|
Revision ID: 79e8c7bff9c9
|
||||||
|
Revises:
|
||||||
|
Create Date: 2023-06-01 15:31:33.635236
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '79e8c7bff9c9'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('permissions',
|
||||||
|
sa.Column('access', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('entity_type', sa.Enum('UNKNOWN', 'BOOK', 'COLLECTION', 'SECTION', 'INTERPRETATION', 'COMMENT', name='entity'), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('tags',
|
||||||
|
sa.Column('name', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('name')
|
||||||
|
)
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('username', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('password_hash', sa.String(length=256), nullable=True),
|
||||||
|
sa.Column('is_activated', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('wallet_id', sa.String(length=64), nullable=True),
|
||||||
|
sa.Column('avatar_img', sa.Text(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('username')
|
||||||
|
)
|
||||||
|
op.create_table('books',
|
||||||
|
sa.Column('label', sa.String(length=256), nullable=False),
|
||||||
|
sa.Column('about', sa.Text(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('access_groups',
|
||||||
|
sa.Column('name', sa.String(length=32), nullable=False),
|
||||||
|
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('book_contributors',
|
||||||
|
sa.Column('role', sa.Enum('UNKNOWN', 'MODERATOR', 'EDITOR', name='roles'), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('book_tags',
|
||||||
|
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('book_versions',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('semver', sa.String(length=16), nullable=False),
|
||||||
|
sa.Column('exported', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('derivative_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['derivative_id'], ['book_versions.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('books_stars',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('books_access_groups',
|
||||||
|
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('collections',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('label', sa.String(length=256), nullable=False),
|
||||||
|
sa.Column('about', sa.Text(), nullable=True),
|
||||||
|
sa.Column('is_root', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('is_leaf', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('version_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['parent_id'], ['collections.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['version_id'], ['book_versions.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('permissions_access_groups',
|
||||||
|
sa.Column('permission_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('users_access_groups',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('collections_access_groups',
|
||||||
|
sa.Column('collection_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('sections',
|
||||||
|
sa.Column('label', sa.String(length=256), nullable=False),
|
||||||
|
sa.Column('collection_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('version_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('selected_interpretation_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['version_id'], ['book_versions.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('interpretations',
|
||||||
|
sa.Column('text', sa.Text(), nullable=False),
|
||||||
|
sa.Column('plain_text', sa.Text(), nullable=True),
|
||||||
|
sa.Column('approved', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('marked', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('section_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('section_tags',
|
||||||
|
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('section_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('sections_access_groups',
|
||||||
|
sa.Column('section_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('comments',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('text', sa.Text(), nullable=False),
|
||||||
|
sa.Column('approved', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('marked', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('edited', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['parent_id'], ['comments.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('interpretation_tags',
|
||||||
|
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('interpretation_votes',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('positive', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('interpretations_access_groups',
|
||||||
|
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('comment_tags',
|
||||||
|
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('comment_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_table('comment_votes',
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('comment_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('positive', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||||
|
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('comment_votes')
|
||||||
|
op.drop_table('comment_tags')
|
||||||
|
op.drop_table('interpretations_access_groups')
|
||||||
|
op.drop_table('interpretation_votes')
|
||||||
|
op.drop_table('interpretation_tags')
|
||||||
|
op.drop_table('comments')
|
||||||
|
op.drop_table('sections_access_groups')
|
||||||
|
op.drop_table('section_tags')
|
||||||
|
op.drop_table('interpretations')
|
||||||
|
op.drop_table('sections')
|
||||||
|
op.drop_table('collections_access_groups')
|
||||||
|
op.drop_table('users_access_groups')
|
||||||
|
op.drop_table('permissions_access_groups')
|
||||||
|
op.drop_table('collections')
|
||||||
|
op.drop_table('books_access_groups')
|
||||||
|
op.drop_table('books_stars')
|
||||||
|
op.drop_table('book_versions')
|
||||||
|
op.drop_table('book_tags')
|
||||||
|
op.drop_table('book_contributors')
|
||||||
|
op.drop_table('access_groups')
|
||||||
|
op.drop_table('books')
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_table('tags')
|
||||||
|
op.drop_table('permissions')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -1,44 +0,0 @@
|
||||||
"""section-tags
|
|
||||||
|
|
||||||
Revision ID: 7baa732e01c6
|
|
||||||
Revises: 0961578f302a
|
|
||||||
Create Date: 2023-05-17 18:34:29.178354
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "7baa732e01c6"
|
|
||||||
down_revision = "0961578f302a"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table(
|
|
||||||
"section_tags",
|
|
||||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("section_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["section_id"],
|
|
||||||
["sections.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["tag_id"],
|
|
||||||
["tags.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table("section_tags")
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,44 +0,0 @@
|
||||||
"""remove fields from section and interpretation
|
|
||||||
|
|
||||||
Revision ID: 883298018384
|
|
||||||
Revises: 5df1fabbee7d
|
|
||||||
Create Date: 2023-05-18 15:49:20.145655
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "883298018384"
|
|
||||||
down_revision = "5df1fabbee7d"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
|
||||||
batch_op.drop_column("label")
|
|
||||||
|
|
||||||
with op.batch_alter_table("sections", schema=None) as batch_op:
|
|
||||||
batch_op.drop_column("about")
|
|
||||||
|
|
||||||
# ### 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("about", sa.TEXT(), autoincrement=False, nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
|
||||||
batch_op.add_column(
|
|
||||||
sa.Column(
|
|
||||||
"label", sa.VARCHAR(length=256), autoincrement=False, nullable=False
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,32 +0,0 @@
|
||||||
"""user avatar_img
|
|
||||||
|
|
||||||
Revision ID: a1345b416f81
|
|
||||||
Revises: 067a10a531d7
|
|
||||||
Create Date: 2023-05-04 09:11:09.406698
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "a1345b416f81"
|
|
||||||
down_revision = "067a10a531d7"
|
|
||||||
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("avatar_img", sa.Text(), 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("avatar_img")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,32 +0,0 @@
|
||||||
"""plain_text
|
|
||||||
|
|
||||||
Revision ID: a9df3da8cd00
|
|
||||||
Revises: 883298018384
|
|
||||||
Create Date: 2023-05-23 10:42:06.239982
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "a9df3da8cd00"
|
|
||||||
down_revision = "883298018384"
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column("plain_text", sa.Text()))
|
|
||||||
|
|
||||||
# ### 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.drop_column("plain_text")
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -1,293 +0,0 @@
|
||||||
"""init
|
|
||||||
|
|
||||||
Revision ID: bbc4b55246ba
|
|
||||||
Revises:
|
|
||||||
Create Date: 2023-04-28 10:13:52.011272
|
|
||||||
|
|
||||||
"""
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision = "bbc4b55246ba"
|
|
||||||
down_revision = None
|
|
||||||
branch_labels = None
|
|
||||||
depends_on = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table(
|
|
||||||
"tags",
|
|
||||||
sa.Column("name", sa.String(length=32), nullable=False),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("name"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"users",
|
|
||||||
sa.Column("username", sa.String(length=60), nullable=False),
|
|
||||||
sa.Column("password_hash", sa.String(length=255), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
sa.UniqueConstraint("username"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"books",
|
|
||||||
sa.Column("label", sa.String(length=256), nullable=False),
|
|
||||||
sa.Column("about", sa.Text(), nullable=True),
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"book_contributors",
|
|
||||||
sa.Column(
|
|
||||||
"role",
|
|
||||||
sa.Enum("UNKNOWN", "MODERATOR", "EDITOR", name="roles"),
|
|
||||||
nullable=True,
|
|
||||||
),
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["book_id"],
|
|
||||||
["books.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"book_versions",
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("semver", sa.String(length=16), nullable=False),
|
|
||||||
sa.Column("exported", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("derivative_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["book_id"],
|
|
||||||
["books.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["derivative_id"],
|
|
||||||
["book_versions.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"books_stars",
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["book_id"],
|
|
||||||
["books.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"collections",
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("label", sa.String(length=256), nullable=False),
|
|
||||||
sa.Column("about", sa.Text(), nullable=True),
|
|
||||||
sa.Column("is_root", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("is_leaf", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("version_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("parent_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["parent_id"],
|
|
||||||
["collections.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["version_id"],
|
|
||||||
["book_versions.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"sections",
|
|
||||||
sa.Column("label", sa.String(length=256), nullable=False),
|
|
||||||
sa.Column("about", sa.Text(), nullable=True),
|
|
||||||
sa.Column("collection_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("version_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("selected_interpretation_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["collection_id"],
|
|
||||||
["collections.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["version_id"],
|
|
||||||
["book_versions.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"interpretations",
|
|
||||||
sa.Column("label", sa.String(length=256), nullable=False),
|
|
||||||
sa.Column("text", sa.Text(), nullable=False),
|
|
||||||
sa.Column("marked", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("section_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["section_id"],
|
|
||||||
["sections.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"comments",
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("text", sa.Text(), nullable=False),
|
|
||||||
sa.Column("marked", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("included_with_interpretation", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("parent_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("interpretation_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["interpretation_id"],
|
|
||||||
["interpretations.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["parent_id"],
|
|
||||||
["comments.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"interpretation_tags",
|
|
||||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("interpretation_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["interpretation_id"],
|
|
||||||
["interpretations.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["tag_id"],
|
|
||||||
["tags.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"interpretation_votes",
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("interpretation_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("positive", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["interpretation_id"],
|
|
||||||
["interpretations.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"comment_tags",
|
|
||||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("comment_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["comment_id"],
|
|
||||||
["comments.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["tag_id"],
|
|
||||||
["tags.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
op.create_table(
|
|
||||||
"comment_votes",
|
|
||||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("comment_id", sa.Integer(), nullable=True),
|
|
||||||
sa.Column("positive", sa.Boolean(), nullable=True),
|
|
||||||
sa.Column("id", sa.Integer(), nullable=False),
|
|
||||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
|
||||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["comment_id"],
|
|
||||||
["comments.id"],
|
|
||||||
),
|
|
||||||
sa.ForeignKeyConstraint(
|
|
||||||
["user_id"],
|
|
||||||
["users.id"],
|
|
||||||
),
|
|
||||||
sa.PrimaryKeyConstraint("id"),
|
|
||||||
)
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.drop_table("comment_votes")
|
|
||||||
op.drop_table("comment_tags")
|
|
||||||
op.drop_table("interpretation_votes")
|
|
||||||
op.drop_table("interpretation_tags")
|
|
||||||
op.drop_table("comments")
|
|
||||||
op.drop_table("interpretations")
|
|
||||||
op.drop_table("sections")
|
|
||||||
op.drop_table("collections")
|
|
||||||
op.drop_table("books_stars")
|
|
||||||
op.drop_table("book_versions")
|
|
||||||
op.drop_table("book_contributors")
|
|
||||||
op.drop_table("books")
|
|
||||||
op.drop_table("users")
|
|
||||||
op.drop_table("tags")
|
|
||||||
# ### end Alembic commands ###
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -37,10 +37,12 @@ export function deleteInterpretation() {
|
||||||
const interpretationId = btn.getAttribute('data-interpretation-id');
|
const interpretationId = btn.getAttribute('data-interpretation-id');
|
||||||
interpretationIdInDeleteInterpretationModal.value = interpretationId;
|
interpretationIdInDeleteInterpretationModal.value = interpretationId;
|
||||||
let newActionPath: string = '';
|
let newActionPath: string = '';
|
||||||
|
|
||||||
newActionPath = defaultActionPath.replace(
|
newActionPath = defaultActionPath.replace(
|
||||||
'0/interpretation_delete',
|
'0/delete_interpretation',
|
||||||
`${interpretationId}/interpretation_delete`,
|
`${interpretationId}/delete_interpretation`,
|
||||||
);
|
);
|
||||||
|
console.log(defaultActionPath);
|
||||||
|
|
||||||
deleteInterpretationForm.setAttribute('action', `${newActionPath}`);
|
deleteInterpretationForm.setAttribute('action', `${newActionPath}`);
|
||||||
interpretationDeleteModal.show();
|
interpretationDeleteModal.show();
|
||||||
|
|
|
@ -21,6 +21,8 @@ import {renameSubCollection} from './renameSubCollection';
|
||||||
import {initQuillReadOnly} from './initQuillReadOnly';
|
import {initQuillReadOnly} from './initQuillReadOnly';
|
||||||
import {initGoBack} from './tabGoBackBtn';
|
import {initGoBack} from './tabGoBackBtn';
|
||||||
import {scroll} from './scroll';
|
import {scroll} from './scroll';
|
||||||
|
import {initCheckBoxTree} from './checkBoxTree';
|
||||||
|
import {initPermissions} from './permissions';
|
||||||
import {copyLink} from './copyLink';
|
import {copyLink} from './copyLink';
|
||||||
import {quickSearch} from './quickSearch';
|
import {quickSearch} from './quickSearch';
|
||||||
import {flash} from './flash';
|
import {flash} from './flash';
|
||||||
|
@ -51,6 +53,8 @@ deleteSubCollection();
|
||||||
renameSubCollection();
|
renameSubCollection();
|
||||||
initGoBack();
|
initGoBack();
|
||||||
scroll();
|
scroll();
|
||||||
|
initCheckBoxTree();
|
||||||
|
initPermissions();
|
||||||
copyLink();
|
copyLink();
|
||||||
quickSearch();
|
quickSearch();
|
||||||
flash();
|
flash();
|
||||||
|
|
|
@ -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;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -21,8 +21,7 @@ def test_approve_interpretation(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response
|
assert response
|
||||||
assert response.status_code == 404
|
assert b"You do not have permission" in response.data
|
||||||
assert response.json["message"] == "Interpretation not found"
|
|
||||||
|
|
||||||
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
||||||
user_id=dummy_user.id
|
user_id=dummy_user.id
|
||||||
|
@ -33,7 +32,7 @@ def test_approve_interpretation(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response
|
assert response
|
||||||
assert response.json["message"] == "You dont have permission"
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
||||||
user_id=user.id
|
user_id=user.id
|
||||||
|
@ -78,8 +77,7 @@ def test_approve_comment(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response
|
assert response
|
||||||
assert response.status_code == 404
|
assert b"You do not have permission" in response.data
|
||||||
assert response.json["message"] == "Comment not found"
|
|
||||||
|
|
||||||
comment: m.Comment = m.Comment.query.filter_by(user_id=dummy_user.id).first()
|
comment: m.Comment = m.Comment.query.filter_by(user_id=dummy_user.id).first()
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
|
@ -88,7 +86,7 @@ def test_approve_comment(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response
|
assert response
|
||||||
assert response.json["message"] == "You dont have permission"
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
comment: m.Comment = m.Comment.query.filter_by(user_id=user.id).first()
|
comment: m.Comment = m.Comment.query.filter_by(user_id=user.id).first()
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
# flake8: noqa F501
|
from flask import current_app as Response
|
||||||
from flask import current_app as Response, url_for
|
|
||||||
from flask.testing import FlaskClient, FlaskCliRunner
|
from flask.testing import FlaskClient, FlaskCliRunner
|
||||||
|
|
||||||
from app import models as m, db
|
from app import models as m, db
|
||||||
|
from app.controllers.create_access_groups import create_moderator_group
|
||||||
from tests.utils import (
|
from tests.utils import (
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
|
@ -10,6 +10,7 @@ from tests.utils import (
|
||||||
check_if_nested_collection_entities_is_deleted,
|
check_if_nested_collection_entities_is_deleted,
|
||||||
check_if_nested_section_entities_is_deleted,
|
check_if_nested_section_entities_is_deleted,
|
||||||
check_if_nested_interpretation_entities_is_deleted,
|
check_if_nested_interpretation_entities_is_deleted,
|
||||||
|
create_test_book,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +31,7 @@ def test_create_edit_delete_book(client: FlaskClient):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Label must be between 6 and 256 characters long." in response.data
|
assert b"Label must be between 6 and 256 characters long." in response.data
|
||||||
|
|
||||||
book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
book: m.Book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
||||||
|
|
||||||
assert not book
|
assert not book
|
||||||
assert not m.Book.query.count()
|
assert not m.Book.query.count()
|
||||||
|
@ -47,7 +48,7 @@ def test_create_edit_delete_book(client: FlaskClient):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Label must be between 6 and 256 characters long." in response.data
|
assert b"Label must be between 6 and 256 characters long." in response.data
|
||||||
|
|
||||||
book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
book: m.Book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
||||||
|
|
||||||
assert not book
|
assert not book
|
||||||
assert not m.Book.query.count()
|
assert not m.Book.query.count()
|
||||||
|
@ -63,11 +64,18 @@ def test_create_edit_delete_book(client: FlaskClient):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Book added!" in response.data
|
assert b"Book added!" in response.data
|
||||||
|
|
||||||
book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
book: m.Book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
||||||
|
|
||||||
assert book
|
assert book
|
||||||
assert book.versions
|
assert book.versions
|
||||||
assert len(book.versions) == 1
|
assert len(book.versions) == 1
|
||||||
|
assert book.access_groups
|
||||||
|
assert len(book.access_groups) == 2
|
||||||
|
|
||||||
|
root_collection: m.Collection = book.last_version.collections[0]
|
||||||
|
assert root_collection
|
||||||
|
assert root_collection.access_groups
|
||||||
|
assert len(root_collection.access_groups) == 2
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
"/book/999/edit",
|
"/book/999/edit",
|
||||||
|
@ -79,7 +87,7 @@ def test_create_edit_delete_book(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/edit",
|
f"/book/{book.id}/edit",
|
||||||
|
@ -106,17 +114,17 @@ def test_create_edit_delete_book(client: FlaskClient):
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Success!" in response.data
|
assert b"Success!" in response.data
|
||||||
book = db.session.get(m.Book, book.id)
|
book = db.session.get(m.Book, book.id)
|
||||||
assert book.is_deleted == True
|
assert book.is_deleted
|
||||||
check_if_nested_book_entities_is_deleted(book)
|
check_if_nested_book_entities_is_deleted(book)
|
||||||
|
|
||||||
|
|
||||||
def test_add_contributor(client: FlaskClient):
|
def test_add_delete_contributor(client: FlaskClient):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
|
||||||
moderator = m.User(username="Moderator", password="test").save()
|
moderator = m.User(username="Moderator", password="test").save()
|
||||||
|
|
||||||
moderators_book = m.Book(label="Test Book", user_id=moderator.id).save()
|
moderators_book: m.Book = create_test_book(moderator.id)
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{moderators_book.id}/add_contributor",
|
f"/book/{moderators_book.id}/add_contributor",
|
||||||
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
||||||
|
@ -124,9 +132,10 @@ def test_add_contributor(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
book = m.Book(label="Test Book", user_id=user.id).save()
|
book: m.Book = create_test_book(user.id)
|
||||||
|
m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/add_contributor",
|
f"/book/{book.id}/add_contributor",
|
||||||
|
@ -136,6 +145,12 @@ def test_add_contributor(client: FlaskClient):
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Contributor was added!" in response.data
|
assert b"Contributor was added!" in response.data
|
||||||
|
moderator: m.User = db.session.get(m.User, moderator.id)
|
||||||
|
assert moderator.access_groups
|
||||||
|
for access_group in moderator.access_groups:
|
||||||
|
access_group: m.AccessGroup
|
||||||
|
assert access_group.book_id == book.id
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/add_contributor",
|
f"/book/{book.id}/add_contributor",
|
||||||
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
||||||
|
@ -149,7 +164,7 @@ def test_add_contributor(client: FlaskClient):
|
||||||
user=moderator, book=book
|
user=moderator, book=book
|
||||||
).first()
|
).first()
|
||||||
assert contributor.role == m.BookContributor.Roles.MODERATOR
|
assert contributor.role == m.BookContributor.Roles.MODERATOR
|
||||||
assert len(book.contributors) == 1
|
assert len(book.contributors) == 2
|
||||||
|
|
||||||
editor = m.User(username="Editor", password="test").save()
|
editor = m.User(username="Editor", password="test").save()
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
|
@ -165,30 +180,20 @@ def test_add_contributor(client: FlaskClient):
|
||||||
user=editor, book=book
|
user=editor, book=book
|
||||||
).first()
|
).first()
|
||||||
assert contributor.role == m.BookContributor.Roles.EDITOR
|
assert contributor.role == m.BookContributor.Roles.EDITOR
|
||||||
assert len(book.contributors) == 2
|
assert len(book.contributors) == 3
|
||||||
|
|
||||||
|
contributor_to_delete = m.BookContributor.query.filter_by(
|
||||||
|
user_id=moderator.id, book_id=book.id
|
||||||
|
).first()
|
||||||
|
|
||||||
def test_delete_contributor(client: FlaskClient, runner: FlaskCliRunner):
|
assert moderator.access_groups
|
||||||
_, user = login(client)
|
|
||||||
user: m.User
|
|
||||||
|
|
||||||
# add dummmy data
|
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
|
||||||
book = db.session.get(m.Book, 1)
|
|
||||||
book.user_id = user.id
|
|
||||||
book.save()
|
|
||||||
|
|
||||||
contributors_len = len(book.contributors)
|
|
||||||
assert contributors_len
|
|
||||||
|
|
||||||
contributor_to_delete = book.contributors[0]
|
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/delete_contributor",
|
f"/book/{book.id}/delete_contributor",
|
||||||
data=dict(user_id=contributor_to_delete.user_id),
|
data=dict(user_id=contributor_to_delete.user_id),
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
moderator: m.User = db.session.get(m.User, moderator.id)
|
||||||
|
assert not moderator.access_groups
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"Success!" in response.data
|
assert b"Success!" in response.data
|
||||||
|
@ -209,17 +214,19 @@ def test_delete_contributor(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
|
||||||
# add dummmy data
|
book = create_test_book(user.id)
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
# for contributor in m.BookContributor.query.all():
|
||||||
|
# db.session.delete(contributor)
|
||||||
|
# db.session.commit()
|
||||||
|
|
||||||
book = db.session.get(m.Book, 1)
|
|
||||||
book.user_id = user.id
|
book.user_id = user.id
|
||||||
book.save()
|
book.save()
|
||||||
|
|
||||||
|
@ -244,7 +251,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
|
|
||||||
moderator = m.User(username="Moderator", password="test").save()
|
moderator = m.User(username="Moderator", password="test").save()
|
||||||
|
|
||||||
moderators_book = m.Book(label="Test Book", user_id=moderator.id).save()
|
moderators_book: m.Book = create_test_book(moderator.id)
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{moderators_book.id}/add_contributor",
|
f"/book/{moderators_book.id}/add_contributor",
|
||||||
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
||||||
|
@ -252,28 +259,22 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/999/add_contributor",
|
"/book/999/add_contributor",
|
||||||
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_crud_collection(client: FlaskClient, runner: FlaskCliRunner):
|
def test_crud_collection(client: FlaskClient):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
book = create_test_book(user.id)
|
||||||
# add dummmy data
|
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
|
||||||
book = db.session.get(m.Book, 1)
|
|
||||||
book.user_id = user.id
|
|
||||||
book.save()
|
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/create_collection",
|
f"/book/{book.id}/create_collection",
|
||||||
|
@ -294,16 +295,24 @@ def test_crud_collection(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
assert b"Collection label must be unique!" in response.data
|
assert b"Collection label must be unique!" in response.data
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/999/create_collection",
|
"/book/999/create_collection",
|
||||||
data=dict(label="Test Collection #1 Label", about="Test Collection #1 About"),
|
data=dict(label="Test Collection #1 Label", about="Test Collection #1 About"),
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
collection: m.Collection = m.Collection.query.filter_by(
|
collection: m.Collection = m.Collection.query.filter_by(
|
||||||
label="Test Collection #1 Label"
|
label="Test Collection #1 Label"
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
assert collection
|
||||||
|
assert collection.access_groups
|
||||||
|
assert len(collection.access_groups) == 2
|
||||||
|
for access_group in collection.access_groups:
|
||||||
|
access_group: m.AccessGroup
|
||||||
|
assert access_group.book_id == collection.version.book_id
|
||||||
|
|
||||||
m.Collection(
|
m.Collection(
|
||||||
label="Test Collection #2 Label",
|
label="Test Collection #2 Label",
|
||||||
version_id=collection.version_id,
|
version_id=collection.version_id,
|
||||||
|
@ -342,7 +351,7 @@ def test_crud_collection(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
edited_collection: m.Collection = m.Collection.query.filter_by(
|
edited_collection: m.Collection = m.Collection.query.filter_by(
|
||||||
label=new_label, about=new_about
|
label=new_label, about=new_about
|
||||||
|
@ -387,29 +396,26 @@ def test_crud_collection(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_crud_subcollection(client: FlaskClient, runner: FlaskCliRunner):
|
def test_crud_subcollection(client: FlaskClient):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
|
||||||
# add dummy data
|
book = create_test_book(user.id)
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
|
||||||
book: m.Book = db.session.get(m.Book, 1)
|
collection: m.Collection = m.Collection.query.filter_by(
|
||||||
book.user_id = user.id
|
version_id=book.last_version.id,
|
||||||
book.save()
|
is_leaf=False,
|
||||||
|
parent_id=book.last_version.root_collection.id,
|
||||||
|
).first()
|
||||||
|
|
||||||
leaf_collection: m.Collection = m.Collection(
|
leaf_collection: m.Collection = m.Collection.query.filter_by(
|
||||||
label="Test Leaf Collection #1 Label",
|
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
is_leaf=True,
|
is_leaf=True,
|
||||||
parent_id=book.last_version.root_collection.id,
|
parent_id=collection.id,
|
||||||
).save()
|
).first()
|
||||||
collection: m.Collection = m.Collection(
|
|
||||||
label="Test Collection #1 Label", version_id=book.last_version.id
|
|
||||||
).save()
|
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/999/{leaf_collection.id}/create_sub_collection",
|
f"/book/999/{leaf_collection.id}/create_sub_collection",
|
||||||
|
@ -419,7 +425,7 @@ def test_crud_subcollection(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"Book not found!" in response.data
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{leaf_collection.id}/create_sub_collection",
|
f"/book/{book.id}/{leaf_collection.id}/create_sub_collection",
|
||||||
|
@ -461,6 +467,12 @@ def test_crud_subcollection(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
assert not sub_collection.is_leaf
|
assert not sub_collection.is_leaf
|
||||||
assert sub_collection.parent_id == collection.id
|
assert sub_collection.parent_id == collection.id
|
||||||
|
|
||||||
|
assert sub_collection.access_groups
|
||||||
|
assert len(sub_collection.access_groups) == 2
|
||||||
|
for access_group in sub_collection.access_groups:
|
||||||
|
access_group: m.AccessGroup
|
||||||
|
assert access_group.book_id == sub_collection.version.book_id
|
||||||
|
|
||||||
m.Collection(
|
m.Collection(
|
||||||
label="Test SubCollection #2 Label",
|
label="Test SubCollection #2 Label",
|
||||||
version_id=collection.version_id,
|
version_id=collection.version_id,
|
||||||
|
@ -535,31 +547,20 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
|
||||||
# add dummmy data
|
book = create_test_book(user.id)
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
|
||||||
book: m.Book = db.session.get(m.Book, 1)
|
collection: m.Collection = m.Collection.query.filter_by(
|
||||||
book.user_id = user.id
|
|
||||||
book.save()
|
|
||||||
|
|
||||||
leaf_collection: m.Collection = m.Collection(
|
|
||||||
label="Test Leaf Collection #1 Label",
|
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
is_leaf=True,
|
is_leaf=False,
|
||||||
parent_id=book.last_version.root_collection.id,
|
parent_id=book.last_version.root_collection.id,
|
||||||
).save()
|
).first()
|
||||||
collection: m.Collection = m.Collection(
|
|
||||||
label="Test Collection #1 Label", version_id=book.last_version.id
|
sub_collection: m.Collection = m.Collection.query.filter_by(
|
||||||
).save()
|
version_id=book.last_version.id,
|
||||||
sub_collection: m.Collection = m.Collection(
|
is_leaf=True,
|
||||||
label="Test SubCollection #1 Label",
|
parent_id=collection.id,
|
||||||
version_id=book.last_version.id,
|
).first()
|
||||||
parent_id=collection.id,
|
|
||||||
is_leaf=True,
|
|
||||||
).save()
|
|
||||||
|
|
||||||
leaf_collection.is_leaf = False
|
|
||||||
leaf_collection.save()
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{collection.id}/create_section",
|
f"/book/{book.id}/{collection.id}/create_section",
|
||||||
data=dict(
|
data=dict(
|
||||||
|
@ -571,14 +572,11 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
)
|
)
|
||||||
assert b"You can't create section for this collection" in response.data
|
assert b"You can't create section for this collection" in response.data
|
||||||
|
|
||||||
leaf_collection.is_leaf = True
|
|
||||||
leaf_collection.save()
|
|
||||||
|
|
||||||
label_1 = "Test Section #1 Label"
|
label_1 = "Test Section #1 Label"
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{leaf_collection.id}/create_section",
|
f"/book/{book.id}/{sub_collection.id}/create_section",
|
||||||
data=dict(
|
data=dict(
|
||||||
collection_id=leaf_collection.id,
|
collection_id=sub_collection.id,
|
||||||
label=label_1,
|
label=label_1,
|
||||||
about="Test Section #1 About",
|
about="Test Section #1 About",
|
||||||
),
|
),
|
||||||
|
@ -587,17 +585,23 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
section: m.Section = m.Section.query.filter_by(
|
section: m.Section = m.Section.query.filter_by(
|
||||||
label=label_1, collection_id=leaf_collection.id
|
label=label_1, collection_id=sub_collection.id
|
||||||
).first()
|
).first()
|
||||||
assert section
|
assert section
|
||||||
assert section.collection_id == leaf_collection.id
|
assert section.collection_id == sub_collection.id
|
||||||
assert section.version_id == book.last_version.id
|
assert section.version_id == book.last_version.id
|
||||||
assert not section.interpretations
|
assert not section.interpretations
|
||||||
|
|
||||||
|
assert section.access_groups
|
||||||
|
assert len(section.access_groups) == 2
|
||||||
|
for access_group in section.access_groups:
|
||||||
|
access_group: m.AccessGroup
|
||||||
|
assert access_group.book_id == section.version.book_id
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{leaf_collection.id}/create_section",
|
f"/book/{book.id}/{sub_collection.id}/create_section",
|
||||||
data=dict(
|
data=dict(
|
||||||
collection_id=leaf_collection.id,
|
collection_id=sub_collection.id,
|
||||||
label=label_1,
|
label=label_1,
|
||||||
about="Test Section #1 About",
|
about="Test Section #1 About",
|
||||||
),
|
),
|
||||||
|
@ -663,7 +667,7 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
|
|
||||||
m.Section(
|
m.Section(
|
||||||
label="Test",
|
label="Test",
|
||||||
collection_id=leaf_collection.id,
|
collection_id=sub_collection.id,
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
@ -674,7 +678,7 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
section: m.Section = m.Section.query.filter_by(
|
section: m.Section = m.Section.query.filter_by(
|
||||||
label=label_1, collection_id=leaf_collection.id
|
label=label_1, collection_id=sub_collection.id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
|
@ -707,7 +711,7 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
|
|
||||||
#
|
#
|
||||||
section_2: m.Section = m.Section.query.filter_by(
|
section_2: m.Section = m.Section.query.filter_by(
|
||||||
label=label_1, collection_id=sub_collection.id
|
label="Test Section #1 Label(edited)", collection_id=sub_collection.id
|
||||||
).first()
|
).first()
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{section_2.id}/edit_section",
|
f"/book/{book.id}/{section_2.id}/edit_section",
|
||||||
|
@ -776,43 +780,35 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
assert b"Section not found" in response.data
|
assert b"Section not found" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_crud_interpretation(client: FlaskClient, runner: FlaskCliRunner):
|
def test_crud_interpretation(client: FlaskClient):
|
||||||
_, user = login(client)
|
_, user = login(client)
|
||||||
user: m.User
|
user: m.User
|
||||||
|
book = create_test_book(user.id)
|
||||||
|
|
||||||
# add dummmy data
|
collection: m.Collection = m.Collection.query.filter_by(
|
||||||
runner.invoke(args=["db-populate"])
|
|
||||||
|
|
||||||
book: m.Book = db.session.get(m.Book, 1)
|
|
||||||
book.user_id = user.id
|
|
||||||
book.save()
|
|
||||||
|
|
||||||
leaf_collection: m.Collection = m.Collection(
|
|
||||||
label="Test Leaf Collection #1 Label",
|
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
is_leaf=True,
|
is_leaf=True,
|
||||||
parent_id=book.last_version.root_collection.id,
|
parent_id=book.last_version.root_collection.id,
|
||||||
).save()
|
).first()
|
||||||
section_in_collection: m.Section = m.Section(
|
section_in_collection: m.Section = m.Section.query.filter_by(
|
||||||
label="Test Section in Collection #1 Label",
|
collection_id=collection.id,
|
||||||
collection_id=leaf_collection.id,
|
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
).save()
|
).first()
|
||||||
|
|
||||||
collection: m.Collection = m.Collection(
|
collection: m.Collection = m.Collection.query.filter_by(
|
||||||
label="Test Collection #1 Label", version_id=book.last_version.id
|
version_id=book.last_version.id,
|
||||||
).save()
|
is_leaf=False,
|
||||||
sub_collection: m.Collection = m.Collection(
|
parent_id=book.last_version.root_collection.id,
|
||||||
label="Test SubCollection #1 Label",
|
).first()
|
||||||
|
sub_collection: m.Collection = m.Collection.query.filter_by(
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
parent_id=collection.id,
|
|
||||||
is_leaf=True,
|
is_leaf=True,
|
||||||
).save()
|
parent_id=collection.id,
|
||||||
section_in_subcollection: m.Section = m.Section(
|
).first()
|
||||||
label="Test Section in Subcollection #1 Label",
|
section_in_subcollection: m.Section = m.Section.query.filter_by(
|
||||||
collection_id=sub_collection.id,
|
collection_id=sub_collection.id,
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
).save()
|
).first()
|
||||||
|
|
||||||
text_1 = "Test Interpretation #1 Text"
|
text_1 = "Test Interpretation #1 Text"
|
||||||
|
|
||||||
|
@ -830,6 +826,12 @@ def test_crud_interpretation(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
assert interpretation.section_id == section_in_subcollection.id
|
assert interpretation.section_id == section_in_subcollection.id
|
||||||
assert not interpretation.comments
|
assert not interpretation.comments
|
||||||
|
|
||||||
|
assert interpretation.access_groups
|
||||||
|
assert len(interpretation.access_groups) == 2
|
||||||
|
for access_group in interpretation.access_groups:
|
||||||
|
access_group: m.AccessGroup
|
||||||
|
assert access_group.book_id == interpretation.section.version.book_id
|
||||||
|
|
||||||
response: Response = client.post(
|
response: Response = client.post(
|
||||||
f"/book/{book.id}/{section_in_collection.id}/create_interpretation",
|
f"/book/{book.id}/{section_in_collection.id}/create_interpretation",
|
||||||
data=dict(section_id=section_in_collection.id, text=text_1),
|
data=dict(section_id=section_in_collection.id, text=text_1),
|
||||||
|
@ -873,15 +875,23 @@ def test_crud_interpretation(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
|
|
||||||
# edit
|
# edit
|
||||||
|
|
||||||
m.Interpretation(
|
i_1 = m.Interpretation(
|
||||||
text="Test", section_id=section_in_collection.id, user_id=user.id
|
text="Test", section_id=section_in_collection.id, user_id=user.id
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
m.Interpretation(
|
i_2 = m.Interpretation(
|
||||||
text="Test",
|
text="Test",
|
||||||
section_id=section_in_subcollection.id,
|
section_id=section_in_subcollection.id,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
group = create_moderator_group(book.id)
|
||||||
|
m.InterpretationAccessGroups(
|
||||||
|
interpretation_id=i_1.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
m.InterpretationAccessGroups(
|
||||||
|
interpretation_id=i_2.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
||||||
section_id=section_in_collection.id
|
section_id=section_in_collection.id
|
||||||
).first()
|
).first()
|
||||||
|
@ -967,7 +977,7 @@ def test_crud_comment(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
is_leaf=True,
|
is_leaf=True,
|
||||||
parent_id=book.last_version.root_collection.id,
|
parent_id=book.last_version.root_collection.id,
|
||||||
).save()
|
).save()
|
||||||
section_in_collection: m.Section = m.Section(
|
m.Section(
|
||||||
label="Test Section in Collection #1 Label",
|
label="Test Section in Collection #1 Label",
|
||||||
collection_id=leaf_collection.id,
|
collection_id=leaf_collection.id,
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
|
@ -987,6 +997,10 @@ def test_crud_comment(client: FlaskClient, runner: FlaskCliRunner):
|
||||||
collection_id=sub_collection.id,
|
collection_id=sub_collection.id,
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
).save()
|
).save()
|
||||||
|
group = create_moderator_group(book.id)
|
||||||
|
m.SectionAccessGroups(
|
||||||
|
section_id=section_in_subcollection.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
label_1 = "Test Interpretation #1 Label"
|
label_1 = "Test Interpretation #1 Label"
|
||||||
text_1 = "Test Interpretation #1 Text"
|
text_1 = "Test Interpretation #1 Text"
|
||||||
|
@ -1082,7 +1096,6 @@ def test_access_to_settings_page(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" not in response.data
|
|
||||||
|
|
||||||
logout(client)
|
logout(client)
|
||||||
|
|
||||||
|
@ -1092,7 +1105,7 @@ def test_access_to_settings_page(client: FlaskClient):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert b"You are not owner of this book!" in response.data
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
|
|
||||||
def test_interpretation_in_home_last_inter_section(
|
def test_interpretation_in_home_last_inter_section(
|
||||||
|
@ -1134,6 +1147,13 @@ def test_interpretation_in_home_last_inter_section(
|
||||||
collection_id=sub_collection.id,
|
collection_id=sub_collection.id,
|
||||||
version_id=book.last_version.id,
|
version_id=book.last_version.id,
|
||||||
).save()
|
).save()
|
||||||
|
group = create_moderator_group(book.id)
|
||||||
|
m.SectionAccessGroups(
|
||||||
|
section_id=section_in_subcollection.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
m.SectionAccessGroups(
|
||||||
|
section_id=section_in_collection.id, access_group_id=group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
label_1 = "Test Interpretation no1 Label"
|
label_1 = "Test Interpretation no1 Label"
|
||||||
text_1 = "Test Interpretation no1 Text"
|
text_1 = "Test Interpretation no1 Text"
|
||||||
|
@ -1194,7 +1214,7 @@ def test_interpretation_in_home_last_inter_section(
|
||||||
assert b"Section not found" in response.data
|
assert b"Section not found" in response.data
|
||||||
|
|
||||||
response: Response = client.get(
|
response: Response = client.get(
|
||||||
f"/home",
|
"/home",
|
||||||
follow_redirects=True,
|
follow_redirects=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -104,11 +104,3 @@ def test_approved_comments(client: FlaskClient):
|
||||||
interpretation.is_deleted = False
|
interpretation.is_deleted = False
|
||||||
interpretation.save()
|
interpretation.save()
|
||||||
assert len(book.approved_comments) == 2
|
assert len(book.approved_comments) == 2
|
||||||
|
|
||||||
collection: m.Collection = m.Collection.query.first()
|
|
||||||
collection.is_deleted = True
|
|
||||||
collection.save()
|
|
||||||
|
|
||||||
interpretation.is_deleted = False
|
|
||||||
interpretation.save()
|
|
||||||
assert len(book.approved_comments) == 0
|
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
from app.controllers.create_access_groups import (
|
||||||
|
create_moderator_group,
|
||||||
|
create_editor_group,
|
||||||
|
)
|
||||||
|
from app import models as m
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_moderator_group(client):
|
||||||
|
create_moderator_group(book_id=0)
|
||||||
|
|
||||||
|
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="moderator").first()
|
||||||
|
assert group
|
||||||
|
assert not group.users
|
||||||
|
assert group.permissions
|
||||||
|
|
||||||
|
permissions = group.permissions
|
||||||
|
|
||||||
|
access = m.Permission.Access
|
||||||
|
|
||||||
|
interpretation_DA: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
|
||||||
|
).first()
|
||||||
|
assert interpretation_DA
|
||||||
|
assert interpretation_DA in permissions
|
||||||
|
|
||||||
|
comment_DA: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
|
||||||
|
).first()
|
||||||
|
assert comment_DA
|
||||||
|
assert comment_DA in permissions
|
||||||
|
|
||||||
|
create_moderator_group(book_id=0)
|
||||||
|
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="moderator").all()
|
||||||
|
assert len(groups) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_editor_group(client):
|
||||||
|
create_editor_group(book_id=0)
|
||||||
|
|
||||||
|
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="editor").first()
|
||||||
|
assert group
|
||||||
|
assert not group.users
|
||||||
|
assert group.permissions
|
||||||
|
|
||||||
|
permissions = group.permissions
|
||||||
|
|
||||||
|
access = m.Permission.Access
|
||||||
|
|
||||||
|
interpretation_DA: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
|
||||||
|
).first()
|
||||||
|
assert interpretation_DA
|
||||||
|
assert interpretation_DA in permissions
|
||||||
|
|
||||||
|
comment_DA: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
|
||||||
|
).first()
|
||||||
|
assert comment_DA
|
||||||
|
assert comment_DA in permissions
|
||||||
|
|
||||||
|
section_CUD: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.C | access.U | access.D,
|
||||||
|
entity_type=m.Permission.Entity.SECTION,
|
||||||
|
).first()
|
||||||
|
assert section_CUD
|
||||||
|
assert section_CUD in permissions
|
||||||
|
|
||||||
|
collection_CUD: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.C | access.U | access.D,
|
||||||
|
entity_type=m.Permission.Entity.COLLECTION,
|
||||||
|
).first()
|
||||||
|
assert collection_CUD
|
||||||
|
assert collection_CUD in permissions
|
||||||
|
|
||||||
|
book_U: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.U,
|
||||||
|
entity_type=m.Permission.Entity.BOOK,
|
||||||
|
).first()
|
||||||
|
assert book_U
|
||||||
|
assert book_U in permissions
|
||||||
|
|
||||||
|
create_editor_group(book_id=0)
|
||||||
|
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="editor").all()
|
||||||
|
assert len(groups) == 2
|
|
@ -0,0 +1,32 @@
|
||||||
|
from app import models as m
|
||||||
|
from app.controllers.get_or_create_permission import get_or_create_permission
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_or_create_permission(client):
|
||||||
|
access = m.Permission.Access
|
||||||
|
entity_type = m.Permission.Entity
|
||||||
|
|
||||||
|
book_u: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.U, entity_type=entity_type.BOOK
|
||||||
|
).first()
|
||||||
|
assert not book_u
|
||||||
|
|
||||||
|
assert not m.Permission.query.count()
|
||||||
|
|
||||||
|
book_u: m.Permission = get_or_create_permission(
|
||||||
|
access=access.U, entity_type=entity_type.BOOK
|
||||||
|
)
|
||||||
|
assert book_u
|
||||||
|
assert book_u.access == access.U
|
||||||
|
assert book_u.entity_type == entity_type.BOOK
|
||||||
|
assert m.Permission.query.count() == 1
|
||||||
|
|
||||||
|
book_u: m.Permission = m.Permission.query.filter_by(
|
||||||
|
access=access.U, entity_type=entity_type.BOOK
|
||||||
|
).first()
|
||||||
|
assert book_u
|
||||||
|
assert book_u.access == access.U
|
||||||
|
assert book_u.entity_type == entity_type.BOOK
|
||||||
|
|
||||||
|
get_or_create_permission(access=access.U, entity_type=entity_type.BOOK)
|
||||||
|
assert m.Permission.query.count() == 1
|
|
@ -0,0 +1,340 @@
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
from flask import current_app as Response
|
||||||
|
|
||||||
|
from app import models as m
|
||||||
|
from tests.utils import login, logout
|
||||||
|
|
||||||
|
|
||||||
|
def create_book(client):
|
||||||
|
random_id = randint(1, 100)
|
||||||
|
BOOK_NAME = f"TBook {random_id}"
|
||||||
|
response: Response = client.post(
|
||||||
|
"/book/create",
|
||||||
|
data=dict(label=BOOK_NAME),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Book added!" in response.data
|
||||||
|
|
||||||
|
book: m.Book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
||||||
|
|
||||||
|
assert book
|
||||||
|
assert book.versions
|
||||||
|
assert len(book.versions) == 1
|
||||||
|
assert book.access_groups
|
||||||
|
assert len(book.access_groups) == 2
|
||||||
|
|
||||||
|
root_collection: m.Collection = book.last_version.collections[0]
|
||||||
|
assert root_collection
|
||||||
|
assert root_collection.access_groups
|
||||||
|
assert len(root_collection.access_groups) == 2
|
||||||
|
|
||||||
|
return book
|
||||||
|
|
||||||
|
|
||||||
|
def create_collection(client, book_id):
|
||||||
|
random_id = randint(1, 100)
|
||||||
|
LABEL = f"TCollection {random_id}"
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book_id}/create_collection",
|
||||||
|
data=dict(label=LABEL),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
collection: m.Collection = m.Collection.query.filter_by(label=LABEL).first()
|
||||||
|
|
||||||
|
return collection, response
|
||||||
|
|
||||||
|
|
||||||
|
def create_section(client, book_id, collection_id):
|
||||||
|
random_id = randint(1, 100)
|
||||||
|
LABEL = f"TSection {random_id}"
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book_id}/{collection_id}/create_section",
|
||||||
|
data=dict(collection_id=collection_id, label=LABEL),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
section: m.Section = m.Section.query.filter_by(
|
||||||
|
label=LABEL, collection_id=collection_id
|
||||||
|
).first()
|
||||||
|
return section, response
|
||||||
|
|
||||||
|
|
||||||
|
def create_interpretation(client, book_id, section_id):
|
||||||
|
random_id = randint(1, 100)
|
||||||
|
LABEL = f"TInterpretation {random_id}"
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book_id}/{section_id}/create_interpretation",
|
||||||
|
data=dict(section_id=section_id, text=LABEL),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
||||||
|
section_id=section_id, text=LABEL
|
||||||
|
).first()
|
||||||
|
return interpretation, response
|
||||||
|
|
||||||
|
|
||||||
|
def create_comment(client, book_id, interpretation_id):
|
||||||
|
random_id = randint(1, 100)
|
||||||
|
TEXT = f"TComment {random_id}"
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book_id}/{interpretation_id}/create_comment",
|
||||||
|
data=dict(
|
||||||
|
text=TEXT,
|
||||||
|
interpretation_id=interpretation_id,
|
||||||
|
),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
comment: m.Comment = m.Comment.query.filter_by(text=TEXT).first()
|
||||||
|
return comment, response
|
||||||
|
|
||||||
|
|
||||||
|
def test_editor_access_to_entire_book(client):
|
||||||
|
login(client)
|
||||||
|
book = create_book(client)
|
||||||
|
|
||||||
|
editor = m.User(username="editor", password="editor").save()
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/add_contributor",
|
||||||
|
data=dict(user_id=editor.id, role=m.BookContributor.Roles.EDITOR),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Contributor was added!" in response.data
|
||||||
|
logout(client)
|
||||||
|
|
||||||
|
login(client, "editor", "editor")
|
||||||
|
|
||||||
|
# access to settings page
|
||||||
|
response: Response = client.get(f"/book/{book.id}/settings", follow_redirects=True)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
|
||||||
|
# access to edit book
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/edit",
|
||||||
|
data=dict(book_id=book.id, label="BookEdited"),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# dont have access to delete
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/delete",
|
||||||
|
data=dict(book_id=book.id),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
|
# access to create collection
|
||||||
|
collection, response = create_collection(client, book.id)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to edit collection
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{collection.id}/edit",
|
||||||
|
data=dict(label="NewLabel"),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to delete collection
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{collection.id}/delete", follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# restore collection
|
||||||
|
collection.is_deleted = False
|
||||||
|
collection.save()
|
||||||
|
|
||||||
|
# access to create section
|
||||||
|
section, response = create_section(client, book.id, collection.id)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to edit section
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{section.id}/edit_section",
|
||||||
|
data=dict(section_id=section.id, label="NewLabel"),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to delete section
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{section.id}/delete_section", follow_redirects=True
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# restore section
|
||||||
|
section.is_deleted = False
|
||||||
|
section.save()
|
||||||
|
|
||||||
|
# access to create interpretation
|
||||||
|
interpretation, response = create_interpretation(client, book.id, section.id)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to approve interpretation
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/approve/interpretation/{interpretation.id}",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response
|
||||||
|
assert response.json["message"] == "success"
|
||||||
|
assert response.json["approve"]
|
||||||
|
assert interpretation.approved
|
||||||
|
|
||||||
|
# access to delete interpretation
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{interpretation.id}/delete_interpretation",
|
||||||
|
data=dict(interpretation_id=interpretation.id),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# restore interpretation
|
||||||
|
interpretation.is_deleted = False
|
||||||
|
interpretation.save()
|
||||||
|
|
||||||
|
# access to create comment
|
||||||
|
comment, response = create_comment(client, book.id, interpretation.id)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to approve comment
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/approve/comment/{comment.id}",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response
|
||||||
|
assert response.json["message"] == "success"
|
||||||
|
assert response.json["approve"]
|
||||||
|
assert interpretation.approved
|
||||||
|
|
||||||
|
# access to delete comment
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{interpretation.id}/comment_delete",
|
||||||
|
data=dict(
|
||||||
|
text=comment.text,
|
||||||
|
interpretation_id=interpretation.id,
|
||||||
|
comment_id=comment.id,
|
||||||
|
),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
|
||||||
|
def test_moderator_access_to_entire_book(client):
|
||||||
|
login(client)
|
||||||
|
book = create_book(client)
|
||||||
|
|
||||||
|
editor = m.User(username="moderator", password="moderator").save()
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/add_contributor",
|
||||||
|
data=dict(user_id=editor.id, role=m.BookContributor.Roles.MODERATOR),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert b"Contributor was added!" in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
login(client, "moderator", "moderator")
|
||||||
|
|
||||||
|
# access to settings page
|
||||||
|
response: Response = client.get(f"/book/{book.id}/settings", follow_redirects=True)
|
||||||
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
|
# access to edit book
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/edit",
|
||||||
|
data=dict(book_id=book.id, label="BookEdited"),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
|
# dont have access to delete
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/delete",
|
||||||
|
data=dict(book_id=book.id),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" in response.data
|
||||||
|
|
||||||
|
logout(client)
|
||||||
|
login(client)
|
||||||
|
collection, response = create_collection(client, book.id)
|
||||||
|
section, response = create_section(client, book.id, collection.id)
|
||||||
|
login(client, "moderator", "moderator")
|
||||||
|
|
||||||
|
# access to create interpretation
|
||||||
|
interpretation, response = create_interpretation(client, book.id, section.id)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to approve interpretation
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/approve/interpretation/{interpretation.id}",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response
|
||||||
|
assert response.json["message"] == "success"
|
||||||
|
assert response.json["approve"]
|
||||||
|
assert interpretation.approved
|
||||||
|
|
||||||
|
# access to delete interpretation
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{interpretation.id}/delete_interpretation",
|
||||||
|
data=dict(interpretation_id=interpretation.id),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# restore interpretation
|
||||||
|
interpretation.is_deleted = False
|
||||||
|
interpretation.save()
|
||||||
|
|
||||||
|
# access to create comment
|
||||||
|
comment, response = create_comment(client, book.id, interpretation.id)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
||||||
|
|
||||||
|
# access to approve comment
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/approve/comment/{comment.id}",
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response
|
||||||
|
assert response.json["message"] == "success"
|
||||||
|
assert response.json["approve"]
|
||||||
|
assert interpretation.approved
|
||||||
|
|
||||||
|
# access to delete comment
|
||||||
|
response: Response = client.post(
|
||||||
|
f"/book/{book.id}/{interpretation.id}/comment_delete",
|
||||||
|
data=dict(
|
||||||
|
text=comment.text,
|
||||||
|
interpretation_id=interpretation.id,
|
||||||
|
comment_id=comment.id,
|
||||||
|
),
|
||||||
|
follow_redirects=True,
|
||||||
|
)
|
||||||
|
assert b"You do not have permission" not in response.data
|
||||||
|
assert b"Success!" in response.data
|
|
@ -1,4 +1,8 @@
|
||||||
from app import models as m
|
from app import models as m
|
||||||
|
from app.controllers.create_access_groups import (
|
||||||
|
create_editor_group,
|
||||||
|
create_moderator_group,
|
||||||
|
)
|
||||||
|
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
|
@ -26,15 +30,24 @@ def logout(client):
|
||||||
return client.get("/logout", follow_redirects=True)
|
return client.get("/logout", follow_redirects=True)
|
||||||
|
|
||||||
|
|
||||||
def create_test_book(owner_id: int, entity_id: int = randint(1, 100)):
|
def create_test_book(owner_id: int, entity_id: int = 0):
|
||||||
|
if not entity_id:
|
||||||
|
entity_id = randint(1, 500)
|
||||||
book: m.Book = m.Book(
|
book: m.Book = m.Book(
|
||||||
label=f"Book {entity_id}", about=f"About {entity_id}", user_id=owner_id
|
label=f"Book {entity_id}", about=f"About {entity_id}", user_id=owner_id
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
version: m.BookVersion = m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
version: m.BookVersion = m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
||||||
|
|
||||||
|
root_collection: m.Collection = m.Collection(
|
||||||
|
label="Root", version_id=version.id, is_root=True
|
||||||
|
).save()
|
||||||
|
|
||||||
collection: m.Collection = m.Collection(
|
collection: m.Collection = m.Collection(
|
||||||
label=f"Collection {entity_id}", version_id=version.id
|
label=f"Collection {entity_id}",
|
||||||
|
version_id=version.id,
|
||||||
|
is_leaf=True,
|
||||||
|
parent_id=root_collection.id,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
section: m.Section = m.Section(
|
section: m.Section = m.Section(
|
||||||
|
@ -56,6 +69,67 @@ def create_test_book(owner_id: int, entity_id: int = randint(1, 100)):
|
||||||
interpretation_id=interpretation.id,
|
interpretation_id=interpretation.id,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
# subcollection
|
||||||
|
collection_2: m.Collection = m.Collection(
|
||||||
|
label=f"Collection {entity_id}",
|
||||||
|
version_id=version.id,
|
||||||
|
parent_id=root_collection.id,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
subcollection: m.Collection = m.Collection(
|
||||||
|
label=f"subCollection {entity_id}",
|
||||||
|
version_id=version.id,
|
||||||
|
parent_id=collection_2.id,
|
||||||
|
is_leaf=True,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
section_in_subcollection: m.Section = m.Section(
|
||||||
|
label=f"Section in sub {entity_id}",
|
||||||
|
user_id=owner_id,
|
||||||
|
collection_id=subcollection.id,
|
||||||
|
version_id=version.id,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
# access groups
|
||||||
|
editor_access_group = create_editor_group(book_id=book.id)
|
||||||
|
moderator_access_group = create_moderator_group(book_id=book.id)
|
||||||
|
access_groups = [editor_access_group, moderator_access_group]
|
||||||
|
|
||||||
|
for access_group in access_groups:
|
||||||
|
m.BookAccessGroups(book_id=book.id, access_group_id=access_group.id).save()
|
||||||
|
# root
|
||||||
|
m.CollectionAccessGroups(
|
||||||
|
collection_id=root_collection.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
# leaf
|
||||||
|
m.CollectionAccessGroups(
|
||||||
|
collection_id=collection.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
|
m.CollectionAccessGroups(
|
||||||
|
collection_id=collection_2.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
# subcollection
|
||||||
|
m.CollectionAccessGroups(
|
||||||
|
collection_id=subcollection.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
|
m.SectionAccessGroups(
|
||||||
|
section_id=section.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
m.SectionAccessGroups(
|
||||||
|
section_id=section_in_subcollection.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
m.InterpretationAccessGroups(
|
||||||
|
interpretation_id=section.id, access_group_id=access_group.id
|
||||||
|
).save()
|
||||||
|
|
||||||
|
# Contributors
|
||||||
|
u = m.User(username=f"Bob {entity_id}").save()
|
||||||
|
m.BookContributor(book_id=book.id, user_id=u.id).save()
|
||||||
|
|
||||||
|
return book
|
||||||
|
|
||||||
|
|
||||||
def check_if_nested_book_entities_is_deleted(book: m.Book, is_deleted: bool = True):
|
def check_if_nested_book_entities_is_deleted(book: m.Book, is_deleted: bool = True):
|
||||||
for version in book.versions:
|
for version in book.versions:
|
||||||
|
|
Loading…
Reference in New Issue