Skip to content

Commit

Permalink
Merge fa2a063 into 00bdfb8
Browse files Browse the repository at this point in the history
  • Loading branch information
nitely committed Oct 23, 2020
2 parents 00bdfb8 + fa2a063 commit 03e4f91
Show file tree
Hide file tree
Showing 24 changed files with 718 additions and 230 deletions.
11 changes: 11 additions & 0 deletions spirit/core/apps.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
# -*- coding: utf-8 -*-

from django.apps import AppConfig
from django.core.exceptions import ImproperlyConfigured

from .conf import settings


class SpiritCoreConfig(AppConfig):

name = 'spirit.core'
verbose_name = "Spirit Core"
label = 'spirit_core'

def ready(self):
if not settings.ST_SITE_URL:
raise ImproperlyConfigured('ST_SITE_URL setting not set')
if settings.ST_TASK_MANAGER not in {'huey', 'celery', None}:
raise ImproperlyConfigured(
'ST_TASK_MANAGER setting is invalid. '
'Valid values are: "huey", "celery", and None')
36 changes: 30 additions & 6 deletions spirit/core/conf/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

import os

#: The forum URL, ex: ``"https://community.spirit-project.com/"``.
#: This is used to construct the links in the emails: email verification,
#: password reset, notifications, etc. A ``ImproperlyConfigured`` error
#: is raised if this is not set.
ST_SITE_URL = None

#: The media file storage for Spirit.
#: The default file storage is used if this
#: is not set. In either case, the storage should
Expand All @@ -25,12 +31,30 @@
#: install Celery
ST_TASK_MANAGER = None

#: The age in hours of the items
#: to index into the search index on each update.
#: The update runs every this amount of time
#: when ``ST_TASK_MANAGER`` is set to ``'huey'``.
#: Other task managers will need to sync their
#: configuration to this value
#: Tasks schedule for the Huey task manager.
#: It contains a dict of tasks, and every
#: task a dict of crontab params. Beware, the
#: default value for every missing param is ``'*'``.
#: See `Huey periodic tasks <https://huey.readthedocs.io/en/latest/guide.html#periodic-tasks>`_
ST_HUEY_SCHEDULE = {
'full_search_index_update': {
'minute': '0',
'hour': '*/24'
},
'notify_weekly': {
'minute': '0',
'hour': '0',
'day_of_week': '1' # 0=Sunday, 6=Saturday
}
}

#: | The age in hours of the items
#: to index into the search index on each update.
#: | The update is run by a periodic task; ex:
#: ``ST_HUEY_SCHEDULE['full_search_index_update']``
#: in case of Huey.
#: | The task schedule for the selected ``ST_TASK_MANAGER``
#: will need to be set to this value (or lesser)
ST_SEARCH_INDEX_UPDATE_HOURS = 24

#: The category's PK containing all of the private topics.
Expand Down
176 changes: 164 additions & 12 deletions spirit/core/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,29 @@
import logging

from django.db import transaction
from django.core.mail import send_mail
from django.db.models import Q
from django.apps import apps
from django.core.management import call_command
from django.contrib.auth import get_user_model
from django.core import mail
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

import djconfig
from PIL import Image

from spirit.user.utils import tokens
from .conf import settings
from .conf import defaults
from .storage import spirit_storage
from . import signals
from .utils.tasks import avatars
from .utils import site_url

logger = logging.getLogger(__name__)
HUEY_SCHEDULE = {
**defaults.ST_HUEY_SCHEDULE, **settings.ST_HUEY_SCHEDULE}


# XXX support custom task manager __import__('foo.task')?
Expand All @@ -41,9 +51,8 @@ def periodic_task_manager(tm):
if tm == 'huey':
from huey import crontab
from huey.contrib.djhuey import db_periodic_task
def periodic_task(hours):
return db_periodic_task(crontab(
minute='0', hour='*/{}'.format(hours)))
def periodic_task(**kwargs):
return db_periodic_task(crontab(**kwargs))
return periodic_task
assert tm in ('celery', None)
def fake_periodic_task(*args, **kwargs):
Expand All @@ -60,20 +69,32 @@ def delayed_task_inner(*args, **kwargs):
return delayed_task_inner


def _send_email(subject, message, to, unsub=None, conn=None):
assert isinstance(to, str)
# Subject cannot contain new lines
subject = ''.join(subject.splitlines())
headers = {}
if unsub:
headers['List-Unsubscribe'] = '<%s>' % unsub
return mail.EmailMessage(
subject=subject,
body=message,
from_email=settings.DEFAULT_FROM_EMAIL,
to=[to],
headers=headers,
connection=conn).send()


