Skip to content

Commit

Permalink
Add a letter model for the habitability letter. (#2256)
Browse files Browse the repository at this point in the history
* Add models for new letter type

* Move some commonly used code into project/util

* Run django:makemessages

* Styling

* Run dcra python manage.py makemigrations laletterbuilder

* Lint and tests

* fix imports

* Add tests

* Remove duplicate

* Fix imports

* Fix comment

* Fix translation of text

* Add back lost django translation

* Add space back
  • Loading branch information
samaratrilling authored Feb 23, 2022
1 parent 5a14457 commit 9113397
Show file tree
Hide file tree
Showing 15 changed files with 318 additions and 192 deletions.
5 changes: 2 additions & 3 deletions evictionfree/declaration_sending.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def create_declaration(user: JustfixUser) -> SubmittedHardshipDeclaration:

def render_declaration(decl: SubmittedHardshipDeclaration) -> bytes:
from loc.views import render_pdf_bytes
from norent.letter_sending import _merge_pdfs
from project.util.letter_sending import _merge_pdfs

v = hardship_declaration.HardshipDeclarationVariables(**decl.declaration_variables)
form_pdf_bytes = hardship_declaration.fill_hardship_pdf(v, decl.locale)
Expand Down Expand Up @@ -113,8 +113,7 @@ def send_declaration_via_lob(decl: SubmittedHardshipDeclaration, pdf_bytes: byte
Returns True if the declaration was just sent.
"""

from norent.letter_sending import send_pdf_to_landlord_via_lob, USPS_TRACKING_URL_PREFIX
from project.util.letter_sending import send_pdf_to_landlord_via_lob, USPS_TRACKING_URL_PREFIX

if decl.mailed_at is not None:
logger.info(f"{decl} has already been mailed to the landlord.")
Expand Down
40 changes: 40 additions & 0 deletions laletterbuilder/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 3.2.5 on 2022-02-22 19:51

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import project.util.lob_django_util


class Migration(migrations.Migration):

initial = True

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='HabitabilityLetter',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('locale', models.CharField(choices=[('en', 'English'), ('es', 'Spanish')], default='en', help_text="The locale of the user who sent the mail item, at the time that they sent it. Note that this may be different from the user's current locale, e.g. if they changed it after sending the mail item.", max_length=5)),
('lob_letter_object', models.JSONField(blank=True, help_text='If the mail item was sent via Lob, this is the JSON response of the API call that was made to send the mail item, documented at https://lob.com/docs/python#letters.', null=True)),
('tracking_number', models.CharField(blank=True, help_text='The USPS tracking number for the mail item.', max_length=100)),
('html_content', models.TextField(help_text='The HTML content of the letter at the time it was sent, in English.')),
('localized_html_content', models.TextField(blank=True, help_text="The HTML content of the letter at the time it was sent, in the user's locale at the time they sent it. If the user's locale is English, this will be blank (since the English version is already stored in another field).")),
('letter_sent_at', models.DateTimeField(blank=True, help_text='When the letter was mailed.', null=True)),
('letter_emailed_at', models.DateTimeField(blank=True, help_text='When the letter was e-mailed.', null=True)),
('fully_processed_at', models.DateTimeField(blank=True, help_text='When the letter was fully processed, i.e. sent to all relevant parties.', null=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='laletterbuilder_letters', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created_at'],
'abstract': False,
},
bases=(models.Model, project.util.lob_django_util.SendableViaLobMixin),
),
]
Empty file.
25 changes: 25 additions & 0 deletions laletterbuilder/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
from project import common_data
from project.util.lob_models_util import LocalizedHTMLLetter
from users.models import JustfixUser
from django.db import models


LETTER_TYPE_CHOICES = common_data.Choices.from_file("la-letter-builder-letter-choices.json")


class Letter(LocalizedHTMLLetter):
"""
A LA Letter Builder letter that's ready to be sent, or has already been sent.
"""

class Meta:
abstract = True
ordering = ["-created_at"]

user = models.ForeignKey(
JustfixUser, on_delete=models.CASCADE, related_name="laletterbuilder_letters"
)


class HabitabilityLetter(Letter):
def __str__(self):
if not self.pk:
return super().__str__()
return f"{self.user.full_legal_name}'s Habitability LA Letter Builder letter"
Empty file.
21 changes: 14 additions & 7 deletions laletterbuilder/tests/factories.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import factory
from users.tests.factories import UserFactory
from loc.models import LandlordDetails
from laletterbuilder.models import HabitabilityLetter


class LandlordDetailsFactory(factory.django.DjangoModelFactory):
Expand All @@ -14,10 +15,16 @@ class Meta:
address = "1 Cloud City"


class LandlordDetailsFactory(LandlordDetailsFactory):
address = "123 Cloud City Drive\nBespin, NY 12345"
primary_line = "123 Cloud City Drive"
city = "Bespin"
state = "NY"
zip_code = "12345"
email = "[email protected]"
class HabitabilityLetterFactory(factory.django.DjangoModelFactory):
class Meta:
model = HabitabilityLetter

user = factory.SubFactory(UserFactory)

html_content = "<p>hi i am a habitability letter</p>"

@classmethod
def _create(self, model_class, *args, **kwargs):
letter = HabitabilityLetter(*args, **kwargs)
letter.save()
return letter
11 changes: 11 additions & 0 deletions laletterbuilder/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from laletterbuilder.models import HabitabilityLetter
from .factories import HabitabilityLetterFactory


class TestHabitabilityLetter:
def test_str_works_on_brand_new_models(self):
assert str(HabitabilityLetter()) == "HabitabilityLetter object (None)"

def test_str_works_on_filled_out_models(self, db):
decl = HabitabilityLetterFactory()
assert str(decl) == "Boop Jones's Habitability LA Letter Builder letter"
12 changes: 6 additions & 6 deletions locales/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-11-29 19:23+0000\n"
"POT-Creation-Date: 2022-02-23 19:00+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
Expand All @@ -23,22 +23,22 @@ msgstr ""
msgid "%(name)s, your eviction protection form has been emailed to your landlord."
msgstr ""

#: evictionfree/declaration_sending.py:133
#: evictionfree/declaration_sending.py:132
#, python-format
msgid "%(name)s, a hard copy of your eviction protection form has been mailed to your landlord via USPS mail. You can track the delivery of your hard copy form using USPS Tracking: %(url)s."
msgstr ""

#: evictionfree/declaration_sending.py:176
#: evictionfree/declaration_sending.py:175
#, python-format
msgid "%(name)s, your eviction protection form has been emailed to your local housing court."
msgstr ""

#: evictionfree/declaration_sending.py:249
#: evictionfree/declaration_sending.py:248
#, python-format
msgid "%(name)s, you can download a PDF of your completed declaration form by logging back into your account: %(url)s."
msgstr ""

#: evictionfree/declaration_sending.py:257
#: evictionfree/declaration_sending.py:256
#, python-format
msgid "For more information about New York’s eviction protections and your rights as a tenant, visit %(url)s. To get involved in organizing and the fight to #StopEvictions and #CancelRent, follow us on Twitter at @RTCNYC and @housing4allNY."
msgstr ""
Expand Down Expand Up @@ -92,7 +92,7 @@ msgstr ""
msgid "%(city)s, %(state_name)s doesn't seem to exist!"
msgstr ""

#: norent/letter_sending.py:112
#: norent/letter_sending.py:38
#, python-format
msgid "%(name)s you've sent your letter of non-payment of rent. You can track the delivery of your letter using USPS Tracking: %(url)s."
msgstr ""
Expand Down
16 changes: 8 additions & 8 deletions locales/es/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: tenants2\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-08 16:00+0000\n"
"POT-Creation-Date: 2022-02-23 19:00+0000\n"
"PO-Revision-Date: 2021-12-07 22:06\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
Expand All @@ -22,22 +22,22 @@ msgstr ""
msgid "%(name)s, your eviction protection form has been emailed to your landlord."
msgstr "%(name)s, tu formulario de protección contra el desalojo ha sido enviado por correo electrónico al dueño de tu edificio."

#: evictionfree/declaration_sending.py:133
#: evictionfree/declaration_sending.py:132
#, python-format
msgid "%(name)s, a hard copy of your eviction protection form has been mailed to your landlord via USPS mail. You can track the delivery of your hard copy form using USPS Tracking: %(url)s."
msgstr "%(name)s, también se le envió una copia impresa de tu formulario al dueño de tu edificio por correo postal de USPS. Puedes dar seguimiento a la entrega de tu formulario impreso utilizando la herramienta de seguimiento de correspondencia de USPS: %(url)s."

#: evictionfree/declaration_sending.py:176
#: evictionfree/declaration_sending.py:175
#, python-format
msgid "%(name)s, your eviction protection form has been emailed to your local housing court."
msgstr "%(name)s, tu formulario de protección contra el desalojo ha sido enviado por correo electrónico a la corte de vivienda local."

#: evictionfree/declaration_sending.py:249
#: evictionfree/declaration_sending.py:248
#, python-format
msgid "%(name)s, you can download a PDF of your completed declaration form by logging back into your account: %(url)s."
msgstr "%(name)s, también puedes bajarte un PDF de tu formulario de declaración regresando a tu cuenta: %(url)s."

#: evictionfree/declaration_sending.py:257
#: evictionfree/declaration_sending.py:256
#, python-format
msgid "For more information about New York’s eviction protections and your rights as a tenant, visit %(url)s. To get involved in organizing and the fight to #StopEvictions and #CancelRent, follow us on Twitter at @RTCNYC and @housing4allNY."
msgstr "Para obtener más información sobre las protecciones de desalojo de Nueva York y tus derechos como inquilino, visita %(url)s. Para participar en la organización y en la lucha para #StopEvictions (parar los desalojos) y #CancelRent (cancelar la renta), síguenos en Twitter: @RTCNYC y @ housing4allNY."
Expand Down Expand Up @@ -91,8 +91,9 @@ msgstr "cerrar"
msgid "%(city)s, %(state_name)s doesn't seem to exist!"
msgstr "¡%(city)s, %(state_name)s no existe!"

#: norent/letter_sending.py:112
#, python-format
#: norent/letter_sending.py:38
#, fuzzy, python-format
#| msgid "%(name)s you've sent your letter of non-payment of rent. You can track the delivery of your letter using USPS Tracking: %(url)s."
msgid "%(name)s you've sent your letter of non-payment of rent. You can track the delivery of your letter using USPS Tracking: %(url)s."
msgstr "%(name)s has enviado tu carta de no pago de renta. Puedes seguir la entrega de tu carta usando USPS Tracking: %(url)s."

Expand Down Expand Up @@ -198,4 +199,3 @@ msgstr "%(areacode)s no es un prefijo telefónico válido."
#: project/util/phone_number.py:70
msgid "This does not look like a U.S. phone number. Please include the area code, e.g. (555) 123-4567."
msgstr "Ese no parece un número de teléfono de los Estados Unidos de América. Por favor, incluye el prefijo telefónico, por ejemplo, (555) 123-4567."

117 changes: 17 additions & 100 deletions norent/letter_sending.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from typing import Any, Dict, List
from typing import List
from io import BytesIO
import logging
from django.http import FileResponse
from django.utils import timezone
from django.utils.translation import gettext as _
from django.db import transaction
import PyPDF2
from django.utils.translation import gettext as _

from project import slack, locales, common_data
from project import slack, locales
from project.util.letter_sending import (
render_multilingual_letter,
send_letter_via_lob,
)
from project.util.site_util import SITE_CHOICES
from project.util.demo_deployment import is_not_demo_deployment
from frontend.static_content import (
Expand All @@ -16,8 +19,6 @@
ContentType,
)
from users.models import JustfixUser
from loc.views import render_pdf_bytes
from project.util import lob_api
from . import models


Expand All @@ -33,8 +34,10 @@
# email to the user.
NORENT_EMAIL_TO_USER_URL = "letter-email-to-user.html"

# The URL prefix for USPS certified letter tracking.
USPS_TRACKING_URL_PREFIX = common_data.load_json("loc.json")["USPS_TRACKING_URL_PREFIX"]
USER_CONFIRMATION_TEXT = _(
"%(name)s you've sent your letter of non-payment of rent. "
"You can track the delivery of your letter using USPS Tracking: %(url)s."
)

logger = logging.getLogger(__name__)

Expand All @@ -48,80 +51,6 @@ def norent_pdf_response(pdf_bytes: bytes) -> FileResponse:
return FileResponse(BytesIO(pdf_bytes), filename="norent-letter.pdf")


def send_pdf_to_landlord_via_lob(
user: JustfixUser, pdf_bytes: bytes, description: str
) -> Dict[str, Any]:
"""
Mail the given PDF to the given user's landlord using USPS certified
mail, via Lob. Assumes that the user has a landlord with a mailing
address.
Returns the response from the Lob API.
"""

ld = user.landlord_details
assert ld.address_lines_for_mailing
ll_addr_details = ld.get_or_create_address_details_model()
landlord_verification = lob_api.verify_address(**ll_addr_details.as_lob_params())
user_verification = lob_api.verify_address(**user.onboarding_info.as_lob_params())

logger.info(
f"Sending {description} to landlord with {landlord_verification['deliverability']} "
f"landlord address."
)

return lob_api.mail_certified_letter(
description=description,
to_address={
"name": ld.name,
**lob_api.verification_to_inline_address(landlord_verification),
},
from_address={
"name": user.full_legal_name,
**lob_api.verification_to_inline_address(user_verification),
},
file=BytesIO(pdf_bytes),
color=False,
double_sided=False,
)


def send_letter_via_lob(letter: models.Letter, pdf_bytes: bytes) -> bool:
"""
Mails the NoRent letter to the user's landlord via Lob. Does
nothing if the letter has already been sent.
Returns True if the letter was just sent.
"""

if letter.letter_sent_at is not None:
logger.info(f"{letter} has already been mailed to the landlord.")
return False

user = letter.user

response = send_pdf_to_landlord_via_lob(user, pdf_bytes, "No rent letter")

letter.lob_letter_object = response
letter.tracking_number = response["tracking_number"]
letter.letter_sent_at = timezone.now()
letter.save()

user.send_sms_async(
_(
"%(name)s you've sent your letter of non-payment of rent. "
"You can track the delivery of your letter using "
"USPS Tracking: %(url)s."
)
% {
"name": user.full_legal_name,
"url": USPS_TRACKING_URL_PREFIX + letter.tracking_number,
}
)

return True


def email_letter_to_landlord(letter: models.Letter, pdf_bytes: bytes) -> bool:
"""
Email the given letter to the user's landlord. Does nothing if the
Expand Down Expand Up @@ -186,23 +115,6 @@ def create_letter(user: JustfixUser, rps: List[models.RentPeriod]) -> models.Let
return letter


def _merge_pdfs(pdfs: List[bytes]) -> bytes:
merger = PyPDF2.PdfFileMerger()
for pdf_bytes in pdfs:
merger.append(PyPDF2.PdfFileReader(BytesIO(pdf_bytes)))
outfile = BytesIO()
merger.write(outfile)
return outfile.getvalue()


def render_multilingual_letter(letter: models.Letter) -> bytes:
pdf_bytes = render_pdf_bytes(letter.html_content)
if letter.localized_html_content:
localized_pdf_bytes = render_pdf_bytes(letter.localized_html_content)
pdf_bytes = _merge_pdfs([pdf_bytes, localized_pdf_bytes])
return pdf_bytes


def send_letter(letter: models.Letter):
"""
Send the given letter using whatever information is populated
Expand All @@ -223,7 +135,12 @@ def send_letter(letter: models.Letter):
email_letter_to_landlord(letter, pdf_bytes)

if ld.address_lines_for_mailing:
send_letter_via_lob(letter, pdf_bytes)
send_letter_via_lob(
letter,
pdf_bytes,
sms_text=USER_CONFIRMATION_TEXT,
letter_description="No rent letter",
)

if user.email:
email_react_rendered_content_with_attachment(
Expand Down
Loading

0 comments on commit 9113397

Please sign in to comment.