This is an automated email from the ASF dual-hosted git repository. dill0wn pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/allura.git
commit 8294faf5a1004486d2a202939f03f6bd3dcc7266 Author: Guillermo Cruz <[email protected]> AuthorDate: Fri Dec 9 16:35:56 2022 -0600 [#8484] added fediverse and instagram form fields for projects --- Allura/allura/ext/admin/admin_main.py | 11 ++++ Allura/allura/ext/admin/widgets.py | 18 ++++-- .../user_profile/templates/sections/social.html | 4 ++ Allura/allura/lib/helpers.py | 4 ++ Allura/allura/lib/validators.py | 35 ++++++++++- Allura/allura/lib/widgets/forms.py | 10 ++- Allura/allura/model/project.py | 13 ++++ Allura/allura/templates/widgets/forge_form.html | 2 +- .../templates/admin_widgets/metadata_admin.html | 8 +++ Allura/allura/tests/functional/test_auth.py | 71 +++++++++++++++++++--- Allura/development.ini | 3 +- 11 files changed, 159 insertions(+), 20 deletions(-) diff --git a/Allura/allura/ext/admin/admin_main.py b/Allura/allura/ext/admin/admin_main.py index d40d9b9b0..0a9ef9a61 100644 --- a/Allura/allura/ext/admin/admin_main.py +++ b/Allura/allura/ext/admin/admin_main.py @@ -319,6 +319,8 @@ class ProjectAdminController(BaseController): support_page_url='', twitter_handle='', facebook_page='', + fediverse_address='', + instagram_page='', removal='', moved_to_url='', tracking_id='', @@ -385,6 +387,15 @@ class ProjectAdminController(BaseController): M.AuditLog.log( 'change project facebook page to %s', facebook_page) c.project.set_social_account('Facebook', facebook_page) + old_fediverse = c.project.social_account('Fediverse') + if not old_fediverse or fediverse_address != old_fediverse.accounturl: + M.AuditLog.log('change project fediverse username to %s', + fediverse_address) + c.project.set_social_account('Fediverse', fediverse_address) + old_instagram = c.project.social_account('Instagram') + if not old_instagram or instagram_page != old_instagram: + M.AuditLog.log('change project instagram page to %s', instagram_page) + c.project.set_social_account('Instagram', instagram_page) if support_page_url != c.project.support_page_url: M.AuditLog.log('change project support page url to %s', support_page_url) diff --git a/Allura/allura/ext/admin/widgets.py b/Allura/allura/ext/admin/widgets.py index 5a2e0e3cb..7af817d0b 100644 --- a/Allura/allura/ext/admin/widgets.py +++ b/Allura/allura/ext/admin/widgets.py @@ -14,7 +14,7 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. - +import tg from tg import tmpl_context as c import ew as ew_core @@ -28,6 +28,7 @@ from allura.lib.widgets import forms as ff from allura.lib.widgets import form_fields as ffw from bson import ObjectId +from paste.deploy.converters import aslist class CardField(ew._Jinja2Widget): @@ -163,7 +164,9 @@ class MetadataAdmin(ff.AdminForm): defaults = dict( ff.AdminForm.defaults, enctype='multipart/form-data') - + allowed_social_domains = aslist(tg.config.get('allowed_social_domains', + ['facebook', 'instagram', 'linkedin', 'twitter']), + ',') class fields(ew_core.NameList): name = ew.InputField(field_type='text', label='Name', @@ -221,9 +224,16 @@ class MetadataAdmin(ff.AdminForm): field_type="text", label="Google Analytics ID", attrs=(dict(placeholder='UA-123456-0', pattern='UA-[0-9]+-[0-9]+'))) twitter_handle = ew.InputField( - field_type="text", label='Twitter Handle') + field_type="text", label='Twitter Handle', validator=V.SocialDomainValidator('twitter.com')) facebook_page = ew.InputField(field_type="text", label='Facebook page', - validator=fev.URL(add_http=True)) + validator=formencode.All(fev.URL(add_http=True), V.SocialDomainValidator('facebook.com')) ) + instagram_page = ew.InputField( + field_type="text", label='Instagram page', + validator=formencode.All(fev.URL(add_http=True), V.SocialDomainValidator('instagram.com'))) + fediverse_address = ew.InputField(field_type="text", label="Mastodon address", + validator=V.FediverseAddressValidator) + + class AuditLog(ew_core.Widget): diff --git a/Allura/allura/ext/user_profile/templates/sections/social.html b/Allura/allura/ext/user_profile/templates/sections/social.html index fe7eb970f..d94f0bf48 100644 --- a/Allura/allura/ext/user_profile/templates/sections/social.html +++ b/Allura/allura/ext/user_profile/templates/sections/social.html @@ -33,7 +33,11 @@ {% block content %} <dl> {% for contact in user.get_pref('socialnetworks') %} + {% if contact.socialnetwork == 'Mastodon' %} + <dt>{{ contact.socialnetwork }}</dt><dd><a href="{{ h.parse_fediverse_address(contact.accounturl) }}" rel="me nofollow">{{ contact.accounturl }}</a></dd> + {% else %} <dt>{{ contact.socialnetwork }}</dt><dd>{{ contact.accounturl|urlize(nofollow=True) }}</dd> + {% endif %} {% else %} <dd class="empty">No social networks entered.</dd> {% endfor %} diff --git a/Allura/allura/lib/helpers.py b/Allura/allura/lib/helpers.py index 6aefc5627..7e7600a3d 100644 --- a/Allura/allura/lib/helpers.py +++ b/Allura/allura/lib/helpers.py @@ -1334,3 +1334,7 @@ def pluralize_tool_name(tool_name: string, count: int): if tool_name is not None and tool_name in pluralize_tools: return f"{tool_name}{'s'[:count^1]}" return tool_name + +def parse_fediverse_address(username: str): + pieces = username.split('@') + return f'https://{pieces[-1]}/@{pieces[1]}' diff --git a/Allura/allura/lib/validators.py b/Allura/allura/lib/validators.py index b7e56d7ba..aa0488705 100644 --- a/Allura/allura/lib/validators.py +++ b/Allura/allura/lib/validators.py @@ -24,7 +24,7 @@ from tg import tmpl_context as c from . import helpers as h from datetime import datetime import six -from urllib.parse import urlparse +from urllib.parse import urlsplit from ipaddress import ip_address import socket @@ -52,7 +52,7 @@ class URLIsPrivate(URL): def _to_python(self, value, state): value = super(URLIsPrivate, self)._to_python(value, state) - url_components = urlparse(value) + url_components = urlsplit(value) try: host_ip = socket.gethostbyname(url_components.netloc) except socket.gaierror: @@ -487,3 +487,34 @@ class IconValidator(fev.FancyValidator): value, state) return value + +FEDIVERSE_REGEX = r'^@[a-zA-Z_]*@[a-zA-Z_]*\.{1}[A-Za-z]{0,10}$' + +class FediverseAddressValidator(fev.FancyValidator): + + + def _to_python(self, value, state): + match = re.match(FEDIVERSE_REGEX , value) + if not match: + raise fe.Invalid('Address format must be @your username@your server', value, state) + + return value.lower() + + + +class SocialDomainValidator(fev.FancyValidator): + def __init__(self, domain='', **kw): + self.domain = domain + self.domains = kw.get('domains') + + def _to_python(self, value, state): + if value.startswith('@') and not re.match(FEDIVERSE_REGEX , value): + value = f'https://twitter.com/{value.replace("@","")}' + url = urlsplit(value) + if not re.match(FEDIVERSE_REGEX , value): + if self.domain and not self.domain == url.netloc.replace('www.',''): + raise fe.Invalid('Invalid domain for this field', value, state) + if self.domains and not any(domain == url.netloc.replace('www.','') for domain in self.domains): + raise fe.Invalid('Invalid domain for this field', value, state) + return value + diff --git a/Allura/allura/lib/widgets/forms.py b/Allura/allura/lib/widgets/forms.py index 69bf853cd..3444bf170 100644 --- a/Allura/allura/lib/widgets/forms.py +++ b/Allura/allura/lib/widgets/forms.py @@ -434,8 +434,12 @@ class AddSocialNetworkForm(ForgeForm): @property def fields(self): socialnetworks = aslist(tg.config.get('socialnetworks', - ['Facebook', 'Linkedin', 'Twitter', 'Google+']), + ['Facebook', 'Linkedin', 'Twitter',]), ',') + allowed_social_domains = aslist(tg.config.get('allowed_social_domains', + ['facebook.com', 'instagram.com', 'linkedin.com', 'twitter.com']), + ',') + return [ ew.SingleSelectField( name='socialnetwork', @@ -446,7 +450,9 @@ class AddSocialNetworkForm(ForgeForm): ew.TextField( name='accounturl', label='Account url', - validator=V.UnicodeString(not_empty=True)) + validator=formencode.All( + V.UnicodeString(not_empty=True), V.SocialDomainValidator(domains=allowed_social_domains) + )) ] diff --git a/Allura/allura/model/project.py b/Allura/allura/model/project.py index fad47aefc..743384775 100644 --- a/Allura/allura/model/project.py +++ b/Allura/allura/model/project.py @@ -1134,6 +1134,19 @@ class Project(SearchIndexable, MappedClass, ActivityNode, ActivityObject): def facebook_page(self): return self.social_account('Facebook').accounturl + @property + def fediverse_address(self): + return self.social_account('Fediverse').accounturl + + @classmethod + def fediverse_parse_address(cls, username): + pieces = username.split('@') + return f'https://{pieces[-1]}/@{pieces[1]}' + + @property + def instagram_page(self): + return self.social_account('Instagram').accounturl + def social_account(self, socialnetwork): try: account = next( diff --git a/Allura/allura/templates/widgets/forge_form.html b/Allura/allura/templates/widgets/forge_form.html index 4f182d1f8..47656c9bc 100644 --- a/Allura/allura/templates/widgets/forge_form.html +++ b/Allura/allura/templates/widgets/forge_form.html @@ -32,7 +32,7 @@ {% set ctx=widget.context_for(field) %} {% if field.field_type != 'hidden' %} {% if ctx.errors and field.show_errors -%} - <div class="grid-{{19 + extra_width}}"><span {{widget.j2_attrs({'class':error_class})}}>{{ctx.errors|nl2br}}</span></div> + <div class="grid-{{19 + extra_width}}"><span {{widget.j2_attrs({'class':error_class})}}>{{ctx.errors|nl2br}}</span></div><br> {%- endif %} {% if field.show_label and field.label %} <label for="{{ctx.id}}" class="grid-4">{{field.label}}:</label> diff --git a/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html b/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html index b47b0cce3..68ac3c213 100644 --- a/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html +++ b/Allura/allura/templates_responsive/override/allura/ext/admin/templates/admin_widgets/metadata_admin.html @@ -97,6 +97,14 @@ {{ widget.display_label(widget.fields.facebook_page) }} {{widget.display_field(widget.fields.facebook_page) }} </div> + <div class="field"> + {{ widget.display_label(widget.fields.instagram_page) }} + {{ widget.display_field(widget.fields.instagram_page) }} + </div> + <div class="field"> + {{ widget.display_label(widget.fields.fediverse_address) }} + {{ widget.display_field(widget.fields.fediverse_address) }} + </div> </div> {% if c.project.neighborhood.features['google_analytics'] %} diff --git a/Allura/allura/tests/functional/test_auth.py b/Allura/allura/tests/functional/test_auth.py index 5c459db65..941215623 100644 --- a/Allura/allura/tests/functional/test_auth.py +++ b/Allura/allura/tests/functional/test_auth.py @@ -1172,6 +1172,7 @@ class TestAuthRest(TestRestApiBase): class TestPreferences(TestController): + @td.with_user_project('test-admin') def test_personal_data(self): from pytz import country_names @@ -1179,7 +1180,6 @@ class TestPreferences(TestController): setsex, setbirthdate, setcountry, setcity, settimezone = \ ('Male', '19/08/1988', 'IT', 'Milan', 'Europe/Rome') self.app.get('/auth/user_info/') - # Check if personal data is properly set r = self.app.post('/auth/user_info/change_personal_data', params=dict( @@ -1231,6 +1231,40 @@ class TestPreferences(TestController): user = M.User.query.get(username='test-admin') assert user.birthdate is None + @td.with_user_project('test-admin') + def test_contacts_not_allowed(self): + self.app.get('/auth/user_info/') + socialnetwork = 'Facebook' + accounturl = 'http://www.faceboookk.com/test' + self.app.post('/auth/user_info/contacts/add_social_network', + params=dict(socialnetwork=socialnetwork, + accounturl=accounturl, + _session_id=self.app.cookies['_session_id'], + )) + user = M.User.query.get(username='test-admin') + assert len(user.socialnetworks) == 0 + + socialnetwork = 'Instagram' + accounturl = 'http://www.insta.com/test' + self.app.post('/auth/user_info/contacts/add_social_network', + params=dict(socialnetwork=socialnetwork, + accounturl=accounturl, + _session_id=self.app.cookies['_session_id'], + )) + user = M.User.query.get(username='test-admin') + assert len(user.socialnetworks) == 0 + + socialnetwork = 'Mastodon' + accounturl = '@username@server' + self.app.post('/auth/user_info/contacts/add_social_network', + params=dict(socialnetwork=socialnetwork, + accounturl=accounturl, + _session_id=self.app.cookies['_session_id'], + )) + user = M.User.query.get(username='test-admin') + assert len(user.socialnetworks) == 0 + + @td.with_user_project('test-admin') def test_contacts(self): # Add skype account @@ -1256,7 +1290,7 @@ class TestPreferences(TestController): # Add second social network account socialnetwork2 = 'Twitter' - accounturl2 = 'http://twitter.com/test' + accounturl2 = 'https://twitter.com/test' self.app.post('/auth/user_info/contacts/add_social_network', params=dict(socialnetwork=socialnetwork2, accounturl='@test', @@ -1264,8 +1298,19 @@ class TestPreferences(TestController): )) user = M.User.query.get(username='test-admin') assert len(user.socialnetworks) == 2 - assert {'socialnetwork': socialnetwork, 'accounturl': accounturl} in user.socialnetworks - assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks + expected = [{'socialnetwork': socialnetwork, 'accounturl': accounturl}, + {'socialnetwork': socialnetwork2, 'accounturl': accounturl2}] + assert all([social in expected for social in user.socialnetworks]) + + socialnetwork3 = 'Mastodon' + accounturl3 = '@[email protected]' + self.app.post('/auth/user_info/contacts/add_social_network', + params=dict(socialnetwork=socialnetwork3, + accounturl=accounturl3, + _session_id=self.app.cookies['_session_id'], + )) + user = M.User.query.get(username='test-admin') + assert len(user.socialnetworks) == 3 # Remove first social network account self.app.post('/auth/user_info/contacts/remove_social_network', @@ -1274,8 +1319,10 @@ class TestPreferences(TestController): _session_id=self.app.cookies['_session_id'], )) user = M.User.query.get(username='test-admin') - assert len(user.socialnetworks) == 1 - assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks + assert len(user.socialnetworks) == 2 + expected = [{'socialnetwork': socialnetwork2, 'accounturl': accounturl2}, + {'socialnetwork': socialnetwork3, 'accounturl': accounturl3}] + assert all([social in expected for social in user.socialnetworks]) # Add empty social network account self.app.post('/auth/user_info/contacts/add_social_network', @@ -1283,8 +1330,10 @@ class TestPreferences(TestController): _session_id=self.app.cookies['_session_id'], )) user = M.User.query.get(username='test-admin') - assert len(user.socialnetworks) == 1 - assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks + assert len(user.socialnetworks) == 2 + expected = [{'socialnetwork': socialnetwork2, 'accounturl': accounturl2}, + {'socialnetwork': socialnetwork3, 'accounturl': accounturl3}] + assert all([social in expected for social in user.socialnetworks]) # Add invalid social network account self.app.post('/auth/user_info/contacts/add_social_network', @@ -1292,8 +1341,10 @@ class TestPreferences(TestController): _session_id=self.app.cookies['_session_id'], )) user = M.User.query.get(username='test-admin') - assert len(user.socialnetworks) == 1 - assert {'socialnetwork': socialnetwork2, 'accounturl': accounturl2} in user.socialnetworks + assert len(user.socialnetworks) == 2 + expected = [{'socialnetwork': socialnetwork2, 'accounturl': accounturl2}, + {'socialnetwork': socialnetwork3, 'accounturl': accounturl3}] + assert all([social in expected for social in user.socialnetworks]) # Add telephone number telnumber = '+3902123456' diff --git a/Allura/development.ini b/Allura/development.ini index 171240692..8499ef58a 100644 --- a/Allura/development.ini +++ b/Allura/development.ini @@ -198,7 +198,8 @@ auth.allow_birth_date = true auth.allow_non_primary_email_password_reset = true auth.require_email_addr = true ; List of social network options to use on user account settings -socialnetworks = Facebook, Linkedin, Twitter, Google+ +socialnetworks = Facebook, Linkedin, Twitter, Instagram, Mastodon +allowed_social_domains = facebook.com, instagram.com, linkedin.com, twitter.com ; Allow uploading ssh key, optionally set ssh preferences url auth.allow_upload_ssh_key = false
