start of notifications (table,triggers,connect to front)

This commit is contained in:
Kostiantyn Stoliarskyi 2023-06-08 15:22:32 +03:00
parent f098514cbc
commit 999fbe1951
14 changed files with 344 additions and 51 deletions

View File

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

View File

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

View File

@ -0,0 +1,14 @@
from app.models.utils import BaseModel
from app import db
class Notification(BaseModel):
__tablename__ = "notifications"
link = db.Column(db.String(256), unique=False, nullable=False)
text = db.Column(db.String(256), unique=False, nullable=False)
is_read = db.Column(db.Boolean, default=False)
# Foreign keys
user_id = db.Column(db.ForeignKey("users.id")) # for what user notification is
# Relationships
user = db.relationship("User", viewonly=True) # for what user notification is

View File

@ -31,6 +31,7 @@ class User(BaseModel, UserMixin):
)
stars = db.relationship("Book", secondary="books_stars", back_populates="stars")
books = db.relationship("Book")
notifications = db.relationship("Notification")
@hybrid_property
def password(self):

File diff suppressed because one or more lines are too long

View File

@ -40,9 +40,11 @@
<!-- prettier-ignore -->
<button id="dropdownNotificationButton" data-dropdown-toggle="dropdownNotification" class="inline-flex items-center text-sm font-medium text-center text-gray-500 hover:text-gray-900 focus:outline-none dark:hover:text-white dark:text-gray-400" type="button">
<svg class="w-6 h-6" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> <path d="M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z"></path> </svg>
{% if current_user.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,25 +91,7 @@
<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>
{% include 'notification.html' %}
<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">
<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 File

@ -0,0 +1,23 @@
<!-- prettier-ignore -->
{% if not current_user.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.notifications[:5]%}
<a
href="{{notification.link}}"
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">
{{notification.text}}
</div>
</div>
</a>
{% endfor %}

View File

@ -1,6 +1,7 @@
from flask import (
Blueprint,
jsonify,
url_for,
)
from flask_login import login_required, current_user
@ -25,6 +26,8 @@ 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)
section: m.Section = db.session.get(m.Section, interpretation.section_id)
if not interpretation:
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
return jsonify({"message": "Interpretation not found"}), 404
@ -55,6 +58,40 @@ def approve_interpretation(interpretation_id: int):
"Approve" if interpretation.approved else "Cancel approve",
interpretation,
)
if (
interpretation.approved
and current_user.id != book.owner.id
and current_user.id != interpretation.user_id
):
# notifications
redirect_url = url_for(
"book.interpretation_view", book_id=book.id, section_id=section.id
)
notification_text = f"{current_user.username} approved an interpretation for {section.label} on {book.label}"
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
elif interpretation.approved and current_user.id != interpretation.user_id:
# Your interpretation has been approved for SectionLabel on BookLabel
notification_text = (
f"Your interpretation has been approved for {section.label} on {book.label}"
)
m.Notification(
link=redirect_url, text=notification_text, user_id=interpretation.user_id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
# -------------
interpretation.save()
return jsonify({"message": "success", "approve": interpretation.approved})
@ -72,6 +109,8 @@ 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)
section: m.Section = db.session.get(m.Section, comment.interpretation.section_id)
if not comment:
log(log.WARNING, "Comment with id [%s] not found", comment_id)
return jsonify({"message": "Comment not found"}), 404
@ -84,6 +123,37 @@ def approve_comment(comment_id: int):
"Approve" if comment.approved else "Cancel approve",
comment,
)
if (
comment.approved
and current_user.id != comment.book.owner.id
and current_user.id != comment.user_id
):
# notifications
redirect_url = url_for(
"book.qa_view",
book_id=comment.book.id,
interpretation_id=comment.interpretation_id,
)
notification_text = f"{current_user.username} approved an comment for {section.label} on {book.label}"
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
elif comment.approved and current_user.id != comment.user_id:
# Your interpretation has been approved for SectionLabel on BookLabel
notification_text = (
f"Your interpretation has been approved for {section.label} on {book.label}"
)
m.Notification(
link=redirect_url, text=notification_text, user_id=comment.user_id
).save()
log(log.INFO, "Create notification for user with id [%s]", comment.user_id)
# -------------
comment.save()
return jsonify({"message": "success", "approve": comment.approved})

View File

