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

Add a LetterRequest model, GraphQL mutation + resolver #138

Merged
merged 3 commits into from
Sep 13, 2018
Merged
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
4 changes: 4 additions & 0 deletions common-data/loc-mailing-choices.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[
["WE_WILL_MAIL", "Yes, have JustFix.nyc mail this letter on my behalf."],
["USER_WILL_MAIL", "No thanks, I'll mail it myself."]
]
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 could have just made this field a boolean, but it was easier to make it a choice field with two choices... I guess it also makes room for more options, just in case we ever want to provide more options.

6 changes: 6 additions & 0 deletions loc/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ class LandlordDetailsForm(forms.ModelForm):
class Meta:
model = models.LandlordDetails
fields = ('name', 'address')


class LetterRequestForm(forms.ModelForm):
class Meta:
model = models.LetterRequest
fields = ('mail_choice',)
26 changes: 26 additions & 0 deletions loc/migrations/0003_letterrequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 2.1 on 2018-09-13 12:17

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('loc', '0002_landlorddetails'),
]

operations = [
migrations.CreateModel(
name='LetterRequest',
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)),
('mail_choice', models.TextField(choices=[('choices', [('WE_WILL_MAIL', 'Yes, have JustFix.nyc mail this letter on my behalf.'), ('USER_WILL_MAIL', "No thanks, I'll mail it myself.")])], help_text='How the letter of complaint will be mailed.', max_length=30)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='letter_request', to=settings.AUTH_USER_MODEL)),
],
),
]
22 changes: 22 additions & 0 deletions loc/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
import datetime
from django.db import models

from project.common_data import Choices
from users.models import JustfixUser


LOC_MAILING_CHOICES = Choices.from_file('loc-mailing-choices.json')


class AccessDateManager(models.Manager):
def set_for_user(self, user: JustfixUser, dates: List[datetime.date]):
self.filter(user=user).delete()
Expand Down Expand Up @@ -42,3 +46,21 @@ class LandlordDetails(models.Model):
address = models.CharField(
max_length=1000,
help_text="The full mailing address for the landlord.")


class LetterRequest(models.Model):
'''
A completed letter of complaint request submitted by a user.
'''

created_at = models.DateTimeField(auto_now_add=True)

updated_at = models.DateTimeField(auto_now=True)

user = models.OneToOneField(
JustfixUser, on_delete=models.CASCADE, related_name='letter_request')

mail_choice = models.TextField(
max_length=30,
choices=LOC_MAILING_CHOICES,
help_text="How the letter of complaint will be mailed.")
120 changes: 93 additions & 27 deletions loc/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,47 +7,112 @@
from . import forms, models


class AccessDates(DjangoFormMutation):
class Meta:
form_class = forms.AccessDatesForm
class SessionMutation(DjangoFormMutation):
'''
A base class that can be used for any form mutation
that returns the current user's session.
'''

login_required = True
class Meta:
abstract = True

session = graphene.Field('project.schema.SessionInfo')

@classmethod
def perform_mutate(cls, form: forms.AccessDatesForm, info: ResolveInfo):
request = info.context
models.AccessDate.objects.set_for_user(request.user, form.get_cleaned_dates())
return AccessDates(session=import_string('project.schema.SessionInfo'))

def mutation_success(cls, **kwargs):
'''
This can be returned by any perform_mutate() method
to return a success condition along with the session.
'''

return cls(
errors=[],
session=import_string('project.schema.SessionInfo'),
**kwargs
)


class OneToOneUserModelFormMutation(SessionMutation):
'''
A base class that can be used to make any
ModelForm that represents a one-to-one relationship
with the user into a GraphQL mutation.
'''

class LandlordDetails(DjangoFormMutation):
class Meta:
form_class = forms.LandlordDetailsForm
abstract = True

login_required = True

session = graphene.Field('project.schema.SessionInfo')

@classmethod
def get_form_kwargs(cls, root, info: ResolveInfo, **input):
'''
Either create a new instance of our model, or get the
existing one, and pass it on to the ModelForm.
'''

user = info.context.user
if hasattr(user, 'landlord_details'):
details = user.landlord_details
else:
details = models.LandlordDetails(user=user)
return {"data": input, "instance": details}
model = cls._meta.form_class._meta.model
try:
instance = model.objects.get(user=user)
except model.DoesNotExist:
instance = model(user=user)
return {"data": input, "instance": instance}

@classmethod
def perform_mutate(cls, form: forms.LandlordDetailsForm, info: ResolveInfo):
def perform_mutate(cls, form: forms.LetterRequestForm, info: ResolveInfo):
'''
Save the ModelForm, which will have already been populated with
an instance of our model.
'''

form.save()
return LandlordDetails(session=import_string('project.schema.SessionInfo'))
return cls.mutation_success()

