Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Notifications mailing system #327

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions devops/lms.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ services:
- ./rabbitmq.cookie:/var/lib/rabbitmq/.erlang.cookie
networks:
- lms

mailer:
image: lms:latest
command: celery -A lms.utils worker
volumes:
- ../lms/utils/:/app_dir/lms/utils/
environment:
- CELERY_RABBITMQ_ERLANG_COOKIE=AAVyo5djdSMGIZXiwEQs3JeVaBx5l14z
- CELERY_RABBITMQ_DEFAULT_USER=rabbit-user
- CELERY_RABBITMQ_DEFAULT_PASS=YgKlCvnYVzpTa3T9adG3NrMoUNe4Z5aZ
- CELERY_MAILER_VHOST=utils
- CELERY_RABBITMQ_HOST=rabbitmq
- CELERY_RABBITMQ_PORT=5672
links:
- rabbitmq
depends_on:
- rabbitmq
networks:
- lms

checks-sandbox:
image: lms:latest
Expand Down
20 changes: 20 additions & 0 deletions lms/lmsdb/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ def _add_uuid_to_users_table(table: Model, _column: Field) -> None:
user.save()


def _add_mail_subscription_to_users_table(
table: Model, _column: Field,
) -> None:
log.info(
'Adding mail subscription for users, might take some extra time...',
)
with db_config.database.transaction():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not update query instead of iterating all users in the system? will be much faster than that

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And if its default true, are you sure you need this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to migrate it on my environment and it created the column with null for all the objects

for user in table:
user.mail_subscription = True
user.save()


def _api_keys_migration() -> bool:
User = models.User
_add_not_null_column(User, User.api_key, _add_api_keys_to_users_table)
Expand Down Expand Up @@ -316,6 +328,13 @@ def _uuid_migration() -> bool:
return True


def _mail_subscription() -> bool:
User = models.User
_add_not_null_column(
User, User.mail_subscription, _add_mail_subscription_to_users_table,
)


def _assessment_migration() -> bool:
Solution = models.Solution
_add_not_null_column(Solution, Solution.assessment)
Expand All @@ -335,6 +354,7 @@ def main():
_api_keys_migration()
_last_course_viewed_migration()
_uuid_migration()
_mail_subscription()

if models.database.table_exists(models.UserCourse.__name__.lower()):
_add_user_course_constaint()
Expand Down
36 changes: 36 additions & 0 deletions lms/lmsdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ class User(UserMixin, BaseModel):
api_key = CharField()
last_course_viewed = ForeignKeyField(Course, null=True)
uuid = UUIDField(default=uuid4, unique=True)
mail_subscription = BooleanField(default=True)

def get_id(self):
return str(self.uuid)
Expand Down Expand Up @@ -383,6 +384,41 @@ def on_notification_saved(
instance.delete_instance()


class MailMessage(BaseModel):
user = ForeignKeyField(User, backref='mails')
notification = ForeignKeyField(Notification, backref='mails')
date = DateTimeField(default=datetime.now)

@classmethod
def distincit_users(cls):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need distinct here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And please add types

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For each message there is an instance. I need distinct in order to get the distinct users, for sending only 1 message for each one of them.

return cls.select(cls.user).distinct()

@classmethod
def by_user(cls, user: User) -> Iterable['MailMessage']:
return (
cls
.select()
.where(cls.user == user)
.join(Notification)
.where(Notification.viewed == False) # NOQA: E712
)

@classmethod
def user_messages_number(cls, user: User) -> int:
return (
cls
.select(fn.Count(cls.id))
.where(cls.user == user)
.join(Notification)
.where(Notification.viewed == False) # NOQA: E712
.scalar()
)

@classmethod
def get_instances_number(cls):
return cls.select(fn.Count(cls.id))


class Exercise(BaseModel):
subject = CharField()
date = DateTimeField()
Expand Down
4 changes: 4 additions & 0 deletions lms/lmsweb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import typing

from flask import Flask
from flask_apscheduler import APScheduler # type: ignore
from flask_babel import Babel # type: ignore
from flask_httpauth import HTTPBasicAuth
from flask_limiter import Limiter # type: ignore
Expand Down Expand Up @@ -48,6 +49,9 @@

webmail = Mail(webapp)

webscheduler = APScheduler(app=webapp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this scheduler is part of lmsweb? is it running under the WSGI app?
if so, it's bad. you should use the celery scheduler and run specific daemon for that

webscheduler.start()


# Must import files after app's creation
from lms.lmsdb import models # NOQA: F401, E402, I202
Expand Down
4 changes: 4 additions & 0 deletions lms/lmsweb/config.py.example
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ LOCALE = 'en'
LIMITS_PER_MINUTE = 5
LIMITS_PER_HOUR = 50

# Scheduler
SCHEDULER_API_ENABLED = True
DEFAULT_DO_TASKS_EVERY_HOURS = 2

# Change password settings
MAX_INVALID_PASSWORD_TRIES = 5

Expand Down
86 changes: 61 additions & 25 deletions lms/lmsweb/translations/he/LC_MESSAGES/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.1\n"

#: lmsdb/models.py:921
#: lmsdb/models.py:951
msgid "Fatal error"
msgstr "כישלון חמור"

Expand Down Expand Up @@ -75,6 +75,22 @@ msgstr "קישור לאיפוס הסיסמה נשלח בהצלחה"
msgid "Reset password link is expired"
msgstr "קישור איפוס הסיסמה פג תוקף"

#: lmsweb/views.py:376 templates/user.html:23
msgid "Mail Subscription"
msgstr "מנוי להודעות דוא\"ל"

#: lmsweb/views.py:378
msgid "You've successfully subscribed to get mails for new notifications"
msgstr "נרשמת בהצלחה לקבלת הודעות דוא\"ל עבור התראות חדשות"

#: lmsweb/views.py:383
msgid "You've successfully unsubscribed to get mails for new notifications"
msgstr "ביטלת בהצלחה את ההרשמה לקבלת הודעות דוא\"ל עבור התראות חדשות"

#: lmsweb/views.py:388
msgid "Something went wrong..."
msgstr "משהו השתבש..."

#: lmsweb/forms/change_password.py:20 lmsweb/forms/register.py:32
#: lmsweb/forms/reset_password.py:25
msgid "The passwords are not identical"
Expand Down Expand Up @@ -124,7 +140,7 @@ msgstr "שם המשתמש או הסיסמה שהוזנו לא תקינים"
msgid "You have to confirm your registration with the link sent to your email"
msgstr "עליך לאשר את מייל האימות"

#: models/users.py:50
#: models/users.py:61
#, python-format
msgid "You are already registered to %(course_name)s course."
msgstr "אתה כבר רשום לקורס %(course_name)s."
Expand Down Expand Up @@ -302,7 +318,7 @@ msgstr "חמ\"ל תרגילים"
msgid "Name"
msgstr "שם"

#: templates/status.html:13 templates/user.html:47
#: templates/status.html:13 templates/user.html:53
msgid "Checked"
msgstr "נבדק/ו"

Expand All @@ -328,7 +344,7 @@ msgstr "העלאת מחברות"

#: templates/upload.html:11
msgid "Drag here the notebook file or click and choose it from your computer."
msgstr "גררו לכאן את קובץ המחברת, או לחצו ובחרו אותה מהמחשב שלכם."
msgstr "גררו לכאן את קובץ המחברת, או לחצו ובחרו אותה מהמחשב שלכם."

#: templates/upload.html:14
msgid "Back to Exercises List"
Expand All @@ -350,63 +366,63 @@ msgstr "פרטי משתמש"
msgid "Actions"
msgstr "פעולות"

#: templates/user.html:21
#: templates/user.html:27
msgid "Join Courses"
msgstr "הירשם לקורסים"

#: templates/user.html:27
#: templates/user.html:33
msgid "Exercises Submitted"
msgstr "תרגילים שהוגשו"

#: templates/user.html:32
#: templates/user.html:38
msgid "Course name"
msgstr "שם קורס"

#: templates/user.html:33
#: templates/user.html:39
msgid "Exercise name"
msgstr "שם תרגיל"

#: templates/user.html:34
#: templates/user.html:40
msgid "Submission status"
msgstr "מצב הגשה"

#: templates/user.html:35
#: templates/user.html:41
msgid "Submission"
msgstr "הגשה"

#: templates/user.html:36
#: templates/user.html:42
msgid "Checker"
msgstr "בודק"

#: templates/user.html:37 templates/view.html:25 templates/view.html:112
#: templates/user.html:43 templates/view.html:25 templates/view.html:112
msgid "Assessment"
msgstr "הערה מילולית"

#: templates/user.html:47
#: templates/user.html:53
msgid "Submitted"
msgstr "הוגש"

#: templates/user.html:47
#: templates/user.html:53
msgid "Not submitted"
msgstr "לא הוגש"

#: templates/user.html:59
#: templates/user.html:65
msgid "Notes"
msgstr "פתקיות"

#: templates/user.html:64 templates/user.html:66
#: templates/user.html:70 templates/user.html:72
msgid "New Note"
msgstr "פתקית חדשה"

#: templates/user.html:70
#: templates/user.html:76
msgid "Related Exercise"
msgstr "תרגיל משויך"

#: templates/user.html:79
#: templates/user.html:85
msgid "Privacy Level"
msgstr "רמת פרטיות"

#: templates/user.html:85
#: templates/user.html:91
msgid "Add Note"
msgstr "הוסף פתקית"

Expand Down Expand Up @@ -478,12 +494,12 @@ msgstr "הערות בודק"
msgid "Done Checking"
msgstr "סיום בדיקה"

#: utils/mail.py:25
#: utils/mail.py:30
#, python-format
msgid "Confirmation mail - %(site_name)s"
msgstr "מייל אימות - %(site_name)s"

#: utils/mail.py:32
#: utils/mail.py:37
#, python-format
msgid ""
"Hello %(fullname)s,\n"
Expand All @@ -492,12 +508,12 @@ msgstr ""
"שלום %(fullname)s,\n"
"לינק האימות שלך למערכת הוא: %(link)s"

#: utils/mail.py:42
#: utils/mail.py:47
#, python-format
msgid "Reset password mail - %(site_name)s"
msgstr "מייל איפוס סיסמה - %(site_name)s"

#: utils/mail.py:49
#: utils/mail.py:54
#, python-format
msgid ""
"Hello %(fullname)s,\n"
Expand All @@ -506,12 +522,12 @@ msgstr ""
"שלום %(fullname)s,\n"
"לינק לצורך איפוס הסיסמה שלך הוא: %(link)s"

#: utils/mail.py:58
#: utils/mail.py:63
#, python-format
msgid "Changing password - %(site_name)s"
msgstr "שינוי סיסמה - %(site_name)s"

#: utils/mail.py:62
#: utils/mail.py:67
#, python-format
msgid ""
"Hello %(fullname)s. Your password in %(site_name)s site has been changed."
Expand All @@ -523,3 +539,23 @@ msgstr ""
"אם אתה לא עשית את זה צור קשר עם הנהלת האתר.\n"
"כתובת המייל: %(site_mail)s"

#: utils/mail.py:80
#, python-format
msgid "New notification - %(site_name)s"
msgstr "התראה חדשה - %(site_name)s"

#: utils/mail.py:84
#, python-format
msgid ""
"Hello %(fullname)s. You have %(num)d new notification:\n"
"%(message)s"
msgid_plural ""
"Hello %(fullname)s. You have %(num)d new notifications:\n"
"%(message)s"
msgstr[0] ""
"שלום %(fullname)s. יש לך %(num)d התראה חדשה:\n"
"%(message)s"
msgstr[1] ""
"שלום %(fullname)s. יש לך %(num)d התראות חדשות:\n"
"%(message)s"

19 changes: 19 additions & 0 deletions lms/lmsweb/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,25 @@ def read_all_notification():
return jsonify({'success': success_state})


@webapp.route('/mail/<subscription>', methods=['PATCH'])
def mail_subscription(subscription: str):
success_state = users.change_mail_subscription(current_user, subscription)
title = _('Mail Subscription')
if subscription == 'subscribe':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move 'subscribe' and 'unsubscribe' to enum and replace the usage

body = _(
"You've successfully subscribed to get mails "
'for new notifications',
)
elif subscription == 'unsubscribe':
body = _(
"You've successfully unsubscribed to get mails "
'for new notifications',
)
else:
body = _('Something went wrong...')
return jsonify({'success': success_state, 'title': title, 'body': body})


@webapp.route('/share', methods=['POST'])
@login_required
def share():
Expand Down
6 changes: 4 additions & 2 deletions lms/models/notifications.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import enum
from typing import Iterable, Optional

from lms.lmsdb.models import Notification, User
from lms.lmsdb.models import MailMessage, Notification, User


class NotificationKind(enum.Enum):
Expand Down Expand Up @@ -45,6 +45,8 @@ def send(
related_id: Optional[int] = None,
action_url: Optional[str] = None,
) -> Notification:
return Notification.send(
notification = Notification.send(
user, kind.value, message, related_id, action_url,
)
MailMessage.create(user=user, notification=notification)
return notification
Loading