@ -1,5 +1,5 @@
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,
@ -107,6 +107,22 @@ 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)
# notifications
if current_user.id != book.owner.id:
notification_text = (
f"{current_user.username} added a collection on {book.label}"
)
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
# -------------
collection.save()
for access_group in collection.parent.access_groups:
@ -176,6 +192,21 @@ 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:
notification_text = (
f"{current_user.username} renamed a collection on {book.label}"
)
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
else:
@ -197,6 +228,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 +244,31 @@ 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:
notification_text = (
f"{current_user.username} deleted a collection on {book.label}"
)
)
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
# TODO permission check
# @require_permission(
# entity_type=m.Permission.Entity.COLLECTION,
# access=[m.Permission.Access.C],
# entities=[m.Collection, m.Book],
# )
@require_permission(
entity_type=m.Permission.Entity.COLLECTION,
access=[m.Permission.Access.U],
entities=[m.Collection, m.Book],
)
@bp.route(
"/<int:book_id>/<int:collection_id>/collection/change_position", methods=["POST"]
)

View File

@ -59,6 +59,21 @@ def create_comment(
)
comment.save()
# notifications
if current_user.id != book.owner.id:
notification_text = "New comment to your interpretation"
m.Notification(
link=redirect_url,
text=notification_text,
user_id=interpretation.user_id,
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
interpretation.user_id,
)
# -------------
tags = current_app.config["TAG_REGEX"].findall(text)
set_comment_tags(comment, tags)
# TODO Send notifications
@ -86,6 +101,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 +111,19 @@ 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:
notification_text = "A moderator has removed your comment"
m.Notification(
link=redirect_url, text=notification_text, user_id=comment.user_id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
comment.user_id,
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
flash("Invalid id!", "danger")

View File

@ -66,6 +66,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 +103,19 @@ def interpretation_create(
).save()
# -------------
# notifications
if current_user.id != book.owner.id:
notification_text = f"New interpretation to {section.label} on {book.label}"
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
# -------------
tags = current_app.config["TAG_REGEX"].findall(text)
set_interpretation_tags(interpretation, tags)
@ -128,7 +142,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 +218,28 @@ def interpretation_delete(
delete_nested_interpretation_entities(interpretation)
log(log.INFO, "Delete interpretation [%s]", interpretation)
interpretation.save()
flash("Success!", "success")
return redirect(
url_for(
# notifications
if current_user.id != interpretation.user_id:
redirect_url = url_for(
"book.interpretation_view",
book_id=book_id,
section_id=interpretation.section_id,
)
)
notification_text = "A moderator has removed your interpretation"
m.Notification(
link=redirect_url,
text=notification_text,
user_id=interpretation.user_id,
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
interpretation.user_id,
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
return redirect(
url_for(
"book.collection_view",

View File

@ -1,5 +1,5 @@
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.delete_nested_book_entities import delete_nested_section_entities
@ -22,11 +22,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 +47,20 @@ def section_create(book_id: int, collection_id: int):
).save()
# -------------
if current_user.id != book.owner.id:
# notifications
notification_text = (
f"{current_user.username} create a section on {book.label}"
)
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id[%s]",
book.owner.id,
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
else:
@ -73,6 +82,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 +95,21 @@ 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
notification_text = (
f"{current_user.username} renamed a section on {book.label}"
)
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id[%s]",
book.owner.id,
)
# -------------
flash("Success!", "success")
return redirect(redirect_url)
else:
@ -109,6 +134,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 +150,21 @@ def section_delete(
log(log.INFO, "Delete section [%s]", section.id)
section.save()
if current_user.id != book.owner.id:
# notifications
notification_text = f"{current_user.username} deleted a section on {book.label}"
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id[%s]",
book.owner.id,
)
# -------------
flash("Success!", "success")
return redirect(url_for("book.collection_view", book_id=book_id))
return redirect(redirect_url)
@bp.route("/<int:book_id>/<int:section_id>/section/change_position", methods=["POST"])

View File

@ -1,8 +1,4 @@
from flask import (
Blueprint,
jsonify,
request,
)
from flask import Blueprint, jsonify, request, url_for
from flask_login import login_required, current_user
from app import models as m, db
@ -23,6 +19,7 @@ def vote_interpretation(interpretation_id: int):
if not interpretation:
log(log.WARNING, "Interpretation with id [%s] not found", interpretation_id)
return jsonify({"message": "Interpretation not found"}), 404
book: m.Book = db.session.get(m.Book, interpretation.book.id)
vote: m.InterpretationVote = m.InterpretationVote.query.filter_by(
user_id=current_user.id, interpretation_id=interpretation_id
@ -56,6 +53,24 @@ def vote_interpretation(interpretation_id: int):
)
db.session.commit()
# notifications
if current_user.id != book.owner.id:
redirect_url = url_for(
"book.interpretation_view",
book_id=book.id,
section_id=interpretation.section_id,
)
notification_text = f"{current_user.username} voted your interpretation"
m.Notification(
link=redirect_url, text=notification_text, user_id=book.owner.id
).save()
log(
log.INFO,
"Create notification for user with id [%s]",
book.owner.id,
)
# -------------
return jsonify(
{
"vote_count": interpretation.vote_count,

View File

@ -0,0 +1,38 @@
"""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 = '96995454b90d'
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 ###