← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~bac/launchpad/bug-39212 into lp:launchpad/devel

 

Brad Crittenden has proposed merging lp:~bac/launchpad/bug-39212 into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #39212 +editwikinames needs validation of very large values
  https://bugs.launchpad.net/bugs/39212
  #54696 +editwikinames needs better unique validation.
  https://bugs.launchpad.net/bugs/54696


= Summary =

Several of the person/+editthingy pages pre-date LaunchpadFormView.  In
a misguided attempt to be super-friendly, the pages allowed in-place
editing of lots of data.  Doing so ran the risk that internal
inconsistencies in the submitted batch would cause a failure.

== Proposed fix ==

Change +editwikinames to be a LaunchpadFormView and only allow existing
data to be displayed and marked for removal.

== Pre-implementation notes ==

None.  Similar to the recent change to +editjabberids

== Implementation details ==

As above.

== Tests ==

bin/test -vvm lp.registry -t xx-person-edit-wikis.txt

== Demo and Q/A ==

https://launchpad.dev/people/+me/+editwikinames

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/templates/person-editwikinames.pt
  lib/lp/registry/stories/person/xx-person-edit-wikis.txt
  lib/lp/registry/browser/person.py
-- 
https://code.launchpad.net/~bac/launchpad/bug-39212/+merge/34196
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~bac/launchpad/bug-39212 into lp:launchpad/devel.
=== modified file 'lib/lp/registry/browser/person.py'
--- lib/lp/registry/browser/person.py	2010-08-30 06:38:53 +0000
+++ lib/lp/registry/browser/person.py	2010-08-31 13:20:59 +0000
@@ -219,6 +219,7 @@
     LaunchpadRadioWidgetWithDescription,
     LocationWidget,
     PasswordChangeWidget,
+    URIWidget,
     )
 from canonical.widgets.image import ImageChangeWidget
 from canonical.widgets.itemswidgets import LabeledMultiCheckBoxWidget
@@ -297,7 +298,10 @@
     ITeamMembershipSet,
     TeamMembershipStatus,
     )
-from lp.registry.interfaces.wikiname import IWikiNameSet
+from lp.registry.interfaces.wikiname import (
+    IWikiName,
+    IWikiNameSet,
+    )
 from lp.services.fields import LocationField
 from lp.services.openid.adapters.openid import CurrentOpenIDEndPoint
 from lp.services.openid.browser.openiddiscovery import (
@@ -3498,35 +3502,76 @@
                 sCoC_util.modifySignature(sig_id, self.user, comment, False)
 
 
-class PersonEditWikiNamesView(LaunchpadView):
+class PersonEditWikiNamesView(LaunchpadFormView):
     """View for ~person/+editwikinames"""
 
+    schema = IWikiName
+    fields = ['wiki', 'wikiname']
+    # Use custom widgets solely to get the width correct.  The URIWidget has a
+    # CSS class that does not respect the displayWidth, thus the need to use a
+    # different cssClass.
+    custom_widget('wiki', URIWidget, displayWidth=40, cssClass="textType")
+    custom_widget('wikiname', TextWidget, displayWidth=40)
+
     @property
     def label(self):
         return smartquote("%s's wiki names" % self.context.displayname)
 
     @property
-    def cancel_url(self):
-        return canonical_url(self.context, view_name="+edit")
+    def next_url(self):
+        return canonical_url(self.context)
+
+    cancel_url = next_url
+
+    def setUpFields(self):
+        super(PersonEditWikiNamesView, self).setUpFields()
+        if self.context.wiki_names.count() > 0:
+            # Make the wiki and wiki_name entries optional on the edit page if
+            # one or more ids already exist, which allows the removal of ids
+            # without filling out the new wiki fields.
+            wiki_field = self.form_fields['wiki']
+            wikiname_field = self.form_fields['wikiname']
+            # Copy the fields so as not to modify the interface.
+            wiki_field.field = copy_field(wiki_field.field)
+            wiki_field.field.required = False
+            wikiname_field.field = copy_field(wikiname_field.field)
+            wikiname_field.field.required = False
 
     def _validateWikiURL(self, url):
         """Validate the URL.
 
         Make sure that the result is a valid URL with only the
-        appropriate schemes.
+        appropriate schemes.  The url is assumed to be a string.
         """
+        if url is None:
+            return
         try:
             uri = URI(url)
             if uri.scheme not in ('http', 'https'):
-                self.error_message = structured(
-                    'The URL scheme "%(scheme)s" is not allowed.  '
-                    'Only http or https URLs may be used.', scheme=uri.scheme)
-                return False
+                self.setFieldError(
+                    'wiki',
+                    structured(
+                        'The URL scheme "%(scheme)s" is not allowed.  '
+                        'Only http or https URLs may be used.',
+                        scheme=uri.scheme))
         except InvalidURIError:
-            self.error_message = structured(
-                '"%(url)s" is not a valid URL.', url=url)
-            return False
-        return True
+            self.setFieldError(
+                        'wiki',
+                        structured(
+                            '"%(url)s" is not a valid URL.', url=url))
+
+    def _validateWikiName(self, name):
+        """Ensure the wikiname is valid.
+
+        It must not be longer than 100 characters.  Name is assumed to be a
+        string.
+        """
+        max_len = 100
+        if len(name) > max_len:
+            self.setFieldError(
+                'wikiname',
+                structured(
+                'The wiki name cannot exceed %d characters.' % max_len))
 
     def _sanitizeWikiURL(self, url):
         """Strip whitespaces and make sure :url ends in a single '/'."""
@@ -3534,72 +3579,82 @@
             return url
         return '%s/' % url.strip().rstrip('/')
 
-    def initialize(self):
-        """Process the WikiNames form."""
-        self.error_message = None
-        if self.request.method != "POST":
-            # Nothing to do
-            return
-
-        form = self.request.form
-        context = self.context
+    def validate(self, data):
+        # If there are already form errors then just show them.
+        if self.errors:
+            return
+        wikiurl = self._sanitizeWikiURL(data.get('wiki'))
+        wikiname = data.get('wikiname')
+        if wikiurl or wikiname:
+            if not wikiurl:
+                self.setFieldError(
+                    'wiki',
+                    structured(
+                        'The Wiki URL must be specified.'))
+            if not wikiname:
+                self.setFieldError(
+                    'wikiname',
+                    structured(
+                        'The Wiki name must be specified.'))
+
+        if self.errors:
+            return
+
+        if wikiurl is not None:
+            self._validateWikiURL(wikiurl)
+        if wikiname is not None:
+            self._validateWikiName(wikiname)
+
+        if self.errors:
+            return
+
         wikinameset = getUtility(IWikiNameSet)
-
-        for w in context.wiki_names:
-            # XXX: GuilhermeSalgado 2005-08-25:
-            # We're exposing WikiName IDs here because that's the only
-            # unique column we have. If we don't do this we'll have to
-            # generate the field names using the WikiName.wiki and
-            # WikiName.wikiname columns (because these two columns make
-            # another unique identifier for WikiNames), but that's tricky and
-            # not worth the extra work.
-            if form.get('remove_%d' % w.id):
-                w.destroySelf()
-            else:
-                wiki = self._sanitizeWikiURL(form.get('wiki_%d' % w.id))
-                wikiname = form.get('wikiname_%d' % w.id)
-                if not (wiki and wikiname):
-                    self.error_message = structured(
-                        "Neither Wiki nor WikiName can be empty.")
-                    return
-                if not self._validateWikiURL(wiki):
-                    return
-                w.wiki = wiki
-                w.wikiname = wikiname
-
-        wiki = self._sanitizeWikiURL(form.get('newwiki'))
-        wikiname = form.get('newwikiname')
-        if wiki or wikiname:
-            if wiki and wikiname:
-                existingwiki = wikinameset.getByWikiAndName(wiki, wikiname)
-                if existingwiki and existingwiki.person != context:
-                    owner_name = urllib.quote(existingwiki.person.name)
-                    merge_url = (
-                        '%s/+requestmerge?field.dupe_person=%s'
-                        % (canonical_url(getUtility(IPersonSet)), owner_name))
-                    self.error_message = structured(
+        existingwiki = wikinameset.getByWikiAndName(wikiurl, wikiname)
+        if existingwiki:
+            if existingwiki.person != self.context:
+                owner_name = urllib.quote(existingwiki.person.name)
+                merge_url = (
+                    '%s/+requestmerge?field.dupe_person=%s'
+                    % (canonical_url(getUtility(IPersonSet)), owner_name))
+                self.setFieldError(
+                    'wikiname',
+                    structured(
                         'The WikiName %s%s is already registered by '
                         '<a href="%s">%s</a>. If you think this is a '
                         'duplicated account, you can <a href="%s">merge it'
                         '</a> into your account.',
-                        wiki, wikiname, canonical_url(existingwiki.person),
-                        existingwiki.person.displayname, merge_url)
-                    return
-
-                elif existingwiki:
-                    self.error_message = structured(
-                        'The WikiName %s%s already belongs to you.',
-                        wiki, wikiname)
-                    return
-                if not self._validateWikiURL(wiki):
-                    return
-                wikinameset.new(context, wiki, wikiname)
+                        wikiurl, wikiname,
+                        canonical_url(existingwiki.person),
+                        existingwiki.person.displayname, merge_url))
             else:
-                self.newwiki = wiki
-                self.newwikiname = wikiname
-                self.error_message = structured(
-                    "Neither Wiki nor WikiName can be empty.")
-                return
+                # The person already has this wiki.
+                self.setFieldError(
+                    'wikiname',
+                    'The WikiName %s%s already belongs to you.' %
+                    (wikiurl, wikiname))
+
+    def _save(self, wikiurl, wikiname):
+        """Given a wikiurl and wikiname, attempt to save it.
+
+        Verify someone else doesn't have it already.
+        """
+
+    @action(_("Save Changes"), name="save")
+    def save(self, action, data):
+        """Process the wiki names form."""
+        form = self.request.form
+        for obj in self.context.wiki_names:
+            if form.get('remove_%s' % obj.id):
+                obj.destroySelf()
+
+        if not self.errors:
+            wikiurl = self._sanitizeWikiURL(data.get('wiki'))
+            wikiname = data.get('wikiname')
+            # If either url or name are present then they both must be
+            # entered.
+            if wikiurl and wikiname:
+                wikinameset = getUtility(IWikiNameSet)
+                wikinameset.new(self.context, wikiurl, wikiname)
 
 
 class PersonEditIRCNicknamesView(LaunchpadFormView):
@@ -3664,6 +3719,8 @@
             # ids already exist, which allows the removal of ids without
             # filling out the new jabberid field.
             jabber_field = self.form_fields['jabberid']
+            # Copy the field so as not to modify the interface.
+            jabber_field.field = copy_field(jabber_field.field)
             jabber_field.field.required = False
 
     @property

=== modified file 'lib/lp/registry/stories/person/xx-person-edit-wikis.txt'
--- lib/lp/registry/stories/person/xx-person-edit-wikis.txt	2009-11-15 20:27:01 +0000
+++ lib/lp/registry/stories/person/xx-person-edit-wikis.txt	2010-08-31 13:20:59 +0000
@@ -1,109 +1,135 @@
-= Person's wikinames =
+==================
+Person's wikinames
+==================
 
 A person can have any number of WikiNames registered in Launchpad, and
 they can be managed on the +editwikinames page.
 
     # A helper function for printing all wikinames of a person.
-    >>> import re
-    >>> def print_existing_wikinames(contents):
-    ...     soup = find_main_content(contents)
-    ...     wiki_url_inputs = soup.findAll(
-    ...         'input', attrs={'name': re.compile(r'wiki_\d+')})
-    ...     wiki_urls = [input['value'] for input in wiki_url_inputs]
-    ...     wiki_name_inputs = soup.findAll(
-    ...         'input', attrs={'name': re.compile(r'wikiname_\d+')})
-    ...     wiki_names = [input['value'] for input in wiki_name_inputs]
-    ...     wikis = zip(wiki_urls, wiki_names)
-    ...     print '\n'.join('%s%s' % (wiki_url, wiki_name)
-    ...                     for wiki_url, wiki_name in wikis)
+    >>> def print_existing_wikiurls(contents):
+    ...     trs = find_tags_by_class(contents, 'wikiurl')
+    ...     wikis = []
+    ...     for tr in trs:
+    ...         td = tr.find('td')
+    ...         wikis.append(td.contents[0])
+    ...     print '\n'.join(wikis)
+
+    >>> def print_feedback(browser):
+    ...     print "\n".join(get_feedback_messages(browser.contents))
 
 Mark already has one WikiName registered.
 
     >>> browser.addHeader('Authorization', 'Basic mark@xxxxxxxxxxx:test')
-    >>> browser.open('http://localhost/~mark/+editwikinames')
-    >>> print_existing_wikinames(browser.contents)
+    >>> browser.open('http://launchpad.dev/~mark/+editwikinames')
+    >>> print_existing_wikiurls(browser.contents)
     https://wiki.ubuntu.com/MarkShuttleworth
 
 But he wants to register another one.
 
-    >>> browser.getControl(name='newwiki').value = 'http://foo.bar/wiki/'
-    >>> browser.getControl(name='newwikiname').value = 'FooBar'
+    >>> browser.getControl(name='field.wiki').value = 'http://foo.bar/wiki/'
+    >>> browser.getControl(name='field.wikiname').value = 'FooBar'
     >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
-    >>> print_existing_wikinames(browser.contents)
+    >>> print_feedback(browser)
+    >>> print browser.url
+    http://launchpad.dev/~mark
+    >>> browser.open('http://launchpad.dev/~mark/+editwikinames')
+    >>> print_existing_wikiurls(browser.contents)
     http://foo.bar/wiki/FooBar
     https://wiki.ubuntu.com/MarkShuttleworth
 
 He can't have two identical wiki names, though.
 
-    >>> browser.getControl(name='newwiki').value = 'http://foo.bar/wiki/'
-    >>> browser.getControl(name='newwikiname').value = 'FooBar'
+    >>> browser.getControl(name='field.wiki').value = 'http://foo.bar/wiki/'
+    >>> browser.getControl(name='field.wikiname').value = 'FooBar'
     >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
     >>> for message in find_tags_by_class(browser.contents, 'message'):
     ...     print message.renderContents()
+    There is 1 error.
     The WikiName http://foo.bar/wiki/FooBar already belongs to you.
 
 Nor can he have a WikiName that is already registered in Launchpad.
 
-    >>> browser.getControl(name='newwiki').value = 'https://wiki.ubuntu.com/'
-    >>> browser.getControl(name='newwikiname').value = 'GuilhermeSalgado'
+    >>> browser.getControl(name='field.wiki').value = (
+    ...     'https://wiki.ubuntu.com/')
+    >>> browser.getControl(name='field.wikiname').value = 'GuilhermeSalgado'
     >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
     >>> print "\n".join(get_feedback_messages(browser.contents))
+    There is 1 error.
     The WikiName https://wiki.ubuntu.com/GuilhermeSalgado is already
     registered by Guilherme Salgado. If you think this is a duplicated
     account, you can merge it into your account.
 
 A WikiName's URL can't be empty nor invalid.
 
-    >>> browser.getControl(name='newwiki').value = ''
-    >>> browser.getControl(name='newwikiname').value = 'FooBar'
-    >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    Neither Wiki nor WikiName can be empty.
-
-    >>> browser.getControl(name='newwiki').value = '/this-is-not-a-url/'
-    >>> browser.getControl(name='newwikiname').value = 'FooBar'
-    >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
-    >>> print "\n".join(get_feedback_messages(browser.contents))
-    "/this-is-not-a-url/" is not a valid URL.
+    >>> browser.getControl(name='field.wiki').value = ''
+    >>> browser.getControl(name='field.wikiname').value = 'FooBar'
+    >>> browser.getControl('Save Changes').click()
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
+    >>> print "\n".join(get_feedback_messages(browser.contents))
+    There is 1 error.
+    The Wiki URL must be specified.
+
+    >>> browser.getControl(name='field.wiki').value = '/this-is-not-a-url/'
+    >>> browser.getControl(name='field.wikiname').value = 'FooBar'
+    >>> browser.getControl('Save Changes').click()
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
+    >>> print "\n".join(get_feedback_messages(browser.contents))
+    There is 1 error.
+    "/this-is-not-a-url/" is not a valid URI
+
+And it can't be incredibly long.
+
+    >>> browser.getControl(name='field.wiki').value = (
+    ...     'https://wiki.ubuntu.com/')
+    >>> wikiname = "z" * 101
+    >>> browser.getControl(name='field.wikiname').value = wikiname
+    >>> browser.getControl('Save Changes').click()
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
+    >>> print "\n".join(get_feedback_messages(browser.contents))
+    There is 1 error.
+    The wiki name cannot exceed 100 characters.
 
 The invalid value is escaped using HTML entities when displayed back to
 the user.
 
-    >>> browser.getControl(name='newwiki').value = '<script>alert(1);</script>'
-    >>> browser.getControl(name='newwikiname').value = 'FooBar'
+    >>> browser.getControl(name='field.wiki').value = (
+    ...     '<script>alert(1);</script>')
+    >>> browser.getControl(name='field.wikiname').value = 'FooBar'
     >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
     >>> print "\n".join(get_feedback_messages(browser.contents))
-    "&lt;script&gt;alert(1);&lt;/script&gt;/" is not a valid URL.
+    There is 1 error.
+    "&lt;script&gt;alert(1);&lt;/script&gt;" is not a valid URI
 
 Only http and https URLs are allowed for wikis.
 
-    >>> browser.getControl(name='newwiki').value = "javascript:void"
-    >>> browser.getControl(name='newwikiname').value = 'FooBar'
+    >>> browser.getControl(name='field.wiki').value = "javascript:void"
+    >>> browser.getControl(name='field.wikiname').value = 'FooBar'
     >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
+    >>> print browser.url
+    http://launchpad.dev/%7Emark/+editwikinames
     >>> print "\n".join(get_feedback_messages(browser.contents))
-    The URL scheme "javascript" is not allowed. Only http or https URLs may be
-    used.
-
-Mark can remove any of his wiki names, and here he'll remove the one he
-just added.
-
+    There is 1 error.
+    The URI scheme "javascript" is not allowed. Only URIs with the following
+    schemes may be used: http, https
+
+Mark can remove any of his wiki names.
+
+    >>> browser.getControl(name='field.wiki').value = ""
+    >>> browser.getControl(name='field.wikiname').value = ''
     >>> browser.getControl('Remove', index=0).selected = True
     >>> browser.getControl('Save Changes').click()
-    >>> browser.url
-    'http://localhost/~mark/+editwikinames'
-    >>> print_existing_wikinames(browser.contents)
+    >>> print browser.url
+    http://launchpad.dev/~mark
+    >>> print "\n".join(get_feedback_messages(browser.contents))
+    >>> browser.open('http://launchpad.dev/~mark/+editwikinames')
+    >>> print_existing_wikiurls(browser.contents)
     https://wiki.ubuntu.com/MarkShuttleworth

=== modified file 'lib/lp/registry/templates/person-editwikinames.pt'
--- lib/lp/registry/templates/person-editwikinames.pt	2009-09-17 15:00:38 +0000
+++ lib/lp/registry/templates/person-editwikinames.pt	2010-08-31 13:20:59 +0000
@@ -6,14 +6,12 @@
   metal:use-macro="view/macro:page/main_only"
   i18n:domain="launchpad"
 >
-  <body>
-
-  <div metal:fill-slot="main">
-    <p tal:condition="view/error_message"
-       tal:content="structure view/error_message/escapedtext"
-       class="error message" />
-
-    <form name="edit_wikiname" action="" method="POST">
+<body>
+
+<div metal:fill-slot="main">
+<div metal:use-macro="context/@@launchpad_form/form">
+
+  <div metal:fill-slot="widgets">
     <table id="wikinames">
 
       <tal:XXX condition="nothing">
@@ -22,27 +20,19 @@
         # there's a bug preventing us from # updating our version of storm
         # to trunk.
       </tal:XXX>
-      <tal:block condition="context/wiki_names/any">
-
-        <tr><td colspan="2"><h2>Wiki names</h2></td></tr>
-
-        <tr>
-          <td><label>Wiki base URL:</label></td>
-          <td><label>Wiki name:</label></td>
-        </tr>
-
-        <tr tal:repeat="wiki context/wiki_names">
-          <td>
-            <input type="text" style="margin-bottom: 0.5em;"
-                   tal:attributes="name string:wiki_${wiki/id};
-                                   value wiki/wiki" />
-          </td>
-          <td>
-            <input type="text" size="15"
-                   tal:attributes="name string:wikiname_${wiki/id};
-                                   value wiki/wikiname" />
-          </td>
-
+
+      <tal:existing_wiki condition="context/wiki_names/any">
+
+        <tr>
+          <td><label>Existing wiki names</label></td>
+        </tr>
+
+        <tr>
+          <td><label>Wiki URL</label></td>
+        </tr>
+
+        <tr tal:repeat="wiki context/wiki_names" class="wikiurl">
+          <td tal:content="wiki/url"></td>
           <td>
             <label>
               <input type="checkbox" value="Remove"
@@ -51,41 +41,35 @@
             </label>
           </td>
         </tr>
-
-      </tal:block>
-
-    <tr>
-      <td colspan="2"><h2>New wiki name</h2></td>
-    </tr>
-
-    <tr>
-      <td><label>Wiki Base URL:</label></td>
-      <td><label>Wiki Name:</label></td>
-    </tr>
-
-    <tr>
-      <td>
-        <input name="newwiki" type="text"
-               tal:attributes="value view/newwiki|nothing" />
-      </td>
-      <td>
-        <input name="newwikiname" type="text" size="15"
-               tal:attributes="value view/newwikiname|nothing" />
-      </td>
-    </tr>
-
-    <tr>
-      <td class="formHelp">Example: https://wiki.ubuntu.com/</td>
-      <td class="formHelp">Example: YourName.</td>
-    </tr>
-  </table>
-
-  <div class="actions">
-    <input type="submit" value="Save Changes" name="SAVE" />
-    or&nbsp; <a tal:attributes="href view/cancel_url">Cancel</a>
-  </div>
-  </form>
-  </div>
+        <tr style="height:2em;"><td></td></tr>
+      </tal:existing_wiki>
+
+
+      <tr>
+        <td><label>New wiki name</label></td>
+      </tr>
+
+      <tal:widget define="widget nocall:view/widgets/wiki">
+        <metal:block use-macro="context/@@launchpad_form/widget_row" />
+      </tal:widget>
+
+      <tr>
+        <td class="formHelp">Example: https://wiki.ubuntu.com/</td>
+      </tr>
+
+      <tal:widget define="widget nocall:view/widgets/wikiname">
+        <metal:block use-macro="context/@@launchpad_form/widget_row" />
+      </tal:widget>
+
+      <tr>
+        <td class="formHelp">Example: YourName.</td>
+      </tr>
+
+    </table>
+  </div>
+
+</div>
+</div>
 
 </body>
 </html>