@classmethod
def resolve(cls, parent, info: ResolveInfo):
'''
This can be used as a GraphQL resolver to get the
related model instance for the current user.
'''

user = info.context.user
if not user.is_authenticated:
return None
model = cls._meta.form_class._meta.model
try:
return model.objects.get(user=user)
except model.DoesNotExist:
return None


class AccessDates(SessionMutation):
class Meta:
form_class = forms.AccessDatesForm

login_required = True

@classmethod
def perform_mutate(cls, form: forms.AccessDatesForm, info: ResolveInfo):
request = info.context
models.AccessDate.objects.set_for_user(request.user, form.get_cleaned_dates())
return cls.mutation_success()


class LandlordDetails(OneToOneUserModelFormMutation):
class Meta:
form_class = forms.LandlordDetailsForm


class LetterRequest(OneToOneUserModelFormMutation):
class Meta:
form_class = forms.LetterRequestForm


class LocMutations:
access_dates = AccessDates.Field(required=True)
landlord_details = LandlordDetails.Field(required=True)
letter_request = LetterRequest.Field(required=True)


class LandlordDetailsType(DjangoObjectType):
Expand All @@ -56,18 +121,19 @@ class Meta:
only_fields = ('name', 'address')


class LetterRequestType(DjangoObjectType):
class Meta:
model = models.LetterRequest
only_fields = ('mail_choice', 'updated_at')


class LocSessionInfo:
access_dates = graphene.List(graphene.NonNull(graphene.types.String), required=True)
landlord_details = graphene.Field(LandlordDetailsType)
landlord_details = graphene.Field(LandlordDetailsType, resolver=LandlordDetails.resolve)
letter_request = graphene.Field(LetterRequestType, resolver=LetterRequest.resolve)

def resolve_access_dates(self, info: ResolveInfo):
user = info.context.user
if not user.is_authenticated:
return []
return models.AccessDate.objects.get_for_user(user)

def resolve_landlord_details(self, info: ResolveInfo):
user = info.context.user
if not user.is_authenticated or not hasattr(user, 'landlord_details'):
return None
return user.landlord_details
61 changes: 61 additions & 0 deletions loc/tests/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
}


DEFAULT_LETTER_REQUEST_INPUT = {
'mailChoice': 'WE_WILL_MAIL'
}


def execute_ad_mutation(graphql_client, **input):
input = {**DEFAULT_ACCESS_DATES_INPUT, **input}
return graphql_client.execute(
Expand Down Expand Up @@ -58,6 +63,29 @@ def execute_ld_mutation(graphql_client, **input):
)['data']['output']


def execute_lr_mutation(graphql_client, **input):
input = {**DEFAULT_LETTER_REQUEST_INPUT, **input}
return graphql_client.execute(
"""
mutation MyMutation($input: LetterRequestInput!) {
output: letterRequest(input: $input) {
errors {
field
messages
}
session {
letterRequest {
mailChoice,
updatedAt
}
}
}
}
""",
variables={'input': input}
)['data']['output']


@pytest.mark.django_db
def test_access_dates_works(graphql_client):
graphql_client.request.user = UserFactory.create()
Expand Down Expand Up @@ -123,3 +151,36 @@ def test_landlord_details_is_null_when_user_has_not_yet_provided_it(graphql_clie
graphql_client.request.user = UserFactory.create()
result = graphql_client.execute('query { session { landlordDetails { name } } }')
assert result['data']['session']['landlordDetails'] is None


@pytest.mark.django_db
def test_letter_request_works(graphql_client):
graphql_client.request.user = UserFactory.create()

result = execute_lr_mutation(graphql_client)
assert result['errors'] == []
assert result['session']['letterRequest']['mailChoice'] == 'WE_WILL_MAIL'
assert isinstance(result['session']['letterRequest']['updatedAt'], str)

result = execute_lr_mutation(graphql_client, mailChoice='USER_WILL_MAIL')
assert result['errors'] == []
assert result['session']['letterRequest']['mailChoice'] == 'USER_WILL_MAIL'


def test_letter_request_requires_auth(graphql_client):
result = execute_lr_mutation(graphql_client)
assert result['errors'] == [{'field': '__all__', 'messages': [
'You do not have permission to use this form!'
]}]


def test_letter_request_is_null_when_unauthenticated(graphql_client):
result = graphql_client.execute('query { session { letterRequest { updatedAt } } }')
assert result['data']['session']['letterRequest'] is None


@pytest.mark.django_db
def test_letter_request_is_null_when_user_has_not_yet_requested_letter(graphql_client):
graphql_client.request.user = UserFactory.create()
result = graphql_client.execute('query { session { letterRequest { updatedAt } } }')
assert result['data']['session']['letterRequest'] is None
Loading