diff --git a/profiles/admin.py b/profiles/admin.py index 42df9f64a5eb1eee602566b6dfb6f14af1a0e8e6..27c8d6ec8a7032092072b89fd0c0a51e7e607142 100644 --- a/profiles/admin.py +++ b/profiles/admin.py @@ -4,10 +4,16 @@ __license__ = "AGPL v3" from django.contrib import admin -from .models import Profile +from .models import Profile, ProfileEmail + + +class ProfileEmailInline(admin.TabularInline): + model = ProfileEmail + extra = 0 class ProfileAdmin(admin.ModelAdmin): search_fields = ['first_name', 'last_name', 'email', 'orcid_id'] + inlines = [ProfileEmailInline] admin.site.register(Profile, ProfileAdmin) diff --git a/profiles/forms.py b/profiles/forms.py index c21dddc12f8d86649630c73faeed1cf798f985bf..f999e2d59908d6280425a3227c2c15d9965b3b91 100644 --- a/profiles/forms.py +++ b/profiles/forms.py @@ -4,36 +4,37 @@ __license__ = "AGPL v3" from django import forms -from .models import Profile, AlternativeEmail +from .models import Profile, ProfileEmail class ProfileForm(forms.ModelForm): + email = forms.EmailField() class Meta: model = Profile - fields = ['title', 'first_name', 'last_name', 'email', + fields = ['title', 'first_name', 'last_name', 'discipline', 'expertises', 'orcid_id', 'webpage', 'accepts_SciPost_emails', 'accepts_refereeing_requests'] def clean_email(self): - """ - Check that the email isn't yet associated to an existing Profile - (via either the email field or the m2m-related alternativeemails). - """ + """Check that the email isn't yet associated to an existing Profile.""" cleaned_email = self.cleaned_data['email'] - if Profile.objects.filter(email=cleaned_email).exists(): - raise forms.ValidationError( - 'A Profile with this email (as primary email) already exists.') - elif AlternativeEmail.objects.filter(email=cleaned_email).exists(): - raise forms.ValidationError( - 'A Profile with this email (as alternative email) already exists.') + if self.instance.emails.filter(email=cleaned_email).exists(): + raise forms.ValidationError('A Profile with this email already exists.') return cleaned_email + def save(self): + profile = super().save() + ProfileEmail.objects.create( + profile=profile, + email=self.cleaned_data['email'], + primary=True) + -class AlternativeEmailForm(forms.ModelForm): +class ProfileEmailForm(forms.ModelForm): class Meta: - model = AlternativeEmail + model = ProfileEmail fields = ['email', 'still_valid'] def __init__(self, *args, **kwargs): diff --git a/profiles/migrations/0007_auto_20181002_1104.py b/profiles/migrations/0007_auto_20181002_1104.py new file mode 100644 index 0000000000000000000000000000000000000000..623a787ff328cf145c7bae424b89171998d22e56 --- /dev/null +++ b/profiles/migrations/0007_auto_20181002_1104.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-10-02 09:04 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0006_populate_profile_from_reginv_and_refinv'), + ] + + operations = [ + migrations.RenameModel('AlternativeEmail', 'ProfileEmail') + ] diff --git a/profiles/migrations/0008_auto_20181002_1106.py b/profiles/migrations/0008_auto_20181002_1106.py new file mode 100644 index 0000000000000000000000000000000000000000..5b27f6c1e3402f53226ca8cc3ac66ec7aeaa650d --- /dev/null +++ b/profiles/migrations/0008_auto_20181002_1106.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-10-02 09:06 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0007_auto_20181002_1104'), + ] + + operations = [ + migrations.AlterField( + model_name='profileemail', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='emails', to='profiles.Profile'), + ), + ] diff --git a/profiles/migrations/0009_profileemail_primary.py b/profiles/migrations/0009_profileemail_primary.py new file mode 100644 index 0000000000000000000000000000000000000000..b5cfba402970543c38109337db33dc43f273d65d --- /dev/null +++ b/profiles/migrations/0009_profileemail_primary.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-10-02 09:10 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0008_auto_20181002_1106'), + ] + + operations = [ + migrations.AddField( + model_name='profileemail', + name='primary', + field=models.BooleanField(default=False), + ), + ] diff --git a/profiles/migrations/0010_auto_20181002_1114.py b/profiles/migrations/0010_auto_20181002_1114.py new file mode 100644 index 0000000000000000000000000000000000000000..b09adae8e3298a1cd6356b05a17e59d0ffc6b444 --- /dev/null +++ b/profiles/migrations/0010_auto_20181002_1114.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2018-10-02 09:14 +from __future__ import unicode_literals + +from django.db import migrations + + +def add_primary_emails(apps, schema_editor): + Profile = apps.get_model('profiles', 'Profile') + ProfileEmail = apps.get_model('profiles', 'ProfileEmail') + for profile in Profile.objects.all(): + ProfileEmail.objects.get_or_create( + profile=profile, + email=profile.email, + defaults={'primary': True}) + + +class Migration(migrations.Migration): + + dependencies = [ + ('profiles', '0009_profileemail_primary'), + ] + + operations = [ + migrations.RunPython(add_primary_emails) + ] diff --git a/profiles/models.py b/profiles/models.py index c7a9f9d08c9332e15522eb64bb45160ee41ef960..3ed472d088f53fdbd19a38d46724016adc9ee4eb 100644 --- a/profiles/models.py +++ b/profiles/models.py @@ -38,7 +38,6 @@ class Profile(models.Model): title = models.CharField(max_length=4, choices=TITLE_CHOICES) first_name = models.CharField(max_length=64) last_name = models.CharField(max_length=64) - email = models.EmailField() discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default=DISCIPLINE_PHYSICS, verbose_name='Main discipline') expertises = ChoiceArrayField( @@ -59,19 +58,19 @@ class Profile(models.Model): def __str__(self): return '%s, %s %s' % (self.last_name, self.get_title_display(), self.first_name) + @property + def email(self): + return self.emails.filter(primary=True).first() -class AlternativeEmail(models.Model): - """ - It is often the case that somebody has multiple email addresses. - The 'email' field of a given Profile, Contributor or other person-based object - is then interpreted as representing the current address. - An AlternativeEmail is bound to a Profile and holds info on eventual - secondary email addresses. - """ + +class ProfileEmail(models.Model): + """Any email related to a Profile instance.""" profile = models.ForeignKey(Profile, on_delete=models.CASCADE) email = models.EmailField() still_valid = models.BooleanField(default=True) + primary = models.BooleanField(default=False) class Meta: unique_together = ['profile', 'email'] + ordering = ['-primary', '-still_valid', 'email'] default_related_name = 'emails' diff --git a/profiles/templates/profiles/_profile_card.html b/profiles/templates/profiles/_profile_card.html index 58412b5678836cb9864ea6b80eafa6296cdb6fb1..bead97e5501d7444aafae3d35ad4fc7e3f3f0222 100644 --- a/profiles/templates/profiles/_profile_card.html +++ b/profiles/templates/profiles/_profile_card.html @@ -11,12 +11,29 @@ <tr> <td>Email(s)</td> <td> - <ul> - <li>{{ profile.email }} (current)</li> - {% for altemail in profile.emails.all %} - <li>{{ altemail.email }} (alt, {{ altemail.still_valid|yesno:"still valid,deprecated" }})</li> - {% endfor %} - </ul> + <table class="table table-sm table-borderless"> + <thead> + <tr> + <th colspan="2">Email</th> + <th>Still valid</th> + <th></th> + </tr> + </thead> + {% for profile_mail in profile.emails.all %} + <tr> + <td>{% if profile_mail.primary %}<strong>{% endif %}{{ profile_mail.email }}{% if profile_mail.primary %}</strong>{% endif %}</td> + <td>{{ profile_mail.primary|yesno:'Primary,Alternative' }}</td> + <td> + <i class="fa {{ profile_mail.still_valid|yesno:'fa-check-circle text-success,fa-times-circle text-danger' }}"></i> + + </td> + <td class="d-flex"> + <form method="post" action="{% url 'profiles:toggle_email_status' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link p-0">{{ profile_mail.still_valid|yesno:'Deprecate,Mark valid' }}</button></form> + <form method="post" action="{% url 'profiles:delete_profile_email' profile_mail.id %}">{% csrf_token %}<button type="submit" class="btn btn-link text-danger p-0 ml-2" onclick="return confirm('Sure you want to delete {{ profile_mail.email }}?')"><i class="fa fa-trash"></i></button></form> + </td> + </tr> + {% endfor %} + </table> </td> </tr> <tr> @@ -43,10 +60,10 @@ <li><a href="{% url 'profiles:profile_delete' pk=profile.id %}">Delete</a> this Profile</li> <li> <div> - Add an alternative email to this Profile: - <form action="{% url 'profiles:add_alternative_email' profile_id=profile.id %}" method="post"> + Add an email to this Profile: + <form action="{% url 'profiles:add_profile_email' profile_id=profile.id %}" method="post"> {% csrf_token %} - {{ alternative_email_form|bootstrap }} + {{ email_form|bootstrap }} <input class="btn btn-outline-secondary" type="submit" value="Add"> </form> </div> diff --git a/profiles/urls.py b/profiles/urls.py index ef45fa9392863712d0b6ba14d582ae5aa946190f..1160bd39ec425b461d06a7b405fe15e90f6e5fb0 100644 --- a/profiles/urls.py +++ b/profiles/urls.py @@ -33,8 +33,18 @@ urlpatterns = [ name='profiles' ), url( - r'^(?P<profile_id>[0-9]+)/add_alternative_email/$', - views.add_alternative_email, - name='add_alternative_email' + r'^(?P<profile_id>[0-9]+)/add_email$', + views.add_profile_email, + name='add_profile_email' + ), + url( + r'^(?P<email_id>[0-9]+)/toggle$', + views.toggle_email_status, + name='toggle_email_status' + ), + url( + r'^(?P<email_id>[0-9]+)/delete$', + views.delete_profile_email, + name='delete_profile_email' ), ] diff --git a/profiles/views.py b/profiles/views.py index c1a5fed7e6f67d38f4eea3bb27c2c7520a55d7f4..d447b1676d32a2b59e5fe93c08593e925032bdfd 100644 --- a/profiles/views.py +++ b/profiles/views.py @@ -6,6 +6,7 @@ from django.contrib import messages from django.core.urlresolvers import reverse, reverse_lazy from django.db import IntegrityError from django.shortcuts import get_object_or_404, render, redirect +from django.views.decorators.http import require_POST from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.list import ListView @@ -18,8 +19,8 @@ from scipost.models import Contributor from invitations.models import RegistrationInvitation from submissions.models import RefereeInvitation -from .models import Profile, AlternativeEmail -from .forms import ProfileForm, AlternativeEmailForm, SearchTextForm +from .models import Profile, ProfileEmail +from .forms import ProfileForm, ProfileEmailForm, SearchTextForm @@ -138,22 +139,42 @@ class ProfileListView(PermissionsMixin, PaginationMixin, ListView): 'next_refinv_wo_profile': refinv_wo_profile.first(), 'nr_reginv_wo_profile': reginv_wo_profile.count(), 'next_reginv_wo_profile': reginv_wo_profile.first(), - 'alternative_email_form': AlternativeEmailForm(), + 'email_form': ProfileEmailForm(), }) return context @permission_required('scipost.can_create_profiles') -def add_alternative_email(request, profile_id): +def add_profile_email(request, profile_id): """ - Add an alternative email address to a Profile. + Add an email address to a Profile. """ profile = get_object_or_404(Profile, pk=profile_id) - form = AlternativeEmailForm(request.POST or None, profile=profile) + form = ProfileEmailForm(request.POST or None, profile=profile) if form.is_valid(): form.save() - messages.success(request, 'Alternative email successfully added.') + messages.success(request, 'Email successfully added.') else: for field, err in form.errors.items(): messages.warning(request, err[0]) return redirect(reverse('profiles:profiles')) + + +@require_POST +@permission_required('scipost.can_create_profiles') +def toggle_email_status(request, email_id): + """Toggle valid/deprecated status of ProfileEmail.""" + profile_email = get_object_or_404(ProfileEmail, pk=email_id) + ProfileEmail.objects.filter(id=email_id).update(still_valid=not profile_email.still_valid) + messages.success(request, 'Email updated') + return redirect('profiles:profiles') + + +@require_POST +@permission_required('scipost.can_create_profiles') +def delete_profile_email(request, email_id): + """Delete ProfileEmail.""" + profile_email = get_object_or_404(ProfileEmail, pk=email_id) + profile_email.delete() + messages.success(request, 'Email deleted') + return redirect('profiles:profiles') diff --git a/submissions/admin.py b/submissions/admin.py index b0a33f8670ed092cddd9ad13a3c336a64d2120df..fa235416b9ffe5ce731bd4bc62072f579a356ef0 100644 --- a/submissions/admin.py +++ b/submissions/admin.py @@ -3,7 +3,6 @@ __license__ = "AGPL v3" from django.contrib import admin -# from django.contrib.contenttypes.admin import GenericTabularInline from django import forms from guardian.admin import GuardedModelAdmin