diff --git a/app/__init__.py b/app/__init__.py index 8edbf1a..c6ba42c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -26,8 +26,9 @@ from app import models # IDK why this is here, but it works from app.models.comment import Comment from app.models.thread import Thread from app.models.forum import Forum +from app.models.notification import Notification from app.routes import index, search, users # To load routes at initialization -from app.routes.account import login, account +from app.routes.account import login, account, notification from app.routes.admin import index, groups, account, trophies from app.utils import pluralize # To use pluralize into the templates from app.utils import is_title diff --git a/app/data/groups.yaml b/app/data/groups.yaml index d569a53..8e99afd 100644 --- a/app/data/groups.yaml +++ b/app/data/groups.yaml @@ -11,6 +11,7 @@ shoutbox-kick shoutbox-ban unlimited-pms footer-statistics community-login access-admin-panel edit-account delete-account edit-trophies + delete_notification - name: Modérateur css: "color: green" diff --git a/app/models/notification.py b/app/models/notification.py new file mode 100644 index 0000000..f2830e8 --- /dev/null +++ b/app/models/notification.py @@ -0,0 +1,23 @@ +from app import db +from datetime import datetime + +class Notification(db.Model): + """ A long-term `flash` notification. It is deleted when watched """ + __tablename__ = 'notification' + + id = db.Column(db.Integer, primary_key=True) + + text = db.Column(db.UnicodeText) + href = db.Column(db.UnicodeText) + date = db.Column(db.DateTime, default=datetime.now()) + + owner_id = db.Column(db.Integer, db.ForeignKey('member.id'), nullable=False) + + def __init__(self, owner_id, text, href=None): + """ Check weather or not the id is valid before creating the notif! """ + self.text = text + self.href = href + self.owner_id = owner_id + + def __repr__(self): + return f'' diff --git a/app/models/users.py b/app/models/users.py index d55eeac..21d65c2 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -5,7 +5,9 @@ from flask_login import UserMixin from app.models.privs import SpecialPrivilege, Group, GroupMember, \ GroupPrivilege from app.models.trophies import Trophy, TrophyMember +from app.models.notification import Notification import app.utils.unicode_names as unicode_names +from app.utils.notify import notify from config import V5Config import werkzeug.security @@ -96,6 +98,7 @@ class Member(User): newsletter = db.Column(db.Boolean, default=False) # Relations + notifications = db.relationship('Notification', backref="owner", lazy=True) trophies = db.relationship('Trophy', secondary=TrophyMember, back_populates='owners') @@ -207,6 +210,14 @@ class Member(User): return werkzeug.security.check_password_hash(self.password_hash, password) + def notify(self, message, href=None): + """ Notify a user with a message. + An hyperlink can be added to redirect to the notification source """ + + n = Notification(self.id, message, href=href) + db.session.add(n) + db.session.commit() + def add_trophy(self, t): """ Add a trophy to the current user. Check whether the request sender has @@ -218,8 +229,7 @@ class Member(User): t = Trophy.query.filter_by(name=t).first() if t not in self.trophies: self.trophies.append(t) - # TODO: implement the notification system - # self.notify(f"Vous venez de débloquer le trophée '{t.name}'") + self.notify(f"Vous avez débloqué le trophée '{t.name}'") def del_trophy(self, t): """ diff --git a/app/routes/account/notification.py b/app/routes/account/notification.py new file mode 100644 index 0000000..2b3a3b9 --- /dev/null +++ b/app/routes/account/notification.py @@ -0,0 +1,42 @@ +from flask import redirect, url_for, request, flash +from flask_login import login_required, current_user +from app import app, db +from app.models.notification import Notification +from app.utils.render import render + + +@app.route('/notifications', methods=['GET']) +@login_required +def list_notifications(): + notifications = current_user.notifications + return render('account/notifications.html', notifications=notifications) + + +@app.route('/notifications/delete/', methods=['GET']) +@login_required +def delete_notification(id=None): + if type(id) == int: + notification = Notification.query.get(id) + if notification: + # Only current user or admin can delete notifications + if notification.owner_id == current_user.id: + db.session.delete(notification) + db.session.commit() + return redirect(url_for('list_notifications')) + elif 'delete_notification' in current_user.privs: + db.session.delete(notification) + db.session.commit() + # TODO: change this redirection + return redirect(url_for('list_notifications')) + else: + abort(403) + abort(404) + elif id == "all": + for n in current_user.notifications: + db.session.delete(n) + db.session.commit() + return redirect(url_for('list_notifications')) + # TODO: add something to allow an admin to delete all notifs for a user + # with a GET parameter + else: + abort(404) diff --git a/app/routes/admin/account.py b/app/routes/admin/account.py index d75faac..b5ace4a 100644 --- a/app/routes/admin/account.py +++ b/app/routes/admin/account.py @@ -1,4 +1,5 @@ from flask import flash, redirect, url_for +from flask_login import current_user from wtforms import BooleanField from app.utils.priv_required import priv_required from app.models.users import Member @@ -6,6 +7,7 @@ from app.models.trophies import Trophy from app.forms.account import AdminUpdateAccountForm, AdminDeleteAccountForm, \ AdminAccountEditTrophyForm from app.utils.render import render +from app.utils.notify import notify from app import app, db @@ -47,6 +49,7 @@ def adm_edit_account(user_id): db.session.merge(user) db.session.commit() # TODO: send an email to member saying his account has been modified + user.notify(f"Vos informations personnelles ont été modifiées par {current_user.name}.") flash('Modifications effectuées', 'ok') else: flash('Erreur lors de la modification', 'error') diff --git a/app/templates/account/notifications.html b/app/templates/account/notifications.html new file mode 100644 index 0000000..3da5afc --- /dev/null +++ b/app/templates/account/notifications.html @@ -0,0 +1,33 @@ +{% extends "base/base.html" %} + +{% block title %} +

