Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitf99aa6d

Browse files
authored
Sponsorships notifications (python#1869)
* Sort apps by name* Add missing migration after help text change* Add new app to handle custom email dispatching* Add new model to configure sponsor notifications* Minimal admin* Update admin form to validate content as django template* Add button to preview how template will render* Add new benefit configuration to flag email targeatable* Add method to filter sponsorships by included features* Enable user to select which notification template to use* Rename variable* Display warning message if selected sponsorships aren't targetable* Introduce indirection with use case to send the emails* Implement method to create a EmailMessage from a notification template* Display non targetable sponsorship as checkbox instead of text* Add select all/delete all links* Filter emails by benefits, not feature configuration* Better display for notification objects* Add checkbox to select contact type* Update get_message method to accept boolean flags to control recipients* Rename form field name* Send notification to sponsors* Register email dispatch with admin log entry activity* Add input for custom email content* Display input for custom email content* UC expects sponsorship object, not PK* Consider email subject as a template as well* Refactor to move specific email building part to mailing app* Remove warning message* Optimizes sponsorship admin query* Add option to preview notification* Fix parameters names
1 parent9dc7cbe commitf99aa6d

27 files changed

+934
-36
lines changed

‎mailing/__init__.py

Whitespace-only changes.

‎mailing/admin.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
fromdjango.contribimportadmin
2+
fromdjango.forms.modelsimportmodelform_factory
3+
fromdjango.httpimportHttpResponse
4+
fromdjango.urlsimportpath
5+
fromdjango.shortcutsimportget_object_or_404
6+
7+
frommailing.formsimportBaseEmailTemplateForm
8+
9+
10+
classBaseEmailTemplateAdmin(admin.ModelAdmin):
11+
change_form_template="mailing/admin/base_email_template_form.html"
12+
list_display= ["internal_name","subject"]
13+
readonly_fields= ["created_at","updated_at"]
14+
search_fields= ["internal_name"]
15+
fieldsets= (
16+
(None, {
17+
'fields': ('internal_name',)
18+
}),
19+
('Email template', {
20+
'fields': ('subject','content')
21+
}),
22+
('Timestamps', {
23+
'classes': ('collapse',),
24+
'fields': ('created_at','updated_at'),
25+
}),
26+
)
27+
28+
defget_form(self,*args,**kwargs):
29+
kwargs["form"]=modelform_factory(self.model,form=BaseEmailTemplateForm)
30+
returnsuper().get_form(*args,**kwargs)
31+
32+
defget_urls(self):
33+
urls=super().get_urls()
34+
prefix=self.model._meta.db_table
35+
my_urls= [
36+
path(
37+
"<int:pk>/preview-content/$",
38+
self.admin_site.admin_view(self.preview_email_template),
39+
name=f"{prefix}_preview",
40+
),
41+
]
42+
returnmy_urls+urls
43+
44+
defpreview_email_template(self,request,pk,*args,**kwargs):
45+
qs=self.get_queryset(request)
46+
template=get_object_or_404(qs,pk=pk)
47+
returnHttpResponse(template.render_content({}))

‎mailing/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fromdjango.appsimportAppConfig
2+
3+
4+
classMailingConfig(AppConfig):
5+
name='mailing'

‎mailing/forms.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
fromdjangoimportforms
2+
fromdjango.templateimportTemplate,Context,TemplateSyntaxError
3+
4+
frommailing.modelsimportBaseEmailTemplate
5+
6+
7+
classBaseEmailTemplateForm(forms.ModelForm):
8+
9+
defclean_content(self):
10+
content=self.cleaned_data["content"]
11+
try:
12+
template=Template(content)
13+
template.render(Context({}))
14+
returncontent
15+
exceptTemplateSyntaxErrorase:
16+
raiseforms.ValidationError(e)
17+
18+
classMeta:
19+
model=BaseEmailTemplate
20+
fields="__all__"

‎mailing/migrations/__init__.py

Whitespace-only changes.

‎mailing/models.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
fromdjango.core.mailimportEmailMessage
2+
fromdjango.dbimportmodels
3+
fromdjango.templateimportTemplate,Context
4+
fromdjango.urlsimportreverse
5+
6+
7+
classBaseEmailTemplate(models.Model):
8+
internal_name=models.CharField(max_length=128)
9+
10+
subject=models.CharField(max_length=128)
11+
content=models.TextField()
12+
13+
created_at=models.DateTimeField(auto_now_add=True)
14+
updated_at=models.DateTimeField(auto_now=True)
15+
16+
@property
17+
defpreview_content_url(self):
18+
prefix=self._meta.db_table
19+
url_name=f"admin:{prefix}_preview"
20+
returnreverse(url_name,args=[self.pk])
21+
22+
defrender_content(self,context):
23+
template=Template(self.content)
24+
ctx=Context(context)
25+
returntemplate.render(ctx)
26+
27+
defrender_subject(self,context):
28+
template=Template(self.subject)
29+
ctx=Context(context)
30+
returntemplate.render(ctx)
31+
32+
defget_email(self,from_email,to,context=None,**kwargs):
33+
context=contextor {}
34+
context=self.get_email_context_data(**context)
35+
subject=self.render_subject(context)
36+
content=self.render_content(context)
37+
returnEmailMessage(subject,content,from_email,to,**kwargs)
38+
39+
defget_email_context_data(self,**kwargs):
40+
returnkwargs
41+
42+
classMeta:
43+
abstract=True
44+
45+
def__str__(self):
46+
returnf"Email template:{self.internal_name}"

‎mailing/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Create your tests here

‎mailing/tests/test_forms.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
fromdjango.testimportTestCase
2+
3+
frommailing.formsimportBaseEmailTemplateForm
4+
5+
6+
classBaseEmailTemplateFormTests(TestCase):
7+
8+
defsetUp(self):
9+
self.data= {
10+
"content":"Hi, {{ name }}\n\nThis is a message to you.",
11+
"subject":"Hello",
12+
"internal_name":"notification 01",
13+
}
14+
15+
deftest_validate_required_fields(self):
16+
required=set(self.data)
17+
form=BaseEmailTemplateForm(data={})
18+
self.assertFalse(form.is_valid())
19+
self.assertEqual(required,set(form.errors))
20+
21+
deftest_validate_with_correct_data(self):
22+
form=BaseEmailTemplateForm(data=self.data)
23+
self.assertTrue(form.is_valid())
24+
25+
deftest_invalid_form_if_broken_template_syntax(self):
26+
self.data["content"]="Invalid syntax {% invalid %}"
27+
form=BaseEmailTemplateForm(data=self.data)
28+
self.assertFalse(form.is_valid())
29+
self.assertIn("content",form.errors,form.errors)

‎pydotorg/settings/base.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,24 +167,25 @@
167167
'easy_pdf',
168168
'sorl.thumbnail',
169169

170-
'users',
170+
'banners',
171+
'blogs',
171172
'boxes',
172173
'cms',
173-
'companies',
174+
'codesamples',
174175
'community',
176+
'companies',
177+
'downloads',
178+
'events',
175179
'jobs',
180+
'mailing',
181+
'minutes',
182+
'nominations',
176183
'pages',
184+
'peps',
177185
'sponsors',
178186
'successstories',
179-
'events',
180-
'minutes',
181-
'peps',
182-
'blogs',
183-
'downloads',
184-
'codesamples',
187+
'users',
185188
'work_groups',
186-
'nominations',
187-
'banners',
188189

189190
'allauth',
190191
'allauth.account',

‎sponsors/admin.py

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
fromordered_model.adminimportOrderedModelAdmin
22
frompolymorphic.adminimportPolymorphicInlineSupportMixin,StackedPolymorphicInline
33

4+
fromdjango.db.modelsimportSubquery
45
fromdjango.templateimportContext,Template
56
fromdjango.contribimportadmin
67
fromdjango.contrib.humanize.templatetags.humanizeimportintcomma
78
fromdjango.urlsimportpath,reverse
9+
fromdjango.utils.functionalimportcached_property
810
fromdjango.utils.htmlimportmark_safe
911

12+
frommailing.adminimportBaseEmailTemplateAdmin
1013
from .modelsimport (
1114
SponsorshipPackage,
1215
SponsorshipProgram,
@@ -17,9 +20,12 @@
1720
SponsorBenefit,
1821
LegalClause,
1922
Contract,
23+
BenefitFeature,
2024
BenefitFeatureConfiguration,
2125
LogoPlacementConfiguration,
2226
TieredQuantityConfiguration,
27+
EmailTargetableConfiguration,
28+
SponsorEmailNotificationTemplate,
2329
)
2430
fromsponsorsimportviews_admin
2531
fromsponsors.formsimportSponsorshipReviewAdminForm,SponsorBenefitAdminInlineForm
@@ -42,10 +48,18 @@ class LogoPlacementConfigurationInline(StackedPolymorphicInline.Child):
4248
classTieredQuantityConfigurationInline(StackedPolymorphicInline.Child):
4349
model=TieredQuantityConfiguration
4450

51+
classEmailTargetableConfigurationInline(StackedPolymorphicInline.Child):
52+
model=EmailTargetableConfiguration
53+
readonly_fields= ["display"]
54+
55+
defdisplay(self,obj):
56+
return"Enabled"
57+
4558
model=BenefitFeatureConfiguration
4659
child_inlines= [
4760
LogoPlacementConfigurationInline,
4861
TieredQuantityConfigurationInline,
62+
EmailTargetableConfigurationInline,
4963
]
5064

5165

@@ -159,6 +173,31 @@ def has_delete_permission(self, request, obj=None):
159173
returnobj.open_for_editing
160174

161175

176+
classTargetableEmailBenefitsFilter(admin.SimpleListFilter):
177+
title="targetable email benefits"
178+
parameter_name='email_benefit'
179+
180+
@cached_property
181+
defbenefits(self):
182+
qs=EmailTargetableConfiguration.objects.all().values_list("benefit_id",flat=True)
183+
benefits=SponsorshipBenefit.objects.filter(id__in=Subquery(qs))
184+
return {str(b.id):bforbinbenefits}
185+
186+
deflookups(self,request,model_admin):
187+
return [
188+
(k,b.name)fork,binself.benefits.items()
189+
]
190+
191+
defqueryset(self,request,queryset):
192+
benefit=self.benefits.get(self.value())
193+
ifnotbenefit:
194+
returnqueryset
195+
# all sponsors benefit related with such sponsorship benefit
196+
qs=SponsorBenefit.objects.filter(
197+
sponsorship_benefit_id=benefit.id).values_list("sponsorship_id",flat=True)
198+
returnqueryset.filter(id__in=Subquery(qs))
199+
200+
162201
@admin.register(Sponsorship)
163202
classSponsorshipAdmin(admin.ModelAdmin):
164203
change_form_template="sponsors/admin/sponsorship_change_form.html"
@@ -174,8 +213,8 @@ class SponsorshipAdmin(admin.ModelAdmin):
174213
"start_date",
175214
"end_date",
176215
]
177-
list_filter= ["status","package"]
178-
216+
list_filter= ["status","package",TargetableEmailBenefitsFilter]
217+
actions= ["send_notifications"]
179218
fieldsets= [
180219
(
181220
"Sponsorship Data",
@@ -223,6 +262,14 @@ class SponsorshipAdmin(admin.ModelAdmin):
223262
),
224263
]
225264

265+
defget_queryset(self,*args,**kwargs):
266+
qs=super().get_queryset(*args,**kwargs)
267+
returnqs.select_related("sponsor","package")
268+
269+
defsend_notifications(self,request,queryset):
270+
returnviews_admin.send_sponsorship_notifications_action(self,request,queryset)
271+
send_notifications.short_description='Send notifications to selected'
272+
226273
defget_readonly_fields(self,request,obj):
227274
readonly_fields= [
228275
"for_modified_package",
@@ -251,10 +298,6 @@ def get_readonly_fields(self, request, obj):
251298

252299
returnreadonly_fields
253300

254-
defget_queryset(self,*args,**kwargs):
255-
qs=super().get_queryset(*args,**kwargs)
256-
returnqs.select_related("sponsor")
257-
258301
defget_estimated_cost(self,obj):
259302
cost=None
260303
html="This sponsorship has not customizations so there's no estimated cost"
@@ -303,17 +346,14 @@ def get_urls(self):
303346

304347
defget_sponsor_name(self,obj):
305348
returnobj.sponsor.name
306-
307349
get_sponsor_name.short_description="Name"
308350

309351
defget_sponsor_description(self,obj):
310352
returnobj.sponsor.description
311-
312353
get_sponsor_description.short_description="Description"
313354

314355
defget_sponsor_landing_page_url(self,obj):
315356
returnobj.sponsor.landing_page_url
316-
317357
get_sponsor_landing_page_url.short_description="Landing Page URL"
318358

319359
defget_sponsor_web_logo(self,obj):
@@ -322,7 +362,6 @@ def get_sponsor_web_logo(self, obj):
322362
context=Context({'sponsor':obj.sponsor})
323363
html=template.render(context)
324364
returnmark_safe(html)
325-
326365
get_sponsor_web_logo.short_description="Web Logo"
327366

328367
defget_sponsor_print_logo(self,obj):
@@ -334,12 +373,10 @@ def get_sponsor_print_logo(self, obj):
334373
context=Context({'img':img})
335374
html=template.render(context)
336375
returnmark_safe(html)ifhtmlelse"---"
337-
338376
get_sponsor_print_logo.short_description="Print Logo"
339377

340378
defget_sponsor_primary_phone(self,obj):
341379
returnobj.sponsor.primary_phone
342-
343380
get_sponsor_primary_phone.short_description="Primary Phone"
344381

345382
defget_sponsor_mailing_address(self,obj):
@@ -358,7 +395,6 @@ def get_sponsor_mailing_address(self, obj):
358395
html+=f"<p>{mail_row}</p>"
359396
html+=f"<p>{sponsor.postal_code}</p>"
360397
returnmark_safe(html)
361-
362398
get_sponsor_mailing_address.short_description="Mailing/Billing Address"
363399

364400
defget_sponsor_contacts(self,obj):
@@ -379,7 +415,6 @@ def get_sponsor_contacts(self, obj):
379415
)
380416
html+="</ul>"
381417
returnmark_safe(html)
382-
383418
get_sponsor_contacts.short_description="Contacts"
384419

385420
defrollback_to_editing_view(self,request,pk):
@@ -551,3 +586,8 @@ def execute_contract_view(self, request, pk):
551586

552587
defnullify_contract_view(self,request,pk):
553588
returnviews_admin.nullify_contract_view(self,request,pk)
589+
590+
591+
@admin.register(SponsorEmailNotificationTemplate)
592+
classSponsorEmailNotificationTemplateAdmin(BaseEmailTemplateAdmin):
593+
pass

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp