ljf 7 years ago
parent
commit
a49947cd1a

+ 5 - 22
README.md

@@ -50,7 +50,7 @@ Install dependencies. On Debian, you will probably need the
 `python-dev`, `python-pip`, `libldap-dev`, `libpq-dev`, `libsasl2-dev`,
 `python-dev`, `python-pip`, `libldap-dev`, `libpq-dev`, `libsasl2-dev`,
 and `libjpeg-dev` packages.
 and `libjpeg-dev` packages.
 
 
-    sudo apt-get install python-dev python-pip libldap2-dev libpq-dev libsasl2-dev libjpeg-dev libxml2-dev libxslt1-dev libffi-dev
+    sudo apt-get install python-dev python-pip libldap2-dev libpq-dev libsasl2-dev libjpeg-dev libxml2-dev libxslt1-dev libffi-dev python-cairo libpango1.0-0
 
 
 You need a recent *pip* for the installation of dependencies to work. If you
 You need a recent *pip* for the installation of dependencies to work. If you
 don't meet that requirement (Ubuntu trusty does not), run:
 don't meet that requirement (Ubuntu trusty does not), run:
@@ -337,27 +337,10 @@ List of available settings in your `settings_local.py` file.
 - `SUBSCRIPTION_REFERENCE`: Pattern used to display a unique reference for any subscription. Helpful for bank wire transfer identification
 - `SUBSCRIPTION_REFERENCE`: Pattern used to display a unique reference for any subscription. Helpful for bank wire transfer identification
 - `PAYMENT_DELAY`: Payment delay in days for issued invoices ( default is 30 days which is the default in french law)
 - `PAYMENT_DELAY`: Payment delay in days for issued invoices ( default is 30 days which is the default in french law)
 - `MEMBER_CAN_EDIT_PROFILE`: Allow members to edit their profiles
 - `MEMBER_CAN_EDIT_PROFILE`: Allow members to edit their profiles
-
-
-Accounting logs
----------------
-
-To log 'accounting-related operations' (creation/update of invoice, payment
-and member balance) to a specific file, add the following to settings_local.py :
-
-```
-from settings_base import *
-LOGGING["formatters"]["verbose"] = {'format': "%(asctime)s - %(name)s - %(levelname)s - %(message)s"}
-LOGGING["handlers"]["coin_accounting"] = {
-    'level':'INFO',
-    'class':'logging.handlers.RotatingFileHandler',
-    'formatter': 'verbose',
-    'filename': '/var/log/coin/accounting.log',
-    'maxBytes': 1024*1024*15, # 15MB
-    'backupCount': 10,
-}
-LOGGING["loggers"]["coin.billing"]["handlers"] = [ 'coin_accounting' ]
-```
+- `REGISTRATION_OPEN` : Allow visitor to join the association by register on COIN
+- `ACCOUNT_ACTIVATION_DAYS` : All account with unvalidated email will be deleted after X days
+- `MEMBERSHIP_REFERENCE` : Template string to display the label the member should indicates for the bank transfer, default: "ADH-{{ user.pk }}"
+- `MEMBER_DEFAULT_COTISATION` : Default membership fee, if you have a more complex membership fees policy, you could overwrite templates
 
 
 
 
 Accounting logs
 Accounting logs

+ 15 - 0
coin/members/forms.py

@@ -7,6 +7,21 @@ from django.forms.utils import ErrorList
 
 
 from coin.members.models import Member
 from coin.members.models import Member
 
 
+from registration.forms import RegistrationForm
+
+
+class MemberRegistrationForm(RegistrationForm):
+
+    def __init__(self, *args, **kwargs):
+        super(MemberRegistrationForm, self).__init__(*args, **kwargs)
+
+        for fieldname in ['email', 'organization_name', 'password2']:
+            self.fields[fieldname].help_text = None
+
+    class Meta:
+        model = Member
+        fields = ['type', 'first_name', 'last_name',
+                  'organization_name', 'email', 'username']
 
 
 class MemberCreationForm(forms.ModelForm):
 class MemberCreationForm(forms.ModelForm):
 
 

