mirror of https://github.com/logos-co/open-law.git
Merge pull request #107 from Simple2B/svyat/feat/user_permissions
Svyat/feat/user permissions
This commit is contained in:
commit
a4797e5ddb
|
@ -14,6 +14,7 @@
|
|||
"backref",
|
||||
"bookname",
|
||||
"Btns",
|
||||
"CUDA",
|
||||
"CLEANR",
|
||||
"Divs",
|
||||
"flowbite",
|
||||
|
|
|
@ -25,12 +25,10 @@ def create_app(environment="development"):
|
|||
vote_blueprint,
|
||||
approve_blueprint,
|
||||
star_blueprint,
|
||||
permissions_blueprint,
|
||||
search_blueprint,
|
||||
)
|
||||
from app.models import (
|
||||
User,
|
||||
AnonymousUser,
|
||||
)
|
||||
from app.models import User, AnonymousUser, Permission
|
||||
|
||||
# Instantiate app.
|
||||
app = Flask(__name__)
|
||||
|
@ -56,6 +54,7 @@ def create_app(environment="development"):
|
|||
app.register_blueprint(vote_blueprint)
|
||||
app.register_blueprint(approve_blueprint)
|
||||
app.register_blueprint(star_blueprint)
|
||||
app.register_blueprint(permissions_blueprint)
|
||||
app.register_blueprint(search_blueprint)
|
||||
|
||||
# Set up flask login.
|
||||
|
@ -73,12 +72,17 @@ def create_app(environment="development"):
|
|||
display_inline_elements,
|
||||
build_qa_url_using_interpretation,
|
||||
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["display_inline_elements"] = display_inline_elements
|
||||
app.jinja_env.globals["build_qa_url"] = build_qa_url_using_interpretation
|
||||
app.jinja_env.globals["recursive_render"] = recursive_render
|
||||
app.jinja_env.globals["has_permission"] = has_permission
|
||||
|
||||
# Error handlers.
|
||||
@app.errorhandler(HTTPException)
|
||||
|
|
|
@ -40,9 +40,9 @@ def book_validator() -> Response | None:
|
|||
book_id = request_args.get("book_id")
|
||||
if 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)
|
||||
flash("You are not owner of this book!", "danger")
|
||||
flash("Book not found!", "danger")
|
||||
return redirect(url_for("book.my_library"))
|
||||
|
||||
collection_id = request_args.get("collection_id")
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
from app import models as m
|
||||
from app.logger import log
|
||||
from .get_or_create_permission import get_or_create_permission
|
||||
|
||||
|
||||
def create_moderator_group(book_id: int):
|
||||
log(log.INFO, "Create moderator access group")
|
||||
group: m.AccessGroup = m.AccessGroup(name="moderator", book_id=book_id).save()
|
||||
permissions = []
|
||||
|
||||
comment_DA = get_or_create_permission(
|
||||
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||
entity_type=m.Permission.Entity.COMMENT,
|
||||
)
|
||||
permissions.append(comment_DA)
|
||||
|
||||
interpretation_DA = get_or_create_permission(
|
||||
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||
)
|
||||
permissions.append(interpretation_DA)
|
||||
|
||||
for permission in permissions:
|
||||
log(log.INFO, "Add permission [%d] to group[%d]", permission.id, group.id)
|
||||
m.PermissionAccessGroups(
|
||||
permission_id=permission.id, access_group_id=group.id
|
||||
).save()
|
||||
|
||||
return group
|
||||
|
||||
|
||||
def create_editor_group(book_id: int):
|
||||
log(log.INFO, "Create editor access group")
|
||||
group: m.AccessGroup = m.AccessGroup(name="editor", book_id=book_id).save()
|
||||
permissions = []
|
||||
|
||||
comment_DA = get_or_create_permission(
|
||||
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||
entity_type=m.Permission.Entity.COMMENT,
|
||||
)
|
||||
permissions.append(comment_DA)
|
||||
|
||||
interpretation_DA = get_or_create_permission(
|
||||
access=m.Permission.Access.D | m.Permission.Access.A,
|
||||
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||
)
|
||||
permissions.append(interpretation_DA)
|
||||
|
||||
section_CUD = get_or_create_permission(
|
||||
access=m.Permission.Access.C | m.Permission.Access.U | m.Permission.Access.D,
|
||||
entity_type=m.Permission.Entity.SECTION,
|
||||
)
|
||||
permissions.append(section_CUD)
|
||||
|
||||
collection_CUD = get_or_create_permission(
|
||||
access=m.Permission.Access.C | m.Permission.Access.U | m.Permission.Access.D,
|
||||
entity_type=m.Permission.Entity.COLLECTION,
|
||||
)
|
||||
permissions.append(collection_CUD)
|
||||
|
||||
book_U = get_or_create_permission(
|
||||
access=m.Permission.Access.U,
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
)
|
||||
permissions.append(book_U)
|
||||
|
||||
for permission in permissions:
|
||||
log(log.INFO, "Add permission [%d] to group[%d]", permission.id, group.id)
|
||||
m.PermissionAccessGroups(
|
||||
permission_id=permission.id, access_group_id=group.id
|
||||
).save()
|
||||
|
||||
return group
|
|
@ -0,0 +1,14 @@
|
|||
from app import models as m
|
||||
from app.logger import log
|
||||
|
||||
|
||||
def get_or_create_permission(access: int, entity_type: m.Permission.Entity):
|
||||
permission: m.Permission = m.Permission.query.filter_by(
|
||||
access=access, entity_type=entity_type
|
||||
).first()
|
||||
if not permission:
|
||||
log(log.INFO, "Create permission [%d] for entity [%s]", access, entity_type)
|
||||
permission: m.Permission = m.Permission(
|
||||
access=access, entity_type=entity_type
|
||||
).save()
|
||||
return permission
|
|
@ -3,6 +3,7 @@ import re
|
|||
from flask import current_app
|
||||
from flask_wtf import FlaskForm
|
||||
from flask import url_for, render_template
|
||||
from flask_login import current_user
|
||||
from sqlalchemy import func
|
||||
|
||||
from app import models as m
|
||||
|
@ -76,9 +77,58 @@ def build_qa_url_using_interpretation(interpretation: m.Interpretation):
|
|||
return url
|
||||
|
||||
|
||||
# Using: {{ recursive_render("template.html", collection=collection, book=book) }}
|
||||
def recursive_render(template: str, collection: m.Collection, book: m.Book):
|
||||
return render_template(
|
||||
template,
|
||||
collection=collection,
|
||||
book=book,
|
||||
)
|
||||
|
||||
|
||||
# Using: {{ has_permission(entity=book, required_permissions=[Access.create]) }}
|
||||
def has_permission(
|
||||
entity: m.Book | m.Collection | m.Section | m.Interpretation,
|
||||
required_permissions: m.Permission.Access | list[m.Permission.Access],
|
||||
entity_type: m.Permission.Entity = None,
|
||||
) -> bool:
|
||||
if not current_user.is_authenticated:
|
||||
return False
|
||||
|
||||
# check if user is owner of book
|
||||
match type(entity):
|
||||
case m.Book:
|
||||
if entity.user_id == current_user.id:
|
||||
return True
|
||||
case m.Collection | m.Section:
|
||||
if entity.version.book.user_id == current_user.id:
|
||||
return True
|
||||
case m.Interpretation:
|
||||
if entity.book.user_id == current_user.id:
|
||||
return True
|
||||
case _:
|
||||
...
|
||||
|
||||
if type(required_permissions) == m.Permission.Access:
|
||||
required_permissions = [required_permissions]
|
||||
|
||||
access_groups: list[m.AccessGroup] = list(
|
||||
set(entity.access_groups).intersection(current_user.access_groups)
|
||||
)
|
||||
|
||||
if not access_groups:
|
||||
return False
|
||||
|
||||
if not entity_type:
|
||||
entity_type = m.Permission.Entity[type(entity).__name__.upper()]
|
||||
|
||||
for access_group in access_groups:
|
||||
for permission in access_group.permissions:
|
||||
permission: m.Permission
|
||||
if permission.entity_type != entity_type:
|
||||
continue
|
||||
for required_permission in required_permissions:
|
||||
if permission.access & required_permission:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
from flask_login import current_user
|
||||
from flask import flash, redirect, url_for, request, make_response
|
||||
import functools
|
||||
|
||||
from app import models as m, db
|
||||
from app.logger import log
|
||||
|
||||
|
||||
def check_permissions(
|
||||
entity_type: m.Permission.Entity,
|
||||
access: list[m.Permission.Access],
|
||||
entities: list[dict],
|
||||
):
|
||||
if not current_user.is_authenticated:
|
||||
flash("You do not have permission", "danger")
|
||||
return make_response(redirect(url_for("home.get_all")))
|
||||
|
||||
request_args = (
|
||||
{**request.view_args, **request.args} if request.view_args else {**request.args}
|
||||
)
|
||||
entity = None
|
||||
for model in entities:
|
||||
entity_id_field = (model.__name__ + "_id").lower()
|
||||
entity_id = request_args.get(entity_id_field)
|
||||
entity: m.Book | m.Collection | m.Section | m.Interpretation | m.Comment = (
|
||||
db.session.get(model, entity_id)
|
||||
)
|
||||
|
||||
if entity is None:
|
||||
log(log.INFO, "No entity [%s] found", entities)
|
||||
flash("You do not have permission", "danger")
|
||||
return make_response(redirect(url_for("home.get_all")))
|
||||
|
||||
book_id = request_args.get("book_id")
|
||||
if book_id:
|
||||
book: m.Book = db.session.get(m.Book, book_id)
|
||||
if book and book.user_id == current_user.id:
|
||||
# user has access because he is book owner
|
||||
log(log.INFO, "User [%s] is book owner [%s]", current_user, book)
|
||||
return None
|
||||
|
||||
if type(entity) == m.Comment:
|
||||
log(log.INFO, "Entity is Comment. Replace it by entity.interpretation")
|
||||
entity = entity.interpretation
|
||||
|
||||
if not entity or not entity.access_groups:
|
||||
log(
|
||||
log.INFO,
|
||||
"Entity [%s] of entity.access_groups [%s] not found",
|
||||
access,
|
||||
entity,
|
||||
)
|
||||
flash("You do not have permission", "warning")
|
||||
return make_response(redirect(url_for("home.get_all")))
|
||||
|
||||
# check if user is not owner of book
|
||||
if not book_id and entity.access_groups[0].book.user_id == current_user.id:
|
||||
# user has access because he is book owner
|
||||
log(
|
||||
log.INFO,
|
||||
"User [%s] is book owner [%s]",
|
||||
current_user,
|
||||
entity.access_groups[0].book,
|
||||
)
|
||||
return None
|
||||
|
||||
access_group_query = (
|
||||
m.AccessGroup.query.join(
|
||||
m.PermissionAccessGroups,
|
||||
m.PermissionAccessGroups.access_group_id == m.AccessGroup.id,
|
||||
)
|
||||
.join(m.Permission, m.PermissionAccessGroups.permission_id == m.Permission.id)
|
||||
.filter(
|
||||
m.AccessGroup.id.in_(
|
||||
[access_group.id for access_group in entity.access_groups]
|
||||
)
|
||||
)
|
||||
.filter(m.AccessGroup.users.any(id=current_user.id))
|
||||
.filter(m.Permission.entity_type == entity_type)
|
||||
)
|
||||
|
||||
for access in access:
|
||||
access_group_query = access_group_query.filter(
|
||||
m.Permission.access.op("&")(access) > 0
|
||||
)
|
||||
|
||||
access_groups = access_group_query.all()
|
||||
|
||||
if access_groups:
|
||||
log(
|
||||
log.INFO,
|
||||
"User [%s] has permission to [%s] [%s]",
|
||||
access,
|
||||
current_user,
|
||||
entity,
|
||||
)
|
||||
return
|
||||
|
||||
log(
|
||||
log.INFO,
|
||||
"User [%s] dont have permission to [%s] [%s]",
|
||||
current_user,
|
||||
access,
|
||||
entity,
|
||||
)
|
||||
flash("You do not have permission", "danger")
|
||||
return make_response(redirect(url_for("home.get_all")))
|
||||
|
||||
|
||||
def require_permission(
|
||||
entity_type: m.Permission.Entity,
|
||||
access: list[m.Permission.Access],
|
||||
entities: list[dict],
|
||||
):
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def permission_checker(*args, **kwargs):
|
||||
if response := check_permissions(
|
||||
entity_type=entity_type,
|
||||
access=access,
|
||||
entities=entities,
|
||||
):
|
||||
return response
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return permission_checker
|
||||
|
||||
return decorator
|
|
@ -17,3 +17,4 @@ from .interpretation import (
|
|||
from .comment import CreateCommentForm
|
||||
from .vote import VoteForm
|
||||
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm
|
||||
from .permission import EditPermissionForm
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
from flask_wtf import FlaskForm
|
||||
from wtforms import StringField, SubmitField, IntegerField
|
||||
from wtforms.validators import DataRequired
|
||||
|
||||
|
||||
class EditPermissionForm(FlaskForm):
|
||||
book_id = IntegerField("Book ID", [DataRequired()])
|
||||
user_id = IntegerField("User ID", [DataRequired()])
|
||||
permissions = StringField("Permissions JSON", [DataRequired()])
|
||||
submit = SubmitField("Edit")
|
|
@ -13,5 +13,15 @@ from .interpretation_vote import InterpretationVote
|
|||
from .tag import Tag
|
||||
from .interpretation_tag import InterpretationTag
|
||||
from .comment_tag import CommentTags
|
||||
from .permission import (
|
||||
Permission,
|
||||
AccessGroup,
|
||||
UserAccessGroups,
|
||||
PermissionAccessGroups,
|
||||
BookAccessGroups,
|
||||
CollectionAccessGroups,
|
||||
SectionAccessGroups,
|
||||
InterpretationAccessGroups,
|
||||
)
|
||||
from .book_tag import BookTags
|
||||
from .section_tag import SectionTag
|
||||
|
|
|
@ -18,7 +18,14 @@ class Book(BaseModel):
|
|||
owner = db.relationship("User", viewonly=True)
|
||||
stars = db.relationship("User", secondary="books_stars", back_populates="stars")
|
||||
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(
|
||||
"Tag",
|
||||
secondary="book_tags",
|
||||
|
|
|
@ -25,6 +25,10 @@ class Collection(BaseModel):
|
|||
order_by="asc(Collection.id)",
|
||||
)
|
||||
sections = db.relationship("Section")
|
||||
access_groups = db.relationship(
|
||||
"AccessGroup",
|
||||
secondary="collections_access_groups",
|
||||
) # access_groups related to current entity
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.id}: {self.label}>"
|
||||
|
|
|
@ -26,6 +26,10 @@ class Interpretation(BaseModel):
|
|||
secondary="interpretation_tags",
|
||||
back_populates="interpretations",
|
||||
)
|
||||
access_groups = db.relationship(
|
||||
"AccessGroup",
|
||||
secondary="interpretations_access_groups",
|
||||
) # access_groups related to current entity
|
||||
|
||||
@property
|
||||
def vote_count(self):
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# flake8: noqa F401
|
||||
from .access_group import AccessGroup
|
||||
from .permission import Permission
|
||||
from .user_access_groups import UserAccessGroups
|
||||
from .permission_access_groups import PermissionAccessGroups
|
||||
from .book_access_groups import BookAccessGroups
|
||||
from .collection_access_groups import CollectionAccessGroups
|
||||
from .section_access_groups import SectionAccessGroups
|
||||
from .interpretation_access_groups import InterpretationAccessGroups
|
|
@ -0,0 +1,25 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class AccessGroup(BaseModel):
|
||||
__tablename__ = "access_groups"
|
||||
|
||||
name = db.Column(db.String(32), nullable=False)
|
||||
|
||||
# Foreign Keys
|
||||
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
|
||||
|
||||
# Relationships
|
||||
book = db.relationship("Book", viewonly=True)
|
||||
permissions = db.relationship(
|
||||
"Permission",
|
||||
secondary="permissions_access_groups",
|
||||
back_populates="access_groups",
|
||||
)
|
||||
users = db.relationship(
|
||||
"User", secondary="users_access_groups", back_populates="access_groups"
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<{self.id}: {self.name} | Book: {self.book_id}>"
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class BookAccessGroups(BaseModel):
|
||||
__tablename__ = "books_access_groups"
|
||||
|
||||
# Foreign keys
|
||||
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
|
||||
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<b:{self.book_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class CollectionAccessGroups(BaseModel):
|
||||
__tablename__ = "collections_access_groups"
|
||||
|
||||
# Foreign keys
|
||||
collection_id = db.Column(db.Integer, db.ForeignKey("collections.id"))
|
||||
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<c:{self.collection_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class InterpretationAccessGroups(BaseModel):
|
||||
__tablename__ = "interpretations_access_groups"
|
||||
|
||||
# Foreign keys
|
||||
interpretation_id = db.Column(db.Integer, db.ForeignKey("interpretations.id"))
|
||||
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<c:{self.interpretation_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,33 @@
|
|||
from enum import IntEnum
|
||||
|
||||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class Permission(BaseModel):
|
||||
__tablename__ = "permissions"
|
||||
|
||||
class Access(IntEnum):
|
||||
C = 1 # 0b0001 - Create
|
||||
U = 2 # 0b0010 - Update
|
||||
D = 4 # 0b0100 - Delete
|
||||
A = 8 # 0b1000 - Approve
|
||||
# sum = 0b1111
|
||||
|
||||
class Entity(IntEnum):
|
||||
UNKNOWN = 0
|
||||
BOOK = 1
|
||||
COLLECTION = 2
|
||||
SECTION = 3
|
||||
INTERPRETATION = 4
|
||||
COMMENT = 5
|
||||
|
||||
access = db.Column(db.Integer(), default=Access.C | Access.U | Access.D | Access.A)
|
||||
entity_type = db.Column(db.Enum(Entity), default=Entity.UNKNOWN)
|
||||
|
||||
# Relationships
|
||||
access_groups = db.relationship(
|
||||
"AccessGroup",
|
||||
secondary="permissions_access_groups",
|
||||
back_populates="permissions",
|
||||
)
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class PermissionAccessGroups(BaseModel):
|
||||
__tablename__ = "permissions_access_groups"
|
||||
|
||||
# Foreign keys
|
||||
permission_id = db.Column(db.Integer, db.ForeignKey("permissions.id"))
|
||||
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<p:{self.permission_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class SectionAccessGroups(BaseModel):
|
||||
__tablename__ = "sections_access_groups"
|
||||
|
||||
# Foreign keys
|
||||
section_id = db.Column(db.Integer, db.ForeignKey("sections.id"))
|
||||
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<s:{self.section_id} to a_g:{self.access_group_id}"
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class UserAccessGroups(BaseModel):
|
||||
__tablename__ = "users_access_groups"
|
||||
|
||||
# Foreign keys
|
||||
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
|
||||
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<u:{self.user_id} to a_g:{self.access_group_id}"
|
|
@ -26,6 +26,10 @@ class Section(BaseModel):
|
|||
interpretations = db.relationship(
|
||||
"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(
|
||||
"Tag",
|
||||
secondary="section_tags",
|
||||
|
|
|
@ -23,7 +23,11 @@ class User(BaseModel, UserMixin):
|
|||
is_activated = db.Column(db.Boolean, default=False)
|
||||
wallet_id = db.Column(db.String(64), nullable=True)
|
||||
avatar_img = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
access_groups = db.relationship(
|
||||
"AccessGroup", secondary="users_access_groups", back_populates="users"
|
||||
)
|
||||
stars = db.relationship("Book", secondary="books_stars", back_populates="stars")
|
||||
books = db.relationship("Book")
|
||||
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -28,17 +28,20 @@
|
|||
Table of contents
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex text-black dark:text-white">
|
||||
<!-- prettier-ignore -->
|
||||
<div>
|
||||
{% if not book.versions[-1].children_collections and current_user.is_authenticated %}
|
||||
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" ><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> </button>
|
||||
{% endif %}
|
||||
<a href="{{ url_for("book.settings", book_id=book.id) }}" type="button" class="ml-2" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
|
||||
</a>
|
||||
{% if has_permission(book, Access.U) %}
|
||||
<div class="flex text-black dark:text-white">
|
||||
<!-- prettier-ignore -->
|
||||
<div>
|
||||
{% if not book.versions[-1].children_collections and current_user.is_authenticated %}
|
||||
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" ><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> </button>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('book.settings', book_id=book.id) }}" type="button" class="ml-2" >
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6"> <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
{% 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>
|
||||
</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">
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<li>
|
||||
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Collection</button>
|
||||
</li>
|
||||
{% if not collection.is_leaf %}
|
||||
<li>
|
||||
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Subcollection</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if collection.children|length ==0 or collection.children|length ==0 and collection.is_leaf %}
|
||||
<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>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<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>
|
||||
<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>
|
||||
</ul>
|
||||
{% if current_user.is_authenticated %}
|
||||
{% set access_to_create_collections = has_permission(collection, Access.C) %}
|
||||
{% set access_to_update_collections = has_permission(collection, Access.U) %}
|
||||
{% set access_to_delete_collections = has_permission(collection, Access.D) %}
|
||||
{% set access_to_create_section = has_permission(collection, Access.C, EntityType.SECTION) %}
|
||||
|
||||
{% if access_to_create_collections or access_to_update_collections %}
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
{% if access_to_create_collections %}
|
||||
<li>
|
||||
<button type="button" data-modal-target="add-collection-modal" data-modal-toggle="add-collection-modal" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Collection</button>
|
||||
</li>
|
||||
{% if not collection.is_leaf %}
|
||||
<li>
|
||||
<button type="button" id="callAddSubCollectionModal" data-modal-target="add-sub-collection-modal" data-modal-toggle="add-sub-collection-modal" data-collection-id="{{collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">New Subcollection</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if access_to_create_section %}
|
||||
{% if collection.children|length ==0 or collection.children|length ==0 and collection.is_leaf %}
|
||||
<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>
|
||||
</li>
|
||||
{% 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">
|
||||
<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>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
{% else %}
|
||||
<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">Connect you wallet to do this</button>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
<div id="accordion-collapse-body-{{collection.id}}" class="hidden" aria-labelledby="accordion-collapse-heading-{{collection.id}}">
|
||||
|
|
|
@ -23,7 +23,8 @@
|
|||
id="edit-section-label-{{section.id}}"
|
||||
placeholder="Section label"
|
||||
required
|
||||
readonly />
|
||||
readonly
|
||||
/>
|
||||
<button name="submit" type="submit"></button>
|
||||
</form>
|
||||
</button>
|
||||
|
@ -41,51 +42,63 @@
|
|||
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 %}
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
id="rename-section-button-{{section.id}}"
|
||||
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
Rename Section
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
<button
|
||||
type="button"
|
||||
data-modal-target="delete-section-modal"
|
||||
data-modal-toggle="delete-section-modal"
|
||||
id="callDeleteSectionModal"
|
||||
data-collection-id="{{collection.id}}"
|
||||
{% if sub_collection %}
|
||||
data-sub-collection-id="{{sub_collection.id}}"
|
||||
{% set access_to_create_sections = has_permission(section, Access.C) %}
|
||||
{% set access_to_update_sections = has_permission(section, Access.U) %}
|
||||
{% set access_to_delete_sections = has_permission(section, Access.D) %}
|
||||
|
||||
{% if access_to_update_sections or access_to_delete_sections %}
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
{% if access_to_update_sections %}
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
id="rename-section-button-{{section.id}}"
|
||||
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
Rename Section
|
||||
</button>
|
||||
</li>
|
||||
{% 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">
|
||||
Delete Section
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
{% if access_to_delete_sections %}
|
||||
<li>
|
||||
<!-- prettier-ignore -->
|
||||
<button
|
||||
type="button"
|
||||
data-modal-target="delete-section-modal"
|
||||
data-modal-toggle="delete-section-modal"
|
||||
id="callDeleteSectionModal"
|
||||
data-collection-id="{{collection.id}}"
|
||||
{% if sub_collection %}
|
||||
data-sub-collection-id="{{sub_collection.id}}"
|
||||
{% 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">
|
||||
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 %}
|
||||
<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">
|
||||
Connect your wallet to do this
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<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">
|
||||
Connect your wallet to do this
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -31,43 +31,63 @@
|
|||
<!-- 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">
|
||||
{% if current_user.is_authenticated %}
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
{% if sub_collection.is_leaf and not sub_collection.children %}
|
||||
<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>
|
||||
</li>
|
||||
{% elif not sub_collection.is_leaf and not sub_collection.children %}
|
||||
<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>
|
||||
</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>
|
||||
</li>
|
||||
{% else %}
|
||||
<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>
|
||||
{% set access_to_create_collections = has_permission(sub_collection, Access.C) %}
|
||||
{% set access_to_update_collections = has_permission(sub_collection, Access.U) %}
|
||||
{% set access_to_delete_collections = has_permission(sub_collection, Access.D) %}
|
||||
{% set access_to_create_section = has_permission(collection, Access.C, EntityType.SECTION) %}
|
||||
|
||||
{% if access_to_create_collections or access_to_update_collections or access_to_create_section %}
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
{% if access_to_create_section and sub_collection.is_leaf and not sub_collection.children %}
|
||||
<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>
|
||||
</li>
|
||||
{% elif not sub_collection.is_leaf and not sub_collection.children %}
|
||||
{% if access_to_create_section %}
|
||||
<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>
|
||||
</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 %}
|
||||
</ul>
|
||||
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
|
||||
<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>
|
||||
</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>
|
||||
</li>
|
||||
</ul>
|
||||
<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>
|
||||
|
||||
{% 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-sub-collection-button-{{sub_collection.id}}" class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"> Rename Sub Collection </button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if access_to_delete_collections %}
|
||||
<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>
|
||||
</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 Sub Collection </button>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<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"> Connect your wallet to do this </button>
|
||||
</li>
|
||||
</ul>
|
||||
<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"> Connect your wallet to do this </button>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
|
|
|
@ -80,12 +80,16 @@
|
|||
{% endif %}
|
||||
<!-- prettier-ignore -->
|
||||
<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 -->
|
||||
{% for interpretation in section.active_interpretations %}
|
||||
<!-- 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">
|
||||
<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-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="true">
|
||||
<svg class="w-6 h-6 select-none
|
||||
{% if interpretation.current_user_vote %}
|
||||
|
@ -93,8 +97,6 @@
|
|||
{% 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>
|
||||
|
||||
|
||||
<span
|
||||
class="vote-count text-3xl select-none
|
||||
{% if interpretation.vote_count < 0 %}
|
||||
|
@ -106,7 +108,6 @@
|
|||
>
|
||||
{{ interpretation.vote_count }}
|
||||
</span>
|
||||
|
||||
<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
|
||||
{% 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>
|
||||
</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 }}">
|
||||
<!-- 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">
|
||||
|
@ -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" />
|
||||
</svg>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if interpretation.user_id == current_user.id %}
|
||||
<!--Edit & Delete interpretation-->
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<p>Edit this interpretation</p>
|
||||
<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>
|
||||
</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 class="px-3 py-2">
|
||||
<p>Edit this interpretation</p>
|
||||
</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<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 %}
|
||||
|
||||
{% 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 %}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
<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">
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<!-- prettier-ignore-->
|
||||
<div id="access-level-modal" tabindex="-1" aria-hidden="true"
|
||||
class="fixed top-0 left-0 right-0 z-[150] hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div class="relative w-full max-w-2xl max-h-full">
|
||||
<!-- Modal content -->
|
||||
<form action="{{ url_for('permission.set') }}" method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
{{ form_hidden_tag() }}
|
||||
<input type="hidden" name="book_id" id="permission_modal_book_id"/>
|
||||
<input type="hidden" name="user_id" id="permission_modal_user_id"/>
|
||||
<input type="hidden" name="permissions" id="permissions_json"/>
|
||||
<!-- Modal header -->
|
||||
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Access Level
|
||||
</h3>
|
||||
<button id="modalAddCloseButton" data-modal-hide="access-level-modal" type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<svg aria-hidden="true" class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"> </path> </svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="checkbox-tree">
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" data-root="true" data-access-to="book" data-access-to-id="{{ book.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
|
||||
<span class="text-center dark:text-gray-300">{{ book.label }}</span>
|
||||
</div>
|
||||
{% for collection in book.last_version.children_collections %}
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" data-access-to="collection" data-access-to-id="{{ collection.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
|
||||
<span class="text-center dark:text-gray-300">{{ collection.label }}</span>
|
||||
</div>
|
||||
|
||||
{% for sub_collection in collection.children %}
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" data-access-to="sub_collection" data-access-to-id="{{ sub_collection.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
|
||||
<span class="text-center dark:text-gray-300">{{ sub_collection.label }}</span>
|
||||
</div>
|
||||
{% for section in sub_collection.sections %}
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" data-access-to="section" data-access-to-id="{{ section.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
|
||||
<span class="text-center dark:text-gray-300">{{ section.label }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
||||
{% for section in collection.sections %}
|
||||
<ul class="ml-4">
|
||||
<li>
|
||||
<div class="flex items-center space-x-2">
|
||||
<input type="checkbox" data-access-to="section" data-access-to-id="{{ section.id }}" class="w-4 h-4 text-blue-600 bg-gray-300 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-400 dark:border-gray-600" />
|
||||
<span class="text-center dark:text-gray-300">{{ section.label }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
{% endfor %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal footer -->
|
||||
<div class="flex items-center p-6 space-x-2 border-t border-gray-200 rounded-b dark:border-gray-600">
|
||||
<button name="submit" type="submit"
|
||||
class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
|
@ -72,25 +72,20 @@
|
|||
</div>
|
||||
<!-- prettier-ignore -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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="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">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
||||
<span
|
||||
class="vote-count text-3xl select-none
|
||||
{% if comment.vote_count < 0 %}
|
||||
|
@ -102,18 +97,12 @@
|
|||
>
|
||||
{{ comment.vote_count }}
|
||||
</span>
|
||||
|
||||
|
||||
<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
|
||||
{% 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>
|
||||
<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>
|
||||
</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 }}">
|
||||
<!-- 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">
|
||||
|
@ -151,28 +140,31 @@
|
|||
|
||||
<div class="flex ml-auto justify-end space-x-2 w-24">
|
||||
{% if comment.user_id == current_user.id %}
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<p>Edit this comment</p>
|
||||
<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">
|
||||
<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>
|
||||
<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">
|
||||
<p>Edit this comment</p>
|
||||
</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<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 comment</p>
|
||||
{% endif %}
|
||||
|
||||
{% if comment.user_id == current_user.id or access_to_delete_comment %}
|
||||
<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">
|
||||
<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 comment</p>
|
||||
</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
<div data-popper-arrow></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="relative">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<!-- prettier-ignore -->
|
||||
{% extends 'base.html' %}
|
||||
{% include 'book/modals/access_level_modal.html' %}
|
||||
{% include 'book/modals/add_contributor_modal.html' %}
|
||||
{% include 'book/modals/delete_book_modal.html' %}
|
||||
|
||||
|
@ -26,13 +27,15 @@
|
|||
<span>Book settings</span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="mr-2" role="presentation">
|
||||
<!-- prettier-ignore -->
|
||||
<button class="flex items-center space-x-2 p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="permissions-tab" data-tabs-target="#permissions" type="button" role="tab" aria-controls="permissions" aria-selected="false">
|
||||
<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>
|
||||
<span>User permissions</span>
|
||||
</button>
|
||||
</li>
|
||||
{% if book.user_id == current_user.id %}
|
||||
<li class="mr-2" role="presentation">
|
||||
<!-- prettier-ignore -->
|
||||
<button class="flex items-center space-x-2 p-4 border-b-2 border-transparent rounded-t-lg hover:text-gray-600 hover:border-gray-300 dark:hover:text-gray-300" id="permissions-tab" data-tabs-target="#permissions" type="button" role="tab" aria-controls="permissions" aria-selected="false">
|
||||
<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>
|
||||
<span>User permissions</span>
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -77,55 +80,89 @@
|
|||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="permissions" role="tabpanel" aria-labelledby="permissions-tab">
|
||||
<div class="p-5">
|
||||
<div class="flex justify-between ml-4 mb-2">
|
||||
<h1 class="text-2xl font-extrabold dark:text-white">Contributors</h1>
|
||||
<!-- prettier-ignore -->
|
||||
<button type="button" data-modal-target="add-contributor-modal" data-modal-toggle="add-contributor-modal" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke"currentColor" class="w-6 h-6 mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr> <th scope="col" class="px-6 py-3">Username</th> <th scope="col" class="px-6 py-3">Role</th> <th scope="col" class="px-6 py-3">Action</th> </tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contributor in book.contributors %}
|
||||
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700">
|
||||
<td class="px-6 py-4">{{ contributor.user.username }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<form action="{{ url_for('book.edit_contributor_role', book_id=book.id) }}" method="post" class="mb-0 flex space-x-2">
|
||||
{{ form_hidden_tag() }}
|
||||
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
|
||||
<select id="role" name="role" data-user-id="{{ contributor.user.id }}" class="contributor-role-select block w-1/2 py-1.5 px-2 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" >
|
||||
{% for role in roles if role.value %}
|
||||
<option
|
||||
{% 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-6 py-4">
|
||||
<!-- prettier-ignore -->
|
||||
<form action="{{ url_for('book.delete_contributor', book_id=book.id) }}" method="post" class="mb-0">
|
||||
{{ 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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% if book.user_id == current_user.id %}
|
||||
<div class="hidden px-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="permissions" role="tabpanel" aria-labelledby="permissions-tab">
|
||||
<div class="px-5">
|
||||
|
||||
<div class="mb-3 relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Username
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Address
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
Role
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-center">
|
||||
Access Level
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contributor in book.contributors %}
|
||||
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700">
|
||||
<td class="px-6 max-w-[230]">
|
||||
<div class="flex items-center">
|
||||
{% if contributor.user.avatar_img %}
|
||||
<img class="w-6 h-6 rounded-full" src="data:image/jpeg;base64,{{ contributor.user.avatar_img }}" alt="contributor avatar">
|
||||
{% else %}
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" /> </svg>
|
||||
{% endif %}
|
||||
<span class="ml-2 truncate">{{ contributor.user.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 truncate max-w-[280]">{{ contributor.user.wallet_id }}</td>
|
||||
<td class="px-6">
|
||||
<form action="{{ url_for('book.edit_contributor_role', book_id=book.id) }}" method="post" class="mb-0 flex">
|
||||
{{ form_hidden_tag() }}
|
||||
<input type="hidden" name="user_id" id="user_id" value="{{ contributor.user_id }}" />
|
||||
<select id="role" name="role" data-user-id="{{ contributor.user.id }}" class="mr-2 contributor-role-select block w-1/2 py-1.5 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" >
|
||||
{% for role in roles if role.value %}
|
||||
<option
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -7,4 +7,5 @@ from .home import bp as home_blueprint
|
|||
from .vote import bp as vote_blueprint
|
||||
from .approve import bp as approve_blueprint
|
||||
from .star import bp as star_blueprint
|
||||
from .permission import bp as permissions_blueprint
|
||||
from .search import bp as search_blueprint
|
||||
|
|
|
@ -5,6 +5,7 @@ from flask import (
|
|||
from flask_login import login_required, current_user
|
||||
|
||||
from app import models as m, db
|
||||
from app.controllers.require_permission import require_permission
|
||||
from app.logger import log
|
||||
|
||||
bp = Blueprint("approve", __name__, url_prefix="/approve")
|
||||
|
@ -14,6 +15,11 @@ bp = Blueprint("approve", __name__, url_prefix="/approve")
|
|||
"/interpretation/<int:interpretation_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||
access=[m.Permission.Access.A],
|
||||
entities=[m.Interpretation],
|
||||
)
|
||||
@login_required
|
||||
def approve_interpretation(interpretation_id: int):
|
||||
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)
|
||||
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 = (
|
||||
m.Interpretation.query.filter_by(
|
||||
approved=True, section_id=interpretation.section_id
|
||||
|
@ -65,26 +61,21 @@ def approve_interpretation(interpretation_id: int):
|
|||
|
||||
|
||||
@bp.route(
|
||||
"/comment/<int:interpretation_id>",
|
||||
"/comment/<int:comment_id>",
|
||||
methods=["POST"],
|
||||
)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.COMMENT,
|
||||
access=[m.Permission.Access.A],
|
||||
entities=[m.Comment],
|
||||
)
|
||||
@login_required
|
||||
def approve_comment(interpretation_id: int):
|
||||
comment: m.Comment = db.session.get(m.Comment, interpretation_id)
|
||||
def approve_comment(comment_id: int):
|
||||
comment: m.Comment = db.session.get(m.Comment, comment_id)
|
||||
if not comment:
|
||||
log(log.WARNING, "Comment with id [%s] not found", interpretation_id)
|
||||
log(log.WARNING, "Comment with id [%s] not found", comment_id)
|
||||
return jsonify({"message": "Comment not found"}), 404
|
||||
|
||||
# TODO check permission
|
||||
if comment.interpretation.book.owner != current_user:
|
||||
log(
|
||||
log.WARNING,
|
||||
"User [%s] dont have permission to approve [%s]",
|
||||
current_user,
|
||||
comment,
|
||||
)
|
||||
return jsonify({"message": "You dont have permission"}), 404
|
||||
|
||||
comment.approved = not comment.approved
|
||||
log(
|
||||
log.INFO,
|
||||
|
|
|
@ -17,6 +17,11 @@ from app.controllers.tags import (
|
|||
from app.controllers.delete_nested_book_entities import (
|
||||
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.logger import log
|
||||
from .bp import bp
|
||||
|
@ -85,12 +90,24 @@ def create():
|
|||
log(log.INFO, "Form submitted. Book: [%s]", book)
|
||||
book.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
|
||||
).save()
|
||||
tags = form.tags.data or ""
|
||||
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")
|
||||
return redirect(url_for("book.my_library"))
|
||||
else:
|
||||
|
@ -104,6 +121,11 @@ def create():
|
|||
|
||||
@bp.route("/<int:book_id>/edit", methods=["POST"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Book],
|
||||
)
|
||||
@login_required
|
||||
def edit(book_id: int):
|
||||
form = f.EditBookForm()
|
||||
|
@ -130,13 +152,18 @@ def edit(book_id: int):
|
|||
|
||||
|
||||
@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
|
||||
def delete(book_id: int):
|
||||
book: m.Book = db.session.get(m.Book, book_id)
|
||||
|
||||
if not book or book.is_deleted:
|
||||
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"))
|
||||
|
||||
book.is_deleted = True
|
||||
|
|
|
@ -14,6 +14,7 @@ from app.controllers.delete_nested_book_entities import (
|
|||
delete_nested_collection_entities,
|
||||
)
|
||||
from app import models as m, db, forms as f
|
||||
from app.controllers.require_permission import require_permission
|
||||
from app.logger import log
|
||||
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>/<int:collection_id>/create_sub_collection", methods=["POST"])
|
||||
@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
|
||||
def collection_create(book_id: int, collection_id: int | None = None):
|
||||
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)
|
||||
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")
|
||||
if collection_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"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.COLLECTION,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Collection],
|
||||
)
|
||||
@login_required
|
||||
def collection_edit(book_id: int, collection_id: int):
|
||||
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"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.COLLECTION,
|
||||
access=[m.Permission.Access.D],
|
||||
entities=[m.Collection],
|
||||
)
|
||||
@login_required
|
||||
def collection_delete(book_id: int, collection_id: int):
|
||||
collection: m.Collection = db.session.get(m.Collection, collection_id)
|
||||
|
|
|
@ -25,7 +25,7 @@ def create_comment(
|
|||
book: m.Book = db.session.get(m.Book, book_id)
|
||||
if not book or book.is_deleted:
|
||||
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"))
|
||||
redirect_url = url_for(
|
||||
"book.qa_view", book_id=book_id, interpretation_id=interpretation_id
|
||||
|
|
|
@ -12,6 +12,7 @@ from app.controllers.delete_nested_book_entities import (
|
|||
delete_nested_interpretation_entities,
|
||||
)
|
||||
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.logger import log
|
||||
from .bp import bp
|
||||
|
@ -86,6 +87,13 @@ def interpretation_create(
|
|||
)
|
||||
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)
|
||||
set_interpretation_tags(interpretation, tags)
|
||||
|
||||
|
@ -109,14 +117,18 @@ def interpretation_edit(
|
|||
book_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()
|
||||
|
||||
if form.validate_on_submit():
|
||||
text = form.text.data
|
||||
interpretation_id = form.interpretation_id.data
|
||||
interpretation: m.Interpretation = db.session.get(
|
||||
m.Interpretation, interpretation_id
|
||||
)
|
||||
redirect_url = url_for(
|
||||
"book.interpretation_view",
|
||||
book_id=book_id,
|
||||
|
@ -161,26 +173,24 @@ def interpretation_edit(
|
|||
"/<int:book_id>/<int:interpretation_id>/delete_interpretation", methods=["POST"]
|
||||
)
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.INTERPRETATION,
|
||||
access=[m.Permission.Access.D],
|
||||
entities=[m.Interpretation],
|
||||
)
|
||||
@login_required
|
||||
def interpretation_delete(
|
||||
book_id: int,
|
||||
interpretation_id: int,
|
||||
):
|
||||
form = f.DeleteInterpretationForm()
|
||||
interpretation_id = form.interpretation_id.data
|
||||
interpretation: m.Interpretation = db.session.get(
|
||||
m.Interpretation, interpretation_id
|
||||
)
|
||||
if not interpretation or interpretation.is_deleted:
|
||||
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
|
||||
flash("Interpretation not found", "danger")
|
||||
return redirect(
|
||||
url_for(
|
||||
"book.interpretation_view",
|
||||
book_id=book_id,
|
||||
section_id=interpretation.section_id,
|
||||
)
|
||||
)
|
||||
return redirect(url_for("book.collection_view", book_id=book_id))
|
||||
form = f.DeleteInterpretationForm()
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
if not book or book.is_deleted:
|
||||
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"))
|
||||
|
||||
interpretation: m.Interpretation = db.session.get(
|
||||
|
|
|
@ -8,12 +8,18 @@ from flask_login import login_required
|
|||
from app.controllers import register_book_verify_route
|
||||
from app.controllers.delete_nested_book_entities import delete_nested_section_entities
|
||||
from app import models as m, db, forms as f
|
||||
from app.controllers.require_permission import require_permission
|
||||
from app.logger import log
|
||||
from .bp import bp
|
||||
|
||||
|
||||
@bp.route("/<int:book_id>/<int:collection_id>/create_section", methods=["POST"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.SECTION,
|
||||
access=[m.Permission.Access.C],
|
||||
entities=[m.Collection],
|
||||
)
|
||||
@login_required
|
||||
def section_create(book_id: int, collection_id: int):
|
||||
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)
|
||||
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")
|
||||
return redirect(redirect_url)
|
||||
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"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.SECTION,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Section],
|
||||
)
|
||||
@login_required
|
||||
def section_edit(book_id: int, section_id: int):
|
||||
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"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.SECTION,
|
||||
access=[m.Permission.Access.D],
|
||||
entities=[m.Section],
|
||||
)
|
||||
@login_required
|
||||
def section_delete(
|
||||
book_id: int,
|
||||
|
|
|
@ -10,12 +10,18 @@ from app.controllers import (
|
|||
register_book_verify_route,
|
||||
)
|
||||
from app import models as m, db, forms as f
|
||||
from app.controllers.require_permission import require_permission
|
||||
from app.logger import log
|
||||
from .bp import bp
|
||||
|
||||
|
||||
@bp.route("/<int:book_id>/settings", methods=["GET"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Book],
|
||||
)
|
||||
@login_required
|
||||
def settings(book_id: int):
|
||||
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"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Book],
|
||||
)
|
||||
@login_required
|
||||
def add_contributor(book_id: int):
|
||||
form = f.AddContributorForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_id = form.user_id.data
|
||||
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()
|
||||
if 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))
|
||||
|
||||
role = m.BookContributor.Roles(int(form.role.data))
|
||||
contributor = m.BookContributor(
|
||||
user_id=form.user_id.data, book_id=book_id, role=role
|
||||
)
|
||||
contributor = m.BookContributor(user_id=user_id, book_id=book_id, role=role)
|
||||
log(log.INFO, "New contributor [%s]", contributor)
|
||||
contributor.save()
|
||||
|
||||
groups = (
|
||||
db.session.query(m.AccessGroup)
|
||||
.filter(
|
||||
m.BookAccessGroups.book_id == book_id,
|
||||
m.AccessGroup.id == m.BookAccessGroups.access_group_id,
|
||||
m.AccessGroup.name == role.name.lower(),
|
||||
)
|
||||
.all()
|
||||
)
|
||||
for group in groups:
|
||||
m.UserAccessGroups(user_id=user_id, access_group_id=group.id).save()
|
||||
|
||||
flash("Contributor was added!", "success")
|
||||
return redirect(url_for("book.settings", book_id=book_id))
|
||||
else:
|
||||
|
@ -60,24 +82,47 @@ def add_contributor(book_id: int):
|
|||
|
||||
@bp.route("/<int:book_id>/delete_contributor", methods=["POST"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Book],
|
||||
)
|
||||
@login_required
|
||||
def delete_contributor(book_id: int):
|
||||
form = f.DeleteContributorForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_id = int(form.user_id.data)
|
||||
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()
|
||||
if not book_contributor:
|
||||
log(
|
||||
log.INFO,
|
||||
"BookContributor does not exists user: [%s], book: [%s]",
|
||||
form.user_id.data,
|
||||
user_id,
|
||||
book_id,
|
||||
)
|
||||
flash("Does not exists!", "success")
|
||||
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)
|
||||
db.session.delete(book_contributor)
|
||||
db.session.commit()
|
||||
|
@ -95,12 +140,17 @@ def delete_contributor(book_id: int):
|
|||
|
||||
@bp.route("/<int:book_id>/edit_contributor_role", methods=["POST"])
|
||||
@register_book_verify_route(bp.name)
|
||||
@require_permission(
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
access=[m.Permission.Access.U],
|
||||
entities=[m.Book],
|
||||
)
|
||||
@login_required
|
||||
def edit_contributor_role(book_id: int):
|
||||
form = f.EditContributorRoleForm()
|
||||
|
||||
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
|
||||
).first()
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
log(
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
from flask import redirect, url_for, Blueprint, flash
|
||||
from flask_login import current_user
|
||||
|
||||
from app import forms as f, models as m, db
|
||||
from app.logger import log
|
||||
|
||||
bp = Blueprint("permission", __name__, "/permission")
|
||||
|
||||
|
||||
@bp.route("/set", methods=["POST"])
|
||||
def set():
|
||||
form: f.EditPermissionForm = f.EditPermissionForm()
|
||||
|
||||
if form.validate_on_submit():
|
||||
book_id = form.book_id.data
|
||||
book: m.Book = db.session.get(m.Book, book_id)
|
||||
if not book or book.is_deleted or book.owner != current_user:
|
||||
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
|
||||
flash("You are not owner of this book!", "danger")
|
||||
return redirect(url_for("book.my_library"))
|
||||
|
||||
user_id = form.user_id.data
|
||||
contributor: m.BookContributor = m.BookContributor.query.filter_by(
|
||||
user_id=user_id, book_id=book_id
|
||||
).first()
|
||||
if not contributor:
|
||||
log(
|
||||
log.INFO,
|
||||
"User: [%s] is not contributor of book: [%s]",
|
||||
current_user,
|
||||
book,
|
||||
)
|
||||
flash("User are not contributor of this book!", "danger")
|
||||
return redirect(url_for("book.my_library"))
|
||||
|
||||
# TODO process data from checkbox tree
|
||||
# permissions = json.loads(form.permissions.data)
|
||||
|
||||
return {"status": "ok"}
|
|
@ -1,34 +0,0 @@
|
|||
"""user wallet_id
|
||||
|
||||
Revision ID: 067a10a531d7
|
||||
Revises: bbc4b55246ba
|
||||
Create Date: 2023-05-03 11:53:14.455999
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "067a10a531d7"
|
||||
down_revision = "bbc4b55246ba"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("wallet_id", sa.String(length=255), nullable=True)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.drop_column("wallet_id")
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,44 +0,0 @@
|
|||
"""book-tags
|
||||
|
||||
Revision ID: 0961578f302a
|
||||
Revises: 5df1fabbee7d
|
||||
Create Date: 2023-05-16 10:58:44.518470
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0961578f302a"
|
||||
down_revision = "a9df3da8cd00"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"book_tags",
|
||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["book_id"],
|
||||
["books.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["tag_id"],
|
||||
["tags.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("book_tags")
|
||||
# ### end Alembic commands ###
|
|
@ -1,32 +0,0 @@
|
|||
"""comment_edited
|
||||
|
||||
Revision ID: 1dfa1f2c208f
|
||||
Revises: 2ec60080de3b
|
||||
Create Date: 2023-05-09 17:22:23.028408
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1dfa1f2c208f"
|
||||
down_revision = "2ec60080de3b"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("edited", sa.Boolean(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
||||
batch_op.drop_column("edited")
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,42 +0,0 @@
|
|||
"""wallet_id length
|
||||
|
||||
Revision ID: 2ec60080de3b
|
||||
Revises: 4ce4bacc7c06
|
||||
Create Date: 2023-05-05 16:47:55.533205
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2ec60080de3b"
|
||||
down_revision = "4ce4bacc7c06"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"wallet_id",
|
||||
existing_type=sa.VARCHAR(length=256),
|
||||
type_=sa.String(length=64),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"wallet_id",
|
||||
existing_type=sa.String(length=64),
|
||||
type_=sa.VARCHAR(length=256),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,38 +0,0 @@
|
|||
"""user_is_activated
|
||||
|
||||
Revision ID: 377fc0b7e4bb
|
||||
Revises: a1345b416f81
|
||||
Create Date: 2023-05-04 11:14:58.810826
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "377fc0b7e4bb"
|
||||
down_revision = "a1345b416f81"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("is_activated", sa.Boolean(), nullable=True))
|
||||
batch_op.alter_column(
|
||||
"username", existing_type=sa.VARCHAR(length=60), nullable=True
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"username", existing_type=sa.VARCHAR(length=60), nullable=False
|
||||
)
|
||||
batch_op.drop_column("is_activated")
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,66 +0,0 @@
|
|||
"""created_at_on_inter
|
||||
|
||||
Revision ID: 4ce4bacc7c06
|
||||
Revises: 377fc0b7e4bb
|
||||
Create Date: 2023-05-05 16:31:52.963720
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "4ce4bacc7c06"
|
||||
down_revision = "377fc0b7e4bb"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"username",
|
||||
existing_type=sa.VARCHAR(length=60),
|
||||
type_=sa.String(length=64),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"password_hash",
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
type_=sa.String(length=256),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"wallet_id",
|
||||
existing_type=sa.VARCHAR(length=255),
|
||||
type_=sa.String(length=256),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"wallet_id",
|
||||
existing_type=sa.String(length=256),
|
||||
type_=sa.VARCHAR(length=255),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"password_hash",
|
||||
existing_type=sa.String(length=256),
|
||||
type_=sa.VARCHAR(length=255),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"username",
|
||||
existing_type=sa.String(length=64),
|
||||
type_=sa.VARCHAR(length=60),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,47 +0,0 @@
|
|||
"""approved fields
|
||||
|
||||
Revision ID: 5df1fabbee7d
|
||||
Revises: 1dfa1f2c208f
|
||||
Create Date: 2023-05-11 15:06:42.883725
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "5df1fabbee7d"
|
||||
down_revision = "1dfa1f2c208f"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("approved", sa.Boolean(), nullable=True))
|
||||
batch_op.drop_column("included_with_interpretation")
|
||||
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("approved", sa.Boolean(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.drop_column("approved")
|
||||
|
||||
with op.batch_alter_table("comments", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"included_with_interpretation",
|
||||
sa.BOOLEAN(),
|
||||
autoincrement=False,
|
||||
nullable=True,
|
||||
)
|
||||
)
|
||||
batch_op.drop_column("approved")
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,311 @@
|
|||
"""init
|
||||
|
||||
Revision ID: 79e8c7bff9c9
|
||||
Revises:
|
||||
Create Date: 2023-06-01 15:31:33.635236
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '79e8c7bff9c9'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('permissions',
|
||||
sa.Column('access', sa.Integer(), nullable=True),
|
||||
sa.Column('entity_type', sa.Enum('UNKNOWN', 'BOOK', 'COLLECTION', 'SECTION', 'INTERPRETATION', 'COMMENT', name='entity'), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('tags',
|
||||
sa.Column('name', sa.String(length=32), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('name')
|
||||
)
|
||||
op.create_table('users',
|
||||
sa.Column('username', sa.String(length=64), nullable=True),
|
||||
sa.Column('password_hash', sa.String(length=256), nullable=True),
|
||||
sa.Column('is_activated', sa.Boolean(), nullable=True),
|
||||
sa.Column('wallet_id', sa.String(length=64), nullable=True),
|
||||
sa.Column('avatar_img', sa.Text(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('username')
|
||||
)
|
||||
op.create_table('books',
|
||||
sa.Column('label', sa.String(length=256), nullable=False),
|
||||
sa.Column('about', sa.Text(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('access_groups',
|
||||
sa.Column('name', sa.String(length=32), nullable=False),
|
||||
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('book_contributors',
|
||||
sa.Column('role', sa.Enum('UNKNOWN', 'MODERATOR', 'EDITOR', name='roles'), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('book_tags',
|
||||
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('book_versions',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('semver', sa.String(length=16), nullable=False),
|
||||
sa.Column('exported', sa.Boolean(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('derivative_id', sa.Integer(), nullable=True),
|
||||
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||
sa.ForeignKeyConstraint(['derivative_id'], ['book_versions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('books_stars',
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('books_access_groups',
|
||||
sa.Column('book_id', sa.Integer(), nullable=True),
|
||||
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('collections',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('label', sa.String(length=256), nullable=False),
|
||||
sa.Column('about', sa.Text(), nullable=True),
|
||||
sa.Column('is_root', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_leaf', sa.Boolean(), nullable=True),
|
||||
sa.Column('version_id', sa.Integer(), nullable=True),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['parent_id'], ['collections.id'], ),
|
||||
sa.ForeignKeyConstraint(['version_id'], ['book_versions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('permissions_access_groups',
|
||||
sa.Column('permission_id', sa.Integer(), nullable=True),
|
||||
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('users_access_groups',
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('collections_access_groups',
|
||||
sa.Column('collection_id', sa.Integer(), nullable=True),
|
||||
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sections',
|
||||
sa.Column('label', sa.String(length=256), nullable=False),
|
||||
sa.Column('collection_id', sa.Integer(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('version_id', sa.Integer(), nullable=True),
|
||||
sa.Column('selected_interpretation_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['version_id'], ['book_versions.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('interpretations',
|
||||
sa.Column('text', sa.Text(), nullable=False),
|
||||
sa.Column('plain_text', sa.Text(), nullable=True),
|
||||
sa.Column('approved', sa.Boolean(), nullable=True),
|
||||
sa.Column('marked', sa.Boolean(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('section_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('section_tags',
|
||||
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||
sa.Column('section_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
|
||||
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sections_access_groups',
|
||||
sa.Column('section_id', sa.Integer(), nullable=True),
|
||||
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('comments',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('text', sa.Text(), nullable=False),
|
||||
sa.Column('approved', sa.Boolean(), nullable=True),
|
||||
sa.Column('marked', sa.Boolean(), nullable=True),
|
||||
sa.Column('edited', sa.Boolean(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||
sa.ForeignKeyConstraint(['parent_id'], ['comments.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('interpretation_tags',
|
||||
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('interpretation_votes',
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||
sa.Column('positive', sa.Boolean(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('interpretations_access_groups',
|
||||
sa.Column('interpretation_id', sa.Integer(), nullable=True),
|
||||
sa.Column('access_group_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
|
||||
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('comment_tags',
|
||||
sa.Column('tag_id', sa.Integer(), nullable=True),
|
||||
sa.Column('comment_id', sa.Integer(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
|
||||
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('comment_votes',
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('comment_id', sa.Integer(), nullable=True),
|
||||
sa.Column('positive', sa.Boolean(), nullable=True),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_deleted', sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('comment_votes')
|
||||
op.drop_table('comment_tags')
|
||||
op.drop_table('interpretations_access_groups')
|
||||
op.drop_table('interpretation_votes')
|
||||
op.drop_table('interpretation_tags')
|
||||
op.drop_table('comments')
|
||||
op.drop_table('sections_access_groups')
|
||||
op.drop_table('section_tags')
|
||||
op.drop_table('interpretations')
|
||||
op.drop_table('sections')
|
||||
op.drop_table('collections_access_groups')
|
||||
op.drop_table('users_access_groups')
|
||||
op.drop_table('permissions_access_groups')
|
||||
op.drop_table('collections')
|
||||
op.drop_table('books_access_groups')
|
||||
op.drop_table('books_stars')
|
||||
op.drop_table('book_versions')
|
||||
op.drop_table('book_tags')
|
||||
op.drop_table('book_contributors')
|
||||
op.drop_table('access_groups')
|
||||
op.drop_table('books')
|
||||
op.drop_table('users')
|
||||
op.drop_table('tags')
|
||||
op.drop_table('permissions')
|
||||
# ### end Alembic commands ###
|
|
@ -1,44 +0,0 @@
|
|||
"""section-tags
|
||||
|
||||
Revision ID: 7baa732e01c6
|
||||
Revises: 0961578f302a
|
||||
Create Date: 2023-05-17 18:34:29.178354
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "7baa732e01c6"
|
||||
down_revision = "0961578f302a"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"section_tags",
|
||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
||||
sa.Column("section_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["section_id"],
|
||||
["sections.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["tag_id"],
|
||||
["tags.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("section_tags")
|
||||
# ### end Alembic commands ###
|
|
@ -1,44 +0,0 @@
|
|||
"""remove fields from section and interpretation
|
||||
|
||||
Revision ID: 883298018384
|
||||
Revises: 5df1fabbee7d
|
||||
Create Date: 2023-05-18 15:49:20.145655
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "883298018384"
|
||||
down_revision = "5df1fabbee7d"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.drop_column("label")
|
||||
|
||||
with op.batch_alter_table("sections", schema=None) as batch_op:
|
||||
batch_op.drop_column("about")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("sections", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column("about", sa.TEXT(), autoincrement=False, nullable=True)
|
||||
)
|
||||
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"label", sa.VARCHAR(length=256), autoincrement=False, nullable=False
|
||||
)
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,32 +0,0 @@
|
|||
"""user avatar_img
|
||||
|
||||
Revision ID: a1345b416f81
|
||||
Revises: 067a10a531d7
|
||||
Create Date: 2023-05-04 09:11:09.406698
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a1345b416f81"
|
||||
down_revision = "067a10a531d7"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("avatar_img", sa.Text(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.drop_column("avatar_img")
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,32 +0,0 @@
|
|||
"""plain_text
|
||||
|
||||
Revision ID: a9df3da8cd00
|
||||
Revises: 883298018384
|
||||
Create Date: 2023-05-23 10:42:06.239982
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "a9df3da8cd00"
|
||||
down_revision = "883298018384"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("plain_text", sa.Text()))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("interpretations", schema=None) as batch_op:
|
||||
batch_op.drop_column("plain_text")
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,293 +0,0 @@
|
|||
"""init
|
||||
|
||||
Revision ID: bbc4b55246ba
|
||||
Revises:
|
||||
Create Date: 2023-04-28 10:13:52.011272
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "bbc4b55246ba"
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"tags",
|
||||
sa.Column("name", sa.String(length=32), nullable=False),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("name"),
|
||||
)
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("username", sa.String(length=60), nullable=False),
|
||||
sa.Column("password_hash", sa.String(length=255), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("username"),
|
||||
)
|
||||
op.create_table(
|
||||
"books",
|
||||
sa.Column("label", sa.String(length=256), nullable=False),
|
||||
sa.Column("about", sa.Text(), nullable=True),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"book_contributors",
|
||||
sa.Column(
|
||||
"role",
|
||||
sa.Enum("UNKNOWN", "MODERATOR", "EDITOR", name="roles"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["book_id"],
|
||||
["books.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"book_versions",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("semver", sa.String(length=16), nullable=False),
|
||||
sa.Column("exported", sa.Boolean(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("derivative_id", sa.Integer(), nullable=True),
|
||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["book_id"],
|
||||
["books.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["derivative_id"],
|
||||
["book_versions.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"books_stars",
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("book_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["book_id"],
|
||||
["books.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"collections",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("label", sa.String(length=256), nullable=False),
|
||||
sa.Column("about", sa.Text(), nullable=True),
|
||||
sa.Column("is_root", sa.Boolean(), nullable=True),
|
||||
sa.Column("is_leaf", sa.Boolean(), nullable=True),
|
||||
sa.Column("version_id", sa.Integer(), nullable=True),
|
||||
sa.Column("parent_id", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_id"],
|
||||
["collections.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["version_id"],
|
||||
["book_versions.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"sections",
|
||||
sa.Column("label", sa.String(length=256), nullable=False),
|
||||
sa.Column("about", sa.Text(), nullable=True),
|
||||
sa.Column("collection_id", sa.Integer(), nullable=True),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("version_id", sa.Integer(), nullable=True),
|
||||
sa.Column("selected_interpretation_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["collection_id"],
|
||||
["collections.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["version_id"],
|
||||
["book_versions.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"interpretations",
|
||||
sa.Column("label", sa.String(length=256), nullable=False),
|
||||
sa.Column("text", sa.Text(), nullable=False),
|
||||
sa.Column("marked", sa.Boolean(), nullable=True),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("section_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["section_id"],
|
||||
["sections.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"comments",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("text", sa.Text(), nullable=False),
|
||||
sa.Column("marked", sa.Boolean(), nullable=True),
|
||||
sa.Column("included_with_interpretation", sa.Boolean(), nullable=True),
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("parent_id", sa.Integer(), nullable=True),
|
||||
sa.Column("interpretation_id", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["interpretation_id"],
|
||||
["interpretations.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["parent_id"],
|
||||
["comments.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"interpretation_tags",
|
||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
||||
sa.Column("interpretation_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["interpretation_id"],
|
||||
["interpretations.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["tag_id"],
|
||||
["tags.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"interpretation_votes",
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("interpretation_id", sa.Integer(), nullable=True),
|
||||
sa.Column("positive", sa.Boolean(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["interpretation_id"],
|
||||
["interpretations.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"comment_tags",
|
||||
sa.Column("tag_id", sa.Integer(), nullable=True),
|
||||
sa.Column("comment_id", sa.Integer(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["comment_id"],
|
||||
["comments.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["tag_id"],
|
||||
["tags.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_table(
|
||||
"comment_votes",
|
||||
sa.Column("user_id", sa.Integer(), nullable=True),
|
||||
sa.Column("comment_id", sa.Integer(), nullable=True),
|
||||
sa.Column("positive", sa.Boolean(), nullable=True),
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("is_deleted", sa.Boolean(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
["comment_id"],
|
||||
["comments.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("comment_votes")
|
||||
op.drop_table("comment_tags")
|
||||
op.drop_table("interpretation_votes")
|
||||
op.drop_table("interpretation_tags")
|
||||
op.drop_table("comments")
|
||||
op.drop_table("interpretations")
|
||||
op.drop_table("sections")
|
||||
op.drop_table("collections")
|
||||
op.drop_table("books_stars")
|
||||
op.drop_table("book_versions")
|
||||
op.drop_table("book_contributors")
|
||||
op.drop_table("books")
|
||||
op.drop_table("users")
|
||||
op.drop_table("tags")
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,81 @@
|
|||
interface Permissions {
|
||||
[key: string]: number[];
|
||||
}
|
||||
|
||||
const updatePermissionsJSON = (
|
||||
permissionsJSON: Permissions,
|
||||
checkBoxTrees: Element,
|
||||
) => {
|
||||
const inputs: NodeListOf<HTMLInputElement> = checkBoxTrees.querySelectorAll(
|
||||
'input[type=checkbox]',
|
||||
);
|
||||
inputs.forEach(element => {
|
||||
const accessTo: string = `${element.getAttribute('data-access-to')}`;
|
||||
const accessToId: number = parseInt(
|
||||
element.getAttribute('data-access-to-id'),
|
||||
);
|
||||
const checked = element.checked;
|
||||
if (checked && !permissionsJSON[accessTo].includes(accessToId)) {
|
||||
permissionsJSON[accessTo].push(accessToId);
|
||||
} else if (!checked && permissionsJSON[accessTo].includes(accessToId)) {
|
||||
permissionsJSON[accessTo] = permissionsJSON[accessTo].filter(
|
||||
el => el != accessToId,
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const uncheckParentInputs = (checkbox: HTMLElement) => {
|
||||
const parentLiElement: HTMLElement =
|
||||
checkbox.parentElement.parentElement.parentElement.parentElement;
|
||||
|
||||
const parentInputElement: HTMLInputElement = parentLiElement.querySelector(
|
||||
'input[type=checkbox]',
|
||||
);
|
||||
|
||||
parentInputElement.checked = false;
|
||||
|
||||
if (parentInputElement.getAttribute('data-root') != 'true') {
|
||||
uncheckParentInputs(parentInputElement);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCheckboxClick = (checkbox: HTMLInputElement) => {
|
||||
const parentLiElement: HTMLElement = checkbox.parentElement.parentElement;
|
||||
|
||||
const checked = checkbox.checked;
|
||||
|
||||
if (!checked) {
|
||||
uncheckParentInputs(checkbox);
|
||||
}
|
||||
|
||||
const checkboxes = parentLiElement.querySelectorAll('input[type=checkbox]');
|
||||
|
||||
checkboxes.forEach((checkbox: HTMLInputElement) => {
|
||||
checkbox.checked = checked;
|
||||
});
|
||||
};
|
||||
|
||||
export const initCheckBoxTree = () => {
|
||||
const permissionsJSON: Permissions = {
|
||||
book: [],
|
||||
sub_collection: [],
|
||||
collection: [],
|
||||
section: [],
|
||||
};
|
||||
const permissionsJsonInput: HTMLInputElement =
|
||||
document.querySelector('#permissions_json');
|
||||
const checkBoxTrees = document.querySelectorAll('.checkbox-tree');
|
||||
|
||||
checkBoxTrees.forEach((checkBoxTree: Element) => {
|
||||
const checkboxes = checkBoxTree.querySelectorAll('input[type=checkbox]');
|
||||
|
||||
checkboxes.forEach((checkbox: HTMLInputElement) => {
|
||||
checkbox.addEventListener('click', () => {
|
||||
handleCheckboxClick(checkbox);
|
||||
updatePermissionsJSON(permissionsJSON, checkBoxTree);
|
||||
permissionsJsonInput.value = JSON.stringify(permissionsJSON);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -37,10 +37,12 @@ export function deleteInterpretation() {
|
|||
const interpretationId = btn.getAttribute('data-interpretation-id');
|
||||
interpretationIdInDeleteInterpretationModal.value = interpretationId;
|
||||
let newActionPath: string = '';
|
||||
|
||||
newActionPath = defaultActionPath.replace(
|
||||
'0/interpretation_delete',
|
||||
`${interpretationId}/interpretation_delete`,
|
||||
'0/delete_interpretation',
|
||||
`${interpretationId}/delete_interpretation`,
|
||||
);
|
||||
console.log(defaultActionPath);
|
||||
|
||||
deleteInterpretationForm.setAttribute('action', `${newActionPath}`);
|
||||
interpretationDeleteModal.show();
|
||||
|
|
|
@ -21,6 +21,8 @@ import {renameSubCollection} from './renameSubCollection';
|
|||
import {initQuillReadOnly} from './initQuillReadOnly';
|
||||
import {initGoBack} from './tabGoBackBtn';
|
||||
import {scroll} from './scroll';
|
||||
import {initCheckBoxTree} from './checkBoxTree';
|
||||
import {initPermissions} from './permissions';
|
||||
import {copyLink} from './copyLink';
|
||||
import {quickSearch} from './quickSearch';
|
||||
import {flash} from './flash';
|
||||
|
@ -51,6 +53,8 @@ deleteSubCollection();
|
|||
renameSubCollection();
|
||||
initGoBack();
|
||||
scroll();
|
||||
initCheckBoxTree();
|
||||
initPermissions();
|
||||
copyLink();
|
||||
quickSearch();
|
||||
flash();
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
export const initPermissions = () => {
|
||||
const editBtns = document.querySelectorAll('.edit-permissions-btn');
|
||||
|
||||
editBtns.forEach(element => {
|
||||
const bookIdInput: HTMLInputElement = document.querySelector(
|
||||
'#permission_modal_book_id',
|
||||
);
|
||||
const userIdInput: HTMLInputElement = document.querySelector(
|
||||
'#permission_modal_user_id',
|
||||
);
|
||||
element.addEventListener('click', () => {
|
||||
const book_id = element.getAttribute('data-book-id');
|
||||
const user_id = element.getAttribute('data-user-id');
|
||||
|
||||
bookIdInput.value = book_id;
|
||||
userIdInput.value = user_id;
|
||||
});
|
||||
});
|
||||
};
|
|
@ -21,8 +21,7 @@ def test_approve_interpretation(client: FlaskClient):
|
|||
)
|
||||
|
||||
assert response
|
||||
assert response.status_code == 404
|
||||
assert response.json["message"] == "Interpretation not found"
|
||||
assert b"You do not have permission" in response.data
|
||||
|
||||
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
||||
user_id=dummy_user.id
|
||||
|
@ -33,7 +32,7 @@ def test_approve_interpretation(client: FlaskClient):
|
|||
)
|
||||
|
||||
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(
|
||||
user_id=user.id
|
||||
|
@ -78,8 +77,7 @@ def test_approve_comment(client: FlaskClient):
|
|||
)
|
||||
|
||||
assert response
|
||||
assert response.status_code == 404
|
||||
assert response.json["message"] == "Comment not found"
|
||||
assert b"You do not have permission" in response.data
|
||||
|
||||
comment: m.Comment = m.Comment.query.filter_by(user_id=dummy_user.id).first()
|
||||
response: Response = client.post(
|
||||
|
@ -88,7 +86,7 @@ def test_approve_comment(client: FlaskClient):
|
|||
)
|
||||
|
||||
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()
|
||||
response: Response = client.post(
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# flake8: noqa F501
|
||||
from flask import current_app as Response, url_for
|
||||
from flask import current_app as Response
|
||||
from flask.testing import FlaskClient, FlaskCliRunner
|
||||
|
||||
from app import models as m, db
|
||||
from app.controllers.create_access_groups import create_moderator_group
|
||||
from tests.utils import (
|
||||
login,
|
||||
logout,
|
||||
|
@ -10,6 +10,7 @@ from tests.utils import (
|
|||
check_if_nested_collection_entities_is_deleted,
|
||||
check_if_nested_section_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 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 m.Book.query.count()
|
||||
|
@ -47,7 +48,7 @@ def test_create_edit_delete_book(client: FlaskClient):
|
|||
assert response.status_code == 200
|
||||
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 m.Book.query.count()
|
||||
|
@ -63,11 +64,18 @@ def test_create_edit_delete_book(client: FlaskClient):
|
|||
assert response.status_code == 200
|
||||
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.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
|
||||
|
||||
response: Response = client.post(
|
||||
"/book/999/edit",
|
||||
|
@ -79,7 +87,7 @@ def test_create_edit_delete_book(client: FlaskClient):
|
|||
)
|
||||
|
||||
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(
|
||||
f"/book/{book.id}/edit",
|
||||
|
@ -106,17 +114,17 @@ def test_create_edit_delete_book(client: FlaskClient):
|
|||
assert response.status_code == 200
|
||||
assert b"Success!" in response.data
|
||||
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)
|
||||
|
||||
|
||||
def test_add_contributor(client: FlaskClient):
|
||||
def test_add_delete_contributor(client: FlaskClient):
|
||||
_, user = login(client)
|
||||
user: m.User
|
||||
|
||||
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(
|
||||
f"/book/{moderators_book.id}/add_contributor",
|
||||
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 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(
|
||||
f"/book/{book.id}/add_contributor",
|
||||
|
@ -136,6 +145,12 @@ def test_add_contributor(client: FlaskClient):
|
|||
|
||||
assert response.status_code == 200
|
||||
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(
|
||||
f"/book/{book.id}/add_contributor",
|
||||
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
|
||||
).first()
|
||||
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()
|
||||
response: Response = client.post(
|
||||
|
@ -165,30 +180,20 @@ def test_add_contributor(client: FlaskClient):
|
|||
user=editor, book=book
|
||||
).first()
|
||||
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):
|
||||
_, 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]
|
||||
|
||||
assert moderator.access_groups
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/delete_contributor",
|
||||
data=dict(user_id=contributor_to_delete.user_id),
|
||||
follow_redirects=True,
|
||||
)
|
||||
moderator: m.User = db.session.get(m.User, moderator.id)
|
||||
assert not moderator.access_groups
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"Success!" in response.data
|
||||
|
@ -209,17 +214,19 @@ def test_delete_contributor(client: FlaskClient, runner: FlaskCliRunner):
|
|||
)
|
||||
|
||||
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):
|
||||
_, user = login(client)
|
||||
user: m.User
|
||||
|
||||
# add dummmy data
|
||||
runner.invoke(args=["db-populate"])
|
||||
book = create_test_book(user.id)
|
||||
|
||||
# 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.save()
|
||||
|
||||
|
@ -244,7 +251,7 @@ def test_edit_contributor_role(client: FlaskClient, runner: FlaskCliRunner):
|
|||
|
||||
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(
|
||||
f"/book/{moderators_book.id}/add_contributor",
|
||||
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 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(
|
||||
f"/book/999/add_contributor",
|
||||
"/book/999/add_contributor",
|
||||
data=dict(user_id=moderator.id, role=m.BookContributor.Roles.MODERATOR),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
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: m.User
|
||||
|
||||
# add dummmy data
|
||||
runner.invoke(args=["db-populate"])
|
||||
|
||||
book = db.session.get(m.Book, 1)
|
||||
book.user_id = user.id
|
||||
book.save()
|
||||
book = create_test_book(user.id)
|
||||
|
||||
response: Response = client.post(
|
||||
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
|
||||
|
||||
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"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
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(
|
||||
label="Test Collection #1 Label"
|
||||
).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(
|
||||
label="Test Collection #2 Label",
|
||||
version_id=collection.version_id,
|
||||
|
@ -342,7 +351,7 @@ def test_crud_collection(client: FlaskClient, runner: FlaskCliRunner):
|
|||
follow_redirects=True,
|
||||
)
|
||||
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(
|
||||
label=new_label, about=new_about
|
||||
|
@ -387,29 +396,26 @@ def test_crud_collection(client: FlaskClient, runner: FlaskCliRunner):
|
|||
)
|
||||
|
||||
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: m.User
|
||||
|
||||
# add dummy data
|
||||
runner.invoke(args=["db-populate"])
|
||||
book = create_test_book(user.id)
|
||||
|
||||
book: m.Book = db.session.get(m.Book, 1)
|
||||
book.user_id = user.id
|
||||
book.save()
|
||||
collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
is_leaf=False,
|
||||
parent_id=book.last_version.root_collection.id,
|
||||
).first()
|
||||
|
||||
leaf_collection: m.Collection = m.Collection(
|
||||
label="Test Leaf Collection #1 Label",
|
||||
leaf_collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
is_leaf=True,
|
||||
parent_id=book.last_version.root_collection.id,
|
||||
).save()
|
||||
collection: m.Collection = m.Collection(
|
||||
label="Test Collection #1 Label", version_id=book.last_version.id
|
||||
).save()
|
||||
parent_id=collection.id,
|
||||
).first()
|
||||
|
||||
response: Response = client.post(
|
||||
f"/book/999/{leaf_collection.id}/create_sub_collection",
|
||||
|
@ -419,7 +425,7 @@ def test_crud_subcollection(client: FlaskClient, runner: FlaskCliRunner):
|
|||
follow_redirects=True,
|
||||
)
|
||||
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(
|
||||
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 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(
|
||||
label="Test SubCollection #2 Label",
|
||||
version_id=collection.version_id,
|
||||
|
@ -535,31 +547,20 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
|||
_, user = login(client)
|
||||
user: m.User
|
||||
|
||||
# add dummmy data
|
||||
runner.invoke(args=["db-populate"])
|
||||
book = create_test_book(user.id)
|
||||
|
||||
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",
|
||||
collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
is_leaf=True,
|
||||
is_leaf=False,
|
||||
parent_id=book.last_version.root_collection.id,
|
||||
).save()
|
||||
collection: m.Collection = m.Collection(
|
||||
label="Test Collection #1 Label", version_id=book.last_version.id
|
||||
).save()
|
||||
sub_collection: m.Collection = m.Collection(
|
||||
label="Test SubCollection #1 Label",
|
||||
version_id=book.last_version.id,
|
||||
parent_id=collection.id,
|
||||
is_leaf=True,
|
||||
).save()
|
||||
).first()
|
||||
|
||||
sub_collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
is_leaf=True,
|
||||
parent_id=collection.id,
|
||||
).first()
|
||||
|
||||
leaf_collection.is_leaf = False
|
||||
leaf_collection.save()
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{collection.id}/create_section",
|
||||
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
|
||||
|
||||
leaf_collection.is_leaf = True
|
||||
leaf_collection.save()
|
||||
|
||||
label_1 = "Test Section #1 Label"
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{leaf_collection.id}/create_section",
|
||||
f"/book/{book.id}/{sub_collection.id}/create_section",
|
||||
data=dict(
|
||||
collection_id=leaf_collection.id,
|
||||
collection_id=sub_collection.id,
|
||||
label=label_1,
|
||||
about="Test Section #1 About",
|
||||
),
|
||||
|
@ -587,17 +585,23 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
|||
|
||||
assert response.status_code == 200
|
||||
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()
|
||||
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 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(
|
||||
f"/book/{book.id}/{leaf_collection.id}/create_section",
|
||||
f"/book/{book.id}/{sub_collection.id}/create_section",
|
||||
data=dict(
|
||||
collection_id=leaf_collection.id,
|
||||
collection_id=sub_collection.id,
|
||||
label=label_1,
|
||||
about="Test Section #1 About",
|
||||
),
|
||||
|
@ -663,7 +667,7 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
|||
|
||||
m.Section(
|
||||
label="Test",
|
||||
collection_id=leaf_collection.id,
|
||||
collection_id=sub_collection.id,
|
||||
version_id=book.last_version.id,
|
||||
).save()
|
||||
|
||||
|
@ -674,7 +678,7 @@ def test_crud_sections(client: FlaskClient, runner: FlaskCliRunner):
|
|||
).save()
|
||||
|
||||
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()
|
||||
|
||||
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(
|
||||
label=label_1, collection_id=sub_collection.id
|
||||
label="Test Section #1 Label(edited)", collection_id=sub_collection.id
|
||||
).first()
|
||||
response: Response = client.post(
|
||||
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
|
||||
|
||||
|
||||
def test_crud_interpretation(client: FlaskClient, runner: FlaskCliRunner):
|
||||
def test_crud_interpretation(client: FlaskClient):
|
||||
_, user = login(client)
|
||||
user: m.User
|
||||
book = create_test_book(user.id)
|
||||
|
||||
# add dummmy data
|
||||
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",
|
||||
collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
is_leaf=True,
|
||||
parent_id=book.last_version.root_collection.id,
|
||||
).save()
|
||||
section_in_collection: m.Section = m.Section(
|
||||
label="Test Section in Collection #1 Label",
|
||||
collection_id=leaf_collection.id,
|
||||
).first()
|
||||
section_in_collection: m.Section = m.Section.query.filter_by(
|
||||
collection_id=collection.id,
|
||||
version_id=book.last_version.id,
|
||||
).save()
|
||||
).first()
|
||||
|
||||
collection: m.Collection = m.Collection(
|
||||
label="Test Collection #1 Label", version_id=book.last_version.id
|
||||
).save()
|
||||
sub_collection: m.Collection = m.Collection(
|
||||
label="Test SubCollection #1 Label",
|
||||
collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
is_leaf=False,
|
||||
parent_id=book.last_version.root_collection.id,
|
||||
).first()
|
||||
sub_collection: m.Collection = m.Collection.query.filter_by(
|
||||
version_id=book.last_version.id,
|
||||
parent_id=collection.id,
|
||||
is_leaf=True,
|
||||
).save()
|
||||
section_in_subcollection: m.Section = m.Section(
|
||||
label="Test Section in Subcollection #1 Label",
|
||||
parent_id=collection.id,
|
||||
).first()
|
||||
section_in_subcollection: m.Section = m.Section.query.filter_by(
|
||||
collection_id=sub_collection.id,
|
||||
version_id=book.last_version.id,
|
||||
).save()
|
||||
).first()
|
||||
|
||||
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 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(
|
||||
f"/book/{book.id}/{section_in_collection.id}/create_interpretation",
|
||||
data=dict(section_id=section_in_collection.id, text=text_1),
|
||||
|
@ -873,15 +875,23 @@ def test_crud_interpretation(client: FlaskClient, runner: FlaskCliRunner):
|
|||
|
||||
# edit
|
||||
|
||||
m.Interpretation(
|
||||
i_1 = m.Interpretation(
|
||||
text="Test", section_id=section_in_collection.id, user_id=user.id
|
||||
).save()
|
||||
|
||||
m.Interpretation(
|
||||
i_2 = m.Interpretation(
|
||||
text="Test",
|
||||
section_id=section_in_subcollection.id,
|
||||
).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(
|
||||
section_id=section_in_collection.id
|
||||
).first()
|
||||
|
@ -967,7 +977,7 @@ def test_crud_comment(client: FlaskClient, runner: FlaskCliRunner):
|
|||
is_leaf=True,
|
||||
parent_id=book.last_version.root_collection.id,
|
||||
).save()
|
||||
section_in_collection: m.Section = m.Section(
|
||||
m.Section(
|
||||
label="Test Section in Collection #1 Label",
|
||||
collection_id=leaf_collection.id,
|
||||
version_id=book.last_version.id,
|
||||
|
@ -987,6 +997,10 @@ def test_crud_comment(client: FlaskClient, runner: FlaskCliRunner):
|
|||
collection_id=sub_collection.id,
|
||||
version_id=book.last_version.id,
|
||||
).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"
|
||||
text_1 = "Test Interpretation #1 Text"
|
||||
|
@ -1082,7 +1096,6 @@ def test_access_to_settings_page(client: FlaskClient):
|
|||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"You are not owner of this book!" not in response.data
|
||||
|
||||
logout(client)
|
||||
|
||||
|
@ -1092,7 +1105,7 @@ def test_access_to_settings_page(client: FlaskClient):
|
|||
)
|
||||
|
||||
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(
|
||||
|
@ -1134,6 +1147,13 @@ def test_interpretation_in_home_last_inter_section(
|
|||
collection_id=sub_collection.id,
|
||||
version_id=book.last_version.id,
|
||||
).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"
|
||||
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
|
||||
|
||||
response: Response = client.get(
|
||||
f"/home",
|
||||
"/home",
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
|
|
|
@ -104,11 +104,3 @@ def test_approved_comments(client: FlaskClient):
|
|||
interpretation.is_deleted = False
|
||||
interpretation.save()
|
||||
assert len(book.approved_comments) == 2
|
||||
|
||||
collection: m.Collection = m.Collection.query.first()
|
||||
collection.is_deleted = True
|
||||
collection.save()
|
||||
|
||||
interpretation.is_deleted = False
|
||||
interpretation.save()
|
||||
assert len(book.approved_comments) == 0
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
from app.controllers.create_access_groups import (
|
||||
create_moderator_group,
|
||||
create_editor_group,
|
||||
)
|
||||
from app import models as m
|
||||
|
||||
|
||||
def test_init_moderator_group(client):
|
||||
create_moderator_group(book_id=0)
|
||||
|
||||
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="moderator").first()
|
||||
assert group
|
||||
assert not group.users
|
||||
assert group.permissions
|
||||
|
||||
permissions = group.permissions
|
||||
|
||||
access = m.Permission.Access
|
||||
|
||||
interpretation_DA: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
|
||||
).first()
|
||||
assert interpretation_DA
|
||||
assert interpretation_DA in permissions
|
||||
|
||||
comment_DA: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
|
||||
).first()
|
||||
assert comment_DA
|
||||
assert comment_DA in permissions
|
||||
|
||||
create_moderator_group(book_id=0)
|
||||
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="moderator").all()
|
||||
assert len(groups) == 2
|
||||
|
||||
|
||||
def test_init_editor_group(client):
|
||||
create_editor_group(book_id=0)
|
||||
|
||||
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="editor").first()
|
||||
assert group
|
||||
assert not group.users
|
||||
assert group.permissions
|
||||
|
||||
permissions = group.permissions
|
||||
|
||||
access = m.Permission.Access
|
||||
|
||||
interpretation_DA: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
|
||||
).first()
|
||||
assert interpretation_DA
|
||||
assert interpretation_DA in permissions
|
||||
|
||||
comment_DA: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
|
||||
).first()
|
||||
assert comment_DA
|
||||
assert comment_DA in permissions
|
||||
|
||||
section_CUD: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.C | access.U | access.D,
|
||||
entity_type=m.Permission.Entity.SECTION,
|
||||
).first()
|
||||
assert section_CUD
|
||||
assert section_CUD in permissions
|
||||
|
||||
collection_CUD: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.C | access.U | access.D,
|
||||
entity_type=m.Permission.Entity.COLLECTION,
|
||||
).first()
|
||||
assert collection_CUD
|
||||
assert collection_CUD in permissions
|
||||
|
||||
book_U: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.U,
|
||||
entity_type=m.Permission.Entity.BOOK,
|
||||
).first()
|
||||
assert book_U
|
||||
assert book_U in permissions
|
||||
|
||||
create_editor_group(book_id=0)
|
||||
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="editor").all()
|
||||
assert len(groups) == 2
|
|
@ -0,0 +1,32 @@
|
|||
from app import models as m
|
||||
from app.controllers.get_or_create_permission import get_or_create_permission
|
||||
|
||||
|
||||
def test_get_or_create_permission(client):
|
||||
access = m.Permission.Access
|
||||
entity_type = m.Permission.Entity
|
||||
|
||||
book_u: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.U, entity_type=entity_type.BOOK
|
||||
).first()
|
||||
assert not book_u
|
||||
|
||||
assert not m.Permission.query.count()
|
||||
|
||||
book_u: m.Permission = get_or_create_permission(
|
||||
access=access.U, entity_type=entity_type.BOOK
|
||||
)
|
||||
assert book_u
|
||||
assert book_u.access == access.U
|
||||
assert book_u.entity_type == entity_type.BOOK
|
||||
assert m.Permission.query.count() == 1
|
||||
|
||||
book_u: m.Permission = m.Permission.query.filter_by(
|
||||
access=access.U, entity_type=entity_type.BOOK
|
||||
).first()
|
||||
assert book_u
|
||||
assert book_u.access == access.U
|
||||
assert book_u.entity_type == entity_type.BOOK
|
||||
|
||||
get_or_create_permission(access=access.U, entity_type=entity_type.BOOK)
|
||||
assert m.Permission.query.count() == 1
|
|
@ -0,0 +1,340 @@
|
|||
from random import randint
|
||||
|
||||
from flask import current_app as Response
|
||||
|
||||
from app import models as m
|
||||
from tests.utils import login, logout
|
||||
|
||||
|
||||
def create_book(client):
|
||||
random_id = randint(1, 100)
|
||||
BOOK_NAME = f"TBook {random_id}"
|
||||
response: Response = client.post(
|
||||
"/book/create",
|
||||
data=dict(label=BOOK_NAME),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"Book added!" in response.data
|
||||
|
||||
book: m.Book = m.Book.query.filter_by(label=BOOK_NAME).first()
|
||||
|
||||
assert book
|
||||
assert book.versions
|
||||
assert len(book.versions) == 1
|
||||
assert book.access_groups
|
||||
assert len(book.access_groups) == 2
|
||||
|
||||
root_collection: m.Collection = book.last_version.collections[0]
|
||||
assert root_collection
|
||||
assert root_collection.access_groups
|
||||
assert len(root_collection.access_groups) == 2
|
||||
|
||||
return book
|
||||
|
||||
|
||||
def create_collection(client, book_id):
|
||||
random_id = randint(1, 100)
|
||||
LABEL = f"TCollection {random_id}"
|
||||
response: Response = client.post(
|
||||
f"/book/{book_id}/create_collection",
|
||||
data=dict(label=LABEL),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
collection: m.Collection = m.Collection.query.filter_by(label=LABEL).first()
|
||||
|
||||
return collection, response
|
||||
|
||||
|
||||
def create_section(client, book_id, collection_id):
|
||||
random_id = randint(1, 100)
|
||||
LABEL = f"TSection {random_id}"
|
||||
response: Response = client.post(
|
||||
f"/book/{book_id}/{collection_id}/create_section",
|
||||
data=dict(collection_id=collection_id, label=LABEL),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
section: m.Section = m.Section.query.filter_by(
|
||||
label=LABEL, collection_id=collection_id
|
||||
).first()
|
||||
return section, response
|
||||
|
||||
|
||||
def create_interpretation(client, book_id, section_id):
|
||||
random_id = randint(1, 100)
|
||||
LABEL = f"TInterpretation {random_id}"
|
||||
response: Response = client.post(
|
||||
f"/book/{book_id}/{section_id}/create_interpretation",
|
||||
data=dict(section_id=section_id, text=LABEL),
|
||||
follow_redirects=True,
|
||||
)
|
||||
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
|
||||
section_id=section_id, text=LABEL
|
||||
).first()
|
||||
return interpretation, response
|
||||
|
||||
|
||||
def create_comment(client, book_id, interpretation_id):
|
||||
random_id = randint(1, 100)
|
||||
TEXT = f"TComment {random_id}"
|
||||
response: Response = client.post(
|
||||
f"/book/{book_id}/{interpretation_id}/create_comment",
|
||||
data=dict(
|
||||
text=TEXT,
|
||||
interpretation_id=interpretation_id,
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
comment: m.Comment = m.Comment.query.filter_by(text=TEXT).first()
|
||||
return comment, response
|
||||
|
||||
|
||||
def test_editor_access_to_entire_book(client):
|
||||
login(client)
|
||||
book = create_book(client)
|
||||
|
||||
editor = m.User(username="editor", password="editor").save()
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/add_contributor",
|
||||
data=dict(user_id=editor.id, role=m.BookContributor.Roles.EDITOR),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Contributor was added!" in response.data
|
||||
logout(client)
|
||||
|
||||
login(client, "editor", "editor")
|
||||
|
||||
# access to settings page
|
||||
response: Response = client.get(f"/book/{book.id}/settings", follow_redirects=True)
|
||||
assert b"You do not have permission" not in response.data
|
||||
|
||||
# access to edit book
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/edit",
|
||||
data=dict(book_id=book.id, label="BookEdited"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# dont have access to delete
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/delete",
|
||||
data=dict(book_id=book.id),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" in response.data
|
||||
|
||||
# access to create collection
|
||||
collection, response = create_collection(client, book.id)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to edit collection
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{collection.id}/edit",
|
||||
data=dict(label="NewLabel"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to delete collection
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{collection.id}/delete", follow_redirects=True
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# restore collection
|
||||
collection.is_deleted = False
|
||||
collection.save()
|
||||
|
||||
# access to create section
|
||||
section, response = create_section(client, book.id, collection.id)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to edit section
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{section.id}/edit_section",
|
||||
data=dict(section_id=section.id, label="NewLabel"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to delete section
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{section.id}/delete_section", follow_redirects=True
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# restore section
|
||||
section.is_deleted = False
|
||||
section.save()
|
||||
|
||||
# access to create interpretation
|
||||
interpretation, response = create_interpretation(client, book.id, section.id)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to approve interpretation
|
||||
response: Response = client.post(
|
||||
f"/approve/interpretation/{interpretation.id}",
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response
|
||||
assert response.json["message"] == "success"
|
||||
assert response.json["approve"]
|
||||
assert interpretation.approved
|
||||
|
||||
# access to delete interpretation
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{interpretation.id}/delete_interpretation",
|
||||
data=dict(interpretation_id=interpretation.id),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# restore interpretation
|
||||
interpretation.is_deleted = False
|
||||
interpretation.save()
|
||||
|
||||
# access to create comment
|
||||
comment, response = create_comment(client, book.id, interpretation.id)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to approve comment
|
||||
response: Response = client.post(
|
||||
f"/approve/comment/{comment.id}",
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response
|
||||
assert response.json["message"] == "success"
|
||||
assert response.json["approve"]
|
||||
assert interpretation.approved
|
||||
|
||||
# access to delete comment
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{interpretation.id}/comment_delete",
|
||||
data=dict(
|
||||
text=comment.text,
|
||||
interpretation_id=interpretation.id,
|
||||
comment_id=comment.id,
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
|
||||
def test_moderator_access_to_entire_book(client):
|
||||
login(client)
|
||||
book = create_book(client)
|
||||
|
||||
editor = m.User(username="moderator", password="moderator").save()
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/add_contributor",
|
||||
data=dict(user_id=editor.id, role=m.BookContributor.Roles.MODERATOR),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Contributor was added!" in response.data
|
||||
|
||||
logout(client)
|
||||
login(client, "moderator", "moderator")
|
||||
|
||||
# access to settings page
|
||||
response: Response = client.get(f"/book/{book.id}/settings", follow_redirects=True)
|
||||
assert b"You do not have permission" in response.data
|
||||
|
||||
# access to edit book
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/edit",
|
||||
data=dict(book_id=book.id, label="BookEdited"),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" in response.data
|
||||
|
||||
# dont have access to delete
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/delete",
|
||||
data=dict(book_id=book.id),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" in response.data
|
||||
|
||||
logout(client)
|
||||
login(client)
|
||||
collection, response = create_collection(client, book.id)
|
||||
section, response = create_section(client, book.id, collection.id)
|
||||
login(client, "moderator", "moderator")
|
||||
|
||||
# access to create interpretation
|
||||
interpretation, response = create_interpretation(client, book.id, section.id)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to approve interpretation
|
||||
response: Response = client.post(
|
||||
f"/approve/interpretation/{interpretation.id}",
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response
|
||||
assert response.json["message"] == "success"
|
||||
assert response.json["approve"]
|
||||
assert interpretation.approved
|
||||
|
||||
# access to delete interpretation
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{interpretation.id}/delete_interpretation",
|
||||
data=dict(interpretation_id=interpretation.id),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# restore interpretation
|
||||
interpretation.is_deleted = False
|
||||
interpretation.save()
|
||||
|
||||
# access to create comment
|
||||
comment, response = create_comment(client, book.id, interpretation.id)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
||||
|
||||
# access to approve comment
|
||||
response: Response = client.post(
|
||||
f"/approve/comment/{comment.id}",
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response
|
||||
assert response.json["message"] == "success"
|
||||
assert response.json["approve"]
|
||||
assert interpretation.approved
|
||||
|
||||
# access to delete comment
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{interpretation.id}/comment_delete",
|
||||
data=dict(
|
||||
text=comment.text,
|
||||
interpretation_id=interpretation.id,
|
||||
comment_id=comment.id,
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert b"You do not have permission" not in response.data
|
||||
assert b"Success!" in response.data
|
|
@ -1,4 +1,8 @@
|
|||
from app import models as m
|
||||
from app.controllers.create_access_groups import (
|
||||
create_editor_group,
|
||||
create_moderator_group,
|
||||
)
|
||||
|
||||
from random import randint
|
||||
|
||||
|
@ -26,15 +30,24 @@ def logout(client):
|
|||
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(
|
||||
label=f"Book {entity_id}", about=f"About {entity_id}", user_id=owner_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(
|
||||
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()
|
||||
|
||||
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,
|
||||
).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):
|
||||
for version in book.versions:
|
||||
|
|
Loading…
Reference in New Issue