← Back to team overview

launchpad-reviewers team mailing list archive

lp:~edwin-grubbs/launchpad/bug-602385-register-project-from-sourcepackage-page into lp:launchpad/devel

 

Edwin Grubbs has proposed merging lp:~edwin-grubbs/launchpad/bug-602385-register-project-from-sourcepackage-page into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


Summary
-------

This branch makes it easy to register an upstream project from a source
package by prefilling the project registration form with data from the
source package.

Implementation details
----------------------

Added link to $sourcepackage/+edit-packaging and radio button to
$sourcepackage/+index
    lib/lp/registry/browser/sourcepackage.py
    lib/lp/registry/templates/sourcepackage-edit-packaging.pt
    lib/lp/registry/browser/product.py
    lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt

The makeBinaryPackagePublishingHistory() method had a
binarypackagerelease argument, but it completed ignored it.
    lib/lp/testing/factory.py

When +edit-packaging became a MultiStepView, some of the tests
quit exercising the view completely, since no errors were expected, and
the view had just stopped doing anything. I added a check inside the
MultiStepView to help prevent this from happening.
    lib/lp/registry/browser/tests/sourcepackage-views.txt
    lib/canonical/launchpad/webapp/launchpadform.py
    lib/canonical/launchpad/browser/multistep.py

Drive-by lint fixes.
    lib/lp/registry/model/sourcepackage.py

Tests
-----

./bin/test -vv -t '/sourcepackage-views.txt|xx-sourcepackage-packaging.txt'

Demo and Q/A
------------

The summary field is not always populated, since the
only info a source package has is the summaries from its binary
packages that might not exist yet.


* Open http://launchpad.dev/ubuntu/warty/+source/foobar
  * Select "Register the upstream project" radio button.
  * Click the "Link to Upstream Project" button.
  * The form should be prefilled.
  * Enter the license.
  * Submit the form.
  * You should now be redirected to the source package page.

* Open http://launchpad.dev/ubuntu/warty/+source/pmount/+edit-packaging
  * Click the "Register the upstream project" link.
  * The form should be prefilled.
  * Enter the license.
  * Submit the form.
  * You should now be redirected to the +edit-packaging page.
-- 
https://code.launchpad.net/~edwin-grubbs/launchpad/bug-602385-register-project-from-sourcepackage-page/+merge/31856
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~edwin-grubbs/launchpad/bug-602385-register-project-from-sourcepackage-page into lp:launchpad/devel.
=== modified file 'lib/canonical/launchpad/browser/multistep.py'
--- lib/canonical/launchpad/browser/multistep.py	2010-05-12 19:06:17 +0000
+++ lib/canonical/launchpad/browser/multistep.py	2010-08-05 16:17:58 +0000
@@ -93,6 +93,14 @@
         view.total_steps = self.total_steps
         view.is_step = self.getIsStepDict()
         self.step_number += 1
+
+        action_required = None
+        for name in self.request.form.keys():
+            if name.startswith('field.actions.'):
+                action_required = (name, self.request.form[name])
+                break
+
+        action_taken = view.action_taken
         while view.next_step is not None:
             view = view.next_step(self.context, self.request)
             assert isinstance(view, StepView), 'Not a StepView: %s' % view
@@ -102,8 +110,19 @@
             view.is_step = self.getIsStepDict()
             self.step_number += 1
             view.injectStepNameInRequest()
+            if view.action_taken is not None:
+                action_taken = view.action_taken
+
         self.view = view
 
+        if action_required is not None and action_taken is None:
+            # This is mostly useful for catching tests that pass
+            # in invalid form data via a dictionary instead of
+            # using a test browser.
+            raise AssertionError(
+                'MultiStepView did not find action for %s=%r'
+                % action_required)
+
     def render(self):
         return self.view.render()
 

=== modified file 'lib/canonical/launchpad/webapp/launchpadform.py'
--- lib/canonical/launchpad/webapp/launchpadform.py	2010-06-23 23:07:10 +0000
+++ lib/canonical/launchpad/webapp/launchpadform.py	2010-08-05 16:17:58 +0000
@@ -74,6 +74,8 @@
 
     actions = ()
 
+    action_taken = None
+
     render_context = False
 
     form_result = None
@@ -112,6 +114,7 @@
             self.form_result = action.success(data)
             if self.next_url:
                 self.request.response.redirect(self.next_url)
+        self.action_taken = action
 
     def render(self):
         """Return the body of the response.

=== modified file 'lib/lp/registry/browser/product.py'
--- lib/lp/registry/browser/product.py	2010-08-04 04:07:21 +0000
+++ lib/lp/registry/browser/product.py	2010-08-05 16:17:58 +0000
@@ -120,7 +120,7 @@
 from canonical.launchpad.webapp.breadcrumb import Breadcrumb
 from canonical.launchpad.webapp.launchpadform import (
     action, custom_widget, LaunchpadEditFormView, LaunchpadFormView,
-    ReturnToReferrerMixin)
+    ReturnToReferrerMixin, safe_action)
 from canonical.launchpad.webapp.menu import NavigationMenu
 from canonical.launchpad.webapp.tales import MenuAPI
 from canonical.widgets.popup import PersonPickerWidget
@@ -1850,6 +1850,16 @@
     search_results_count = 0
 
     @property
+    def _return_url(self):
+        """This view is using the hidden _return_url field.
+
+        It is not using the `ReturnToReferrerMixin`, since none
+        of its other code is used, because multistep views can't
+        have next_url set until the form submission succeeds.
+        """
+        return self.request.form.get('_return_url')
+
+    @property
     def _next_step(self):
         """Define the next step.
 
@@ -1866,6 +1876,10 @@
         self.request.form['name'] = data['name'].lower()
         self.request.form['summary'] = data['summary']
 
+    # Make this a safe_action, so that the sourcepackage page can skip
+    # the first step with a link (GET request) providing form values.
+    continue_action = safe_action(StepView.continue_action)
+
 
 class ProjectAddStepTwo(StepView, ProductLicenseMixin, ReturnToReferrerMixin):
     """Step 2 (of 2) in the +new project add wizard."""
@@ -1887,6 +1901,16 @@
     custom_widget('license_info', GhostWidget)
 
     @property
+    def _return_url(self):
+        """This view is using the hidden _return_url field.
+
+        It is not using the `ReturnToReferrerMixin`, since none
+        of its other code is used, because multistep views can't
+        have next_url set until the form submission succeeds.
+        """
+        return self.request.form.get('_return_url')
+
+    @property
     def step_description(self):
         """See `MultiStepView`."""
         if self.search_results_count > 0:
@@ -2001,7 +2025,10 @@
         self.product = self.create_product(data)
         self.notifyCommercialMailingList()
         notify(ObjectCreatedEvent(self.product))
-        self.next_url = canonical_url(self.product)
+        if self._return_url is None:
+            self.next_url = canonical_url(self.product)
+        else:
+            self.next_url = self._return_url
 
 
 class ProductAddView(MultiStepView):
@@ -2027,7 +2054,7 @@
 
     driver = copy_field(IProduct['driver'])
 
-    transfer_to_registry =  Bool(
+    transfer_to_registry = Bool(
         title=_("I do not want to maintain this project"),
         required=False,
         description=_(

=== modified file 'lib/lp/registry/browser/sourcepackage.py'
--- lib/lp/registry/browser/sourcepackage.py	2010-07-02 14:34:58 +0000
+++ lib/lp/registry/browser/sourcepackage.py	2010-08-05 16:17:58 +0000
@@ -19,6 +19,8 @@
 
 from apt_pkg import ParseSrcDepends
 from cgi import escape
+import string
+import urllib
 from z3c.ptcompat import ViewPageTemplateFile
 from zope.app.form.browser import DropdownWidget
 from zope.app.form.interfaces import IInputWidget
@@ -35,12 +37,14 @@
 
 from canonical.launchpad import helpers
 from canonical.launchpad.browser.multistep import MultiStepView, StepView
+
 from lp.bugs.browser.bugtask import BugTargetTraversalMixin
 from canonical.launchpad.browser.packagerelationship import (
     relationship_builder)
 from lp.answers.browser.questiontarget import (
     QuestionTargetFacetMixin, QuestionTargetAnswersMenu)
 from lp.services.worlddata.interfaces.country import ICountry
+from lp.registry.browser.product import ProjectAddStepOne
 from lp.registry.interfaces.packaging import IPackaging, IPackagingUtil
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.product import IProductSet
@@ -62,6 +66,25 @@
 from canonical.lazr.utils import smartquote
 
 
+def get_register_upstream_url(source_package, return_url):
+    displayname = string.capwords(source_package.name.replace('-', ' '))
+    params = {
+        '_return_url': return_url,
+        'field.name': source_package.name,
+        'field.displayname': displayname,
+        'field.title': displayname,
+        'field.__visited_steps__': ProjectAddStepOne.step_name,
+        'field.actions.continue': 'Continue',
+        }
+    if source_package.summary is None:
+        params['field.summary'] = ''
+    else:
+        params['field.summary'] = source_package.summary
+    query_string = urllib.urlencode(
+        sorted(params.items()), doseq=True)
+    return '/projects/+new?%s' % query_string
+
+
 class SourcePackageNavigation(GetitemNavigation, BugTargetTraversalMixin):
 
     usedfor = ISourcePackage
@@ -190,6 +213,11 @@
         self.next_step = SourcePackageChangeUpstreamStepTwo
         self.request.form['product'] = data['product']
 
+    @property
+    def register_upstream_url(self):
+        return get_register_upstream_url(
+            self.context, return_url=self.request.getURL())
+
 
 class SourcePackageChangeUpstreamStepTwo(ReturnToReferrerMixin, StepView):
     """A view to set the `IProductSeries` of a sourcepackage."""
@@ -345,7 +373,7 @@
     def processForm(self):
         # look for an update to any of the things we track
         form = self.request.form
-        if form.has_key('packaging'):
+        if 'packaging' in form:
             if self.productseries_widget.hasValidInput():
                 new_ps = self.productseries_widget.getInputValue()
                 # we need to create or update the packaging
@@ -445,6 +473,7 @@
     initial_focus_widget = None
     max_suggestions = 9
     other_upstream = object()
+    register_upstream = object()
 
     def setUpFields(self):
         """See `LaunchpadFormView`."""
@@ -467,9 +496,12 @@
             vocab_terms.append(SimpleTerm(product, product.name, description))
         # Add an option to represent the user's decision to choose a
         # different project. Note that project names cannot be uppercase.
-        description = 'Choose another upstream project'
-        vocab_terms.append(
-            SimpleTerm(self.other_upstream, 'OTHER_UPSTREAM', description))
+        vocab_terms.append(
+            SimpleTerm(self.other_upstream, 'OTHER_UPSTREAM',
+                       'Choose another upstream project'))
+        vocab_terms.append(
+            SimpleTerm(self.register_upstream, 'REGISTER_UPSTREAM',
+                       'Register the upstream project'))
         upstream_vocabulary = SimpleVocabulary(vocab_terms)
 
         self.form_fields = Fields(
@@ -487,6 +519,12 @@
             self.next_url = canonical_url(
                 self.context, view_name="+edit-packaging")
             return
+        elif upstream is self.register_upstream:
+            # The user wants to create a new project.
+            url = get_register_upstream_url(
+                self.context, return_url=self.request.getURL())
+            self.request.response.redirect(url)
+            return
         self.context.setPackaging(upstream.development_focus, self.user)
         self.request.response.addInfoNotification(
             'The project %s was linked to this source package.' %

=== modified file 'lib/lp/registry/browser/tests/sourcepackage-views.txt'
--- lib/lp/registry/browser/tests/sourcepackage-views.txt	2010-05-13 18:55:10 +0000
+++ lib/lp/registry/browser/tests/sourcepackage-views.txt	2010-08-05 16:17:58 +0000
@@ -119,6 +119,7 @@
 empty.
 
     >>> form = {
+    ...     'field.__visited_steps__': 'sourcepackage_change_upstream_step1',
     ...     'field.product': '',
     ...     'field.actions.continue': 'Continue',
     ...     }
@@ -133,12 +134,18 @@
 but there is no notification message that the upstream link was updated.
 
     >>> form = {
-    ...     'field.productseries': 'bonkers/crazy',
-    ...     'field.actions.change': 'Change',
+    ...     'field.__visited_steps__': 'sourcepackage_change_upstream_step2',
+    ...     'field.product': 'bonkers',
+    ...     'field.productseries': 'crazy',
+    ...     'field.actions.continue': 'Continue',
     ...     }
     >>> view = create_initialized_view(
     ...     package, name='+edit-packaging', form=form,
     ...     principal=product.owner)
+    >>> print view.view
+    <...SourcePackageChangeUpstreamStepTwo object...>
+    >>> print view.view.next_url
+    http://launchpad.dev/youbuntu/busy/+source/bonkers
     >>> view.view.errors
     []
 
@@ -199,6 +206,7 @@
     Registered upstream project:
     Lernid
     Choose another upstream project
+    Register the upstream project
 
 The form does not steal focus because it is not the primary purpose of the
 page.
@@ -229,6 +237,7 @@
     Lernid...
     Lernid Dev...
     Choose another upstream project
+    Register the upstream project
 
 Choosing the "Choose another upstream project" option redirects the user
 to the +edit-packaging page where the user can search for a project.
@@ -259,7 +268,8 @@
     ...     name='stinkyseries', product=product)
     >>> distroseries = factory.makeDistroRelease(name='wonky',
     ...     distribution=distribution)
-    >>> sourcepackagename = factory.makeSourcePackageName(name='stinkypackage')
+    >>> sourcepackagename = factory.makeSourcePackageName(
+    ...     name='stinkypackage')
     >>> package = factory.makeSourcePackage(
     ...     sourcepackagename=sourcepackagename, distroseries=distroseries)
 
@@ -360,3 +370,4 @@
     match for this source package. Can you help us find one?
     Registered upstream project:
     Choose another upstream project
+    Register the upstream project

=== modified file 'lib/lp/registry/model/sourcepackage.py'
--- lib/lp/registry/model/sourcepackage.py	2010-07-15 15:01:18 +0000
+++ lib/lp/registry/model/sourcepackage.py	2010-08-05 16:17:58 +0000
@@ -39,7 +39,6 @@
 from lp.registry.model.packaging import Packaging
 from lp.translations.model.potemplate import (
     HasTranslationTemplatesMixin,
-    POTemplate,
     TranslationTemplatesCollection)
 from canonical.launchpad.interfaces.lpstorm import IStore
 from lp.soyuz.model.publishing import (
@@ -52,7 +51,6 @@
     SourcePackageRelease)
 from lp.translations.model.translationimportqueue import (
     HasTranslationImportsMixin)
-from canonical.launchpad.helpers import shortlist
 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
 from lp.registry.interfaces.packaging import PackagingType
 from lp.registry.interfaces.distribution import NoPartnerArchive
@@ -302,6 +300,7 @@
         """See `ISourcePackage`."""
         releases = self.releases
         if len(releases) == 0:
+            print '\n@@@@@@@@@@@@@@@@@@@@@@No releases'
             return None
         current = releases[0]
         name_summaries = [

=== modified file 'lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt'
--- lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt	2010-05-18 17:05:29 +0000
+++ lib/lp/registry/stories/packaging/xx-sourcepackage-packaging.txt	2010-08-05 16:17:58 +0000
@@ -1,4 +1,59 @@
-= Packaging =
+Packaging
+=========
+
+Create test data.
+
+    >>> login('admin@xxxxxxxxxxxxx')
+    >>> distribution = factory.makeDistribution(
+    ...     name='youbuntu', displayname='Youbuntu')
+    >>> distroseries = factory.makeDistroRelease(name='busy',
+    ...     distribution=distribution)
+    >>> sourcepackagename = factory.makeSourcePackageName(name='bonkers')
+    >>> package = factory.makeSourcePackage(
+    ...     sourcepackagename=sourcepackagename, distroseries=distroseries)
+    >>> component = factory.makeComponent('multiverse')
+    >>> das = factory.makeDistroArchSeries(distroseries=distroseries)
+    >>> spph = factory.makeSourcePackagePublishingHistory(
+    ...     sourcepackagename=sourcepackagename, distroseries=distroseries,
+    ...     component=component)
+
+    # The setup below this point is needed to fill in the
+    # SourcePackageName.summary, which is assembled from the
+    # BinaryPackageRelease.summary info stored in the
+    # DistroSeriesPackageCache.summary.
+    >>> build = factory.makeBinaryPackageBuild(
+    ...     source_package_release=spph.sourcepackagerelease,
+    ...     distroarchseries=das)
+    >>> bpr = factory.makeBinaryPackageRelease(
+    ...     binarypackagename=factory.makeBinaryPackageName('flubber'),
+    ...     summary='floating rubber summary',
+    ...     build=build, component=component)
+    >>> bpph = factory.makeBinaryPackagePublishingHistory(
+    ...     binarypackagerelease=bpr, distroarchseries=das)
+    >>> class TestLogger:
+    ...     def debug(self, msg):
+    ...         print 'DEBUG:', msg
+    >>> import transaction
+    >>> from canonical.config import config
+    >>> from canonical.testing.layers import reconnect_stores
+    >>> transaction.commit()
+
+    # XXX: EdwinGrubbs 2010-08-04 bug=396419. Currently there is no
+    # test api call to switchDbUser that works for non-zopeless layers.
+    # When bug 396419 is fixed, we can instead use
+    # DatabaseLayer.switchDbUser() instead of reconnect_stores()
+    >>> reconnect_stores(config.statistician.dbuser)
+    >>> from zope.component import getUtility
+    >>> from lp.registry.interfaces.distroseries import IDistroSeriesSet
+    >>> distroseries = getUtility(IDistroSeriesSet).get(distroseries.id)
+    >>> distroseries.updateCompletePackageCache(
+    ...     archive=distroseries.distribution.main_archive,
+    ...     ztm=transaction,
+    ...     log=TestLogger())
+    DEBUG: Considering binary 'flubber'...
+    >>> transaction.commit()
+    >>> reconnect_stores('launchpad')
+    >>> logout()
 
 No Privileges Person visit the distroseries upstream links page for Hoary
 and sees that pmount is not linked.
@@ -20,6 +75,7 @@
     match for this source package. Can you help us find one?
     Registered upstream project:
     Choose another upstream project
+    Register the upstream project
 
 No Privileges Person knows that the pmount package comes from the thunderbird
 project. He sets the upstream packaging link and sees that it is set.
@@ -58,3 +114,77 @@
     ...     user_browser.contents, 'packages_list'))
     The Hoary Hedgehog Release (active development) ...
       0.1-2  release (main) ... weeks ago
+
+Register a project from a source package
+----------------------------------------
+
+If an upstream project doesn't already exist in Launchpad, it can
+be registered with data from the source package prefilling the first
+step of the multistep form.
+
+    >>> user_browser.open(
+    ...     'http://launchpad.dev/youbuntu/busy/+source/bonkers')
+    >>> user_browser.getControl(
+    ...     'Register the upstream project').selected = True
+    >>> user_browser.getControl("Link to Upstream Project").click()
+    >>> print user_browser.url.replace('&', '\n&')
+    http://launchpad.dev/projects/+new?_return_url=http...%2Bindex
+    &field.__visited_steps__=projectaddstep1
+    &field.actions.continue=Continue
+    &field.displayname=Bonkers
+    &field.name=bonkers
+    &field.summary=flubber%3A+floating+rubber+summary
+    &field.title=Bonkers
+    >>> print user_browser.getControl(name='field.name').value
+    bonkers
+    >>> print user_browser.getControl(name='field.displayname').value
+    Bonkers
+    >>> print user_browser.getControl(name='field.title').value
+    Bonkers
+    >>> print user_browser.getControl(name='field.summary').value
+    flubber: floating rubber summary
+    >>> print extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'step-title'))
+    Step 2 (of 2): Check for duplicate projects
+
+If the user selects "Choose another upstream project" and then finds out
+that the project doesn't exist, there is a also a link on the
++edit-packaging page to register the project.
+
+    >>> user_browser.open(
+    ...     'http://launchpad.dev/youbuntu/busy/+source/bonkers/')
+    >>> user_browser.getControl(
+    ...     'Choose another upstream project').selected = True
+    >>> user_browser.getControl("Link to Upstream Project").click()
+    >>> print user_browser.url
+    http://launchpad.dev/youbuntu/busy/+source/bonkers/+edit-packaging
+
+    >>> user_browser.getLink("Register the upstream project").click()
+    >>> print user_browser.url.replace('&', '\n&')
+    http://launchpad.dev/projects/+new?_return_url=http...%2Bedit-packaging
+    &field.__visited_steps__=projectaddstep1
+    &field.actions.continue=Continue
+    &field.displayname=Bonkers
+    &field.name=bonkers
+    &field.summary=flubber%3A+floating+rubber+summary
+    &field.title=Bonkers
+    >>> print user_browser.getControl(name='field.name').value
+    bonkers
+    >>> print user_browser.getControl(name='field.displayname').value
+    Bonkers
+    >>> print user_browser.getControl(name='field.title').value
+    Bonkers
+    >>> print user_browser.getControl(name='field.summary').value
+    flubber: floating rubber summary
+    >>> print extract_text(
+    ...     find_tag_by_id(user_browser.contents, 'step-title'))
+    Step 2 (of 2): Check for duplicate projects
+
+If there are no problems with the prefilled data, then the license
+just needs to be selected. The user will then be redirected back
+to the source package page so that it can be linked.
+
+    >>> user_browser.getControl(name='field.licenses').value = ['BSD']
+    >>> user_browser.getControl("Complete Registration").click()
+    >>> print user_browser.url
+    http://launchpad.dev/youbuntu/busy/+source/bonkers/+edit-packaging

=== modified file 'lib/lp/registry/templates/sourcepackage-edit-packaging.pt'
--- lib/lp/registry/templates/sourcepackage-edit-packaging.pt	2010-02-16 17:37:36 +0000
+++ lib/lp/registry/templates/sourcepackage-edit-packaging.pt	2010-08-05 16:17:58 +0000
@@ -27,6 +27,26 @@
       If you need a new series created, contact the owner of
       <a tal:content="structure view/product/fmt:link"/>.
     </div>
+
+    <div metal:fill-slot="buttons">
+      <input tal:repeat="action view/actions"
+             tal:replace="structure action/render"
+        />
+      &nbsp;or&nbsp;
+      <tal:comment condition="nothing">
+        This template is for a multistep view, and only the first
+        step provides the register_upstream_url.
+      </tal:comment>
+      <a id="register-upstream-link"
+         tal:condition="view/register_upstream_url | nothing"
+         tal:attributes="href view/register_upstream_url">
+        Register the upstream project
+      </a>
+      <tal:has-cancel-link condition="view/cancel_url">
+        &nbsp;or&nbsp;
+        <a tal:attributes="href view/cancel_url">Cancel</a>
+      </tal:has-cancel-link>
+    </div>
   </div>
 
 </div>

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-08-04 00:47:20 +0000
+++ lib/lp/testing/factory.py	2010-08-05 16:17:58 +0000
@@ -2520,6 +2520,7 @@
                 section_name=section_name,
                 priority=priority)
 
+<<<<<<< TREE
         bpph = getUtility(IPublishingSet).newBinaryPublication(
             archive, binarypackagerelease, distroarchseries,
             binarypackagerelease.component, binarypackagerelease.section,
@@ -2530,6 +2531,19 @@
         naked_bpph.scheduleddeletiondate = scheduleddeletiondate
         naked_bpph.priority = priority
         return bpph
+=======
+        return BinaryPackagePublishingHistory(
+            distroarchseries=distroarchseries,
+            binarypackagerelease=binarypackagerelease,
+            component=binarypackagerelease.component,
+            section=binarypackagerelease.section,
+            status=status,
+            dateremoved=dateremoved,
+            scheduleddeletiondate=scheduleddeletiondate,
+            pocket=pocket,
+            priority=priority,
+            archive=archive)
+>>>>>>> MERGE-SOURCE
 
     def makeBinaryPackageName(self, name=None):
         if name is None: