Merge lp:~sinzui/launchpad/project-notify-5 into lp:launchpad

Proposed by Curtis Hovey on 2012-04-19
Status: Merged
Approved by: Curtis Hovey on 2012-04-19
Approved revision: no longer in the source branch.
Merged at revision: 15128
Proposed branch: lp:~sinzui/launchpad/project-notify-5
Merge into: lp:launchpad
Diff against target: 601 lines (+490/-1)
7 files modified
lib/lp/registry/emailtemplates/product-commercial-subscription-expiration.txt (+47/-0)
lib/lp/registry/emailtemplates/product-commercial-subscription-expired-open-source.txt (+40/-0)
lib/lp/registry/emailtemplates/product-commercial-subscription-expired-proprietary.txt (+41/-0)
lib/lp/registry/interfaces/productjob.py (+56/-0)
lib/lp/registry/model/productjob.py (+106/-1)
lib/lp/registry/tests/test_productjob.py (+197/-0)
lib/lp/testing/factory.py (+3/-0)
To merge this branch: bzr merge lp:~sinzui/launchpad/project-notify-5
Reviewer Review Type Date Requested Status
j.c.sackett (community) 2012-04-19 Approve on 2012-04-19
Richard Harding (community) code* 2012-04-19 Approve on 2012-04-19
Review via email: mp+102734@code.launchpad.net

Commit Message

Send emails about commercial subscription expiration.

Description of the Change

Pre-implementation: abentley, jcsackett

Lp needs to send 30-day and 7-day commercial subscription expiration
notices, and it needs to send a message after expiration. The job that
deals with after expiration needs to deactivate the commercial features.

I hoped to also provide the mechanism to create the job's but this branch
is large and very old.

--------------------------------------------------------------------

RULES

    * Create a job that sends an expired commercial subscription email
      that also handles commercial feature deactivations:
      * When the project license is proprietary, deactivate the project.
      * When the project license is open source, deactivate private bugs
        and branches.
      * Do not make anything public...the information remains private,
        but new private information cannot be made.
    * Create a job that sends an commercial subscription expiration email
      notice 7 days before the expiration date.
    * Create a job that sends an commercial subscription expiration email
      notice 30 days before the expiration date.

QA

    None. My next branch will create a mechanism that created the 30, 7, and -1
    day job so that we can test them. This also allows us to put the draft
    emails in place while Dan provides the final draft.

LINT

    lib/lp/registry/emailtemplates/product-commercial-subscription-expiration.txt
    lib/lp/registry/emailtemplates/product-commercial-subscription-expired-open-source.txt
    lib/lp/registry/emailtemplates/product-commercial-subscription-expired-proprietary.txt
    lib/lp/registry/interfaces/productjob.py
    lib/lp/registry/model/productjob.py
    lib/lp/registry/tests/test_productjob.py
    lib/lp/testing/factory.py

TEST

    ./bin/test -vvc --layer=Database lp.registry.tests.test_productjob

IMPLEMENTATION

Created three email templates for the conditions we recognise. These are
drafts. I will ask Dan to revise them.
    lib/lp/registry/emailtemplates/product-commercial-subscription-expiration.txt
    lib/lp/registry/emailtemplates/product-commercial-subscription-expired-open-source.txt
    lib/lp/registry/emailtemplates/product-commercial-subscription-expired-proprietary.txt

Created three kinds of emails for 30 day, 7 day, and -1 day expiration
notifications. Commercial features are deactivated by the same job that sends
the -1 day expiration. I did not want to redefine CommercialExpiredJob's
email_template_name, I had planned to create a single email template for the
case, but for the sake of Dan and future editors, I decided not to inject
whole paragraphs to describe what changed.
    lib/lp/registry/interfaces/productjob.py
    lib/lp/registry/model/productjob.py
    lib/lp/registry/tests/test_productjob.py

