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

Reminder notifications #121

Merged
merged 15 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,11 @@ cython_debug/
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn-error.log*

### Redis ###
# Ignore redis binary dump (dump.rdb) files

*.rdb

# End of https://www.toptal.com/developers/gitignore/api/
3 changes: 3 additions & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ boto3 = "*"
sentry-sdk = {extras = ["django"], version = "*"}
pytz = "*"
flake8 = "*"
celery = "*"
redis = "*"
coverage = "*"
django-celery-beat = "*"

[dev-packages]
autopep8 = "*"
Expand Down
267 changes: 203 additions & 64 deletions Pipfile.lock

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,28 @@ python manage.py runserver

The app should now be running at http://localhost:8000/

## Run Celery and Redis locally

Only needed if you want 15/60 minute reminders of scheduled sessions.

Start the Redis server:

```bash
redis-server
```

Start the Celery server:

```bash
celery -A config.celery worker --loglevel=info
```

Start the Celery Beat server:

```bash
celery -A config.celery beat -l debug
```

## Environment Variables

1. Create a file named .env in the root directory of your project. This file will contain your environment variables.
Expand All @@ -85,6 +107,8 @@ DEBUG=True
DJANGO_SUPERUSER_USERNAME=admin
DJANGO_SUPERUSER_PASSWORD=admin_password
[email protected]
CELERY_BROKER_URL = local_redis_url
CELERY_RESULT_BACKEND = local_redis_url
```

- DATABASE_URL: This should be set to the URL of your database. Depending on your database type, this may include a username, password, host, and port.
Expand All @@ -99,6 +123,10 @@ [email protected]

- DJANGO_SUPERUSER_EMAIL: This should be set to the email address you want to use for the Django superuser account.

- CELERY_BROKER_URL: This should be set to your local redis url.

- CELERY_RESULT_BACKEND: This should be set to your local redis url.

3. Save the .env file.

# Testing
Expand Down
3 changes: 3 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ pipenv install
pipenv run python manage.py migrate
pipenv run python manage.py collectstatic --no-input
pipenv run python manage.py add_superuser

celery --app tasks worker --loglevel info --concurrency 4
celery -A config.celery beat -l debug
Empty file added celerybeat-schedule.db
Empty file.
4 changes: 4 additions & 0 deletions config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import absolute_import
from .celery import app as celery_app

__all__ = ('celery_app',)
31 changes: 31 additions & 0 deletions config/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import absolute_import
import django
from django.conf import settings
import os
import ssl
from celery import Celery

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's look into how to automate the production versus development environments for celery. Here is the Render documentation for Celery: https://render.com/docs/deploy-celery

# for production
# app = Celery(
# "config",
# broker_use_ssl={
# 'ssl_cert_reqs': ssl.CERT_NONE
# },
# redis_backend_use_ssl={
# 'ssl_cert_reqs': ssl.CERT_NONE
# }
# )

# for development
app = Celery('config')

app.conf.beat_schedule = {
'notify-every-5-min': {
'task': 'team_production_system.tasks.notify',
'schedule': 300.0,
},
}

app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
5 changes: 5 additions & 0 deletions config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
'imagekit',
'multiselectfield',
"team_production_system",
"django_celery_beat",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -224,3 +225,7 @@
# We recommend adjusting this value in production.
traces_sample_rate=1.0,
)

CELERY_BROKER_URL = env('CELERY_BROKER_URL')
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')
CELERY_TIMEZONE = 'UTC'
2 changes: 2 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ EMAIL_HOST=Your email host
EMAIL_HOST_USER=Example
EMAIL_HOST_PASSWORD=Example
SENTRY_DSN=Example
CELERY_BROKER_URL=Example
CELERY_RESULT_BACKEND=Example

(If you do not plan to create an Email server, AWS S3 bucket,
and/or a sentry account, you will need to comment out lines 202 - 226
Expand Down
36 changes: 36 additions & 0 deletions team_production_system/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,42 @@ def mentee_cancel_notify(self):
recipient_list=[self.mentee.user.email],
)

# Notify a user that a session is coming up in 60 min
def sixty_min_notify(self):
nzeager marked this conversation as resolved.
Show resolved Hide resolved
# Define the timezone
est = pytz.timezone('US/Eastern')

# Convert the start time to EST
est_start_time = self.start_time.astimezone(est)

# Format the time and date
session_time = est_start_time.strftime('%-I:%M %p')

send_mail(
subject=('Mentor Session in 60 Minutes'),
message=(f'Your {self.session_length}-minute session with {self.mentee.user.first_name} and {self.mentor.user.first_name} at {session_time} EST is coming up in 60 minutes.'),
from_email=settings.EMAIL_HOST_USER,
recipient_list=[self.mentor.user.email, self.mentee.user.email],
)

# Notify a user that a session is coming up in 15 min
nzeager marked this conversation as resolved.
Show resolved Hide resolved
def fifteen_min_notify(self):
# Define the timezone
est = pytz.timezone('US/Eastern')

# Convert the start time to EST
est_start_time = self.start_time.astimezone(est)

# Format the time and date
session_time = est_start_time.strftime('%-I:%M %p')

send_mail(
subject=('Mentor Session in 15 Minutes'),
message=(f'Your {self.session_length}-minute session with {self.mentee.user.first_name} and {self.mentor.user.first_name} at {session_time} EST is coming up in 15 minutes.'),
from_email=settings.EMAIL_HOST_USER,
recipient_list=[self.mentor.user.email, self.mentee.user.email],
)


# Notification settings model that allows users to choose to be alerted when
# they have a session requested, confirmed, or canceled.
Expand Down
26 changes: 26 additions & 0 deletions team_production_system/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from celery import shared_task
from datetime import datetime, timedelta
from django.utils import timezone
from .models import Session


@shared_task
# Redis will run this every 5 minutes
def notify():
now = datetime.now(timezone.utc)
# session = Session.objects.get(pk=session_pk)
sessions = Session.objects.filter(status="Confirmed")
for session in sessions:
start_time = session.start_time

# We check a range of times to have Redis run every 5 minutes
if start_time - timedelta(minutes=60) \
<= now \
<= start_time - timedelta(minutes=55):
if session.mentor.user.notification_settings.sixty_minute_alert:
session.sixty_min_notify()
elif start_time - timedelta(minutes=15) \
<= now \
<= start_time - timedelta(minutes=10):
if session.mentor.user.notification_settings.fifteen_minute_alert:
session.fifteen_min_notify()
Empty file.
50 changes: 50 additions & 0 deletions team_production_system/tests/unit_tests/tasks/test_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.test import TestCase
from django.utils import timezone
from datetime import timedelta
from unittest.mock import MagicMock
from ....models import Session
from ....tasks import notify


class NotifyTestCase(TestCase):
def test_notify_sixty_min(self):
# Create a mock session
session = MagicMock()
session.start_time = timezone.now() + timedelta(minutes=60)
session.mentor.user.notification_settings.sixty_minute_alert = True

# Create a MagicMock object for the Session.objects manager
session_manager = MagicMock()

# Set up the mock filter method to return a list of mock sessions
session_manager.filter.return_value = [session]

# Replace the Session.objects manager with the mock object
Session.objects = session_manager

# Call the notify function
notify()

# Check that the session's sixty_min_notify method was called
session.sixty_min_notify.assert_called_once()

def test_notify_fifteen_min(self):
# Create a mock session
session = MagicMock()
session.start_time = timezone.now() + timedelta(minutes=15)
session.mentor.user.notification_settings.fifteen_minute_alert = True

# Create a MagicMock object for the Session.objects manager
session_manager = MagicMock()

# Set up the mock filter method to return a list of mock sessions
session_manager.filter.return_value = [session]

# Replace the Session.objects manager with the mock object
Session.objects = session_manager

# Call the notify function
notify()

# Check that the session's sixty_min_notify method was called
session.fifteen_min_notify.assert_called_once()