Skip to content

Commit 21853df

Browse files
ewdurbinberinhard
andauthored
Sponsorship feedback fixes (#1681)
* fix typo * update sponsorship notification from email and make configurable. * add a "if you have questions..." to the confirmation screen * Fix bug with benefit uncheck Bug: If you uncheck a benefit and change your mind, you can’t check it again. * Remove legacy query set * Check if application can be created * Raise form error if sponsor with in progress application * Split address in fields in Django form * Display address fields in the frontend * add missing dependency Co-authored-by: Bernardo Fontes <bernardoxhc@gmail.com>
1 parent bf0bd1f commit 21853df

17 files changed

+319
-62
lines changed

base-requirements.txt

+1
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ djangorestframework==3.8.2
3838
django-filter==1.1.0
3939
django-ordered-model==3.4.1
4040
django-widget-tweaks==1.4.8
41+
django-countries==6.1.3

pydotorg/settings/base.py

+4
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
'waffle',
155155
'ordered_model',
156156
'widget_tweaks',
157+
'django_countries',
157158

158159
'users',
159160
'boxes',
@@ -248,6 +249,9 @@
248249
EVENTS_TO_EMAIL = 'events@python.org'
249250

250251
# Sponsors
252+
SPONSORSHIP_NOTIFICATION_FROM_EMAIL = os.environ.get(
253+
"SPONSORSHIP_NOTIFICATION_FROM_EMAIL", "sponsors@python.org"
254+
)
251255
SPONSORSHIP_NOTIFICATION_TO_EMAIL = os.environ.get(
252256
"SPONSORSHIP_NOTIFICATION_TO_EMAIL", "psf-sponsors@python.org"
253257
)

sponsors/exceptions.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class SponsorWithExistingApplicationException(Exception):
2+
"""
3+
Raised when user tries to create a new Sponsorship application
4+
for a Sponsor which already has applications pending to review
5+
"""

sponsors/forms.py

+63-7
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from itertools import chain
2+
from django_countries.fields import CountryField
23
from django import forms
34
from django.utils.text import slugify
45
from django.utils.translation import ugettext_lazy as _
56
from django.utils.functional import cached_property
7+
from django.conf import settings
68

79
from sponsors.models import (
810
SponsorshipBenefit,
@@ -164,11 +166,25 @@ class SponsorshipApplicationForm(forms.Form):
164166
max_length=32,
165167
required=False,
166168
)
167-
mailing_address = forms.CharField(
168-
label="Sponsor Mailing/Billing Address",
169+
mailing_address_line_1 = forms.CharField(
170+
label="Mailing Address line 1",
169171
widget=forms.TextInput,
170172
required=False,
171173
)
174+
mailing_address_line_2 = forms.CharField(
175+
label="Mailing Address line 2",
176+
widget=forms.TextInput,
177+
required=False,
178+
)
179+
180+
city = forms.CharField(max_length=64, required=False)
181+
state = forms.CharField(
182+
label="State/Province/Region", max_length=64, required=False
183+
)
184+
postal_code = forms.CharField(
185+
label="Zip/Postal Code", max_length=64, required=False
186+
)
187+
country = CountryField().formfield(required=False)
172188

173189
def __init__(self, *args, **kwargs):
174190
self.user = kwargs.pop("user", None)
@@ -203,6 +219,20 @@ def clean(self):
203219
msg = "You have to mark at least one contact as the primary one."
204220
raise forms.ValidationError(msg)
205221

222+
def clean_sponsor(self):
223+
sponsor = self.cleaned_data.get("sponsor")
224+
if not sponsor:
225+
return
226+
227+
if Sponsorship.objects.in_progress().filter(sponsor=sponsor).exists():
228+
msg = f"The sponsor {sponsor.name} already have open Sponsorship applications. "
229+
msg += f"Get in contact with {settings.SPONSORSHIP_NOTIFICATION_FROM_EMAIL} to discuss."
230+
raise forms.ValidationError(msg)
231+
232+
return sponsor
233+
234+
# Required fields are being manually validated because if the form
235+
# data has a Sponsor they shouldn't be required
206236
def clean_name(self):
207237
name = self.cleaned_data.get("name", "")
208238
sponsor = self.data.get("sponsor")
@@ -224,12 +254,33 @@ def clean_primary_phone(self):
224254
raise forms.ValidationError("This field is required.")
225255
return primary_phone.strip()
226256

227-
def clean_mailing_address(self):
228-
mailing_address = self.cleaned_data.get("mailing_address", "")
257+
def clean_mailing_address_line_1(self):
258+
mailing_address_line_1 = self.cleaned_data.get("mailing_address_line_1", "")
259+
sponsor = self.data.get("sponsor")
260+
if not sponsor and not mailing_address_line_1:
261+
raise forms.ValidationError("This field is required.")
262+
return mailing_address_line_1.strip()
263+
264+
def clean_city(self):
265+
city = self.cleaned_data.get("city", "")
266+
sponsor = self.data.get("sponsor")
267+
if not sponsor and not city:
268+
raise forms.ValidationError("This field is required.")
269+
return city.strip()
270+
271+
def clean_postal_code(self):
272+
postal_code = self.cleaned_data.get("postal_code", "")
273+
sponsor = self.data.get("sponsor")
274+
if not sponsor and not postal_code:
275+
raise forms.ValidationError("This field is required.")
276+
return postal_code.strip()
277+
278+
def clean_country(self):
279+
country = self.cleaned_data.get("country", "")
229280
sponsor = self.data.get("sponsor")
230-
if not sponsor and not mailing_address:
281+
if not sponsor and not country:
231282
raise forms.ValidationError("This field is required.")
232-
return mailing_address.strip()
283+
return country.strip()
233284

234285
def save(self):
235286
selected_sponsor = self.cleaned_data.get("sponsor")
@@ -240,7 +291,12 @@ def save(self):
240291
name=self.cleaned_data["name"],
241292
web_logo=self.cleaned_data["web_logo"],
242293
primary_phone=self.cleaned_data["primary_phone"],
243-
mailing_address=self.cleaned_data["mailing_address"],
294+
mailing_address_line_1=self.cleaned_data["mailing_address_line_1"],
295+
mailing_address_line_2=self.cleaned_data.get("mailing_address_line_2", ""),
296+
city=self.cleaned_data["city"],
297+
state=self.cleaned_data.get("state", ""),
298+
postal_code=self.cleaned_data["postal_code"],
299+
country=self.cleaned_data["country"],
244300
description=self.cleaned_data.get("description", ""),
245301
landing_page_url=self.cleaned_data.get("landing_page_url", ""),
246302
print_logo=self.cleaned_data.get("print_logo"),

sponsors/managers.py

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
from django.db.models.query import QuerySet
22

33

4-
class SponsorQuerySet(QuerySet):
5-
def draft(self):
6-
return self.filter(is_published=False)
7-
8-
def published(self):
9-
return self.filter(is_published=True)
10-
11-
def featured(self):
12-
return self.published().filter(featured=True)
4+
class SponsorshipQuerySet(QuerySet):
5+
def in_progress(self):
6+
status = [self.model.APPLIED, self.model.APPROVED]
7+
return self.filter(status__in=status)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Generated by Django 2.0.13 on 2020-11-19 14:48
2+
3+
from django.db import migrations, models
4+
import django_countries.fields
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("sponsors", "0015_auto_20201117_1739"),
11+
]
12+
13+
operations = [
14+
migrations.RemoveField(
15+
model_name="sponsor",
16+
name="mailing_address",
17+
),
18+
migrations.AddField(
19+
model_name="sponsor",
20+
name="city",
21+
field=models.CharField(default="", max_length=64, verbose_name="City"),
22+
),
23+
migrations.AddField(
24+
model_name="sponsor",
25+
name="country",
26+
field=django_countries.fields.CountryField(default="", max_length=2),
27+
),
28+
migrations.AddField(
29+
model_name="sponsor",
30+
name="mailing_address_line_1",
31+
field=models.CharField(
32+
default="", max_length=128, verbose_name="Mailing Address line 1"
33+
),
34+
),
35+
migrations.AddField(
36+
model_name="sponsor",
37+
name="mailing_address_line_2",
38+
field=models.CharField(
39+
blank=True,
40+
default="",
41+
max_length=128,
42+
verbose_name="Mailing Address line 2",
43+
),
44+
),
45+
migrations.AddField(
46+
model_name="sponsor",
47+
name="postal_code",
48+
field=models.CharField(
49+
default="", max_length=64, verbose_name="Zip/Postal Code"
50+
),
51+
),
52+
migrations.AddField(
53+
model_name="sponsor",
54+
name="state",
55+
field=models.CharField(
56+
blank=True,
57+
default="",
58+
max_length=64,
59+
verbose_name="State/Province/Region",
60+
),
61+
),
62+
]

sponsors/models.py

+22-4
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
from markupfield.fields import MarkupField
99
from ordered_model.models import OrderedModel, OrderedModelManager
1010
from allauth.account.admin import EmailAddress
11+
from django_countries.fields import CountryField
1112

1213
from cms.models import ContentManageable
1314
from companies.models import Company
1415

15-
from .managers import SponsorQuerySet
16+
from .managers import SponsorshipQuerySet
17+
from .exceptions import SponsorWithExistingApplicationException
1618

1719
DEFAULT_MARKUP_TYPE = getattr(settings, "DEFAULT_MARKUP_TYPE", "restructuredtext")
1820

@@ -233,6 +235,8 @@ class Sponsorship(models.Model):
233235
(FINALIZED, "Finalized"),
234236
]
235237

