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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,73 @@
from app import models as m
from app.logger import log
from .get_or_create_permission import get_or_create_permission
def create_moderator_group(book_id: int):
log(log.INFO, "Create moderator access group")
group: m.AccessGroup = m.AccessGroup(name="moderator", book_id=book_id).save()
permissions = []
comment_DA = get_or_create_permission(
access=m.Permission.Access.D | m.Permission.Access.A,
entity_type=m.Permission.Entity.COMMENT,
)
permissions.append(comment_DA)
interpretation_DA = get_or_create_permission(
access=m.Permission.Access.D | m.Permission.Access.A,
entity_type=m.Permission.Entity.INTERPRETATION,
)
permissions.append(interpretation_DA)
for permission in permissions:
log(log.INFO, "Add permission [%d] to group[%d]", permission.id, group.id)
m.PermissionAccessGroups(
permission_id=permission.id, access_group_id=group.id
).save()
return group
def create_editor_group(book_id: int):
log(log.INFO, "Create editor access group")
group: m.AccessGroup = m.AccessGroup(name="editor", book_id=book_id).save()
permissions = []
comment_DA = get_or_create_permission(
access=m.Permission.Access.D | m.Permission.Access.A,
entity_type=m.Permission.Entity.COMMENT,
)
permissions.append(comment_DA)
interpretation_DA = get_or_create_permission(
access=m.Permission.Access.D | m.Permission.Access.A,
entity_type=m.Permission.Entity.INTERPRETATION,
)
permissions.append(interpretation_DA)
section_CUD = get_or_create_permission(
access=m.Permission.Access.C | m.Permission.Access.U | m.Permission.Access.D,
entity_type=m.Permission.Entity.SECTION,
)
permissions.append(section_CUD)
collection_CUD = get_or_create_permission(
access=m.Permission.Access.C | m.Permission.Access.U | m.Permission.Access.D,
entity_type=m.Permission.Entity.COLLECTION,
)
permissions.append(collection_CUD)
book_U = get_or_create_permission(
access=m.Permission.Access.U,
entity_type=m.Permission.Entity.BOOK,
)
permissions.append(book_U)
for permission in permissions:
log(log.INFO, "Add permission [%d] to group[%d]", permission.id, group.id)
m.PermissionAccessGroups(
permission_id=permission.id, access_group_id=group.id
).save()
return group

View File

@ -0,0 +1,14 @@
from app import models as m
from app.logger import log
def get_or_create_permission(access: int, entity_type: m.Permission.Entity):
permission: m.Permission = m.Permission.query.filter_by(
access=access, entity_type=entity_type
).first()
if not permission:
log(log.INFO, "Create permission [%d] for entity [%s]", access, entity_type)
permission: m.Permission = m.Permission(
access=access, entity_type=entity_type
).save()
return permission

View File

@ -3,6 +3,7 @@ import re
from flask import current_app
from flask_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

View File

@ -0,0 +1,128 @@
from flask_login import current_user
from flask import flash, redirect, url_for, request, make_response
import functools
from app import models as m, db
from app.logger import log
def check_permissions(
entity_type: m.Permission.Entity,
access: list[m.Permission.Access],
entities: list[dict],
):
if not current_user.is_authenticated:
flash("You do not have permission", "danger")
return make_response(redirect(url_for("home.get_all")))
request_args = (
{**request.view_args, **request.args} if request.view_args else {**request.args}
)
entity = None
for model in entities:
entity_id_field = (model.__name__ + "_id").lower()
entity_id = request_args.get(entity_id_field)
entity: m.Book | m.Collection | m.Section | m.Interpretation | m.Comment = (
db.session.get(model, entity_id)
)
if entity is None:
log(log.INFO, "No entity [%s] found", entities)
flash("You do not have permission", "danger")
return make_response(redirect(url_for("home.get_all")))
book_id = request_args.get("book_id")
if book_id:
book: m.Book = db.session.get(m.Book, book_id)
if book and book.user_id == current_user.id:
# user has access because he is book owner
log(log.INFO, "User [%s] is book owner [%s]", current_user, book)
return None
if type(entity) == m.Comment:
log(log.INFO, "Entity is Comment. Replace it by entity.interpretation")
entity = entity.interpretation
if not entity or not entity.access_groups:
log(
log.INFO,
"Entity [%s] of entity.access_groups [%s] not found",
access,
entity,
)
flash("You do not have permission", "warning")
return make_response(redirect(url_for("home.get_all")))
# check if user is not owner of book
if not book_id and entity.access_groups[0].book.user_id == current_user.id:
# user has access because he is book owner
log(
log.INFO,
"User [%s] is book owner [%s]",
current_user,
entity.access_groups[0].book,
)
return None
access_group_query = (
m.AccessGroup.query.join(
m.PermissionAccessGroups,
m.PermissionAccessGroups.access_group_id == m.AccessGroup.id,
)
.join(m.Permission, m.PermissionAccessGroups.permission_id == m.Permission.id)
.filter(
m.AccessGroup.id.in_(
[access_group.id for access_group in entity.access_groups]
)
)
.filter(m.AccessGroup.users.any(id=current_user.id))
.filter(m.Permission.entity_type == entity_type)
)
for access in access:
access_group_query = access_group_query.filter(
m.Permission.access.op("&")(access) > 0
)
access_groups = access_group_query.all()
if access_groups:
log(
log.INFO,
"User [%s] has permission to [%s] [%s]",
access,
current_user,
entity,
)
return
log(
log.INFO,
"User [%s] dont have permission to [%s] [%s]",
current_user,
access,
entity,
)
flash("You do not have permission", "danger")
return make_response(redirect(url_for("home.get_all")))
def require_permission(
entity_type: m.Permission.Entity,
access: list[m.Permission.Access],
entities: list[dict],
):
def decorator(f):
@functools.wraps(f)
def permission_checker(*args, **kwargs):
if response := check_permissions(
entity_type=entity_type,
access=access,
entities=entities,
):
return response
return f(*args, **kwargs)
return permission_checker
return decorator

