Merge lp:~sinzui/launchpad/team-renewal-0 into lp:launchpad

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: 11620
Proposed branch: lp:~sinzui/launchpad/team-renewal-0
Merge into: lp:launchpad
Diff against target: 329 lines (+111/-102)
4 files modified
lib/lp/registry/interfaces/person.py (+17/-7)
lib/lp/registry/stories/team/xx-team-edit.txt (+10/-27)
lib/lp/registry/stories/teammembership/xx-new-team.txt (+0/-68)
lib/lp/registry/tests/test_team.py (+84/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/team-renewal-0
Reviewer Review Type Date Requested Status
Edwin Grubbs (community) code Approve
Review via email: mp+36246@code.launchpad.net

Description of the change

This is my branch to prevent impossibly default renewal periods.

    lp:~sinzui/launchpad/team-renewal-0
    Diff size: 146
    Launchpad bug:
          https://bugs.launchpad.net/bugs/630907
    Test command: ./bin/test -vv \
          -t TestDefaultRenewalPeriodIsRequiredForSomeTeams
    Pre-implementation: no one
    Target release: 10.10

Prevent impossibly default renewal periods
------------------------------------------

A user entered an integer that exceeds the expectations of postgresql;
The default renewal period for a team membership was set to be older
than the Himalayas. We expect the value in days to be a few months to a
few years.

Meanwhile, we have a constraint issue. There is an invariant guarding the
minimum number of days, there could be a maximum number of days

Rules
-----

    * Revise the invariant to raise Invalid if the days are greater than
      3650 (10 years).
      * There are a few insane cases in the DB where teams have set
        the value greater than a human life time. The teams will need to
        set the renewal policy to None. or revise the number if they choose
        to edit the team details.

QA
--

    * Choose the Change details link for a team you control.
    * Set the renewal policy to automatic or ondemand
    * Set the renewal period to 9999999999 in disregard of the text
      that asks for a number from 1 to 3650.
    * Verify the form states that it wants a number from 1 to 3650.

Lint
----

Linting changed files:
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/tests/test_team.py

Test
----

    * lib/lp/registry/tests/test_team.py
      * Added a test to verify the invariant that governs membership
        policy and renewal period.

Implementation
--------------

    * lib/lp/registry/interfaces/person.py
      * Added a maximum day constraint and refactored the test to raise
        Invalid to be in two parts.

To post a comment you must log in.
Revision history for this message
Edwin Grubbs (edwin-grubbs) wrote :

Hi Curtis,

This looks good. I just have a couple of comments below. Feel free to ignore the comment about changing the minimum number of days since that requires more work.

>=== modified file 'lib/lp/registry/interfaces/person.py'
>--- lib/lp/registry/interfaces/person.py 2010-08-22 03:09:51 +0000
>+++ lib/lp/registry/interfaces/person.py 2010-09-22 20:06:34 +0000
>@@ -182,15 +182,19 @@
>
> def validate_person(obj, attr, value):
> """Validate the person is a real person with no other restrictions."""
>+
> def validate(person):
> return IPerson.providedBy(person)
>+
> return validate_person_common(obj, attr, value, validate)
>
>
> def validate_public_person(obj, attr, value):
> """Validate that the person identified by value is public."""
>+
> def validate(person):
> return is_public_person(person)
>+
> return validate_person_common(obj, attr, value, validate)
>
>
>@@ -1630,13 +1634,18 @@
> return
>
> renewal_period = person.defaultrenewalperiod
>- automatic, ondemand = [TeamMembershipRenewalPolicy.AUTOMATIC,
>- TeamMembershipRenewalPolicy.ONDEMAND]
>- cannot_be_none = renewal_policy in [automatic, ondemand]
>- if ((renewal_period is None and cannot_be_none)
>- or (renewal_period is not None and renewal_period <= 0)):
>+ cannot_be_none = (

It seems like it would make more sense to call this
is_required_value_missing. Otherwise, it looks like a logic error when
you check whether renewal_period is None to decide whether it cannot be
None.

>+ renewal_period is None
>+ and renewal_policy in [
>+ TeamMembershipRenewalPolicy.AUTOMATIC,
>+ TeamMembershipRenewalPolicy.ONDEMAND])
>+ out_of_range = (
>+ renewal_period is not None
>+ and (renewal_period <= 0 or renewal_period > 3650))
>+ if cannot_be_none or out_of_range:
> raise Invalid(
>- 'You must specify a default renewal period greater than 0.')
>+ 'You must specify a default renewal period greater than 0 '
>+ 'and less than 3651 days.')

Wouldn't it be easier to read "from 1 to 3650 days"?
Also, if the renewal period is less than 2 days, the user may not get
the next renewal email if the cronjob runs before they renew it.

> teamdescription = exported(
> Text(title=_('Team Description'), required=False, readonly=False,
>@@ -1676,6 +1685,7 @@
> Int(title=_('Renewal period'), required=False,
> description=_(
> "Number of days a subscription lasts after being renewed. "
>+ "The number can be from 1 to 3650 (10 years). "
> "You can customize the lengths of individual renewals, but "
> "this is what's used for auto-renewed and user-renewed "
> "memberships.")),
>

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/registry/interfaces/person.py'
2--- lib/lp/registry/interfaces/person.py 2010-08-22 03:09:51 +0000
3+++ lib/lp/registry/interfaces/person.py 2010-09-23 13:01:29 +0000
4@@ -182,15 +182,19 @@
5
6 def validate_person(obj, attr, value):
7 """Validate the person is a real person with no other restrictions."""
8+
9 def validate(person):
10 return IPerson.providedBy(person)
11+
12 return validate_person_common(obj, attr, value, validate)
13
14
15 def validate_public_person(obj, attr, value):
16 """Validate that the person identified by value is public."""
17+
18 def validate(person):
19 return is_public_person(person)
20+
21 return validate_person_common(obj, attr, value, validate)
22
23
24@@ -1630,13 +1634,18 @@
25 return
26
27 renewal_period = person.defaultrenewalperiod
28- automatic, ondemand = [TeamMembershipRenewalPolicy.AUTOMATIC,
29- TeamMembershipRenewalPolicy.ONDEMAND]
30- cannot_be_none = renewal_policy in [automatic, ondemand]
31- if ((renewal_period is None and cannot_be_none)
32- or (renewal_period is not None and renewal_period <= 0)):
33+ is_required_value_missing = (
34+ renewal_period is None
35+ and renewal_policy in [
36+ TeamMembershipRenewalPolicy.AUTOMATIC,
37+ TeamMembershipRenewalPolicy.ONDEMAND])
38+ out_of_range = (
39+ renewal_period is not None
40+ and (renewal_period <= 0 or renewal_period > 3650))
41+ if is_required_value_missing or out_of_range:
42 raise Invalid(
43- 'You must specify a default renewal period greater than 0.')
44+ 'You must specify a default renewal period '
45+ 'from 1 to 3650 days.')
46
47 teamdescription = exported(
48 Text(title=_('Team Description'), required=False, readonly=False,
49@@ -1664,7 +1673,7 @@
50 default=TeamMembershipRenewalPolicy.NONE))
51
52 defaultmembershipperiod = exported(
53- Int(title=_('Subscription period'), required=False,
54+ Int(title=_('Subscription period'), required=False, max=3650,
55 description=_(
56 "Number of days a new subscription lasts before expiring. "
57 "You can customize the length of an individual subscription "
58@@ -1676,6 +1685,7 @@
59 Int(title=_('Renewal period'), required=False,
60 description=_(
61 "Number of days a subscription lasts after being renewed. "
62+ "The number can be from 1 to 3650 (10 years). "
63 "You can customize the lengths of individual renewals, but "
64 "this is what's used for auto-renewed and user-renewed "
65 "memberships.")),
66
67=== modified file 'lib/lp/registry/stories/team/xx-team-edit.txt'
68--- lib/lp/registry/stories/team/xx-team-edit.txt 2009-12-07 16:07:43 +0000
69+++ lib/lp/registry/stories/team/xx-team-edit.txt 2010-09-23 13:01:29 +0000
70@@ -1,4 +1,5 @@
71-= Editing team details =
72+Editing team details
73+====================
74
75 Only an administrator (as well as the team owner) of a team can change the
76 details of that team.
77@@ -65,23 +66,9 @@
78 There is 1 error.
79 mark is already in use by another person or team.
80
81-We also get an error message if we attempt to create a team with a renewal
82-policy other than None and don't specify a default renewal period.
83-
84- >>> browser.getControl('Name', index=0).value = 'ubuntuteam'
85- >>> browser.getControl(
86- ... 'invite them to renew their own membership').selected = True
87- >>> browser.getControl('Save').click()
88-
89- >>> browser.url
90- 'http://launchpad.dev/%7Eubuntuteam/+edit'
91- >>> for tag in find_tags_by_class(browser.contents, 'message'):
92- ... print extract_text(tag)
93- There is 1 error.
94- You must specify a default renewal period greater than 0.
95-
96-
97-== Teams with mailing lists may not be renamed ==
98+
99+Teams with mailing lists may not be renamed
100+-------------------------------------------
101
102 Because renaming mailing lists is non-trivial in Mailman 2.1, renaming teams
103 with mailing lists is prohibited. This is done by making the 'name' field of
104@@ -92,14 +79,9 @@
105 >>> browser.getControl('Apply for Mailing List').click()
106
107 # Approval of mailing lists is not yet exposed through the web.
108- >>> from zope.component import getUtility
109- >>> from canonical.launchpad.interfaces import (
110- ... MailingListStatus, ILaunchpadCelebrities)
111 >>> from lp.registry.model.mailinglist import MailingListSet
112 >>> from canonical.launchpad.ftests import login, logout
113 >>> login('foo.bar@canonical.com')
114- >>> experts = getUtility(ILaunchpadCelebrities).mailing_list_experts
115- >>> admin = list(experts.allmembers)[0]
116 >>> mailing_list = MailingListSet().get('landscape-developers')
117 >>> mailing_list.syncUpdate()
118 >>> logout()
119@@ -132,12 +114,12 @@
120 'guadamen'
121
122
123-== Private teams may not be renamed ==
124+Private teams may not be renamed
125+--------------------------------
126
127 >>> # Create the necessary teams.
128 >>> login('foo.bar@canonical.com')
129- >>> from lp.registry.interfaces.person import (
130- ... IPersonSet, PersonVisibility)
131+ >>> from lp.registry.interfaces.person import PersonVisibility
132 >>> owner = factory.makePerson(name='team-owner')
133 >>> priv_team = factory.makeTeam(name='private-team',
134 ... owner=owner,
135@@ -161,7 +143,8 @@
136 ...
137
138
139-== Multiple conditions blocking team renaming ==
140+Multiple conditions blocking team renaming
141+------------------------------------------
142
143 Since public teams can have mailing lists and PPAs simultaneously,
144 there will be scenarios where more than one of these conditions are
145
146=== removed file 'lib/lp/registry/stories/teammembership/xx-new-team.txt'
147--- lib/lp/registry/stories/teammembership/xx-new-team.txt 2009-09-18 15:24:30 +0000
148+++ lib/lp/registry/stories/teammembership/xx-new-team.txt 1970-01-01 00:00:00 +0000
149@@ -1,68 +0,0 @@
150-== Validating teams ==
151-
152-The team renewal period cannot be less than 1 day, and when the renewal
153-policy is is On Demand or Automatic, it cannot be None. The renewal
154-period is checked during creation and editing.
155-
156-Sample Person cannot create a team without a renewal period when
157-he chooses a renewal policy of Automatic. He is shown an error
158-message when the from is redisplayed.
159-
160- >>> browser = setupBrowser(auth='Basic test@canonical.com:test')
161-
162- >>> browser.open('http://launchpad.dev/people/')
163- >>> browser.getLink('Register a team').click()
164-
165- >>> browser.title
166- 'Register a new team in Launchpad'
167-
168- >>> browser.getControl(name='field.name').value = 'newtestteam'
169- >>> browser.getControl('Display Name').value = 'New Test Team'
170- >>> browser.getControl(
171- ... 'Contact Email Address').value = 'newtestteam@canonical.com'
172- >>> browser.getControl('Team Description').value = 'The team description'
173- >>> browser.getControl('Subscription period').value = '365'
174- >>> browser.getControl(
175- ... 'renew their membership automatically, '
176- ... 'also notifying the admins').selected = True
177- >>> browser.getControl('Create').click()
178- >>> browser.title
179- 'Register a new team in Launchpad'
180-
181- >>> for message in get_feedback_messages(browser.contents):
182- ... print message
183- There is 1 error.
184- You must specify a default renewal period greater than 0.
185-
186-Changing the renewal policy to On Demand also redisplays the form with
187-an error message.
188-
189- >>> browser.getControl(
190- ... 'invite them to renew their own membership').selected = True
191- >>> browser.getControl('Create').click()
192-
193- >>> for message in get_feedback_messages(browser.contents):
194- ... print message
195- There is 1 error.
196- You must specify a default renewal period greater than 0.
197-
198-Sample Person finaly sets a sane renewal period and created the team.
199-
200- >>> browser.getControl('Renewal period').value = '365'
201- >>> browser.getControl('Create').click()
202- >>> browser.title
203- 'New Test Team in Launchpad'
204-
205-Sample Person cannot change the renewal period 'New Test Team' to zero.
206-
207- >>> browser.getLink('Change details').click()
208- >>> browser.title
209- 'Edit "New Test Team" team...
210-
211- >>> browser.getControl('Renewal period').value = '0'
212- >>> browser.getControl('Save').click()
213-
214- >>> for message in get_feedback_messages(browser.contents):
215- ... print message
216- There is 1 error.
217- You must specify a default renewal period greater than 0.
218
219=== modified file 'lib/lp/registry/tests/test_team.py'
220--- lib/lp/registry/tests/test_team.py 2010-08-20 20:31:18 +0000
221+++ lib/lp/registry/tests/test_team.py 2010-09-23 13:01:29 +0000
222@@ -7,12 +7,17 @@
223
224 import transaction
225 from zope.component import getUtility
226+from zope.interface.exceptions import Invalid
227
228 from canonical.launchpad.database.emailaddress import EmailAddress
229 from canonical.launchpad.interfaces.emailaddress import IEmailAddressSet
230 from canonical.launchpad.interfaces.lpstorm import IMasterStore
231 from canonical.testing import DatabaseFunctionalLayer
232 from lp.registry.interfaces.mailinglist import MailingListStatus
233+from lp.registry.interfaces.person import (
234+ ITeamPublic,
235+ TeamMembershipRenewalPolicy,
236+ )
237 from lp.testing import (
238 login_celebrity,
239 login_person,
240@@ -38,6 +43,7 @@
241
242 def setUp(self):
243 super(TestTeamContactAddress, self).setUp()
244+
245 self.team = self.factory.makeTeam(name='alpha')
246 self.address = self.factory.makeEmail('team@noplace.org', self.team)
247 self.store = IMasterStore(self.address)
248@@ -102,3 +108,81 @@
249 self.team.setContactAddress(None)
250 self.assertEqual(None, self.team.preferredemail)
251 self.assertEqual([], self.getAllEmailAddresses())
252+
253+
254+class TestDefaultRenewalPeriodIsRequiredForSomeTeams(TestCaseWithFactory):
255+
256+ layer = DatabaseFunctionalLayer
257+
258+ def setUp(self):
259+ super(TestDefaultRenewalPeriodIsRequiredForSomeTeams, self).setUp()
260+ self.team = self.factory.makeTeam()
261+ login_person(self.team.teamowner)
262+
263+ def assertInvalid(self, policy, period):
264+ self.team.renewal_policy = policy
265+ self.team.defaultrenewalperiod = period
266+ self.assertRaises(Invalid, ITeamPublic.validateInvariants, self.team)
267+
268+ def assertValid(self, policy, period):
269+ self.team.renewal_policy = policy
270+ self.team.defaultrenewalperiod = period
271+ ITeamPublic.validateInvariants(self.team)
272+
273+ def test_policy_automatic_period_none(self):
274+ # Automatic policy cannot have a none day period.
275+ self.assertInvalid(
276+ TeamMembershipRenewalPolicy.AUTOMATIC, None)
277+
278+ def test_policy_ondemand_period_none(self):
279+ # Ondemand policy cannot have a none day period.
280+ self.assertInvalid(
281+ TeamMembershipRenewalPolicy.ONDEMAND, None)
282+
283+ def test_policy_none_period_none(self):
284+ # None policy can have a None day period.
285+ self.assertValid(
286+ TeamMembershipRenewalPolicy.NONE, None)
287+
288+ def test_policy_requres_period_below_minimum(self):
289+ # Automatic and ondemand policy cannot have a zero day period.
290+ self.assertInvalid(
291+ TeamMembershipRenewalPolicy.AUTOMATIC, 0)
292+
293+ def test_policy_requres_period_minimum(self):
294+ # Automatic and ondemand policy can have a 1 day period.
295+ self.assertValid(
296+ TeamMembershipRenewalPolicy.AUTOMATIC, 1)
297+
298+ def test_policy_requres_period_maximum(self):
299+ # Automatic and ondemand policy cannot have a 3650 day max value.
300+ self.assertValid(
301+ TeamMembershipRenewalPolicy.AUTOMATIC, 3650)
302+
303+ def test_policy_requres_period_over_maximum(self):
304+ # Automatic and ondemand policy cannot have a 3650 day max value.
305+ self.assertInvalid(
306+ TeamMembershipRenewalPolicy.AUTOMATIC, 3651)
307+
308+
309+class TestDefaultMembershipPeriod(TestCaseWithFactory):
310+
311+ layer = DatabaseFunctionalLayer
312+
313+ def setUp(self):
314+ super(TestDefaultMembershipPeriod, self).setUp()
315+ self.team = self.factory.makeTeam()
316+ login_person(self.team.teamowner)
317+
318+ def test_default_membership_period_over_maximum(self):
319+ self.assertRaises(
320+ Invalid, ITeamPublic['defaultmembershipperiod'].validate, 3651)
321+
322+ def test_default_membership_period_none(self):
323+ ITeamPublic['defaultmembershipperiod'].validate(None)
324+
325+ def test_default_membership_period_zero(self):
326+ ITeamPublic['defaultmembershipperiod'].validate(0)
327+
328+ def test_default_membership_period_maximum(self):
329+ ITeamPublic['defaultmembershipperiod'].validate(3650)