Notifications

+{% endblock %} + +{% block content %} +
+ {% if notifications %} + + + + + + + {% for n in notifications %} + + + + + {% endfor %} +
DateNotificationTout supprimer
{{ n.date.strftime('Le %Y-%m-%d à %H:%M') }} + {% if n.href %}{% endif %} + {{ n.text }} + {% if n.href %}{% endif %} + Supprimer +
+ {% else %} + Aucune notification. + {% endif %} + +
+{% endblock %} diff --git a/app/templates/base/navbar/account.html b/app/templates/base/navbar/account.html index 3921c3f..fb2e481 100644 --- a/app/templates/base/navbar/account.html +++ b/app/templates/base/navbar/account.html @@ -6,7 +6,7 @@ {{ current_user.name }} - + Notifications diff --git a/app/utils/notify.py b/app/utils/notify.py new file mode 100644 index 0000000..2a98dc8 --- /dev/null +++ b/app/utils/notify.py @@ -0,0 +1,20 @@ +from app import db +from app.models.notification import Notification +# from app.models.users import Member + +def notify(user, message, href=None): + """ Notify a user (by id, name or object reference) with a message. + An hyperlink can be added to redirect to the notification source """ + + # Cuz' duck typing is quite cool + # TODO: maybe abort if no user is found + if type(user) == str: + user = Member.query.filter_by(name=user).first() + if isinstance(user, Member): + user = user.id + if user and Member.query.get(user): + n = Notification(user, message, href=href) + db.session.add(n) + db.session.commit() + else: + print("User not found") diff --git a/migrations/versions/ebca7362eb22_ajout_des_notifications.py b/migrations/versions/ebca7362eb22_ajout_des_notifications.py new file mode 100644 index 0000000..755f652 --- /dev/null +++ b/migrations/versions/ebca7362eb22_ajout_des_notifications.py @@ -0,0 +1,36 @@ +"""Ajout des notifications + +Revision ID: ebca7362eb22 +Revises: e3b140752719 +Create Date: 2019-09-01 11:36:25.962212 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ebca7362eb22' +down_revision = 'e3b140752719' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('notification', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('text', sa.UnicodeText(), nullable=True), + sa.Column('href', sa.UnicodeText(), nullable=True), + sa.Column('date', sa.DateTime(), nullable=True), + sa.Column('owner_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['owner_id'], ['member.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('notification') + # ### end Alembic commands ###