mirror of https://github.com/logos-co/open-law.git
section tagging
This commit is contained in:
parent
5d9cde449e
commit
c09bc4ee41
|
@ -98,3 +98,28 @@ def set_interpretation_tags(interpretation: m.InterpretationTag, tags: str):
|
|||
log(log.INFO, "Create InterpretationTag: [%s]", interpretation_tag)
|
||||
interpretation_tag.save(False)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def set_section_tags(section: m.SectionTag, tags: str):
|
||||
section_tags = m.SectionTag.query.filter_by(section_id=section.id).all()
|
||||
for tag in section_tags:
|
||||
db.session.delete(tag)
|
||||
tags_names = [tag.title() for tag in tags.split(",") if len(tag)]
|
||||
|
||||
for tag_name in tags_names:
|
||||
try:
|
||||
tag = get_or_create_tag(tag_name)
|
||||
except ValueError as e:
|
||||
if str(e) == "Exceeded name length":
|
||||
continue
|
||||
log(
|
||||
log.CRITICAL,
|
||||
"Unexpected error [%s]",
|
||||
str(e),
|
||||
)
|
||||
raise e
|
||||
|
||||
section_tag = m.SectionTag(tag_id=tag.id, section_id=section.id)
|
||||
log(log.INFO, "Create SectionTag: [%s]", section_tag)
|
||||
section_tag.save(False)
|
||||
db.session.commit()
|
||||
|
|
|
@ -9,6 +9,7 @@ from app.logger import log
|
|||
class BaseSectionForm(FlaskForm):
|
||||
label = StringField("Label", [DataRequired(), Length(3, 256)])
|
||||
about = StringField("About")
|
||||
tags = StringField("Tags")
|
||||
|
||||
|
||||
class CreateSectionForm(BaseSectionForm):
|
||||
|
@ -49,7 +50,12 @@ class EditSectionForm(BaseSectionForm):
|
|||
label = field.data
|
||||
section_id = self.section_id.data
|
||||
|
||||
collection_id = db.session.get(m.Section, section_id).collection_id
|
||||
session = db.session.get(m.Section, section_id)
|
||||
if not session:
|
||||
log(log.WARNING, "Session with id [%s] not found", section_id)
|
||||
raise ValidationError("Invalid session id")
|
||||
|
||||
collection_id = session.collection_id
|
||||
section: m.Section = (
|
||||
m.Section.query.filter_by(
|
||||
is_deleted=False, label=label, collection_id=collection_id
|
||||
|
|
|
@ -14,3 +14,4 @@ from .tag import Tag
|
|||
from .interpretation_tag import InterpretationTag
|
||||
from .comment_tag import CommentTags
|
||||
from .book_tag import BookTags
|
||||
from .section_tag import SectionTag
|
||||
|
|
|
@ -22,6 +22,11 @@ class Section(BaseModel):
|
|||
interpretations = db.relationship(
|
||||
"Interpretation", viewonly=True, order_by="desc(Interpretation.id)"
|
||||
)
|
||||
tags = db.relationship(
|
||||
"Tag",
|
||||
secondary="section_tags",
|
||||
back_populates="sections",
|
||||
)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
from app import db
|
||||
from app.models.utils import BaseModel
|
||||
|
||||
|
||||
class SectionTag(BaseModel):
|
||||
__tablename__ = "section_tags"
|
||||
|
||||
# Foreign keys
|
||||
tag_id = db.Column(db.Integer, db.ForeignKey("tags.id"))
|
||||
section_id = db.Column(db.Integer, db.ForeignKey("sections.id"))
|
||||
|
||||
def __repr__(self):
|
||||
return f"<t:{self.tag_id} to i:{self.section_id}"
|
|
@ -8,6 +8,9 @@ class Tag(BaseModel):
|
|||
name = db.Column(db.String(32), unique=True, nullable=False)
|
||||
|
||||
# Relationships
|
||||
sections = db.relationship(
|
||||
"Section", secondary="section_tags", back_populates="tags"
|
||||
)
|
||||
interpretations = db.relationship(
|
||||
"Interpretation", secondary="interpretation_tags", back_populates="tags"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<!-- Add collection modal -->
|
||||
<!-- prettier-ignore-->
|
||||
<div id="add-section-modal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div id="add-section-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
|
||||
|
@ -9,7 +9,9 @@
|
|||
{% else %}
|
||||
action="{{ url_for('book.section_create', book_id=book.id, collection_id=collection.id) }}"
|
||||
{% endif %}
|
||||
method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
method="post"
|
||||
class="prevent-submit-on-enter relative bg-white rounded-lg shadow dark:bg-gray-700"
|
||||
>
|
||||
{{ form_hidden_tag() }}
|
||||
<input type="hidden" name="collection_id" id="collection_id" value="{{sub_collection.id if sub_collection else collection.id}}" />
|
||||
<input type="hidden" name="about" id="new-section-input" />
|
||||
|
@ -30,10 +32,25 @@
|
|||
<div class="p-6 pt-0 space-y-6">
|
||||
<div class="w-full max-w-6xl mx-auto rounded-xl bg-gray-50 dark:bg-gray-600 shadow-lg text-white-900">
|
||||
<div class="overflow-hidden rounded-md bg-gray-50 [&>*]:dark:bg-gray-600 text-black [&>*]:!border-none [&>*]:!stroke-black dark:text-white dark:[&>*]:!stroke-white">
|
||||
<div id="new-section" class="quill-editor dark:text-white h-80"></div>
|
||||
<div id="new-section" class="quill-editor dark:text-white h-40"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multiple-input-block mb-6 px-6 ">
|
||||
<label for="tags-input" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
Tags
|
||||
</label>
|
||||
<input type="text" name="tags" class="hidden tags-to-submit">
|
||||
<input
|
||||
type="text"
|
||||
id="tags-input"
|
||||
class="multiple-input mb-3 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="e.g. Law (press 'Enter' or Comma to add tag. Click on tag to edit it)"
|
||||
data-save-results-to="tags-to-submit"
|
||||
>
|
||||
<div class="multiple-input-items gap-1 flex flex-wrap">
|
||||
</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">Create</button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<!-- prettier-ignore-->
|
||||
<div id="edit-section-modal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
||||
<div id="edit-section-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
|
||||
|
@ -8,7 +8,9 @@
|
|||
{% else %}
|
||||
action="{{ url_for('book.section_edit', book_id=book.id, collection_id=collection.id, section_id=section.id) }}"
|
||||
{% endif %}
|
||||
method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
|
||||
method="post"
|
||||
class="relative bg-white rounded-lg shadow dark:bg-gray-700"
|
||||
>
|
||||
{{ form_hidden_tag() }}
|
||||
<input type="hidden" name="section_id" id="section_id" value="{{section.id}}" />
|
||||
<input type="hidden" name="about" id="new-section-about-input" />
|
||||
|
@ -30,12 +32,30 @@
|
|||
<div class="p-6 pt-0 space-y-6">
|
||||
<div class="w-full max-w-6xl mx-auto rounded-xl bg-gray-50 dark:bg-gray-600 shadow-lg text-white-900">
|
||||
<div class="overflow-hidden rounded-md bg-gray-50 [&>*]:dark:bg-gray-600 text-black [&>*]:!border-none [&>*]:!stroke-black dark:text-white dark:[&>*]:!stroke-white">
|
||||
<div id="new-section-about" class="quill-editor dark:text-white h-80">
|
||||
<div id="new-section-about" class="quill-editor dark:text-white h-40">
|
||||
{{ section.about|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="multiple-input-block mb-6 px-6 ">
|
||||
<label for="tags-input" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
|
||||
Tags
|
||||
</label>
|
||||
<input type="text" name="tags" class="hidden tags-to-submit">
|
||||
<input
|
||||
type="text"
|
||||
id="tags-input"
|
||||
class="multiple-input mb-3 shadow-sm bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-600 focus:border-blue-600 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||
placeholder="e.g. Law (press 'Enter' or Comma to add tag. Click on tag to edit it)"
|
||||
data-save-results-to="tags-to-submit"
|
||||
>
|
||||
<div class="multiple-input-items gap-1 flex flex-wrap">
|
||||
{% for tag in section.tags %}
|
||||
<div class="cursor-pointer multiple-input-word bg-sky-300 hover:bg-sky-400 dark:bg-blue-600 dark:hover:bg-blue-700 dark:text-white rounded text-center py-1/2 px-2">{{tag.name}}</div>
|
||||
{% endfor %}
|
||||
</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>
|
||||
|
|
|
@ -18,6 +18,7 @@ from app.controllers.tags import (
|
|||
set_book_tags,
|
||||
set_comment_tags,
|
||||
set_interpretation_tags,
|
||||
set_section_tags,
|
||||
)
|
||||
from app import models as m, db, forms as f
|
||||
from app.logger import log
|
||||
|
@ -675,6 +676,10 @@ def section_create(
|
|||
log(log.INFO, "Create section [%s]. Collection: [%s]", section, collection_id)
|
||||
section.save()
|
||||
|
||||
tags = form.tags.data
|
||||
if tags:
|
||||
set_section_tags(section, tags)
|
||||
|
||||
flash("Success!", "success")
|
||||
return redirect(redirect_url)
|
||||
else:
|
||||
|
@ -721,6 +726,10 @@ def section_edit(
|
|||
if about:
|
||||
section.about = about
|
||||
|
||||
tags = form.tags.data
|
||||
if tags:
|
||||
set_section_tags(section, tags)
|
||||
|
||||
log(log.INFO, "Edit section [%s]", section.id)
|
||||
section.save()
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
"""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 ###
|
|
@ -5,7 +5,7 @@ from app import models as m, db
|
|||
from tests.utils import login, create_test_book
|
||||
|
||||
|
||||
def test_create_tags_on_book_create(client: FlaskClient):
|
||||
def test_create_tags_on_book_create_and_edit(client: FlaskClient):
|
||||
login(client)
|
||||
|
||||
BOOK_NAME = "Test Book"
|
||||
|
@ -32,15 +32,6 @@ def test_create_tags_on_book_create(client: FlaskClient):
|
|||
tags_from_db: m.Tag = m.Tag.query.all()
|
||||
assert len(tags_from_db) == 3
|
||||
|
||||
|
||||
def test_create_tags_on_book_edit(client: FlaskClient):
|
||||
_, user = login(client)
|
||||
|
||||
book: m.Book = m.Book(label="Test book", user_id=user.id).save()
|
||||
m.BookVersion(semver="1.0.0", book_id=book.id).save()
|
||||
|
||||
assert not book.tags
|
||||
|
||||
tags = "tag1,tag2,tag3"
|
||||
|
||||
client.post(
|
||||
|
@ -201,3 +192,71 @@ def test_create_tags_on_interpretation_create_and_edit(client: FlaskClient):
|
|||
|
||||
tags_from_db: m.Tag = m.Tag.query.all()
|
||||
assert len(tags_from_db) == 5
|
||||
|
||||
|
||||
def test_create_tags_on_section_create_and_edit(client: FlaskClient):
|
||||
_, user = login(client)
|
||||
create_test_book(user.id, 1)
|
||||
|
||||
book: m.Book = db.session.get(m.Book, 1)
|
||||
collection: m.Collection = db.session.get(m.Collection, 1)
|
||||
section: m.Section = db.session.get(m.Section, 1)
|
||||
|
||||
tags = "tag1,tag2,tag3"
|
||||
label_1 = "Test Interpretation #1 Label"
|
||||
text_1 = "Test Interpretation #1 Text"
|
||||
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{collection.id}/create_section",
|
||||
data=dict(
|
||||
collection_id=collection.id,
|
||||
label=label_1,
|
||||
about=text_1,
|
||||
tags=tags,
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
assert response.status_code == 200
|
||||
section: m.Section = m.Section.query.filter_by(label=label_1).first()
|
||||
assert section
|
||||
assert section.tags
|
||||
|
||||
splitted_tags = [tag.title() for tag in tags.split(",")]
|
||||
assert len(section.tags) == 3
|
||||
for tag in section.tags:
|
||||
tag: m.Tag
|
||||
assert tag.name in splitted_tags
|
||||
|
||||
tags_from_db: m.Tag = m.Tag.query.all()
|
||||
assert len(tags_from_db) == 3
|
||||
|
||||
tags = "tag-4,tag5,tag3"
|
||||
response: Response = client.post(
|
||||
f"/book/{book.id}/{collection.id}/{section.id}/edit_section",
|
||||
data=dict(
|
||||
section_id=section.id,
|
||||
label=label_1,
|
||||
about=text_1,
|
||||
tags=tags,
|
||||
),
|
||||
follow_redirects=True,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Success!" in response.data
|
||||
|
||||
assert response.status_code == 200
|
||||
section: m.Section = m.Section.query.filter_by(label=label_1).first()
|
||||
assert section
|
||||
assert section.tags
|
||||
|
||||
splitted_tags = [tag.title() for tag in tags.split(",")]
|
||||
assert len(section.tags) == 3
|
||||
for tag in section.tags:
|
||||
tag: m.Tag
|
||||
assert tag.name in splitted_tags
|
||||
|
||||
tags_from_db: m.Tag = m.Tag.query.all()
|
||||
assert len(tags_from_db) == 5
|
||||
|
|
Loading…
Reference in New Issue