+ 16 - 0
coin/members/registration_views.py

@@ -0,0 +1,16 @@
+from registration.backends.hmac.views import ActivationView, RegistrationView
+
+
+class MemberActivationView(ActivationView):
+    template_name = 'members/registration/activate.html'
+    def get_success_url(self, user):
+        return ('members:registration_activation_complete', (), {})
+
+class MemberRegistrationView(RegistrationView):
+    success_url = ('member:registration_activation_complete', (), {})
+    email_subject_template = 'members/registration/activation_email_subject.txt'
+    email_body_template = 'members/registration/activation_email.txt'
+    template_name = 'members/registration/registration_form.html'
+
+    def get_success_url(self, user):
+        return ('members:registration_complete', (), {})

+ 8 - 0
coin/members/templates/members/registration/activate.html

@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<p>L'activation de votre compte a échoué ou ce dernier a déjà été activé.</p>
+
+{% endblock %}

+ 18 - 0
coin/members/templates/members/registration/activation_complete.html

@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+
+<h2>Compte adhérent créé</h2>
+{% if dues %}
+    <p>Votre compte adhérent est désormais créé, pour finaliser votre adhésion merci d'<b>envoyer votre adhésion de {{ dues }} €</b> par virement  avec le libellé suivant:&nbsp;<b>{{ bank_transfer_label }}</b> .</p>
+
+    <h2>Coordonnées bancaires</h2>
+    <div id="payment-howto" class="panel">
+        {% include "billing/payment_howto.html" %}
+    </div>
+{% else %}
+    <p>Votre compte adhérent est désormais créé.</p>
+{% endif %}
+
+{% endblock %}

+ 12 - 0
coin/members/templates/members/registration/activation_email.txt

@@ -0,0 +1,12 @@
+{% load i18n %}
+Bonjour,
+
+Pour activer votre compte adhérent {{ site.name }}, merci de suivre le lien suivant:
+
+http://{{ site.domain }}{% url 'members:registration_activate' activation_key %}
+
+{% blocktrans %}Ce lien sera valable pendant {{ expiration_days }} jours.{% endblocktrans %}
+
+Ceci est un courriel automatique envoyé à la suite d'une demande de créayion de compte.
+
+L'équipe de l'association {{ branding.shortname|capfirst }}

+ 1 - 0
coin/members/templates/members/registration/activation_email_subject.txt

@@ -0,0 +1 @@
+{% load i18n %}Activation de votre compte adhérent {{ site.name }}

+ 10 - 4
coin/members/templates/members/registration/login.html

@@ -39,14 +39,20 @@
             <input type="submit" value="Connexion" class="button radius"/>
             <input type="submit" value="Connexion" class="button radius"/>
             <input type="hidden" name="next" value="{{ next }}" />
             <input type="hidden" name="next" value="{{ next }}" />
             <a id="password-reset-link" href="{% url 'members:password_reset' %}" tabindex="100">Mot de passe oublié ?</a>
             <a id="password-reset-link" href="{% url 'members:password_reset' %}" tabindex="100">Mot de passe oublié ?</a>
+            {% if settings.REGISTRATION_OPEN  %}
+                / <a href="{% url 'members:registration_register' %}">Créer un compte</a>
+            {% endif %}
         </form>
         </form>
     </div>
     </div>
     <div class="medium-5 columns">
     <div class="medium-5 columns">
         <div class="panel callout" id="newcomers">
         <div class="panel callout" id="newcomers">
