launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #33130
[Merge] ~ines-almeida/launchpad:add-github-social into launchpad:master
Ines Almeida has proposed merging ~ines-almeida/launchpad:add-github-social into launchpad:master.
Commit message:
Add GitHub as social account choice
Users can now add a new GitHub social account via browser same as they do Matrix
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ines-almeida/launchpad/+git/launchpad/+merge/494263
Unfortunately, the UI for adding social accounts is not the best - it requires its own view per social platform (IRC, Jabber, Matrix, GitHub)
But it's at least clear, and hopefully we get to do a remake of User pages in the future
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ines-almeida/launchpad:add-github-social into launchpad:master.
diff --git a/lib/canonical/launchpad/images/social-github.png b/lib/canonical/launchpad/images/social-github.png
new file mode 100644
index 0000000..6cb3b70
Binary files /dev/null and b/lib/canonical/launchpad/images/social-github.png differ
diff --git a/lib/lp/app/tests/test_tales.py b/lib/lp/app/tests/test_tales.py
index 52730b8..b056a67 100644
--- a/lib/lp/app/tests/test_tales.py
+++ b/lib/lp/app/tests/test_tales.py
@@ -6,6 +6,7 @@
from datetime import datetime, timedelta, timezone
from lxml import html
+from testscenarios import WithScenarios
from zope.component import getAdapter, getUtility
from zope.traversing.interfaces import IPathAdapter, TraversalError
@@ -389,27 +390,47 @@ class TestIRCNicknameFormatterAPI(TestCaseWithFactory):
)
-class TestSocialAccountFormatterAPI(TestCaseWithFactory):
+class TestSocialAccountFormatterAPI(TestCaseWithFactory, WithScenarios):
"""Tests for SocialAccountFormatterAPI"""
layer = DatabaseFunctionalLayer
+ scenarios = [
+ (
+ "Matrix",
+ {
+ "platform": SocialPlatformType.MATRIX,
+ "identity": {"username": "fred", "homeserver": "ubuntu.com"},
+ "icon": "social-matrix",
+ "href": "https://matrix.to//#/@fred:ubuntu.com",
+ "display": "@fred:ubuntu.com",
+ },
+ ),
+ (
+ "GitHub",
+ {
+ "platform": SocialPlatformType.GITHUB,
+ "identity": {
+ "username": "fred",
+ },
+ "icon": "social-github",
+ "href": "https://github.com/fred",
+ "display": "/fred",
+ },
+ ),
+ ]
+
def test_formatted_display(self):
"""Social account is displayed as expected."""
person = self.factory.makePerson()
social_account_set = getUtility(ISocialAccountSet)
- identity = {
- "username": "fred",
- "homeserver": "ubuntu.com",
- }
social_account = social_account_set.new(
- person, SocialPlatformType.MATRIX, identity
+ person, self.platform, self.identity
)
expected_html = (
'<img class="user-social-accounts__icon" alt="Matrix" '
- 'title="Matrix" src="/@@/social-matrix" /> '
- "<a href=https://matrix.to//#/@fred:ubuntu.com "
- 'target="_blank"><strong>@fred:ubuntu.com</strong></a>'
+ 'title="Matrix" src="/@@/{self.icon}" /> <a href={self.href} '
+ 'target="_blank"><strong>{display}</strong></a>'
)
self.assertEqual(
diff --git a/lib/lp/registry/browser/configure.zcml b/lib/lp/registry/browser/configure.zcml
index bff5d4f..fd0eb65 100644
--- a/lib/lp/registry/browser/configure.zcml
+++ b/lib/lp/registry/browser/configure.zcml
@@ -1252,6 +1252,13 @@
template="../templates/person-editsocialaccounts.pt"
/>
<browser:page
+ name="+editgithubaccounts"
+ for="lp.registry.interfaces.person.IPerson"
+ class="lp.registry.browser.person.PersonEditGitHubAccountsView"
+ permission="launchpad.Edit"
+ template="../templates/person-editsocialaccounts.pt"
+ />
+ <browser:page
name="+editjabberids"
for="lp.registry.interfaces.person.IPerson"
class="lp.registry.browser.person.PersonEditJabberIDsView"
diff --git a/lib/lp/registry/browser/person.py b/lib/lp/registry/browser/person.py
index bf3c6fd..d0af80b 100644
--- a/lib/lp/registry/browser/person.py
+++ b/lib/lp/registry/browser/person.py
@@ -20,6 +20,7 @@ __all__ = [
"PersonEditIRCNicknamesView",
"PersonEditJabberIDsView",
"PersonEditMatrixAccountsView",
+ "PersonEditGitHubAccountsView",
"PersonEditTimeZoneView",
"PersonEditSSHKeysView",
"PersonEditView",
@@ -154,6 +155,7 @@ from lp.registry.interfaces.pillar import IPillarNameSet
from lp.registry.interfaces.poll import IPollSubset
from lp.registry.interfaces.product import InvalidProductName, IProduct
from lp.registry.interfaces.socialaccount import (
+ GithubPlatform,
ISocialAccountSet,
MatrixPlatform,
SocialAccountIdentityError,
@@ -833,6 +835,7 @@ class PersonOverviewMenu(
"editircnicknames",
"editjabberids",
"editmatrixaccounts",
+ "editgithubaccounts",
"editsshkeys",
"editpgpkeys",
"editlocation",
@@ -905,6 +908,12 @@ class PersonOverviewMenu(
return Link(target, text, icon="edit", summary=text)
@enabled_with_permission("launchpad.Edit")
+ def editgithubaccounts(self):
+ target = "+editgithubaccounts"
+ text = "Edit GitHub accounts"
+ return Link(target, text, icon="edit", summary=text)
+
+ @enabled_with_permission("launchpad.Edit")
def editjabberids(self):
target = "+editjabberids"
text = "Update Jabber IDs"
@@ -1696,11 +1705,23 @@ class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
)
@property
+ def should_show_github_accounts_section(self):
+ """Should the github accounts section be shown?
+
+ It's shown when the person has social accounts for the Matrix platform
+ registered or has rights to register new ones.
+ """
+ return bool(self.github_accounts) or (
+ check_permission("launchpad.Edit", self.context)
+ )
+
+ @property
def should_show_socialaccounts_section(self):
return (
self.should_show_ircnicknames_section
or self.should_show_jabberids_section
or self.should_show_matrix_accounts_section
+ or self.should_show_github_accounts_section
)
@property
@@ -1737,6 +1758,12 @@ class PersonView(LaunchpadView, FeedsMixin, ContactViaWebLinksMixin):
)
@cachedproperty
+ def github_accounts(self):
+ return self.context.getSocialAccountsByPlatform(
+ SocialPlatformType.GITHUB
+ )
+
+ @cachedproperty
def is_probationary_or_invalid_user(self):
"""True when the user is not active or does not have karma.
@@ -2550,6 +2577,10 @@ class PersonEditMatrixAccountsView(PersonEditSocialAccountsView):
platform = MatrixPlatform
+class PersonEditGitHubAccountsView(PersonEditSocialAccountsView):
+ platform = GithubPlatform
+
+
class PersonEditJabberIDsView(LaunchpadFormView):
schema = IJabberID
field_names = ["jabberid"]
diff --git a/lib/lp/registry/interfaces/socialaccount.py b/lib/lp/registry/interfaces/socialaccount.py
index 4c8134d..f7f6d9e 100644
--- a/lib/lp/registry/interfaces/socialaccount.py
+++ b/lib/lp/registry/interfaces/socialaccount.py
@@ -6,6 +6,7 @@
__all__ = [
"ISocialAccount",
"ISocialAccountSet",
+ "GithubPlatform",
"MatrixPlatform",
"SocialPlatformType",
"SOCIAL_PLATFORM_TYPES_MAP",
@@ -46,6 +47,15 @@ class SocialPlatformType(DBEnumeratedType):
""",
)
+ GITHUB = DBItem(
+ 2,
+ """
+ GitHub platform
+
+ The Social Account will hold GitHub account info.
+ """,
+ )
+
# XXX pelpsi 2023-12-14 bug=760849: "beta" is a lie to get WADL generation
# working.
@@ -160,7 +170,39 @@ class MatrixPlatform(SocialPlatform):
)
+class GithubPlatform(SocialPlatform):
+ title = "GitHub"
+ identity_fields = ["username"]
+ identity_fields_example = {
+ "username": "mark",
+ "homeserver": "ubuntu.com",
+ }
+ platform_type = SocialPlatformType.GITHUB
+ icon = "social-github"
+ display_format = "<strong>/{username}</strong>"
+ url = "https://github.com/{username}"
+
+ @classmethod
+ def validate_identity(cls, identity):
+ if not all(
+ identity.get(required_field)
+ for required_field in cls.identity_fields
+ ):
+ raise SocialAccountIdentityError(
+ f"You must provide the following fields: "
+ f"{', '.join(cls.identity_fields)}."
+ )
+ if not isinstance(identity["username"], str):
+ raise SocialAccountIdentityError("Username must be a string.")
+ # GitHub username can contain a-z, 0-9, and -
+ # ref: https://docs.github.com/en/enterprise-cloud@latest/admin/managing-iam/iam-configuration-reference/username-considerations-for-external-authentication#about-username-normalization # noqa: E501
+ username_regex = r"^[A-z0-9-]+"
+ if not re.match(username_regex, identity["username"]):
+ raise SocialAccountIdentityError("Username must be valid.")
+
+
SOCIAL_PLATFORM_TYPES_MAP = {
+ SocialPlatformType.GITHUB: GithubPlatform,
SocialPlatformType.MATRIX: MatrixPlatform,
}
diff --git a/lib/lp/registry/templates/person-portlet-contact-details.pt b/lib/lp/registry/templates/person-portlet-contact-details.pt
index 906d22d..1349c71 100644
--- a/lib/lp/registry/templates/person-portlet-contact-details.pt
+++ b/lib/lp/registry/templates/person-portlet-contact-details.pt
@@ -189,6 +189,11 @@
<a tal:replace="structure overview_menu/editmatrixaccounts/fmt:icon" />
</dd>
+ <dd class="user-social-accounts__item github-account" tal:repeat="social_account view/github_accounts">
+ <span><span tal:replace="structure social_account/fmt:formatted_display" /></span>
+ <a tal:replace="structure overview_menu/editgithubaccounts/fmt:icon" />
+ </dd>
+
<tal:irc condition="view/should_show_ircnicknames_section">
<dd class="user-social-accounts__item" tal:condition="not: context/ircnicknames" id="empty-irc">
<img class="user-social-accounts__icon" alt="IRC" title="IRC" src="/@@/social-irc"/>
@@ -213,6 +218,14 @@
</dd>
</tal:matrix>
+ <tal:matrix condition="view/should_show_github_accounts_section">
+ <dd class="user-social-accounts__item" tal:condition="not: view/github_accounts" id="empty-matrix">
+ <img class="user-social-accounts__icon" alt="Matrix" title="Matrix" src="/@@/social-github" />
+ <span>No github accounts registered.</span>
+ <a tal:replace="structure overview_menu/editgithubaccounts/fmt:icon" />
+ </dd>
+ </tal:matrix>
+
</dl>
</div>
</div>
diff --git a/lib/lp/registry/tests/test_socialaccount.py b/lib/lp/registry/tests/test_socialaccount.py
index 10dba86..f9f4dd0 100644
--- a/lib/lp/registry/tests/test_socialaccount.py
+++ b/lib/lp/registry/tests/test_socialaccount.py
@@ -13,7 +13,7 @@ from lp.testing import TestCaseWithFactory, login_person
from lp.testing.layers import DatabaseFunctionalLayer
-class TestSocialAccount(TestCaseWithFactory):
+class TestMatrixSocialAccount(TestCaseWithFactory):
layer = DatabaseFunctionalLayer
def test_social_account(self):
@@ -49,6 +49,22 @@ class TestSocialAccount(TestCaseWithFactory):
self.assertEqual(social_account.identity["homeserver"], "abc.org")
self.assertEqual(social_account.identity["username"], "test-nickname")
+ def test_github_account(self):
+ # GitHub Social Account is created as expected and
+ # associated to the user.
+ user = self.factory.makePerson()
+ attributes = {}
+ attributes["username"] = "test-nickname"
+ social_account = getUtility(ISocialAccountSet).new(
+ user, SocialPlatformType.GITHUB, attributes
+ )
+
+ self.assertEqual(len(user.social_accounts), 1)
+ social_account = user.social_accounts[0]
+
+ self.assertEqual(social_account.platform, SocialPlatformType.GITHUB)
+ self.assertEqual(social_account.identity["username"], "test-nickname")
+
def test_multilevel_domain_matrix_account(self):
# Homeserver with a multi-level domain is allowed
# Matrix username can contain a-z, 0-9, ., _, =, -, and /
@@ -173,6 +189,21 @@ class TestSocialAccount(TestCaseWithFactory):
attributes,
)
+ def test_malformed_github_account_username(self):
+ # Username must be a string
+ user = self.factory.makePerson()
+ attributes = {}
+ attributes["username"] = "@username!(*)"
+ utility = getUtility(ISocialAccountSet)
+
+ self.assertRaises(
+ SocialAccountIdentityError,
+ utility.new,
+ user,
+ SocialPlatformType.GITHUB,
+ attributes,
+ )
+
def test_malformed_matrix_account_homeserver(self):
# Homeserver must be a valid address
user = self.factory.makePerson()
@@ -205,6 +236,21 @@ class TestSocialAccount(TestCaseWithFactory):
attributes,
)
+ def test_empty_fields_github_account(self):
+ # Identity field must be not empty
+ user = self.factory.makePerson()
+ attributes = {}
+ attributes["username"] = ""
+ utility = getUtility(ISocialAccountSet)
+
+ self.assertRaises(
+ SocialAccountIdentityError,
+ utility.new,
+ user,
+ SocialPlatformType.GITHUB,
+ attributes,
+ )
+
def test_multiple_social_accounts(self):
# Users can have multiple social accounts
user = self.factory.makePerson()
@@ -250,16 +296,14 @@ class TestSocialAccount(TestCaseWithFactory):
user_two = self.factory.makePerson()
attributes = {}
- attributes["homeserver"] = "ghi.org"
attributes["username"] = "test-nickname"
getUtility(ISocialAccountSet).new(
- user_two, SocialPlatformType.MATRIX, attributes
+ user_two, SocialPlatformType.GITHUB, attributes
)
attributes = {}
- attributes["homeserver"] = "lmn.org"
- attributes["username"] = "test-nickname"
+ attributes["username"] = "test-nickname2"
getUtility(ISocialAccountSet).new(
- user_two, SocialPlatformType.MATRIX, attributes
+ user_two, SocialPlatformType.GITHUB, attributes
)
self.assertEqual(len(user.social_accounts), 2)
@@ -275,11 +319,9 @@ class TestSocialAccount(TestCaseWithFactory):
self.assertEqual(len(user_two.social_accounts), 2)
social_account = user_two.social_accounts[0]
- self.assertEqual(social_account.platform, SocialPlatformType.MATRIX)
- self.assertEqual(social_account.identity["homeserver"], "ghi.org")
+ self.assertEqual(social_account.platform, SocialPlatformType.GITHUB)
self.assertEqual(social_account.identity["username"], "test-nickname")
social_account = user_two.social_accounts[1]
- self.assertEqual(social_account.platform, SocialPlatformType.MATRIX)
- self.assertEqual(social_account.identity["homeserver"], "lmn.org")
- self.assertEqual(social_account.identity["username"], "test-nickname")
+ self.assertEqual(social_account.platform, SocialPlatformType.GITHUB)
+ self.assertEqual(social_account.identity["username"], "test-nickname2")
References