Fixed the factory which permitted me to create multiple commercial
subscriptions for a product and caused Storm to raise an exception.
    lib/lp/testing/factory.py

To post a comment you must log in.
Richard Harding (rharding) wrote :

Looks ok to me. My only nitpick is that I'd usually suggest using if else vs just if return, return. However I don't find anything noting it in the style guide.

review: Approve (code*)
Curtis Hovey (sinzui) wrote :

Yes, the if-else looks better and better for future extension. I made the change.

j.c.sackett (jcsackett) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

I have nothing to add. This looks good.

 review approve
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iQEcBAEBAgAGBQJPkGyNAAoJEJvCBZ3E2NUPKFQH/3GOFrJv7YNix768a+eXL9YP
o4Vb4GfmQPzTgOe1n4UVkCAkxOhtIQhCenp41MylOfZ4ClJsugTSzxcNnF2+Gv6K
36H8TrM2Cxb2IeuSf3wnmioga+PNcX6UaS27hH1INbYibkRvyCvWISnW0eMugIiO
Pe28LG1yTncS+pMfAVIj5poN0+UYiwom2lhGzT3Y0iQ7bCJA5vor7PW4r+e8iD5f
9HmoTrVBAMZmh9g1PwssH4esokZksfSTXuruu6QGixSe3xjqSnlGYGJRSftUHkfb
LAsd1oNOhVjloGrXVZAObDCo8Zl0+YCXePvN6HaHTA7YGthrXqAgJceRrOzF7Q0=
=H2+i
-----END PGP SIGNATURE-----

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/registry/emailtemplates/product-commercial-subscription-expiration.txt'
2--- lib/lp/registry/emailtemplates/product-commercial-subscription-expiration.txt 1970-01-01 00:00:00 +0000
3+++ lib/lp/registry/emailtemplates/product-commercial-subscription-expiration.txt 2012-04-19 19:17:23 +0000
4@@ -0,0 +1,47 @@
5+Hello %(user_displayname)s,
6+
7+The commercial subscription for project '%(product_name)s' in
8+Launchpad will expire soon.
9+%(commercial_use_expiration)s
10+
11+You can renew the commercial subscription which costs
12+US$250/year/project. Follow the instructions presented on your
13+project overview page to purchase a subscription voucher.
14+%(product_url)s
15+
16+A commercial subscription allows you to host your commercial project
17+on Launchpad in the same way as any other project. Project's with a
18+commercial subscription may have private-by-default bugs, and you
19+may also request the setup of private code hosting.
20+
21+As the maintainer of a project with a commercial subscription, you
22+may create private teams with private mailing lists and private package
23+archives. Find out more about this here:
24+https://help.launchpad.net/CommercialHosting
25+
26+If '%(product_name)s' possessed an Other/Proprietary license at the time
27+of expiration, the project will be deactivated. Otherwise, the
28+commercial features will be deactivated. Things that are private will
29+remain private, but no one will be able to create new things that are
30+private.
31+
32+Launchpad is a collaboration site, free to use for projects with an
33+approved open source license. When you registered your project, you'd
34+have seen a list of licenses presented. These are the licences we
35+automatically recognise.
36+
37+If you have a different licence to one on the approved list, it must
38+follow the guidelines we list on the following page in order to be
39+approved: https://help.launchpad.net/Legal/ProjectLicensing
40+
41+Want to know more?
42+Further information is on our FAQ "Can closed-source or proprietary
43+projects use Launchpad?" The link is here:
44+https://answers.launchpad.net/launchpad/+faq/208
45+
46+If the license for your project needs to be corrected, you can do this
47+by following the 'Change Details' link on your project's overview page.
48+
49+Thanks,
50+
51+The Launchpad team.
52
53=== added file 'lib/lp/registry/emailtemplates/product-commercial-subscription-expired-open-source.txt'
54--- lib/lp/registry/emailtemplates/product-commercial-subscription-expired-open-source.txt 1970-01-01 00:00:00 +0000
55+++ lib/lp/registry/emailtemplates/product-commercial-subscription-expired-open-source.txt 2012-04-19 19:17:23 +0000
56@@ -0,0 +1,40 @@
57+Hello %(user_displayname)s,
58+
59+The commercial subscription for project '%(product_name)s' in
60+Launchpad has expired.
61+%(commercial_use_expiration)s
62+
63+Commercial features were deactivated. Things that are private will
64+remain private. New bugs and branches are public by default! Private
65+branches cannot be linked to project series.
66+
67+A commercial subscription allows you to host your commercial project
68+on Launchpad in the same way as any other project. Project's with a
69+commercial subscription may have private-by-default bugs, and you
70+may also request the setup of private code hosting.
71+
72+As the maintainer of a project with a commercial subscription, you
73+may create private teams with private mailing lists and private package
74+archives. Find out more about this here:
75+https://help.launchpad.net/CommercialHosting
76+
77+Launchpad is a collaboration site, free to use for projects with an
78+approved open source license. When you registered your project, you'd
79+have seen a list of licenses presented. These are the licences we
80+automatically recognise.
81+
82+If you have a different licence to one on the approved list, it must
83+follow the guidelines we list on the following page in order to be
84+approved: https://help.launchpad.net/Legal/ProjectLicensing
85+
86+Want to know more?
87+Further information is on our FAQ "Can closed-source or proprietary
88+projects use Launchpad?" The link is here:
89+https://answers.launchpad.net/launchpad/+faq/208
90+
91+If the license for your project needs to be corrected, you can do this
92+by following the 'Change Details' link on your project's overview page.
93+
94+Thanks,
95+
96+The Launchpad team.
97
98=== added file 'lib/lp/registry/emailtemplates/product-commercial-subscription-expired-proprietary.txt'
99--- lib/lp/registry/emailtemplates/product-commercial-subscription-expired-proprietary.txt 1970-01-01 00:00:00 +0000
100+++ lib/lp/registry/emailtemplates/product-commercial-subscription-expired-proprietary.txt 2012-04-19 19:17:23 +0000
101@@ -0,0 +1,41 @@
102+Hello %(user_displayname)s,
103+
104+The commercial subscription for project '%(product_name)s' in
105+Launchpad has expired.
106+%(commercial_use_expiration)s
107+
108+'%(product_name)s' was deactivated because its license is
109+Other/Proprietary and requires a commercial subscription to use
110+Launchpad's services. If you wish to reactivate the project contact us
111+at commercial@launchpad.net to discuss your options.
112+
113+A commercial subscription allows you to host your commercial project
114+on Launchpad in the same way as any other project. Project's with a
115+commercial subscription may have private-by-default bugs, and you
116+may also request the setup of private code hosting.
117+
118+As the maintainer of a project with a commercial subscription, you
119+may create private teams with private mailing lists and private package
120+archives. Find out more about this here:
121+https://help.launchpad.net/CommercialHosting
122+
123+Launchpad is a collaboration site, free to use for projects with an
124+approved open source license. When you registered your project, you'd
125+have seen a list of licenses presented. These are the licences we
126+automatically recognise.
127+
128+If you have a different licence to one on the approved list, it must
129+follow the guidelines we list on the following page in order to be
130+approved: https://help.launchpad.net/Legal/ProjectLicensing
131+
132+Want to know more?
133+Further information is on our FAQ "Can closed-source or proprietary
134+projects use Launchpad?" The link is here:
135+https://answers.launchpad.net/launchpad/+faq/208
136+
137+If the license for your project needs to be corrected, you can do this
138+by following the 'Change Details' link on your project's overview page.
139+
140+Thanks,
141+
142+The Launchpad team.
143
144=== modified file 'lib/lp/registry/interfaces/productjob.py'
145--- lib/lp/registry/interfaces/productjob.py 2012-03-24 12:41:36 +0000
146+++ lib/lp/registry/interfaces/productjob.py 2012-04-19 19:17:23 +0000
147@@ -9,6 +9,12 @@
148 'IProductJobSource',
149 'IProductNotificationJob',
150 'IProductNotificationJobSource',
151+ 'ICommercialExpiredJob',
152+ 'ICommercialExpiredJobSource',
153+ 'ISevenDayCommercialExpirationJob',
154+ 'ISevenDayCommercialExpirationJobSource',
155+ 'IThirtyDayCommercialExpirationJob',
156+ 'IThirtyDayCommercialExpirationJobSource',
157 ]
158
159 from zope.interface import Attribute
160@@ -119,3 +125,53 @@
161 :param reply_to_commercial: Set the reply_to property to the
162 commercial email address.
163 """
164+
165+
166+class ISevenDayCommercialExpirationJob(IProductNotificationJob):
167+ """A job that sends an email about an expiring commercial subscription."""
168+
169+
170+class ISevenDayCommercialExpirationJobSource(IProductNotificationJobSource):
171+ """An interface for creating `ISevenDayCommercialExpirationJob`s."""
172+
173+ def create(product, reviewer):
174+ """Create a new `ISevenDayCommercialExpirationJob`.
175+
176+ :param product: An IProduct.
177+ :param reviewer: The user or agent sending the email.
178+ """
179+
180+
181+class IThirtyDayCommercialExpirationJob(IProductNotificationJob):
182+ """A job that sends an email about an expiring commercial subscription."""
183+
184+
185+class IThirtyDayCommercialExpirationJobSource(IProductNotificationJobSource):
186+ """An interface for creating `IThirtyDayCommercialExpirationJob`s."""
187+
188+ def create(product, reviewer):
189+ """Create a new `IThirtyDayCommercialExpirationJob`.
190+
191+ :param product: An IProduct.
192+ :param reviewer: The user or agent sending the email.
193+ """
194+
195+
196+class ICommercialExpiredJob(IProductNotificationJob):
197+ """A job that sends an email about an expired commercial subscription.
198+
199+ This job is responsible for deactivating the project if it has a
200+ proprietary license or deactivating the commercial features if the
201+ license is open.
202+ """
203+
204+
205+class ICommercialExpiredJobSource(IProductNotificationJobSource):
206+ """An interface for creating `IThirtyDayCommercialExpirationJob`s."""
207+
208+ def create(product, reviewer):
209+ """Create a new `ICommercialExpiredJob`.
210+
211+ :param product: An IProduct.
212+ :param reviewer: The user or agent sending the email.
213+ """
214
215=== modified file 'lib/lp/registry/model/productjob.py'
216--- lib/lp/registry/model/productjob.py 2012-03-24 12:36:13 +0000
217+++ lib/lp/registry/model/productjob.py 2012-04-19 19:17:23 +0000
218@@ -6,6 +6,9 @@
219 __metaclass__ = type
220 __all__ = [
221 'ProductJob',
222+ 'CommercialExpiredJob',
223+ 'SevenDayCommercialExpirationJob',
224+ 'ThirtyDayCommercialExpirationJob',
225 ]
226
227 from lazr.delegates import delegates
228@@ -21,15 +24,25 @@
229 classProvides,
230 implements,
231 )
232+from zope.security.proxy import removeSecurityProxy
233
234 from lp.registry.enums import ProductJobType
235 from lp.registry.interfaces.person import IPersonSet
236-from lp.registry.interfaces.product import IProduct
237+from lp.registry.interfaces.product import (
238+ IProduct,
239+ License,
240+ )
241 from lp.registry.interfaces.productjob import (
242 IProductJob,
243 IProductJobSource,
244 IProductNotificationJob,
245 IProductNotificationJobSource,
246+ ICommercialExpiredJob,
247+ ICommercialExpiredJobSource,
248+ ISevenDayCommercialExpirationJob,
249+ ISevenDayCommercialExpirationJobSource,
250+ IThirtyDayCommercialExpirationJob,
251+ IThirtyDayCommercialExpirationJobSource,
252 )
253 from lp.registry.model.product import Product
254 from lp.services.config import config
255@@ -282,3 +295,95 @@
256 'Launchpad', config.canonical.noreply_from_address)
257 self.sendEmailToMaintainer(
258 self.email_template_name, self.subject, from_address)
259+
260+
261+class CommericialExpirationMixin:
262+
263+ _email_template_name = 'product-commercial-subscription-expiration'
264+ _subject_template = (
265+ 'The commercial subscription for %s in Launchpad is expiring')
266+
267+ @classmethod
268+ def create(cls, product, reviewer):
269+ """Create a job."""
270+ subject = cls._subject_template % product.name
271+ return super(CommericialExpirationMixin, cls).create(
272+ product, cls._email_template_name, subject, reviewer,
273+ reply_to_commercial=True)
274+
275+ @cachedproperty
276+ def message_data(self):
277+ """See `IProductNotificationJob`."""
278+ data = super(CommericialExpirationMixin, self).message_data
279+ commercial_subscription = self.product.commercial_subscription
280+ iso_date = commercial_subscription.date_expires.date().isoformat()
281+ extra_data = {
282+ 'commercial_use_expiration': iso_date,
283+ }
284+ data.update(extra_data)
285+ return data
286+
287+
288+class SevenDayCommercialExpirationJob(CommericialExpirationMixin,
289+ ProductNotificationJob):
290+ """A job that sends an email about an expiring commercial subscription."""
291+
292+ implements(ISevenDayCommercialExpirationJob)
293+ classProvides(ISevenDayCommercialExpirationJobSource)
294+ class_job_type = ProductJobType.COMMERCIAL_EXPIRATION_7_DAYS
295+
296+
297+class ThirtyDayCommercialExpirationJob(CommericialExpirationMixin,
298+ ProductNotificationJob):
299+ """A job that sends an email about an expiring commercial subscription."""
300+
301+ implements(IThirtyDayCommercialExpirationJob)
302+ classProvides(IThirtyDayCommercialExpirationJobSource)
303+ class_job_type = ProductJobType.COMMERCIAL_EXPIRATION_30_DAYS
304+
305+
306+class CommercialExpiredJob(CommericialExpirationMixin, ProductNotificationJob):
307+ """A job that sends an email about an expired commercial subscription."""
308+
309+ implements(ICommercialExpiredJob)
310+ classProvides(ICommercialExpiredJobSource)
311+ class_job_type = ProductJobType.COMMERCIAL_EXPIRED
312+
313+ _email_template_name = '' # email_template_name does not need this.
314+ _subject_template = (
315+ 'The commercial subscription for %s in Launchpad expired')
316+
317+ @property
318+ def _is_proprietary(self):
319+ """Does the product have a proprietary license?"""
320+ return License.OTHER_PROPRIETARY in self.product.licenses
321+
322+ @property
323+ def email_template_name(self):
324+ """See `IProductNotificationJob`.
325+
326+ The email template is determined by the product's licenses.
327+ """
328+ if self._is_proprietary:
329+ return 'product-commercial-subscription-expired-proprietary'
330+ else:
331+ return 'product-commercial-subscription-expired-open-source'
332+
333+ def _deactivateCommercialFeatures(self):
334+ """Deactivate the project or just the commercial features it uses."""
335+ if self._is_proprietary:
336+ self.product.active = False
337+ else:
338+ removeSecurityProxy(self.product).private_bugs = False
339+ for series in self.product.series:
340+ if series.branch.private:
341+ removeSecurityProxy(series).branch = None
342+
343+ def run(self):
344+ """See `ProductNotificationJob`."""
345+ if self.product.has_current_commercial_subscription:
346+ # The commercial subscription was renewed after this job was
347+ # created. Nothing needs to be done.
348+ return
349+ super(CommercialExpiredJob, self).run()
350+ self._deactivateCommercialFeatures()
351
352=== modified file 'lib/lp/registry/tests/test_productjob.py'
353--- lib/lp/registry/tests/test_productjob.py 2012-03-24 12:41:36 +0000
354+++ lib/lp/registry/tests/test_productjob.py 2012-04-19 19:17:23 +0000
355@@ -11,17 +11,28 @@
356 )
357
358 import pytz
359+from zope.component import getUtility
360 from zope.interface import (
361 classProvides,
362 implements,
363 )
364 from zope.security.proxy import removeSecurityProxy
365
366+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
367 from lp.registry.enums import ProductJobType
368+from lp.registry.interfaces.product import (
369+ License,
370+ )
371 from lp.registry.interfaces.productjob import (
372 IProductJob,
373 IProductJobSource,
374 IProductNotificationJobSource,
375+ ICommercialExpiredJob,
376+ ICommercialExpiredJobSource,
377+ ISevenDayCommercialExpirationJob,
378+ ISevenDayCommercialExpirationJobSource,
379+ IThirtyDayCommercialExpirationJob,
380+ IThirtyDayCommercialExpirationJobSource,
381 )
382 from lp.registry.interfaces.person import TeamSubscriptionPolicy
383 from lp.registry.interfaces.teammembership import TeamMembershipStatus
384@@ -29,6 +40,9 @@
385 ProductJob,
386 ProductJobDerived,
387 ProductNotificationJob,
388+ CommercialExpiredJob,
389+ SevenDayCommercialExpirationJob,
390+ ThirtyDayCommercialExpirationJob,
391 )
392 from lp.testing import (
393 person_logged_in,
394@@ -225,6 +239,9 @@
395 self.assertIs(
396 True,
397 IProductNotificationJobSource.providedBy(ProductNotificationJob))
398+ self.assertEqual(
399+ ProductJobType.REVIEWER_NOTIFICATION,
400+ ProductNotificationJob.class_job_type)
401 job = ProductNotificationJob.create(
402 product, email_template_name, subject, reviewer,
403 reply_to_commercial=False)
404@@ -369,3 +386,183 @@
405 self.assertEqual(subject, notifications[0]['Subject'])
406 self.assertIn(
407 'Launchpad <noreply@launchpad.net>', notifications[0]['From'])
408+
409+
410+class CommericialExpirationMixin:
411+
412+ layer = DatabaseFunctionalLayer
413+
414+ EXPIRE_SUBSCRIPTION = False
415+
416+ def make_notification_data(self, licenses=[License.MIT]):
417+ product = self.factory.makeProduct(licenses=licenses)
418+ if License.OTHER_PROPRIETARY not in product.licenses:
419+ # The proprietary project was automatically given a CS.
420+ self.factory.makeCommercialSubscription(product)
421+ reviewer = getUtility(ILaunchpadCelebrities).janitor
422+ return product, reviewer
423+
424+ def test_create(self):
425+ # Create an instance of an commercial expiration job that stores
426+ # the notification information.
427+ product = self.factory.makeProduct()
428+ reviewer = getUtility(ILaunchpadCelebrities).janitor
429+ self.assertIs(
430+ True,
431+ self.JOB_SOURCE_INTERFACE.providedBy(self.JOB_CLASS))
432+ self.assertEqual(
433+ self.JOB_CLASS_TYPE, self.JOB_CLASS.class_job_type)
434+ job = self.JOB_CLASS.create(product, reviewer)
435+ self.assertIsInstance(job, self.JOB_CLASS)
436+ self.assertIs(
437+ True, self.JOB_INTERFACE.providedBy(job))
438+ self.assertEqual(product, job.product)
439+ self.assertEqual(job._subject_template % product.name, job.subject)
440+ self.assertEqual(reviewer, job.reviewer)
441+ self.assertEqual(True, job.reply_to_commercial)
442+
443+ def test_email_template_name(self):
444+ # The class defines the email_template_name.
445+ product, reviewer = self.make_notification_data()
446+ job = self.JOB_CLASS.create(product, reviewer)
447+ self.assertEqual(job.email_template_name, job._email_template_name)
448+
449+ def test_message_data(self):
450+ # The commercial expiration data is added.
451+ product, reviewer = self.make_notification_data()
452+ job = self.JOB_CLASS.create(product, reviewer)
453+ commercial_subscription = product.commercial_subscription
454+ iso_date = commercial_subscription.date_expires.date().isoformat()
455+ self.assertEqual(
456+ iso_date, job.message_data['commercial_use_expiration'])
457+
458+ def test_run(self):
459+ # Smoke test that run() can make the email from the template and data.
460+ product, reviewer = self.make_notification_data(
461+ licenses=[License.OTHER_PROPRIETARY])
462+ commercial_subscription = product.commercial_subscription
463+ if self.EXPIRE_SUBSCRIPTION:
464+ expired_date = (
465+ commercial_subscription.date_expires - timedelta(days=365))
466+ removeSecurityProxy(
467+ commercial_subscription).date_expires = expired_date
468+ iso_date = commercial_subscription.date_expires.date().isoformat()
469+ job = self.JOB_CLASS.create(product, reviewer)
470+ pop_notifications()
471+ job.run()
472+ notifications = pop_notifications()
473+ self.assertEqual(1, len(notifications))
474+ self.assertIn(iso_date, notifications[0].get_payload())
475+
476+
477+class SevenDayCommercialExpirationJobTestCase(CommericialExpirationMixin,
478+ TestCaseWithFactory):
479+ """Test case for the SevenDayCommercialExpirationJob class."""
480+
481+ JOB_INTERFACE = ISevenDayCommercialExpirationJob
482+ JOB_SOURCE_INTERFACE = ISevenDayCommercialExpirationJobSource
483+ JOB_CLASS = SevenDayCommercialExpirationJob
484+ JOB_CLASS_TYPE = ProductJobType.COMMERCIAL_EXPIRATION_7_DAYS
485+
486+
487+class ThirtyDayCommercialExpirationJobTestCase(CommericialExpirationMixin,
488+ TestCaseWithFactory):
489+ """Test case for the SevenDayCommercialExpirationJob class."""
490+
491+ JOB_INTERFACE = IThirtyDayCommercialExpirationJob
492+ JOB_SOURCE_INTERFACE = IThirtyDayCommercialExpirationJobSource
493+ JOB_CLASS = ThirtyDayCommercialExpirationJob
494+ JOB_CLASS_TYPE = ProductJobType.COMMERCIAL_EXPIRATION_30_DAYS
495+
496+
497+class CommercialExpiredJobTestCase(CommericialExpirationMixin,
498+ TestCaseWithFactory):
499+ """Test case for the CommercialExpiredJob class."""
500+
501+ EXPIRE_SUBSCRIPTION = True
502+ JOB_INTERFACE = ICommercialExpiredJob
503+ JOB_SOURCE_INTERFACE = ICommercialExpiredJobSource
504+ JOB_CLASS = CommercialExpiredJob
505+ JOB_CLASS_TYPE = ProductJobType.COMMERCIAL_EXPIRED
506+
507+ def test_is_proprietary_open_source(self):
508+ product, reviewer = self.make_notification_data(licenses=[License.MIT])
509+ job = CommercialExpiredJob.create(product, reviewer)
510+ self.assertIs(False, job._is_proprietary)
511+
512+ def test_is_proprietary_proprietary(self):
513+ product, reviewer = self.make_notification_data(
514+ licenses=[License.OTHER_PROPRIETARY])
515+ job = CommercialExpiredJob.create(product, reviewer)
516+ self.assertIs(True, job._is_proprietary)
517+
518+ def test_email_template_name(self):
519+ # Redefine the inherited test to verify the open source license case.
520+ # The state of the product's license defines the email_template_name.
521+ product, reviewer = self.make_notification_data(licenses=[License.MIT])
522+ job = CommercialExpiredJob.create(product, reviewer)
523+ self.assertEqual(
524+ 'product-commercial-subscription-expired-open-source',
525+ job.email_template_name)
526+
527+ def test_email_template_name_proprietary(self):
528+ # The state of the product's license defines the email_template_name.
529+ product, reviewer = self.make_notification_data(
530+ licenses=[License.OTHER_PROPRIETARY])
531+ job = CommercialExpiredJob.create(product, reviewer)
532+ self.assertEqual(
533+ 'product-commercial-subscription-expired-proprietary',
534+ job.email_template_name)
535+
536+ def test_deactivateCommercialFeatures_proprietary(self):
537+ # When the project is proprietary, the product is deactivated.
538+ product, reviewer = self.make_notification_data(
539+ licenses=[License.OTHER_PROPRIETARY])
540+ job = CommercialExpiredJob.create(product, reviewer)
541+ job._deactivateCommercialFeatures()
542+ self.assertIs(False, product.active)
543+
544+ def test_deactivateCommercialFeatures_open_source(self):
545+ # When the project is open source, the product's commercial features
546+ # are deactivated.
547+ product, reviewer = self.make_notification_data(licenses=[License.MIT])
548+ public_branch = self.factory.makeBranch(
549+ owner=product.owner, product=product)
550+ private_branch = self.factory.makeBranch(
551+ owner=product.owner, product=product, private=True)
552+ with person_logged_in(product.owner):
553+ product.setPrivateBugs(True, product.owner)
554+ public_series = product.development_focus
555+ public_series.branch = public_branch
556+ private_series = product.newSeries(
557+ product.owner, 'special', 'testing', branch=private_branch)
558+ job = CommercialExpiredJob.create(product, reviewer)
559+ job._deactivateCommercialFeatures()
560+ self.assertIs(True, product.active)
561+ self.assertIs(False, product.private_bugs)
562+ self.assertEqual(public_branch, public_series.branch)
563+ self.assertIs(None, private_series.branch)
564+
565+ def test_run_deactivation_performed(self):
566+ # An email is sent and the deactivation steps are performed.
567+ product, reviewer = self.make_notification_data(
568+ licenses=[License.OTHER_PROPRIETARY])
569+ expired_date = (
570+ product.commercial_subscription.date_expires - timedelta(days=365))
571+ removeSecurityProxy(
572+ product.commercial_subscription).date_expires = expired_date
573+ job = CommercialExpiredJob.create(product, reviewer)
574+ job.run()
575+ self.assertIs(False, product.active)
576+
577+ def test_run_deactivation_aborted(self):
578+ # The deactivation steps and email are aborted if the commercial
579+ # subscription was renewed after the job was created.
580+ product, reviewer = self.make_notification_data(
581+ licenses=[License.OTHER_PROPRIETARY])
582+ job = CommercialExpiredJob.create(product, reviewer)
583+ pop_notifications()
584+ job.run()
585+ notifications = pop_notifications()
586+ self.assertEqual(0, len(notifications))
587+ self.assertIs(True, product.active)
588
589=== modified file 'lib/lp/testing/factory.py'
590--- lib/lp/testing/factory.py 2012-04-10 20:24:43 +0000
591+++ lib/lp/testing/factory.py 2012-04-19 19:17:23 +0000
592@@ -4418,6 +4418,9 @@
593
594 def makeCommercialSubscription(self, product, expired=False):
595 """Create a commercial subscription for the given product."""
596+ if CommercialSubscription.selectOneBy(product=product) is not None:
597+ raise AssertionError(
598+ "The product under test already has a CommercialSubscription.")
599 if expired:
600 expiry = datetime.now(pytz.UTC) - timedelta(days=1)
601 else: