Merge pull request #107 from Simple2B/svyat/feat/user_permissions

Svyat/feat/user permissions
This commit is contained in:
Svyatoslav Artymovych 2023-06-01 15:38:07 +03:00 committed by GitHub
commit a4797e5ddb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 2199 additions and 1179 deletions

View File

@ -14,6 +14,7 @@
"backref", "backref",
"bookname", "bookname",
"Btns", "Btns",
"CUDA",
"CLEANR", "CLEANR",
"Divs", "Divs",
"flowbite", "flowbite",

View File

@ -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)

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

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

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

View File

@ -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

View File

@ -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",

View File

@ -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}>"

View File

@ -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):

View File

@ -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

View File

@ -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}>"

View File

@ -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}"

View File

@ -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}"

View File

@ -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}"

View File

@ -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",
)

View File

@ -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}"

View File

@ -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}"

View File

@ -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}"

View File

@ -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",

View File

@ -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

View File

@ -28,17 +28,20 @@
Table of contents Table of contents
</h1> </h1>
</div> </div>
<div class="flex text-black dark:text-white"> {% if has_permission(book, Access.U) %}
<!-- prettier-ignore --> <div class="flex text-black dark:text-white">
<div> <!-- prettier-ignore -->
{% if not book.versions[-1].children_collections and current_user.is_authenticated %} <div>
<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> {% if not book.versions[-1].children_collections and current_user.is_authenticated %}
{% endif %} <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>
<a href="{{ url_for("book.settings", book_id=book.id) }}" type="button" class="ml-2" > {% endif %}
<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 href="{{ url_for('book.settings', book_id=book.id) }}" type="button" class="ml-2" >
</a> <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>
</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 %}
@ -59,42 +62,60 @@
<svg id="dropdownCollectionContextButton{{collection.id}}" data-dropdown-toggle="dropdown" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 0 0" stroke-width="1.5" stroke="none" class="w-0 h-0"></svg> <svg id="dropdownCollectionContextButton{{collection.id}}" data-dropdown-toggle="dropdown" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 0 0" stroke-width="1.5" stroke="none" class="w-0 h-0"></svg>
</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 %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> {% set access_to_create_collections = has_permission(collection, Access.C) %}
<li> {% set access_to_update_collections = has_permission(collection, Access.U) %}
<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> {% set access_to_delete_collections = has_permission(collection, Access.D) %}
</li> {% set access_to_create_section = has_permission(collection, Access.C, EntityType.SECTION) %}
{% if not collection.is_leaf %}
<li> {% if access_to_create_collections or access_to_update_collections %}
<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> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
</li> {% if access_to_create_collections %}
{% endif %} <li>
{% if collection.children|length ==0 or collection.children|length ==0 and collection.is_leaf %} <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>
<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> {% if not collection.is_leaf %}
</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="{{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>
</ul> </li>
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> {% endif %}
<li> {% endif %}
<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> {% if access_to_create_section %}
</li> {% if collection.children|length ==0 or collection.children|length ==0 and collection.is_leaf %}
<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="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>
</ul> {% endif %}
{% endif %}
</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">
{% if access_to_update_collections %}
<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>
</li>
{% endif %}
{% if access_to_delete_collections %}
<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>
</li>
{% endif %}
</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>
</li> </li>
</ul> </ul>
{% else %} {% else %}
<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">Connect you wallet to do this</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">Connect you wallet to do this</button>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<div id="accordion-collapse-body-{{collection.id}}" class="hidden" aria-labelledby="accordion-collapse-heading-{{collection.id}}"> <div id="accordion-collapse-body-{{collection.id}}" class="hidden" aria-labelledby="accordion-collapse-heading-{{collection.id}}">

View File

