Merge branch 'Kerl/ldap' into 'master'

The end of Clipper

GestioCOF fetches the clipper accounts from an LDAP database and doesn't
need to store clippers in a table anymore.

You need to run migrations to apply this patch

The default behaviour is not to fetch data from the LDAP because you may not have access to it.
To test over a ldap database server or in production, you need to set `settings.LDAP_SERVER_URL`
to the correct value.

Fixes #13

See merge request !140
This commit is contained in:
Robin Champenois 2017-02-11 02:42:00 +01:00
commit 1060a0a368
17 changed files with 158 additions and 162 deletions

View file

@ -9,10 +9,6 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/ https://docs.djangoproject.com/en/1.8/ref/settings/
""" """
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
@ -162,6 +158,8 @@ AUTHENTICATION_BACKENDS = (
'kfet.backends.GenericTeamBackend', 'kfet.backends.GenericTeamBackend',
) )
# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636'
# EMAIL_HOST="nef.ens.fr" # EMAIL_HOST="nef.ens.fr"
RECAPTCHA_PUBLIC_KEY = "DUMMY" RECAPTCHA_PUBLIC_KEY = "DUMMY"

View file

@ -4,10 +4,6 @@
Fichier principal de configuration des urls du projet GestioCOF Fichier principal de configuration des urls du projet GestioCOF
""" """
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light import autocomplete_light
from django.conf import settings from django.conf import settings
@ -61,7 +57,8 @@ urlpatterns = [
name='password_change_done'), name='password_change_done'),
# Inscription d'un nouveau membre # Inscription d'un nouveau membre
url(r'^registration$', gestioncof_views.registration), url(r'^registration$', gestioncof_views.registration),
url(r'^registration/clipper/(?P<login_clipper>[\w-]+)$', url(r'^registration/clipper/(?P<login_clipper>[\w-]+)/'
r'(?P<fullname>.*)$',
gestioncof_views.registration_form2, name="clipper-registration"), gestioncof_views.registration_form2, name="clipper-registration"),
url(r'^registration/user/(?P<username>.+)$', url(r'^registration/user/(?P<username>.+)$',
gestioncof_views.registration_form2, name="user-registration"), gestioncof_views.registration_form2, name="user-registration"),

View file

@ -1,18 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from ldap3 import Connection
from __future__ import print_function
from __future__ import unicode_literals
from django import shortcuts from django import shortcuts
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
from django.contrib.auth.models import User from django.contrib.auth.models import User
from gestioncof.models import CofProfile, Clipper from django.conf import settings
from gestioncof.models import CofProfile
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@buro_required @buro_required
def autocomplete(request): def autocomplete(request):
if "q" not in request.GET: if "q" not in request.GET:
@ -25,37 +30,51 @@ def autocomplete(request):
queries = {} queries = {}
bits = q.split() bits = q.split()
queries['members'] = CofProfile.objects.filter(Q(is_cof=True)) # Fetching data from User and CofProfile tables
queries['users'] = User.objects.filter(Q(profile__is_cof=False)) queries['members'] = CofProfile.objects.filter(is_cof=True)
queries['clippers'] = Clipper.objects queries['users'] = User.objects.filter(profile__is_cof=False)
for bit in bits: for bit in bits:
queries['members'] = queries['members'].filter( queries['members'] = queries['members'].filter(
Q(user__first_name__icontains=bit) Q(user__first_name__icontains=bit)
| Q(user__last_name__icontains=bit) | Q(user__last_name__icontains=bit)
| Q(user__username__icontains=bit) | Q(user__username__icontains=bit)
| Q(login_clipper__icontains=bit)) | Q(login_clipper__icontains=bit))
queries['users'] = queries['users'].filter( queries['users'] = queries['users'].filter(
Q(first_name__icontains=bit) Q(first_name__icontains=bit)
| Q(last_name__icontains=bit) | Q(last_name__icontains=bit)
| Q(username__icontains=bit)) | Q(username__icontains=bit))
queries['clippers'] = queries['clippers'].filter(
Q(fullname__icontains=bit)
| Q(username__icontains=bit))
queries['members'] = queries['members'].distinct() queries['members'] = queries['members'].distinct()
queries['users'] = queries['users'].distinct() queries['users'] = queries['users'].distinct()
usernames = list(queries['members'].values_list('login_clipper',
flat='True')) \
+ list(queries['users'].values_list('profile__login_clipper',
flat='True'))
queries['clippers'] = queries['clippers'] \
.exclude(username__in=usernames).distinct()
# add clippers
# Clearing redundancies
usernames = (
set(queries['members'].values_list('login_clipper', flat='True'))
| set(queries['users'].values_list('profile__login_clipper',
flat='True'))
)
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(**{"bit": bit})
for bit in bits]
))
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
]
# Resulting data
data.update(queries) data.update(queries)
data['options'] = sum(len(query) for query in queries)
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
return shortcuts.render(request, "autocomplete_user.html", data) return shortcuts.render(request, "autocomplete_user.html", data)

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light import autocomplete_light
from django.contrib.auth.models import User from django.contrib.auth.models import User
autocomplete_light.register( autocomplete_light.register(
User, search_fields=('username', 'first_name', 'last_name'), User, search_fields=('username', 'first_name', 'last_name'),
autocomplete_js_attributes={'placeholder': 'membre...'}) attrs={'placeholder': 'membre...'}
)

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0008_py3'),
]
operations = [
migrations.DeleteModel(
name='Clipper',
),
]

View file

@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -264,15 +260,6 @@ class SurveyAnswer(models.Model):
self.survey.title) self.survey.title)
@python_2_unicode_compatible
class Clipper(models.Model):
username = models.CharField("Identifiant", max_length=20)
fullname = models.CharField("Nom complet", max_length=200)
def __str__(self):
return "Clipper %s" % self.username
@python_2_unicode_compatible @python_2_unicode_compatible
class CalendarSubscription(models.Model): class CalendarSubscription(models.Model):
token = models.UUIDField() token = models.UUIDField()

View file

@ -15,7 +15,7 @@
{% if clippers %} {% if clippers %}
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li> <li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
{% for clipper in clippers %}{% if forloop.counter < 5 %} {% for clipper in clippers %}{% if forloop.counter < 5 %}
<li class="autocomplete-value"><a href="{% url 'clipper-registration' clipper.username %}">{{ clipper|highlight_clipper:q }}</a></li> <li class="autocomplete-value"><a href="{% url 'clipper-registration' clipper.clipper clipper.fullname %}">{{ clipper|highlight_clipper:q }}</a></li>
{% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %} {% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %}
{% endif %} {% endif %}

View file

@ -18,7 +18,7 @@
$(document).ready(function() { $(document).ready(function() {
$('input#search_autocomplete').yourlabsAutocomplete({ $('input#search_autocomplete').yourlabsAutocomplete({
url: '{% url 'gestioncof.autocomplete.autocomplete' %}', url: '{% url 'gestioncof.autocomplete.autocomplete' %}',
minimumCharacters: 1, minimumCharacters: 3,
id: 'search_autocomplete', id: 'search_autocomplete',
choiceSelector: 'li:has(a)', choiceSelector: 'li:has(a)',
placeholder: "Chercher un utilisateur par nom, prénom ou identifiant clipper", placeholder: "Chercher un utilisateur par nom, prénom ou identifiant clipper",

View file

@ -43,7 +43,7 @@ def highlight_user(user, q):
@register.filter @register.filter
def highlight_clipper(clipper, q): def highlight_clipper(clipper, q):
if clipper.fullname: if clipper.fullname:
text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.username) text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.clipper)
else: else:
text = clipper.username text = clipper.clipper
return highlight_text(text, q) return highlight_text(text, q)

View file

@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import unicodecsv import unicodecsv
import uuid import uuid
from datetime import timedelta from datetime import timedelta
@ -25,7 +21,7 @@ from gestioncof.models import Event, EventRegistration, EventOption, \
from gestioncof.models import EventCommentField, EventCommentValue, \ from gestioncof.models import EventCommentField, EventCommentValue, \
CalendarSubscription CalendarSubscription
from gestioncof.shared import send_custom_mail from gestioncof.shared import send_custom_mail
from gestioncof.models import CofProfile, Clipper, Club from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \
SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \
@ -321,11 +317,10 @@ def registration_set_ro_fields(user_form, profile_form):
@buro_required @buro_required
def registration_form2(request, login_clipper=None, username=None): def registration_form2(request, login_clipper=None, username=None, fullname=None):
events = Event.objects.filter(old=False).all() events = Event.objects.filter(old=False).all()
member = None member = None
if login_clipper: if login_clipper:
clipper = get_object_or_404(Clipper, username=login_clipper)
try: # check if the given user is already registered try: # check if the given user is already registered
member = User.objects.get(username=login_clipper) member = User.objects.get(username=login_clipper)
username = member.username username = member.username
@ -336,8 +331,8 @@ def registration_form2(request, login_clipper=None, username=None):
user_form = RegistrationUserForm(initial={ user_form = RegistrationUserForm(initial={
'username': login_clipper, 'username': login_clipper,
'email': "%s@clipper.ens.fr" % login_clipper}) 'email': "%s@clipper.ens.fr" % login_clipper})
if clipper.fullname: if fullname:
bits = clipper.fullname.split(" ") bits = fullname.split(" ")
user_form.fields['first_name'].initial = bits[0] user_form.fields['first_name'].initial = bits[0]
if len(bits) > 1: if len(bits) > 1:
user_form.fields['last_name'].initial = " ".join(bits[1:]) user_form.fields['last_name'].initial = " ".join(bits[1:])
@ -412,12 +407,12 @@ def registration(request):
try: try:
member = User.objects.get(username=username) member = User.objects.get(username=username)
user_form = RegistrationUserForm(request_dict, instance=member) user_form = RegistrationUserForm(request_dict, instance=member)
except User.DoesNotExist: if member.profile.login_clipper:
try: login_clipper = member.profile.login_clipper
clipper = Clipper.objects.get(username=username) else:
login_clipper = clipper.username
except Clipper.DoesNotExist:
user_form.force_long_username() user_form.force_long_username()
except User.DoesNotExist:
user_form.force_long_username()
else: else:
user_form.force_long_username() user_form.force_long_username()

View file

@ -1,16 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, import ldap3
print_function, unicode_literals)
from builtins import *
from django.shortcuts import render from django.shortcuts import render
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
from gestioncof.models import User, Clipper from django.conf import settings
from gestioncof.models import User
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import Account from kfet.models import Account
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@teamkfet_required @teamkfet_required
def account_create(request): def account_create(request):
if "q" not in request.GET: if "q" not in request.GET:
@ -25,58 +32,67 @@ def account_create(request):
queries = {} queries = {}
search_words = q.split() search_words = q.split()
# Fetching data from User, CofProfile and Account tables
queries['kfet'] = Account.objects queries['kfet'] = Account.objects
queries['users_cof'] = User.objects.filter(Q(profile__is_cof = True)) queries['users_cof'] = User.objects.filter(profile__is_cof = True)
queries['users_notcof'] = User.objects.filter(Q(profile__is_cof = False)) queries['users_notcof'] = User.objects.filter(profile__is_cof = False)
queries['clippers'] = Clipper.objects
for word in search_words: for word in search_words:
queries['kfet'] = queries['kfet'].filter( queries['kfet'] = queries['kfet'].filter(
Q(cofprofile__user__username__icontains = word) Q(cofprofile__user__username__icontains = word)
| Q(cofprofile__user__first_name__icontains = word) | Q(cofprofile__user__first_name__icontains = word)
| Q(cofprofile__user__last_name__icontains = word) | Q(cofprofile__user__last_name__icontains = word)
) )
queries['users_cof'] = queries['users_cof'].filter( queries['users_cof'] = queries['users_cof'].filter(
Q(username__icontains = word) Q(username__icontains = word)
| Q(first_name__icontains = word) | Q(first_name__icontains = word)
| Q(last_name__icontains = word) | Q(last_name__icontains = word)
) )
queries['users_notcof'] = queries['users_notcof'].filter( queries['users_notcof'] = queries['users_notcof'].filter(
Q(username__icontains = word) Q(username__icontains = word)
| Q(first_name__icontains = word) | Q(first_name__icontains = word)
| Q(last_name__icontains = word) | Q(last_name__icontains = word)
) )
queries['clippers'] = queries['clippers'].filter(
Q(username__icontains = word)
| Q(fullname__icontains = word)
)
# Clearing redundancies
queries['kfet'] = queries['kfet'].distinct() queries['kfet'] = queries['kfet'].distinct()
usernames = set(
usernames = list( \
queries['kfet'].values_list('cofprofile__user__username', flat=True)) queries['kfet'].values_list('cofprofile__user__username', flat=True))
queries['kfet'] = [
(account, account.cofprofile.user)
for account in queries['kfet']
]
queries['kfet'] = [ (account, account.cofprofile.user) \ queries['users_cof'] = \
for account in queries['kfet'] ]
queries['users_cof'] = \
queries['users_cof'].exclude(username__in=usernames).distinct() queries['users_cof'].exclude(username__in=usernames).distinct()
queries['users_notcof'] = \ queries['users_notcof'] = \
queries['users_notcof'].exclude(username__in=usernames).distinct() queries['users_notcof'].exclude(username__in=usernames).distinct()
usernames |= set(
usernames += list( \
queries['users_cof'].values_list('username', flat=True)) queries['users_cof'].values_list('username', flat=True))
usernames += list( \ usernames |= set(
queries['users_notcof'].values_list('username', flat=True)) queries['users_notcof'].values_list('username', flat=True))
queries['clippers'] = \ # Fetching data from the SPI
queries['clippers'].exclude(username__in=usernames).distinct() if hasattr(settings, 'LDAP_SERVER_URL'):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(**{"bit": bit}) for bit in bits]
))
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
]
# Resulting data
data.update(queries) data.update(queries)
data['options'] = sum([len(query) for query in queries])
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
return render(request, "kfet/account_create_autocomplete.html", data) return render(request, "kfet/account_create_autocomplete.html", data)

View file

@ -36,7 +36,7 @@
<li class="user_category"><span class="text">Utilisateurs clipper</span></li> <li class="user_category"><span class="text">Utilisateurs clipper</span></li>
{% for clipper in clippers %} {% for clipper in clippers %}
<li> <li>
<a href="{% url "kfet.account.create.fromclipper" clipper.username %}"> <a href="{% url "kfet.account.create.fromclipper" clipper.clipper clipper.fullname%}">
{{ clipper|highlight_clipper:q }} {{ clipper|highlight_clipper:q }}
</a> </a>
</li> </li>

View file

@ -35,7 +35,8 @@ urlpatterns = [
name = 'kfet.account.create_special'), name = 'kfet.account.create_special'),
url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax, url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax,
name = 'kfet.account.create.fromuser'), name = 'kfet.account.create.fromuser'),
url(r'^accounts/new/clipper/(?P<login_clipper>.+)$', views.account_create_ajax, url(r'^accounts/new/clipper/(?P<login_clipper>[\w-]+)/(?P<fullname>.*)$',
views.account_create_ajax,
name = 'kfet.account.create.fromclipper'), name = 'kfet.account.create.fromclipper'),
url(r'^accounts/new/empty$', views.account_create_ajax, url(r'^accounts/new/empty$', views.account_create_ajax,
name = 'kfet.account.create.empty'), name = 'kfet.account.create.empty'),

