You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by di...@apache.org on 2023/01/12 20:42:15 UTC

[allura] 01/02: [#8484] added fediverse and instagram form fields for projects

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 <gu...@slashdotmedia.com>
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 = '@username@server.social'
+        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