← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wallyworld/launchpad/recipe-date-last-modified into lp:launchpad

 

Ian Booth has proposed merging lp:~wallyworld/launchpad/recipe-date-last-modified into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #735900 in Launchpad itself: "Inline editing of recipe doesn't update the date_last_modified property"
  https://bugs.launchpad.net/launchpad/+bug/735900

For more details, see:
https://code.launchpad.net/~wallyworld/launchpad/recipe-date-last-modified/+merge/53573

When using the inline editing widgets on the recipe page, the date_last_modified is not updated. It is only updated when the recipe is edited using the HTML form.

== Implementation ==

The date_last_modified field was only being set in the HTML form action implemented on the view class. Inline editing of properties doesn't use this code. However, in all cases, regardless of the method used to update the recipe, an ObjectModifiedEvent is published. So the code to set the date_last_modified property was removed from the view code and put into a subscription listener.

== Tests ==

There were no existing tests for the date_last_modified updates on recipes. So tests were created. The makeSourcePackageRecipe factory method (and friends) was modified to take an optional date_created parameter so that the date_last_modified property can be set to a user specified value on recipe creation.

lp.code.model.tests.test_sourcepackagerecipe.RecipeDateLastModified
  test_initialValue
  test_modifiedevent_sets_date_last_updated

lp.code.browser.tests.test_sourcepackagerecipe.TestSourcePackageRecipeEditView
  test_edit_recipe_sets_date_last_modified

A test was added for TestSourcePackageRecipeEditView because the recipe update action explicitly publishes an ObjectModifiedEvent.

== Lint ==

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/code/configure.zcml
  lib/lp/code/browser/sourcepackagerecipe.py
  lib/lp/code/browser/tests/test_sourcepackagerecipe.py
  lib/lp/code/interfaces/sourcepackagerecipe.py
  lib/lp/code/model/sourcepackagerecipe.py
  lib/lp/code/model/tests/test_sourcepackagerecipe.py
  lib/lp/testing/factory.py

./lib/lp/code/browser/tests/test_sourcepackagerecipe.py
     482: E501 line too long (80 characters)
    1102: E501 line too long (85 characters)
    1132: E501 line too long (85 characters)
    1167: E501 line too long (85 characters)
     232: Line exceeds 78 characters.
     482: Line exceeds 78 characters.
    1102: Line exceeds 78 characters.
    1132: Line exceeds 78 characters.
    1154: Line exceeds 78 characters.
    1167: Line exceeds 78 characters.
-- 
https://code.launchpad.net/~wallyworld/launchpad/recipe-date-last-modified/+merge/53573
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wallyworld/launchpad/recipe-date-last-modified into lp:launchpad.
=== modified file 'lib/lp/code/browser/sourcepackagerecipe.py'
--- lib/lp/code/browser/sourcepackagerecipe.py	2011-03-02 01:47:19 +0000
+++ lib/lp/code/browser/sourcepackagerecipe.py	2011-03-16 07:17:31 +0000
@@ -49,7 +49,6 @@
     SimpleVocabulary,
     )
 
-from canonical.database.constants import UTC_NOW
 from canonical.launchpad import _
 from canonical.launchpad.browser.launchpad import Hierarchy
 from canonical.launchpad.webapp import (
@@ -852,9 +851,6 @@
                 form_field.__name__ for form_field in self.form_fields]
             notify(ObjectModifiedEvent(
                 self.context, recipe_before_modification, field_names))
-            # Only specify that the context was modified if there
-            # was in fact a change.
-            self.context.date_last_modified = UTC_NOW
 
         self.next_url = canonical_url(self.context)
 

=== modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2011-03-02 22:38:44 +0000
+++ lib/lp/code/browser/tests/test_sourcepackagerecipe.py	2011-03-16 07:17:31 +0000
@@ -15,14 +15,16 @@
 from textwrap import dedent
 
 from mechanize import LinkNotFoundError
-from pytz import utc
+from pytz import UTC
 from testtools.matchers import Equals
 import transaction
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
+from canonical.database.constants import UTC_NOW
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
+from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.launchpad.testing.pages import (
     extract_text,
     find_main_content,
@@ -40,8 +42,8 @@
 from lp.buildmaster.enums import BuildStatus
 from lp.code.browser.sourcepackagerecipe import (
     SourcePackageRecipeRequestBuildsView,
-    SourcePackageRecipeView,
-    )
+    SourcePackageRecipeEditView,
+    SourcePackageRecipeView)
 from lp.code.browser.sourcepackagerecipebuild import (
     SourcePackageRecipeBuildView,
     )
@@ -63,6 +65,7 @@
     ANONYMOUS,
     BrowserTestCase,
     login,
+    login_person,
     person_logged_in,
     TestCaseWithFactory,
     time_counter,
@@ -201,8 +204,6 @@
     return extract_text(tags)
 
 
-
-
 class TestSourcePackageRecipeAddViewInitalValues(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
@@ -775,6 +776,24 @@
         self.assertThat(
             'PPA 2', MatchesPickerText(content, 'edit-daily_build_archive'))
 
+    def test_edit_recipe_sets_date_last_modified(self):
+        """Editing a recipe sets the date_last_modified property."""
+        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        recipe = self.factory.makeSourcePackageRecipe(
+            owner=self.chef, registrant=self.chef,
+            name=u'things', description=u'This is a recipe',
+            distroseries=self.squirrel, date_created=date_created)
+
+        login_person(self.chef)
+        view = SourcePackageRecipeEditView(recipe, LaunchpadTestRequest())
+        view.initialize()
+        view.request_action.success({
+            'name': u'fings',
+            'recipe_text': recipe.recipe_text,
+            'distros': recipe.distroseries})
+        self.assertSqlAttributeEqualsDate(
+            recipe, 'date_last_modified', UTC_NOW)
+
     def test_admin_edit(self):
         self.factory.makeDistroSeries(
             displayname='Mumbly Midget', name='mumbly',
@@ -1041,8 +1060,8 @@
         build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
         build.status = BuildStatus.FULLYBUILT
-        build.date_started = datetime(2010, 03, 16, tzinfo=utc)
-        build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
+        build.date_started = datetime(2010, 03, 16, tzinfo=UTC)
+        build.date_finished = datetime(2010, 03, 16, tzinfo=UTC)
 
         self.assertTextMatchesExpressionIgnoreWhitespace("""\
             Master Chef Recipes cake_recipe
@@ -1073,8 +1092,8 @@
         build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
         build.status = BuildStatus.FULLYBUILT
-        build.date_started = datetime(2010, 03, 16, tzinfo=utc)
-        build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
+        build.date_started = datetime(2010, 03, 16, tzinfo=UTC)
+        build.date_finished = datetime(2010, 03, 16, tzinfo=UTC)
         build.log = self.factory.makeLibraryFileAlias()
 
         self.assertTextMatchesExpressionIgnoreWhitespace("""\
@@ -1089,8 +1108,8 @@
         build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
         build.status = BuildStatus.FULLYBUILT
-        build.date_started = datetime(2010, 03, 16, tzinfo=utc)
-        build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
+        build.date_started = datetime(2010, 03, 16, tzinfo=UTC)
+        build.date_finished = datetime(2010, 03, 16, tzinfo=UTC)
         build.log = self.factory.makeLibraryFileAlias()
         package_name = self.factory.getOrMakeSourcePackageName('chocolate')
         source_package_release = self.factory.makeSourcePackageRelease(
@@ -1120,8 +1139,8 @@
         build = removeSecurityProxy(self.factory.makeSourcePackageRecipeBuild(
             recipe=recipe, distroseries=self.squirrel, archive=self.ppa))
         build.status = BuildStatus.FULLYBUILT
-        build.date_started = datetime(2010, 03, 16, tzinfo=utc)
-        build.date_finished = datetime(2010, 03, 16, tzinfo=utc)
+        build.date_started = datetime(2010, 03, 16, tzinfo=UTC)
+        build.date_finished = datetime(2010, 03, 16, tzinfo=UTC)
         build.log = self.factory.makeLibraryFileAlias()
         package_name = self.factory.getOrMakeSourcePackageName('chocolate')
         source_package_release = self.factory.makeSourcePackageRelease(
@@ -1138,8 +1157,8 @@
             processor=builder.processor))
         binary_build.queueBuild()
         binary_build.status = BuildStatus.FULLYBUILT
-        binary_build.date_started = datetime(2010, 04, 16, tzinfo=utc)
-        binary_build.date_finished = datetime(2010, 04, 16, tzinfo=utc)
+        binary_build.date_started = datetime(2010, 04, 16, tzinfo=UTC)
+        binary_build.date_finished = datetime(2010, 04, 16, tzinfo=UTC)
         binary_build.log = self.factory.makeLibraryFileAlias()
 
         self.assertTextMatchesExpressionIgnoreWhitespace("""\
@@ -1197,7 +1216,7 @@
         # We create builds in time ascending order (oldest first) since we
         # use id as the ordering attribute and lower ids mean created earlier.
         date_gen = time_counter(
-            datetime(2010, 03, 16, tzinfo=utc), timedelta(days=1))
+            datetime(2010, 03, 16, tzinfo=UTC), timedelta(days=1))
         build1 = self.makeBuildJob(recipe, date_gen.next())
         build2 = self.makeBuildJob(recipe, date_gen.next())
         build3 = self.makeBuildJob(recipe, date_gen.next())
@@ -1516,7 +1535,7 @@
         view.context.buildqueue_record.job.start()
         clear_property_cache(view)
         self.assertTrue(view.estimate)
-        removeSecurityProxy(view.context).date_finished = datetime.now(utc)
+        removeSecurityProxy(view.context).date_finished = datetime.now(UTC)
         clear_property_cache(view)
         self.assertFalse(view.estimate)
 
@@ -1534,7 +1553,7 @@
         self.assertIs(None, view.eta)
         queue_entry = self.factory.makeSourcePackageRecipeBuildJob(
             recipe_build=build)
-        queue_entry._now = lambda: datetime(1970, 1, 1, 0, 0, 0, 0, utc)
+        queue_entry._now = lambda: datetime(1970, 1, 1, 0, 0, 0, 0, UTC)
         self.factory.makeBuilder()
         clear_property_cache(view)
         self.assertIsNot(None, view.eta)
@@ -1576,7 +1595,7 @@
         self.makeBinaryBuild(release, 'itanic')
         naked_build = removeSecurityProxy(release.source_package_recipe_build)
         naked_build.status = BuildStatus.FULLYBUILT
-        naked_build.date_finished = datetime(2009, 1, 1, tzinfo=utc)
+        naked_build.date_finished = datetime(2009, 1, 1, tzinfo=UTC)
         naked_build.date_started = (
             naked_build.date_finished - timedelta(minutes=1))
         naked_build.buildqueue_record.destroySelf()

=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2011-03-07 07:27:56 +0000
+++ lib/lp/code/configure.zcml	2011-03-16 07:17:31 +0000
@@ -985,6 +985,10 @@
     <allow attributes="as_tuple recipe_branch nest_path" />
   </class>
 
+  <subscriber
+    for="lp.code.interfaces.sourcepackagerecipe.ISourcePackageRecipe zope.lifecycleevent.interfaces.IObjectModifiedEvent"
+    handler="lp.code.model.sourcepackagerecipe.recipe_modified"/>
+
   <utility component="lp.code.model.sourcepackagerecipebuild.SourcePackageRecipeBuildJob"
         name="RECIPEBRANCHBUILD"
         provides="lp.buildmaster.interfaces.buildfarmjob.IBuildFarmJob"/>

=== modified file 'lib/lp/code/interfaces/sourcepackagerecipe.py'
--- lib/lp/code/interfaces/sourcepackagerecipe.py	2011-03-11 23:54:32 +0000
+++ lib/lp/code/interfaces/sourcepackagerecipe.py	2011-03-16 07:17:31 +0000
@@ -261,7 +261,7 @@
     """
 
     def new(registrant, owner, distroseries, name,
-            builder_recipe, description):
+            builder_recipe, description, date_created):
         """Create an `ISourcePackageRecipe`."""
 
     def exists(owner, name):

=== modified file 'lib/lp/code/model/sourcepackagerecipe.py'
--- lib/lp/code/model/sourcepackagerecipe.py	2011-03-15 14:32:17 +0000
+++ lib/lp/code/model/sourcepackagerecipe.py	2011-03-16 07:17:31 +0000
@@ -41,6 +41,7 @@
     implements,
     )
 
+from canonical.database.constants import UTC_NOW
 from canonical.database.datetimecol import UtcDateTimeCol
 from canonical.launchpad.interfaces.launchpad import ILaunchpadCelebrities
 from canonical.launchpad.interfaces.lpstorm import (
@@ -91,6 +92,15 @@
     return buildables
 
 
+def recipe_modified(recipe, event):
+    """Update the date_last_modified property when a recipe is modified.
+
+    This method is registered as a subscriber to `IObjectModifiedEvent` events
+    on recipes.
+    """
+    recipe.date_last_modified = UTC_NOW
+
+
 class NonPPABuildRequest(Exception):
     """A build was requested to a non-PPA and this is currently
     unsupported."""
@@ -176,7 +186,8 @@
 
     @staticmethod
     def new(registrant, owner, name, recipe, description,
-            distroseries=None, daily_build_archive=None, build_daily=False):
+            distroseries=None, daily_build_archive=None, build_daily=False,
+            date_created=None):
         """See `ISourcePackageRecipeSource.new`."""
         store = IMasterStore(SourcePackageRecipe)
         sprecipe = SourcePackageRecipe()
@@ -191,6 +202,8 @@
         sprecipe.description = description
         sprecipe.daily_build_archive = daily_build_archive
         sprecipe.build_daily = build_daily
+        if date_created is not None:
+            sprecipe.date_created = date_created
         store.add(sprecipe)
         return sprecipe
 

=== modified file 'lib/lp/code/model/tests/test_sourcepackagerecipe.py'
--- lib/lp/code/model/tests/test_sourcepackagerecipe.py	2011-03-15 14:32:17 +0000
+++ lib/lp/code/model/tests/test_sourcepackagerecipe.py	2011-03-16 07:17:31 +0000
@@ -16,9 +16,12 @@
 from storm.locals import Store
 import transaction
 from zope.component import getUtility
+from zope.event import notify
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
 
+from lazr.lifecycle.event import ObjectModifiedEvent
+from canonical.database.constants import UTC_NOW
 from canonical.launchpad.webapp.authorization import check_permission
 from canonical.launchpad.webapp.testing import verifyObject
 from canonical.testing.layers import (
@@ -855,6 +858,31 @@
             child_branch, "zam", self.merged_branch.bzr_identity, revspec="2")
 
 
+class RecipeDateLastModified(TestCaseWithFactory):
+    """Exercises the situations where date_last_modified is updated."""
+    layer = DatabaseFunctionalLayer
+
+    def setUp(self):
+        TestCaseWithFactory.setUp(self, 'test@xxxxxxxxxxxxx')
+
+    def test_initialValue(self):
+        """Initially the date_last_modifed is the date_created."""
+        recipe = self.factory.makeSourcePackageRecipe()
+        self.assertEqual(recipe.date_last_modified, recipe.date_created)
+
+    def test_modifiedevent_sets_date_last_updated(self):
+        # We publish an object modified event to check that the last modified
+        # date is set to UTC_NOW.
+        date_created = datetime(2000, 1, 1, 12, tzinfo=UTC)
+        recipe = self.factory.makeSourcePackageRecipe(
+                                    date_created=date_created)
+        field = ISourcePackageRecipe['name']
+        notify(ObjectModifiedEvent(
+            removeSecurityProxy(recipe), recipe, [field]))
+        self.assertSqlAttributeEqualsDate(
+            recipe, 'date_last_modified', UTC_NOW)
+
+
 class TestWebservice(TestCaseWithFactory):
 
     layer = AppServerLayer

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-03-15 23:13:37 +0000
+++ lib/lp/testing/factory.py	2011-03-16 07:17:31 +0000
@@ -2073,11 +2073,11 @@
         for version in versions:
             entry = dedent('''
             %s (%s) unstable; urgency=low
-            
-              * %s. 
-            
+
+              * %s.
+
              -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
-            
+
             ''' % (spn, version, version))
             changelog += entry
         return self.makeLibraryFileAlias(content=changelog)
@@ -2492,7 +2492,8 @@
                                 distroseries=None, name=None,
                                 description=None, branches=(),
                                 build_daily=False, daily_build_archive=None,
-                                is_stale=None, recipe=None):
+                                is_stale=None, recipe=None,
+                                date_created=None):
         """Make a `SourcePackageRecipe`."""
         if registrant is None:
             registrant = self.makePerson()
@@ -2514,7 +2515,7 @@
             assert branches == ()
         source_package_recipe = getUtility(ISourcePackageRecipeSource).new(
             registrant, owner, name, recipe, description, [distroseries],
-            daily_build_archive, build_daily)
+            daily_build_archive, build_daily, date_created)
         if is_stale is not None:
             removeSecurityProxy(source_package_recipe).is_stale = is_stale
         IStore(source_package_recipe).flush()