diff --git a/SciPost_v1/urls.py b/SciPost_v1/urls.py index 7b29434e2aae982aa946d1363cfefcf35d4f4d79..13711bb780de959a0325ff40ee66840435e020c2 100644 --- a/SciPost_v1/urls.py +++ b/SciPost_v1/urls.py @@ -1,18 +1,3 @@ -"""SciPost_v1 URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/1.8/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') -Including another URLconf - 1. Add an import: from blog import urls as blog_urls - 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) -""" from django.conf import settings from django.conf.urls import include, url from django.contrib import admin diff --git a/partners/forms.py b/partners/forms.py index 32e4956b75281473bfb567a54da5ed1266126017..3f7b2d9f53fb14c9805a44a9ec212e9b48734535 100644 --- a/partners/forms.py +++ b/partners/forms.py @@ -1,5 +1,7 @@ from django import forms from django.contrib.auth.models import User +from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError from django.db.models import Q from captcha.fields import ReCaptchaField @@ -14,6 +16,37 @@ from .models import Partner, ProspectivePartner, ProspectiveContact, Prospective from scipost.models import TITLE_CHOICES +class ActivationForm(forms.ModelForm): + class Meta: + model = User + fields = [] + + password_new = forms.CharField(label='* Password', widget=forms.PasswordInput()) + password_verif = forms.CharField(label='* Verify password', widget=forms.PasswordInput(), + help_text='Your password must contain at least 8 characters') + + def clean_password(self): + password = self.cleaned_data.get('password_new', '') + try: + validate_password(password, self.instance) + except ValidationError as error_message: + self.add_error('password_new', error_message) + return password + + def clean_password_verif(self): + if self.cleaned_data.get('password_new', '') != self.cleaned_data.get('password_verif', ''): + self.add_error('password_verif', 'Your password entries must match') + return self.cleaned_data.get('password_verif', '') + + def activate_user(self): + if self.errors: + return forms.ValidationError + self.instance.is_active = True + self.instance.set_password(self.cleaned_data['password_new']) + self.instance.save() + return self.instance + + class InstitutionForm(forms.ModelForm): class Meta: model = Institution @@ -123,6 +156,7 @@ class NewContactForm(ContactForm): title=self.cleaned_data['title'], kind=self.cleaned_data['kind'] ) + contact.generate_key() contact.save() contact.partners.add(self.partner) # TODO: Send mail to contact to let him/her activate account diff --git a/partners/migrations/0016_auto_20170624_0905.py b/partners/migrations/0016_auto_20170624_0905.py new file mode 100644 index 0000000000000000000000000000000000000000..e44535151dfb413e5aea7db04c29a82f36dea374 --- /dev/null +++ b/partners/migrations/0016_auto_20170624_0905.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2017-06-24 07:05 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('partners', '0015_auto_20170620_1634'), + ] + + operations = [ + migrations.AddField( + model_name='contact', + name='activation_key', + field=models.CharField(blank=True, max_length=40), + ), + migrations.AddField( + model_name='contact', + name='key_expires', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterField( + model_name='membershipagreement', + name='partner', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='agreements', to='partners.Partner'), + ), + migrations.AlterField( + model_name='partner', + name='status', + field=models.CharField(choices=[('Initiated', 'Initiated'), ('Contacted', 'Contacted'), ('Negotiating', 'Negotiating'), ('Uninterested', 'Uninterested'), ('Active', 'Active'), ('Inactive', 'Inactive')], default='Initiated', max_length=16), + ), + migrations.AlterField( + model_name='prospectivecontact', + name='prospartner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prospective_contacts', to='partners.ProspectivePartner'), + ), + ] diff --git a/partners/models.py b/partners/models.py index 5989cd4abc133c8fffa644bd2073f5cb5aed4de9..1b6b277084f4e91f642e80a608fcbf0307d714de 100644 --- a/partners/models.py +++ b/partners/models.py @@ -1,5 +1,11 @@ +import datetime +import hashlib +import random +import string + from django.contrib.auth.models import User from django.db import models +from django.utils import timezone from django_countries.fields import CountryField @@ -126,10 +132,23 @@ class Contact(models.Model): consortia = models.ManyToManyField('partners.Consortium', blank=True, help_text=('All Consortia for which the Contact has' ' explicit permission to view/edit its data.')) + activation_key = models.CharField(max_length=40, blank=True) + key_expires = models.DateTimeField(default=timezone.now) def __str__(self): return '%s %s, %s' % (self.get_title_display(), self.user.last_name, self.user.first_name) + def generate_key(self, feed=''): + """ + Generate and save a new activation_key for the Contact, given a certain feed. + """ + for i in range(5): + feed += random.choice(string.ascii_letters) + feed = feed.encode('utf8') + salt = self.user.username.encode('utf8') + self.activation_key = hashlib.sha1(salt+salt).hexdigest() + self.key_expires = datetime.datetime.now() + datetime.timedelta(days=2) + def delete_or_remove_partner(self, partner, *args, **kwargs): """ Custom `delete` method as the contact does not always need to be deleted, diff --git a/partners/templates/partners/activate_account.html b/partners/templates/partners/activate_account.html new file mode 100644 index 0000000000000000000000000000000000000000..0e3aef12f1e53ddb919c6a2fc5f06620e70f6f36 --- /dev/null +++ b/partners/templates/partners/activate_account.html @@ -0,0 +1,31 @@ +{% extends 'scipost/base.html' %} + + +{% block pagetitle %}{{block.super}} Activate Account{% endblock pagetitle %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Activate Account</h1> + </div> + <div class="col-md-8 offset-md-2"> + <h2>{{contact.get_title_display}} {{contact.user.first_name}} {{contact.user.last_name}}</h2> + <h3>{{contact.user.email}}</h3> + </div> +</div> + +<div class="row"> + <div class="col-md-8 offset-md-2 mb-5"> + <form method="post"> + {% csrf_token %} + {{ form|bootstrap }} + + <input class="btn btn-primary" type="submit" value="Activate"/> + </form> + </div> +</div> + +{% endblock content %} diff --git a/partners/urls.py b/partners/urls.py index 3c3e1d14fc31304bd9cb181604353b4e7ea66f43..6faf56a0a6eb3e180c35e5dfaab2d70170e77761 100644 --- a/partners/urls.py +++ b/partners/urls.py @@ -23,6 +23,9 @@ urlpatterns = [ url(r'institutions/(?P<institution_id>[0-9]+)/edit$', views.institution_edit, name='institution_edit'), + # Users + url(r'activate/(?P<activation_key>.+)$', views.activate_account, name='activate_account'), + # Partners url(r'(?P<partner_id>[0-9]+)/edit$', views.partner_edit, name='partner_edit'), url(r'(?P<partner_id>[0-9]+)/contacts/add$', views.partner_add_contact, diff --git a/partners/views.py b/partners/views.py index 6d0265f2042387775ccae563f2ebf3af7bedf3c4..423fceaa671d7248d1267e6577ebac4eb18d9bfd 100644 --- a/partners/views.py +++ b/partners/views.py @@ -15,7 +15,7 @@ from .forms import ProspectivePartnerForm, ProspectiveContactForm,\ EmailProspectivePartnerContactForm, PromoteToPartnerForm,\ ProspectivePartnerEventForm, MembershipQueryForm, PromoteToContactForm,\ PromoteToContactFormset, PartnerForm, ContactForm, ContactFormset,\ - NewContactForm, InstitutionForm + NewContactForm, InstitutionForm, ActivationForm from .utils import PartnerUtils @@ -29,6 +29,7 @@ def supporting_partners(request): return render(request, 'partners/supporting_partners.html', context) +# @login_required @permission_required('scipost.can_read_personal_page', return_403=True) def dashboard(request): ''' @@ -238,3 +239,24 @@ def add_prospartner_event(request, prospartner_id): return render(request, 'scipost/error.html', {'errormessage': errormessage}) errormessage = 'This view can only be posted to.' return render(request, 'scipost/error.html', {'errormessage': errormessage}) + + +######### +# Account +######### +def activate_account(request, activation_key): + contact = get_object_or_404(Contact, user__is_active=False, + activation_key=activation_key, + user__email__icontains=request.GET.get('email', None)) + + # TODO: Key Expires fallback + form = ActivationForm(request.POST or None, instance=contact.user) + if form.is_valid(): + form.activate_user() + messages.success(request, '<h3>Thank you for registration</h3>.') + return redirect(reverse('partners:dashboard')) + context = { + 'contact': contact, + 'form': form + } + return render(request, 'partners/activate_account.html', context) diff --git a/scipost/views.py b/scipost/views.py index 229123403a324cc62918110819e7349e0045dc0b..2bba5fd6a3d6d03a0b48c64499743e8fd4720a51 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -46,7 +46,11 @@ from theses.models import ThesisLink ############## def is_registered(user): - return user.groups.filter(name='Registered Contributors').exists() + if user.groups.filter(name='Registered Contributors').exists(): + return True + if user.partner_contact: + return True + return False # Global search