@delayed_task
def send_email(subject, message, from_email, recipients):
def send_email(subject, message, recipients):
# Avoid retrying this task. It's better to log the exception
# here instead of possibly spamming users on retry
# We send to one recipient at the time, because otherwise
# it'll likely get flagged as spam, or it won't reach the
# the recipient at all
for recipient in recipients:
try:
send_mail(
subject=subject,
message=message,
from_email=from_email,
recipient_list=[recipient])
_send_email(subject, message, recipient)
except OSError as err:
logger.exception(err)
return # bail out
Expand All @@ -91,7 +112,7 @@ def search_index_update(topic_pk):
instance=Topic.objects.get(pk=topic_pk))


@periodic_task(hours=settings.ST_SEARCH_INDEX_UPDATE_HOURS)
@periodic_task(**HUEY_SCHEDULE['full_search_index_update'])
def full_search_index_update():
age = settings.ST_SEARCH_INDEX_UPDATE_HOURS
call_command("update_index", age=age)
Expand All @@ -115,6 +136,138 @@ def make_avatars(user_id):
user.st.small_avatar_name(), small_avatar)


def _notify_comment(
comment_id, site, subject, template, action
):
if settings.ST_TASK_MANAGER is None:
return
djconfig.reload_maybe()
Comment = apps.get_model('spirit_comment.Comment')
Notification = apps.get_model(
'spirit_topic_notification.TopicNotification')
UserProfile = apps.get_model('spirit_user.UserProfile')
Notify = UserProfile.Notify
comment = (
Comment.objects
.select_related('user__st', 'topic')
.get(pk=comment_id))
actions = {
'mention': Notification.MENTION,
'reply': Notification.COMMENT}
notify = {
'mention': Notify.MENTION,
'reply': Notify.REPLY}
notifications = (
Notification.objects
.exclude(user_id=comment.user_id)
.filter(
topic_id=comment.topic_id,
comment_id=comment_id,
is_read=False,
is_active=True,
action=actions[action],
user__st__notify__in=[
Notify.IMMEDIATELY | notify[action],
Notify.IMMEDIATELY | Notify.MENTION | Notify.REPLY])
.order_by('-pk')
.only('user_id', 'user__email'))
# Since this is a task, the default language will
# be used; we don't know what language each user prefers
# XXX auto save user prefer/browser language in some field
subject = subject.format(
user=comment.user.st.nickname,
topic=comment.topic.title)
with mail.get_connection() as connection:
for n in notifications.iterator(chunk_size=2000):
unsub_token = tokens.unsub_token(n.user_id)
message = render_to_string(template, {
'site': site,
'comment_id': comment_id,
'unsub_token': unsub_token})
unsub = ''.join((site, reverse(
'spirit:user:unsubscribe',
kwargs={'token': unsub_token})))
try:
_send_email(
subject, message,
to=n.user.email,
unsub=unsub,
conn=connection)
except OSError as err:
logger.exception(err)
return # bail out


@delayed_task
def notify_reply(comment_id):
_notify_comment(
comment_id=comment_id,
site=site_url(),
subject=_("{user} commented on {topic}"),
template='spirit/topic/notification/email_notification.txt',
action='reply')


@delayed_task
def notify_mention(comment_id):
_notify_comment(
comment_id=comment_id,
site=site_url(),
subject=_("{user} mention you on {topic}"),
template='spirit/topic/notification/email_notification.txt',
action='mention')


@periodic_task(**HUEY_SCHEDULE['notify_weekly'])
def notify_weekly():
from django.contrib.auth import get_user_model
djconfig.reload_maybe()
Notification = apps.get_model(
'spirit_topic_notification.TopicNotification')
UserProfile = apps.get_model('spirit_user.UserProfile')
Notify = UserProfile.Notify
User = get_user_model()
users = (
User.objects
.filter(
Q(
st__notify__in=[
Notify.WEEKLY | Notify.MENTION,
Notify.WEEKLY | Notify.MENTION | Notify.REPLY],
st_topic_notifications__action=Notification.MENTION) |
Q(
st__notify__in=[
Notify.WEEKLY | Notify.REPLY,
Notify.WEEKLY | Notify.MENTION | Notify.REPLY],
st_topic_notifications__action=Notification.COMMENT),
st_topic_notifications__is_read=False,
st_topic_notifications__is_active=True)
.order_by('-pk')
.only('pk', 'email')
.distinct())
subject = _('New notifications')
site = site_url()
with mail.get_connection() as connection:
for u in users.iterator(chunk_size=2000):
unsub_token = tokens.unsub_token(u.pk)
message = render_to_string(
'spirit/topic/notification/email_notification_weekly.txt',
{'site': site,
'unsub_token': unsub_token})
unsub = ''.join((site, reverse(
'spirit:user:unsubscribe',
kwargs={'token': unsub_token})))
try:
_send_email(
subject, message,
to=u.email,
unsub=unsub,
conn=connection)
except OSError as err:
logger.exception(err)
return # bail out


@delayed_task
def clean_sessions():
pass
Expand All @@ -123,4 +276,3 @@ def clean_sessions():
@delayed_task
def backup_database():
pass

0 comments on commit 03e4f91

Please sign in to comment.