View file

@ -22,7 +22,7 @@ from django.db.models import F, Sum, Prefetch, Count, Func
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from gestioncof.models import CofProfile, Clipper from gestioncof.models import CofProfile
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative, from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
@ -222,19 +222,20 @@ def account_form_set_readonly_fields(user_form, cof_form):
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
cof_form.fields['is_cof'].widget.attrs['disabled'] = True cof_form.fields['is_cof'].widget.attrs['disabled'] = True
def get_account_create_forms(request=None, username=None, login_clipper=None): def get_account_create_forms(request=None, username=None, login_clipper=None,
fullname=None):
user = None user = None
clipper = None clipper = False
if login_clipper and (login_clipper == username or not username): if login_clipper and (login_clipper == username or not username):
# à partir d'un clipper # à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore exister # le user associé à ce clipper ne devrait pas encore exister
clipper = get_object_or_404(Clipper, username = login_clipper) clipper = True
try: try:
# Vérification que clipper ne soit pas déjà dans User # Vérification que clipper ne soit pas déjà dans User
user = User.objects.get(username=login_clipper) user = User.objects.get(username=login_clipper)
# Ici, on nous a menti, le user existe déjà # Ici, on nous a menti, le user existe déjà
username = user.username username = user.username
clipper = None clipper = False
except User.DoesNotExist: except User.DoesNotExist:
# Clipper (sans user déjà existant) # Clipper (sans user déjà existant)
@ -242,9 +243,9 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
user_initial = { user_initial = {
'username' : login_clipper, 'username' : login_clipper,
'email' : "%s@clipper.ens.fr" % login_clipper} 'email' : "%s@clipper.ens.fr" % login_clipper}
if clipper.fullname: if fullname:
# Prefill du nom et prénom # Prefill du nom et prénom
names = clipper.fullname.split() names = fullname.split()
# Le premier, c'est le prénom # Le premier, c'est le prénom
user_initial['first_name'] = names[0] user_initial['first_name'] = names[0]
if len(names) > 1: if len(names) > 1:
@ -308,8 +309,11 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
@login_required @login_required
@teamkfet_required @teamkfet_required
def account_create_ajax(request, username=None, login_clipper=None): def account_create_ajax(request, username=None, login_clipper=None,
forms = get_account_create_forms(request=None, username=username, login_clipper=login_clipper) fullname=None):
forms = get_account_create_forms(
request=None, username=username, login_clipper=login_clipper,
fullname=fullname)
return render(request, "kfet/account_create_form.html", { return render(request, "kfet/account_create_form.html", {
'account_form' : forms['account_form'], 'account_form' : forms['account_form'],
'cof_form' : forms['cof_form'], 'cof_form' : forms['cof_form'],

View file

@ -17,4 +17,5 @@ asgi-redis==0.14.0
statistics==1.0.3.5 statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
ldap3
git+https://github.com/Aureplop/channels.git#egg=channels git+https://github.com/Aureplop/channels.git#egg=channels

View file

@ -1,34 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
from gestioncof.models import Clipper
current = {}
print("[ FETCHING ]")
for clipper in Clipper.objects.all():
current[clipper.username] = clipper
print("[ SYNCING ]")
for line in sys.stdin:
bits = line.split(":")
username = bits[0]
fullname = bits[4]
if username in current:
clipper = current[username]
if clipper.fullname != fullname:
clipper.fullname = fullname
clipper.save()
print("Updated", username)
else:
clipper = Clipper(username=username, fullname=fullname)
clipper.save()
print("Created", username)
print("[ DONE ]")

View file

@ -1,2 +0,0 @@
#!/bin/sh
ssh cof@sas.eleves.ens.fr ypcat passwd | python sync_clipper.py