Skip to content

Commit

Permalink
Exposure notification (#147)
Browse files Browse the repository at this point in the history
* Add exposure notification model

* Add stories/background with notification control logic

* Add backend/emails/content/py to flake8 ignored files

* Add email sender based on aws ses
see https://docs.aws.amazon.com/ses/latest/DeveloperGuide/examples-send-using-smtp.html

* Notify contacts only if user is sick and tested positive
  • Loading branch information
anaPerezGhiglia authored Jul 6, 2020
1 parent a1879e6 commit 9d7b15c
Show file tree
Hide file tree
Showing 8 changed files with 381 additions and 5 deletions.
6 changes: 5 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ repos:
rev: ""
hooks:
- id: flake8
exclude: legacy/
exclude: >
(?x)^(
legacy/|
backend/emails/contents.py
)$
language_version: python3.7
# tried to to all the precommit work here, didn't work.
# Leaving it as something we might want to look into
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""create exposure notifications model
Revision ID: a31bfe8eba6b
Revises: 8bc2e50b6d22
Create Date: 2020-07-02 19:19:19.971471
"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = "a31bfe8eba6b"
down_revision = "8bc2e50b6d22"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"exposure_notifications",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("email", sa.String(length=128), nullable=True),
sa.Column("notified_at", sa.Date(), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("email"),
)
op.create_index(
op.f("ix_exposure_notifications_id"),
"exposure_notifications",
["id"],
unique=False,
)
op.create_table(
"story_notifications",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.Column("story_id", sa.Integer(), nullable=True),
sa.Column("notification_id", sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(
["notification_id"], ["exposure_notifications.id"],
),
sa.ForeignKeyConstraint(["story_id"], ["stories.id"],),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
op.f("ix_story_notifications_id"),
"story_notifications",
["id"],
unique=False,
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f("ix_story_notifications_id"), table_name="story_notifications"
)
op.drop_table("story_notifications")
op.drop_index(
op.f("ix_exposure_notifications_id"),
table_name="exposure_notifications",
)
op.drop_table("exposure_notifications")
# ### end Alembic commands ###
Empty file added backend/emails/__init__.py
Empty file.
82 changes: 82 additions & 0 deletions backend/emails/contents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
EXPOSURE_NOTIFICATION_SUBJECT = "COVID-19 - Important notification"

EXPOSURE_NOTIFICATION_TEXT_CONTENT = """
Hello. This is a notice from Oasis, a citizen-led global community working to stop the spread of COVID-19.
Oasis community volunteers document their symptoms and COVID-19 test results on the Oasis platform.
In addition, they can provide contact information of people whom they have been near and who may have been exposed to the virus.
This notification is to let you know that you may have been exposed to the COVID-19 virus (https://www.cdc.gov/coronavirus/2019-ncov/faq.html) and we offer a few suggestions on what you can do.
To protect everyone’s privacy, we keep all notifications confidential - that means we do not share information about who has symptoms,
who has confirmed infection or who has been in contact with people who may be infected. We only use your contact information to notify
you now of possible COVID exposure and will never use personal information for anything else.
Because you may have been exposed to the COVID-19 virus, we encourage you to self-monitor for symptoms. Actions you can take now follow:
- Wear a facial covering to prevent infection of others. People can spread COVID-19 even without having symptoms, and we all need to protect those around us.
- Monitor yourself by checking your temperature twice daily and watching for new symptoms like:
- Fever
- Cough or shortness of breath.
- Fatigue
- Muscle or body aches
- Headache
- Loss of taste or smell
- Sore throat
- Congestion or runny nose
- Nausea or vomiting
- Diarrhea
- Wash your hands frequently and avoid touching your face.
- Disinfect areas that you touch.
If you have symptoms:
- check with your healthcare provider about the need for medical care or testing
- self-quarantine until you know you are not infectious
- maintain social distance from others (at least 6 feet) for 14 days or, until cleared by a medical professional.
Please go to the Oasis website (http://1oasis.org/) to access COVID-19 information and, if you choose, you can report how you are feeling.
You can learn more from the CDC (https://www.cdc.gov/coronavirus/2019-ncov/if-you-are-sick/quarantine.html?CDC_AA_refVal=https%3A%2F%2Fwww.cdc.gov%2Fcoronavirus%2F2019-ncov%2Fif-you-are-sick%2Fquarantine-isolation.html) about how to care for yourself if you’ve been exposed to COVID-19.
"""

EXPOSURE_NOTIFICATION_HTML_CONTENT = """
<html>
<head></head>
<body>
<p>Hello. This is a notice from Oasis, a citizen-led global community working to stop the spread of COVID-19.
Oasis community volunteers document their symptoms and COVID-19 test results on the Oasis platform.
In addition, they can provide contact information of people whom they have been near and who may have been exposed to the virus.
This notification is to let you know that <b>you may have been exposed</b> to the <a href='https://www.cdc.gov/coronavirus/2019-ncov/faq.html'>COVID-19 virus</a> and we offer a few suggestions on what you can do.</p>
<p>To protect everyone’s privacy, we keep all notifications confidential - that means we do not share information about who has symptoms,
who has confirmed infection or who has been in contact with people who may be infected. We only use your contact information to notify you now of possible COVID exposure and will never use personal information for anything else.</p>
<p>Because <b>you may have been exposed</b> to the COVID-19 virus, we encourage you to self-monitor for symptoms. Actions you can take now follow:</p>
<ul>
<li>Wear a facial covering to prevent infection of others. People can spread COVID-19 even without having symptoms, and we all need to protect those around us.</li>
<li>Monitor yourself by checking your temperature twice daily and watching for new symptoms like:
<ul>
<li>Fever</li>
<li>Cough or shortness of breath.</li>
<li>Fatigue</li>
<li>Muscle or body aches</li>
<li>Headache</li>
<li>Loss of taste or smell</li>
<li>Sore throat</li>
<li>Congestion or runny nose</li>
<li>Nausea or vomiting</li>
<li>Diarrhea</li>
</UL>
</li>
<li>Wash your hands frequently and avoid touching your face.</li>
<li>Disinfect areas that you touch.</li>
</ul>
<p><b>If you have symptoms:</b></p>
<ul>
<li>check with your healthcare provider about the need for medical care or testing</li>
<li>self-quarantine until you know you are not infectious</li>
<li>maintain social distance from others (at least 6 feet) for 14 days or, until cleared by a medical professional.</li>
</ul>
<p>Please go to the <a href='http://1oasis.org/'>Oasis website</a> to access COVID-19 information and, if you choose, you can report how you are feeling.
You can learn more from the <a href='https://www.cdc.gov/coronavirus/2019-ncov/if-you-are-sick/quarantine.html?CDC_AA_refVal=https%3A%2F%2Fwww.cdc.gov%2Fcoronavirus%2F2019-ncov%2Fif-you-are-sick%2Fquarantine-isolation.html'>CDC</a> about how to care for yourself if you’ve been exposed to COVID-19. </p>
</body>
</html>
"""
46 changes: 46 additions & 0 deletions backend/emails/email_sender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

SENDER = os.environ.get("SMTP_FROM_ADDRESS", "")
USERNAME_SMTP = os.environ.get("SMTP_USER", "")
PASSWORD_SMTP = os.environ.get("SMTP_PASS", "")
HOST = os.environ.get("SMTP_SERVER", "")
PORT = os.environ.get("SMTP_PORT", "")


def send(to: str, subject: str, body_text: str, body_html: str = None):

# Create message container
msg = MIMEMultipart("alternative")
msg["From"] = SENDER
msg["To"] = to
msg["Subject"] = subject

# Record the MIME types of both parts - text/plain and text/html.
part1 = MIMEText(body_text, "plain")
part2 = MIMEText(body_html, "html")

# Attach parts into message container.
# According to RFC 2046, the last part of a multipart message, in this case
# the HTML message, is best and preferred.
msg.attach(part1)
msg.attach(part2)

# Try to send the message.
try:
if os.environ.get("DEV", False):
print(msg.as_string())
else:
server = smtplib.SMTP(HOST, PORT)
server.ehlo()
server.starttls()
# stmplib docs recommend calling ehlo() before & after starttls()
server.ehlo()
server.login(USERNAME_SMTP, PASSWORD_SMTP)
server.sendmail(SENDER, to, msg.as_string())
server.close()
# Display an error message if something goes wrong.
except Exception as e:
print("Error: ", e)
30 changes: 26 additions & 4 deletions backend/router/stories.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
from typing import List
import os

from fastapi import Depends, APIRouter, HTTPException, status
from fastapi import Depends, APIRouter, HTTPException, status, BackgroundTasks
from sqlalchemy.orm import Session
from starlette.responses import JSONResponse
from starlette.requests import Request
from fastapi.encoders import jsonable_encoder

from database import get_db
from auth import main
from stories import crud, schemas
from stories import crud, schemas, background


router = APIRouter()
Expand Down Expand Up @@ -108,22 +108,44 @@ def update_travels(
def create_close_contacts(
story_id: int,
close_contacts: List[schemas.CloseContactCreate],
background_tasks: BackgroundTasks,
current_story: schemas.Story = Depends(main.get_current_story),
db: Session = Depends(get_db),
):
check_permissions(current_story, story_id)
return crud.create_close_contacts(db, close_contacts=close_contacts)
new_contacts = crud.create_close_contacts(
db, close_contacts=close_contacts
)

background_tasks.add_task(
background.send_exposure_notification,
story=current_story,
contacts=new_contacts,
db=db,
)

return new_contacts


@router.put("/{story_id}/contacts", response_model=List[schemas.CloseContact])
def update_close_contacts(
story_id: int,
close_contacts: List[schemas.CloseContact],
background_tasks: BackgroundTasks,
current_story: schemas.Story = Depends(main.get_current_story),
db: Session = Depends(get_db),
):
check_permissions(current_story, story_id)
return [
updated_contacts = [
crud.update_close_contact(db, close_contact=contact)
for contact in close_contacts
]

background_tasks.add_task(
background.send_exposure_notification,
story=current_story,
contacts=updated_contacts,
db=db,
)

return updated_contacts
Loading

0 comments on commit 9d7b15c

Please sign in to comment.