Merge ~ines-almeida/launchpad:social-accounts-display-accounts into launchpad:master
- Git
- lp:~ines-almeida/launchpad
- social-accounts-display-accounts
- Merge into 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 |
Related bugs: |
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
Description of the change
To post a comment you must log in.
Unmerged commits
- 39f0afb... by Ines Almeida
-
Add CSS to new social accounts display
-
docs:0 (build) lint:0 (build) mypy:0 (build) 1 → 3 of 3 results First • Previous • Next • Last - 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'
-
docs:0 (build) lint:0 (build) mypy:0 (build) 1 → 3 of 3 results First • Previous • Next • Last - 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
1 | diff --git a/lib/canonical/launchpad/icing/css/components/_index.scss b/lib/canonical/launchpad/icing/css/components/_index.scss |
2 | index 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'; |
11 | diff --git a/lib/canonical/launchpad/icing/css/components/social_accounts.scss b/lib/canonical/launchpad/icing/css/components/social_accounts.scss |
12 | new file mode 100644 |
13 | index 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 | +} |
34 | diff --git a/lib/canonical/launchpad/icing/css/typography.scss b/lib/canonical/launchpad/icing/css/typography.scss |
35 | index 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 { |
52 | diff --git a/lib/canonical/launchpad/images/social-irc.png b/lib/canonical/launchpad/images/social-irc.png |
53 | new file mode 100644 |
54 | index 0000000..ed760ea |
55 | Binary files /dev/null and b/lib/canonical/launchpad/images/social-irc.png differ |
56 | diff --git a/lib/canonical/launchpad/images/social-jabber.png b/lib/canonical/launchpad/images/social-jabber.png |
57 | new file mode 100644 |
58 | index 0000000..95cf8a0 |
59 | Binary files /dev/null and b/lib/canonical/launchpad/images/social-jabber.png differ |
60 | diff --git a/lib/canonical/launchpad/images/social-matrix.png b/lib/canonical/launchpad/images/social-matrix.png |
61 | new file mode 100644 |
62 | index 0000000..031885f |
63 | Binary files /dev/null and b/lib/canonical/launchpad/images/social-matrix.png differ |
64 | diff --git a/lib/canonical/launchpad/images/src/social-irc.svg b/lib/canonical/launchpad/images/src/social-irc.svg |
65 | new file mode 100644 |
66 | index 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 |
76 | diff --git a/lib/canonical/launchpad/images/src/social-jabber.svg b/lib/canonical/launchpad/images/src/social-jabber.svg |
77 | new file mode 100644 |
78 | index 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 |
85 | diff --git a/lib/canonical/launchpad/images/src/social-matrix.svg b/lib/canonical/launchpad/images/src/social-matrix.svg |
86 | new file mode 100644 |
87 | index 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> |
100 | diff --git a/lib/lp/app/browser/configure.zcml b/lib/lp/app/browser/configure.zcml |
101 | index 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" |
118 | diff --git a/lib/lp/app/browser/tales.py b/lib/lp/app/browser/tales.py |
119 | index 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 |
172 | diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml |
173 | index 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" |
203 | diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py |
204 | index 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"] |
386 | diff --git a/lib/lp/registry/configure.zcml b/lib/lp/registry/configure.zcml |
387 | index 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"/> |
424 | diff --git a/lib/lp/registry/doc/socialaccount.rst b/lib/lp/registry/doc/socialaccount.rst |
425 | new file mode 100644 |
426 | index 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 |
462 | diff --git a/lib/lp/registry/interfaces/person.py b/lib/lp/registry/interfaces/person.py |
463 | index 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) |
494 | diff --git a/lib/lp/registry/interfaces/socialaccount.py b/lib/lp/registry/interfaces/socialaccount.py |
495 | new file mode 100644 |
496 | index 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 | + """ |
603 | diff --git a/lib/lp/registry/interfaces/webservice.py b/lib/lp/registry/interfaces/webservice.py |
604 | index 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, |
623 | diff --git a/lib/lp/registry/model/person.py b/lib/lp/registry/model/person.py |
624 | index 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" |
740 | diff --git a/lib/lp/registry/stories/webservice/xx-person.rst b/lib/lp/registry/stories/webservice/xx-person.rst |
741 | index 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 | ............. |
779 | diff --git a/lib/lp/registry/templates/person-editircnicknames.pt b/lib/lp/registry/templates/person-editircnicknames.pt |
780 | index 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> |
807 | diff --git a/lib/lp/registry/templates/person-editmatrixaccounts.pt b/lib/lp/registry/templates/person-editmatrixaccounts.pt |
808 | new file mode 100644 |
809 | index 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> |
886 | diff --git a/lib/lp/registry/templates/person-portlet-contact-details.pt b/lib/lp/registry/templates/person-portlet-contact-details.pt |
887 | index 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> |
956 | diff --git a/lib/lp/registry/tests/test_person.py b/lib/lp/registry/tests/test_person.py |
957 | index 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. |
1086 | diff --git a/lib/lp/services/webservice/wadl-to-refhtml.xsl b/lib/lp/services/webservice/wadl-to-refhtml.xsl |
1087 | index 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><id></var> |
1093 | </xsl:when> |
1094 | + <xsl:when test="@id = 'socialaccount_id'"> |
1095 | + <xsl:text>/</xsl:text> |
1096 | + <var><person.name></var> |
1097 | + <xsl:text>/+socialaccount/</xsl:text> |
1098 | + <var><id></var> |
1099 | + </xsl:when> |
1100 | <xsl:when test="@id = 'irc_id'"> |
1101 | <xsl:text>/</xsl:text> |
1102 | <var><person.name></var> |