Merge branch 'develop' into svyat/fix/breadcrubs

This commit is contained in:
SvyatoslavArtymovych 2023-05-10 16:46:56 +03:00
commit 01ce0c5bbd
15 changed files with 576 additions and 59 deletions

View File

@ -2,7 +2,7 @@
"python.linting.pylintEnabled": false, "python.linting.pylintEnabled": false,
"python.linting.flake8Enabled": true, "python.linting.flake8Enabled": true,
"python.linting.enabled": true, "python.linting.enabled": true,
"python.formatting.provider": "black", "python.formatting.provider": "none",
"python.terminal.activateEnvironment": true, "python.terminal.activateEnvironment": true,
"files.watcherExclude": { "files.watcherExclude": {
"**/.git/objects/**": true, "**/.git/objects/**": true,

View File

@ -15,7 +15,6 @@ migration = Migrate()
def create_app(environment="development"): def create_app(environment="development"):
from config import config from config import config
from app.views import ( from app.views import (
main_blueprint, main_blueprint,
@ -24,6 +23,7 @@ def create_app(environment="development"):
book_blueprint, book_blueprint,
home_blueprint, home_blueprint,
section_blueprint, section_blueprint,
vote_blueprint,
) )
from app.models import ( from app.models import (
User, User,
@ -52,6 +52,7 @@ def create_app(environment="development"):
app.register_blueprint(book_blueprint) app.register_blueprint(book_blueprint)
app.register_blueprint(home_blueprint) app.register_blueprint(home_blueprint)
app.register_blueprint(section_blueprint) app.register_blueprint(section_blueprint)
app.register_blueprint(vote_blueprint)
# Set up flask login. # Set up flask login.
@login_manager.user_loader @login_manager.user_loader

View File

@ -10,4 +10,6 @@ from .contributor import (
from .collection import CreateCollectionForm, EditCollectionForm from .collection import CreateCollectionForm, EditCollectionForm
from .section import CreateSectionForm, EditSectionForm from .section import CreateSectionForm, EditSectionForm
from .interpretation import CreateInterpretationForm, EditInterpretationForm from .interpretation import CreateInterpretationForm, EditInterpretationForm
from .comment import CreateCommentForm
from .vote import VoteForm
from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm from .comment import CreateCommentForm, DeleteCommentForm, EditCommentForm

6
app/forms/vote.py Normal file
View File

@ -0,0 +1,6 @@
from flask_wtf import FlaskForm
from wtforms import BooleanField
class VoteForm(FlaskForm):
positive = BooleanField("Positive")

View File

@ -1,3 +1,5 @@
from flask_login import current_user
from app import db from app import db
from app.models.utils import BaseModel from app.models.utils import BaseModel
@ -30,5 +32,24 @@ class Comment(BaseModel):
back_populates="comments", back_populates="comments",
) )
@property
def vote_count(self):
count = 0
for vote in self.votes:
if vote.positive:
count += 1
continue
count -= 1
return count
@property
def current_user_vote(self):
for vote in self.votes:
if vote.user_id == current_user.id:
return vote.positive
return None
def __repr__(self): def __repr__(self):
return f"<{self.id}: {self.text[:20]}>" return f"<{self.id}: {self.text[:20]}>"

View File

@ -1,5 +1,7 @@
from datetime import datetime from datetime import datetime
from flask_login import current_user
from app import db from app import db
from app.models.utils import BaseModel from app.models.utils import BaseModel
@ -27,6 +29,25 @@ class Interpretation(BaseModel):
back_populates="interpretations", back_populates="interpretations",
) )
@property
def vote_count(self):
count = 0
for vote in self.votes:
if vote.positive:
count += 1
continue
count -= 1
return count
@property
def current_user_vote(self):
for vote in self.votes:
if vote.user_id == current_user.id:
return vote.positive
return None
@property @property
def active_comments(self): def active_comments(self):
return [comment for comment in self.comments if not comment.is_deleted] return [comment for comment in self.comments if not comment.is_deleted]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -97,14 +97,38 @@
<!-- 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-10/12"> <div class="flex flex-row pb-3 p-3 w-2/3 md:w-10/12">
<div class="flex flex-col m-5 justify-center items-center"> <div class="vote-block flex flex-col m-5 justify-center items-center">
<div> {% if interpretation.user_id != current_user.id %}
<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 class="vote-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="true">
</div> <svg class="w-6 h-6 select-none
<span class="text-3xl">35</span> {% if interpretation.current_user_vote %}
<div> stroke-green-500
<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> {% endif %}
</div> " xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" > <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
</div>
{% endif %}
<span
class="vote-count text-3xl select-none
{% if interpretation.vote_count < 0 %}
text-red-500
{% elif interpretation.vote_count > 0 %}
text-green-500
{% endif %}
"
>
{{ interpretation.vote_count }}
</span>
{% if interpretation.user_id != current_user.id %}
<div class="vote-button cursor-pointer" data-vote-for="interpretation" data-entity-id="{{ interpretation.id }}" data-positive="false">
<svg class="w-6 h-6 select-none
{% if interpretation.current_user_vote == False %}
stroke-red-500
{% endif %}
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
</div>
{% endif %}
</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"> <dt class="flex w-full mb-1 text-gray-500 md:text-lg dark:text-gray-400 flex-col">
@ -127,7 +151,10 @@
</div> </div>
</div> </div>
<div class="flex mt-auto align-center justify-between md:w-full"> <div class="flex mt-auto align-center justify-between md:w-full">
<div><span class="hidden md:inline-block">Interpretation by</span> {{interpretation.user.username}} on {{interpretation.created_at.strftime('%B %d, %Y')}}</div> <div>
<span class="hidden md:inline-block">Interpretation by</span>
{{interpretation.user.username}} on {{interpretation.created_at.strftime('%B %d, %Y')}}
</div>
<div class="flex ml-auto justify-between w-24"> <div class="flex ml-auto justify-between w-24">
<span class="space-x-0.5 flex items-center"> <span 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="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15m0-3l-3-3m0 0l-3 3m3-3V15" /> </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="M9 8.25H7.5a2.25 2.25 0 00-2.25 2.25v9a2.25 2.25 0 002.25 2.25h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25H15m0-3l-3-3m0 0l-3 3m3-3V15" /> </svg>

View File

@ -46,64 +46,94 @@
<button 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"> Leave comment </button> <button 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"> Leave comment </button>
</form> </form>
</div> </div>
<dl <!-- prettier-ignore -->
class="w-md md:w-full text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700"> <dl 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 -->
<div class="quill-editor text-sm dark:text-white p-3">Comments:</div> <div class="quill-editor text-sm dark:text-white p-3">Comments:</div>
{% for comment in interpretation.comments if not comment.is_deleted %} {% 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="vote-block flex flex-col m-5 justify-center items-center">
<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> {% if comment.user_id != current_user.id %}
<span class="text-3xl">35</span> <div class="vote-button cursor-pointer" data-vote-for="comment" data-entity-id="{{ comment.id }}" data-positive="true">
<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
class="w-6 h-6 select-none
{% if comment.current_user_vote %}
stroke-green-500
{% endif %}
"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" > <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 10.5L12 3m0 0l7.5 7.5M12 3v18" /> </svg>
</div> </div>
<!-- prettier-ignore --> {% endif %}
<dt class="flex w-full mb-1 text-gray-500 md:text-lg dark:text-gray-400 flex-col md:max-w-xl">
<div class="ql-snow"> <span
<div class="dark:text-white h-30 ql-editor"> class="vote-count text-3xl select-none
<p>{{ comment.text }}</p> {% if comment.vote_count < 0 %}
text-red-500
{% elif comment.vote_count > 0 %}
text-green-500
{% endif %}
"
>
{{ comment.vote_count }}
</span>
{% if comment.user_id != current_user.id %}
<div class="vote-button cursor-pointer" data-vote-for="comment" data-entity-id="{{ comment.id }}" data-positive="false">
<svg class="w-6 h-6 select-none
{% if comment.current_user_vote == False %}
stroke-red-500
{% endif %}
" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3" /> </svg>
</div>
{% endif %}
</div>
<!-- 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">
<div class="ql-snow">
<div class="dark:text-white h-30 ql-editor">
<p>{{ comment.text }}</p>
</div>
</div>
<div id="accordion-collapse" data-accordion="collapse" class="flex mt-auto align-center justify-between space-x-3">
<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="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>
</div> <div class="relative">
<div id="accordion-collapse" data-accordion="collapse" class="flex mt-auto align-center justify-between space-x-3"> <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">
<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> <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>
{% if comment.user_id == current_user.id %} </button>
<div class="flex ml-auto justify-between w-24"> <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="relative"> <div class="px-3 py-2">
<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"> <p>Delete this comment</p>
<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> </div>
</button> <div data-popper-arrow></div>
<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>
<div class="px-3 py-2"> </div>
<p>Edit this comment</p> <div class="relative">
</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"> <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 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 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"> <div class="px-3 py-2">
<p>Comment to this comment</p> <p>Comment to this comment</p>
</div>
<div data-popper-arrow></div>
</div> </div>
</div></div> </div></div>
{% endif %} {% endif %}
</dt> </dt>
</div> </div>
<div class="p-5 m-3"> <div class="p-5 m-3">
{% for child in comment.children %}<div class="p-5 mb-2 flex justify-between items-end bg-slate-600 rounded-lg"><div> {% for child in comment.children %}<div class="p-5 mb-2 flex justify-between items-end bg-slate-600 rounded-lg"><div>

View File

@ -5,3 +5,4 @@ from .user import bp as user_blueprint
from .book import bp as book_blueprint from .book import bp as book_blueprint
from .home import bp as home_blueprint from .home import bp as home_blueprint
from .section import bp as section_blueprint from .section import bp as section_blueprint
from .vote import bp as vote_blueprint

136
app/views/vote.py Normal file
View File

@ -0,0 +1,136 @@
from flask import (
Blueprint,
jsonify,
)
from flask_login import login_required, current_user
from app import models as m, db, forms as f
from app.logger import log
bp = Blueprint("vote", __name__, url_prefix="/vote")
@bp.route(
"/interpretation/<int:interpretation_id>",
methods=["POST"],
)
@login_required
def vote_interpretation(interpretation_id: int):
interpretation: m.Interpretation = db.session.get(
m.Interpretation, interpretation_id
)
if not interpretation:
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
return jsonify({"message": "Interpretation not found"}), 404
form = f.VoteForm()
if form.validate_on_submit():
vote: m.InterpretationVote = m.InterpretationVote.query.filter_by(
user_id=current_user.id, interpretation_id=interpretation_id
).first()
if vote:
db.session.delete(vote)
positive = form.positive.data and "true" in [
data.lower() for data in form.positive.raw_data
]
if not vote or vote.positive != positive:
vote: m.InterpretationVote = m.InterpretationVote(
user_id=current_user.id,
interpretation_id=interpretation_id,
positive=positive,
)
log(
log.INFO,
"User [%s]. [%s] vote interpretation: [%s]",
current_user,
"Positive" if positive else "Negative",
interpretation,
)
vote.save(False)
else:
log(
log.INFO,
"User [%s]. Remove [%s] vote for interpretation: [%s]",
current_user,
"positive" if positive else "negative",
interpretation,
)
db.session.commit()
return jsonify(
{
"vote_count": interpretation.vote_count,
"current_user_vote": interpretation.current_user_vote,
}
)
log(
log.CRITICAL,
"Unexpected error: User [%s]. Vote for interpretation: [%s]",
current_user,
interpretation,
)
return jsonify({"message": "Unexpected error"}), 400
@bp.route(
"/comment/<int:comment_id>",
methods=["POST"],
)
@login_required
def vote_comment(comment_id: int):
comment: m.Comment = db.session.get(m.Comment, comment_id)
if not comment:
log(log.WARNING, "Comment with id [%s] not found", comment_id)
return jsonify({"message": "Comment not found"}), 404
form = f.VoteForm()
if form.validate_on_submit():
vote: m.CommentVote = m.CommentVote.query.filter_by(
user_id=current_user.id, comment_id=comment_id
).first()
if vote:
db.session.delete(vote)
positive = form.positive.data and "true" in [
data.lower() for data in form.positive.raw_data
]
if not vote or vote.positive != positive:
vote: m.CommentVote = m.CommentVote(
user_id=current_user.id,
comment_id=comment_id,
positive=positive,
)
log(
log.INFO,
"User [%s]. [%s] vote comment: [%s]",
current_user,
"Positive" if positive else "Negative",
comment,
)
vote.save(False)
else:
log(
log.INFO,
"User [%s]. Remove [%s] vote for comment: [%s]",
current_user,
"positive" if positive else "negative",
comment,
)
db.session.commit()
return jsonify(
{
"vote_count": comment.vote_count,
"current_user_vote": comment.current_user_vote,
}
)
log(
log.CRITICAL,
"Unexpected error: User [%s]. Vote for comment: [%s]",
current_user,
comment,
)
return jsonify({"message": "Unexpected error"}), 400

View File

@ -4,6 +4,7 @@ import {initWallet} from './wallet';
import {initQuill} from './initQuill'; import {initQuill} from './initQuill';
import {initQuillValueToInput} from './quillValueToInput'; import {initQuillValueToInput} from './quillValueToInput';
import {initComments} from './comment'; import {initComments} from './comment';
import {initVote} from './vote';
import {initTheme} from './theme'; import {initTheme} from './theme';
initBooks(); initBooks();
@ -12,4 +13,5 @@ initQuill();
initQuillValueToInput(); initQuillValueToInput();
initWallet(); initWallet();
initComments(); initComments();
initVote();
initTheme(); initTheme();

74
src/vote.ts Normal file
View File

@ -0,0 +1,74 @@
const REQUEST_URLS: {[key: string]: string} = {
interpretation: '/vote/interpretation/',
comment: '/vote/comment/',
};
const voteClickEventListener = async (
btn: Element,
voteCountElement: Element,
setStrokeToVoteBtns: (positive: boolean | null) => void,
) => {
const voteFor = btn.getAttribute('data-vote-for');
if (!(voteFor in REQUEST_URLS)) {
console.error('Unknown data-vote-for attribute');
return;
}
const positive = btn.getAttribute('data-positive');
const entityId = btn.getAttribute('data-entity-id');
const requestUrl = REQUEST_URLS[voteFor] + entityId;
const response = await fetch(requestUrl, {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({positive: positive}),
});
const json = await response.json();
const voteCount = json.vote_count;
voteCountElement.innerHTML = json.vote_count;
voteCountElement.classList.remove('text-red-500');
voteCountElement.classList.remove('text-green-500');
if (voteCount > 0) {
voteCountElement.classList.add('text-green-500');
} else if (voteCount < 0) {
voteCountElement.classList.add('text-red-500');
}
const currentUserVote = json.current_user_vote;
setStrokeToVoteBtns(currentUserVote);
};
export function initVote() {
const voteBlocks = document.querySelectorAll('.vote-block');
voteBlocks.forEach(voteBlock => {
const voteCountElement = voteBlock.querySelector('.vote-count');
const voteBtns = voteBlock.querySelectorAll('.vote-button');
const setStrokeToVoteBtns = (positive: boolean | null) => {
voteBtns.forEach(btn => {
const svg = btn.querySelector('svg');
const dataPositive = btn.getAttribute('data-positive');
svg.classList.remove('stroke-red-500');
svg.classList.remove('stroke-green-500');
if (dataPositive == `${positive}` && positive == true) {
svg.classList.add('stroke-green-500');
} else if (dataPositive == `${positive}` && positive == false) {
svg.classList.add('stroke-red-500');
}
});
};
voteBtns.forEach(btn => {
btn.addEventListener('click', e => {
voteClickEventListener(btn, voteCountElement, setStrokeToVoteBtns);
});
});
});
}

196
tests/test_upvote.py Normal file
View File

@ -0,0 +1,196 @@
from flask import current_app as Response
from flask.testing import FlaskClient
from app import models as m
from tests.utils import login
def test_upvote_interpretation(client: FlaskClient):
_, user = login(client)
response: Response = client.post(
"/vote/interpretation/999",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 404
assert response.json["message"] == "Interpretation not found"
interpretation = m.Interpretation(
label="Test Interpretation 1 Label",
text="Test Interpretation 1 Text",
user_id=user.id,
).save()
assert interpretation.vote_count == 0
response: Response = client.post(
f"/vote/interpretation/{interpretation.id}",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == 1
assert "current_user_vote" in json
assert json["current_user_vote"]
assert interpretation.vote_count == 1
response: Response = client.post(
f"/vote/interpretation/{interpretation.id}",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == 0
assert "current_user_vote" in json
assert json["current_user_vote"] is None
assert interpretation.vote_count == 0
response: Response = client.post(
f"/vote/interpretation/{interpretation.id}",
data=dict(
positive=False,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == -1
assert "current_user_vote" in json
assert not json["current_user_vote"]
assert interpretation.vote_count == -1
response: Response = client.post(
f"/vote/interpretation/{interpretation.id}",
data=dict(
positive=False,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == 0
assert "current_user_vote" in json
assert json["current_user_vote"] is None
assert interpretation.vote_count == 0
def test_upvote_comment(client: FlaskClient):
_, user = login(client)
response: Response = client.post(
"/vote/comment/999",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 404
assert response.json["message"] == "Comment not found"
comment = m.Comment(
text="Test Comment 1 Text",
user_id=user.id,
).save()
assert comment.vote_count == 0
response: Response = client.post(
f"/vote/comment/{comment.id}",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == 1
assert "current_user_vote" in json
assert json["current_user_vote"]
assert comment.vote_count == 1
response: Response = client.post(
f"/vote/comment/{comment.id}",
data=dict(
positive=True,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == 0
assert "current_user_vote" in json
assert json["current_user_vote"] is None
assert comment.vote_count == 0
response: Response = client.post(
f"/vote/comment/{comment.id}",
data=dict(
positive=False,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == -1
assert "current_user_vote" in json
assert not json["current_user_vote"]
assert comment.vote_count == -1
response: Response = client.post(
f"/vote/comment/{comment.id}",
data=dict(
positive=False,
),
follow_redirects=True,
)
assert response
assert response.status_code == 200
json = response.json
assert json
assert "vote_count" in json
assert json["vote_count"] == 0
assert "current_user_vote" in json
assert json["current_user_vote"] is None
assert comment.vote_count == 0