Merge branch 'develop' into svyat/feat/fork

This commit is contained in:
SvyatoslavArtymovych 2023-06-15 11:06:26 +03:00
commit ed6df92f45
51 changed files with 1669 additions and 3991 deletions

View File

@ -29,6 +29,7 @@ def create_app(environment="development"):
star_blueprint,
permissions_blueprint,
search_blueprint,
notifications_blueprint,
)
from app import models as m
@ -58,6 +59,7 @@ def create_app(environment="development"):
app.register_blueprint(star_blueprint)
app.register_blueprint(permissions_blueprint)
app.register_blueprint(search_blueprint)
app.register_blueprint(notifications_blueprint)
# Set up flask login.
@login_manager.user_loader

View File

@ -0,0 +1,285 @@
from flask import url_for
from flask_login import current_user
from app import models as m, db
from app.logger import log
def create_notification(
entity: m.Notification.Entities,
action: m.Notification.Actions,
entity_id: int,
user_id: int,
text: str,
link: str,
):
m.Notification(
link=link,
text=text,
user_id=user_id,
action=action,
entity=entity,
entity_id=entity_id,
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
user_id,
)
def section_notification(action: m.Notification.Actions, entity_id: int, user_id: int):
text = None
link = None
section: m.Section = db.session.get(m.Section, entity_id)
book: m.Book = db.session.get(m.Book, section.book_id)
match action:
case m.Notification.Actions.CREATE:
text = f"{current_user.username} create a new section on {book.label}"
link = (
url_for("book.collection_view", book_id=book.id)
+ f"#section-{section.label}"
)
case m.Notification.Actions.EDIT:
text = f"{current_user.username} renamed a section on {book.label}"
link = (
url_for("book.collection_view", book_id=book.id)
+ f"#section-{section.label}"
)
case m.Notification.Actions.DELETE:
text = f"{current_user.username} delete a section on {book.label}"
link = url_for("book.collection_view", book_id=book.id)
create_notification(
m.Notification.Entities.SECTION, action, entity_id, user_id, text, link
)
def collection_notification(
action: m.Notification.Actions, entity_id: int, user_id: int
):
text = None
link = None
collection: m.Collection = db.session.get(m.Collection, entity_id)
book: m.Book = db.session.get(m.Book, collection.book_id)
match action:
case m.Notification.Actions.CREATE:
text = f"{current_user.username} create a new collection on {book.label}"
link = (
url_for("book.collection_view", book_id=book.id)
+ f"#collection-{collection.label}"
)
case m.Notification.Actions.EDIT:
text = f"{current_user.username} renamed a collection on {book.label}"
link = (
url_for("book.collection_view", book_id=book.id)
+ f"#collection-{collection.label}"
)
case m.Notification.Actions.DELETE:
text = f"{current_user.username} delete a collection on {book.label}"
link = url_for("book.collection_view", book_id=book.id)
create_notification(
m.Notification.Entities.COLLECTION, action, entity_id, user_id, text, link
)
def interpretation_notification(
action: m.Notification.Actions, entity_id: int, user_id: int
):
text = None
link = None
interpretation: m.Interpretation = db.session.get(m.Interpretation, entity_id)
section: m.Section = db.session.get(m.Section, interpretation.section_id)
book: m.Book = db.session.get(m.Book, interpretation.book.id)
match action:
case m.Notification.Actions.CREATE:
text = f"New interpretation to {section.label} on {book.label}"
link = url_for(
"book.interpretation_view",
book_id=book.id,
section_id=section.id,
)
# if have such notification stat to batch them
user: m.User = db.session.get(m.User, user_id)
for notification in user.active_notifications:
if (
f"new interpretations to {section.label} on {book.label}".lower()
in notification.text.lower()
):
splitted_text = notification.text.split()
counter = 2
if splitted_text[0].isnumeric():
counter = int(splitted_text[0]) + 1
notification.text = f"{counter} new interpretations to {section.label} on {book.label}"
notification.save()
return
case m.Notification.Actions.DELETE:
text = "A moderator has removed your interpretation"
link = url_for(
"book.interpretation_view",
book_id=book.id,
section_id=section.id,
)
case m.Notification.Actions.APPROVE:
if user_id == book.owner.id:
if current_user.id == book.owner.id:
return
# This for the book owner
text = f"{current_user.username} approved an interpretation for {section.label} on {book.label}"
link = url_for(
"book.interpretation_view",
book_id=book.id,
section_id=section.id,
)
elif user_id == interpretation.user_id and user_id != book.owner.id:
# This for the interpretation owner
text = f"Your interpretation has been approved for {section.label} on {book.label}"
link = url_for(
"book.interpretation_view",
book_id=book.id,
section_id=section.id,
)
else:
return
case m.Notification.Actions.VOTE:
text = f"{current_user.username} voted your interpretation"
link = url_for(
"book.interpretation_view",
book_id=book.id,
section_id=interpretation.section_id,
)
# if user already have such notification
user: m.User = db.session.get(m.User, user_id)
for notification in user.active_notifications:
if (
"voted your interpretation".lower() in notification.text.lower()
and notification.entity_id == entity_id
):
if current_user.id == notification.user_id:
return
splitted_text = notification.text.split()
counter = 2
if splitted_text[0].isnumeric():
counter = int(splitted_text[0]) + 1
notification.text = f"{counter} users voted your interpretation"
notification.save()
return
create_notification(
m.Notification.Entities.INTERPRETATION, action, entity_id, user_id, text, link
)
def comment_notification(action: m.Notification.Actions, entity_id: int, user_id: int):
text = None
link = None
comment: m.Comment = db.session.get(m.Comment, entity_id)
interpretation: m.Interpretation = db.session.get(
m.Interpretation, comment.interpretation_id
)
section: m.Section = db.session.get(m.Section, interpretation.section_id)
book: m.Book = db.session.get(m.Book, comment.book.id)
match action:
case m.Notification.Actions.CREATE:
text = "New comment to your interpretation"
link = url_for(
"book.qa_view",
book_id=book.id,
interpretation_id=comment.interpretation_id,
)
case m.Notification.Actions.DELETE:
text = "A moderator has removed your comment"
link = url_for(
"book.qa_view",
book_id=book.id,
interpretation_id=comment.interpretation_id,
)
case m.Notification.Actions.APPROVE:
if user_id == comment.user_id and user_id != book.owner.id:
text = f"Your comment has been approved for {section.label} on {book.label}"
link = url_for(
"book.qa_view",
book_id=comment.book.id,
interpretation_id=comment.interpretation_id,
)
elif user_id == book.owner.id:
text = f"{current_user.username} approved an comment for {section.label} on {book.label}"
link = url_for(
"book.qa_view",
book_id=comment.book.id,
interpretation_id=comment.interpretation_id,
)
else:
return
case m.Notification.Actions.MENTION:
text = "You been mention in comment"
link = url_for(
"book.qa_view",
book_id=book.id,
interpretation_id=comment.interpretation_id,
)
case m.Notification.Actions.VOTE:
text = f"{current_user.username} voted your comment"
link = url_for(
"book.qa_view",
book_id=book.id,
interpretation_id=comment.interpretation_id,
)
# if user already have such notification
user: m.User = db.session.get(m.User, user_id)
for notification in user.active_notifications:
if (
"voted your comment".lower() in notification.text.lower()
and notification.entity_id == entity_id
):
if current_user.id == notification.user_id:
return
splitted_text = notification.text.split()
counter = 2
if splitted_text[0].isnumeric():
counter = int(splitted_text[0]) + 1
notification.text = f"{counter} users voted your comment"
notification.save()
return
create_notification(
m.Notification.Entities.COMMENT, action, entity_id, user_id, text, link
)
def contributor_notification(
action: m.Notification.Actions, entity_id: int, user_id: int
):
text = None
link = None
book: m.Book = db.session.get(m.Book, entity_id)
match action:
case m.Notification.Actions.CONTRIBUTING:
text = f"You've been added to {book.label} as an Editor/Moderator"
link = url_for(
"book.collection_view",
book_id=book.id,
)
case m.Notification.Actions.DELETE:
text = f"You've been removed from {book.label} as an Editor/Moderator"
link = url_for(
"book.collection_view",
book_id=book.id,
)
create_notification(
m.Notification.Entities.BOOK, action, entity_id, user_id, text, link
)

View File

@ -0,0 +1,26 @@
from sqlalchemy import text
from app.logger import log
from app.controllers import create_pagination
def sort_by(query, sort: str):
match sort:
case "favored":
query = query.order_by(text("stars_count DESC"))
case "upvoted":
query = query.order_by(text("score DESC"))
case "recent":
query = query.order_by(text("created_at DESC"))
case "commented":
query = query.order_by(text("comments_count DESC"))
case "interpretations":
query = query.order_by(text("interpretations_count DESC"))
case _:
query = query.order_by(text("created_at DESC"))
pagination = create_pagination(total=query.count())
log(log.INFO, "Returns data for front end")
query = query.paginate(page=pagination.page, per_page=pagination.per_page)
query.items = [item[0] for item in query.items]
return pagination, query

View File

@ -25,3 +25,4 @@ from .permission import (
)
from .book_tag import BookTags
from .section_tag import SectionTag
from .notification import Notification

View File

@ -1,6 +1,6 @@
from flask_login import current_user
from app import db
from app import db, models as m
from app.models.utils import BaseModel
@ -51,5 +51,9 @@ class Comment(BaseModel):
return vote.positive
return None
@property
def book(self) -> m.Book:
return self.interpretation.book
def __repr__(self):
return f"<{self.id}: {self.text[:20]}>"

View File

@ -15,6 +15,7 @@ class Interpretation(BaseModel):
# Foreign keys
user_id = db.Column(db.ForeignKey("users.id"))
section_id = db.Column(db.ForeignKey("sections.id"))
score = db.Column(db.Integer(), default=0)
# Relationships
user = db.relationship("User")

View File

@ -0,0 +1,37 @@
from enum import IntEnum
from app.models.utils import BaseModel
from app import db
class Notification(BaseModel):
__tablename__ = "notifications"
class Actions(IntEnum):
CREATE = 1
EDIT = 2
DELETE = 3
VOTE = 4
APPROVE = 5
CONTRIBUTING = 6
MENTION = 7
class Entities(IntEnum):
SECTION = 1
COLLECTION = 2
INTERPRETATION = 3
COMMENT = 4
BOOK = 5
action = db.Column(db.Enum(Actions))
entity = db.Column(db.Enum(Entities))
entity_id = db.Column(db.Integer, nullable=False)
link = db.Column(db.String(256), unique=False, nullable=False)
text = db.Column(db.String(256), unique=False, nullable=False)
is_read = db.Column(db.Boolean, default=False)
# Foreign keys
user_id = db.Column(db.ForeignKey("users.id")) # for what user notification is
# Relationships
user = db.relationship("User", viewonly=True) # for what user notification is

View File

@ -31,6 +31,7 @@ class User(BaseModel, UserMixin):
)
stars = db.relationship("Book", secondary="books_stars", back_populates="stars")
books = db.relationship("Book")
notifications = db.relationship("Notification")
@hybrid_property
def password(self):
@ -70,6 +71,16 @@ class User(BaseModel, UserMixin):
contributions.append(comment.interpretation)
return contributions
@property
def active_notifications(self):
items = [
notification
for notification in self.notifications
if not notification.is_read
]
items.sort(key=lambda x: x.created_at)
return items
class AnonymousUser(AnonymousUserMixin):
pass

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -84,9 +84,6 @@
{% block body %}
{% if current_user.is_authenticated %}
{% block right_sidebar %}
{% include 'right_sidebar.html' %}
{% endblock %}
{% include 'book/modals/add_book_modal.html' %}
{% endif %}

View File

@ -1,99 +0,0 @@
<!-- prettier-ignore -->
{% extends 'base.html' %}
{% block title %}Books{% endblock %}
{% block content %}
<div class="md:mr-64 relative overflow-x-auto shadow-md sm:rounded-lg mt-1">
<!-- prettier-ignore -->
<div class="p-5 flex border-b-2 border-gray-200 border-solid dark:border-gray-700 text-gray-900 dark:text-white dark:divide-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>
<h1 class="text-2xl font-extrabold dark:text-white ml-4">Books</h1>
</div>
{% for book in books if not book.is_deleted %}
<!-- prettier-ignore -->
<dl class="bg-white dark:bg-gray-900 max-w-full p-5 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">
<dt class="mb-2"> <a class="flex flex-col pb-4" href="{{url_for('book.collection_view',book_id=book.id)}}">{{book.owner.username}}/{{book.label}}</a> </dt>
<dd class="flex flex-col md:flex-row text-lg font-semibold text-gray-500 md:text-lg dark:text-gray-400">
{% if book.versions %}
<p> Last updated on {{book.versions[-1].updated_at.strftime('%B %d, %Y')}}</p>
{% endif %}
<div class="flex ml-auto align-center justify-center space-x-3">
<span class="book-star-block space-x-0.5 flex items-center">
<svg class="star-btn cursor-pointer w-4 h-4 inline-flex mr-1 {% if book.current_user_has_star %}fill-yellow-300{% endif %}" data-book-id={{ book.id }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg>
<a href={{ url_for('book.statistic_view', book_id=book.id ) }} class="total-stars">{{ book.stars|length }}</a>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>
<p>{{ book.interpretations|length }}</p>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> </svg>
<p>{{ book.approved_comments|length }}</p>
</span>
</div>
</dd>
</dl>
{% endfor %}
<!-- prettier-ignore -->
{% if page.pages > 1 %}
<div class="container content-center mt-3 flex bg-white dark:bg-gray-800">
<nav aria-label="Page navigation example" class="mx-auto">
<ul class="inline-flex items-center -space-x-px">
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('book.get_all') }}?page=1&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<span class="sr-only">First</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" /> </svg>
</a>
</li>
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('book.get_all') }}?page={{page.page-1 if page.page > 1 else 1}}&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<span class="sr-only">Previous</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
<!-- prettier-ignore -->
{% for p in page.pages_for_links %}
<li>
<!-- prettier-ignore -->
{% if p == page.page %}
<!-- prettier-ignore -->
<a href="{{ url_for('book.get_all') }}?page={{p}}&q={{page.query}}" aria-current="page" class="z-10 px-3 py-2 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">{{p}}</a>
{% else %}
<!-- prettier-ignore -->
<a href="{{ url_for('book.get_all') }}?page={{p}}&q={{page.query}}" class="px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">{{p}}</a>
{% endif %}
</li>
{% endfor %}
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('book.get_all') }}?page={{page.page+1 if page.page < page.pages else page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<!-- prettier-ignore -->
<span class="sr-only">Next</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('book.get_all') }}?page={{page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<!-- prettier-ignore -->
<span class="sr-only">Last</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01.02-1.06L14.168 10 10.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M4.21 14.77a.75.75 0 01.02-1.06L8.168 10 4.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
<!-- prettier-ignore -->
{% include 'book/modals/add_book_modal.html' %}
<!-- prettier-ignore -->
{% endblock %}

View File

@ -8,28 +8,28 @@
{% if access_to_create_collections or access_to_update_collections %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
{% if access_to_create_collections_in_root %}
<li>
<button
type="button"
data-modal-target="add-collection-modal"
data-modal-toggle="add-collection-modal"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Collection
</button>
</li>
{% endif %}
{% if collection.active_children or not collection.active_sections%}
<li>
<button
type="button"
id="callAddSubCollectionModal"
data-modal-target="add-sub-collection-modal"
data-modal-toggle="add-sub-collection-modal"
data-collection-id="{{collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Subcollection
</button>
</li>
<li>
<button
type="button"
data-modal-target="add-collection-modal"
data-modal-toggle="add-collection-modal"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Collection
</button>
</li>
{% endif %} {% if collection.active_children or not
collection.active_sections%}
<li>
<button
type="button"
id="callAddSubCollectionModal"
data-modal-target="add-sub-collection-modal"
data-modal-toggle="add-sub-collection-modal"
data-collection-id="{{collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Subcollection
</button>
</li>
<!-- prettier-ignore -->
{% endif %}
{% if access_to_create_section %}
@ -80,15 +80,19 @@
{% endif %}
</ul>
{% endif %}
<!-- prettier-ignore -->
{% if not access_to_create_collections_in_root and not access_to_create_collections and not access_to_update_collections and not access_to_delete_collections and not access_to_create_section %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>
<button
type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Export Collection
You have no permissions for this collection
</button>
</li>
</ul>
{% endif %}
<!-- prettier-ignore -->
{% else %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>

View File

@ -0,0 +1,37 @@
<div class="bg-white dark:bg-gray-800 mr-5">
<ul class="flex font-medium">
<li>
<!-- prettier-ignore -->
<button type="button" data-modal-target="add-book-modal" data-modal-toggle="add-book-modal" class="text-white ml-2 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"><svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4"> <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /> </svg> New book </button>
</li>
<li>
<!-- prettier-ignore -->
<button id="dropdownDelayButton" data-dropdown-toggle="dropdownDelay" data-dropdown-delay="500" data-dropdown-trigger="hover" class="text-white ml-4 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
Sort by
{{request.args.get('sort',"")}}
<!-- prettier-ignore -->
<svg class="w-4 h-4 ml-auto" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg>
</button>
<!-- Dropdown menu -->
<!-- prettier-ignore -->
<div id="dropdownDelay" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDelayButton">
<!-- prettier-ignore -->
{% if selected_tab=='latest_interpretations' or selected_tab=='my_contributions' %}
<li> <a href="?sort=upvoted" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Most upvoted</a > </li>
{% endif %}
{% if selected_tab=='my_library' or selected_tab=='favorite_books' or selected_tab=='explore_books'%}
<li> <a href="?sort=favored" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Most favored</a > </li>
{% endif %}
<li> <a href="?sort=recent" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Most recent</a > </li>
{% if selected_tab=='latest_interpretations' or selected_tab=='my_contributions' %}
<li> <a href="?sort=commented" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Most comments</a > </li>
{% endif %}
{% if selected_tab=='my_library' or selected_tab=='favorite_books' or selected_tab=='explore_books'%}
<li> <a href="?sort=interpretations" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Most interpretations</a > </li>
{% endif %}
</ul>
</div>
</li>
</ul>
</div>

View File

@ -24,16 +24,19 @@
{% endif %}
</ul>
{% endif %}
<!-- prettier-ignore -->
{% if not access_to_create_sections and not access_to_update_sections and not access_to_delete_sections %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>
<button
type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Export Section
You have no permissions for this section
</button>
</li>
</ul>
{% endif %}
<!-- prettier-ignore -->
{% else %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>

View File

@ -6,120 +6,118 @@
{% set access_to_create_section = has_permission(sub_collection, Access.C,EntityType.SECTION) %}
{% if access_to_create_collections or access_to_update_collections or access_to_create_section %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<!-- prettier-ignore -->
{% if access_to_create_section and sub_collection.active_sections and not sub_collection.active_children %}
<li>
<button
type="button"
id="callAddSectionModal"
data-modal-target="add-section-modal"
data-modal-toggle="add-section-modal"
data-collection-id="{{collection.id}}"
data-sub-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
New Section
</button>
</li>
<!-- prettier-ignore -->
{% elif not sub_collection.active_sections and not sub_collection.active_children %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<!-- prettier-ignore -->
{% if access_to_create_section and sub_collection.active_sections and not sub_collection.active_children %}
<li>
<button
type="button"
id="callAddSectionModal"
data-modal-target="add-section-modal"
data-modal-toggle="add-section-modal"
data-collection-id="{{collection.id}}"
data-sub-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Section
</button>
</li>
<!-- prettier-ignore -->
{% elif not sub_collection.active_sections and not sub_collection.active_children %}
{% if access_to_create_section %}
<li>
<button
type="button"
id="callAddSectionModal"
data-modal-target="add-section-modal"
data-modal-toggle="add-section-modal"
data-collection-id="{{collection.id}}"
data-sub-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
New Section
</button>
</li>
{% endif %}
<!-- prettier-ignore -->
{% if access_to_create_collections %}
<li>
<button
type="button"
id="callAddSubCollectionModal"
data-modal-target="add-sub-collection-modal"
data-modal-toggle="add-sub-collection-modal"
data-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
New Subcollection
</button>
</li>
{% endif %}
{% else %}
<!-- prettier-ignore -->
{% if access_to_create_collections %}
<li>
<button
type="button"
id="callAddSubCollectionModal"
data-modal-target="add-sub-collection-modal"
data-modal-toggle="add-sub-collection-modal"
data-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Subcollection
</button>
</li>
{% endif %}
{% endif %}
</ul>
<li>
<button
type="button"
id="callAddSectionModal"
data-modal-target="add-section-modal"
data-modal-toggle="add-section-modal"
data-collection-id="{{collection.id}}"
data-sub-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Section
</button>
</li>
{% endif %}
<!-- prettier-ignore -->
{% if access_to_update_collections or access_to_delete_collections%}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
{% if access_to_update_collections %}
<li>
<button
type="button"
id="rename-sub-collection-button-{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Rename Sub Collection
</button>
</li>
{% endif %}
<!-- prettier-ignore -->
{% if access_to_delete_collections %}
<li>
<button
type="button"
id="callDeleteSubCollectionModal"
data-modal-target="delete-sub-collection-modal"
data-modal-toggle="delete-sub-collection-modal"
data-collection-id="{{collection.id}}"
data-sub-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
>
Delete Sub Collection
</button>
</li>
{% endif %}
</ul>
{% endif %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>
<button
type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Export Sub Collection
</button>
</li>
</ul>
{% else %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>
<button
type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Connect your wallet to do this
</button>
</li>
</ul>
{% if access_to_create_collections %}
<li>
<button
type="button"
id="callAddSubCollectionModal"
data-modal-target="add-sub-collection-modal"
data-modal-toggle="add-sub-collection-modal"
data-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Subcollection
</button>
</li>
{% endif %} {% else %}
<!-- prettier-ignore -->
{% if access_to_create_collections %}
<li>
<button
type="button"
id="callAddSubCollectionModal"
data-modal-target="add-sub-collection-modal"
data-modal-toggle="add-sub-collection-modal"
data-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
New Subcollection
</button>
</li>
{% endif %} {% endif %}
</ul>
{% endif %}
<!-- prettier-ignore -->
{% if access_to_update_collections or access_to_delete_collections%}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
{% if access_to_update_collections %}
<li>
<button
type="button"
id="rename-sub-collection-button-{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Rename Sub Collection
</button>
</li>
{% endif %}
<!-- prettier-ignore -->
{% if access_to_delete_collections %}
<li>
<button
type="button"
id="callDeleteSubCollectionModal"
data-modal-target="delete-sub-collection-modal"
data-modal-toggle="delete-sub-collection-modal"
data-collection-id="{{collection.id}}"
data-sub-collection-id="{{sub_collection.id}}"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Delete Sub Collection
</button>
</li>
{% endif %}
</ul>
{% endif %}
<!-- prettier-ignore -->
{% if not access_to_create_collections and not access_to_update_collections and not access_to_delete_collections and not access_to_create_section%}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>
<button
type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
You have no permissions for this sub collection
</button>
</li>
</ul>
{% endif %}
<!-- prettier-ignore -->
{% else %}
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200">
<li>
<button
type="button"
class="w-full block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
Connect your wallet to do this
</button>
</li>
</ul>
{% endif %}

View File

@ -6,25 +6,30 @@
{% block content %}
<div
class="md:mr-64 pt-1 relative overflow-x-auto shadow-md sm:rounded-lg mt-1 h-box w-box flex">
class="pt-1 relative pr-5 shadow-md sm:rounded-lg mt-1 h-box flex">
{% if not current_user.is_authenticated %}
<!-- prettier-ignore -->
<div class="mx-auto my-auto h-full w-full p-2">
<button type="button" id="connectWalletBtn" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"><div class="my-auto"></div> Connect you wallet to see your favorite books! </div></button></div>
<button type="button" id="connectWalletBtn" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"> Connect you wallet to see your favorite books! </div></button></div>
<!-- prettier-ignore -->
{% endif %}
{% if current_user.is_authenticated and current_user.stars|length==0 %}
<!-- prettier-ignore -->
<div class="mx-auto my-auto h-full w-full p-2">
<a type="button" href="{{ url_for('book.get_all') }}" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"><div class="my-auto"></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="M12 4.5v15m7.5-7.5h-15" /> </svg> You don't have favorite books start to explore book to choose one! </div></button></div>
<div class="mx-auto my-auto h-full w-full ">
<a href="{{ url_for('home.explore_books') }}" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"><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="M12 4.5v15m7.5-7.5h-15" /> </svg> You don't have favorite books start to explore book to choose one! </div></a></div>
<!-- prettier-ignore -->
{% endif %}
<!-- prettier-ignore -->
<div class="flex flex-col w-4/5">
<div class="flex flex-col w-full">
{% if current_user.is_authenticated and current_user.stars|length>0 %}
<div class="flex justify-between mt-1">
<h1 class=" text-lg font-extrabold dark:text-white ml-4">Fav books</h1>
{% if current_user.is_authenticated %}
{% include 'book/components/header_buttons.html' %}
{% endif %}
</div>
{% endif %}
{% for book in books %}
{% if loop.index==1 %}
<h1 class=" text-lg font-extrabold dark:text-white ml-4">Fav books</h1>
{% endif %}
<!-- prettier-ignore -->
<dl class="bg-white dark:bg-gray-900 h-max w-full p-5 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">
<dt class="mb-2"><a class="flex flex-col pb-4" href="{{url_for('book.collection_view',book_id=book.id)}}">{{book.label}}</a></dt>
@ -37,7 +42,7 @@
<div class="flex ml-auto align-center justify-center space-x-3">
<span class="book-star-block space-x-0.5 flex items-center">
<svg class="star-btn cursor-pointer w-4 h-4 inline-flex mr-1 {% if book.current_user_has_star %}fill-yellow-300{% endif %}" data-book-id={{ book.id }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg>
<a href={{ url_for('book.statistic_view', book_id=book.id ) }} class="total-stars">{{ book.stars|length }}</a>
<a href="{{ url_for('book.statistic_view', book_id=book.id ) }}" class="total-stars">{{ book.stars|length }}</a>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>

View File

@ -6,25 +6,30 @@
{% block content %}
<div
class="md:mr-64 pt-1 relative overflow-x-auto shadow-md sm:rounded-lg mt-1 h-box w-box flex">
class="pt-1 relative pr-5 shadow-md sm:rounded-lg mt-1 h-box flex">
{% if not current_user.is_authenticated %}
<!-- prettier-ignore -->
<div class="mx-auto my-auto h-full w-full p-2">
<button type="button" id="connectWalletBtn" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"><div class="my-auto"></div> Connect you wallet to see your contributions! </div></button></div>
<!-- prettier-ignore -->
{% endif %}
{% if current_user.is_authenticated and not interpretations %}
{% if current_user.is_authenticated and not interpretations.total %}
<!-- prettier-ignore -->
<div class="mx-auto my-auto h-full w-full p-2">
<a href="{{url_for('home.get_all')}}" type="button" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"><div class="my-auto"></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="M12 4.5v15m7.5-7.5h-15" /> </svg> You don't have contributions! Start review books to create one!</div></button></div>
<a href="{{url_for('home.get_all')}}" type="button" class="w-full h-full text-black dark:text-white focus:ring-4 focus:outline-none focus:ring-blue-100 font-medium rounded-lg text-sm px-4 py-2.5 justify-center text-center inline-flex items-center border border-gray-200 dark:border-gray-700"><div class="my-auto"></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="M12 4.5v15m7.5-7.5h-15" /> </svg> You don't have contributions! Start review books to create one!</div></a></div>
<!-- prettier-ignore -->
{% endif %}
<!-- prettier-ignore -->
<div class="flex flex-col w-4/5">
<div class="flex flex-col w-full">
{% if current_user.is_authenticated and interpretations.total %}
<div class="flex justify-between mt-1">
<h1 class=" text-lg font-extrabold dark:text-white ml-4">My contributions</h1>
{% if current_user.is_authenticated %}
{% include 'book/components/header_buttons.html' %}
{% endif %}
</div>
{% endif %}
{% for interpretation in interpretations %}
{% if loop.index==1 %}
<h1 class=" text-lg font-extrabold dark:text-white ml-4">My contributions</h1>
{% endif %}
<!-- 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">
<div class="flex flex-row pb-3 p-3">

View File

@ -12,7 +12,7 @@
<div
class="md:mr-64 pt-1 relative overflow-x-auto shadow-md sm:rounded-lg mt-1 h-box w-box flex">
class="pt-1 relative pr-5 shadow-md sm:rounded-lg mt-1 h-box flex">
{% if not current_user.is_authenticated %}
<!-- prettier-ignore -->
<div class="mx-auto my-auto h-full w-full p-2">
@ -26,11 +26,17 @@
<!-- prettier-ignore -->
{% endif %}
<!-- prettier-ignore -->
<div class="flex flex-col w-4/5">
<div class="flex flex-col w-full">
<div class="flex justify-between mt-1">
{% if current_user.is_authenticated and books.total>0 %}
<h1 class=" text-lg font-extrabold dark:text-white ml-4">My library</h1>
{% if current_user.is_authenticated %}
{% include 'book/components/header_buttons.html' %}
{% endif %}
</div>
{% endif %}
{% for book in books if not book.is_deleted%}
{% if loop.index==1 %}
<h1 class=" text-lg font-extrabold dark:text-white ml-4">My library</h1>
{% endif %}
<!-- prettier-ignore -->
<dl class="bg-white dark:bg-gray-900 h-max w-full p-5 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">
<dt class="mb-2 flex">

View File

@ -7,7 +7,7 @@
{% include 'book/modals/fork_book_modal.html' %}
<div class="border-b border-gray-200 dark:border-gray-700 md:mr-64">
<div class="border-b border-gray-200 dark:border-gray-700">
<!-- prettier-ignore -->
<h1 class="hidden md:inline font-extrabold text-lg dark:text-white ml-4 my-2">{{book.label}}</h1>
<!-- prettier-ignore -->
@ -40,7 +40,7 @@
</li>
</ul>
</div>
<div id="myTabContent" class="md:mr-64">
<div id="myTabContent">
<!-- prettier-ignore -->
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="favorited" role="tabpanel" aria-labelledby="favorited-tab">
<div class="relative w-full overflow-x-auto border-2 border-gray-200 border-solid rounded-lg dark:border-gray-700">

View File

@ -2,7 +2,7 @@
<!-- prettier-ignore -->
{% block content %}
<div class="jumbotron my-4 mr-64">
<div class="jumbotron my-4">
<div class="text-center">
<!-- prettier-ignore -->
<h1>{{ '{} - {}'.format(error.code, error.name) }}</h1>

View File

@ -40,9 +40,11 @@
<!-- prettier-ignore -->
<button id="dropdownNotificationButton" data-dropdown-toggle="dropdownNotification" class="inline-flex items-center text-sm font-medium text-center text-gray-500 hover:text-gray-900 focus:outline-none dark:hover:text-white dark:text-gray-400" type="button">
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"></path> </svg>
{% if current_user.active_notifications %}
<div class="relative flex">
<div class="relative inline-flex w-3 h-3 bg-red-500 border-2 border-white rounded-full -top-2 right-3 dark:border-gray-900"></div>
</div>
{% endif %}
</button>
</div>
<div class="items-center md:ml-3 hidden md:flex">
@ -89,26 +91,8 @@
<div id="dropdownNotification" class="shadow-md z-20 hidden w-screen bg-white divide-y divide-gray-100 rounded-lg dark:bg-gray-800 dark:divide-gray-700 border border-gray-600 dark:shadow-gray-600 md:w-1/2" aria-labelledby="dropdownNotificationButton">
<div class="block px-4 py-2 font-medium text-center text-gray-700 rounded-t-lg bg-gray-50 dark:bg-gray-800 dark:text-white"> Notifications </div>
<div class="divide-y divide-gray-100 dark:divide-gray-700">
<a href="#" class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="flex-shrink-0">
<img class="rounded-full w-11 h-11" src="" alt="Jese image" />
<div class="absolute flex items-center justify-center w-5 h-5 ml-6 -mt-5 bg-blue-600 border border-white rounded-full dark:border-gray-800">
<svg class="w-3 h-3 text-white" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path d="M8.707 7.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l2-2a1 1 0 00-1.414-1.414L11 7.586V3a1 1 0 10-2 0v4.586l-.293-.293z"></path> <path d="M3 5a2 2 0 012-2h1a1 1 0 010 2H5v7h2l1 2h4l1-2h2V5h-1a1 1 0 110-2h1a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2V5z"></path> </svg>
</div>
</div>
<div class="w-full pl-3">
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
New message from
<span class="font-semibold text-gray-900 dark:text-white"
>Jese Leos</span
>: "Hey, what's up? All set for the presentation?"
</div>
<div class="text-xs text-blue-600 dark:text-blue-500">
a few moments ago
</div>
</div>
</a>
<a href="#" class="block py-2 text-sm font-medium text-center text-gray-900 rounded-b-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-white">
{% include 'notification.html' %}
<a href="{{url_for('notifications.get_all')}}" class="block py-2 text-sm font-medium text-center text-gray-900 rounded-b-lg bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-white">
<div class="inline-flex items-center">
<svg class="w-4 h-4 mr-2 text-gray-500 dark:text-gray-400" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path> <path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path> </svg>
View all

View File

@ -0,0 +1,73 @@
{% set selected_tab='explore_books' %}
<!-- prettier-ignore -->
{% extends 'base.html' %}
{% block content %}
<div class="border-b pt-1 border-gray-200 dark:border-gray-700">
<!-- prettier-ignore -->
<h1 class="hidden md:inline font-extrabold text-lg dark:text-white ml-4 mt-5">Open Common Law</h1>
<div class="flex justify-between">
<p
class="hidden md:block text-sm ml-4 w-1/2 text-gray-500 text-center md:text-left dark:text-gray-400">
An open-source law hosting platform that allows online communities to
easily create, collaborate, and publish their own body of law.
</p>
<div class="flex">
<!-- prettier-ignore -->
{% if current_user.is_authenticated %}
{% include 'book/components/header_buttons.html' %}
{% endif %}
</div>
</div>
<!-- prettier-ignore -->
<ul class="flex md:flex-wrap -mb-px text-xs md:text-sm font-medium text-center" id="myTab" data-tabs-toggle="#myTabContent" role="tablist">
<li class="mr-2 w-full md:w-auto" role="presentation">
<!-- prettier-ignore -->
<a href="{{url_for('home.get_all')}}" class="inline-flex p-4 rounded-t-lg" id="last-interpretations-tab" data-tabs-target="#last-interpretations" type="button" role="tab" aria-controls="last-interpretations" aria-selected="false"> <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 mr-3"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>
Latest Interpretations
</a>
</li>
<li class="mr-2 w-full md:w-auto" role="presentation">
<!-- prettier-ignore -->
<button class="inline-flex p-4 rounded-t-lg hover:text-gray-600 dark:hover:text-gray-300" id="explore-books-tab" data-tabs-target="#explore-books" type="button" role="tab" aria-controls="explore-books" aria-selected="true"> <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 mr-3"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>
Explore Books
</button>
</li>
</ul>
</div>
<div id="myTabContent">
<!-- prettier-ignore -->
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="last-interpretations" role="tabpanel" aria-labelledby="last-interpretations-tab"></div>
<div
class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800"
id="explore-books"
role="tabpanel"
aria-labelledby="explore-books-tab">
{% for book in books if not book.is_deleted %}
<!-- prettier-ignore -->
<dl class=" bg-white dark:bg-gray-900 max-w-full p-5 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">
<dt class="mb-2"><a class="flex flex-col" href="{{url_for('book.collection_view',book_id=book.id)}}">{{book.label}}</a></dt>
<dd class="flex flex-col md:flex-row text-lg font-semibold text-gray-500 md:text-lg dark:text-gray-400">
{% if book.versions %}
<p> Last updated by <a href="{{url_for('user.profile',user_id=book.owner.id)}}" class=" text-blue-500 {% if book.owner.is_deleted %}line-through{% endif %}">{{book.owner.username}}</a> on {{book.versions[-1].updated_at.strftime('%B %d, %Y')}} </p>
{% endif %}
<div class="flex ml-auto align-center justify-center space-x-3">
<span class="book-star-block space-x-0.5 flex items-center">
<svg class="star-btn cursor-pointer w-4 h-4 inline-flex mr-1 {% if book.current_user_has_star %}fill-yellow-300{% endif %}" data-book-id={{ book.id }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg>
<a href="{{ url_for('book.statistic_view', book_id=book.id ) }}" class="total-stars">{{ book.stars|length }}</a>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>
<p>{{ book.interpretations|length }}</p>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> </svg>
<p>{{ book.approved_comments|length }}</p>
</span>
</div>
</dd>
</dl>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@ -1,14 +1,21 @@
{% set selected_tab='latest_interpretations' %}
<!-- prettier-ignore -->
{% extends 'base.html' %}
{% block content %}
<div class="border-b pt-1 border-gray-200 dark:border-gray-700 md:mr-64">
<div class="border-b pt-1 border-gray-200 dark:border-gray-700">
<!-- prettier-ignore -->
<h1 class="hidden md:inline font-extrabold text-lg dark:text-white ml-4 mt-5">Open Common Law</h1>
<div class="flex justify-between">
<p
class="hidden md:block text-sm ml-4 w-1/2 text-gray-500 text-center md:text-left dark:text-gray-400">
An open-source law hosting platform that allows online communities to easily
create, collaborate, and publish their own body of law.
</p>
{% if current_user.is_authenticated %}
{% include 'book/components/header_buttons.html' %}
{% endif %}
</div>
<!-- prettier-ignore -->
<ul class="flex md:flex-wrap -mb-px text-xs md:text-sm font-medium text-center" id="myTab" data-tabs-toggle="#myTabContent" role="tablist">
<li class="mr-2 w-full md:w-auto" role="presentation">
@ -19,13 +26,13 @@
</li>
<li class="mr-2 w-full md:w-auto" role="presentation">
<!-- prettier-ignore -->
<button class="inline-flex p-4 rounded-t-lg hover:text-gray-600 dark:hover:text-gray-300" id="explore-books-tab" data-tabs-target="#explore-books" type="button" role="tab" aria-controls="explore-books" aria-selected="false"> <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 mr-3"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>
<a href="{{url_for('home.explore_books')}}" class="inline-flex p-4 rounded-t-lg hover:text-gray-600 dark:hover:text-gray-300" id="explore-books-tab" data-tabs-target="#explore-books" type="button" role="tab" aria-controls="explore-books" aria-selected="false"> <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 mr-3"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>
Explore Books
</button>
</a>
</li>
</ul>
</div>
<div id="myTabContent" class="md:mr-64">
<div id="myTabContent">
<!-- prettier-ignore -->
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="last-interpretations" role="tabpanel" aria-labelledby="last-interpretations-tab">
{% for interpretation in interpretations %}
@ -97,35 +104,62 @@
</dl>
{% endfor %}
{% if current_user.is_authenticated and page.pages > 1 %}
<div class="container content-center mt-3 flex bg-white dark:bg-gray-800">
<nav aria-label="Page navigation example" class="mx-auto">
<ul class="inline-flex items-center -space-x-px">
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('home.get_all') }}?page=1&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<span class="sr-only">First</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" /> </svg>
</a>
</li>
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('home.get_all') }}?page={{page.page-1 if page.page > 1 else 1}}&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<span class="sr-only">Previous</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
<!-- prettier-ignore -->
{% for p in page.pages_for_links %}
<li>
<!-- prettier-ignore -->
{% if p == page.page %}
<!-- prettier-ignore -->
<a href="{{ url_for('home.get_all') }}?page={{p}}&q={{page.query}}" aria-current="page" class="z-10 px-3 py-2 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">{{p}}</a>
{% else %}
<!-- prettier-ignore -->
<a href="{{ url_for('home.get_all') }}?page={{p}}&q={{page.query}}" class="px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">{{p}}</a>
{% endif %}
</li>
{% endfor %}
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('home.get_all') }}?page={{page.page+1 if page.page < page.pages else page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<!-- prettier-ignore -->
<span class="sr-only">Next</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('home.get_all') }}?page={{page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<!-- prettier-ignore -->
<span class="sr-only">Last</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01.02-1.06L14.168 10 10.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M4.21 14.77a.75.75 0 01.02-1.06L8.168 10 4.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
<!-- prettier-ignore -->
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="explore-books" role="tabpanel" aria-labelledby="explore-books-tab">
{% for book in books if not book.is_deleted %}
<!-- prettier-ignore -->
<dl class=" bg-white dark:bg-gray-900 max-w-full p-5 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">
<dt class="mb-2"><a class="flex flex-col pb-4" href="{{url_for('book.collection_view',book_id=book.id)}}">{{book.label}}</a></dt>
<dd class="flex flex-col md:flex-row text-lg font-semibold text-gray-500 md:text-lg dark:text-gray-400">
{% if book.versions %}
<p> Last updated by <a href="{{url_for('user.profile',user_id=book.owner.id)}}" class=" text-blue-500 {% if book.owner.is_deleted %}line-through{% endif %}">{{book.owner.username}}</a> on {{book.versions[-1].updated_at.strftime('%B %d, %Y')}} </p>
{% endif %}
<div class="flex ml-auto align-center justify-center space-x-3">
<span class="book-star-block space-x-0.5 flex items-center">
<svg class="star-btn cursor-pointer w-4 h-4 inline-flex mr-1 {% if book.current_user_has_star %}fill-yellow-300{% endif %}" data-book-id={{ book.id }} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" /> </svg>
<a href={{ url_for('book.statistic_view', book_id=book.id ) }} class="total-stars">{{ book.stars|length }}</a>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75z" /> </svg>
<p>{{ book.interpretations|length }}</p>
</span>
<span class="space-x-0.5 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 22 22" stroke-width="1" stroke="currentColor" class="w-4 h-4 inline-flex mr-1"> <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" /> </svg>
<p>{{ book.approved_comments|length }}</p>
</span>
</div>
</dd>
</dl>
{% endfor %}
<a type="button" href="{{ url_for('book.get_all') }}" 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 inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"> Explore all books... <svg aria-hidden="true" class="w-5 h-5 ml-2 -mr-1" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg> </a>
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="explore-books" role="tabpanel" aria-labelledby="explore-books-tab">
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
<!-- prettier-ignore -->
{% if not current_user.active_notifications %}
<div class="flex px-4 py-3">
<div class="w-full pl-3">
<div class="text-gray-500 text-sm mb-1.5 dark:text-gray-400">
You haven't notifications!
</div>
</div>
</div>
<!-- prettier-ignore -->
{% endif %}
<!-- prettier-ignore -->
{% for notification in current_user.active_notifications[:3]%}
<a
href="{{url_for('notifications.mark_as_read',notification_id=notification.id)}}"
class="flex px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700">
<div class="w-full pl-3">
<div
class="text-gray-500 text-sm mb-1.5 dark:text-gray-400 {% if not notification.is_read %} font-bold{% endif %}">
{{notification.text}}
</div>
</div>
</a>
{% endfor %}

View File

@ -0,0 +1,86 @@
<!-- prettier-ignore -->
{% extends 'base.html' %}
{% block title %}Notifications{% endblock %}
{% block right_sidebar %}
{% endblock %}
{% block content %}
<div class="relative overflow-x-auto shadow-md sm:rounded-lg mt-1">
<!-- prettier-ignore -->
<div class="p-5 flex border-b-2 border-gray-200 border-solid dark:border-gray-700 text-gray-900 dark:text-white dark:divide-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"> <path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0M3.124 7.5A8.969 8.969 0 015.292 3m13.416 0a8.969 8.969 0 012.168 4.5" /> </svg>
<h1 class="text-2xl font-extrabold dark:text-white ml-4">Notifications</h1>
<a href="{{url_for('notifications.mark_all_as_read')}}" type="button" class="{% if not current_user.active_notifications %}disabled{% endif %} ml-auto text-green-700 hover:text-white border border-green-700 hover:bg-green-800 focus:ring-4 focus:outline-none focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center mr-2 mb-2 dark:border-green-500 dark:text-green-500 dark:hover:text-white dark:hover:bg-green-600 dark:focus:ring-green-800">Mark all notifications as READ</a>
</div>
{% if not current_user.notifications %}
<p
class="hidden md:block text-l ml-4 w-1/2 mt-2 text-gray-500 text-center md:text-left dark:text-gray-400">
You don't have notifications!
</p>
{% endif %} {% for notification in notifications %}
<!-- prettier-ignore -->
<dl class="bg-white dark:bg-gray-900 max-w-full p-5 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">
<dt class="mb-2"> <a class="flex flex-col pb-4 text-gray-500 {% if not notification.is_read %} text-blue-950 dark:text-white font-bold {% endif %}" href="{{url_for('notifications.mark_as_read',notification_id=notification.id)}}">{{notification.text}}</a> </dt>
<dd class="flex flex-col md:flex-row text-lg font-semibold text-gray-500 md:text-lg dark:text-gray-400">
<p> Created at {{notification.created_at.strftime('%B %d, %Y')}}</p>
</dd>
</dl>
{% endfor %}
<!-- prettier-ignore -->
{% if page.pages > 1 %}
<div class="container content-center mt-3 flex bg-white dark:bg-gray-800">
<nav aria-label="Page navigation example" class="mx-auto">
<ul class="inline-flex items-center -space-x-px">
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('notifications.get_all') }}?page=1&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<span class="sr-only">First</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M15.79 14.77a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L11.832 10l3.938 3.71a.75.75 0 01.02 1.06zm-6 0a.75.75 0 01-1.06.02l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 111.04 1.08L5.832 10l3.938 3.71a.75.75 0 01.02 1.06z" clip-rule="evenodd" /> </svg>
</a>
</li>
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('notifications.get_all') }}?page={{page.page-1 if page.page > 1 else 1}}&q={{page.query}}" class="block px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<span class="sr-only">Previous</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
<!-- prettier-ignore -->
{% for p in page.pages_for_links %}
<li>
<!-- prettier-ignore -->
{% if p == page.page %}
<!-- prettier-ignore -->
<a href="{{ url_for('notifications.get_all') }}?page={{p}}&q={{page.query}}" aria-current="page" class="z-10 px-3 py-2 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white">{{p}}</a>
{% else %}
<!-- prettier-ignore -->
<a href="{{ url_for('notifications.get_all') }}?page={{p}}&q={{page.query}}" class="px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">{{p}}</a>
{% endif %}
</li>
{% endfor %}
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('notifications.get_all') }}?page={{page.page+1 if page.page < page.pages else page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<!-- prettier-ignore -->
<span class="sr-only">Next</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
<li>
<!-- prettier-ignore -->
<a href="{{ url_for('notifications.get_all') }}?page={{page.pages}}&q={{page.query}}" class="block px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white">
<!-- prettier-ignore -->
<span class="sr-only">Last</span>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M10.21 14.77a.75.75 0 01.02-1.06L14.168 10 10.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M4.21 14.77a.75.75 0 01.02-1.06L8.168 10 4.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> </svg>
</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
<!-- prettier-ignore -->
{% endblock %}

View File

@ -8,38 +8,6 @@
<!-- prettier-ignore -->
<button type="button" data-modal-target="add-book-modal" data-modal-toggle="add-book-modal" class="text-white ml-2 w-11/12 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"><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="M12 4.5v15m7.5-7.5h-15" /> </svg> New book </button>
</li>
<li>
<!-- prettier-ignore -->
<button id="dropdownDelayButton" data-dropdown-toggle="dropdownDelay" data-dropdown-delay="500" data-dropdown-trigger="hover" class="text-white ml-2 w-11/12 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
<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="M6 13.5V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m12-3V3.75m0 9.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 3.75V16.5m-6-9V3.75m0 3.75a1.5 1.5 0 010 3m0-3a1.5 1.5 0 000 3m0 9.75V10.5" /> </svg>
Filters
</button>
<!-- Dropdown menu -->
<!-- prettier-ignore -->
<div id="dropdownDelay" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDelayButton">
<!-- prettier-ignore -->
<li> <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Dashboard</a > </li>
<li> <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Settings</a > </li>
</ul>
</div>
</li>
<li>
<!-- prettier-ignore -->
<button id="dropdownDelayButton" data-dropdown-toggle="dropdownDelay" data-dropdown-delay="500" data-dropdown-trigger="hover" class="text-white ml-2 w-11/12 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">
Tags
<svg class="w-4 h-4 ml-auto" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> </svg>
</button>
<!-- Dropdown menu -->
<!-- prettier-ignore -->
<div id="dropdownDelay" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow w-44 dark:bg-gray-700">
<ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownDelayButton">
<!-- prettier-ignore -->
<li> <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Dashboard</a > </li>
<li> <a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" >Settings</a > </li>
</ul>
</div>
</li>
<li>
<!-- prettier-ignore -->
<button id="dropdownDelayButton" data-dropdown-toggle="dropdownDelay" data-dropdown-delay="500" data-dropdown-trigger="hover" class="text-white ml-2 w-11/12 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800" type="button">

View File

@ -4,7 +4,7 @@
{% block title %}Sections{% endblock %}
{% block content %}
<div class="md:mr-64 relative overflow-x-auto shadow-md sm:rounded-lg mt-1">
<div class="relative overflow-x-auto shadow-md sm:rounded-lg mt-1">
<!-- prettier-ignore -->
<div class="p-5 flex border-b-2 border-gray-200 border-solid dark:border-gray-700 text-gray-900 dark:text-white dark:divide-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 3.75V16.5L12 14.25 7.5 16.5V3.75m9 0H18A2.25 2.25 0 0120.25 6v12A2.25 2.25 0 0118 20.25H6A2.25 2.25 0 013.75 18V6A2.25 2.25 0 016 3.75h1.5m9 0h-9" /> </svg>

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="border-b border-gray-200 dark:border-gray-700 md:mr-64">
<div class="border-b border-gray-200 dark:border-gray-700">
{% if user.is_deleted %}
<h1 class="hidden md:inline font-extrabold text-lg dark:text-white ml-4">Sorry this user was deactivated</h1>
{% else %}
@ -43,7 +43,7 @@
</li>
</ul>
</div>
<div id="myTabContent" class="md:mr-64">
<div id="myTabContent">
<!-- prettier-ignore -->
<div class="hidden p-4 rounded-lg bg-gray-50 dark:bg-gray-800" id="library" role="tabpanel" aria-labelledby="library-tab">
{% for book in user.books if not book.is_deleted %}

View File

@ -9,3 +9,4 @@ from .approve import bp as approve_blueprint
from .star import bp as star_blueprint
from .permission import bp as permissions_blueprint
from .search import bp as search_blueprint
from .notifications import bp as notifications_blueprint

View File

@ -5,6 +5,10 @@ from flask import (
from flask_login import login_required, current_user
from app import models as m, db
from app.controllers.notification_producer import (
interpretation_notification,
comment_notification,
)
from app.controllers.require_permission import require_permission
from app.logger import log
@ -25,6 +29,7 @@ def approve_interpretation(interpretation_id: int):
interpretation: m.Interpretation = db.session.get(
m.Interpretation, interpretation_id
)
book: m.Book = db.session.get(m.Book, interpretation.book.id)
if not interpretation:
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
return jsonify({"message": "Interpretation not found"}), 404
@ -55,6 +60,15 @@ def approve_interpretation(interpretation_id: int):
"Approve" if interpretation.approved else "Cancel approve",
interpretation,
)
if interpretation.approved and current_user.id != interpretation.user_id:
# notifications
interpretation_notification(
m.Notification.Actions.APPROVE, interpretation.id, book.owner.id
)
interpretation_notification(
m.Notification.Actions.APPROVE, interpretation.id, interpretation.user_id
)
interpretation.save()
return jsonify({"message": "success", "approve": interpretation.approved})
@ -72,6 +86,7 @@ def approve_interpretation(interpretation_id: int):
@login_required
def approve_comment(comment_id: int):
comment: m.Comment = db.session.get(m.Comment, comment_id)
book: m.Book = db.session.get(m.Book, comment.book.id)
if not comment:
log(log.WARNING, "Comment with id [%s] not found", comment_id)
return jsonify({"message": "Comment not found"}), 404
@ -84,6 +99,20 @@ def approve_comment(comment_id: int):
"Approve" if comment.approved else "Cancel approve",
comment,
)
# TODO:refactor if
if (
comment.approved
and current_user.id != comment.book.owner.id
and current_user.id != comment.user_id
):
# notifications
comment_notification(m.Notification.Actions.APPROVE, comment.id, book.owner.id)
elif comment.approved and current_user.id != comment.user_id:
# Your interpretation has been approved for SectionLabel on BookLabel
comment_notification(
m.Notification.Actions.APPROVE, comment.id, comment.user_id
)
# -------------
comment.save()
return jsonify({"message": "success", "approve": comment.approved})

View File

@ -1,9 +1,8 @@
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_required, current_user
from sqlalchemy import and_, or_
from sqlalchemy import and_, or_, func
from app.controllers import (
create_pagination,
register_book_verify_route,
)
from app.controllers.tags import (
@ -17,37 +16,56 @@ from app.controllers.create_access_groups import (
create_moderator_group,
)
from app.controllers.require_permission import require_permission
from app.controllers.sorting import sort_by
from app import models as m, db, forms as f
from app.logger import log
from .bp import bp
@bp.route("/all", methods=["GET"])
def get_all():
log(log.INFO, "Create query for books")
books: m.Book = m.Book.query.filter(m.Book.is_deleted is not False).order_by(
m.Book.id
)
log(log.INFO, "Create pagination for books")
pagination = create_pagination(total=books.count())
log(log.INFO, "Returning data for front end")
return render_template(
"book/all.html",
books=books.paginate(page=pagination.page, per_page=pagination.per_page),
page=pagination,
all_books=True,
)
@bp.route("/my_library", methods=["GET"])
def my_library():
if current_user.is_authenticated:
log(log.INFO, "Create query for my_library page for books")
sort = request.args.get("sort")
books: m.Book = (
db.session.query(m.Book)
db.session.query(
m.Book,
m.Book.created_at.label("created_at"),
func.count(m.Interpretation.id).label("interpretations_count"),
func.count(m.BookStar.id).label("stars_count"),
)
.join(
m.BookStar,
and_(
m.BookStar.book_id == m.Book.id,
m.BookStar.is_deleted == False, # noqa: E712
),
full=True,
)
.join(
m.BookVersion,
and_(
m.BookVersion.book_id == m.Book.id,
m.BookVersion.is_deleted == False, # noqa: E712
),
)
.join(
m.Section,
and_(
m.BookVersion.id == m.Section.version_id,
m.Section.is_deleted == False, # noqa: E712
),
full=True,
)
.join(
m.Interpretation,
and_(
m.Interpretation.section_id == m.Section.id,
m.Interpretation.is_deleted == False, # noqa: E712
),
full=True,
)
.join(m.BookContributor, m.BookContributor.book_id == m.Book.id, full=True)
.filter(
or_(
@ -59,14 +77,13 @@ def my_library():
.group_by(m.Book.id)
)
log(log.INFO, "Create pagination for books")
pagination, books = sort_by(books, sort)
pagination = create_pagination(total=books.count())
log(log.INFO, "Returns data for front end")
return render_template(
"book/my_library.html",
books=books.paginate(page=pagination.page, per_page=pagination.per_page),
books=books,
page=pagination,
)
log(log.INFO, "Returns data for front end is user is anonym")
@ -187,30 +204,61 @@ def statistic_view(book_id: int):
def favorite_books():
if current_user.is_authenticated:
log(log.INFO, "Creating query for books")
sort = request.args.get("sort")
books = (
db.session.query(
m.Book,
m.Book.created_at.label("created_at"),
func.count(m.Interpretation.id).label("interpretations_count"),
func.count(m.BookStar.id).label("stars_count"),
)
.join(
m.BookStar,
and_(
m.BookStar.book_id == m.Book.id,
m.BookStar.is_deleted == False, # noqa: E712
),
full=True,
)
.join(
m.BookVersion,
and_(
m.BookVersion.book_id == m.Book.id,
m.BookVersion.is_deleted == False, # noqa: E712
),
)
.join(
m.Section,
and_(
m.BookVersion.id == m.Section.version_id,
m.Section.is_deleted == False, # noqa: E712
),
full=True,
)
.join(
m.Interpretation,
and_(
m.Interpretation.section_id == m.Section.id,
m.Interpretation.is_deleted == False, # noqa: E712
),
full=True,
)
.filter(
and_(
m.Book.id == m.BookStar.book_id,
m.BookStar.user_id == current_user.id,
m.Book.is_deleted.is_(False),
)
m.Book.id == m.BookStar.book_id,
m.BookStar.user_id == current_user.id,
m.Book.is_deleted == False, # noqa: E712
)
.order_by(m.Book.created_at.desc())
.group_by(m.Book.id)
)
books = books.filter_by(is_deleted=False)
log(log.INFO, "Creating pagination for books")
pagination, books = sort_by(books, sort)
pagination = create_pagination(total=books.count())
log(log.INFO, "Returns data for front end")
return render_template(
"book/favorite_books.html",
books=books.paginate(page=pagination.page, per_page=pagination.per_page),
books=books,
page=pagination,
)
return render_template("book/favorite_books.html", books=[])
@ -220,9 +268,15 @@ def favorite_books():
def my_contributions():
if current_user.is_authenticated:
log(log.INFO, "Creating query for interpretations")
sort = request.args.get("sort")
interpretations = (
db.session.query(m.Interpretation)
db.session.query(
m.Interpretation,
m.Interpretation.score.label("score"),
m.Interpretation.created_at.label("created_at"),
func.count(m.Comment.interpretation_id).label("comments_count"),
)
.join(
m.Comment, m.Comment.interpretation_id == m.Interpretation.id, full=True
)
@ -250,18 +304,15 @@ def my_contributions():
m.Interpretation.copy_of == None, # noqa: E711
)
.group_by(m.Interpretation.id)
.order_by(m.Interpretation.created_at.desc())
)
log(log.INFO, "Creating pagination for interpretations")
pagination = create_pagination(total=interpretations.count())
pagination, interpretations = sort_by(interpretations, sort)
log(log.INFO, "Returns data for front end")
return render_template(
"book/my_contributions.html",
interpretations=interpretations.paginate(
page=pagination.page, per_page=pagination.per_page
),
interpretations=interpretations,
page=pagination,
)
return render_template("book/my_contributions.html", interpretations=[])

View File

@ -1,10 +1,11 @@
from flask import render_template, flash, redirect, url_for, request
from flask_login import login_required
from flask_login import login_required, current_user
from app.controllers import (
create_breadcrumbs,
register_book_verify_route,
)
from app.controllers.notification_producer import collection_notification
from app.controllers.delete_nested_book_entities import (
delete_nested_collection_entities,
)
@ -107,8 +108,14 @@ def collection_create(book_id: int, collection_id: int | None = None):
collection.parent_id = collection_id
log(log.INFO, "Create collection [%s]. Book: [%s]", collection, book.id)
collection.save()
collection.save()
# notifications
if current_user.id != book.owner.id:
collection_notification(
m.Notification.Actions.CREATE, collection.id, book.owner.id
)
# -------------
for access_group in collection.parent.access_groups:
m.CollectionAccessGroups(
collection_id=collection.id, access_group_id=access_group.id
@ -176,6 +183,13 @@ def collection_edit(book_id: int, collection_id: int):
log(log.INFO, "Edit collection [%s]", collection.id)
collection.save()
# notifications
if current_user.id != book.owner.id:
collection_notification(
m.Notification.Actions.EDIT, collection.id, book.owner.id
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
else:
@ -197,6 +211,11 @@ def collection_edit(book_id: int, collection_id: int):
@login_required
def collection_delete(book_id: int, collection_id: int):
collection: m.Collection = db.session.get(m.Collection, collection_id)
book: m.Book = db.session.get(m.Book, book_id)
redirect_url = url_for(
"book.collection_view",
book_id=book_id,
)
collection.is_deleted = True
if collection.active_children:
@ -208,21 +227,23 @@ def collection_delete(book_id: int, collection_id: int):
delete_nested_collection_entities(collection)
collection.save()
flash("Success!", "success")
return redirect(
url_for(
"book.collection_view",
book_id=book_id,
# notifications
if current_user.id != book.owner.id:
collection_notification(
m.Notification.Actions.DELETE, collection.id, book.owner.id
)
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
# TODO permission check
# @require_permission(
# entity_type=m.Permission.Entity.COLLECTION,
# access=[m.Permission.Access.C],
# entities=[m.Collection, m.Book],
# )
@require_permission(
entity_type=m.Permission.Entity.COLLECTION,
access=[m.Permission.Access.U],
entities=[m.Collection, m.Book],
)
@bp.route(
"/<int:book_id>/<int:collection_id>/collection/change_position", methods=["POST"]
)

View File

@ -1,9 +1,8 @@
from flask import flash, redirect, url_for, current_app
from flask_login import login_required, current_user
from app.controllers import (
register_book_verify_route,
)
from app.controllers import register_book_verify_route
from app.controllers.notification_producer import comment_notification
from app.controllers.delete_nested_book_entities import (
delete_nested_comment_entities,
)
@ -59,8 +58,27 @@ def create_comment(
)
comment.save()
# notifications
if current_user.id != interpretation.user_id:
comment_notification(
m.Notification.Actions.CREATE, comment.id, interpretation.user_id
)
# -------------
tags = current_app.config["TAG_REGEX"].findall(text)
set_comment_tags(comment, tags)
users_mentions = current_app.config["USER_MENTION_REGEX"].findall(text)
for mention in users_mentions:
mention = mention.replace("@", "")
user = m.User.query.filter(m.User.username.ilike(mention.lower())).first()
if user:
# notifications
comment_notification(
m.Notification.Actions.MENTION, comment.id, user.id
)
# -------------
# TODO Send notifications
# users_mentions = current_app.config["USER_MENTION_REGEX"].findall(text)
@ -86,6 +104,9 @@ def comment_delete(book_id: int, interpretation_id: int):
form = f.DeleteCommentForm()
comment_id = form.comment_id.data
comment: m.Comment = db.session.get(m.Comment, comment_id)
interpretation: m.Interpretation = db.session.get(
m.Interpretation, interpretation_id
)
if form.validate_on_submit():
comment.is_deleted = True
@ -93,6 +114,13 @@ def comment_delete(book_id: int, interpretation_id: int):
log(log.INFO, "Delete comment [%s]", comment)
comment.save()
# notifications
if current_user.id != interpretation.user_id:
comment_notification(
m.Notification.Actions.DELETE, comment.id, comment.user_id
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
flash("Invalid id!", "danger")

View File

@ -7,7 +7,12 @@ from flask import (
)
from flask_login import login_required, current_user
from app.controllers import register_book_verify_route, create_breadcrumbs, clean_html
from app.controllers import (
register_book_verify_route,
create_breadcrumbs,
clean_html,
)
from app.controllers.notification_producer import interpretation_notification
from app.controllers.delete_nested_book_entities import (
delete_nested_interpretation_entities,
)
@ -66,6 +71,7 @@ def interpretation_create(
section_id: int,
):
section: m.Section = db.session.get(m.Section, section_id)
book: m.Book = db.session.get(m.Book, book_id)
form = f.CreateInterpretationForm()
redirect_url = url_for(
"book.interpretation_view",
@ -102,6 +108,13 @@ def interpretation_create(
).save()
# -------------
# notifications
if current_user.id != book.owner.id:
interpretation_notification(
m.Notification.Actions.CREATE, interpretation.id, book.owner.id
)
# -------------
tags = current_app.config["TAG_REGEX"].findall(text)
set_interpretation_tags(interpretation, tags)
@ -128,7 +141,6 @@ def interpretation_edit(
interpretation: m.Interpretation = db.session.get(
m.Interpretation, interpretation_id
)
if interpretation and interpretation.user_id != current_user.id:
flash("You dont have permission to edit this interpretation", "danger")
return redirect(url_for("book.collection_view", book_id=book_id))
@ -205,15 +217,20 @@ def interpretation_delete(
delete_nested_interpretation_entities(interpretation)
log(log.INFO, "Delete interpretation [%s]", interpretation)
interpretation.save()
redirect_url = url_for(
"book.interpretation_view",
book_id=book_id,
section_id=interpretation.section_id,
)
# notifications
if current_user.id != interpretation.user_id:
interpretation_notification(
m.Notification.Actions.DELETE, interpretation.id, interpretation.user_id
)
# -------------
flash("Success!", "success")
return redirect(
url_for(
"book.interpretation_view",
book_id=book_id,
section_id=interpretation.section_id,
)
)
return redirect(redirect_url)
return redirect(
url_for(
"book.collection_view",

View File

@ -1,7 +1,8 @@
from flask import flash, redirect, url_for, request
from flask_login import login_required
from flask_login import login_required, current_user
from app.controllers import register_book_verify_route
from app.controllers.notification_producer import section_notification
from app.controllers.delete_nested_book_entities import delete_nested_section_entities
from app import models as m, db, forms as f
from app.controllers.require_permission import require_permission
@ -22,11 +23,6 @@ def section_create(book_id: int, collection_id: int):
collection: m.Collection = db.session.get(m.Collection, collection_id)
redirect_url = url_for("book.collection_view", book_id=book_id)
if collection_id:
redirect_url = url_for(
"book.collection_view",
book_id=book_id,
)
form = f.CreateSectionForm()
@ -52,6 +48,12 @@ def section_create(book_id: int, collection_id: int):
).save()
# -------------
if current_user.id != book.owner.id:
# notifications
section_notification(
m.Notification.Actions.CREATE, section.id, book.owner.id
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
else:
@ -73,6 +75,7 @@ def section_create(book_id: int, collection_id: int):
@login_required
def section_edit(book_id: int, section_id: int):
section: m.Section = db.session.get(m.Section, section_id)
book: m.Book = db.session.get(m.Book, book_id)
form = f.EditSectionForm()
redirect_url = url_for("book.collection_view", book_id=book_id)
@ -85,6 +88,11 @@ def section_edit(book_id: int, section_id: int):
log(log.INFO, "Edit section [%s]", section.id)
section.save()
if current_user.id != book.owner.id:
# notifications
section_notification(m.Notification.Actions.EDIT, section.id, book.owner.id)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
else:
@ -109,6 +117,8 @@ def section_delete(
section_id: int,
):
section: m.Section = db.session.get(m.Section, section_id)
book: m.Book = db.session.get(m.Book, book_id)
redirect_url = url_for("book.collection_view", book_id=book_id)
section.is_deleted = True
delete_nested_section_entities(section)
@ -123,8 +133,13 @@ def section_delete(
log(log.INFO, "Delete section [%s]", section.id)
section.save()
if current_user.id != book.owner.id:
# notifications
section_notification(m.Notification.Actions.DELETE, section.id, book.owner.id)
# -------------
flash("Success!", "success")
return redirect(url_for("book.collection_view", book_id=book_id))
return redirect(redirect_url)
@bp.route("/<int:book_id>/<int:section_id>/section/change_position", methods=["POST"])

View File

@ -7,9 +7,8 @@ from flask import (
)
from flask_login import login_required
from app.controllers import (
register_book_verify_route,
)
from app.controllers import register_book_verify_route
from app.controllers.notification_producer import contributor_notification
from app import models as m, db, forms as f
from app.controllers.require_permission import require_permission
from app.controllers.contributor import (
@ -52,6 +51,10 @@ def add_contributor(book_id: int):
form = f.AddContributorForm()
selected_tab = "user_permissions"
if form.validate_on_submit():
user_id = form.user_id.data
# notifications
contributor_notification(m.Notification.Actions.CONTRIBUTING, book_id, user_id)
# -------------
response = add_contributor_to_book(form, book_id, selected_tab)
return response
else:
@ -78,6 +81,10 @@ def delete_contributor(book_id: int):
selected_tab = "user_permissions"
if form.validate_on_submit():
user_id = form.user_id.data
# notifications
contributor_notification(m.Notification.Actions.DELETE, book_id, user_id)
# -------------
response = delete_contributor_from_book(form, book_id, selected_tab)
return response
else:

View File

@ -1,10 +1,12 @@
from flask import (
Blueprint,
render_template,
request,
)
from sqlalchemy import and_
from sqlalchemy import and_, func
from app import models as m, db
from app.logger import log
from app.controllers.sorting import sort_by
bp = Blueprint("home", __name__, url_prefix="/home")
@ -12,38 +14,88 @@ bp = Blueprint("home", __name__, url_prefix="/home")
@bp.route("/", methods=["GET"])
def get_all():
log(log.INFO, "Create query for home page for books")
books: m.Book = (
m.Book.query.filter_by(is_deleted=False).order_by(m.Book.id).limit(5)
).all()
log(log.INFO, "Create query for home page for interpretations")
sort = request.args.get("sort")
interpretations = (
db.session.query(
m.Interpretation,
m.Interpretation.score.label("score"),
m.Interpretation.created_at.label("created_at"),
func.count(m.Comment.interpretation_id).label("comments_count"),
)
.join(
m.Comment,
and_(
m.Comment.interpretation_id == m.Interpretation.id,
m.Comment.is_deleted == False, # noqa: E712
),
isouter=True,
)
.filter(
and_(
m.Section.id == m.Interpretation.section_id,
m.Collection.id == m.Section.collection_id,
m.BookVersion.id == m.Section.version_id,
m.Book.id == m.BookVersion.book_id,
m.Book.is_deleted == False, # noqa: E712
m.BookVersion.is_deleted == False, # noqa: E712
m.Interpretation.is_deleted == False, # noqa: E712
m.Section.is_deleted == False, # noqa: E712
m.Collection.is_deleted == False, # noqa: E712
)
m.Interpretation.is_deleted == False, # noqa: E712
)
.order_by(m.Interpretation.created_at.desc())
.limit(5)
.all()
.group_by(m.Interpretation.id)
)
log(log.INFO, "Returning data to front end")
pagination, interpretations = sort_by(interpretations, sort)
return render_template(
"home/index.html",
books=books,
interpretations=interpretations,
page=pagination,
)
@bp.route("/explore_books", methods=["GET"])
def explore_books():
log(log.INFO, "Create query for home page for books")
sort = request.args.get("sort")
books: m.Book = (
db.session.query(
m.Book,
m.Book.created_at.label("created_at"),
func.count(m.Interpretation.id).label("interpretations_count"),
func.count(m.BookStar.id).label("stars_count"),
)
.join(
m.BookStar,
and_(
m.BookStar.book_id == m.Book.id,
m.BookStar.is_deleted == False, # noqa: E712
),
full=True,
)
.join(
m.BookVersion,
and_(
m.BookVersion.book_id == m.Book.id,
m.BookVersion.is_deleted == False, # noqa: E712
),
)
.join(
m.Section,
and_(
m.BookVersion.id == m.Section.version_id,
m.Section.is_deleted == False, # noqa: E712
),
full=True,
)
.join(
m.Interpretation,
and_(
m.Interpretation.section_id == m.Section.id,
m.Interpretation.is_deleted == False, # noqa: E712
),
full=True,
)
.filter(
m.Book.is_deleted == False, # noqa: E712
)
.group_by(m.Book.id)
)
log(log.INFO, "Creating pagination for books")
pagination, books = sort_by(books, sort)
return render_template(
"home/explore_books.html",
books=books,
page=pagination,
)

View File

@ -0,0 +1,2 @@
# flake8: noqa F401
from .notifications import bp

View File

@ -0,0 +1,53 @@
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required, current_user
from app.controllers import (
create_pagination,
)
from app import models as m, db
from app.logger import log
bp = Blueprint("notifications", __name__, url_prefix="/notifications")
@bp.route("/all", methods=["GET"])
@login_required
def get_all():
log(log.INFO, "Create query for notifications")
notifications: m.Notification = m.Notification.query.filter_by(
user_id=current_user.id
).order_by(m.Notification.created_at.desc())
log(log.INFO, "Create pagination for books")
pagination = create_pagination(total=notifications.count())
log(log.INFO, "Returning data for front end")
return render_template(
"notifications/index.html",
notifications=notifications.paginate(
page=pagination.page, per_page=pagination.per_page
),
page=pagination,
)
@bp.route("/<int:notification_id>/mark_as_read", methods=["GET"])
@login_required
def mark_as_read(notification_id: int):
notification: m.Notification = db.session.get(m.Notification, notification_id)
notification.is_read = True
notification.save()
return redirect(notification.link)
@bp.route("/mark_all_as_read", methods=["GET"])
@login_required
def mark_all_as_read():
for notification in current_user.notifications:
notification.is_read = True
notification.save(False)
db.session.commit()
return redirect(url_for("notifications.get_all"))

View File

@ -1,12 +1,12 @@
from flask import (
Blueprint,
jsonify,
request,
)
from flask import Blueprint, jsonify, request
from flask_login import login_required, current_user
from app import models as m, db
from app.logger import log
from app.controllers.notification_producer import (
interpretation_notification,
comment_notification,
)
bp = Blueprint("vote", __name__, url_prefix="/vote")
@ -55,6 +55,13 @@ def vote_interpretation(interpretation_id: int):
interpretation,
)
db.session.commit()
interpretation.score = interpretation.vote_count
interpretation.save()
# notifications
if current_user.id != interpretation.user_id:
interpretation_notification(
m.Notification.Actions.VOTE, interpretation_id, interpretation.user_id
)
return jsonify(
{
@ -106,7 +113,9 @@ def vote_comment(comment_id: int):
comment,
)
db.session.commit()
# notifications
if current_user.id != comment.user_id:
comment_notification(m.Notification.Actions.VOTE, comment_id, comment.user_id)
return jsonify(
{
"vote_count": comment.vote_count,

View File

@ -0,0 +1,104 @@
"""add_fields_to_notification
Revision ID: 3d45df38ffa9
Revises: 8f9233babba4
Create Date: 2023-06-09 17:23:32.809580
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "3d45df38ffa9"
down_revision = "8f9233babba4"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
action = postgresql.ENUM(
"CREATE",
"EDIT",
"DELETE",
"VOTE",
"APPROVE",
"CONTRIBUTING",
"MENTION",
name="actions",
)
action.create(op.get_bind())
entity = postgresql.ENUM(
"SECTION",
"COLLECTION",
"INTERPRETATION",
"COMMENT",
"BOOK",
name="entities",
)
entity.create(op.get_bind())
with op.batch_alter_table("notifications", schema=None) as batch_op:
batch_op.add_column(
sa.Column(
"action",
sa.Enum(
"CREATE",
"EDIT",
"DELETE",
"VOTE",
"APPROVE",
"CONTRIBUTING",
"MENTION",
name="actions",
),
nullable=True,
)
)
batch_op.add_column(
sa.Column(
"entity",
sa.Enum(
"SECTION",
"COLLECTION",
"INTERPRETATION",
"COMMENT",
"BOOK",
name="entities",
),
nullable=True,
)
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
action = postgresql.ENUM(
"CREATE",
"EDIT",
"DELETE",
"VOTE",
"APPROVE",
"CONTRIBUTING",
"MENTION",
name="actions",
)
action.drop(op.get_bind())
entity = postgresql.ENUM(
"SECTION",
"COLLECTION",
"INTERPRETATION",
"COMMENT",
"BOOK",
name="entities",
)
entity.drop(op.get_bind())
with op.batch_alter_table("notifications", schema=None) as batch_op:
batch_op.drop_column("entity")
batch_op.drop_column("action")
# ### end Alembic commands ###

View File

@ -0,0 +1,42 @@
"""notifications
Revision ID: 8f9233babba4
Revises: 96995454b90d
Create Date: 2023-06-08 14:49:51.600531
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "8f9233babba4"
down_revision = "a41f004cad1a"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"notifications",
sa.Column("link", sa.String(length=256), nullable=False),
sa.Column("text", sa.String(length=256), nullable=False),
sa.Column("is_read", sa.Boolean(), nullable=True),
sa.Column("user_id", sa.Integer(), nullable=True),
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("is_deleted", sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(
["user_id"],
["users.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("notifications")
# ### end Alembic commands ###

View File

@ -0,0 +1,32 @@
"""entity_id
Revision ID: ad0ed27f417f
Revises: 3d45df38ffa9
Create Date: 2023-06-12 12:03:44.954134
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "ad0ed27f417f"
down_revision = "3d45df38ffa9"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("notifications", schema=None) as batch_op:
batch_op.add_column(sa.Column("entity_id", sa.Integer(), nullable=False))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("notifications", schema=None) as batch_op:
batch_op.drop_column("entity_id")
# ### end Alembic commands ###

View File

@ -0,0 +1,34 @@
"""score
Revision ID: f104cc0131c5
Revises: ad0ed27f417f
Create Date: 2023-06-13 17:04:08.590895
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "f104cc0131c5"
down_revision = "ad0ed27f417f"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.add_column(
sa.Column("score", sa.Integer(), nullable=True, server_default="0")
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("interpretations", schema=None) as batch_op:
batch_op.drop_column("score")
# ### end Alembic commands ###

View File

@ -0,0 +1,10 @@
export function activeNotifications() {
const notificationButton = document.querySelector(
'#dropdownNotificationButton',
);
if (notificationButton) {
notificationButton.addEventListener('click', () => {
console.log('CLICK');
});
}
}

View File

@ -35,6 +35,7 @@ import {initRefreshAccessLevelTree} from './refreshAccessLevelTree';
import {deleteContributor} from './deleteContributor';
import {initUnsavedChangedAlerts} from './unsavedChangedAlert';
import {initVersions} from './versions';
import {activeNotifications} from './activeNotifications';
initQuillReadOnly();
initBooks();
@ -73,3 +74,4 @@ initRefreshAccessLevelTree();
deleteContributor();
initUnsavedChangedAlerts();
initVersions();
activeNotifications();

View File

@ -37,3 +37,7 @@
.mt-135 {
margin-top: 135px;
}
.disabled {
pointer-events: none;
cursor: default;
}

View File

@ -924,15 +924,6 @@ def test_crud_interpretation(client: FlaskClient):
assert deleted_interpretation.is_deleted
check_if_nested_interpretation_entities_is_deleted(deleted_interpretation)
response: Response = client.post(
(
f"/book/{book.id}/{section_in_collection.interpretations[0].id}/delete_interpretation"
),
data=dict(interpretation_id=section_in_subcollection.interpretations[0].id),
follow_redirects=True,
)
assert response.status_code == 200
def test_crud_comment(client: FlaskClient, runner: FlaskCliRunner):
_, user = login(client)

186
tests/test_notifications.py Normal file
View File

@ -0,0 +1,186 @@
from flask import Response
from flask.testing import FlaskClient
from app import models as m, db
from tests.utils import (
login,
create_book,
create_collection,
create_section,
create_interpretation,
create_comment,
create,
logout,
)
def test_notifications(client: FlaskClient):
_, user = login(client)
user: m.User
assert user.id
book = create_book(client)
collection, _ = create_collection(client, book.id)
section, _ = create_section(client, book.id, collection.id)
interpretation, _ = create_interpretation(client, book.id, section.id)
user_2_id = create(username="user_2")
user_2: m.User = db.session.get(m.User, user_2_id.id)
assert user_2
response: Response = client.post(
f"/book/{book.id}/add_contributor",
data=dict(user_id=user_2.id, role=m.BookContributor.Roles.MODERATOR),
follow_redirects=True,
)
assert response.status_code == 200
logout(client)
login(client, user_2.username)
comment, _ = create_comment(client, book.id, interpretation.id)
assert comment
assert comment.user_id == user_2_id.id
assert len(user.active_notifications) == 1
logout(client)
login(client)
response: Response = client.post(
f"/approve/comment/{comment.id}",
follow_redirects=True,
)
assert response.status_code == 200
response: Response = client.post(
f"/book/{book.id}/delete_contributor",
data=dict(user_id=user_2_id.id),
follow_redirects=True,
)
assert response.status_code == 200
# check that user_2 have notification about he was added, deleted as Editor/Moderator and his comment was approved
assert len(user_2.active_notifications) == 3
response: Response = client.post(
f"/book/{book.id}/add_contributor",
data=dict(user_id=user_2.id, role=m.BookContributor.Roles.EDITOR),
follow_redirects=True,
)
assert response.status_code == 200
assert len(user_2.active_notifications) == 4
logout(client)
login(client, user_2.username)
collection_2, _ = create_collection(client, book.id)
assert collection_2
assert len(user.active_notifications) == 2
response: Response = client.post(
f"/book/{book.id}/{collection_2.id}/edit",
data=dict(label="Test Collection #1 Label"),
follow_redirects=True,
)
assert response.status_code == 200
section_2, _ = create_section(client, book.id, collection_2.id)
interpretation_2, _ = create_interpretation(client, book.id, section_2.id)
comment_2, _ = create_comment(client, book.id, interpretation_2.id)
assert interpretation_2
assert len(user.active_notifications) == 5
logout(client)
login(client)
response: Response = client.post(
f"/book/{book.id}/{interpretation_2.id}/comment_delete",
data=dict(
text=comment.text,
interpretation_id=interpretation_2.id,
comment_id=comment_2.id,
),
follow_redirects=True,
)
response.status_code == 200
assert len(user_2.active_notifications) == 5
response: Response = client.post(
f"/approve/interpretation/{interpretation_2.id}",
follow_redirects=True,
)
response.status_code == 200
assert len(user_2.active_notifications) == 6
response: Response = client.post(
f"/book/{book.id}/{interpretation_2.id}/delete_interpretation",
data=dict(interpretation_id=interpretation_2.id),
follow_redirects=True,
)
response.status_code == 200
assert len(user_2.active_notifications) == 7
logout(client)
login(client, user_2.username)
response: Response = client.post(
f"/book/{book.id}/{section_2.id}/edit_section",
data=dict(
section_id=section_2.id,
label="Test",
),
follow_redirects=True,
)
assert response.status_code == 200
assert len(user.active_notifications) == 6
response: Response = client.post(
f"/book/{book.id}/{section_2.id}/delete_section",
follow_redirects=True,
)
assert response.status_code == 200
response: Response = client.post(
f"/book/{book.id}/{collection_2.id}/delete",
follow_redirects=True,
)
assert response.status_code == 200
assert len(user.active_notifications) == 8
strings = [
"New interpretation to%",
"%renamed a section on%",
"%create a new section on%",
"%delete a section on%",
"%create a new collection on%",
"%renamed a collection on%",
"%delete a collection on%",
"New comment to your interpretation",
]
for string in strings:
notification = m.Notification.query.filter(
m.Notification.text.ilike(f"{string}")
).first()
assert notification
assert notification.user_id == user.id
strings_for_user_2 = [
"You've been added to%",
"Your comment has been approved%",
"You've been removed from%",
"A moderator has removed your comment",
"Your interpretation has been approved%",
"A moderator has removed your interpretation",
]
for string in strings_for_user_2:
notification = m.Notification.query.filter(
m.Notification.text.ilike(f"{string}")
).first()
assert notification
assert notification.user_id == user_2.id
response: Response = client.get(
"/notifications/mark_all_as_read",
follow_redirects=True,
)
assert response.status_code == 200
assert len(user_2.active_notifications) == 0

View File

@ -2,7 +2,13 @@ from flask import current_app as Response
from flask.testing import FlaskClient
from app import models as m
from tests.utils import login
from tests.utils import (
login,
create_interpretation,
create_section,
create_book,
create_collection,
)
def test_upvote_interpretation(client: FlaskClient):
@ -21,10 +27,17 @@ def test_upvote_interpretation(client: FlaskClient):
assert response.status_code == 404
assert response.json["message"] == "Interpretation not found"
interpretation = m.Interpretation(
text="Test Interpretation 1 Text",
user_id=user.id,
).save()
book = create_book(client)
assert book
collection, _ = create_collection(client=client, book_id=book.id)
assert collection
section, _ = create_section(
client=client, book_id=book.id, collection_id=collection.id
)
assert section
interpretation, _ = create_interpretation(
client=client, book_id=book.id, section_id=section.id
)
assert interpretation.vote_count == 0