Merge ~ines-almeida/launchpad:social-accounts-display-accounts into launchpad:master

Proposed by Ines Almeida
Status: Work in progress
Proposed branch: ~ines-almeida/launchpad:social-accounts-display-accounts
Merge into: launchpad:master
Diff against target: 1102 lines (+704/-9) (has conflicts)
22 files modified
lib/canonical/launchpad/icing/css/components/_index.scss (+1/-0)
lib/canonical/launchpad/icing/css/components/social_accounts.scss (+17/-0)
lib/canonical/launchpad/icing/css/typography.scss (+7/-0)
lib/canonical/launchpad/images/src/social-irc.svg (+5/-0)
lib/canonical/launchpad/images/src/social-jabber.svg (+2/-0)
lib/canonical/launchpad/images/src/social-matrix.svg (+9/-0)
lib/lp/app/browser/configure.zcml (+7/-0)
lib/lp/app/browser/tales.py (+39/-0)
lib/lp/registry/browser/configure.zcml (+13/-0)
lib/lp/registry/browser/person.py (+109/-2)
lib/lp/registry/configure.zcml (+27/-0)
lib/lp/registry/doc/socialaccount.rst (+32/-0)
lib/lp/registry/interfaces/person.py (+10/-0)
lib/lp/registry/interfaces/socialaccount.py (+103/-0)
lib/lp/registry/interfaces/webservice.py (+2/-0)
lib/lp/registry/model/person.py (+64/-0)
lib/lp/registry/stories/webservice/xx-person.rst (+14/-0)
lib/lp/registry/templates/person-editircnicknames.pt (+2/-6)
lib/lp/registry/templates/person-editmatrixaccounts.pt (+73/-0)
lib/lp/registry/templates/person-portlet-contact-details.pt (+50/-1)
lib/lp/registry/tests/test_person.py (+112/-0)
lib/lp/services/webservice/wadl-to-refhtml.xsl (+6/-0)
Conflict in lib/canonical/launchpad/icing/css/typography.scss
Conflict in lib/lp/registry/templates/person-portlet-contact-details.pt
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+458636@code.launchpad.net

Commit message

Update social accounts display view

To post a comment you must log in.

Unmerged commits

39f0afb... by Ines Almeida

Add CSS to new social accounts display

Succeeded
[SUCCEEDED] docs:0 (build)
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] mypy:0 (build)
13 of 3 results
5462f46... by Ines Almeida

Add matrix and update other social accounts display in profile

c51cae2... by Ines Almeida

Add new social accounts icons

7df013c... by Ines Almeida

Send user to profile page after matrix account form 'Save'

Succeeded
[SUCCEEDED] docs:0 (build)
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] mypy:0 (build)
13 of 3 results
8e251f4... by Ines Almeida

ui: Add edit matrix accounts view

d78ac63... by Guruprasad

charm/launchpad-codehosting: Reconfigure haproxy relations on config changes

When a configuration variable used by an haproxy relation changes,
reconfigure that relation to apply the change.

efb7543... by Ines Almeida

Improve IRC edit view

 - Return user to profile after save
 - Add placeholder text to inputs
 - Remove extra titles

23c217d... by Ines Almeida

Add small margin-bottom to '.yui-u dl' elements

5bed095... by Ines Almeida

Add 'title' attribute to social accounts edit buttons

622ea06... by Ines Almeida

Move Jabber and IRC profile items into a 'Social Accounts' section

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/canonical/launchpad/icing/css/components/_index.scss b/lib/canonical/launchpad/icing/css/components/_index.scss
2index 3facac8..59e25ed 100644
3--- a/lib/canonical/launchpad/icing/css/components/_index.scss
4+++ b/lib/canonical/launchpad/icing/css/components/_index.scss
5@@ -6,4 +6,5 @@
6 'bug_listing',
7 'portlets',
8 'sharing',
9+ 'social_accounts',
10 'yui_picker';
11diff --git a/lib/canonical/launchpad/icing/css/components/social_accounts.scss b/lib/canonical/launchpad/icing/css/components/social_accounts.scss
12new file mode 100644
13index 0000000..a439581
14--- /dev/null
15+++ b/lib/canonical/launchpad/icing/css/components/social_accounts.scss
16@@ -0,0 +1,17 @@
17+.social_accounts {
18+ &__icon {
19+ opacity: 70%;
20+ margin-right: 0.5em;
21+ height: 1.2em;
22+ width: 1.2em;
23+ margin-bottom: -0.2em;
24+ }
25+
26+ &__item {
27+ margin-bottom: 0.2em;
28+
29+ & a.action-icon {
30+ padding-bottom: 0;
31+ }
32+ }
33+}
34diff --git a/lib/canonical/launchpad/icing/css/typography.scss b/lib/canonical/launchpad/icing/css/typography.scss
35index c6717f7..1b52e56 100644
36--- a/lib/canonical/launchpad/icing/css/typography.scss
37+++ b/lib/canonical/launchpad/icing/css/typography.scss
38@@ -41,6 +41,13 @@ h1, h2, h3, h4, h5, h6 {
39
40 .yui-u {
41 padding-bottom: 1em;
42+<<<<<<< lib/canonical/launchpad/icing/css/typography.scss
43+=======
44+
45+ & dl {
46+ margin-bottom: 1em;
47+ }
48+>>>>>>> lib/canonical/launchpad/icing/css/typography.scss
49 }
50
51 p {
52diff --git a/lib/canonical/launchpad/images/social-irc.png b/lib/canonical/launchpad/images/social-irc.png
53new file mode 100644
54index 0000000..ed760ea
55Binary files /dev/null and b/lib/canonical/launchpad/images/social-irc.png differ
56diff --git a/lib/canonical/launchpad/images/social-jabber.png b/lib/canonical/launchpad/images/social-jabber.png
57new file mode 100644
58index 0000000..95cf8a0
59Binary files /dev/null and b/lib/canonical/launchpad/images/social-jabber.png differ
60diff --git a/lib/canonical/launchpad/images/social-matrix.png b/lib/canonical/launchpad/images/social-matrix.png
61new file mode 100644
62index 0000000..031885f
63Binary files /dev/null and b/lib/canonical/launchpad/images/social-matrix.png differ
64diff --git a/lib/canonical/launchpad/images/src/social-irc.svg b/lib/canonical/launchpad/images/src/social-irc.svg
65new file mode 100644
66index 0000000..7a47f88
67--- /dev/null
68+++ b/lib/canonical/launchpad/images/src/social-irc.svg
69@@ -0,0 +1,5 @@
70+<?xml version="1.0" encoding="utf-8"?>
71+<svg viewBox="0.998 0 113.071 112.509" xmlns="http://www.w3.org/2000/svg">
72+ <path style="fill: rgba(102, 102, 102, 0); stroke: rgb(0, 0, 0); stroke-width: 6px;" d="M 95.357 103.13 C 95.357 103.13 94.172 106.772 89.194 104.345 C 86.054 102.815 68.659 94.195 56.229 88.031 L 32.944 88.031 C 17.966 88.031 5.823 75.889 5.823 60.91 L 5.823 35.783 C 5.823 20.805 17.966 8.662 32.944 8.662 L 80.759 8.662 C 95.737 8.662 107.88 20.805 107.88 35.783 L 107.88 60.91 C 107.88 71.928 101.309 81.413 91.872 85.657 L 95.357 103.13 Z"/>
73+ <path d="M 66.973 58.811 L 64.06 71.347 L 55.386 71.347 L 58.297 58.811 L 51.495 58.811 L 48.661 71.347 L 40.141 71.347 L 42.901 58.811 L 34.069 58.811 L 34.069 51.951 L 44.463 51.951 L 46.111 45.261 L 36.75 45.261 L 36.75 38.338 L 47.601 38.338 L 50.514 25.74 L 59.186 25.74 L 56.276 38.338 L 63.231 38.338 L 66.143 25.74 L 74.665 25.74 L 71.754 38.338 L 80.658 38.338 L 80.658 45.261 L 70.188 45.261 L 68.541 51.951 L 78.13 51.951 L 78.13 58.811 L 66.973 58.811 Z M 59.94 51.951 L 61.588 45.261 L 54.711 45.261 L 53.062 51.951 L 59.94 51.951 Z" style="stroke: rgba(186, 218, 85, 0); opacity: 0.9;"/>
74+</svg>
75\ No newline at end of file
76diff --git a/lib/canonical/launchpad/images/src/social-jabber.svg b/lib/canonical/launchpad/images/src/social-jabber.svg
77new file mode 100644
78index 0000000..816d392
79--- /dev/null
80+++ b/lib/canonical/launchpad/images/src/social-jabber.svg
81@@ -0,0 +1,2 @@
82+<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
83+<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" role="img" xmlns="http://www.w3.org/2000/svg"><path d="M9.597 11.737c0-.35-.065-.732-.268-1.025-.408-.588-1.283-.775-1.892-.405-.308.188-.48.515-.576.851-.191.668-.104 1.43.03 2.1.043.214.088.428.148.639.021.076.031.186.08.25.087.11.297.141.426.12.387-.065.291-.703.278-.974-.03-.634-.218-1.25-.036-1.881.076-.268.225-.568.494-.684.244-.105.49.023.586.261.156.385.117.83.215 1.23.033.137.07.272.131.399.018.037.043.113.094.108.126-.011.304-.22.398-.298.304-.25.616-.52.965-.705.165-.088.435-.23.603-.08a.612.612 0 0 1 .108.13c.198.31.002.55-.127.845-.166.38-.336.758-.577 1.098-.207.293-.49.549-.655.869-.107.205-.167.43-.123.663.036.188.181.301.373.257.143-.033.24-.156.322-.269.146-.202.281-.412.426-.615.28-.393.61-.76.846-1.183a3.41 3.41 0 0 0 .42-1.664c0-.474-.171-1.198-.723-1.298a.974.974 0 0 0-.326.01 1.432 1.432 0 0 0-.374.12 2.715 2.715 0 0 0-.818.637c-.146.16-.276.363-.449.495M9.078.016c-.435.058-.878.052-1.315.12-.838.129-1.64.389-2.425.703-.286.114-.568.241-.845.376-.103.05-.26.09-.343.17-.043.041-.039.139-.044.195-.014.156-.034.313-.05.47-.058.605-.1 1.229-.013 1.834.028.195.09.55.33.587.369.058.656-.397.837-.648.424-.586.905-1.132 1.6-1.394.817-.308 1.753-.381 2.618-.44 2.426-.167 5.078.277 6.865 2.064.254.254.495.524.7.82.8 1.159 1.223 2.477 1.427 3.86.096.65.161 1.308.013 1.955-.257 1.122-.932 2.1-1.706 2.931-.53.57-1.128 1.084-1.749 1.552-.347.261-.736.483-1.062.768-.375.329-.688.74-.925 1.179-.639 1.181-.81 2.602-.622 3.92.038.27.073.542.134.809.018.08.022.217.073.282.097.122.36.189.508.196.154.007.256-.11.294-.249.064-.236.026-.498-.012-.736-.076-.487-.147-.977-.125-1.471a3.71 3.71 0 0 1 1.026-2.425c.643-.673 1.512-1.061 2.243-1.625 1.474-1.136 2.794-2.668 3.301-4.492a5.194 5.194 0 0 0 .159-2.015c-.105-.849-.415-1.697-.708-2.497-.892-2.437-2.422-4.755-4.851-5.87-.964-.443-1.973-.645-3.016-.79-.49-.068-.98-.11-1.472-.132-.274-.012-.572-.042-.845-.006M5.277 15.796c-.473.068-.61.447-.523.876.112.548.543.965.97 1.295a6.03 6.03 0 0 0 3.884 1.238c.538-.023 1.124-.112 1.617-.34.265-.122.542-.563.181-.751a.59.59 0 0 0-.169-.051c-.157-.026-.333.041-.482.084-.263.075-.526.153-.797.196-.808.13-1.683-.055-2.352-.534-.542-.387-.98-.898-1.393-1.415-.253-.316-.482-.663-.936-.598m-.615 2.678c-.12.016-.259.011-.362.087-.215.158.022.476.135.62.328.417.76.763 1.192 1.068a7.832 7.832 0 0 0 4.03 1.442c.421.03.85 0 1.267-.07.152-.026.342-.037.482-.103.399-.186.284-.939-.072-1.106-.155-.073-.404.023-.567.046-.385.054-.771.06-1.158.05-1.015-.025-2.096-.338-2.98-.831a5.589 5.589 0 0 1-.966-.693c-.181-.16-.368-.42-.603-.502-.11-.037-.284-.023-.398-.008m.241 2.256a.638.638 0 0 0-.413.236c-.078.088-.152.167-.197.278-.246.609.41 1.183.864 1.47.504.32 1.055.558 1.616.758 1.266.45 2.752.739 4.066.336.391-.12.778-.338 1.062-.634.16-.167.27-.419-.024-.526-.174-.063-.385.098-.543.162a4.57 4.57 0 0 1-1.158.312c-.527.064-1.001-.052-1.508-.179a11.982 11.982 0 0 1-1.291-.373 4.457 4.457 0 0 1-1.026-.513c-.094-.066-.206-.125-.282-.211-.25-.282-.439-.612-.707-.88-.116-.116-.281-.256-.459-.236"/></svg>
84\ No newline at end of file
85diff --git a/lib/canonical/launchpad/images/src/social-matrix.svg b/lib/canonical/launchpad/images/src/social-matrix.svg
86new file mode 100644
87index 0000000..4e5bfb4
88--- /dev/null
89+++ b/lib/canonical/launchpad/images/src/social-matrix.svg
90@@ -0,0 +1,9 @@
91+<?xml version="1.0" encoding="UTF-8"?>
92+<svg version="1.1" viewBox="0 0 27.9 32" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
93+ <title>Matrix (protocol) logo</title>
94+ <g transform="translate(-.095 .005)" fill="#040404">
95+ <path d="m27.1 31.2v-30.5h-2.19v-0.732h3.04v32h-3.04v-0.732z"/>
96+ <path d="m8.23 10.4v1.54h0.044c0.385-0.564 0.893-1.03 1.49-1.37 0.58-0.323 1.25-0.485 1.99-0.485 0.72 0 1.38 0.14 1.97 0.42 0.595 0.279 1.05 0.771 1.36 1.48 0.338-0.5 0.796-0.941 1.38-1.32 0.58-0.383 1.27-0.574 2.06-0.574 0.602 0 1.16 0.074 1.67 0.22 0.514 0.148 0.954 0.383 1.32 0.707 0.366 0.323 0.653 0.746 0.859 1.27 0.205 0.522 0.308 1.15 0.308 1.89v7.63h-3.13v-6.46c0-0.383-0.015-0.743-0.044-1.08-0.0209-0.307-0.103-0.607-0.242-0.882-0.133-0.251-0.336-0.458-0.584-0.596-0.257-0.146-0.606-0.22-1.05-0.22-0.44 0-0.796 0.085-1.07 0.253-0.272 0.17-0.485 0.39-0.639 0.662-0.159 0.287-0.264 0.602-0.308 0.927-0.052 0.347-0.078 0.697-0.078 1.05v6.35h-3.13v-6.4c0-0.338-7e-3 -0.673-0.021-1-0.0114-0.314-0.0749-0.623-0.188-0.916-0.108-0.277-0.3-0.512-0.55-0.673-0.258-0.168-0.636-0.253-1.14-0.253-0.198 0.0083-0.394 0.042-0.584 0.1-0.258 0.0745-0.498 0.202-0.705 0.374-0.228 0.184-0.422 0.449-0.584 0.794-0.161 0.346-0.242 0.798-0.242 1.36v6.62h-3.13v-11.4z"/>
97+ <path d="m0.936 0.732v30.5h2.19v0.732h-3.04v-32h3.03v0.732z"/>
98+ </g>
99+</svg>
100diff --git a/lib/lp/app/browser/configure.zcml b/lib/lp/app/browser/configure.zcml
101index 9391c39..70e44d9 100644
102--- a/lib/lp/app/browser/configure.zcml
103+++ b/lib/lp/app/browser/configure.zcml
104@@ -669,6 +669,13 @@
105 />
106
107 <adapter
108+ for="lp.registry.interfaces.socialaccount.ISocialAccount"
109+ provides="zope.traversing.interfaces.IPathAdapter"
110+ factory="lp.app.browser.tales.SocialAccountFormatterAPI"
111+ name="fmt"
112+ />
113+
114+ <adapter
115 for="datetime.timedelta"
116 provides="zope.traversing.interfaces.IPathAdapter"
117 factory="lp.app.browser.tales.DurationFormatterAPI"
118diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py
119index eb5cad2..22490b6 100644
120--- a/lib/lp/app/browser/tales.py
121+++ b/lib/lp/app/browser/tales.py
122@@ -55,6 +55,7 @@ from lp.registry.interfaces.distributionsourcepackage import (
123 from lp.registry.interfaces.person import IPerson
124 from lp.registry.interfaces.product import IProduct
125 from lp.registry.interfaces.projectgroup import IProjectGroup
126+from lp.registry.interfaces.socialaccount import SocialPlatform
127 from lp.services.compat import tzname
128 from lp.services.utils import round_half_up
129 from lp.services.webapp.authorization import check_permission
130@@ -3049,3 +3050,41 @@ class IRCNicknameFormatterAPI(ObjectFormatterAPI):
131 self._context.nickname,
132 self._context.network,
133 ).escapedtext
134+
135+
136+@implementer(ITraversable)
137+class SocialAccountFormatterAPI(ObjectFormatterAPI):
138+ """Adapter from social account objects to a formatted string."""
139+
140+ traversable_names = {
141+ "formatted_displayname": "formatted_displayname",
142+ }
143+
144+ platform_icons = {
145+ SocialPlatform.MATRIX: "social-matrix",
146+ }
147+
148+ def icon(self):
149+ icon = self.platform_icons[self._context.platform]
150+ platform = self._context.platform.title
151+ return (
152+ f'<img class="social_accounts__icon" height="14" width="14" '
153+ f'alt="{platform}" title="{platform}" src="/@@/{icon}" />'
154+ )
155+
156+ def formatted_displayname(self, view_name=None):
157+ if self._context.platform == SocialPlatform.MATRIX:
158+ icon = self.icon()
159+ return structured(
160+ dedent(
161+ """\
162+ {icon}
163+ <strong>{nickname}</strong>
164+ <span class="lesser"> on </span>
165+ <strong>{network}</strong>
166+ """.format(
167+ icon=icon, **self._context.identity
168+ )
169+ ),
170+ ).escapedtext
171+ return None
172diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
173index 6fd29fb..eaf8e17 100644
174--- a/lib/lp/registry/browser/configure.zcml
175+++ b/lib/lp/registry/browser/configure.zcml
176@@ -665,6 +665,12 @@
177 rootsite="api"
178 />
179 <lp:url
180+ for="lp.registry.interfaces.socialaccount.ISocialAccount"
181+ path_expression="string:+socialaccount/${id}"
182+ attribute_to_parent="person"
183+ rootsite="api"
184+ />
185+ <lp:url
186 for="lp.registry.interfaces.pillar.IPillarNameSet"
187 path_expression="string:pillars"
188 parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"
189@@ -1213,6 +1219,13 @@
190 template="../templates/person-editircnicknames.pt"
191 />
192 <browser:page
193+ name="+editsocialaccounts-matrix"
194+ for="lp.registry.interfaces.person.IPerson"
195+ class="lp.registry.browser.person.PersonEditMatrixAccountsView"
196+ permission="launchpad.Edit"
197+ template="../templates/person-editmatrixaccounts.pt"
198+ />
199+ <browser:page
200 name="+editjabberids"
201 for="lp.registry.interfaces.person.IPerson"
202 class="lp.registry.browser.person.PersonEditJabberIDsView"
203diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
204index 6dab8c8..f923680 100644
205--- a/lib/lp/registry/browser/person.py
206+++ b/lib/lp/registry/browser/person.py
207@@ -19,6 +19,7 @@ __all__ = [
208 "PersonEditEmailsView",
209 "PersonEditIRCNicknamesView",
210 "PersonEditJabberIDsView",
211+ "PersonEditMatrixAccountsView",
212 "PersonEditTimeZoneView",
213 "PersonEditSSHKeysView",
214 "PersonEditView",
215@@ -152,6 +153,10 @@ from lp.registry.interfaces.persontransferjob import (
216 from lp.registry.interfaces.pillar import IPillarNameSet
217 from lp.registry.interfaces.poll import IPollSubset
218 from lp.registry.interfaces.product import InvalidProductName, IProduct
219+from lp.registry.interfaces.socialaccount import (
220+ ISocialAccountSet,
221+ SocialPlatform,
222+)
223 from lp.registry.interfaces.ssh import ISSHKeySet, SSHKeyAdditionError
224 from lp.registry.interfaces.teammembership import (
225 ITeamMembershipSet,
226@@ -555,6 +560,14 @@ class PersonNavigation(BranchTraversalMixin, Navigation):
227 return None
228 return irc_nick
229
230+ @stepthrough("+socialaccount")
231+ def traverse_socialaccount(self, id):
232+ """Traverse to this person's SocialAccount on the webservice layer."""
233+ social_account = getUtility(ISocialAccountSet).get(id)
234+ if social_account is None or social_account.person != self.context:
235+ return None
236+ return social_account
237+
238 @stepthrough("+oci-registry-credential")
239 def traverse_oci_registry_credential(self, id):
240 """Traverse to this person's OCI registry credentials."""
241@@ -815,6 +828,7 @@ class PersonOverviewMenu(
242 "editmailinglists",
243 "editircnicknames",
244 "editjabberids",
245+ "editmatrixaccounts",
246 "editsshkeys",
247 "editpgpkeys",
248 "editlocation",
249@@ -877,13 +891,19 @@ class PersonOverviewMenu(
250 def editircnicknames(self):
251 target = "+editircnicknames"
252 text = "Update IRC nicknames"
253- return Link(target, text, icon="edit")
254+ return Link(target, text, icon="edit", summary=text)
255+
256+ @enabled_with_permission("launchpad.Edit")
257+ def editmatrixaccounts(self):
258+ target = "+editsocialaccounts-matrix"
259+ text = "Update Matrix accounts"
260+ return Link(target, text, icon="edit", summary=text)
261
262 @enabled_with_permission("launchpad.Edit")
263 def editjabberids(self):
264 target = "+editjabberids"
265 text = "Update Jabber IDs"
266- return Link(target, text, icon="edit")
267+ return Link(target, text, icon="edit", summary=text)
268
269 @enabled_with_permission("launchpad.Edit")
270 def editlocation(self):
271@@ -1660,6 +1680,17 @@ class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
272 )
273
274 @property
275+ def should_show_matrix_accounts_section(self):
276+ """Should the matrix accounts section be shown?
277+
278+ It's shown when the person has social accounts for the Matrix platform
279+ registered or has rights to register new ones.
280+ """
281+ return bool(self.matrix_accounts) or (
282+ check_permission("launchpad.Edit", self.context)
283+ )
284+
285+ @property
286 def should_show_sshkeys_section(self):
287 """Should the 'SSH keys' section be shown?
288
289@@ -1687,6 +1718,14 @@ class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
290 return self.context.gpg_keys
291
292 @cachedproperty
293+ def matrix_accounts(self):
294+ accounts = []
295+ for account in self.context.social_accounts:
296+ if account.platform == SocialPlatform.MATRIX:
297+ accounts.append(account)
298+ return accounts
299+
300+ @cachedproperty
301 def is_probationary_or_invalid_user(self):
302 """True when the user is not active or does not have karma.
303
304@@ -2368,6 +2407,10 @@ class PersonEditIRCNicknamesView(LaunchpadFormView):
305 def cancel_url(self):
306 return canonical_url(self.context)
307
308+ @property
309+ def next_url(self):
310+ return canonical_url(self.context)
311+
312 @action(_("Save Changes"), name="save")
313 def save(self, action, data):
314 """Process the IRC nicknames form."""
315@@ -2406,6 +2449,70 @@ class PersonEditIRCNicknamesView(LaunchpadFormView):
316 )
317
318
319+class PersonEditMatrixAccountsView(LaunchpadFormView):
320+ schema = Interface
321+
322+ @property
323+ def page_title(self):
324+ return smartquote(f"{self.context.displayname}'s Matrix accounts")
325+
326+ label = page_title
327+
328+ @property
329+ def cancel_url(self):
330+ return canonical_url(self.context)
331+
332+ @property
333+ def next_url(self):
334+ return canonical_url(self.context)
335+
336+ @property
337+ def matrix_accounts(self):
338+ accounts = []
339+ for account in self.context.social_accounts:
340+ if account.platform == SocialPlatform.MATRIX:
341+ accounts.append(account)
342+ return accounts
343+
344+ @action(_("Save Changes"), name="save")
345+ def save(self, action, data):
346+ """Process the matrix accounts form."""
347+ form = self.request.form
348+ for social_account in self.matrix_accounts:
349+ if form.get(f"remove_{social_account.id}"):
350+ social_account.destroySelf()
351+
352+ else:
353+ updated_identity = {
354+ field: form.get(f"{field}_{social_account.id}")
355+ for field in SocialPlatform.matrix_identity_fields
356+ }
357+ if not all(updated_identity.values()):
358+ self.request.response.addErrorNotification(
359+ "Fields cannot be empty."
360+ )
361+ return
362+
363+ social_account.identity = updated_identity
364+
365+ new_account_identity = {
366+ field_name: form.get(f"new_{field_name}")
367+ for field_name in SocialPlatform.matrix_identity_fields
368+ }
369+
370+ if all(new_account_identity.values()):
371+ getUtility(ISocialAccountSet).new(
372+ self.context, SocialPlatform.MATRIX, new_account_identity
373+ )
374+
375+ elif any(new_account_identity.values()):
376+ for field_key, field_value in new_account_identity.items():
377+ self.__setattr__(f"new_{field_key}", field_value)
378+ self.request.response.addErrorNotification(
379+ "All fields must be filled."
380+ )
381+
382+
383 class PersonEditJabberIDsView(LaunchpadFormView):
384 schema = IJabberID
385 field_names = ["jabberid"]
386diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml
387index 3cf36a8..041e557 100644
388--- a/lib/lp/registry/configure.zcml
389+++ b/lib/lp/registry/configure.zcml
390@@ -650,6 +650,33 @@
391 interface="lp.registry.interfaces.jabber.IJabberIDSet"/>
392 </lp:securedutility>
393 <class
394+ class="lp.registry.model.person.SocialAccount">
395+ <allow
396+ interface="lp.registry.interfaces.role.IHasOwner"/>
397+ <allow
398+ attributes="
399+ id
400+ person
401+ platform
402+ identity"/>
403+ <require
404+ permission="launchpad.Edit"
405+ set_schema="lp.registry.interfaces.socialaccount.ISocialAccount"
406+ attributes="
407+ destroySelf"/>
408+ </class>
409+ <class
410+ class="lp.registry.model.person.SocialAccountSet">
411+ <allow
412+ interface="lp.registry.interfaces.socialaccount.ISocialAccountSet"/>
413+ </class>
414+ <lp:securedutility
415+ class="lp.registry.model.person.SocialAccountSet"
416+ provides="lp.registry.interfaces.socialaccount.ISocialAccountSet">
417+ <allow
418+ interface="lp.registry.interfaces.socialaccount.ISocialAccountSet"/>
419+ </lp:securedutility>
420+ <class
421 class="lp.registry.model.pillar.PillarName">
422 <allow
423 interface="lp.registry.interfaces.pillar.IPillarName"/>
424diff --git a/lib/lp/registry/doc/socialaccount.rst b/lib/lp/registry/doc/socialaccount.rst
425new file mode 100644
426index 0000000..05591eb
427--- /dev/null
428+++ b/lib/lp/registry/doc/socialaccount.rst
429@@ -0,0 +1,32 @@
430+Social Accounts
431+==========
432+
433+Social Accounts are associated with a person and must be created through the
434+ISocialAccountSet utility.
435+
436+ >>> from lp.registry.interfaces.person import IPersonSet
437+ >>> from lp.registry.interfaces.role import IHasOwner
438+ >>> from lp.registry.interfaces.socialaccount import (
439+ ... ISocialAccount,
440+ ... ISocialAccountSet,
441+ ... SocialPlatform,
442+ ... )
443+
444+The new() method of ISocialAccountSet takes the person who will be associated
445+with the Social Account, a platform type and an identity dictionary.
446+
447+ >>> salgado = getUtility(IPersonSet).getByName("salgado")
448+ >>> identity = {}
449+ >>> identity["network"] = "abc.org"
450+ >>> identity["nickname"] = "salgado"
451+ >>> social_account = getUtility(ISocialAccountSet).new(
452+ ... salgado, SocialPlatform.MATRIX, identity
453+ ... )
454+
455+The returned SocialAccount object provides both ISocialAccount and IHasOwner.
456+
457+ >>> from lp.testing import verifyObject
458+ >>> verifyObject(ISocialAccount, social_account)
459+ True
460+ >>> verifyObject(IHasOwner, social_account)
461+ True
462diff --git a/lib/lp/registry/interfaces/person.py b/lib/lp/registry/interfaces/person.py
463index 3aeae4c..68be805 100644
464--- a/lib/lp/registry/interfaces/person.py
465+++ b/lib/lp/registry/interfaces/person.py
466@@ -121,6 +121,7 @@ from lp.registry.interfaces.location import (
467 from lp.registry.interfaces.mailinglistsubscription import (
468 MailingListAutoSubscribePolicy,
469 )
470+from lp.registry.interfaces.socialaccount import ISocialAccount
471 from lp.registry.interfaces.ssh import ISSHKey
472 from lp.registry.interfaces.teammembership import (
473 ITeamMembership,
474@@ -1030,6 +1031,14 @@ class IPersonViewRestricted(
475 ),
476 exported_as="jabber_ids",
477 )
478+ social_accounts = exported(
479+ CollectionField(
480+ title=_("List of Social Accounts of this Person."),
481+ readonly=True,
482+ required=False,
483+ value_type=Reference(schema=ISocialAccount),
484+ )
485+ )
486 team_memberships = exported(
487 CollectionField(
488 title=_(
489@@ -3241,3 +3250,4 @@ patch_reference_property(IIrcID, "person", IPerson)
490 patch_reference_property(IJabberID, "person", IPerson)
491 patch_reference_property(IWikiName, "person", IPerson)
492 patch_reference_property(IEmailAddress, "person", IPerson)
493+patch_reference_property(ISocialAccount, "person", IPerson)
494diff --git a/lib/lp/registry/interfaces/socialaccount.py b/lib/lp/registry/interfaces/socialaccount.py
495new file mode 100644
496index 0000000..e8c219e
497--- /dev/null
498+++ b/lib/lp/registry/interfaces/socialaccount.py
499@@ -0,0 +1,103 @@
500+# Copyright 2024 Canonical Ltd. This software is licensed under the
501+# GNU Affero General Public License version 3 (see the file LICENSE).
502+
503+"""SocialAccount interfaces."""
504+
505+__all__ = [
506+ "ISocialAccount",
507+ "ISocialAccountSet",
508+ "SocialPlatform",
509+ "SocialAccountIdentityError",
510+]
511+
512+import http.client
513+
514+from lazr.enum import DBEnumeratedType, DBItem
515+from lazr.restful.declarations import (
516+ error_status,
517+ exported,
518+ exported_as_webservice_entry,
519+)
520+from lazr.restful.fields import Reference
521+from zope.interface import Interface
522+from zope.schema import Choice, Dict, Int, TextLine
523+
524+from lp import _
525+from lp.registry.interfaces.role import IHasOwner
526+
527+
528+class SocialPlatform(DBEnumeratedType):
529+ """Social Platform Type
530+
531+ Social Account is associated with a SocialPlatform.
532+ """
533+
534+ MATRIX = DBItem(
535+ 1,
536+ """
537+ Matrix platform
538+
539+ The Social Account will hold Matrix account info.
540+ """,
541+ )
542+ matrix_identity_fields = ["nickname", "network"]
543+
544+
545+# XXX pelpsi 2023-12-14 bug=760849: "beta" is a lie to get WADL generation
546+# working.
547+@exported_as_webservice_entry("social_account", as_of="beta")
548+class ISocialAccount(IHasOwner):
549+ """Social Account"""
550+
551+ id = Int(title=_("Database ID"), required=True, readonly=True)
552+ # schema=Interface will be overridden in person.py because of circular
553+ # dependencies.
554+ person = exported(
555+ Reference(
556+ title=_("Owner"), required=True, schema=Interface, readonly=True
557+ )
558+ )
559+
560+ platform = exported(
561+ Choice(
562+ title=_("Social platform"),
563+ required=True,
564+ vocabulary=SocialPlatform,
565+ )
566+ )
567+
568+ identity = exported(
569+ Dict(
570+ title=_("Identity"),
571+ key_type=TextLine(),
572+ required=True,
573+ readonly=False,
574+ description=_(
575+ "A dictionary with the identity attributes and values for the "
576+ "social account. The format is specific for each platform. "
577+ ),
578+ )
579+ )
580+
581+ def destroySelf():
582+ """Delete this SocialAccount from the database."""
583+
584+
585+class ISocialAccountSet(Interface):
586+ """The set of SocialAccounts."""
587+
588+ def new(self, person, platform, identity):
589+ """Create a new SocialAccount pointing to the given Person."""
590+
591+ def getByPerson(person):
592+ """Return all SocialAccounts for the given person."""
593+
594+ def get(id):
595+ """Return the SocialAccount with the given id or None."""
596+
597+
598+@error_status(http.client.BAD_REQUEST)
599+class SocialAccountIdentityError(Exception):
600+ """Raised when Social Account's identity is
601+ invalid for a given social platform.
602+ """
603diff --git a/lib/lp/registry/interfaces/webservice.py b/lib/lp/registry/interfaces/webservice.py
604index 215c9ef..fc783eb 100644
605--- a/lib/lp/registry/interfaces/webservice.py
606+++ b/lib/lp/registry/interfaces/webservice.py
607@@ -35,6 +35,7 @@ __all__ = [
608 "IServiceFactory",
609 "ISharingService",
610 "ISSHKey",
611+ "ISocialAccount",
612 "ISourcePackage",
613 "ISourcePackageName",
614 "ITeam",
615@@ -94,6 +95,7 @@ from lp.registry.interfaces.productseries import (
616 )
617 from lp.registry.interfaces.projectgroup import IProjectGroup, IProjectGroupSet
618 from lp.registry.interfaces.sharingservice import ISharingService
619+from lp.registry.interfaces.socialaccount import ISocialAccount
620 from lp.registry.interfaces.sourcepackage import (
621 ISourcePackage,
622 ISourcePackageEdit,
623diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py
624index 779616d..996e9b3 100644
625--- a/lib/lp/registry/model/person.py
626+++ b/lib/lp/registry/model/person.py
627@@ -19,6 +19,8 @@ __all__ = [
628 "PersonLanguage",
629 "PersonSet",
630 "PersonSettings",
631+ "SocialAccount",
632+ "SocialAccountSet",
633 "SSHKey",
634 "SSHKeySet",
635 "TeamInvitationEvent",
636@@ -40,6 +42,7 @@ import transaction
637 from lazr.delegates import delegate_to
638 from lazr.restful.utils import get_current_browser_request, smartquote
639 from requests import PreparedRequest
640+from storm.databases.postgres import JSON
641 from storm.expr import (
642 SQL,
643 Alias,
644@@ -161,6 +164,12 @@ from lp.registry.interfaces.persontransferjob import IPersonMergeJobSource
645 from lp.registry.interfaces.product import IProduct, IProductSet
646 from lp.registry.interfaces.projectgroup import IProjectGroup
647 from lp.registry.interfaces.role import IPersonRoles
648+from lp.registry.interfaces.socialaccount import (
649+ ISocialAccount,
650+ ISocialAccountSet,
651+ SocialAccountIdentityError,
652+ SocialPlatform,
653+)
654 from lp.registry.interfaces.ssh import (
655 SSH_TEXT_TO_KEY_TYPE,
656 ISSHKey,
657@@ -646,6 +655,7 @@ class Person(
658 signedcocs = ReferenceSet("id", "SignedCodeOfConduct.owner_id")
659 _ircnicknames = ReferenceSet("id", "IrcID.person_id")
660 jabberids = ReferenceSet("id", "JabberID.person_id")
661+ _social_accounts = ReferenceSet("id", "SocialAccount.person_id")
662
663 visibility = DBEnum(
664 enum=PersonVisibility,
665@@ -688,6 +698,10 @@ class Person(
666 return list(self._ircnicknames)
667
668 @cachedproperty
669+ def social_accounts(self):
670+ return list(self._social_accounts)
671+
672+ @cachedproperty
673 def languages(self):
674 """See `IPerson`."""
675 results = Store.of(self).find(
676@@ -2779,6 +2793,7 @@ class Person(
677 ("sharingjob", "grantee"),
678 ("signedcodeofconduct", "owner"),
679 ("snapbuild", "requester"),
680+ ("socialaccount", "person"),
681 ("specificationsubscription", "person"),
682 ("sshkey", "person"),
683 ("structuralsubscription", "subscriber"),
684@@ -5321,6 +5336,55 @@ class WikiNameSet:
685 return wiki_name
686
687
688+@implementer(ISocialAccount)
689+class SocialAccount(StormBase, HasOwnerMixin):
690+ __storm_table__ = "SocialAccount"
691+
692+ id = Int(primary=True)
693+ person_id = Int(name="person", allow_none=False)
694+ person = Reference(person_id, "Person.id")
695+ platform = DBEnum(
696+ name="platform",
697+ allow_none=False,
698+ enum=SocialPlatform,
699+ )
700+ identity = JSON(name="identity", allow_none=False)
701+
702+ def __init__(self, person, platform, identity):
703+ super().__init__()
704+ self.person = person
705+ self.platform = platform
706+ self.identity = identity
707+
708+ def destroySelf(self):
709+ IStore(self).remove(self)
710+
711+
712+@implementer(ISocialAccountSet)
713+class SocialAccountSet:
714+ def get(self, id):
715+ """See `ISocialAccountSet`."""
716+ return IStore(SocialAccount).get(SocialAccount, int(id))
717+
718+ def getByPerson(self, person):
719+ """See `ISocialAccountSet`."""
720+ return IStore(SocialAccount).find(SocialAccount, person=person)
721+
722+ def new(self, person, platform, identity):
723+ """See `ISocialAccountSet`."""
724+ if platform == SocialPlatform.MATRIX:
725+ if "nickname" not in identity or "network" not in identity:
726+ raise SocialAccountIdentityError(
727+ "You must provide `nickname` and `network`"
728+ " for a matrix account."
729+ )
730+ social_account = SocialAccount(
731+ person=person, platform=platform, identity=identity
732+ )
733+ IStore(social_account).flush()
734+ return social_account
735+
736+
737 @implementer(IJabberID)
738 class JabberID(StormBase, HasOwnerMixin):
739 __storm_table__ = "JabberID"
740diff --git a/lib/lp/registry/stories/webservice/xx-person.rst b/lib/lp/registry/stories/webservice/xx-person.rst
741index 073c8a4..7e93f8b 100644
742--- a/lib/lp/registry/stories/webservice/xx-person.rst
743+++ b/lib/lp/registry/stories/webservice/xx-person.rst
744@@ -53,6 +53,7 @@ for teams (as they're defined in the ITeam interface).
745 recipes_collection_link: 'http://.../~salgado/recipes'
746 resource_type_link: 'http://.../#person'
747 self_link: 'http://.../~salgado'
748+ social_accounts_collection_link: 'http://.../~salgado/social_accounts'
749 sshkeys_collection_link: 'http://.../~salgado/sshkeys'
750 sub_teams_collection_link: 'http://.../~salgado/sub_teams'
751 super_teams_collection_link: 'http://.../~salgado/super_teams'
752@@ -116,6 +117,7 @@ for teams (as they're defined in the ITeam interface).
753 renewal_policy: 'invite them to apply for renewal'
754 resource_type_link: 'http://.../#team'
755 self_link: 'http://.../~ubuntu-team'
756+ social_accounts_collection_link: 'http://.../~ubuntu-team/social_accounts'
757 sshkeys_collection_link: 'http://.../~ubuntu-team/sshkeys'
758 sub_teams_collection_link: 'http://.../~ubuntu-team/sub_teams'
759 subscription_policy: 'Moderated Team'
760@@ -635,6 +637,18 @@ to, obviously.
761 HTTP/1.1 404 Not Found
762 ...
763
764+Social Accounts
765+..........
766+
767+Social Accounts of a person are also linked.
768+
769+ >>> mark = webservice.get("/~mark").jsonBody()
770+ >>> social_accounts_link = mark["social_accounts_collection_link"]
771+ >>> print(social_accounts_link)
772+ http://.../~mark/social_accounts
773+ >>> print_self_link_of_entries(
774+ ... webservice.get(social_accounts_link).jsonBody()
775+ ... )
776
777 IRC nicknames
778 .............
779diff --git a/lib/lp/registry/templates/person-editircnicknames.pt b/lib/lp/registry/templates/person-editircnicknames.pt
780index ec64be1..bc034fe 100644
781--- a/lib/lp/registry/templates/person-editircnicknames.pt
782+++ b/lib/lp/registry/templates/person-editircnicknames.pt
783@@ -46,21 +46,17 @@
784 </label>
785 </td>
786 </tr>
787-
788 </div>
789
790 <tr>
791- <td><label>Network:</label></td>
792- <td><label>Nickname:</label></td>
793- </tr>
794-
795- <tr>
796 <td>
797 <input name="newnetwork" type="text"
798+ placeholder="Enter new network"
799 tal:attributes="value view/newnetwork|nothing" />
800 </td>
801 <td>
802 <input name="newnick" type="text"
803+ placeholder="Enter new nickname"
804 tal:attributes="value view/newnick|nothing" />
805 </td>
806 </tr>
807diff --git a/lib/lp/registry/templates/person-editmatrixaccounts.pt b/lib/lp/registry/templates/person-editmatrixaccounts.pt
808new file mode 100644
809index 0000000..7d8e659
810--- /dev/null
811+++ b/lib/lp/registry/templates/person-editmatrixaccounts.pt
812@@ -0,0 +1,73 @@
813+<html
814+ xmlns="http://www.w3.org/1999/xhtml"
815+ xmlns:tal="http://xml.zope.org/namespaces/tal"
816+ xmlns:metal="http://xml.zope.org/namespaces/metal"
817+ xmlns:i18n="http://xml.zope.org/namespaces/i18n"
818+ metal:use-macro="view/macro:page/main_only"
819+ i18n:domain="launchpad"
820+>
821+<body>
822+
823+<div metal:fill-slot="main">
824+<div metal:use-macro="context/@@launchpad_form/form">
825+ <div metal:fill-slot="widgets">
826+
827+ <p tal:condition="view/error_message"
828+ tal:content="structure view/error_message/escapedtext" class="error message" />
829+
830+ <table>
831+
832+ <div tal:condition="view/matrix_accounts">
833+ <tr>
834+ <td><label>Network:</label></td>
835+ <td><label>Nickname:</label></td>
836+ </tr>
837+ <tr tal:repeat="matrix_account view/matrix_accounts">
838+ <td>
839+ <input tal:attributes="name string:network_${matrix_account/id};
840+ value matrix_account/identity/network"
841+ type="text" style="margin-bottom: 0.5em;"/>
842+ </td>
843+ <td>
844+ <input type="text"
845+ tal:attributes="name string:nickname_${matrix_account/id};
846+ value matrix_account/identity/nickname" />
847+ </td>
848+
849+ <td>
850+ <label>
851+ <input type="checkbox"
852+ value="Remove"
853+ tal:attributes="name string:remove_${matrix_account/id}" />
854+ Remove
855+ </label>
856+ </td>
857+ </tr>
858+ </div>
859+
860+ <tr>
861+ <td>
862+ <input name="new_network"
863+ type="text"
864+ placeholder="Enter new network"
865+ tal:attributes="value view/new_network|nothing" />
866+ </td>
867+ <td>
868+ <input name="new_nickname"
869+ type="text"
870+ placeholder="Enter new nickname"
871+ tal:attributes="value view/new_nickname|nothing" />
872+ </td>
873+ </tr>
874+
875+ <tr>
876+ <td class="formHelp">Example: ubuntu.com</td>
877+ <td class="formHelp">Example: mark</td>
878+ </tr>
879+ </table>
880+ </div>
881+</div>
882+</div>
883+
884+</body>
885+</html>
886diff --git a/lib/lp/registry/templates/person-portlet-contact-details.pt b/lib/lp/registry/templates/person-portlet-contact-details.pt
887index e9b37b2..dd6ba64 100644
888--- a/lib/lp/registry/templates/person-portlet-contact-details.pt
889+++ b/lib/lp/registry/templates/person-portlet-contact-details.pt
890@@ -170,8 +170,9 @@
891 class="sprite maybe action-icon">Karma help</a>
892 </dd>
893 </dl>
894- </div>
895+</div>
896
897+<<<<<<< lib/lp/registry/templates/person-portlet-contact-details.pt
898 <div class="yui-u two-column-list">
899 <dt>Social Accounts:</dt>
900 <dl tal:condition="view/should_show_ircnicknames_section">
901@@ -202,6 +203,54 @@
902 </dl>
903 </div>
904
905+=======
906+ <div class="yui-u social_accounts">
907+ <dl id="social-accounts">
908+ <dt>Social accounts:</dt>
909+
910+ <dd class="social_accounts__item" tal:repeat="ircnick context/ircnicknames">
911+ <img class="social_accounts__icon" alt="IRC" title="IRC" src="/@@/social-irc"/>
912+ <span tal:replace="structure ircnick/fmt:formatted_displayname"/>
913+ <a tal:condition="repeat/ircnick/end" tal:replace="structure overview_menu/editircnicknames/fmt:icon"/>
914+ </dd>
915+
916+ <dd class="social_accounts__item" tal:repeat="jabberid context/jabberids">
917+ <img class="social_accounts__icon" alt="Jabber" title="Jabber" src="/@@/social-jabber" />
918+ <span tal:replace="jabberid/jabberid/fmt:obfuscate-email"/>
919+ <a tal:condition="repeat/jabberid/end" tal:replace="structure overview_menu/editjabberids/fmt:icon"/>
920+ </dd>
921+
922+ <dd class="social_accounts__item" tal:repeat="social_account context/social_accounts">
923+ <span tal:replace="structure social_account/fmt:formatted_displayname"/>
924+ <a tal:condition="repeat/social_account/end" tal:replace="structure overview_menu/editmatrixaccounts/fmt:icon" />
925+ </dd>
926+
927+ <tal:irc condition="view/should_show_ircnicknames_section">
928+ <dd class="social_accounts__item" tal:condition="not: context/ircnicknames">
929+ <img class="social_accounts__icon" alt="IRC" title="IRC" src="/@@/social-irc"/>
930+ <span>No IRC nicknames registered.</span>
931+ <a tal:replace="structure overview_menu/editircnicknames/fmt:icon"/>
932+ </dd>
933+ </tal:irc>
934+
935+ <tal:jabber condition="view/should_show_jabberids_section">
936+ <dd class="social_accounts__item" tal:condition="context/jabberids/is_empty">
937+ <img class="social_accounts__icon" alt="Jabber" title="Jabber" src="/@@/social-jabber" />
938+ <span>No Jabber IDs registered.</span>
939+ <a tal:replace="structure overview_menu/editjabberids/fmt:icon" />
940+ </dd>
941+ </tal:jabber>
942+
943+ <tal:matrix condition="view/should_show_matrix_accounts_section">
944+ <dd class="social_accounts__item" tal:condition="not: context/social_accounts">
945+ <span>No matrix accounts registered.</span>
946+ <a tal:replace="structure overview_menu/editmatrixaccounts/fmt:icon" />
947+ </dd>
948+ </tal:matrix>
949+
950+ </dl>
951+ </div>
952+>>>>>>> lib/lp/registry/templates/person-portlet-contact-details.pt
953 </div>
954
955 </tal:root>
956diff --git a/lib/lp/registry/tests/test_person.py b/lib/lp/registry/tests/test_person.py
957index 5381103..47c0ec3 100644
958--- a/lib/lp/registry/tests/test_person.py
959+++ b/lib/lp/registry/tests/test_person.py
960@@ -38,6 +38,11 @@ from lp.registry.interfaces.karma import IKarmaCacheManager
961 from lp.registry.interfaces.person import ImmutableVisibilityError, IPersonSet
962 from lp.registry.interfaces.pocket import PackagePublishingPocket
963 from lp.registry.interfaces.product import IProductSet
964+from lp.registry.interfaces.socialaccount import (
965+ ISocialAccountSet,
966+ SocialAccountIdentityError,
967+ SocialPlatform,
968+)
969 from lp.registry.interfaces.teammembership import ITeamMembershipSet
970 from lp.registry.model.karma import KarmaCategory, KarmaTotalCache
971 from lp.registry.model.person import Person, get_recipients
972@@ -438,6 +443,113 @@ class TestPerson(TestCaseWithFactory):
973 self.assertEqual(None, person.homepage_content)
974 self.assertEqual(None, person.teamdescription)
975
976+ def test_social_account(self):
977+ user = self.factory.makePerson()
978+ attributes = {}
979+ attributes["network"] = "abc"
980+ attributes["nickname"] = "test-nickname"
981+ getUtility(ISocialAccountSet).new(
982+ user, SocialPlatform.MATRIX, attributes
983+ )
984+
985+ self.assertEqual(len(user.social_accounts), 1)
986+ social_account = user.social_accounts[0]
987+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
988+ self.assertEqual(social_account.identity["network"], "abc")
989+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
990+
991+ def test_malformed_matrix_account(self):
992+ user = self.factory.makePerson()
993+ attributes = {}
994+ attributes["network"] = "abc"
995+ attributes["name"] = "test-nickname"
996+ utility = getUtility(ISocialAccountSet)
997+
998+ self.assertRaises(
999+ SocialAccountIdentityError,
1000+ utility.new,
1001+ user,
1002+ SocialPlatform.MATRIX,
1003+ attributes,
1004+ )
1005+
1006+ def test_multiple_social_accounts(self):
1007+ user = self.factory.makePerson()
1008+ attributes = {}
1009+ attributes["network"] = "abc"
1010+ attributes["nickname"] = "test-nickname"
1011+ getUtility(ISocialAccountSet).new(
1012+ user, SocialPlatform.MATRIX, attributes
1013+ )
1014+ attributes = {}
1015+ attributes["network"] = "def"
1016+ attributes["nickname"] = "test-nickname"
1017+ getUtility(ISocialAccountSet).new(
1018+ user, SocialPlatform.MATRIX, attributes
1019+ )
1020+
1021+ self.assertEqual(len(user.social_accounts), 2)
1022+ social_account = user.social_accounts[1]
1023+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
1024+ self.assertEqual(social_account.identity["network"], "def")
1025+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
1026+
1027+ social_account = user.social_accounts[0]
1028+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
1029+ self.assertEqual(social_account.identity["network"], "abc")
1030+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
1031+
1032+ def test_multiple_social_accounts_on_multiple_users(self):
1033+ user = self.factory.makePerson()
1034+ attributes = {}
1035+ attributes["network"] = "abc"
1036+ attributes["nickname"] = "test-nickname"
1037+ getUtility(ISocialAccountSet).new(
1038+ user, SocialPlatform.MATRIX, attributes
1039+ )
1040+ attributes = {}
1041+ attributes["network"] = "def"
1042+ attributes["nickname"] = "test-nickname"
1043+ getUtility(ISocialAccountSet).new(
1044+ user, SocialPlatform.MATRIX, attributes
1045+ )
1046+
1047+ user_two = self.factory.makePerson()
1048+ attributes = {}
1049+ attributes["network"] = "ghi"
1050+ attributes["nickname"] = "test-nickname"
1051+ getUtility(ISocialAccountSet).new(
1052+ user_two, SocialPlatform.MATRIX, attributes
1053+ )
1054+ attributes = {}
1055+ attributes["network"] = "lmn"
1056+ attributes["nickname"] = "test-nickname"
1057+ getUtility(ISocialAccountSet).new(
1058+ user_two, SocialPlatform.MATRIX, attributes
1059+ )
1060+
1061+ self.assertEqual(len(user.social_accounts), 2)
1062+ social_account = user.social_accounts[1]
1063+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
1064+ self.assertEqual(social_account.identity["network"], "def")
1065+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
1066+
1067+ social_account = user.social_accounts[0]
1068+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
1069+ self.assertEqual(social_account.identity["network"], "abc")
1070+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
1071+
1072+ self.assertEqual(len(user_two.social_accounts), 2)
1073+ social_account = user_two.social_accounts[1]
1074+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
1075+ self.assertEqual(social_account.identity["network"], "lmn")
1076+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
1077+
1078+ social_account = user_two.social_accounts[0]
1079+ self.assertEqual(social_account.platform, SocialPlatform.MATRIX)
1080+ self.assertEqual(social_account.identity["network"], "ghi")
1081+ self.assertEqual(social_account.identity["nickname"], "test-nickname")
1082+
1083 def test_getAffiliatedPillars_kinds(self):
1084 # Distributions, project groups, and projects are returned in this
1085 # same order.
1086diff --git a/lib/lp/services/webservice/wadl-to-refhtml.xsl b/lib/lp/services/webservice/wadl-to-refhtml.xsl
1087index 0f6d973..4ddccb9 100644
1088--- a/lib/lp/services/webservice/wadl-to-refhtml.xsl
1089+++ b/lib/lp/services/webservice/wadl-to-refhtml.xsl
1090@@ -425,6 +425,12 @@
1091 <xsl:text>/+jabberid/</xsl:text>
1092 <var>&lt;id&gt;</var>
1093 </xsl:when>
1094+ <xsl:when test="@id = 'socialaccount_id'">
1095+ <xsl:text>/</xsl:text>
1096+ <var>&lt;person.name&gt;</var>
1097+ <xsl:text>/+socialaccount/</xsl:text>
1098+ <var>&lt;id&gt;</var>
1099+ </xsl:when>
1100 <xsl:when test="@id = 'irc_id'">
1101 <xsl:text>/</xsl:text>
1102 <var>&lt;person.name&gt;</var>

Subscribers

People subscribed via source and target branches

to status/vote changes: