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/
"""
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, ...)
import os
@ -162,6 +158,8 @@ AUTHENTICATION_BACKENDS = (
'kfet.backends.GenericTeamBackend',
)
# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636'
# EMAIL_HOST="nef.ens.fr"
RECAPTCHA_PUBLIC_KEY = "DUMMY"

View file

@ -4,10 +4,6 @@
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
from django.conf import settings
@ -61,7 +57,8 @@ urlpatterns = [
name='password_change_done'),
# Inscription d'un nouveau membre
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"),
url(r'^registration/user/(?P<username>.+)$',
gestioncof_views.registration_form2, name="user-registration"),

View file

@ -1,18 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from ldap3 import Connection
from django import shortcuts
from django.http import Http404
from django.db.models import Q
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
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@buro_required
def autocomplete(request):
if "q" not in request.GET:
@ -25,9 +30,9 @@ def autocomplete(request):
queries = {}
bits = q.split()
queries['members'] = CofProfile.objects.filter(Q(is_cof=True))
queries['users'] = User.objects.filter(Q(profile__is_cof=False))
queries['clippers'] = Clipper.objects
# Fetching data from User and CofProfile tables
queries['members'] = CofProfile.objects.filter(is_cof=True)
queries['users'] = User.objects.filter(profile__is_cof=False)
for bit in bits:
queries['members'] = queries['members'].filter(
Q(user__first_name__icontains=bit)
@ -38,24 +43,38 @@ def autocomplete(request):
Q(first_name__icontains=bit)
| Q(last_name__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['users'] = queries['users'].distinct()
usernames = list(queries['members'].values_list('login_clipper',
flat='True')) \
+ list(queries['users'].values_list('profile__login_clipper',
# Clearing redundancies
usernames = (
set(queries['members'].values_list('login_clipper', flat='True'))
| set(queries['users'].values_list('profile__login_clipper',
flat='True'))
queries['clippers'] = queries['clippers'] \
.exclude(username__in=usernames).distinct()
# add clippers
)
# 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)
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
data['options'] = sum(len(query) for query in queries)
return shortcuts.render(request, "autocomplete_user.html", data)

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light
from django.contrib.auth.models import User
autocomplete_light.register(
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 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models
from django.dispatch import receiver
from django.contrib.auth.models import User
@ -264,15 +260,6 @@ class SurveyAnswer(models.Model):
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
class CalendarSubscription(models.Model):
token = models.UUIDField()

View file

@ -15,7 +15,7 @@
{% if clippers %}
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
{% 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 %}
{% endif %}

View file

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

View file

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

View file

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

View file

@ -1,16 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
import ldap3
from django.shortcuts import render
from django.http import Http404
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.models import Account
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@teamkfet_required
def account_create(request):
if "q" not in request.GET:
@ -25,10 +32,10 @@ def account_create(request):
queries = {}
search_words = q.split()
# Fetching data from User, CofProfile and Account tables
queries['kfet'] = Account.objects
queries['users_cof'] = User.objects.filter(Q(profile__is_cof = True))
queries['users_notcof'] = User.objects.filter(Q(profile__is_cof = False))
queries['clippers'] = Clipper.objects
queries['users_cof'] = User.objects.filter(profile__is_cof = True)
queries['users_notcof'] = User.objects.filter(profile__is_cof = False)
for word in search_words:
queries['kfet'] = queries['kfet'].filter(
@ -46,37 +53,46 @@ def account_create(request):
| Q(first_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()
usernames = list( \
usernames = set(
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)
for account in queries['kfet']
]
queries['users_cof'] = \
queries['users_cof'].exclude(username__in=usernames).distinct()
queries['users_notcof'] = \
queries['users_notcof'].exclude(username__in=usernames).distinct()
usernames += list( \
usernames |= set(
queries['users_cof'].values_list('username', flat=True))
usernames += list( \
usernames |= set(
queries['users_notcof'].values_list('username', flat=True))
queries['clippers'] = \
queries['clippers'].exclude(username__in=usernames).distinct()
# 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)
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
data['options'] = sum([len(query) for query in queries])
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>
{% for clipper in clippers %}
<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 }}
</a>
</li>

View file

@ -35,7 +35,8 @@ urlpatterns = [
name = 'kfet.account.create_special'),
url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax,
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'),
url(r'^accounts/new/empty$', views.account_create_ajax,
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.utils import timezone
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.models import (Account, Checkout, Article, Settings, AccountNegative,
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['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
clipper = None
clipper = False
if login_clipper and (login_clipper == username or not username):
# à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore exister
clipper = get_object_or_404(Clipper, username = login_clipper)
clipper = True
try:
# Vérification que clipper ne soit pas déjà dans User
user = User.objects.get(username=login_clipper)
# Ici, on nous a menti, le user existe déjà
username = user.username
clipper = None
clipper = False
except User.DoesNotExist:
# Clipper (sans user déjà existant)
@ -242,9 +243,9 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
user_initial = {
'username' : login_clipper,
'email' : "%s@clipper.ens.fr" % login_clipper}
if clipper.fullname:
if fullname:
# Prefill du nom et prénom
names = clipper.fullname.split()
names = fullname.split()
# Le premier, c'est le prénom
user_initial['first_name'] = names[0]
if len(names) > 1:
@ -308,8 +309,11 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
@login_required
@teamkfet_required
def account_create_ajax(request, username=None, login_clipper=None):
forms = get_account_create_forms(request=None, username=username, login_clipper=login_clipper)
def account_create_ajax(request, username=None, login_clipper=None,
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", {
'account_form' : forms['account_form'],
'cof_form' : forms['cof_form'],

View file

@ -17,4 +17,5 @@ asgi-redis==0.14.0
statistics==1.0.3.5
future==0.15.2
django-widget-tweaks==1.4.1
ldap3
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