mirror of
https://github.com/logos-co/open-law.git
synced 2025-02-12 23:06:30 +00:00
Merge branch 'develop' into svyat/feat/fork
This commit is contained in:
commit
ed6df92f45
@ -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
|
||||
|
285
app/controllers/notification_producer.py
Normal file
285
app/controllers/notification_producer.py
Normal 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
|
||||
)
|
26
app/controllers/sorting.py
Normal file
26
app/controllers/sorting.py
Normal 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
|
@ -25,3 +25,4 @@ from .permission import (
|
||||
)
|
||||
from .book_tag import BookTags
|
||||
from .section_tag import SectionTag
|
||||
from .notification import Notification
|
||||
|
@ -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]}>"
|
||||
|
@ -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")
|
||||
|
37
app/models/notification.py
Normal file
37
app/models/notification.py
Normal 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
|
@ -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
@ -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 %}
|
||||
|
||||
|
@ -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 %}
|
@ -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>
|
||||
|
37
app/templates/book/components/header_buttons.html
Normal file
37
app/templates/book/components/header_buttons.html
Normal 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>
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
73
app/templates/home/explore_books.html
Normal file
73
app/templates/home/explore_books.html
Normal 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 %}
|
@ -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 %}
|
||||
|
24
app/templates/notification.html
Normal file
24
app/templates/notification.html
Normal 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 %}
|
86
app/templates/notifications/index.html
Normal file
86
app/templates/notifications/index.html
Normal 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 %}
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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
|
||||
|
@ -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})
|
||||
|
@ -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=[])
|
||||
|
@ -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"]
|
||||
)
|
||||
|
@ -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")
|
||||
|
@ -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",
|
||||
|
@ -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"])
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
)
|
||||
|
2
app/views/notifications/__init__.py
Normal file
2
app/views/notifications/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# flake8: noqa F401
|
||||
from .notifications import bp
|
53
app/views/notifications/notifications.py
Normal file
53
app/views/notifications/notifications.py
Normal 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"))
|
@ -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,
|
||||
|
104
migrations/versions/3d45df38ffa9_add_fields_to_notification.py
Normal file
104
migrations/versions/3d45df38ffa9_add_fields_to_notification.py
Normal 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 ###
|
42
migrations/versions/8f9233babba4_notifications.py
Normal file
42
migrations/versions/8f9233babba4_notifications.py
Normal 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 ###
|
32
migrations/versions/ad0ed27f417f_entity_id.py
Normal file
32
migrations/versions/ad0ed27f417f_entity_id.py
Normal 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 ###
|
34
migrations/versions/f104cc0131c5_score.py
Normal file
34
migrations/versions/f104cc0131c5_score.py
Normal 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 ###
|
10
src/activeNotifications.ts
Normal file
10
src/activeNotifications.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export function activeNotifications() {
|
||||
const notificationButton = document.querySelector(
|
||||
'#dropdownNotificationButton',
|
||||
);
|
||||
if (notificationButton) {
|
||||
notificationButton.addEventListener('click', () => {
|
||||
console.log('CLICK');
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -37,3 +37,7 @@
|
||||
.mt-135 {
|
||||
margin-top: 135px;
|
||||
}
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
@ -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
186
tests/test_notifications.py
Normal 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
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user