diff --git a/app/views/book/collection.py b/app/views/book/collection.py index 2d2763d..8dd2a36 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,32 @@ 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") + + collections_to_edit = m.Collection.query.filter( + m.Collection.parent_id == collection.parent.id, + m.Collection.position >= new_position, + ).all() + for child in collections_to_edit: + child: m.Collection + if child.position >= new_position: + child.position += 1 + child.save(False) + collection.position = new_position + collection.save() + return {} diff --git a/app/views/book/section.py b/app/views/book/section.py index b602bcc..f1fb16b 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,24 @@ 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") + + sections_to_edit = m.Section.query.filter( + m.Section.collection_id == section.collection.id, + m.Section.position >= new_position, + ).all() + for child in sections_to_edit: + child: m.Section + if child.position >= new_position: + child.position += 1 + child.save(False) + section.position = new_position + section.save() + return {} diff --git a/tests/test_ordering.py b/tests/test_ordering.py new file mode 100644 index 0000000..739e05d --- /dev/null +++ b/tests/test_ordering.py @@ -0,0 +1,109 @@ +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 + + +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 + + current_ordering = {} # collection_id : position + for position in range(0, 10): + section, _ = create_section(client, book.id, root_collection.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=root_collection.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 diff --git a/tests/utils.py b/tests/utils.py index f7e7518..8a4d9b2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,4 +1,5 @@ from random import randint +from uuid import uuid4 from flask import current_app as Response @@ -192,7 +193,7 @@ def check_if_nested_comment_entities_is_deleted( def create_book(client): - random_id = randint(1, 100) + random_id = str(uuid4()) BOOK_NAME = f"TBook {random_id}" response: Response = client.post( "/book/create", @@ -220,7 +221,7 @@ def create_book(client): def create_collection(client, book_id): - random_id = randint(1, 100) + random_id = str(uuid4()) LABEL = f"TCollection {random_id}" response: Response = client.post( f"/book/{book_id}/create_collection", @@ -234,8 +235,23 @@ def create_collection(client, book_id): 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 = randint(1, 100) + random_id = str(uuid4()) LABEL = f"TSection {random_id}" response: Response = client.post( f"/book/{book_id}/{collection_id}/create_section", @@ -250,7 +266,7 @@ def create_section(client, book_id, collection_id): def create_interpretation(client, book_id, section_id): - random_id = randint(1, 100) + random_id = str(uuid4()) LABEL = f"TInterpretation {random_id}" response: Response = client.post( f"/book/{book_id}/{section_id}/create_interpretation", @@ -264,7 +280,7 @@ def create_interpretation(client, book_id, section_id): def create_comment(client, book_id, interpretation_id): - random_id = randint(1, 100) + random_id = str(uuid4()) TEXT = f"TComment {random_id}" response: Response = client.post( f"/book/{book_id}/{interpretation_id}/create_comment",