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

Commita256e96

Browse files
authored
Finalize sponsorships with existing contracts (python#1845)
* Sponsorship approval should also log contract creation* Fix test names to be more accurate* Create use case to approve contract using existing file* Sponsorship approval form must require all data* Create new view to approve a sponsorship within a signed contract* Add links to generate new contract or upload existing one when approving sponsorship* Add links to easily navigate between sponsorships and contracts within admin* Update Contract model to always use random names to signed documents
1 parentfc9cc47 commita256e96

File tree

11 files changed

+340
-23
lines changed

11 files changed

+340
-23
lines changed

‎sponsors/admin.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
fromdjango.templateimportContext,Template
55
fromdjango.contribimportadmin
66
fromdjango.contrib.humanize.templatetags.humanizeimportintcomma
7-
fromdjango.urlsimportpath
7+
fromdjango.urlsimportpath,reverse
88
fromdjango.utils.htmlimportmark_safe
99

1010
from .modelsimport (
@@ -217,6 +217,7 @@ class SponsorshipAdmin(admin.ModelAdmin):
217217
"get_estimated_cost",
218218
"start_date",
219219
"end_date",
220+
"get_contract"
220221
),
221222
},
222223
),
@@ -267,6 +268,7 @@ def get_readonly_fields(self, request, obj):
267268
"get_sponsor_primary_phone",
268269
"get_sponsor_mailing_address",
269270
"get_sponsor_contacts",
271+
"get_contract",
270272
]
271273

272274
ifobjandobj.status!=Sponsorship.APPLIED:
@@ -287,9 +289,16 @@ def get_estimated_cost(self, obj):
287289
cost=intcomma(obj.estimated_cost)
288290
html=f"{cost} USD <br/><b>Important: </b>{msg}"
289291
returnmark_safe(html)
290-
291292
get_estimated_cost.short_description="Estimated cost"
292293

294+
defget_contract(self,obj):
295+
ifnotobj.contract:
296+
return"---"
297+
url=reverse("admin:sponsors_contract_change",args=[obj.contract.pk])
298+
html=f"<a href='{url}' target='_blank'>{obj.contract}</a>"
299+
returnmark_safe(html)
300+
get_contract.short_description="Contract"
301+
293302
defget_urls(self):
294303
urls=super().get_urls()
295304
my_urls= [
@@ -300,6 +309,11 @@ def get_urls(self):
300309
self.admin_site.admin_view(self.reject_sponsorship_view),
301310
name="sponsors_sponsorship_reject",
302311
),
312+
path(
313+
"<int:pk>/approve-existing",
314+
self.admin_site.admin_view(self.approve_signed_sponsorship_view),
315+
name="sponsors_sponsorship_approve_existing_contract",
316+
),
303317
path(
304318
"<int:pk>/approve",
305319
self.admin_site.admin_view(self.approve_sponsorship_view),
@@ -403,6 +417,9 @@ def reject_sponsorship_view(self, request, pk):
403417
defapprove_sponsorship_view(self,request,pk):
404418
returnviews_admin.approve_sponsorship_view(self,request,pk)
405419

420+
defapprove_signed_sponsorship_view(self,request,pk):
421+
returnviews_admin.approve_signed_sponsorship_view(self,request,pk)
422+
406423

407424
@admin.register(LegalClause)
408425
classLegalClauseModelAdmin(OrderedModelAdmin):
@@ -435,7 +452,7 @@ def get_revision(self, obj):
435452
(
436453
"Info",
437454
{
438-
"fields": ("sponsorship","status","revision"),
455+
"fields": ("get_sponsorship_url","status","revision"),
439456
},
440457
),
441458
(
@@ -480,6 +497,7 @@ def get_readonly_fields(self, request, obj):
480497
"sponsorship",
481498
"revision",
482499
"document",
500+
"get_sponsorship_url",
483501
]
484502

485503
ifobjandnotobj.is_draft:
@@ -509,9 +527,17 @@ def document_link(self, obj):
509527
ifurlandmsg:
510528
html=f'<a href="{url}" target="_blank">{msg}</a>'
511529
returnmark_safe(html)
512-
513530
document_link.short_description="Contract document"
514531

532+
533+
defget_sponsorship_url(self,obj):
534+
ifnotobj.sponsorship:
535+
return"---"
536+
url=reverse("admin:sponsors_sponsorship_change",args=[obj.sponsorship.pk])
537+
html=f"<a href='{url}' target='_blank'>{obj.sponsorship}</a>"
538+
returnmark_safe(html)
539+
get_sponsorship_url.short_description="Sponsorship"
540+
515541
defget_urls(self):
516542
urls=super().get_urls()
517543
my_urls= [

‎sponsors/forms.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,13 @@ def clean(self):
361361
returncleaned_data
362362

363363

364+
classSignedSponsorshipReviewAdminForm(SponsorshipReviewAdminForm):
365+
"""
366+
Form to approve sponsorships that already have a signed contract
367+
"""
368+
signed_contract=forms.FileField(help_text="Please upload the final version of the signed contract.")
369+
370+
364371
classSponsorBenefitAdminInlineForm(forms.ModelForm):
365372
sponsorship_benefit=forms.ModelChoiceField(
366373
queryset=SponsorshipBenefit.objects.order_by('program','order').select_related("program"),

‎sponsors/models.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
importuuid
12
fromabcimportABC
23
frompathlibimportPath
34
fromitertoolsimportchain
@@ -697,6 +698,16 @@ class Meta(OrderedModel.Meta):
697698
pass
698699

699700

701+
defsigned_contract_random_path(instance,filename):
702+
"""
703+
Use random UUID to name signed contracts
704+
"""
705+
dir=instance.SIGNED_PDF_DIR
706+
ext="".join(Path(filename).suffixes)
707+
name=uuid.uuid4()
708+
returnf"{dir}{name}{ext}"
709+
710+
700711
classContract(models.Model):
701712
"""
702713
Contract model to oficialize a Sponsorship
@@ -729,7 +740,7 @@ class Contract(models.Model):
729740
verbose_name="Unsigned PDF",
730741
)
731742
signed_document=models.FileField(
732-
upload_to=SIGNED_PDF_DIR,
743+
upload_to=signed_contract_random_path,
733744
blank=True,
734745
verbose_name="Signed PDF",
735746
)
@@ -872,8 +883,8 @@ def set_final_version(self, pdf_file):
872883
self.status=self.AWAITING_SIGNATURE
873884
self.save()
874885

875-
defexecute(self,commit=True):
876-
ifself.EXECUTEDnotinself.next_status:
886+
defexecute(self,commit=True,force=False):
887+
ifnotforceandself.EXECUTEDnotinself.next_status:
877888
msg=f"Can't execute a{self.get_status_display()} contract."
878889
raiseInvalidStatusException(msg)
879890

‎sponsors/notifications.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
fromdjango.core.mailimportEmailMessage
22
fromdjango.template.loaderimportrender_to_string
33
fromdjango.confimportsettings
4-
fromdjango.contrib.admin.modelsimportLogEntry,CHANGE
4+
fromdjango.contrib.admin.modelsimportLogEntry,CHANGE,ADDITION
55
fromdjango.contrib.contenttypes.modelsimportContentType
66

77
fromsponsors.modelsimportSponsorship,Contract
@@ -110,7 +110,7 @@ def get_attachments(self, context):
110110

111111
classSponsorshipApprovalLogger():
112112

113-
defnotify(self,request,sponsorship,**kwargs):
113+
defnotify(self,request,sponsorship,contract,**kwargs):
114114
LogEntry.objects.log_action(
115115
user_id=request.user.id,
116116
content_type_id=ContentType.objects.get_for_model(Sponsorship).pk,
@@ -119,6 +119,14 @@ def notify(self, request, sponsorship, **kwargs):
119119
action_flag=CHANGE,
120120
change_message="Sponsorship Approval"
121121
)
122+
LogEntry.objects.log_action(
123+
user_id=request.user.id,
124+
content_type_id=ContentType.objects.get_for_model(Contract).pk,
125+
object_id=contract.pk,
126+
object_repr=str(contract),
127+
action_flag=ADDITION,
128+
change_message="Created After Sponsorship Approval"
129+
)
122130

123131

124132
classSentContractLogger():
@@ -147,6 +155,19 @@ def notify(self, request, contract, **kwargs):
147155
)
148156

149157

158+
classExecutedExistingContractLogger():
159+
160+
defnotify(self,request,contract,**kwargs):
161+
LogEntry.objects.log_action(
162+
user_id=request.user.id,
163+
content_type_id=ContentType.objects.get_for_model(Contract).pk,
164+
object_id=contract.pk,
165+
object_repr=str(contract),
166+
action_flag=CHANGE,
167+
change_message="Existing Contract Uploaded and Executed"
168+
)
169+
170+
150171
classNullifiedContractLogger():
151172

152173
defnotify(self,request,contract,**kwargs):

‎sponsors/tests/test_notifications.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
fromdjango.coreimportmail
66
fromdjango.template.loaderimportrender_to_string
77
fromdjango.testimportTestCase,RequestFactory
8-
fromdjango.contrib.admin.modelsimportLogEntry,CHANGE
8+
fromdjango.contrib.admin.modelsimportLogEntry,CHANGE,ADDITION
9+
fromdjango.contrib.contenttypes.modelsimportContentType
910

1011
fromsponsorsimportnotifications
1112
fromsponsors.modelsimportSponsorship,Contract
@@ -233,24 +234,34 @@ def setUp(self):
233234
self.request=RequestFactory().get('/')
234235
self.request.user=baker.make(settings.AUTH_USER_MODEL)
235236
self.sponsorship=baker.make(Sponsorship,status=Sponsorship.APPROVED,sponsor__name='foo',_fill_optional=True)
237+
self.contract=baker.make_recipe("sponsors.tests.empty_contract",sponsorship=self.sponsorship)
236238
self.kwargs= {
237239
"request":self.request,
238240
"sponsorship":self.sponsorship,
241+
"contract":self.contract
239242
}
240243
self.logger=notifications.SponsorshipApprovalLogger()
241244

242245
deftest_create_log_entry_for_change_operation_with_approval_message(self):
243246
self.assertEqual(LogEntry.objects.count(),0)
247+
sponsorship_content_id=ContentType.objects.get_for_model(Sponsorship).pk
248+
contract_id=ContentType.objects.get_for_model(Contract).pk
244249

245250
self.logger.notify(**self.kwargs)
246251

247-
self.assertEqual(LogEntry.objects.count(),1)
248-
log_entry=LogEntry.objects.get()
252+
self.assertEqual(LogEntry.objects.count(),2)
253+
log_entry=LogEntry.objects.get(content_type_id=sponsorship_content_id)
249254
self.assertEqual(log_entry.user,self.request.user)
250255
self.assertEqual(log_entry.object_id,str(self.sponsorship.pk))
251256
self.assertEqual(str(self.sponsorship),log_entry.object_repr)
252257
self.assertEqual(log_entry.action_flag,CHANGE)
253258
self.assertEqual(log_entry.change_message,"Sponsorship Approval")
259+
log_entry=LogEntry.objects.get(content_type_id=contract_id)
260+
self.assertEqual(log_entry.user,self.request.user)
261+
self.assertEqual(log_entry.object_id,str(self.contract.pk))
262+
self.assertEqual(str(self.contract),log_entry.object_repr)
263+
self.assertEqual(log_entry.action_flag,ADDITION)
264+
self.assertEqual(log_entry.change_message,"Created After Sponsorship Approval")
254265

255266

256267
classSentContractLoggerTests(TestCase):
@@ -305,6 +316,32 @@ def test_create_log_entry_for_change_operation_with_approval_message(self):
305316
self.assertEqual(log_entry.change_message,"Contract Executed")
306317

307318

319+
classExecutedExistingContractLoggerTests(TestCase):
320+
321+
defsetUp(self):
322+
self.request=RequestFactory().get('/')
323+
self.request.user=baker.make(settings.AUTH_USER_MODEL)
324+
self.contract=baker.make_recipe('sponsors.tests.empty_contract')
325+
self.kwargs= {
326+
"request":self.request,
327+
"contract":self.contract,
328+
}
329+
self.logger=notifications.ExecutedExistingContractLogger()
330+
331+
deftest_create_log_entry_for_change_operation_with_approval_message(self):
332+
self.assertEqual(LogEntry.objects.count(),0)
333+
334+
self.logger.notify(**self.kwargs)
335+
336+
self.assertEqual(LogEntry.objects.count(),1)
337+
log_entry=LogEntry.objects.get()
338+
self.assertEqual(log_entry.user,self.request.user)
339+
self.assertEqual(log_entry.object_id,str(self.contract.pk))
340+
self.assertEqual(str(self.contract),log_entry.object_repr)
341+
self.assertEqual(log_entry.action_flag,CHANGE)
342+
self.assertEqual(log_entry.change_message,"Existing Contract Uploaded and Executed")
343+
344+
308345
classNullifiedContractLoggerTests(TestCase):
309346

310347
defsetUp(self):

‎sponsors/tests/test_use_cases.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
fromunittest.mockimportMock
1+
fromunittest.mockimportMock,patch
22
frommodel_bakeryimportbaker
33
fromdatetimeimporttimedelta,date
44

55
fromdjango.confimportsettings
66
fromdjango.testimportTestCase
77
fromdjango.utilsimporttimezone
8+
fromdjango.core.files.uploadedfileimportSimpleUploadedFile
89

910
fromsponsorsimportuse_cases
1011
fromsponsors.notificationsimport*
@@ -122,7 +123,7 @@ def test_send_notifications_using_sponsorship(self):
122123
contract=self.sponsorship.contract,
123124
)
124125

125-
deftest_build_use_case_without_notificationss(self):
126+
deftest_build_use_case_with_default_notificationss(self):
126127
uc=use_cases.ApproveSponsorshipApplicationUseCase.build()
127128
self.assertEqual(len(uc.notifications),1)
128129
self.assertIsInstance(uc.notifications[0],SponsorshipApprovalLogger)
@@ -147,7 +148,7 @@ def test_send_and_update_contract_with_document(self):
147148
contract=self.contract,
148149
)
149150

150-
deftest_build_use_case_without_notificationss(self):
151+
deftest_build_use_case_with_default_notificationss(self):
151152
uc=use_cases.SendContractUseCase.build()
152153
self.assertEqual(len(uc.notifications),2)
153154
self.assertIsInstance(uc.notifications[0],ContractNotificationToPSF)
@@ -168,14 +169,38 @@ def test_execute_and_update_database_object(self):
168169
self.contract.refresh_from_db()
169170
self.assertEqual(self.contract.status,Contract.EXECUTED)
170171

171-
deftest_build_use_case_without_notificationss(self):
172+
deftest_build_use_case_with_default_notificationss(self):
172173
uc=use_cases.ExecuteContractUseCase.build()
173174
self.assertEqual(len(uc.notifications),1)
174175
self.assertIsInstance(
175176
uc.notifications[0],ExecutedContractLogger
176177
)
177178

178179

180+
classExecuteExistingContractUseCaseTests(TestCase):
181+
defsetUp(self):
182+
self.notifications= [Mock()]
183+
self.use_case=use_cases.ExecuteExistingContractUseCase(self.notifications)
184+
self.user=baker.make(settings.AUTH_USER_MODEL)
185+
self.file=SimpleUploadedFile("contract.txt",b"Contract content")
186+
self.contract=baker.make_recipe("sponsors.tests.empty_contract",status=Contract.DRAFT)
187+
188+
@patch("sponsors.models.uuid.uuid4",Mock(return_value="1234"))
189+
deftest_execute_and_update_database_object(self):
190+
self.use_case.execute(self.contract,self.file)
191+
self.contract.refresh_from_db()
192+
self.assertEqual(self.contract.status,Contract.EXECUTED)
193+
self.assertEqual(b"Contract content",self.contract.signed_document.read())
194+
self.assertEqual(f"{Contract.SIGNED_PDF_DIR}1234.txt",self.contract.signed_document.name)
195+
196+
deftest_build_use_case_with_default_notificationss(self):
197+
uc=use_cases.ExecuteExistingContractUseCase.build()
198+
self.assertEqual(len(uc.notifications),1)
199+
self.assertIsInstance(
200+
uc.notifications[0],ExecutedExistingContractLogger
201+
)
202+
203+
179204
classNullifyContractUseCaseTests(TestCase):
180205
defsetUp(self):
181206
self.notifications= [Mock()]
@@ -188,7 +213,7 @@ def test_nullify_and_update_database_object(self):
188213
self.contract.refresh_from_db()
189214
self.assertEqual(self.contract.status,Contract.NULLIFIED)
190215

191-
deftest_build_use_case_without_notificationss(self):
216+
deftest_build_use_case_with_default_notificationss(self):
192217
uc=use_cases.NullifyContractUseCase.build()
193218
self.assertEqual(len(uc.notifications),1)
194219
self.assertIsInstance(

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp