diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..8319823 --- /dev/null +++ b/accounts/__init__.py @@ -0,0 +1 @@ +default_app_config = 'accounts.apps.AccountsConfig' diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..f4628d2 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + name = 'accounts' + + def ready(self): + import accounts.signals # noqa diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..2c3618e --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,20 @@ +from django.contrib.auth.forms import AuthenticationForm +from django.utils.translation import ugettext_lazy as _ +from django.forms.models import modelform_factory + + +from .models import User, Profile + + +# email MUST be validated, we do not allow to edit it +UserForm = modelform_factory(User, fields=['first_name', 'last_name', 'username']) + +ProfileForm = modelform_factory(Profile, fields=[ + 'phone_number', 'biography', 'twitter', 'website', + 'linkedin', 'facebook', 'mastodon']) + + +class EmailAuthenticationForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['username'].label = _('Email address') diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..130f19d --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.1 on 2017-11-18 20:14 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +def profile_forward(apps, schema_editor): + User = apps.get_model(settings.AUTH_USER_MODEL) + Profile = apps.get_model("accounts", "Profile") + db_alias = schema_editor.connection.alias + for user in User.objects.using(db_alias).all(): + Profile.objects.using(db_alias).get_or_create(user=user) + + +def profile_backward(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone_number', models.CharField(blank=True, default='', max_length=16, verbose_name='Phone number')), + ('sms_prefered', models.BooleanField(default=False, verbose_name='SMS prefered')), + ('biography', models.TextField(blank=True, verbose_name='Biography')), + ('twitter', models.CharField(blank=True, default='', max_length=100, verbose_name='Twitter')), + ('linkedin', models.CharField(blank=True, default='', max_length=100, verbose_name='LinkedIn')), + ('github', models.CharField(blank=True, default='', max_length=100, verbose_name='Github')), + ('website', models.CharField(blank=True, default='', max_length=100, verbose_name='Website')), + ('facebook', models.CharField(blank=True, default='', max_length=100, verbose_name='Facebook')), + ('mastodon', models.CharField(blank=True, default='', max_length=100, verbose_name='Mastodon')), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.RunPython(profile_forward, profile_backward), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..146f0fe --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,25 @@ +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.db import models +from django.utils.translation import ugettext_lazy as _ + + +class Profile(models.Model): + + user = models.OneToOneField(User) + phone_number = models.CharField(max_length=16, blank=True, default='', verbose_name=_('Phone number')) + sms_prefered = models.BooleanField(default=False, verbose_name=_('SMS prefered')) + biography = models.TextField(blank=True, verbose_name=_('Biography')) + + twitter = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Twitter')) + linkedin = models.CharField(max_length=100, blank=True, default='', verbose_name=_('LinkedIn')) + github = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Github')) + website = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Website')) + facebook = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Facebook')) + mastodon = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Mastodon')) + + def __str__(self): + return self.user.get_full_name() or self.user.username + + def get_absolute_url(self): + return reverse('profile') diff --git a/accounts/signals.py b/accounts/signals.py new file mode 100644 index 0000000..ed9d24b --- /dev/null +++ b/accounts/signals.py @@ -0,0 +1,35 @@ +from django.contrib import messages +#from django.contrib.auth.models import User +from django.contrib.auth.signals import user_logged_in, user_logged_out +#from django.contrib.sites.shortcuts import get_current_site +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ +#from django.utils.translation import ugettext_noop + +from ponyconf.decorators import disable_for_loaddata + +from .models import User, Profile + + +@receiver(user_logged_in) +def on_user_logged_in(sender, request, user, **kwargs): + #participation, created = Participation.objects.get_or_create(user=user, site=get_current_site(request)) + #if user.is_superuser: + # participation.orga = True + # participation.save() + #if created: + # messages.info(request, "Please check your profile!\n", fail_silently=True) # FIXME + messages.success(request, _('Welcome!'), fail_silently=True) # FIXME + + +@receiver(user_logged_out) +def on_user_logged_out(sender, request, **kwargs): + messages.success(request, _('Goodbye!'), fail_silently=True) # FIXME + + +@receiver(post_save, sender=User, weak=False, dispatch_uid='create_profile') +@disable_for_loaddata +def create_profile(sender, instance, created, **kwargs): + if created: + Profile.objects.create(user=instance) diff --git a/accounts/templates/accounts/profile.html b/accounts/templates/accounts/profile.html new file mode 100644 index 0000000..c1ae638 --- /dev/null +++ b/accounts/templates/accounts/profile.html @@ -0,0 +1,41 @@ +{% extends 'base.html' %} + +{% load bootstrap3 i18n %} + +{% block profiletab %} class="active"{% endblock %} + + +{% block content %} + +
+
+

{% trans "Profile" %}

+
+
+
+ {% csrf_token %} + {% for form in forms %} + {% bootstrap_form form layout="horizontal" %} + {% endfor %} + {% buttons layout="horizontal" %} + + {% for url, class, text in buttons %} + {{ text }} + {% endfor %} + {% trans "Cancel" %} + {% endbuttons %} +
+
+
+ +{% endblock %} + +{% block css %} +{{ block.super }} +{% for form in forms %}{{ form.media.css }}{% endfor %} +{% endblock %} + +{% block js_end %} +{{ block.super }} +{% for form in forms %}{{ form.media.js }}{% endfor %} +{% endblock %} diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..fe35a6d --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,15 @@ +from django.conf import settings +from django.conf.urls import include, url +from django.contrib.auth import views as auth_views + +from . import views + +urlpatterns = [ + url(r'^profile/$', views.profile, name='profile'), + url(r'accounts/login/', views.EmailLoginView.as_view(), {'extra_context': {'buttons': [views.RESET_PASSWORD_BUTTON]}}, name='login'), + #url(r'^login/$', auth_views.login, {'extra_context': {'buttons': [views.RESET_PASSWORD_BUTTON]}}, name='login'), + url(r'^logout/$', auth_views.logout, {'next_page': settings.LOGOUT_REDIRECT_URL}, name='logout'), + #url(r'^avatar/', include('avatar.urls')), + url(r'', include('django.contrib.auth.urls')), + #url(r'', include('registration.backends.default.urls')), +] diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..f3694c7 --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,34 @@ +from django.contrib.auth.decorators import login_required +from django.contrib.auth.views import LoginView +from django.utils.translation import ugettext as _ +from django.shortcuts import redirect, render +from django.contrib import messages + +from accounts.models import User, Profile +from accounts.forms import UserForm, ProfileForm, EmailAuthenticationForm + + +RESET_PASSWORD_BUTTON = ('password_reset', 'warning', _('Reset your password')) +CHANGE_PASSWORD_BUTTON = ('password_change', 'warning', _('Change password')) + + +class EmailLoginView(LoginView): + authentication_form = EmailAuthenticationForm + + +@login_required +def profile(request): + user_form = UserForm(request.POST or None, instance=request.user) + profile_form = ProfileForm(request.POST or None, instance=request.user.profile) + forms = [user_form, profile_form] + if request.method == 'POST': + if all(map(lambda form: form.is_valid(), forms)): + for form in forms: + form.save() + messages.success(request, _('Profile updated successfully.')) + else: + messages.error(request, _('Please correct those errors.')) + return render(request, 'accounts/profile.html', { + 'forms': forms, + 'buttons': [CHANGE_PASSWORD_BUTTON], + }) diff --git a/cfp/urls.py b/cfp/urls.py index f2b9c79..b2cce0f 100644 --- a/cfp/urls.py +++ b/cfp/urls.py @@ -27,10 +27,10 @@ urlpatterns = [ url(r'^cfp/(?P[\w\-]+)/(?P[\w\-]+)/confirm/$', views.talk_acknowledgment, {'confirm': True}, name='talk-confirm'), url(r'^cfp/(?P[\w\-]+)/(?P[\w\-]+)/desist/$', views.talk_acknowledgment, {'confirm': False}, name='talk-desist'), # End backward compatibility - url(r'^volunteer/$', views.volunteer_enrole, name='volunteer-enrole'), - url(r'^volunteer/(?P[\w\-]+)/$', views.volunteer_home, name='volunteer-home'), - url(r'^volunteer/(?P[\w\-]+)/join/(?P[\w\-]+)/$', views.volunteer_update_activity, {'join': True}, name='volunteer-join'), - url(r'^volunteer/(?P[\w\-]+)/quit/(?P[\w\-]+)/$', views.volunteer_update_activity, {'join': False}, name='volunteer-quit'), + url(r'^volunteer/enrole/$', views.volunteer_enrole, name='volunteer-enrole'), + url(r'^volunteer/(?:(?P[\w\-]+)/)?$', views.volunteer_home, name='volunteer-home'), + url(r'^volunteer/(?:(?P[\w\-]+)/)?join/(?P[\w\-]+)/$', views.volunteer_update_activity, {'join': True}, name='volunteer-join'), + url(r'^volunteer/(?:(?P[\w\-]+)/)?quit/(?P[\w\-]+)/$', views.volunteer_update_activity, {'join': False}, name='volunteer-quit'), #url(r'^talk/(?P[\w\-]+)/$', views.talk_show, name='show-talk'), #url(r'^speaker/(?P[\w\-]+)/$', views.speaker_show, name='show-speaker'), url(r'^staff/$', views.staff, name='staff'), diff --git a/cfp/views.py b/cfp/views.py index 28d2495..a133a2e 100644 --- a/cfp/views.py +++ b/cfp/views.py @@ -49,9 +49,10 @@ def volunteer_enrole(request): if Volunteer.objects.filter(site=request.conference.site, email=request.user.email).exists(): return redirect(reverse('volunteer-home')) elif not request.POST: - # TODO: import biography, phone number and sms_prefered from User profile initial.update({ 'name': request.user.get_full_name(), + 'phone_number': request.user.profile.phone_number, + 'sms_prefered': request.user.profile.sms_prefered, }) form = VolunteerForm(request.POST or None, initial=initial, conference=request.conference) if request.user.is_authenticated(): @@ -174,9 +175,9 @@ def proposal_home(request): if Participant.objects.filter(site=request.conference.site, email=request.user.email).exists(): return redirect(reverse('proposal-dashboard')) elif not request.POST: - # TODO: import biography from User profile initial.update({ 'name': request.user.get_full_name(), + 'biography': request.user.profile.biography, }) fields.remove('email') NewSpeakerForm = modelform_factory(Participant, form=ParticipantForm, fields=fields) diff --git a/ponyconf/settings.py b/ponyconf/settings.py index 7f34d22..49ceca3 100644 --- a/ponyconf/settings.py +++ b/ponyconf/settings.py @@ -37,8 +37,8 @@ INSTALLED_APPS = [ 'django.contrib.sites', # our apps - #'accounts', 'ponyconf', + 'accounts', 'cfp', 'mailing', #'planning', diff --git a/ponyconf/templates/base.html b/ponyconf/templates/base.html index 0ea8ea2..e46fd43 100644 --- a/ponyconf/templates/base.html +++ b/ponyconf/templates/base.html @@ -43,12 +43,7 @@  {% trans "Organisation" %}  {% trans "Administration" %} {% endif %} - {% comment %} -  {% trans "Talks" %} -  {% trans "Speakers" %} -  Inbox  {% trans "Profile" %} - {% endcomment %}
  •  {% trans "Logout" %}
  • {% else %}  {% trans "Staff" %} diff --git a/ponyconf/urls.py b/ponyconf/urls.py index 0402eb7..260dc01 100644 --- a/ponyconf/urls.py +++ b/ponyconf/urls.py @@ -15,26 +15,12 @@ Including another URLconf """ from django.conf.urls import include, url from django.contrib import admin -from django.contrib.auth.views import LoginView -from django.contrib.auth.forms import AuthenticationForm -from django.utils.translation import ugettext_lazy as _ from django.conf import settings -class EmailAuthenticationForm(AuthenticationForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['username'].label = _('Email address') - - -class EmailLoginView(LoginView): - authentication_form = EmailAuthenticationForm - - urlpatterns = [ url(r'^admin/django/', admin.site.urls), - url(r'accounts/login/', EmailLoginView.as_view()), - url(r'accounts/', include('django.contrib.auth.urls')), + url(r'^accounts/', include('accounts.urls')), url(r'^', include('cfp.urls')), ]