View File

@ -17,3 +17,4 @@ from .interpretation import (
from .comment import CreateCommentForm
from .vote import VoteForm
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm
from .permission import EditPermissionForm

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

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

View File

@ -13,5 +13,15 @@ from .interpretation_vote import InterpretationVote
from .tag import Tag
from .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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,9 @@
# flake8: noqa F401
from .access_group import AccessGroup
from .permission import Permission
from .user_access_groups import UserAccessGroups
from .permission_access_groups import PermissionAccessGroups
from .book_access_groups import BookAccessGroups
from .collection_access_groups import CollectionAccessGroups
from .section_access_groups import SectionAccessGroups
from .interpretation_access_groups import InterpretationAccessGroups

View File

@ -0,0 +1,25 @@
from app import db
from app.models.utils import BaseModel
class AccessGroup(BaseModel):
__tablename__ = "access_groups"
name = db.Column(db.String(32), nullable=False)
# Foreign Keys
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
# Relationships
book = db.relationship("Book", viewonly=True)
permissions = db.relationship(
"Permission",
secondary="permissions_access_groups",
back_populates="access_groups",
)
users = db.relationship(
"User", secondary="users_access_groups", back_populates="access_groups"
)
def __repr__(self):
return f"<{self.id}: {self.name} | Book: {self.book_id}>"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class BookAccessGroups(BaseModel):
__tablename__ = "books_access_groups"
# Foreign keys
book_id = db.Column(db.Integer, db.ForeignKey("books.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<b:{self.book_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class CollectionAccessGroups(BaseModel):
__tablename__ = "collections_access_groups"
# Foreign keys
collection_id = db.Column(db.Integer, db.ForeignKey("collections.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<c:{self.collection_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class InterpretationAccessGroups(BaseModel):
__tablename__ = "interpretations_access_groups"
# Foreign keys
interpretation_id = db.Column(db.Integer, db.ForeignKey("interpretations.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<c:{self.interpretation_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,33 @@
from enum import IntEnum
from app import db
from app.models.utils import BaseModel
class Permission(BaseModel):
__tablename__ = "permissions"
class Access(IntEnum):
C = 1 # 0b0001 - Create
U = 2 # 0b0010 - Update
D = 4 # 0b0100 - Delete
A = 8 # 0b1000 - Approve
# sum = 0b1111
class Entity(IntEnum):
UNKNOWN = 0
BOOK = 1
COLLECTION = 2
SECTION = 3
INTERPRETATION = 4
COMMENT = 5
access = db.Column(db.Integer(), default=Access.C | Access.U | Access.D | Access.A)
entity_type = db.Column(db.Enum(Entity), default=Entity.UNKNOWN)
# Relationships
access_groups = db.relationship(
"AccessGroup",
secondary="permissions_access_groups",
back_populates="permissions",
)

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class PermissionAccessGroups(BaseModel):
__tablename__ = "permissions_access_groups"
# Foreign keys
permission_id = db.Column(db.Integer, db.ForeignKey("permissions.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<p:{self.permission_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class SectionAccessGroups(BaseModel):
__tablename__ = "sections_access_groups"
# Foreign keys
section_id = db.Column(db.Integer, db.ForeignKey("sections.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<s:{self.section_id} to a_g:{self.access_group_id}"

View File

@ -0,0 +1,13 @@
from app import db
from app.models.utils import BaseModel
class UserAccessGroups(BaseModel):
__tablename__ = "users_access_groups"
# Foreign keys
user_id = db.Column(db.Integer, db.ForeignKey("users.id"))
access_group_id = db.Column(db.Integer, db.ForeignKey("access_groups.id"))
def __repr__(self):
return f"<u:{self.user_id} to a_g:{self.access_group_id}"

View File

@ -26,6 +26,10 @@ class Section(BaseModel):
interpretations = db.relationship(
"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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,25 +72,20 @@
</div>
<!-- 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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,39 @@
from flask import redirect, url_for, Blueprint, flash
from flask_login import current_user
from app import forms as f, models as m, db
from app.logger import log
bp = Blueprint("permission", __name__, "/permission")
@bp.route("/set", methods=["POST"])
def set():
form: f.EditPermissionForm = f.EditPermissionForm()
if form.validate_on_submit():
book_id = form.book_id.data
book: m.Book = db.session.get(m.Book, book_id)
if not book or book.is_deleted or book.owner != current_user:
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
flash("You are not owner of this book!", "danger")
return redirect(url_for("book.my_library"))
user_id = form.user_id.data
contributor: m.BookContributor = m.BookContributor.query.filter_by(
user_id=user_id, book_id=book_id
).first()
if not contributor:
log(
log.INFO,
"User: [%s] is not contributor of book: [%s]",
current_user,
book,
)
flash("User are not contributor of this book!", "danger")
return redirect(url_for("book.my_library"))
# TODO process data from checkbox tree
# permissions = json.loads(form.permissions.data)
return {"status": "ok"}

View File

@ -1,34 +0,0 @@
"""user wallet_id
Revision ID: 067a10a531d7
Revises: bbc4b55246ba
Create Date: 2023-05-03 11:53:14.455999
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "067a10a531d7"
down_revision = "bbc4b55246ba"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.add_column(
sa.Column("wallet_id", sa.String(length=255), nullable=True)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_column("wallet_id")
# ### end Alembic commands ###

View File

@ -1,44 +0,0 @@
"""book-tags
Revision ID: 0961578f302a
Revises: 5df1fabbee7d
Create Date: 2023-05-16 10:58:44.518470
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "0961578f302a"
down_revision = "a9df3da8cd00"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"book_tags",
sa.Column("tag_id", sa.Integer(), nullable=True),
sa.Column("book_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["book_id"],
["books.id"],
),
sa.ForeignKeyConstraint(
["tag_id"],
["tags.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("book_tags")
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""comment_edited
Revision ID: 1dfa1f2c208f
Revises: 2ec60080de3b
Create Date: 2023-05-09 17:22:23.028408
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "1dfa1f2c208f"
down_revision = "2ec60080de3b"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("comments", schema=None) as batch_op:
batch_op.add_column(sa.Column("edited", sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("comments", schema=None) as batch_op:
batch_op.drop_column("edited")
# ### end Alembic commands ###

View File

@ -1,42 +0,0 @@
"""wallet_id length
Revision ID: 2ec60080de3b
Revises: 4ce4bacc7c06
Create Date: 2023-05-05 16:47:55.533205
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "2ec60080de3b"
down_revision = "4ce4bacc7c06"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"wallet_id",
existing_type=sa.VARCHAR(length=256),
type_=sa.String(length=64),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"wallet_id",
existing_type=sa.String(length=64),
type_=sa.VARCHAR(length=256),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@ -1,38 +0,0 @@
"""user_is_activated
Revision ID: 377fc0b7e4bb
Revises: a1345b416f81
Create Date: 2023-05-04 11:14:58.810826
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "377fc0b7e4bb"
down_revision = "a1345b416f81"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.add_column(sa.Column("is_activated", sa.Boolean(), nullable=True))
batch_op.alter_column(
"username", existing_type=sa.VARCHAR(length=60), nullable=True
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"username", existing_type=sa.VARCHAR(length=60), nullable=False
)
batch_op.drop_column("is_activated")
# ### end Alembic commands ###

View File

@ -1,66 +0,0 @@
"""created_at_on_inter
Revision ID: 4ce4bacc7c06
Revises: 377fc0b7e4bb
Create Date: 2023-05-05 16:31:52.963720
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "4ce4bacc7c06"
down_revision = "377fc0b7e4bb"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"username",
existing_type=sa.VARCHAR(length=60),
type_=sa.String(length=64),
existing_nullable=True,
)
batch_op.alter_column(
"password_hash",
existing_type=sa.VARCHAR(length=255),
type_=sa.String(length=256),
existing_nullable=True,
)
batch_op.alter_column(
"wallet_id",
existing_type=sa.VARCHAR(length=255),
type_=sa.String(length=256),
existing_nullable=True,
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.alter_column(
"wallet_id",
existing_type=sa.String(length=256),
type_=sa.VARCHAR(length=255),
existing_nullable=True,
)
batch_op.alter_column(
"password_hash",
existing_type=sa.String(length=256),
type_=sa.VARCHAR(length=255),
existing_nullable=True,
)
batch_op.alter_column(
"username",
existing_type=sa.String(length=64),
type_=sa.VARCHAR(length=60),
existing_nullable=True,
)
# ### end Alembic commands ###

View File

@ -1,47 +0,0 @@
"""approved fields
Revision ID: 5df1fabbee7d
Revises: 1dfa1f2c208f
Create Date: 2023-05-11 15:06:42.883725
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "5df1fabbee7d"
down_revision = "1dfa1f2c208f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("comments", schema=None) as batch_op:
batch_op.add_column(sa.Column("approved", sa.Boolean(), nullable=True))
batch_op.drop_column("included_with_interpretation")
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.add_column(sa.Column("approved", sa.Boolean(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.drop_column("approved")
with op.batch_alter_table("comments", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"included_with_interpretation",
sa.BOOLEAN(),
autoincrement=False,
nullable=True,
)
)
batch_op.drop_column("approved")
# ### end Alembic commands ###

View File

@ -0,0 +1,311 @@
"""init
Revision ID: 79e8c7bff9c9
Revises:
Create Date: 2023-06-01 15:31:33.635236
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '79e8c7bff9c9'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('permissions',
sa.Column('access', sa.Integer(), nullable=True),
sa.Column('entity_type', sa.Enum('UNKNOWN', 'BOOK', 'COLLECTION', 'SECTION', 'INTERPRETATION', 'COMMENT', name='entity'), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tags',
sa.Column('name', sa.String(length=32), nullable=False),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('users',
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('password_hash', sa.String(length=256), nullable=True),
sa.Column('is_activated', sa.Boolean(), nullable=True),
sa.Column('wallet_id', sa.String(length=64), nullable=True),
sa.Column('avatar_img', sa.Text(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('books',
sa.Column('label', sa.String(length=256), nullable=False),
sa.Column('about', sa.Text(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('access_groups',
sa.Column('name', sa.String(length=32), nullable=False),
sa.Column('book_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('book_contributors',
sa.Column('role', sa.Enum('UNKNOWN', 'MODERATOR', 'EDITOR', name='roles'), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('book_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('book_tags',
sa.Column('tag_id', sa.Integer(), nullable=True),
sa.Column('book_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('book_versions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('semver', sa.String(length=16), nullable=False),
sa.Column('exported', sa.Boolean(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('derivative_id', sa.Integer(), nullable=True),
sa.Column('book_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
sa.ForeignKeyConstraint(['derivative_id'], ['book_versions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('books_stars',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('book_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('books_access_groups',
sa.Column('book_id', sa.Integer(), nullable=True),
sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
sa.ForeignKeyConstraint(['book_id'], ['books.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('collections',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('label', sa.String(length=256), nullable=False),
sa.Column('about', sa.Text(), nullable=True),
sa.Column('is_root', sa.Boolean(), nullable=True),
sa.Column('is_leaf', sa.Boolean(), nullable=True),
sa.Column('version_id', sa.Integer(), nullable=True),
sa.Column('parent_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['parent_id'], ['collections.id'], ),
sa.ForeignKeyConstraint(['version_id'], ['book_versions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('permissions_access_groups',
sa.Column('permission_id', sa.Integer(), nullable=True),
sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('users_access_groups',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('collections_access_groups',
sa.Column('collection_id', sa.Integer(), nullable=True),
sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('sections',
sa.Column('label', sa.String(length=256), nullable=False),
sa.Column('collection_id', sa.Integer(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('version_id', sa.Integer(), nullable=True),
sa.Column('selected_interpretation_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['collection_id'], ['collections.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['version_id'], ['book_versions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('interpretations',
sa.Column('text', sa.Text(), nullable=False),
sa.Column('plain_text', sa.Text(), nullable=True),
sa.Column('approved', sa.Boolean(), nullable=True),
sa.Column('marked', sa.Boolean(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('section_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('section_tags',
sa.Column('tag_id', sa.Integer(), nullable=True),
sa.Column('section_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('sections_access_groups',
sa.Column('section_id', sa.Integer(), nullable=True),
sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
sa.ForeignKeyConstraint(['section_id'], ['sections.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('comments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('text', sa.Text(), nullable=False),
sa.Column('approved', sa.Boolean(), nullable=True),
sa.Column('marked', sa.Boolean(), nullable=True),
sa.Column('edited', sa.Boolean(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('parent_id', sa.Integer(), nullable=True),
sa.Column('interpretation_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
sa.ForeignKeyConstraint(['parent_id'], ['comments.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('interpretation_tags',
sa.Column('tag_id', sa.Integer(), nullable=True),
sa.Column('interpretation_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('interpretation_votes',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('interpretation_id', sa.Integer(), nullable=True),
sa.Column('positive', sa.Boolean(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('interpretations_access_groups',
sa.Column('interpretation_id', sa.Integer(), nullable=True),
sa.Column('access_group_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['access_group_id'], ['access_groups.id'], ),
sa.ForeignKeyConstraint(['interpretation_id'], ['interpretations.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('comment_tags',
sa.Column('tag_id', sa.Integer(), nullable=True),
sa.Column('comment_id', sa.Integer(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('comment_votes',
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('comment_id', sa.Integer(), nullable=True),
sa.Column('positive', sa.Boolean(), nullable=True),
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('is_deleted', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['comment_id'], ['comments.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('comment_votes')
op.drop_table('comment_tags')
op.drop_table('interpretations_access_groups')
op.drop_table('interpretation_votes')
op.drop_table('interpretation_tags')
op.drop_table('comments')
op.drop_table('sections_access_groups')
op.drop_table('section_tags')
op.drop_table('interpretations')
op.drop_table('sections')
op.drop_table('collections_access_groups')
op.drop_table('users_access_groups')
op.drop_table('permissions_access_groups')
op.drop_table('collections')
op.drop_table('books_access_groups')
op.drop_table('books_stars')
op.drop_table('book_versions')
op.drop_table('book_tags')
op.drop_table('book_contributors')
op.drop_table('access_groups')
op.drop_table('books')
op.drop_table('users')
op.drop_table('tags')
op.drop_table('permissions')
# ### end Alembic commands ###

View File

@ -1,44 +0,0 @@
"""section-tags
Revision ID: 7baa732e01c6
Revises: 0961578f302a
Create Date: 2023-05-17 18:34:29.178354
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "7baa732e01c6"
down_revision = "0961578f302a"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"section_tags",
sa.Column("tag_id", sa.Integer(), nullable=True),
sa.Column("section_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["section_id"],
["sections.id"],
),
sa.ForeignKeyConstraint(
["tag_id"],
["tags.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("section_tags")
# ### end Alembic commands ###

View File

@ -1,44 +0,0 @@
"""remove fields from section and interpretation
Revision ID: 883298018384
Revises: 5df1fabbee7d
Create Date: 2023-05-18 15:49:20.145655
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "883298018384"
down_revision = "5df1fabbee7d"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.drop_column("label")
with op.batch_alter_table("sections", schema=None) as batch_op:
batch_op.drop_column("about")
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("sections", schema=None) as batch_op:
batch_op.add_column(
sa.Column("about", sa.TEXT(), autoincrement=False, nullable=True)
)
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"label", sa.VARCHAR(length=256), autoincrement=False, nullable=False
)
)
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""user avatar_img
Revision ID: a1345b416f81
Revises: 067a10a531d7
Create Date: 2023-05-04 09:11:09.406698
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a1345b416f81"
down_revision = "067a10a531d7"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.add_column(sa.Column("avatar_img", sa.Text(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("users", schema=None) as batch_op:
batch_op.drop_column("avatar_img")
# ### end Alembic commands ###

View File

@ -1,32 +0,0 @@
"""plain_text
Revision ID: a9df3da8cd00
Revises: 883298018384
Create Date: 2023-05-23 10:42:06.239982
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "a9df3da8cd00"
down_revision = "883298018384"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.add_column(sa.Column("plain_text", sa.Text()))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.drop_column("plain_text")
# ### end Alembic commands ###

View File

@ -1,293 +0,0 @@
"""init
Revision ID: bbc4b55246ba
Revises:
Create Date: 2023-04-28 10:13:52.011272
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "bbc4b55246ba"
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"tags",
sa.Column("name", sa.String(length=32), nullable=False),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("name"),
)
op.create_table(
"users",
sa.Column("username", sa.String(length=60), nullable=False),
sa.Column("password_hash", sa.String(length=255), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("username"),
)
op.create_table(
"books",
sa.Column("label", sa.String(length=256), nullable=False),
sa.Column("about", sa.Text(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"book_contributors",
sa.Column(
"role",
sa.Enum("UNKNOWN", "MODERATOR", "EDITOR", name="roles"),
nullable=True,
),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("book_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["book_id"],
["books.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"book_versions",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("semver", sa.String(length=16), nullable=False),
sa.Column("exported", sa.Boolean(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("derivative_id", sa.Integer(), nullable=True),
sa.Column("book_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["book_id"],
["books.id"],
),
sa.ForeignKeyConstraint(
["derivative_id"],
["book_versions.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"books_stars",
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("book_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["book_id"],
["books.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"collections",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("label", sa.String(length=256), nullable=False),
sa.Column("about", sa.Text(), nullable=True),
sa.Column("is_root", sa.Boolean(), nullable=True),
sa.Column("is_leaf", sa.Boolean(), nullable=True),
sa.Column("version_id", sa.Integer(), nullable=True),
sa.Column("parent_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["parent_id"],
["collections.id"],
),
sa.ForeignKeyConstraint(
["version_id"],
["book_versions.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"sections",
sa.Column("label", sa.String(length=256), nullable=False),
sa.Column("about", sa.Text(), nullable=True),
sa.Column("collection_id", sa.Integer(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("version_id", sa.Integer(), nullable=True),
sa.Column("selected_interpretation_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["collection_id"],
["collections.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.ForeignKeyConstraint(
["version_id"],
["book_versions.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"interpretations",
sa.Column("label", sa.String(length=256), nullable=False),
sa.Column("text", sa.Text(), nullable=False),
sa.Column("marked", sa.Boolean(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("section_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["section_id"],
["sections.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"comments",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("text", sa.Text(), nullable=False),
sa.Column("marked", sa.Boolean(), nullable=True),
sa.Column("included_with_interpretation", sa.Boolean(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("parent_id", sa.Integer(), nullable=True),
sa.Column("interpretation_id", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["interpretation_id"],
["interpretations.id"],
),
sa.ForeignKeyConstraint(
["parent_id"],
["comments.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"interpretation_tags",
sa.Column("tag_id", sa.Integer(), nullable=True),
sa.Column("interpretation_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["interpretation_id"],
["interpretations.id"],
),
sa.ForeignKeyConstraint(
["tag_id"],
["tags.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"interpretation_votes",
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("interpretation_id", sa.Integer(), nullable=True),
sa.Column("positive", sa.Boolean(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["interpretation_id"],
["interpretations.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"comment_tags",
sa.Column("tag_id", sa.Integer(), nullable=True),
sa.Column("comment_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["comment_id"],
["comments.id"],
),
sa.ForeignKeyConstraint(
["tag_id"],
["tags.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"comment_votes",
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("comment_id", sa.Integer(), nullable=True),
sa.Column("positive", sa.Boolean(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["comment_id"],
["comments.id"],
),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("comment_votes")
op.drop_table("comment_tags")
op.drop_table("interpretation_votes")
op.drop_table("interpretation_tags")
op.drop_table("comments")
op.drop_table("interpretations")
op.drop_table("sections")
op.drop_table("collections")
op.drop_table("books_stars")
op.drop_table("book_versions")
op.drop_table("book_contributors")
op.drop_table("books")
op.drop_table("users")
op.drop_table("tags")
# ### end Alembic commands ###

81
src/checkBoxTree.ts Normal file
View File

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

View File

@ -37,10 +37,12 @@ export function deleteInterpretation() {
const interpretationId = btn.getAttribute('data-interpretation-id');
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();

View File

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

19
src/permissions.ts Normal file
View File

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

View File

@ -21,8 +21,7 @@ def test_approve_interpretation(client: FlaskClient):
)
assert response
assert response.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(

View File

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

View File

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

View File

@ -0,0 +1,84 @@
from app.controllers.create_access_groups import (
create_moderator_group,
create_editor_group,
)
from app import models as m
def test_init_moderator_group(client):
create_moderator_group(book_id=0)
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="moderator").first()
assert group
assert not group.users
assert group.permissions
permissions = group.permissions
access = m.Permission.Access
interpretation_DA: m.Permission = m.Permission.query.filter_by(
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
).first()
assert interpretation_DA
assert interpretation_DA in permissions
comment_DA: m.Permission = m.Permission.query.filter_by(
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
).first()
assert comment_DA
assert comment_DA in permissions
create_moderator_group(book_id=0)
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="moderator").all()
assert len(groups) == 2
def test_init_editor_group(client):
create_editor_group(book_id=0)
group: m.AccessGroup = m.AccessGroup.query.filter_by(name="editor").first()
assert group
assert not group.users
assert group.permissions
permissions = group.permissions
access = m.Permission.Access
interpretation_DA: m.Permission = m.Permission.query.filter_by(
access=access.D | access.A, entity_type=m.Permission.Entity.INTERPRETATION
).first()
assert interpretation_DA
assert interpretation_DA in permissions
comment_DA: m.Permission = m.Permission.query.filter_by(
access=access.D | access.A, entity_type=m.Permission.Entity.COMMENT
).first()
assert comment_DA
assert comment_DA in permissions
section_CUD: m.Permission = m.Permission.query.filter_by(
access=access.C | access.U | access.D,
entity_type=m.Permission.Entity.SECTION,
).first()
assert section_CUD
assert section_CUD in permissions
collection_CUD: m.Permission = m.Permission.query.filter_by(
access=access.C | access.U | access.D,
entity_type=m.Permission.Entity.COLLECTION,
).first()
assert collection_CUD
assert collection_CUD in permissions
book_U: m.Permission = m.Permission.query.filter_by(
access=access.U,
entity_type=m.Permission.Entity.BOOK,
).first()
assert book_U
assert book_U in permissions
create_editor_group(book_id=0)
groups: list[m.AccessGroup] = m.AccessGroup.query.filter_by(name="editor").all()
assert len(groups) == 2

View File

@ -0,0 +1,32 @@
from app import models as m
from app.controllers.get_or_create_permission import get_or_create_permission
def test_get_or_create_permission(client):
access = m.Permission.Access
entity_type = m.Permission.Entity
book_u: m.Permission = m.Permission.query.filter_by(
access=access.U, entity_type=entity_type.BOOK
).first()
assert not book_u
assert not m.Permission.query.count()
book_u: m.Permission = get_or_create_permission(
access=access.U, entity_type=entity_type.BOOK
)
assert book_u
assert book_u.access == access.U
assert book_u.entity_type == entity_type.BOOK
assert m.Permission.query.count() == 1
book_u: m.Permission = m.Permission.query.filter_by(
access=access.U, entity_type=entity_type.BOOK
).first()
assert book_u
assert book_u.access == access.U
assert book_u.entity_type == entity_type.BOOK
get_or_create_permission(access=access.U, entity_type=entity_type.BOOK)
assert m.Permission.query.count() == 1

340
tests/test_permissions.py Normal file
View File

@ -0,0 +1,340 @@
from random import randint
from flask import current_app as Response
from app import models as m
from tests.utils import login, logout
def create_book(client):
random_id = randint(1, 100)
BOOK_NAME = f"TBook {random_id}"
response: Response = client.post(
"/book/create",
data=dict(label=BOOK_NAME),
follow_redirects=True,
)
assert response.status_code == 200
assert b"Book added!" in response.data
book: m.Book = m.Book.query.filter_by(label=BOOK_NAME).first()
assert book
assert book.versions
assert len(book.versions) == 1
assert book.access_groups
assert len(book.access_groups) == 2
root_collection: m.Collection = book.last_version.collections[0]
assert root_collection
assert root_collection.access_groups
assert len(root_collection.access_groups) == 2
return book
def create_collection(client, book_id):
random_id = randint(1, 100)
LABEL = f"TCollection {random_id}"
response: Response = client.post(
f"/book/{book_id}/create_collection",
data=dict(label=LABEL),
follow_redirects=True,
)
assert response.status_code == 200
collection: m.Collection = m.Collection.query.filter_by(label=LABEL).first()
return collection, response
def create_section(client, book_id, collection_id):
random_id = randint(1, 100)
LABEL = f"TSection {random_id}"
response: Response = client.post(
f"/book/{book_id}/{collection_id}/create_section",
data=dict(collection_id=collection_id, label=LABEL),
follow_redirects=True,
)
section: m.Section = m.Section.query.filter_by(
label=LABEL, collection_id=collection_id
).first()
return section, response
def create_interpretation(client, book_id, section_id):
random_id = randint(1, 100)
LABEL = f"TInterpretation {random_id}"
response: Response = client.post(
f"/book/{book_id}/{section_id}/create_interpretation",
data=dict(section_id=section_id, text=LABEL),
follow_redirects=True,
)
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
section_id=section_id, text=LABEL
).first()
return interpretation, response
def create_comment(client, book_id, interpretation_id):
random_id = randint(1, 100)
TEXT = f"TComment {random_id}"
response: Response = client.post(
f"/book/{book_id}/{interpretation_id}/create_comment",
data=dict(
text=TEXT,
interpretation_id=interpretation_id,
),
follow_redirects=True,
)
comment: m.Comment = m.Comment.query.filter_by(text=TEXT).first()
return comment, response
def test_editor_access_to_entire_book(client):
login(client)
book = create_book(client)
editor = m.User(username="editor", password="editor").save()
response: Response = client.post(
f"/book/{book.id}/add_contributor",
data=dict(user_id=editor.id, role=m.BookContributor.Roles.EDITOR),
follow_redirects=True,
)
assert response.status_code == 200
assert b"Contributor was added!" in response.data
logout(client)
login(client, "editor", "editor")
# access to settings page
response: Response = client.get(f"/book/{book.id}/settings", follow_redirects=True)
assert b"You do not have permission" not in response.data
# access to edit book
response: Response = client.post(
f"/book/{book.id}/edit",
data=dict(book_id=book.id, label="BookEdited"),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# dont have access to delete
response: Response = client.post(
f"/book/{book.id}/delete",
data=dict(book_id=book.id),
follow_redirects=True,
)
assert b"You do not have permission" in response.data
# access to create collection
collection, response = create_collection(client, book.id)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to edit collection
response: Response = client.post(
f"/book/{book.id}/{collection.id}/edit",
data=dict(label="NewLabel"),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to delete collection
response: Response = client.post(
f"/book/{book.id}/{collection.id}/delete", follow_redirects=True
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# restore collection
collection.is_deleted = False
collection.save()
# access to create section
section, response = create_section(client, book.id, collection.id)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to edit section
response: Response = client.post(
f"/book/{book.id}/{section.id}/edit_section",
data=dict(section_id=section.id, label="NewLabel"),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to delete section
response: Response = client.post(
f"/book/{book.id}/{section.id}/delete_section", follow_redirects=True
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# restore section
section.is_deleted = False
section.save()
# access to create interpretation
interpretation, response = create_interpretation(client, book.id, section.id)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to approve interpretation
response: Response = client.post(
f"/approve/interpretation/{interpretation.id}",
follow_redirects=True,
)
assert response
assert response.json["message"] == "success"
assert response.json["approve"]
assert interpretation.approved
# access to delete interpretation
response: Response = client.post(
f"/book/{book.id}/{interpretation.id}/delete_interpretation",
data=dict(interpretation_id=interpretation.id),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# restore interpretation
interpretation.is_deleted = False
interpretation.save()
# access to create comment
comment, response = create_comment(client, book.id, interpretation.id)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to approve comment
response: Response = client.post(
f"/approve/comment/{comment.id}",
follow_redirects=True,
)
assert response
assert response.json["message"] == "success"
assert response.json["approve"]
assert interpretation.approved
# access to delete comment
response: Response = client.post(
f"/book/{book.id}/{interpretation.id}/comment_delete",
data=dict(
text=comment.text,
interpretation_id=interpretation.id,
comment_id=comment.id,
),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
def test_moderator_access_to_entire_book(client):
login(client)
book = create_book(client)
editor = m.User(username="moderator", password="moderator").save()
response: Response = client.post(
f"/book/{book.id}/add_contributor",
data=dict(user_id=editor.id, role=m.BookContributor.Roles.MODERATOR),
follow_redirects=True,
)
assert response.status_code == 200
assert b"Contributor was added!" in response.data
logout(client)
login(client, "moderator", "moderator")
# access to settings page
response: Response = client.get(f"/book/{book.id}/settings", follow_redirects=True)
assert b"You do not have permission" in response.data
# access to edit book
response: Response = client.post(
f"/book/{book.id}/edit",
data=dict(book_id=book.id, label="BookEdited"),
follow_redirects=True,
)
assert b"You do not have permission" in response.data
# dont have access to delete
response: Response = client.post(
f"/book/{book.id}/delete",
data=dict(book_id=book.id),
follow_redirects=True,
)
assert b"You do not have permission" in response.data
logout(client)
login(client)
collection, response = create_collection(client, book.id)
section, response = create_section(client, book.id, collection.id)
login(client, "moderator", "moderator")
# access to create interpretation
interpretation, response = create_interpretation(client, book.id, section.id)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to approve interpretation
response: Response = client.post(
f"/approve/interpretation/{interpretation.id}",
follow_redirects=True,
)
assert response
assert response.json["message"] == "success"
assert response.json["approve"]
assert interpretation.approved
# access to delete interpretation
response: Response = client.post(
f"/book/{book.id}/{interpretation.id}/delete_interpretation",
data=dict(interpretation_id=interpretation.id),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# restore interpretation
interpretation.is_deleted = False
interpretation.save()
# access to create comment
comment, response = create_comment(client, book.id, interpretation.id)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data
# access to approve comment
response: Response = client.post(
f"/approve/comment/{comment.id}",
follow_redirects=True,
)
assert response
assert response.json["message"] == "success"
assert response.json["approve"]
assert interpretation.approved
# access to delete comment
response: Response = client.post(
f"/book/{book.id}/{interpretation.id}/comment_delete",
data=dict(
text=comment.text,
interpretation_id=interpretation.id,
comment_id=comment.id,
),
follow_redirects=True,
)
assert b"You do not have permission" not in response.data
assert b"Success!" in response.data

View File

@ -1,4 +1,8 @@
from app import models as m
from app.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: