Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/redo user profile with kv #685

Merged
merged 24 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 1 addition & 10 deletions wafer/kv/tests/test_kv_api.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
"""Tests for wafer.kv api views."""

from django.contrib.auth.models import Group
from django.test import Client, TestCase

from rest_framework.test import APIClient

from wafer.kv.models import KeyValue
from wafer.tests.utils import create_user


def get_group(group):
return Group.objects.get(name=group)


def create_group(group):
return Group.objects.create(name=group)
from wafer.tests.utils import create_group, create_user, get_group


def create_kv_pair(name, value, group):
Expand Down
1 change: 1 addition & 0 deletions wafer/management/commands/wafer_add_default_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class Command(BaseCommand):
('talks', 'view_all_talks'),
),
'Registration': (),
'Online Profiles': (),
}

def add_wafer_groups(self):
Expand Down
26 changes: 16 additions & 10 deletions wafer/registration/sso.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,22 +119,28 @@ def github_sso(code):
log.warning('Error extracting github email address: %s', e)
raise SSOError('Failed to obtain email address from GitHub')

profile_fields = {
'github_username': login,
}
# TODO: Extend this to also set the github profile url KV
profile_fields = {}
if 'blog' in gh:
profile_fields['blog'] = gh['blog']

try:
user = get_user_model().objects.get(userprofile__github_username=login)
except MultipleObjectsReturned:
log.warning('Multiple accounts have GitHub username %s', login)
raise SSOError('Multiple accounts have GitHub username %s' % login)
except ObjectDoesNotExist:
user = None
group = Group.objects.get_by_natural_key('Registration')
user = None

for kv in KeyValue.objects.filter(
group=group, key='github_sso_account_id', value=login,
userprofile__isnull=False):
if kv.userprofile_set.count() > 1:
message = 'Multiple accounts have the same GitHub username %s'
log.warning(message, login)
raise SSOError(message % login)
user = kv.userprofile_set.first().user
break

user = sso(user=user, desired_username=login, name=name, email=email,
profile_fields=profile_fields)
user.userprofile.kv.get_or_create(group=group, key='github_sso_account_id',
defaults={'value': login})
return user


Expand Down
6 changes: 0 additions & 6 deletions wafer/schedule/templates/wafer.schedule/penta_schedule.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@
{% if user.is_staff %}
{# We will want finer grained control off this eventually, but staff will do for now #}
<person id="{{ author.pk }}"
{% if author.userprofile.twitter_handle %}
twitter="https://twitter.com/{{ author.userprofile.twitter_handle }}"
{% endif %}
contact="{{ author.email }}">{{ author.userprofile.display_name }}</person>
{% else %}
<person id="{{ author.pk }}">{{ author.userprofile.display_name }}</person>
Expand Down Expand Up @@ -100,9 +97,6 @@
{% if user.is_staff %}
{# We will want finer grained control off this eventually, but staff will do for now #}
<person id="{{ person.pk }}"
{% if author.userprofile.twitter_handle %}
twitter="https://twitter.com/{{ person.userprofile.twitter_handle }}"
{% endif %}
contact="{{ person.email }}">{{ person.userprofile.display_name }}</person>
{% else %}
<person id="{{ person.pk }}">{{ person.userprofile.display_name }}</person>
Expand Down
2 changes: 0 additions & 2 deletions wafer/schedule/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,6 @@ def get(self, request):
'name': person.userprofile.display_name(),
'email': person.email
}
if person.userprofile.twitter_handle:
person_data['twitter'] = person.userprofile.twitter_handle
sched_event['authors'].append(person_data)
sched_event['license'] = settings.WAFER_VIDEO_LICENSE
sched_event['license_url'] = settings.WAFER_VIDEO_LICENSE_URL
Expand Down
18 changes: 18 additions & 0 deletions wafer/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,21 @@

# Specify DEFAULT_AUTO_FIELD to make Django >= 3.2 happy
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

# List of social media sites that can be added to the user profile
# We assume people will enter approriate urls
# FIXME: Validation would be nice
# These are dictionaries of database/kv key and then friendly text
SOCIAL_MEDIA_ENTRIES = {
'twitter': _('Twitter Profile link'),
'fediverse': _('Fediverse Profile link'),
'other': _('Other Social'),
}

# List of code hosting sites that can be added to the user profile
# See above
CODE_HOSTING_ENTRIES = {
'github': _('github profile'),
'gitlab': _('gitlab profile'),
'bitbucket': _('bitbucket profile'),
}
8 changes: 8 additions & 0 deletions wafer/tests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
from django.contrib.auth.models import Group, Permission


def get_group(group):
return Group.objects.get(name=group)


def create_group(group):
return Group.objects.create(name=group)


def create_user(username, email=None, superuser=False, perms=(), groups=()):
if superuser:
create = get_user_model().objects.create_superuser
Expand Down
58 changes: 52 additions & 6 deletions wafer/users/forms.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.urls import reverse
from django.utils.translation import gettext as _
from django.conf import settings

from crispy_forms.bootstrap import PrependedText
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit
from crispy_forms.layout import HTML, Submit

from wafer.registration.validators import validate_username
from wafer.users.models import UserProfile
from wafer.users.models import UserProfile, PROFILE_GROUP


class UserForm(forms.ModelForm):
Expand Down Expand Up @@ -37,17 +39,61 @@ class Meta:
class UserProfileForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

pre_social_index = len(self.fields)

# currently everything I'm asking for is an url, and this adds some
# validation - we may need to revisit this later
for field_name in settings.SOCIAL_MEDIA_ENTRIES:
self.fields[field_name] = forms.URLField(label=settings.SOCIAL_MEDIA_ENTRIES[field_name],
max_length=1024, required=False)

pre_code_index = len(self.fields)

for field_name in settings.CODE_HOSTING_ENTRIES:
self.fields[field_name] = forms.URLField(label=settings.CODE_HOSTING_ENTRIES[field_name],
max_length=1024, required=False)

self.helper = FormHelper(self)
self.helper.include_media = False
username = kwargs['instance'].user.username
self.helper.form_action = reverse('wafer_user_edit_profile',
args=(username,))
self.helper['twitter_handle'].wrap(PrependedText,
'@', placeholder=_('handle'))
self.helper['github_username'].wrap(PrependedText,
'@', placeholder=_('username'))

# Add code hosting media header
# We do this in this order to avoid needing to do
# more maths
if settings.CODE_HOSTING_ENTRIES:
self.helper.layout.insert(pre_code_index, HTML(_('<p>Code Hosting Profiles</p>')))

# Add social media header
if settings.SOCIAL_MEDIA_ENTRIES:
self.helper.layout.insert(pre_social_index, HTML(_('<p>Social Profiles</p>')))

self.helper.add_input(Submit('submit', _('Save')))

def save(self):
"""We save the base profile, and then iterate over the
keys, adding/updating any new ones"""
# We don't currently delete keys that have been blamked,
# but perhaps we should.
profile = super().save()

group = Group.objects.get_by_natural_key(PROFILE_GROUP)

for field in settings.SOCIAL_MEDIA_ENTRIES:
if self.cleaned_data[field]:
profile.kv.get_or_create(group=group, key=field,
defaults={'value': self.cleaned_data[field]})

for field in settings.CODE_HOSTING_ENTRIES:
if self.cleaned_data[field]:
profile.kv.get_or_create(group=group, key=field,
defaults={'value': self.cleaned_data[field]})

return profile

class Meta:
model = UserProfile
exclude = ('user', 'kv')

21 changes: 21 additions & 0 deletions wafer/users/migrations/0004_remove_obselete_userprofile_entries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.1.1 on 2023-10-07 16:42

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("users", "0003_auto_20160329_2003"),
]

operations = [
migrations.RemoveField(
model_name="userprofile",
name="github_username",
),
migrations.RemoveField(
model_name="userprofile",
name="twitter_handle",
),
]
11 changes: 1 addition & 10 deletions wafer/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@
PROVISIONAL, CANCELLED)


# validate format of twitter handle
# Max 15 characters, alphanumeric and _ only
# Specification taken from https://support.twitter.com/articles/101299
TwitterValidator = RegexValidator('^[A-Za-z0-9_]{1,15}$',
'Incorrectly formatted twitter handle')
PROFILE_GROUP = 'Online Profiles'


class UserProfile(models.Model):
Expand All @@ -44,11 +40,6 @@ class Meta:
bio = models.TextField(_('bio'), null=True, blank=True)

homepage = models.CharField(_('homepage'), max_length=256, null=True, blank=True)
# We should probably do social auth instead
# And care about other code hosting sites...
twitter_handle = models.CharField(_('Twitter handle'), max_length=15, null=True, blank=True,
validators=[TwitterValidator])
github_username = models.CharField(_('GitHub username'), max_length=32, null=True, blank=True)
stefanor marked this conversation as resolved.
Show resolved Hide resolved

def __str__(self):
return u'%s' % self.user
Expand Down
12 changes: 0 additions & 12 deletions wafer/users/templates/wafer.users/profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,6 @@
{% endblock content %}
{% block extra_foot %}
<script type="text/javascript">
{% if profile.twitter_handle %}
// Twitter boilerplate
!function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (! d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = "//platform.twitter.com/widgets.js";
fjs.parentNode.insertBefore(js, fjs);
}
}(document, "script", "twitter-wjs");
{% endif %}

$("#profile-avatar [rel=popover]").attr("data-bs-content", $("#profile-avatar .popover-contents").html());
$("a[rel=popover]").popover();
Expand Down
20 changes: 6 additions & 14 deletions wafer/users/templates/wafer.users/snippets/profile_20-bio.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,12 @@ <h1>
</h1>
{% endblock name %}
{% block social %}
{% if profile.twitter_handle %}
<p>
<a href="https://twitter.com/{{ profile.twitter_handle }}" class="twitter-follow-button" data-bs-show-count="false">
{% blocktrans with handle=profile.twitter_handle %}Follow @{{ handle }}{% endblocktrans %}
</a>
</p>
{% endif %}
{% if profile.github_username %}
<p>
<a href="https://github.com/{{ profile.github_username }}">
{% blocktrans with username=profile.github_username %}GitHub: {{ username }}{% endblocktrans %}
</a>
</p>
{% endif %}
{% for tag, site_url in social_sites.items %}
<p><b>{{ tag }}</b>: {{ site_url }}</p>
{% endfor %}
{% for tag, site_url in code_sites.items %}
<p><b>{{ tag }}</b>: {{ site_url }}</p>
{% endfor %}
{% endblock social %}
{% endspaceless %}
</div>
Expand Down
5 changes: 4 additions & 1 deletion wafer/users/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

from django.test import Client, TestCase

from wafer.users.models import PROFILE_GROUP
from wafer.talks.models import ACCEPTED
from wafer.talks.tests.fixtures import create_talk
from wafer.tests.utils import create_user, mock_avatar_url
from wafer.tests.utils import create_group, create_user, mock_avatar_url


class UserProfileTests(TestCase):
Expand All @@ -17,6 +18,8 @@ def setUp(self):
# Create 2 users
create_user('test1')
create_user('test2')
# create kv group
create_group(PROFILE_GROUP)
# And a 3rd, with a talk
create_talk(title="Test talk", status=ACCEPTED, username='test3')
self.client = Client()
Expand Down
24 changes: 22 additions & 2 deletions wafer/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.contrib.auth.models import AnonymousUser, Group
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.urls import reverse
Expand All @@ -15,7 +15,7 @@
from wafer.talks.models import ACCEPTED, CANCELLED
from wafer.users.forms import UserForm, UserProfileForm
from wafer.users.serializers import UserSerializer
from wafer.users.models import UserProfile
from wafer.users.models import UserProfile, PROFILE_GROUP
from wafer.utils import PaginatedBuildableListView

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -91,6 +91,26 @@ def get_object(self, *args, **kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['can_edit'] = self.can_edit(context['object'])
# Add social and code profile info
group = Group.objects.get_by_natural_key(PROFILE_GROUP)

context['social_sites'] = {}
context['code_sites'] = {}

profile = self.get_object().userprofile

for field in settings.SOCIAL_MEDIA_ENTRIES:
if profile.kv.filter(group=group, key=field).exists():
value = profile.kv.get(group=group, key=field).value
if value:
context['social_sites'][settings.SOCIAL_MEDIA_ENTRIES[field]] = value

for field in settings.CODE_HOSTING_ENTRIES:
if profile.kv.filter(group=group, key=field).exists():
value = profile.kv.get(group=group, key=field).value
if value:
context['code_sites'][settings.CODE_HOSTING_ENTRIES[field]] = value

return context

def can_edit(self, user):
Expand Down