← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~twom/launchpad:affirm-code-of-conduct into launchpad:master

 

Tom Wardill has proposed merging ~twom/launchpad:affirm-code-of-conduct into launchpad:master.

Commit message:
Add affirmation of Code of Conduct

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~twom/launchpad/+git/launchpad/+merge/406461
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~twom/launchpad:affirm-code-of-conduct into launchpad:master.
diff --git a/lib/lp/registry/browser/codeofconduct.py b/lib/lp/registry/browser/codeofconduct.py
index 892c7fc..045b53c 100644
--- a/lib/lp/registry/browser/codeofconduct.py
+++ b/lib/lp/registry/browser/codeofconduct.py
@@ -6,6 +6,7 @@
 __metaclass__ = type
 
 __all__ = [
+    'AffirmCodeofConductView',
     'SignedCodeOfConductSetNavigation',
     'CodeOfConductSetNavigation',
     'CodeOfConductOverviewMenu',
@@ -160,6 +161,27 @@ class CodeOfConductSetView(LaunchpadView):
     page_title = 'Ubuntu Codes of Conduct'
 
 
+class AffirmCodeofConductView(LaunchpadFormView):
+    """Add a new `SignedCodeOfConduct` via affirmation."""
+    schema = ISignedCodeOfConduct
+    field_names = ['affirmed']
+
+    @property
+    def page_title(self):
+        return "Affirm %s" % self.context.title
+
+    @property
+    def code_of_conduct(self):
+        return self.context.content
+
+    @action('Continue', name='affirm')
+    def affirm_action(self, action, data):
+        if data.get('affirmed'):
+            signedcocset = getUtility(ISignedCodeOfConductSet)
+            signedcocset.affirmAndStore(self.user, self.context.content)
+        self.next_url = canonical_url(self.user) + '/+codesofconduct'
+
+
 class SignedCodeOfConductAddView(LaunchpadFormView):
     """Add a new SignedCodeOfConduct Entry."""
     schema = ISignedCodeOfConduct
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index a92db24..d976704 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -282,6 +282,13 @@
         attribute="__call__"
         />
     <browser:page
+        name="+affirm"
+        for="lp.registry.interfaces.codeofconduct.ICodeOfConduct"
+        class="lp.registry.browser.codeofconduct.AffirmCodeofConductView"
+        permission="launchpad.AnyPerson"
+        template="../templates/signedcodeofconduct-affirm.pt"
+        />
+    <browser:page
         name="+sign"
         for="lp.registry.interfaces.codeofconduct.ICodeOfConduct"
         class="lp.registry.browser.codeofconduct.SignedCodeOfConductAddView"
diff --git a/lib/lp/registry/browser/tests/test_codeofconduct.py b/lib/lp/registry/browser/tests/test_codeofconduct.py
index fb2cc0a..f6f2da4 100644
--- a/lib/lp/registry/browser/tests/test_codeofconduct.py
+++ b/lib/lp/registry/browser/tests/test_codeofconduct.py
@@ -173,3 +173,27 @@ class TestCodeOfConductBrowser(BrowserTestCase):
         self.assertThat(
             browser.headers['Content-type'],
             MatchesRegex(r'^text/plain;charset="?utf-8"?$'))
+
+
+class TestCodeOfConductAffirmView(BrowserTestCase):
+    """Test the affirmation view for the CoC"""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_affirm(self):
+        """Check we can affirm a CoC"""
+        user = self.factory.makePerson()
+        name = user.name
+        displayname = user.displayname
+        coc = getUtility(ICodeOfConductSet)['2.0']
+        content = coc.content
+        browser = self.getViewBrowser(coc, "+affirm", user=user)
+        self.assertIn(content, browser.contents)
+        browser.getControl('Affirmed').click()
+        browser.getControl('Continue').click()
+        self.assertEqual(
+            "http://launchpad.test/~{}/+codesofconduct".format(name),
+            browser.url)
+        self.assertIn(
+            "affirmed by {}".format(displayname),
+            browser.contents)
diff --git a/lib/lp/registry/interfaces/codeofconduct.py b/lib/lp/registry/interfaces/codeofconduct.py
index bd8df54..5b335fd 100644
--- a/lib/lp/registry/interfaces/codeofconduct.py
+++ b/lib/lp/registry/interfaces/codeofconduct.py
@@ -87,6 +87,9 @@ class ISignedCodeOfConduct(Interface):
 
     displayname = Attribute("Fancy Title for CoC.")
 
+    affirmed = Bool(title=_("Affirmed"),
+                    description=_("Whether this CoC has been affirmed."))
+
     def sendAdvertisementEmail(subject, content):
         """Send Advertisement email to signature owner preferred address
         containing arbitrary content and subject.
@@ -127,6 +130,9 @@ class ISignedCodeOfConductSet(Interface):
     def verifyAndStore(user, signedcode):
         """Verify and Store a Signed CoC."""
 
+    def affirmAndStore(user):
+        """Affirm and Store a Signed CoC."""
+
     def searchByDisplayname(displayname, searchfor=None):
         """Search SignedCoC by Owner.displayname"""
 
diff --git a/lib/lp/registry/model/codeofconduct.py b/lib/lp/registry/model/codeofconduct.py
index 421440e..414c453 100644
--- a/lib/lp/registry/model/codeofconduct.py
+++ b/lib/lp/registry/model/codeofconduct.py
@@ -200,14 +200,17 @@ class SignedCodeOfConduct(StormBase):
 
     active = Bool(name='active', allow_none=False, default=False)
 
+    affirmed = Bool(name='affirmed', allow_none=False, default=False)
+
     def __init__(self, owner, signedcode=None, signing_key_fingerprint=None,
-            recipient=None, active=False):
+            recipient=None, active=False, affirmed=False):
         super(SignedCodeOfConduct, self).__init__()
         self.owner = owner
         self.signedcode = signedcode
         self.signing_key_fingerprint = signing_key_fingerprint
         self.recipient = recipient
         self.active = active
+        self.affirmed = affirmed
 
     @cachedproperty
     def signingkey(self):
@@ -224,6 +227,9 @@ class SignedCodeOfConduct(StormBase):
             displayname += (': digitally signed by %s (%s)'
                             % (self.owner.displayname,
                                self.signingkey.displayname))
+        elif self.affirmed:
+            displayname += (': affirmed by %s'
+                            % self.owner.displayname)
         else:
             displayname += (': paper submission accepted by %s'
                             % self.recipient.displayname)
@@ -337,6 +343,33 @@ class SignedCodeOfConductSet:
         content = ('Digitally Signed by %s\n' % sig.fingerprint)
         signed.sendAdvertisementEmail(subject, content)
 
+    def affirmAndStore(self, user, codetext):
+        """See `ISignedCodeOfConductSet`."""
+        try:
+            encoded_codetext = codetext.decode('utf-8')
+        except UnicodeDecodeError:
+            raise TypeError('Signed Code Could not be decoded as UTF-8')
+
+        # recover the current CoC release
+        coc = CodeOfConduct(getUtility(ICodeOfConductConf).currentrelease)
+        current = coc.content
+
+        if encoded_codetext.split() != current.decode('UTF-8').split():
+            return ('The affirmed text does not match the current '
+                    'Code of Conduct.')
+
+        # The text of the CoC isn't GPG signed at this point,
+        # but save which version was affirmed
+        affirmation_text = u"Code of Conduct version {}".format(
+            coc.version)
+        affirmed = SignedCodeOfConduct(
+            owner=user, signedcode=affirmation_text, affirmed=True,
+            active=True)
+        # Send Advertisement Email
+        subject = 'You have affirmed the Code of Conduct.'
+        content = ('Version affirmed %s\n' % coc.version)
+        affirmed.sendAdvertisementEmail(subject, content)
+
     def searchByDisplayname(self, displayname, searchfor=None):
         """See ISignedCodeOfConductSet."""
         # Circular import.
diff --git a/lib/lp/registry/templates/codeofconduct-list.pt b/lib/lp/registry/templates/codeofconduct-list.pt
index ff4a05d..bf1e6ba 100644
--- a/lib/lp/registry/templates/codeofconduct-list.pt
+++ b/lib/lp/registry/templates/codeofconduct-list.pt
@@ -24,10 +24,10 @@
 
       <div tal:condition="not: is_ubuntu_coc_signer">
 
-        <h2>Sign the Ubuntu Code of Conduct</h2>
+        <h2>Sign the Ubuntu Code of Conduct using GPG</h2>
 
         Ubuntu community members may commit to observing the Ubuntu Code of
-        Conduct by signing it online:
+        Conduct by signing it using GPG online:
 
         <ol style="margin-left: 4em">
             <li>
@@ -63,6 +63,16 @@
                 string:${context/current_code_of_conduct/fmt:url}/+sign">
                 Sign it!</a></li>
         </ol>
+
+        <h2>Affirm the Ubuntu Code of Conduct on Launchpad</h2>
+
+          Ubuntu community members can affirm the Code of Conduct
+          on Launchpad via a simple web form.
+
+          <p>
+            <a tal:attributes="href
+                string:${context/current_code_of_conduct/fmt:url}/+affirm">Affirm the Code of Conduct</a>
+          </p>
       </div>
 
       <p tal:condition="is_ubuntu_coc_signer">
diff --git a/lib/lp/registry/templates/signedcodeofconduct-affirm.pt b/lib/lp/registry/templates/signedcodeofconduct-affirm.pt
new file mode 100644
index 0000000..8696c37
--- /dev/null
+++ b/lib/lp/registry/templates/signedcodeofconduct-affirm.pt
@@ -0,0 +1,23 @@
+<html
+  xmlns="http://www.w3.org/1999/xhtml";
+  xmlns:tal="http://xml.zope.org/namespaces/tal";
+  xmlns:metal="http://xml.zope.org/namespaces/metal";
+  xmlns:i18n="http://xml.zope.org/namespaces/i18n";
+  metal:use-macro="view/macro:page/main_only"
+  i18n:domain="launchpad"
+>
+  <body>
+    <div
+      metal:fill-slot="main"
+      tal:define="user view/user;
+                 is_ubuntu_coc_signer user/is_ubuntu_coc_signer|nothing;"
+    >
+      <pre tal:content="structure view/code_of_conduct"></pre>
+
+      <div metal:use-macro="context/@@launchpad_form/form" />
+    </div>
+
+    
+
+  </body>
+</html>
diff --git a/lib/lp/registry/tests/test_codeofconduct.py b/lib/lp/registry/tests/test_codeofconduct.py
index baf64a2..d50f410 100644
--- a/lib/lp/registry/tests/test_codeofconduct.py
+++ b/lib/lp/registry/tests/test_codeofconduct.py
@@ -191,3 +191,44 @@ class TestSignedCodeOfConductSet(TestCaseWithFactory):
                     'fingerprint': gpgkey.fingerprint,
                     },
             notification.get_payload(decode=True).decode("UTF-8"))
+
+    def test_affirmAndStore_good(self):
+        user = self.factory.makePerson()
+        current = getUtility(ICodeOfConductSet).current_code_of_conduct
+        self.assertIsNone(
+            getUtility(ISignedCodeOfConductSet).affirmAndStore(
+                user, current.content))
+        [notification] = self.assertEmailQueueLength(1)
+        self.assertThat(dict(notification), ContainsDict({
+            "From": Equals(format_address(
+                "Launchpad Code Of Conduct System",
+                config.canonical.noreply_from_address)),
+            "To": Equals(user.preferredemail.email),
+            "Subject": Equals(
+                "You have affirmed the Code of Conduct."),
+            }))
+        self.assertEqual(
+            dedent("""\
+
+                Hello
+
+                Your Code of Conduct Signature was modified.
+
+                User: '%(user)s'
+                Version affirmed %(version)s
+
+
+                Thanks,
+
+                The Launchpad Team
+                """) % {
+                    'user': user.display_name,
+                    'version': current.version,
+                    },
+            notification.get_payload(decode=True).decode("UTF-8"))
+
+    def test_affirmAndStore_incorrect_text(self):
+        user = self.factory.makePerson()
+        self.assertEqual(
+            u"The affirmed text does not match the current Code of Conduct.",
+            getUtility(ISignedCodeOfConductSet).affirmAndStore(user, "foo"))

Follow ups