Merge branch 'develop' into svyat/feat/upvote

This commit is contained in:
SvyatoslavArtymovych 2023-05-09 17:57:03 +03:00
commit 33de3b4a5e
21 changed files with 617 additions and 82 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@ node_modules/
.env .env
*.pyc *.pyc
/*.sqlite3 /*.sqlite3
.DS_Store

View File

@ -12,6 +12,7 @@
}, },
"cSpell.words": [ "cSpell.words": [
"bookname", "bookname",
"Btns",
"flowbite", "flowbite",
"jsonify", "jsonify",
"pydantic", "pydantic",

View File

@ -52,24 +52,23 @@ def create_breadcrumbs(
) )
] ]
for collection_id in collection_path: for index, collection_id in enumerate(collection_path):
if collection_id is None: if collection_id is None:
continue continue
collection: m.Collection = db.session.get(m.Collection, collection_id) collection: m.Collection = db.session.get(m.Collection, collection_id)
crumples += [ if index == 0:
s.BreadCrumb( crumples += [
type=s.BreadCrumbType.Collection, s.BreadCrumb(
url=url_for( type=s.BreadCrumbType.Collection,
"book.sub_collection_view", url=url_for(
book_id=book_id, "book.sub_collection_view",
collection_id=collection_id, book_id=book_id,
), collection_id=collection_id,
label=collection.label, ),
) label=collection.label,
] )
if section_id and collection_path: ]
section: m.Section = db.session.get(m.Section, section_id) elif index == 1:
if len(collection_path) == 2:
crumples += [ crumples += [
s.BreadCrumb( s.BreadCrumb(
type=s.BreadCrumbType.Section, type=s.BreadCrumbType.Section,
@ -79,53 +78,46 @@ def create_breadcrumbs(
collection_id=collection_path[0], collection_id=collection_path[0],
sub_collection_id=collection_path[-1], sub_collection_id=collection_path[-1],
), ),
label=section.label, label=collection.label,
)
]
else:
crumples += [
s.BreadCrumb(
type=s.BreadCrumbType.Section,
url=url_for(
"book.section_view",
book_id=book_id,
collection_id=collection_path[0],
sub_collection_id=collection_path[0],
),
label=section.label,
) )
] ]
if section_id and collection_path:
section: m.Section = db.session.get(m.Section, section_id)
crumples += [
s.BreadCrumb(
type=s.BreadCrumbType.Section,
url=url_for(
"book.interpretation_view",
book_id=book_id,
collection_id=collection_path[0],
sub_collection_id=collection_path[-1]
if len(collection_path) == 2
else collection_path[0],
section_id=section_id,
),
label=section.label,
)
]
if interpretation_id: if interpretation_id:
interpretation: m.Interpretation = db.session.get( interpretation: m.Interpretation = db.session.get(
m.Interpretation, interpretation_id m.Interpretation, interpretation_id
) )
if len(collection_path) == 2: crumples += [
crumples += [ s.BreadCrumb(
s.BreadCrumb( type=s.BreadCrumbType.Interpretation,
type=s.BreadCrumbType.Interpretation, url=url_for(
url=url_for( "book.qa_view",
"book.interpretation_view", book_id=book_id,
book_id=book_id, collection_id=collection_path[0],
collection_id=collection_path[0], sub_collection_id=collection_path[-1]
sub_collection_id=collection_path[-1], if len(collection_path) == 2
section_id=section_id, else collection_path[0],
), section_id=section_id,
label=interpretation.label, interpretation_id=interpretation_id,
) ),
] label=interpretation.label,
else: )
crumples += [ ]
s.BreadCrumb(
type=s.BreadCrumbType.Interpretation,
url=url_for(
"book.interpretation_view",
book_id=book_id,
collection_id=collection_path[0],
sub_collection_id=collection_path[0],
section_id=section_id,
),
label=interpretation.label,
)
]
return crumples return crumples

View File

@ -12,3 +12,4 @@ from .section import CreateSectionForm, EditSectionForm
from .interpretation import CreateInterpretationForm, EditInterpretationForm from .interpretation import CreateInterpretationForm, EditInterpretationForm
from .comment import CreateCommentForm from .comment import CreateCommentForm
from .vote import VoteForm from .vote import VoteForm
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm

View File

@ -7,7 +7,19 @@ class BaseCommentForm(FlaskForm):
text = StringField("Text", [DataRequired(), Length(3, 256)]) text = StringField("Text", [DataRequired(), Length(3, 256)])
marked = BooleanField("Marked") marked = BooleanField("Marked")
included_with_interpretation = BooleanField("Included") included_with_interpretation = BooleanField("Included")
parent_id = StringField("Text")
class CreateCommentForm(BaseCommentForm): class CreateCommentForm(BaseCommentForm):
submit = SubmitField("Create") submit = SubmitField("Create")
class DeleteCommentForm(FlaskForm):
comment_id = StringField("Text")
submit = SubmitField("Delete")
class EditCommentForm(FlaskForm):
comment_id = StringField("Text")
text = StringField("Text", [DataRequired(), Length(3, 256)])
submit = SubmitField("Edit")

View File

@ -9,6 +9,7 @@ class Comment(BaseModel):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
text = db.Column(db.Text, unique=False, nullable=False) text = db.Column(db.Text, unique=False, nullable=False)
marked = db.Column(db.Boolean, default=False) marked = db.Column(db.Boolean, default=False)
edited = db.Column(db.Boolean, default=False)
included_with_interpretation = db.Column(db.Boolean, default=False) included_with_interpretation = db.Column(db.Boolean, default=False)
# Foreign keys # Foreign keys

View File

@ -39,5 +39,9 @@ class Interpretation(BaseModel):
return count return count
@property
def active_comments(self):
return [comment for comment in self.comments if not comment.is_deleted]
def __repr__(self): def __repr__(self):
return f"<{self.id}: {self.label}>" return f"<{self.id}: {self.label}>"

1
app/static/js/comment.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export declare function initComments(): void;

File diff suppressed because one or more lines are too long

View File

@ -1,10 +1,10 @@
<!-- prettier-ignore --> <!-- prettier-ignore -->
<nav class="fixed flex p-4 pl-1 mt-1.5 z-40 w-full md:max-w-4xl bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700" aria-label="Breadcrumb"> <nav class="fixed flex p-4 pl-1 mt-1.5 z-40 w-full md:w-4/5 bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 md:space-x-3 ml-5 overflow-x-scroll md:overflow-auto p-0"> <ol class="inline-flex items-center space-x-1 md:space-x-3 ml-5 overflow-x-scroll md:overflow-auto p-0">
{% for breadcrumb in breadcrumbs %} {% for breadcrumb in breadcrumbs %}
<li class="inline-flex items-center"> <li class="inline-flex items-center">
{% if not loop.index==breadcrumbs|length %} {% if not loop.index==breadcrumbs|length %}
<a href="{{ breadcrumb.url }}" class="inline-flex items-center text-sm truncate w-24 font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white"> <a href="{{ breadcrumb.url }}" class="inline-flex items-center text-sm truncate w-30 font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white">
{% else %} {% else %}
<span class="inline-flex items-center text-sm truncate w-40 font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white"> <span class="inline-flex items-center text-sm truncate w-40 font-medium text-gray-700 hover:text-blue-600 dark:text-gray-400 dark:hover:text-white">
{% endif %} {% endif %}
@ -16,7 +16,7 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-4 h-4 mr-2 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" /> </svg> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="flex-shrink-0 w-4 h-4 mr-2 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" /> </svg>
{% endif %} {% endif %}
<!-- prettier-ignore --> <!-- prettier-ignore -->
<span class="truncate">{{ breadcrumb.label }}</span> <span class="truncate select-none">{{ breadcrumb.label }}</span>
{% if not loop.index==breadcrumbs|length %} {% if not loop.index==breadcrumbs|length %}
</a> </a>
{% else %} {% else %}

View File

@ -0,0 +1,25 @@
<!-- prettier-ignore-->
<div id="delete_comment_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 class="relative w-full max-w-2xl max-h-full">
<!-- Modal content -->
<form
{% if sub_collection %}
action="{{ url_for('book.comment_delete', book_id=book.id, collection_id=collection.id, sub_collection_id=sub_collection.id, section_id=section.id, interpretation_id=interpretation.id) }}"
{% else %}
action="{{ url_for('book.comment_delete', book_id=book.id, collection_id=collection.id, section_id=section.id, interpretation_id=interpretation.id) }}"
{% endif %}
method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
{{ form_hidden_tag() }}
<!-- 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"> Delete Comment </h3>
<input type="hidden" name="comment_id" id="comment_id" value="" />
<button id="modalAddCloseButton" data-modal-hide="delete_comment_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 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-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">Confirm Deletion</button>
</div>
</form>
</div>
</div>

View File

@ -0,0 +1,32 @@
<!-- prettier-ignore-->
<div id="edit_comment_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 class="relative w-full max-w-2xl max-h-full">
<!-- Modal content -->
<form
{% if sub_collection %}
action="{{ url_for('book.comment_edit', book_id=book.id, collection_id=collection.id, sub_collection_id=sub_collection.id, section_id=section.id, interpretation_id=interpretation.id) }}"
{% else %}
action="{{ url_for('book.comment_edit', book_id=book.id, collection_id=collection.id, section_id=section.id, interpretation_id=interpretation.id) }}"
{% endif %}
method="post" class="relative bg-white rounded-lg shadow dark:bg-gray-700">
{{ form_hidden_tag() }}
<!-- Modal header -->
<div class="flex flex-col justify-between p-4 border-b rounded-t dark:border-gray-600">
<div class="flex items-start justify-between w-full">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white"> Edit Comment </h3>
<button id="modalAddCloseButton" data-modal-hide="edit_comment_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>
<input type="hidden" name="comment_id" id="edit_comment_id" value="" />
<div class="col-span-6 sm:col-span-3">
<label for="text" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white" >Text</label >
<input type="text" name="text" id="edit_comment_text" value="" class="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" required />
</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="ml-auto 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 w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Save changes</button>
</div>
</form>
</div>
</div>

View File

@ -141,7 +141,7 @@
</span> </span>
<div class="space-x-0.5 flex items-center"> <div 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="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" /> </svg> <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="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" /> </svg>
<p>55</p> <p>{{interpretation.active_comments | length}}</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,6 +2,10 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% include 'book/delete_interpretation_modal.html' %} {% include 'book/delete_interpretation_modal.html' %}
{% include 'book/delete_comment_modal.html' %}
{% include 'book/edit_comment_modal.html' %}
{% include 'book/edit_interpretation_modal.html' %} {% include 'book/edit_interpretation_modal.html' %}
<!-- show delete section btn on rightside bar --> <!-- show delete section btn on rightside bar -->
@ -16,7 +20,7 @@
{% block content %} {% block content %}
{% include 'book/breadcrumbs_navigation.html'%} {% include 'book/breadcrumbs_navigation.html'%}
<div class="overflow-x-auto shadow-md mt-5 md:mr-64 h-auto"> <div class="shadow-md mt-5 md:mr-64 h-auto overflow-x-hidden">
<div class="ql-snow mt-20"> <div class="ql-snow mt-20">
<h1 class="text-l font-extrabold dark:text-white ml-4 truncate"> <h1 class="text-l font-extrabold dark:text-white ml-4 truncate">
{{ interpretation.label }} {{ interpretation.label }}
@ -45,18 +49,15 @@
<dl <dl
class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700"> class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700">
<!-- prettier-ignore --> <!-- prettier-ignore -->
{% for comment in interpretation.comments %} <div class="quill-editor text-sm dark:text-white p-3">Comments:</div>
{% for comment in interpretation.comments if not comment.is_deleted %}
<!-- prettier-ignore --> <!-- 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"> <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="flex flex-row pb-3 p-3 w-2/3 md:w-full">
<div class="flex flex-col m-5 justify-center items-center"> <div class="flex flex-col m-5 justify-center items-center">
<div> <div> <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="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg> </div>
<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="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
</div>
<span class="text-3xl">35</span> <span class="text-3xl">35</span>
<div> <div> <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="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg> </div>
<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="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
</div>
</div> </div>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<dt class="flex w-full mb-1 text-gray-500 md:text-lg dark:text-gray-400 flex-col md:max-w-xl"> <dt class="flex w-full mb-1 text-gray-500 md:text-lg dark:text-gray-400 flex-col md:max-w-xl">
@ -66,23 +67,60 @@
</div> </div>
</div> </div>
<div id="accordion-collapse" data-accordion="collapse" class="flex mt-auto align-center justify-between space-x-3"> <div id="accordion-collapse" data-accordion="collapse" class="flex mt-auto align-center justify-between space-x-3">
<div>Commented by {{comment.user.username}} on {{comment.created_at.strftime('%B %d, %Y')}}</div> <div>Commented by <span class="text-blue-500">{{comment.user.username}}</span> on {{comment.created_at.strftime('%B %d, %Y')}}{% if comment.edited %}<i class="text-green-200"> edited</i>{% endif %}</div>
{% if comment.user_id == current_user.id %}
<div class="flex ml-auto justify-between w-24"> <div class="flex ml-auto justify-between w-24">
<button type="button" data-accordion-target="#accordion-collapse-body-{{loop.index}}" aria-expanded="false" aria-controls="accordion-collapse-body-1" class="space-x-0.5 flex items-center"> <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>
<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>
<div class="relative">
<button type="button" data-popover-target="popover-comment" data-accordion-target="#accordion-collapse-body-{{loop.index}}" aria-expanded="false" aria-controls="accordion-collapse-body-1" class="relative 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 shrink-0"> <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" /> </svg> <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 shrink-0"> <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 01-.825-.242m9.345-8.334a2.126 2.126 0 00-.476-.095 48.64 48.64 0 00-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0011.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" /> </svg>
</button> </button>
</div> <div data-popover id="popover-comment" 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>Comment to this comment</p>
</div>
<div data-popper-arrow></div>
</div>
</div></div>
{% endif %}
</dt> </dt>
</div> </div>
<div class="p-5 m-3">
{% for child in comment.children %}<div class="p-1 mb-2">
- {{child.text}}<span> - <span class="text-blue-500">{{child.user.username}}</span> {{child.created_at.strftime('%B %d, %Y')}}</span>
</div>
{% endfor %}
</div>
<div id="accordion-collapse-body-{{loop.index}}" class="hidden" aria-labelledby="accordion-collapse-heading-1"> <div id="accordion-collapse-body-{{loop.index}}" class="hidden" aria-labelledby="accordion-collapse-heading-1">
<div class="p-5 border border-b-0 border-gray-200 dark:border-gray-700 dark:bg-gray-900"> <div class="p-5 border-t border-gray-200 dark:border-gray-700 dark:bg-gray-900">
<form {% if sub_collection %} <form {% if sub_collection %}
action="{{ url_for('book.create_comment', book_id=book.id, collection_id=collection.id, sub_collection_id=sub_collection.id,section_id=section.id,interpretation_id=interpretation.id) }}" action="{{ url_for('book.create_comment', book_id=book.id, collection_id=collection.id, sub_collection_id=sub_collection.id,section_id=section.id,interpretation_id=interpretation.id) }}"
{% else %} {% else %}
action="{{ url_for('book.create_comment', book_id=book.id, collection_id=collection.id,section_id=section.id,interpretation_id=interpretation.id) }}" action="{{ url_for('book.create_comment', book_id=book.id, collection_id=collection.id,section_id=section.id,interpretation_id=interpretation.id) }}"
{% endif %} {% endif %}
method="post" class="flex flex-col 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"> method="post" class="flex flex-col bg-white dark:bg-gray-900 max-w-full p-3 text-gray-900 dark:text-white dark:divide-gray-700 m-3 border-2 border-gray-200 border-solid rounded-lg dark:border-gray-700">
{{ form_hidden_tag() }} {{ form_hidden_tag() }}
<input type="hidden" name="parent_id" id="parent_id" value="{{comment.id}}" />
<div class="relative z-0 w-full mb-6 group"> <div class="relative z-0 w-full mb-6 group">
<!-- prettier-ignore --> <!-- prettier-ignore -->
<input autocomplete="off" type="text" name="text" id="floating_email" class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer" placeholder=" " required /> <input autocomplete="off" type="text" name="text" id="floating_email" class="block py-2.5 px-0 w-full text-sm text-gray-900 bg-transparent border-0 border-b-2 border-gray-300 appearance-none dark:text-white dark:border-gray-600 dark:focus:border-blue-500 focus:outline-none focus:ring-0 focus:border-blue-600 peer" placeholder=" " required />

View File

@ -1,5 +1,5 @@
<!-- prettier-ignore --> <!-- prettier-ignore -->
<aside id="logo-right-sidebar" class="fixed top-0 right-0 left-auto z-40 w-64 h-screen pt-28 transition-transform translate-x-96 bg-white border-r border-gray-200 md:translate-x-0 dark:bg-gray-800 dark:border-gray-700" aria-label="Right-sidebar"> <aside id="logo-right-sidebar" class="fixed top-0 right-0 left-auto z-50 w-64 h-screen pt-28 transition-transform translate-x-96 bg-white border-r border-gray-200 md:translate-x-0 dark:bg-gray-800 dark:border-gray-700" aria-label="Right-sidebar">
<div class="h-full pb-4 overflow-y-auto bg-white dark:bg-gray-800"> <div class="h-full pb-4 overflow-y-auto bg-white dark:bg-gray-800">
<ul class="space-y-4 mt-1 font-medium"> <ul class="space-y-4 mt-1 font-medium">
{% if book.owner.id == current_user.id %} {% if book.owner.id == current_user.id %}

View File

@ -1129,14 +1129,14 @@ def interpretation_delete(
@bp.route( @bp.route(
"/<int:book_id>/<int:collection_id>/<int:section_id>/<int:interpretation_id>/preview", "/<int:book_id>/<int:collection_id>/<int:section_id>/<int:interpretation_id>/preview",
methods=["GET", "POST"], methods=["GET"],
) )
@bp.route( @bp.route(
( (
"/<int:book_id>/<int:collection_id>/<int:sub_collection_id>/" "/<int:book_id>/<int:collection_id>/<int:sub_collection_id>/"
"<int:section_id>/<int:interpretation_id>/preview" "<int:section_id>/<int:interpretation_id>/preview"
), ),
methods=["GET", "POST"], methods=["GET"],
) )
@login_required @login_required
def qa_view( def qa_view(
@ -1147,7 +1147,7 @@ def qa_view(
sub_collection_id: int | None = None, sub_collection_id: int | None = None,
): ):
book: m.Book = db.session.get(m.Book, book_id) book: m.Book = db.session.get(m.Book, book_id)
if not book or book.owner != current_user or book.is_deleted: if not book or book.is_deleted:
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book) log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
flash("You are not owner of this book!", "danger") flash("You are not owner of this book!", "danger")
return redirect(url_for("book.my_books")) return redirect(url_for("book.my_books"))
@ -1232,7 +1232,7 @@ def create_comment(
sub_collection_id: int | None = None, sub_collection_id: int | None = None,
): ):
book: m.Book = db.session.get(m.Book, book_id) book: m.Book = db.session.get(m.Book, book_id)
if not book or book.owner != current_user or book.is_deleted: if not book or book.is_deleted:
log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book) log(log.INFO, "User: [%s] is not owner of book: [%s]", current_user, book)
flash("You are not owner of this book!", "danger") flash("You are not owner of this book!", "danger")
return redirect(url_for("book.my_books")) return redirect(url_for("book.my_books"))
@ -1298,6 +1298,10 @@ def create_comment(
user_id=current_user.id, user_id=current_user.id,
interpretation_id=interpretation_id, interpretation_id=interpretation_id,
) )
if form.parent_id.data:
comment.parent_id = form.parent_id.data
comment.interpretation = None
log( log(
log.INFO, log.INFO,
"Create comment for interpretation [%s]. Section: [%s]", "Create comment for interpretation [%s]. Section: [%s]",
@ -1316,3 +1320,194 @@ def create_comment(
flash(error.lower().replace("field", field_label).title(), "danger") flash(error.lower().replace("field", field_label).title(), "danger")
return redirect(redirect_url) return redirect(redirect_url)
@bp.route(
"/<int:book_id>/<int:collection_id>/<int:section_id>/<int:interpretation_id>/comment_delete",
methods=["POST"],
)
@bp.route(
(
"/<int:book_id>/<int:collection_id>/<int:sub_collection_id>/"
"<int:section_id>/<int:interpretation_id>/comment_delete"
),
methods=["POST"],
)
@login_required
def comment_delete(
book_id: int,
collection_id: int,
section_id: int,
interpretation_id: int,
sub_collection_id: int | None = None,
):
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")
return redirect(url_for("book.my_books"))
collection: m.Collection = db.session.get(m.Collection, collection_id)
if not collection or collection.is_deleted:
log(log.WARNING, "Collection with id [%s] not found", collection_id)
flash("Collection not found", "danger")
return redirect(url_for("book.collection_view", book_id=book_id))
if sub_collection_id:
sub_collection: m.Collection = db.session.get(m.Collection, sub_collection_id)
if not sub_collection or sub_collection.is_deleted:
log(
log.WARNING,
"Sub_collection with id [%s] not found",
sub_collection_id,
)
flash("SubCollection not found", "danger")
return redirect(
url_for(
"book.sub_collection_view",
book_id=book_id,
collection_id=collection_id,
)
)
redirect_url = url_for(
"book.qa_view",
book_id=book_id,
collection_id=collection_id,
sub_collection_id=sub_collection_id,
section_id=section_id,
interpretation_id=interpretation_id,
)
section: m.Section = db.session.get(m.Section, section_id)
if not section or section.is_deleted:
log(log.WARNING, "Section with id [%s] not found", section_id)
flash("Section not found", "danger")
return redirect(redirect_url)
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(redirect_url)
form = f.DeleteCommentForm()
comment_id = form.comment_id.data
comment: m.Comment = db.session.get(m.Comment, comment_id)
if not comment or comment.is_deleted:
log(log.WARNING, "Comment with id [%s] not found", comment_id)
flash("Comment not found", "danger")
return redirect(redirect_url)
if form.validate_on_submit():
comment.is_deleted = True
log(log.INFO, "Delete comment [%s]", comment)
comment.save()
flash("Success!", "success")
return redirect(redirect_url)
return redirect(
url_for(
"book.sub_collection_view",
book_id=book_id,
collection_id=collection_id,
)
)
@bp.route(
"/<int:book_id>/<int:collection_id>/<int:section_id>/<int:interpretation_id>/comment_edit",
methods=["POST"],
)
@bp.route(
(
"/<int:book_id>/<int:collection_id>/<int:sub_collection_id>/"
"<int:section_id>/<int:interpretation_id>/comment_edit"
),
methods=["POST"],
)
@login_required
def comment_edit(
book_id: int,
collection_id: int,
section_id: int,
interpretation_id: int,
sub_collection_id: int | None = None,
):
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")
return redirect(url_for("book.my_books"))
collection: m.Collection = db.session.get(m.Collection, collection_id)
if not collection or collection.is_deleted:
log(log.WARNING, "Collection with id [%s] not found", collection_id)
flash("Collection not found", "danger")
return redirect(url_for("book.collection_view", book_id=book_id))
if sub_collection_id:
sub_collection: m.Collection = db.session.get(m.Collection, sub_collection_id)
if not sub_collection or sub_collection.is_deleted:
log(
log.WARNING,
"Sub_collection with id [%s] not found",
sub_collection_id,
)
flash("SubCollection not found", "danger")
return redirect(
url_for(
"book.sub_collection_view",
book_id=book_id,
collection_id=collection_id,
)
)
redirect_url = url_for(
"book.qa_view",
book_id=book_id,
collection_id=collection_id,
sub_collection_id=sub_collection_id,
section_id=section_id,
interpretation_id=interpretation_id,
)
section: m.Section = db.session.get(m.Section, section_id)
if not section or section.is_deleted:
log(log.WARNING, "Section with id [%s] not found", section_id)
flash("Section not found", "danger")
return redirect(redirect_url)
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(redirect_url)
form = f.EditCommentForm()
comment_id = form.comment_id.data
comment: m.Comment = db.session.get(m.Comment, comment_id)
if not comment or comment.is_deleted:
log(log.WARNING, "Comment with id [%s] not found", comment_id)
flash("Comment not found", "danger")
return redirect(redirect_url)
if form.validate_on_submit():
comment.text = form.text.data
comment.edited = True
log(log.INFO, "Delete comment [%s]", comment)
comment.save()
flash("Success!", "success")
return redirect(redirect_url)
return redirect(
url_for(
"book.sub_collection_view",
book_id=book_id,
collection_id=collection_id,
)
)

View File

@ -0,0 +1,32 @@
"""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 ###

36
src/comment.ts Normal file
View File

@ -0,0 +1,36 @@
export function initComments() {
// deleting comment
const deleteCommentBtns: NodeListOf<HTMLButtonElement> =
document.querySelectorAll('#delete_comment_btn');
const deleteCommentInputOnModal: HTMLInputElement =
document.querySelector('#comment_id');
if (deleteCommentBtns && deleteCommentInputOnModal) {
deleteCommentBtns.forEach(btn =>
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-comment-id');
deleteCommentInputOnModal.value = id;
}),
);
}
// edit comment
const editCommentBtns: NodeListOf<HTMLButtonElement> =
document.querySelectorAll('#edit_comment_btn');
const editCommentInputOnModal: HTMLInputElement =
document.querySelector('#edit_comment_id');
const editCommentTextInputOnModal: HTMLInputElement =
document.querySelector('#edit_comment_text');
if (
editCommentBtns &&
editCommentInputOnModal &&
editCommentTextInputOnModal
) {
editCommentBtns.forEach(btn =>
btn.addEventListener('click', () => {
const id = btn.getAttribute('data-edit-comment-id');
const text = btn.getAttribute('data-edit-comment-text');
editCommentInputOnModal.value = id;
editCommentTextInputOnModal.value = text;
}),
);
}
}

View File

@ -4,6 +4,7 @@ import {initContributors} from './contributors';
import {initWallet} from './wallet'; import {initWallet} from './wallet';
import {initQuill} from './initQuill'; import {initQuill} from './initQuill';
import {initQuillValueToInput} from './quillValueToInput'; import {initQuillValueToInput} from './quillValueToInput';
import {initComments} from './comment';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initBooks(); initBooks();
@ -11,4 +12,5 @@ document.addEventListener('DOMContentLoaded', () => {
initQuill(); initQuill();
initQuillValueToInput(); initQuillValueToInput();
initWallet(); initWallet();
initComments();
}); });

View File

@ -1,3 +1,4 @@
# flake8: noqa F501
from flask import current_app as Response from flask import current_app as Response
from flask.testing import FlaskClient, FlaskCliRunner from flask.testing import FlaskClient, FlaskCliRunner
@ -887,3 +888,118 @@ def test_crud_interpretation(client: FlaskClient, runner: FlaskCliRunner):
m.Interpretation, section_in_collection.interpretations[0].id m.Interpretation, section_in_collection.interpretations[0].id
) )
assert deleted_interpretation.is_deleted assert deleted_interpretation.is_deleted
def test_crud_comment(client: FlaskClient, runner: FlaskCliRunner):
_, user = login(client)
user: m.User
# 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",
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",
about="Test Section in Collection #1 About",
collection_id=leaf_collection.id,
version_id=book.last_version.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()
section_in_subcollection: m.Section = m.Section(
label="Test Section in Subcollection #1 Label",
about="Test Section in Subcollection #1 About",
collection_id=sub_collection.id,
version_id=book.last_version.id,
).save()
label_1 = "Test Interpretation #1 Label"
text_1 = "Test Interpretation #1 Text"
response: Response = client.post(
f"/book/{book.id}/{collection.id}/{sub_collection.id}/{section_in_subcollection.id}/create_interpretation",
data=dict(section_id=section_in_subcollection.id, label=label_1, text=text_1),
follow_redirects=True,
)
assert response.status_code == 200
interpretation: m.Interpretation = m.Interpretation.query.filter_by(
label=label_1, section_id=section_in_subcollection.id
).first()
assert interpretation
assert interpretation.section_id == section_in_subcollection.id
assert not interpretation.comments
comment_text = "Some comment text"
response: Response = client.post(
f"/book/{book.id}/{collection.id}/{sub_collection.id}/{section_in_subcollection.id}/{interpretation.id}/preview/create_comment",
data=dict(
section_id=section_in_subcollection.id,
text=comment_text,
interpretation_id=interpretation.id,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
assert b"Success" in response.data
assert str.encode(comment_text) in response.data
comment: m.Comment = m.Comment.query.filter_by(text=comment_text).first()
assert comment
new_text = "Some new text"
# edit
response: Response = client.post(
f"/book/{book.id}/{collection.id}/{sub_collection.id}/{section_in_subcollection.id}/{interpretation.id}/comment_edit",
data=dict(
section_id=section_in_subcollection.id,
text=new_text,
interpretation_id=interpretation.id,
comment_id=comment.id,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
assert b"Success" in response.data
assert str.encode(new_text) in response.data
assert str.encode(comment_text) not in response.data
# delete
response: Response = client.post(
f"/book/{book.id}/{collection.id}/{sub_collection.id}/{section_in_subcollection.id}/{interpretation.id}/comment_delete",
data=dict(
section_id=section_in_subcollection.id,
text=comment_text,
interpretation_id=interpretation.id,
comment_id=comment.id,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
assert b"Success" in response.data
assert str.encode(comment_text) not in response.data

View File

@ -3,4 +3,4 @@
max-line-length = 120 max-line-length = 120
;exclude = tests/* ;exclude = tests/*
;max-complexity = 10 ;max-complexity = 10
exclude = .git,__pycache__,.venv/,migrations/ exclude = .git,__pycache__,.venv/,migrations/,node_modules