@ -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,51 +42,63 @@
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 %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> {% set access_to_create_sections = has_permission(section, Access.C) %}
<li> {% set access_to_update_sections = has_permission(section, Access.U) %}
<button {% set access_to_delete_sections = has_permission(section, Access.D) %}
type="button"
id="rename-section-button-{{section.id}}" {% if access_to_update_sections or access_to_delete_sections %}
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
Rename Section {% if access_to_update_sections %}
</button> <li>
</li> <button
<li> type="button"
<!-- prettier-ignore --> id="rename-section-button-{{section.id}}"
<button class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
type="button" Rename Section
data-modal-target="delete-section-modal" </button>
data-modal-toggle="delete-section-modal" </li>
id="callDeleteSectionModal"
data-collection-id="{{collection.id}}"
{% if sub_collection %}
data-sub-collection-id="{{sub_collection.id}}"
{% endif %} {% endif %}
data-section-id="{{section.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> {% if access_to_delete_sections %}
Delete Section <li>
</button> <!-- prettier-ignore -->
</li> <button
</ul> type="button"
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> data-modal-target="delete-section-modal"
<li> data-modal-toggle="delete-section-modal"
<button id="callDeleteSectionModal"
type="button" 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"> {% if sub_collection %}
Export Section data-sub-collection-id="{{sub_collection.id}}"
</button> {% endif %}
</li> data-section-id="{{section.id}}"
</ul> class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Delete Section
</button>
</li>
{% endif %}
</ul>
{% endif %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<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 Section
</button>
</li>
</ul>
{% else %} {% else %}
<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
type="button" type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Connect your wallet to do this Connect your wallet to do this
</button> </button>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -31,43 +31,63 @@
<!-- 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 %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> {% set access_to_create_collections = has_permission(sub_collection, Access.C) %}
{% if sub_collection.is_leaf and not sub_collection.children %} {% set access_to_update_collections = has_permission(sub_collection, Access.U) %}
<li> {% set access_to_delete_collections = has_permission(sub_collection, Access.D) %}
<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> {% set access_to_create_section = has_permission(collection, Access.C, EntityType.SECTION) %}
</li>
{% elif not sub_collection.is_leaf and not sub_collection.children %} {% if access_to_create_collections or access_to_update_collections or access_to_create_section %}
<li> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<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> {% if access_to_create_section and sub_collection.is_leaf and not sub_collection.children %}
</li> <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="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> {% elif not sub_collection.is_leaf and not sub_collection.children %}
{% else %} {% if access_to_create_section %}
<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="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>
{% endif %}
{% 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 %}
{% 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>
{% endif %} {% endif %}
</ul>
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> {% if access_to_update_collections or access_to_delete_collections %}
<li> <ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<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> {% if access_to_update_collections %}
</li> <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="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> {% if access_to_delete_collections %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200"> <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" 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> </li>
</li> {% endif %}
</ul> </ul>
{% endif %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<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>
</li>
</ul>
{% else %} {% else %}
<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"> Connect your wallet to do this </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"> Connect your wallet to do this </button>
</li> </li>
</ul> </ul>
{% endif %} {% endif %}
</div> </div>
<!-- prettier-ignore --> <!-- prettier-ignore -->

View File

@ -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,33 +129,39 @@
<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">
<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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /> </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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /> </svg>
</button> </button>
<div data-popover id="popover-edit" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800"> <div data-popover id="popover-edit" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
<div class="px-3 py-2"> <div class="px-3 py-2">
<p>Edit this interpretation</p> <p>Edit this interpretation</p>
</div>
<div data-popper-arrow></div>
</div> </div>
<div data-popper-arrow></div>
</div> </div>
</div> {% endif %}
<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">
<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>
</button>
<div data-popover id="popover-delete" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
<div class="px-3 py-2">
<p>Delete this interpretation</p>
</div>
<div data-popper-arrow></div>
</div>
</div>
{% if interpretation.book.owner == current_user or access_to_delete_interpretation %}
<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">
<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>
</button>
<div data-popover id="popover-delete" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
<div class="px-3 py-2">
<p>Delete this interpretation</p>
</div>
<div data-popper-arrow></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">

View File

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

View File

@ -72,25 +72,20 @@
</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 %}
<!-- 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 w-2/3 md:w-full"> <div class="flex flex-row pb-3 p-3 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="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">
@ -151,28 +140,31 @@
<div class="flex ml-auto justify-end space-x-2 w-24"> <div class="flex ml-auto justify-end space-x-2 w-24">
{% if comment.user_id == current_user.id %} {% if comment.user_id == current_user.id %}
<div class="relative"> <div class="relative">
<button id="edit_comment_btn" data-popover-target="popover-edit" data-edit-comment-id="{{comment.id}}" data-edit-comment-text="{{comment.text}}" type="button" data-modal-target="edit_comment_modal" data-modal-toggle="edit_comment_modal" class="space-x-0.5 flex items-center"> <button id="edit_comment_btn" data-popover-target="popover-edit" data-edit-comment-id="{{comment.id}}" data-edit-comment-text="{{comment.text}}" type="button" data-modal-target="edit_comment_modal" data-modal-toggle="edit_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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /> </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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /> </svg>
</button> </button>
<div data-popover id="popover-edit" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800"> <div data-popover id="popover-edit" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
<div class="px-3 py-2"> <div class="px-3 py-2">
<p>Edit this comment</p> <p>Edit this comment</p>
</div>
<div data-popper-arrow></div>
</div> </div>
<div data-popper-arrow></div>
</div> </div>
</div> {% endif %}
<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"> {% if comment.user_id == current_user.id or access_to_delete_comment %}
<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> <div class="relative">
</button> <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">
<div data-popover id="popover-delete" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-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"> <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>
<div class="px-3 py-2"> </button>
<p>Delete this comment</p> <div data-popover id="popover-delete" role="tooltip" class="absolute z-10 invisible inline-block w-64 text-sm text-gray-500 transition-opacity duration-300 bg-white border border-gray-200 rounded-lg shadow-sm opacity-0 dark:text-gray-400 dark:border-gray-600 dark:bg-gray-800">
<div class="px-3 py-2">
<p>Delete this comment</p>
</div>
<div data-popper-arrow></div>
</div> </div>
<div data-popper-arrow></div>
</div> </div>
</div>
{% endif %} {% endif %}
<div class="relative"> <div class="relative">

View File

@ -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,13 +27,15 @@
<span>Book settings</span> <span>Book settings</span>
</button> </button>
</li> </li>
<li class="mr-2" role="presentation"> {% if book.user_id == current_user.id %}
<!-- prettier-ignore --> <li class="mr-2" role="presentation">
<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"> <!-- prettier-ignore -->
<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="M6 13.5V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 9.75V10.5" /> </svg> <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">
<span>User permissions</span> <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="M6 13.5V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 9.75V10.5" /> </svg>
</button> <span>User permissions</span>
</li> </button>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
@ -77,55 +80,89 @@
</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> <table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
Add <thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
</button> <tr>
</div> <th scope="col" class="px-6 py-3">
<div class="relative overflow-x-auto shadow-md sm:rounded-lg"> Username
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400"> </th>
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"> <th scope="col" class="px-6 py-3">
<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> Address
</thead> </th>
<tbody> <th scope="col" class="px-6 py-3">
{% for contributor in book.contributors %} Role
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700"> </th>
<td class="px-6 py-4">{{ contributor.user.username }}</td> <th scope="col" class="px-6 py-3 text-center">
<td class="px-6 py-4"> Access Level
<form action="{{ url_for('book.edit_contributor_role', book_id=book.id) }}" method="post" class="mb-0 flex space-x-2"> </th>
{{ form_hidden_tag() }} <th scope="col" class="px-6 py-3">
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" /> </th>
<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" > </tr>
{% for role in roles if role.value %} </thead>
<option <tbody>
{% if contributor.role == role %} selected {% endif %} {% for contributor in book.contributors %}
value="{{ role.value }}"> <tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700">
{{ role.name.title() }} <td class="px-6 max-w-[230]">
</option> <div class="flex items-center">
{% endfor %} {% if contributor.user.avatar_img %}
</select> <img class="w-6 h-6 rounded-full" src="data:image/jpeg;base64,{{ contributor.user.avatar_img }}" alt="contributor avatar">
<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> {% else %}
</form> <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>
</td> {% endif %}
<td class="px-6 py-4"> <span class="ml-2 truncate">{{ contributor.user.username }}</span>
<!-- prettier-ignore --> </div>
<form action="{{ url_for('book.delete_contributor', book_id=book.id) }}" method="post" class="mb-0"> </td>
{{ form_hidden_tag() }} <td class="px-6 truncate max-w-[280]">{{ contributor.user.wallet_id }}</td>
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" /> <td class="px-6">
<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 action="{{ url_for('book.edit_contributor_role', book_id=book.id) }}" method="post" class="mb-0 flex">
</form> {{ form_hidden_tag() }}
</td> <input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
</tr> <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" >
{% endfor %} {% for role in roles if role.value %}
</tbody> <option
</table> {% if contributor.role == role %} selected {% endif %}
value="{{ role.value }}">
{{ role.name.title() }}
</option>
{% endfor %}
</select>
<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>
</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">
<!-- prettier-ignore -->
{% 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() }}
<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>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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>
</div> {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -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

View File

@ -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,26 +61,21 @@ def approve_interpretation(interpretation_id: int):
@bp.route( @bp.route(
"/comment/<int:interpretation_id>", "/comment/<int:comment_id>",
methods=["POST"], methods=["POST"],
) )
@require_permission(
entity_type=m.Permission.Entity.COMMENT,
access=[m.Permission.Access.A],
entities=[m.Comment],
)
@login_required @login_required
def approve_comment(interpretation_id: int): def approve_comment(comment_id: int):
comment: m.Comment = db.session.get(m.Comment, interpretation_id) comment: m.Comment = db.session.get(m.Comment, comment_id)
if not comment: if not comment:
log(log.WARNING, "Comment with id [%s] not found", interpretation_id) log(log.WARNING, "Comment with id [%s] not found", comment_id)
return jsonify({"message": "Comment not found"}), 404 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
comment.approved = not comment.approved comment.approved = not comment.approved
log( log(
log.INFO, log.INFO,

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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(

View File

@ -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,

View File

@ -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(

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

@ -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"}

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

View File

@ -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 ###

81
src/checkBoxTree.ts Normal file
View File

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

View File

@ -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();

View File

@ -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();

19
src/permissions.ts Normal file
View File

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

View File

@ -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(

View File

@ -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,
) )

View File

@ -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

View File

@ -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

View File

@ -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

340
tests/test_permissions.py Normal file
View File

@ -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

View File

@ -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: