Skip to content

Commit b09feee

Browse files
authored
Orgs: Collect terms of service agreement before billing activation (#17376)
* Orgs: Collect terms of service agreement before billing activation Adds a checkbox before activating billing for paid orgs to confirm agreement with Terms of Service. Stores affirmative agreement on a model which also has provisions for recording notification of updated terms. * orgs: update price per seat, add metadata to relevant stripe objects * translations * simplify validator * translations
1 parent 47ee74b commit b09feee

File tree

13 files changed

+376
-61
lines changed

13 files changed

+376
-61
lines changed

tests/unit/manage/views/test_organizations.py

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,16 +1066,67 @@ def test_activate_subscription(
10661066
self,
10671067
db_request,
10681068
organization,
1069+
monkeypatch,
10691070
):
1071+
organization_activate_billing_form_obj = pretend.stub()
1072+
organization_activate_billing_form_cls = pretend.call_recorder(
1073+
lambda *a, **kw: organization_activate_billing_form_obj
1074+
)
1075+
monkeypatch.setattr(
1076+
org_views,
1077+
"OrganizationActivateBillingForm",
1078+
organization_activate_billing_form_cls,
1079+
)
1080+
db_request.POST = MultiDict()
1081+
10701082
view = org_views.ManageOrganizationBillingViews(organization, db_request)
10711083

1072-
# We're not ready for companies to activate their own subscriptions yet.
1073-
with pytest.raises(HTTPNotFound):
1074-
assert view.activate_subscription()
1084+
result = view.activate_subscription()
1085+
1086+
assert result == {
1087+
"organization": organization,
1088+
"form": organization_activate_billing_form_obj,
1089+
}
1090+
1091+
@pytest.mark.usefixtures("_enable_organizations")
1092+
def test_post_activate_subscription_valid(
1093+
self,
1094+
db_request,
1095+
organization,
1096+
monkeypatch,
1097+
):
1098+
db_request.method = "POST"
1099+
db_request.POST = MultiDict({"terms_of_service_agreement": "1"})
10751100

1076-
# result = view.activate_subscription()
1101+
db_request.route_path = pretend.call_recorder(
1102+
lambda *a, **kw: "mock-billing-url"
1103+
)
1104+
1105+
view = org_views.ManageOrganizationBillingViews(organization, db_request)
1106+
1107+
result = view.activate_subscription()
1108+
1109+
assert isinstance(result, HTTPSeeOther)
1110+
assert result.headers["Location"] == "mock-billing-url"
1111+
1112+
@pytest.mark.usefixtures("_enable_organizations")
1113+
def test_post_activate_subscription_invalid(
1114+
self,
1115+
db_request,
1116+
organization,
1117+
monkeypatch,
1118+
):
1119+
db_request.method = "POST"
1120+
db_request.POST = MultiDict()
1121+
1122+
view = org_views.ManageOrganizationBillingViews(organization, db_request)
10771123

1078-
# assert result == {"organization": organization}
1124+
result = view.activate_subscription()
1125+
1126+
assert result["organization"] == organization
1127+
assert result["form"].terms_of_service_agreement.errors == [
1128+
"Terms of Service must be accepted."
1129+
]
10791130

10801131
@pytest.mark.usefixtures("_enable_organizations")
10811132
def test_create_subscription(

tests/unit/organizations/test_services.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
OrganizationRoleType,
2828
OrganizationStripeCustomer,
2929
OrganizationStripeSubscription,
30+
OrganizationTermsOfServiceAgreement,
3031
OrganizationType,
3132
Team,
3233
TeamProjectRole,
@@ -634,6 +635,42 @@ def test_delete_organization_project(self, organization_service, db_request):
634635
.count()
635636
)
636637

638+
def test_add_organization_terms_of_service_agreement(
639+
self, organization_service, db_request
640+
):
641+
organization = OrganizationFactory.create()
642+
assert organization.terms_of_service_agreements == []
643+
organization_service.add_organization_terms_of_service_agreement(
644+
organization.id
645+
)
646+
assert (
647+
db_request.db.query(OrganizationTermsOfServiceAgreement)
648+
.filter(
649+
OrganizationTermsOfServiceAgreement.organization_id == organization.id,
650+
OrganizationTermsOfServiceAgreement.agreed.isnot(None),
651+
OrganizationTermsOfServiceAgreement.notified.is_(None),
652+
)
653+
.count()
654+
) == 1
655+
656+
def test_add_organization_terms_of_service_agreement_notified(
657+
self, organization_service, db_request
658+
):
659+
organization = OrganizationFactory.create()
660+
assert organization.terms_of_service_agreements == []
661+
organization_service.add_organization_terms_of_service_agreement(
662+
organization.id, notified=True
663+
)
664+
assert (
665+
db_request.db.query(OrganizationTermsOfServiceAgreement)
666+
.filter(
667+
OrganizationTermsOfServiceAgreement.organization_id == organization.id,
668+
OrganizationTermsOfServiceAgreement.agreed.is_(None),
669+
OrganizationTermsOfServiceAgreement.notified.isnot(None),
670+
)
671+
.count()
672+
) == 1
673+
637674
def test_add_organization_subscription(self, organization_service, db_request):
638675
organization = OrganizationFactory.create()
639676
stripe_customer = StripeCustomerFactory.create()

warehouse/locale/messages.pot

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ msgid ""
9191
msgstr ""
9292

9393
#: warehouse/accounts/forms.py:410 warehouse/manage/forms.py:139
94-
#: warehouse/manage/forms.py:730
94+
#: warehouse/manage/forms.py:741
9595
msgid "The name is too long. Choose a name with 100 characters or less."
9696
msgstr ""
9797

@@ -345,102 +345,102 @@ msgstr ""
345345
msgid "Banner Preview"
346346
msgstr ""
347347

348-
#: warehouse/manage/forms.py:408
348+
#: warehouse/manage/forms.py:419
349349
msgid "Choose an organization account name with 50 characters or less."
350350
msgstr ""
351351

352-
#: warehouse/manage/forms.py:416
352+
#: warehouse/manage/forms.py:427
353353
msgid ""
354354
"The organization account name is invalid. Organization account names must"
355355
" be composed of letters, numbers, dots, hyphens and underscores. And must"
356356
" also start and finish with a letter or number. Choose a different "
357357
"organization account name."
358358
msgstr ""
359359

360-
#: warehouse/manage/forms.py:439
360+
#: warehouse/manage/forms.py:450
361361
msgid ""
362362
"This organization account name has already been used. Choose a different "
363363
"organization account name."
364364
msgstr ""
365365

366-
#: warehouse/manage/forms.py:454
366+
#: warehouse/manage/forms.py:465
367367
msgid ""
368368
"You have already submitted an application for that name. Choose a "
369369
"different organization account name."
370370
msgstr ""
371371

372-
#: warehouse/manage/forms.py:490
372+
#: warehouse/manage/forms.py:501
373373
msgid "Select project"
374374
msgstr ""
375375

376-
#: warehouse/manage/forms.py:495 warehouse/oidc/forms/_core.py:23
376+
#: warehouse/manage/forms.py:506 warehouse/oidc/forms/_core.py:23
377377
#: warehouse/oidc/forms/gitlab.py:57
378378
msgid "Specify project name"
379379
msgstr ""
380380

381-
#: warehouse/manage/forms.py:498
381+
#: warehouse/manage/forms.py:509
382382
msgid ""
383383
"Start and end with a letter or numeral containing only ASCII numeric and "
384384
"'.', '_' and '-'."
385385
msgstr ""
386386

387-
#: warehouse/manage/forms.py:505
387+
#: warehouse/manage/forms.py:516
388388
msgid "This project name has already been used. Choose a different project name."
389389
msgstr ""
390390

391-
#: warehouse/manage/forms.py:578
391+
#: warehouse/manage/forms.py:589
392392
msgid ""
393393
"The organization name is too long. Choose a organization name with 100 "
394394
"characters or less."
395395
msgstr ""
396396

397-
#: warehouse/manage/forms.py:590
397+
#: warehouse/manage/forms.py:601
398398
msgid ""
399399
"The organization URL is too long. Choose a organization URL with 400 "
400400
"characters or less."
401401
msgstr ""
402402

403-
#: warehouse/manage/forms.py:597
403+
#: warehouse/manage/forms.py:608
404404
msgid "The organization URL must start with http:// or https://"
405405
msgstr ""
406406

407-
#: warehouse/manage/forms.py:608
407+
#: warehouse/manage/forms.py:619
408408
msgid ""
409409
"The organization description is too long. Choose a organization "
410410
"description with 400 characters or less."
411411
msgstr ""
412412

413-
#: warehouse/manage/forms.py:643
413+
#: warehouse/manage/forms.py:654
414414
msgid "You have already submitted the maximum number of "
415415
msgstr ""
416416

417-
#: warehouse/manage/forms.py:673
417+
#: warehouse/manage/forms.py:684
418418
msgid "Choose a team name with 50 characters or less."
419419
msgstr ""
420420

421-
#: warehouse/manage/forms.py:679
421+
#: warehouse/manage/forms.py:690
422422
msgid ""
423423
"The team name is invalid. Team names cannot start or end with a space, "
424424
"period, underscore, hyphen, or slash. Choose a different team name."
425425
msgstr ""
426426

427-
#: warehouse/manage/forms.py:707
427+
#: warehouse/manage/forms.py:718
428428
msgid "This team name has already been used. Choose a different team name."
429429
msgstr ""
430430

431-
#: warehouse/manage/forms.py:726
431+
#: warehouse/manage/forms.py:737
432432
msgid "Specify your alternate repository name"
433433
msgstr ""
434434

435-
#: warehouse/manage/forms.py:740
435+
#: warehouse/manage/forms.py:751
436436
msgid "Specify your alternate repository URL"
437437
msgstr ""
438438

439-
#: warehouse/manage/forms.py:744
439+
#: warehouse/manage/forms.py:755
440440
msgid "The URL is too long. Choose a URL with 400 characters or less."
441441
msgstr ""
442442

443-
#: warehouse/manage/forms.py:758
443+
#: warehouse/manage/forms.py:769
444444
msgid ""
445445
"The description is too long. Choose a description with 400 characters or "
446446
"less."
@@ -579,12 +579,12 @@ msgid ""
579579
msgstr ""
580580

581581
#: warehouse/manage/views/__init__.py:2817
582-
#: warehouse/manage/views/organizations.py:878
582+
#: warehouse/manage/views/organizations.py:887
583583
msgid "User '${username}' already has an active invite. Please try again later."
584584
msgstr ""
585585

586586
#: warehouse/manage/views/__init__.py:2882
587-
#: warehouse/manage/views/organizations.py:943
587+
#: warehouse/manage/views/organizations.py:952
588588
msgid "Invitation sent to '${username}'"
589589
msgstr ""
590590

@@ -597,30 +597,30 @@ msgid "Invitation already expired."
597597
msgstr ""
598598

599599
#: warehouse/manage/views/__init__.py:2958
600-
#: warehouse/manage/views/organizations.py:1130
600+
#: warehouse/manage/views/organizations.py:1139
601601
msgid "Invitation revoked from '${username}'."
602602
msgstr ""
603603

604-
#: warehouse/manage/views/organizations.py:854
604+
#: warehouse/manage/views/organizations.py:863
605605
msgid "User '${username}' already has ${role_name} role for organization"
606606
msgstr ""
607607

608-
#: warehouse/manage/views/organizations.py:865
608+
#: warehouse/manage/views/organizations.py:874
609609
msgid ""
610610
"User '${username}' does not have a verified primary email address and "
611611
"cannot be added as a ${role_name} for organization"
612612
msgstr ""
613613

614-
#: warehouse/manage/views/organizations.py:1026
615-
#: warehouse/manage/views/organizations.py:1068
614+
#: warehouse/manage/views/organizations.py:1035
615+
#: warehouse/manage/views/organizations.py:1077
616616
msgid "Could not find organization invitation."
617617
msgstr ""
618618

619-
#: warehouse/manage/views/organizations.py:1036
619+
#: warehouse/manage/views/organizations.py:1045
620620
msgid "Organization invitation could not be re-sent."
621621
msgstr ""
622622

623-
#: warehouse/manage/views/organizations.py:1083
623+
#: warehouse/manage/views/organizations.py:1092
624624
msgid "Expired invitation for '${username}' deleted."
625625
msgstr ""
626626

@@ -1432,6 +1432,7 @@ msgstr ""
14321432
#: warehouse/templates/manage/account/token.html:150
14331433
#: warehouse/templates/manage/account/totp-provision.html:69
14341434
#: warehouse/templates/manage/account/webauthn-provision.html:44
1435+
#: warehouse/templates/manage/organization/activate_subscription.html:34
14351436
#: warehouse/templates/manage/organization/projects.html:128
14361437
#: warehouse/templates/manage/organization/projects.html:151
14371438
#: warehouse/templates/manage/organization/roles.html:270
@@ -4059,7 +4060,7 @@ msgstr ""
40594060

40604061
#: warehouse/templates/manage/manage_base.html:366
40614062
#: warehouse/templates/manage/manage_base.html:418
4062-
#: warehouse/templates/manage/organization/activate_subscription.html:32
4063+
#: warehouse/templates/manage/organization/activate_subscription.html:52
40634064
msgid "Cancel"
40644065
msgstr ""
40654066

@@ -5124,17 +5125,28 @@ msgid ""
51245125
msgstr ""
51255126

51265127
#: warehouse/templates/manage/organization/activate_subscription.html:17
5127-
#: warehouse/templates/manage/organization/activate_subscription.html:21
5128-
#: warehouse/templates/manage/organization/activate_subscription.html:35
5128+
#: warehouse/templates/manage/organization/activate_subscription.html:22
5129+
#: warehouse/templates/manage/organization/activate_subscription.html:54
51295130
msgid "Activate Subscription"
51305131
msgstr ""
51315132

5132-
#: warehouse/templates/manage/organization/activate_subscription.html:27
5133+
#: warehouse/templates/manage/organization/activate_subscription.html:26
51335134
msgid ""
51345135
"Company accounts require an active subscription. Please enter up-to-date "
51355136
"billing information to enable the account."
51365137
msgstr ""
51375138

5139+
#: warehouse/templates/manage/organization/activate_subscription.html:33
5140+
msgid "Terms of Service"
5141+
msgstr ""
5142+
5143+
#: warehouse/templates/manage/organization/activate_subscription.html:37
5144+
#, python-format
5145+
msgid ""
5146+
"I agree to the PyPI <a href=\"%(tos_url)s\">Terms of Service</a> on "
5147+
"behalf of the %(organization_name)s organization."
5148+
msgstr ""
5149+
51385150
#: warehouse/templates/manage/organization/history.html:20
51395151
#: warehouse/templates/manage/project/history.html:20
51405152
#: warehouse/templates/manage/team/history.html:20

warehouse/manage/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,17 @@ def validate_macaroon_id(self, field):
382382
# /manage/organizations/ forms
383383

384384

385+
class OrganizationActivateBillingForm(wtforms.Form):
386+
terms_of_service_agreement = wtforms.BooleanField(
387+
validators=[
388+
wtforms.validators.DataRequired(
389+
message="Terms of Service must be accepted.",
390+
),
391+
],
392+
default=False,
393+
)
394+
395+
385396
class OrganizationRoleNameMixin:
386397
role_name = wtforms.SelectField(
387398
"Select role",

0 commit comments

Comments
 (0)