-            <h3>Nouvel adhérent ?</h3>
-            <p>Pour votre première connexion, il faut définir votre mot de passe.<br />
-            <a href="{% url 'members:password_reset' %}"><i class="fa fa-arrow-right"></i>
- Initialiser mon mot de passe</a></p>
+                <h3>Première connexion</h3>
+                {% if not settings.REGISTRATION_OPEN  %}
+                    <p>Pour votre première connexion, il faut définir votre mot de passe.<br />
+                {% else %}
+                    <p>Si un·e bénévole a déjà créé votre compte, il faut commencer par choisir votre mot de passe.<br />
+                {% endif %}
+                <a href="{% url 'members:password_reset' %}"><i class="fa fa-arrow-right"></i> Initialiser mon mot de passe</a></p>
         </div>
         </div>
     </div>
     </div>
 
 

+ 6 - 0
coin/members/templates/members/registration/registration_closed.html

@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+	<p>{% trans "Registration is currently closed." %}</p>
+{% endblock %}

+ 6 - 0
coin/members/templates/members/registration/registration_complete.html

@@ -0,0 +1,6 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+<p>Votre demande de création de compte a été enregistrée. Afin de finaliser la création du compte, un email d'activation vous a été envoyé.</p>
+{% endblock %}

+ 65 - 0
coin/members/templates/members/registration/registration_form.html

@@ -0,0 +1,65 @@
+{% extends "base.html" %}
+{% load i18n %}
+
+{% block content %}
+        <h2>Créer un compte adhérent</h2>
+<div class="row">
+    <div class="medium-4 large-4 columns">
+        <div class="panel callout">
+            <h3>Déjà un compte ?</h3>
+            <p>Il est possible qu'un bénévole ait déjà créé votre compte.</p><p>Dans ce cas pour votre première connexion, il faut commencer par choisir votre mot de passe.<br />
+            <a href="{% url 'members:password_reset' %}"><i class="fa fa-arrow-right"></i> Initialiser mon mot de passe</a></p>
+        
+        <a href="{% url 'members:login' %}"><i class="fa fa-arrow-right"></i> OU essayer de me connecter</a>
+        </div>
+    </div>
+
+    <div class="medium-8 large-8 columns">
+        <form method="post" action=".">
+        
+            {% csrf_token %}
+            <fieldset class="module aligned wide">
+            {{ form.as_p }}
+            </fieldset>
+        <input type="submit" class="button radius" value="Créer mon compte" />
+        </form>
+    </div>
+</div>
+{% endblock %}
+{% block extra_js %}
+<script>
+$(document).ready(function () {
+  var hideFields=function(){
+    var optionSelected = $("option:selected", this);
+    var valueSelected = this.value;
+    if (valueSelected == 'legal_entity') {
+      $('#id_first_name').parent().hide();
+      $('#id_last_name').parent().hide();
+      $('#id_organization_name').parent().show();
+      $('#id_first_name').val('');
+      $('#id_last_name').val('');
+    } else if (valueSelected == 'natural_person') {
+      $('#id_organization_name').parent().hide();
+      $('#id_first_name').parent().show();
+      $('#id_last_name').parent().show();
+      $('#id_organization_name').val('');
+    }
+    return false;
+  };
+  $('#id_type').on('change', hideFields);
+  if (!$('#id_organization_name').val()) {
+    $('#id_type').val('natural_person');
+      $('#id_organization_name').parent().hide();
+      $('#id_first_name').parent().show();
+      $('#id_last_name').parent().show();
+  }
+  else {
+    $('#id_type').val('legal_entity');
+      $('#id_first_name').parent().hide();
+      $('#id_last_name').parent().hide();
+      $('#id_organization_name').parent().show();
+  }
+});
+</script>
+
+{% endblock %}

+ 44 - 3
coin/members/urls.py

@@ -2,16 +2,29 @@
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
 from django.conf.urls import patterns, url
 from django.conf.urls import patterns, url
-from django.views.generic import DetailView
+from coin.members import forms
 from coin.members import views
 from coin.members import views
