diff --git a/app/models/collection.py b/app/models/collection.py index 6c631e9..9d1d113 100644 --- a/app/models/collection.py +++ b/app/models/collection.py @@ -11,6 +11,7 @@ class Collection(BaseModel): about = db.Column(db.Text, unique=False, nullable=True) is_root = db.Column(db.Boolean, default=False) is_leaf = db.Column(db.Boolean, default=False) + position = db.Column(db.Integer, default=-1, nullable=True) # Foreign keys version_id = db.Column(db.ForeignKey("book_versions.id")) diff --git a/app/models/section.py b/app/models/section.py index 16ea606..fa6e027 100644 --- a/app/models/section.py +++ b/app/models/section.py @@ -18,6 +18,7 @@ class Section(BaseModel): user_id = db.Column(db.ForeignKey("users.id")) version_id = db.Column(db.ForeignKey("book_versions.id")) selected_interpretation_id = db.Column(db.Integer, nullable=True) + position = db.Column(db.Integer, default=-1, nullable=True) # Relationships collection = db.relationship("Collection", viewonly=True) diff --git a/app/views/book/collection.py b/app/views/book/collection.py index 2d2763d..4e86176 100644 --- a/app/views/book/collection.py +++ b/app/views/book/collection.py @@ -1,9 +1,4 @@ -from flask import ( - render_template, - flash, - redirect, - url_for, -) +from flask import render_template, flash, redirect, url_for, request from flask_login import login_required from app.controllers import ( @@ -45,10 +40,11 @@ def collection_view(book_id: int): def collection_create(book_id: int, collection_id: int | None = None): book: m.Book = db.session.get(m.Book, book_id) + parent_collection: m.Collection = None redirect_url = url_for("book.collection_view", book_id=book_id) if collection_id: - collection: m.Collection = db.session.get(m.Collection, collection_id) - if collection.is_leaf: + parent_collection: m.Collection = db.session.get(m.Collection, collection_id) + if parent_collection.is_leaf: log(log.WARNING, "Collection with id [%s] is leaf", collection_id) flash("You can't create subcollection for this collection", "danger") return redirect( @@ -86,11 +82,16 @@ def collection_create(book_id: int, collection_id: int | None = None): flash("Collection label must be unique!", "danger") return redirect(redirect_url) + position = 0 + if parent_collection and parent_collection.active_children: + position = len(parent_collection.active_children) + collection: m.Collection = m.Collection( label=label, about=form.about.data, parent_id=book.versions[-1].root_collection.id, version_id=book.last_version.id, + position=position, ) if collection_id: collection.parent_id = collection_id @@ -204,3 +205,68 @@ def collection_delete(book_id: int, collection_id: int): book_id=book_id, ) ) + + +# TODO permission check +# @require_permission( +# entity_type=m.Permission.Entity.COLLECTION, +# access=[m.Permission.Access.C], +# entities=[m.Collection, m.Book], +# ) +@bp.route( + "///collection/change_position", methods=["POST"] +) +@register_book_verify_route(bp.name) +@login_required +def change_collection_position(book_id: int, collection_id: int): + collection: m.Collection = db.session.get(m.Collection, collection_id) + new_position = request.json.get("position") + collection_id = request.json.get("collection_id") + + new_parent: m.Collection = collection.parent + if collection_id is not None: + new_parent: m.Collection = db.session.get(m.Collection, collection_id) + if not new_parent: + log(log.INFO, "Collection with id [%s] not found", collection_id) + return {"message": "new parent collection not found"}, 404 + + log( + log.INFO, + "Change collection [%s] parent_id to [%s]", + collection, + collection_id, + ) + collection.parent_id = collection_id + + if new_parent.active_children: + collections_to_edit = m.Collection.query.filter( + m.Collection.parent_id == new_parent.id, + m.Collection.position >= new_position, + ).all() + if collections_to_edit: + log(log.INFO, "Calculate new positions of collections in [%s]", collection) + for child in collections_to_edit: + child: m.Collection + if child.position >= new_position: + child.position += 1 + child.save(False) + + log( + log.INFO, + "Set new position [%s] of collection [%s]", + new_position, + collection, + ) + collection.position = new_position + else: + log( + log.INFO, + "Collection [%s] does not have active collection. Set collection [%s] position to 1", + collection, + new_parent, + ) + collection.position = 1 + + log(log.INFO, "Apply position changes on [%s]", collection) + collection.save() + return {"message": "success"} diff --git a/app/views/book/section.py b/app/views/book/section.py index b602bcc..cdcf8d5 100644 --- a/app/views/book/section.py +++ b/app/views/book/section.py @@ -1,8 +1,4 @@ -from flask import ( - flash, - redirect, - url_for, -) +from flask import flash, redirect, url_for, request from flask_login import login_required from app.controllers import register_book_verify_route @@ -35,10 +31,15 @@ def section_create(book_id: int, collection_id: int): form = f.CreateSectionForm() if form.validate_on_submit(): + position = 0 + if collection.active_sections: + position = len(collection.active_sections) + section: m.Section = m.Section( label=form.label.data, collection_id=collection_id, version_id=book.last_version.id, + position=position, ) collection.is_leaf = True log(log.INFO, "Create section [%s]. Collection: [%s]", section, collection_id) @@ -124,3 +125,55 @@ def section_delete( flash("Success!", "success") return redirect(url_for("book.collection_view", book_id=book_id)) + + +@bp.route("///section/change_position", methods=["POST"]) +@register_book_verify_route(bp.name) +@login_required +def change_section_position(book_id: int, section_id: int): + section: m.Section = db.session.get(m.Section, section_id) + new_position = request.json.get("position") + collection_id = request.json.get("collection_id") + + collection: m.Collection = section.collection + if collection_id is not None: + collection: m.Collection = db.session.get(m.Collection, collection_id) + if not collection: + log(log.INFO, "Collection with id [%s] not found", collection_id) + return {"message": "collection not found"}, 404 + + log( + log.INFO, + "Change section [%s] collection_id to [%s]", + section, + collection_id, + ) + section.collection_id = collection_id + + if collection.active_sections: + sections_to_edit = m.Section.query.filter( + m.Section.collection_id == collection.id, + m.Section.position >= new_position, + ).all() + if sections_to_edit: + log(log.INFO, "Calculate new positions of sections in [%s]", collection) + for child in sections_to_edit: + child: m.Section + if child.position >= new_position: + child.position += 1 + child.save(False) + + log(log.INFO, "Set new position [%s] of section [%s]", new_position, section) + section.position = new_position + else: + log( + log.INFO, + "Collection [%s] does not have active sections. Set section [%s] position to 1", + collection, + section, + ) + section.position = 1 + + log(log.INFO, "Apply position changes on [%s]", section) + section.save() + return {"message": "success"} diff --git a/migrations/versions/79e8c7bff9c9_init.py b/migrations/versions/79e8c7bff9c9_init.py index 092b528..7a4c682 100644 --- a/migrations/versions/79e8c7bff9c9_init.py +++ b/migrations/versions/79e8c7bff9c9_init.py @@ -1,7 +1,7 @@ """init Revision ID: 79e8c7bff9c9 -Revises: +Revises: Create Date: 2023-06-01 15:31:33.635236 """ @@ -10,7 +10,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision = '79e8c7bff9c9' +revision = "79e8c7bff9c9" down_revision = None branch_labels = None depends_on = None @@ -18,294 +18,463 @@ 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( + "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( + "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( + "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( + "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( + "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_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_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( + "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_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( + "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( + "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( + "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( + "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( + "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( + "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( + "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( + "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( + "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( + "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_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( + "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( + "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_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') + 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') + 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 ### + + +"" diff --git a/migrations/versions/96995454b90d_ordering.py b/migrations/versions/96995454b90d_ordering.py new file mode 100644 index 0000000..76e6445 --- /dev/null +++ b/migrations/versions/96995454b90d_ordering.py @@ -0,0 +1,38 @@ +"""ordering + +Revision ID: 96995454b90d +Revises: 79e8c7bff9c9 +Create Date: 2023-06-01 17:01:00.877443 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "96995454b90d" +down_revision = "79e8c7bff9c9" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("collections", schema=None) as batch_op: + batch_op.add_column(sa.Column("position", sa.Integer(), nullable=True)) + + with op.batch_alter_table("sections", schema=None) as batch_op: + batch_op.add_column(sa.Column("position", sa.Integer(), nullable=True)) + + # ### 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.drop_column("position") + + with op.batch_alter_table("collections", schema=None) as batch_op: + batch_op.drop_column("position") + + # ### end Alembic commands ### diff --git a/tests/test_ordering.py b/tests/test_ordering.py new file mode 100644 index 0000000..7f3cbaf --- /dev/null +++ b/tests/test_ordering.py @@ -0,0 +1,162 @@ +from flask import current_app as Response + +from app import models as m, db +from tests.utils import ( + login, + create_book, + create_sub_collection, + create_section, +) + + +def test_ordering_on_collection_create(client): + login(client) + book = create_book(client) + + root_collection = m.Collection.query.filter_by(is_root=True).first() + assert root_collection + assert root_collection.is_root + + for position in range(0, 10): + collection, _ = create_sub_collection(client, book.id, root_collection.id) + assert collection.position == position + + +def test_change_collection_ordering(client): + login(client) + book = create_book(client) + + root_collection = m.Collection.query.filter_by(is_root=True).first() + assert root_collection + assert root_collection.is_root + + current_ordering = {} # collection_id : position + for position in range(0, 10): + collection, _ = create_sub_collection(client, book.id, root_collection.id) + assert collection.position == position + current_ordering[collection.id] = collection.position + + collection: m.Collection = db.session.get(m.Collection, 3) + new_position = 4 + assert current_ordering[collection.id] != new_position + response: Response = client.post( + f"/book/{book.id}/{collection.id}/collection/change_position", + headers={"Content-Type": "application/json"}, + json=dict( + position=new_position, + ), + follow_redirects=True, + ) + + assert response.status_code == 200 + collection: m.Collection = db.session.get(m.Collection, 3) + assert current_ordering[collection.id] != collection.position + assert collection.position == new_position + for collection in m.Collection.query.filter_by(parent_id=root_collection.id).all(): + if collection.position < new_position: + assert current_ordering[collection.id] == collection.position + elif collection.position > new_position: + assert current_ordering[collection.id] + 1 == collection.position + + collection: m.Collection = db.session.get(m.Collection, 3) + collection_1, _ = create_sub_collection(client, book.id, root_collection.id) + assert collection.parent_id != collection_1.id + + response: Response = client.post( + f"/book/{book.id}/{collection.id}/collection/change_position", + headers={"Content-Type": "application/json"}, + json=dict(position=999, collection_id=collection_1.id), + follow_redirects=True, + ) + + collection: m.Collection = db.session.get(m.Collection, 3) + assert collection.parent_id == collection_1.id + assert collection.position == 1 + + response: Response = client.post( + f"/book/{book.id}/{collection.id}/collection/change_position", + headers={"Content-Type": "application/json"}, + json=dict(position=999, collection_id=999), + follow_redirects=True, + ) + assert response.status_code == 404 + assert response.json["message"] == "new parent collection not found" + + +def test_ordering_on_section_create(client): + login(client) + book = create_book(client) + + root_collection = m.Collection.query.filter_by(is_root=True).first() + assert root_collection + assert root_collection.is_root + + for position in range(0, 10): + section, _ = create_section(client, book.id, root_collection.id) + assert section.position == position + + +def test_change_section_ordering(client): + login(client) + book = create_book(client) + + root_collection = m.Collection.query.filter_by(is_root=True).first() + assert root_collection + assert root_collection.is_root + collection_1, _ = create_sub_collection(client, book.id, root_collection.id) + collection_2, _ = create_sub_collection(client, book.id, root_collection.id) + + current_ordering = {} # collection_id : position + for position in range(0, 10): + section, _ = create_section(client, book.id, collection_1.id) + assert section.position == position + current_ordering[section.id] = section.position + + section: m.Section = db.session.get(m.Section, 3) + new_position = 4 + assert current_ordering[section.id] != new_position + response: Response = client.post( + f"/book/{book.id}/{section.id}/section/change_position", + headers={"Content-Type": "application/json"}, + json=dict(position=new_position), + follow_redirects=True, + ) + + assert response.status_code == 200 + section: m.Section = db.session.get(m.Section, 3) + assert current_ordering[section.id] != section.position + assert section.position == new_position + for section in m.Section.query.filter_by(collection_id=collection_1.id).all(): + if section.position < new_position: + assert current_ordering[section.id] == section.position + elif section.position > new_position: + assert current_ordering[section.id] + 1 == section.position + + new_position = 999 + assert section.collection_id == collection_1.id + assert not len(collection_2.active_sections) + response: Response = client.post( + f"/book/{book.id}/{section.id}/section/change_position", + headers={"Content-Type": "application/json"}, + json=dict(position=new_position, collection_id=collection_2.id), + follow_redirects=True, + ) + assert response.status_code == 200 + + section: m.Section = db.session.get(m.Section, section.id) + assert section.collection_id != collection_1.id + assert section.collection_id == collection_2.id + + collection: m.Collection = section.collection + assert len(collection.active_sections) == 1 + assert section.position != new_position + assert section.position == 1 + + response: Response = client.post( + f"/book/{book.id}/{section.id}/section/change_position", + headers={"Content-Type": "application/json"}, + json=dict(position=new_position, collection_id=999), + follow_redirects=True, + ) + assert response.status_code == 404 + assert response.json["message"] == "collection not found" diff --git a/tests/test_permissions.py b/tests/test_permissions.py index cf01678..274b61b 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -1,96 +1,15 @@ -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 +from tests.utils import ( + login, + logout, + create_book, + create_collection, + create_section, + create_interpretation, + create_comment, +) def test_editor_access_to_entire_book(client): diff --git a/tests/utils.py b/tests/utils.py index f4e148c..8a4d9b2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,11 +1,14 @@ +from random import randint +from uuid import uuid4 + +from flask import current_app as Response + from app import models as m from app.controllers.create_access_groups import ( create_editor_group, create_moderator_group, ) -from random import randint - TEST_ADMIN_NAME = "bob" TEST_ADMIN_EMAIL = "bob@test.com" TEST_ADMIN_PASSWORD = "password" @@ -184,3 +187,108 @@ def check_if_nested_comment_entities_is_deleted( for child in comment.children: child: m.Comment assert child.is_deleted == is_deleted + + +# book entities: + + +def create_book(client): + random_id = str(uuid4()) + 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 = str(uuid4()) + 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_sub_collection(client, book_id, collection_id): + random_id = str(uuid4()) + LABEL = f"TCollection {random_id}" + response: Response = client.post( + f"/book/{book_id}/{collection_id}/create_sub_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 = str(uuid4()) + 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 = str(uuid4()) + 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 = str(uuid4()) + 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