238+
objects = SponsorshipQuerySet.as_manager()
239+
236240
submited_by = models.ForeignKey(
237241
settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL
238242
)
@@ -267,6 +271,9 @@ def new(cls, sponsor, benefits, package=None, submited_by=None):
267271
elif not package:
268272
for_modified_package = True
269273

274+
if cls.objects.in_progress().filter(sponsor=sponsor).exists():
275+
raise SponsorWithExistingApplicationException(f"Sponsor pk: {sponsor.pk}")
276+
270277
sponsorship = cls.objects.create(
271278
submited_by=submited_by,
272279
sponsor=sponsor,
@@ -384,9 +391,20 @@ class Sponsor(ContentManageable):
384391
)
385392

386393
primary_phone = models.CharField("Sponsor Primary Phone", max_length=32)
387-
mailing_address = models.TextField("Sponsor Mailing/Billing Address")
388-
389-
objects = SponsorQuerySet.as_manager()
394+
mailing_address_line_1 = models.CharField(
395+
verbose_name="Mailing Address line 1", max_length=128, default=""
396+
)
397+
mailing_address_line_2 = models.CharField(
398+
verbose_name="Mailing Address line 2", max_length=128, blank=True, default=""
399+
)
400+
city = models.CharField(verbose_name="City", max_length=64, default="")
401+
state = models.CharField(
402+
verbose_name="State/Province/Region", max_length=64, blank=True, default=""
403+
)
404+
postal_code = models.CharField(
405+
verbose_name="Zip/Postal Code", max_length=64, default=""
406+
)
407+
country = CountryField(default="")
390408

391409
class Meta:
392410
verbose_name = "sponsor"

sponsors/notifications.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def notify(self, **kwargs):
2424
subject=self.get_subject(context),
2525
message=self.get_message(context),
2626
recipient_list=self.get_recipient_list(context),
27-
from_email=settings.DEFAULT_FROM_EMAIL,
27+
from_email=settings.SPONSORSHIP_NOTIFICATION_FROM_EMAIL,
2828
)
2929

3030

sponsors/tests/test_forms.py

+31-3
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,12 @@ def setUp(self):
155155
self.data = {
156156
"name": "CompanyX",
157157
"primary_phone": "+14141413131",
158-
"mailing_address": "4th street",
158+
"mailing_address_line_1": "4th street",
159+
"mailing_address_line_2": "424",
160+
"city": "New York",
161+
"state": "NY",
162+
"postal_code": "10212",
163+
"country": "US",
159164
"contact-0-name": "Bernardo",
160165
"contact-0-email": "bernardo@companyemail.com",
161166
"contact-0-phone": "+1999999999",
@@ -174,7 +179,10 @@ def test_required_fields(self):
174179
"name",
175180
"web_logo",
176181
"primary_phone",
177-
"mailing_address",
182+
"mailing_address_line_1",
183+
"city",
184+
"postal_code",
185+
"country",
178186
"__all__",
179187
]
180188

@@ -203,7 +211,13 @@ def test_create_sponsor_with_valid_data(self):
203211
self.assertEqual(sponsor.name, "CompanyX")
204212
self.assertTrue(sponsor.web_logo)
205213
self.assertEqual(sponsor.primary_phone, "+14141413131")
206-
self.assertEqual(sponsor.mailing_address, "4th street")
214+
self.assertEqual(sponsor.mailing_address_line_1, "4th street")
215+
self.assertEqual(sponsor.mailing_address_line_2, "424")
216+
self.assertEqual(sponsor.city, "New York")
217+
self.assertEqual(sponsor.state, "NY")
218+
self.assertEqual(sponsor.postal_code, "10212")
219+
self.assertEqual(sponsor.country, "US")
220+
self.assertEqual(sponsor.country.name, "United States of America")
207221
self.assertEqual(sponsor.description, "")
208222
self.assertIsNone(sponsor.print_logo.name)
209223
self.assertEqual(sponsor.landing_page_url, "")
@@ -257,6 +271,20 @@ def test_invalidate_form_if_user_selects_sponsort_from_other_user(self):
257271
self.assertIn("sponsor", form.errors)
258272
self.assertEqual(1, len(form.errors))
259273

274+
def test_invalidate_form_if_sponsor_with_sponsorships(self):
275+
contact = baker.make(SponsorContact, user__email="foo@foo.com")
276+
self.data = {"sponsor": contact.sponsor.id}
277+
278+
prev_sponsorship = baker.make("sponsors.Sponsorship", sponsor=contact.sponsor)
279+
form = SponsorshipApplicationForm(self.data, self.files, user=contact.user)
280+
self.assertFalse(form.is_valid())
281+
self.assertIn("sponsor", form.errors)
282+
283+
prev_sponsorship.status = prev_sponsorship.FINALIZED
284+
prev_sponsorship.save()
285+
form = SponsorshipApplicationForm(self.data, self.files, user=contact.user)
286+
self.assertTrue(form.is_valid())
287+
260288
def test_create_multiple_contacts_and_user_contact(self):
261289
user_email = "secondary@companyemail.com"
262290
self.data.update(

sponsors/tests/test_models.py

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.utils import timezone
77

88
from ..models import Sponsor, SponsorshipBenefit, Sponsorship
9+
from ..exceptions import SponsorWithExistingApplicationException
910

1011

1112
class SponsorshipBenefitModelTests(TestCase):
@@ -109,6 +110,26 @@ def test_approve_sponsorship(self):
109110
self.assertEqual(sponsorship.status, Sponsorship.APPROVED)
110111
self.assertEqual(sponsorship.approved_on, timezone.now().date())
111112

113+
def test_raise_exception_when_trying_to_create_sponsorship_for_same_sponsor(self):
114+
sponsorship = Sponsorship.new(self.sponsor, self.benefits)
115+
finalized_status = [Sponsorship.REJECTED, Sponsorship.FINALIZED]
116+
for status in finalized_status:
117+
sponsorship.status = status
118+
sponsorship.save()
119+
120+
new_sponsorship = Sponsorship.new(self.sponsor, self.benefits)
121+
new_sponsorship.refresh_from_db()
122+
self.assertTrue(new_sponsorship.pk)
123+
new_sponsorship.delete()
124+
125+
pending_status = [Sponsorship.APPLIED, Sponsorship.APPROVED]
126+
for status in pending_status:
127+
sponsorship.status = status
128+
sponsorship.save()
129+
130+
with self.assertRaises(SponsorWithExistingApplicationException):
131+
Sponsorship.new(self.sponsor, self.benefits)
132+
112133

113134
class SponsorshipPackageTests(TestCase):
114135
def setUp(self):

0 commit comments

Comments
 (0)