-from coin.members.models import Member
+from django.views.generic.base import TemplateView
+from . import registration_views as views_r
+from coin import settings
 
 
+from registration.signals import user_activated
+from django.contrib.auth import login
+
+def login_on_activation(sender, user, request, **kwargs):
+    """Logs in the user after activation"""
+    user.backend = 'django.contrib.auth.backends.ModelBackend'
+    login(request, user)
+
+# Registers the function with the django-registration user_activated signal
+user_activated.connect(login_on_activation)
 
 
 urlpatterns = patterns(
 urlpatterns = patterns(
     '',
     '',
     url(r'^$', views.index, name='index'),
     url(r'^$', views.index, name='index'),
     url(r'^login/$', 'django.contrib.auth.views.login',
     url(r'^login/$', 'django.contrib.auth.views.login',
-        {'template_name': 'members/registration/login.html'},
+        {'template_name': 'members/registration/login.html',
+         'extra_context': {'settings': settings} },
         name='login'),
         name='login'),
     url(r'^logout/$', 'django.contrib.auth.views.logout_then_login',
     url(r'^logout/$', 'django.contrib.auth.views.logout_then_login',
         name='logout'),
         name='logout'),
@@ -43,6 +56,34 @@ urlpatterns = patterns(
         name='password_reset_complete'),
         name='password_reset_complete'),
 
 
 
 
+    url(r'^activate/complete/$', views.activation_completed,
+        name='registration_activation_complete'),
+    # The activation key can make use of any character from the
+    # URL-safe base64 alphabet, plus the colon as a separator.
+    url(r'^activate/(?P<activation_key>[-:\w]+)/$',
+        views_r.MemberActivationView.as_view(),
+        name='registration_activate'),
+    url(r'^register/$',
+        views_r.MemberRegistrationView.as_view(
+            form_class=forms.MemberRegistrationForm,
+            template_name='members/registration/registration_form.html'
+        ),
+        name='registration_register'),
+    url(r'^register/complete/$',
+        TemplateView.as_view(
+            template_name='members/registration/registration_complete.html'
+        ),
+        name='registration_complete'),
+    url(r'^register/closed/$',
+        TemplateView.as_view(
+            template_name='members/registration/registration_closed.html'
+        ),
+        name='registration_disallowed'),
+    #url(r'', include('registration.auth_urls')),
+
+
+
+
     url(r'^detail/$', views.detail,
     url(r'^detail/$', views.detail,
         name='detail'),
         name='detail'),
 
 

+ 28 - 17
coin/members/views.py

@@ -1,25 +1,23 @@
 # -*- coding: utf-8 -*-
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
 from __future__ import unicode_literals
 
 
-from django.template import RequestContext
-from django.shortcuts import render_to_response, render
+from django.shortcuts import render
 from django.contrib.auth.decorators import login_required
 from django.contrib.auth.decorators import login_required
-from django.http import Http404
 from django.conf import settings
 from django.conf import settings
 from forms import PersonMemberChangeForm, OrganizationMemberChangeForm
 from forms import PersonMemberChangeForm, OrganizationMemberChangeForm
 
 
 @login_required
 @login_required
 def index(request):
 def index(request):
     has_isp_feed = 'isp' in [k for k, _, _ in settings.FEEDS]
     has_isp_feed = 'isp' in [k for k, _, _ in settings.FEEDS]
-    return render_to_response('members/index.html',
-                              {'has_isp_feed': has_isp_feed},
-                              context_instance=RequestContext(request))
+    return render(request, 'members/index.html',
+                  {'has_isp_feed': has_isp_feed})
 
 
 
 
 @login_required
 @login_required
 def detail(request):
 def detail(request):
 
 
     membership_info_url = settings.MEMBER_MEMBERSHIP_INFO_URL
     membership_info_url = settings.MEMBER_MEMBERSHIP_INFO_URL
+<<<<<<< HEAD
     context={
     context={
         'membership_info_url': membership_info_url,
         'membership_info_url': membership_info_url,
     }
     }
@@ -40,6 +38,10 @@ def detail(request):
         context['form'] = form
         context['form'] = form
 
 
     return render(request, 'members/detail.html', context)
     return render(request, 'members/detail.html', context)
+=======
+    return render(request, 'members/detail.html',
+                  {'membership_info_url': membership_info_url})
+>>>>>>> origin/registration
 
 
 
 
 @login_required
 @login_required
@@ -47,10 +49,9 @@ def subscriptions(request):
     subscriptions = request.user.get_active_subscriptions()
     subscriptions = request.user.get_active_subscriptions()
     old_subscriptions = request.user.get_inactive_subscriptions()
     old_subscriptions = request.user.get_inactive_subscriptions()
 
 
-    return render_to_response('members/subscriptions.html',
-                              {'subscriptions': subscriptions,
-                               'old_subscriptions': old_subscriptions},
-                              context_instance=RequestContext(request))
+    return render(request, 'members/subscriptions.html',
+                  {'subscriptions': subscriptions,
+                   'old_subscriptions': old_subscriptions})
 
 
 
 
 @login_required
 @login_required
@@ -59,14 +60,24 @@ def invoices(request):
     invoices = request.user.invoices.filter(validated=True).order_by('-date')
     invoices = request.user.invoices.filter(validated=True).order_by('-date')
     payments = request.user.payments.filter().order_by('-date')
     payments = request.user.payments.filter().order_by('-date')
 
 
-    return render_to_response('members/invoices.html',
-                              {'balance' : balance, 
-                               'invoices': invoices, 
-                               'payments': payments},
-                              context_instance=RequestContext(request))
+    return render(request, 'members/invoices.html',
+                              {'balance' : balance,
+                               'invoices': invoices,
+                               'payments': payments})
 
 
 
 
 @login_required
 @login_required
 def contact(request):
 def contact(request):
-    return render_to_response('members/contact.html',
-                              context_instance=RequestContext(request))
+    return render(request, 'members/contact.html')
+
+
+@login_required
+def activation_completed(request):
+    label_template = settings.MEMBERSHIP_REFERENCE.format(user=request.user)
+    context = {
+        'bank_transfer_label': label_template,
+        'dues': settings.MEMBER_DEFAULT_COTISATION
+    }
+
+    return render(request, 'members/registration/activation_complete.html',
+                  context)

+ 2 - 0
coin/mixins.py

@@ -4,6 +4,8 @@ from __future__ import unicode_literals
 from django.db import transaction
 from django.db import transaction
 from django.conf import settings
 from django.conf import settings
 
 
+from django.views.generic import TemplateView
+
 
 
 class CoinLdapSyncMixin(object):
 class CoinLdapSyncMixin(object):
 
 

+ 12 - 0
coin/settings_base.py

@@ -259,6 +259,10 @@ SUBSCRIPTION_REFERENCE = 'REF-{subscription.offer.reference}-{subscription.pk}'
 # Payment delay in days
 # Payment delay in days
 PAYMENT_DELAY = 30
 PAYMENT_DELAY = 30
 
 
+# Template string to display the label the member should indicates for the bank
+# transfer
+MEMBERSHIP_REFERENCE = "ADH-{user.pk}"
+
 # Reset session if cookie older than 2h.
 # Reset session if cookie older than 2h.
 SESSION_COOKIE_AGE = 7200
 SESSION_COOKIE_AGE = 7200
 
 
@@ -272,3 +276,11 @@ FEEDS = (
 
 
 # Member can edit their own data
 # Member can edit their own data
 MEMBER_CAN_EDIT_PROFILE = False
 MEMBER_CAN_EDIT_PROFILE = False
+
+# Account registration
+# Allow visitor to join the association by register on COIN
+REGISTRATION_OPEN = False
+
+# All account with unvalidated email will be deleted after X days
+ACCOUNT_ACTIVATION_DAYS = 7
+

+ 1 - 0
requirements.txt

@@ -16,3 +16,4 @@ feedparser
 six==1.10.0
 six==1.10.0
 WeasyPrint==0.31
 WeasyPrint==0.31
 freezegun==0.3.8
 freezegun==0.3.8
+django-registration==2.2