launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27324
[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