Remaster (#38)

* setup new 'statistiques' module

* added 'graphos' package and created first test graph

* put graphos in requirements, deleted local folder

* added "load_csv" management command !

* added update of premiere_rencontre field in 'load_csv' management command

* added missing urls.py file

* added 'merge' action and view

* added 'info_completed' ratio

* linked sujets:merge views inside suivi:details

* added link to maraudes:details in notes table headers, if any

* Major reorganisation, moved 'suivi' and 'sujets' to 'notes', cleanup in 'maraudes', dropping 'website' mixins (mostly useless)

* small cleanup

* worked on Maraude and Sujet lists

* corrected missing line in notes.__init__

* restored 'details' view for maraudes and sujets insie 'notes' module

* worked on 'notes': added navigation between maraude's compte-rendu, right content in details, header to list tables

* changed queryset for CompteRenduDetailsView to all notes of same date, minor layout changes

* added right content to 'details-sujet', created 'statistiques' view and update templates

* restored 'statistiques' ajax view in 'details-sujet', fixed 'merge_two' util function

* added auto-creation of FicheStatistique (plus some tests), pagination for notes in 'details-sujet'

* added error-prone cases in paginator

* fixed non-working modals, added titles

* added UpdateStatistiques capacity in CompteRenduCreate view

* fixed missing AjaxTemplateMixin for CreateSujetView, worked on compte-rendu creation scripts

* fixed MaraudeManager.all_of() for common Maraudeurs, added color hints in planning

* re-instated statistiques module link and first test page

* added FinalizeView to send a mail before finalizing compte-rendu

* Added PieChart view for FicheStatistique fields

* small style updates, added 'age' and 'genre' fields from sujets in statistiques.PieChartView

* worked on statistiques, fixed small issues in 'notes' list views

* small theme change

* removed some dead code

* fixed notes.tests, fixed statistiques.info_completed display, added filter in SujetLisView

* added some tests

* added customised admin templates

* added authenticate in CustomAuthenticatationBackend, more verbose login thanks to messages

* added django-nose for test coverage

* Corrected raising exception on first migration

On first migration, qs.exists() would previously be called and raising an Exception, sot he migrations would fail.

* Better try block

* cleaned up custom settings.py, added some overrides of django base_settings

* corrected bad dictionnary key
This commit is contained in:
artus40
2017-06-11 17:16:17 +02:00
committed by GitHub
parent 0be59a61a7
commit be087464fc
155 changed files with 3568 additions and 1988 deletions

View File

@@ -1,4 +1,4 @@
from django.contrib import admin
from django.contrib import admin, messages
from .models import *
# Register your models here.
@@ -9,13 +9,47 @@ admin.register(Organisme)
@admin.register(Maraudeur)
class MaraudeurAdmin(admin.ModelAdmin):
fieldsets = [
('Informations', {'fields': [('first_name', 'last_name')]}),
('Identité', {'fields': [('first_name', 'last_name'),]}),
('Contact', {'fields': [('organisme','email',)]}),
('Statut', {'fields': [('is_active',)]}),
]
list_display = ('username', 'is_active')
list_display = ('username', 'is_active', 'est_referent')
actions = ['set_referent', 'toggle_staff']
ordering = ['-is_active', 'username']
def get_changeform_initial_data(self, request):
return {'organisme': Maraudeur.get_organisme(),
'is_active': True,
}
def set_referent(self, request, queryset):
if len(queryset) > 1:
self.message_user(
request,
"Vous ne pouvez définir qu'un seul référent !",
level=messages.WARNING)
return
maraudeur = queryset.first()
Maraudeur.objects.set_referent(maraudeur.first_name, maraudeur.last_name)
self.message_user(request, "%s a été défini comme référent." % maraudeur,
level=messages.SUCCESS)
set_referent.short_description = "Définir comme référent"
def toggle_staff(self, request, queryset):
try:
for m in queryset:
m.is_active = not m.is_active
m.save()
self.message_user(request,
"%i maraudeurs ont été modifié(s)" % len(queryset),
level=messages.SUCCESS)
except:
self.message_user(request, "Erreur lors de l'inversion", level=messages.WARNING)
toggle_staff.short_description = "Inverser le statut actif"
@admin.register(Organisme)
class OrganismeAdmin(admin.ModelAdmin):
pass

View File

@@ -1,16 +1,7 @@
from django.apps import AppConfig
from website.decorators import Webpage
from .models import Professionnel
class UtilisateursConfig(AppConfig):
name = 'utilisateurs'
utilisateurs = Webpage('utilisateurs',
icon="user",
defaults={
'users': [Professionnel],
'ajax': False,
'title': ('Utilisateurs','app'),
})

34
utilisateurs/backends.py Normal file
View File

@@ -0,0 +1,34 @@
import logging
from django.contrib.auth.backends import ModelBackend
from .models import Maraudeur
logger = logging.getLogger(__name__)
AUTHORIZED_MODELS = (Maraudeur, )
class CustomUserAuthentication(ModelBackend):
""" Custom ModelBackend that can only return an authorized custom models """
def authenticate(self, *args, **kwargs):
user = super().authenticate(*args, **kwargs)
if user:
user = self.get_user(user.pk)
return user
def get_user(self, user_id):
user = super().get_user(user_id)
if not user:
return None
for model in AUTHORIZED_MODELS:
try:
return model.objects.get(user_ptr=user)
except:
continue
logger.warning("WARNING: Could not find any AUTHORIZED_MODEL for %s !" % user)
return user
def has_perm(self, *args, **kwargs):
print('call has_perm', args, kwargs)
return super().has_perm(*args, **kwargs)

20
utilisateurs/managers.py Normal file
View File

@@ -0,0 +1,20 @@
from django.contrib.auth.models import UserManager
class MaraudeurManager(UserManager):
""" Manager for Maraudeurs objects.
"""
def get_referent(self):
try:
return self.get(is_superuser=True)
except self.model.DoesNotExist:
return None
def set_referent(self, first_name, last_name):
maraudeur, created = self.get_or_create(first_name=first_name, last_name=last_name)
for previous in self.get_queryset().filter(is_superuser=True):
previous.is_superuser = False
previous.save()
maraudeur.is_superuser = True
maraudeur.save()
return maraudeur

7
utilisateurs/mixins.py Normal file
View File

@@ -0,0 +1,7 @@
from django.contrib.auth.mixins import UserPassesTestMixin
from .models import Maraudeur
class MaraudeurMixin(UserPassesTestMixin):
def test_func(self):
return isinstance(self.request.user, Maraudeur)

View File

@@ -1,25 +1,37 @@
import datetime
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
from django.contrib.auth.models import User, UserManager, AnonymousUser
from django.db import models
from django.contrib.auth.models import User
from .managers import MaraudeurManager
# Create your models here.
if not settings.MARAUDEURS:
raise ImproperlyConfigured("No configuration for Maraudeur model")
else:
try:
assert(isinstance(settings.MARAUDEURS.get('organisme'), dict))
except:
raise ImproperlyConfigured("'organisme' key of MARAUDEURS settings is not a dict !")
## Visiteur
class Visiteur(AnonymousUser):
def get_email_suffix(organisme):
if not organisme.email:
return "unconfigured.org"
else:
return organisme.email.split("@")[1]
def __str__(self):
return "Visiteur"
class Organisme(models.Model):
""" Organisme : Association, Entreprise, Service public, ..."""
nom = models.CharField(max_length=64)
nom = models.CharField(max_length=64, primary_key=True)
email = models.EmailField("e-mail")
adresse = models.CharField(max_length=128)
adresse = models.CharField(max_length=128, blank=True, null=True)
class Meta:
verbose_name = "Organisme"
@@ -31,70 +43,50 @@ class Organisme(models.Model):
class Professionnel(User):
""" Professionnel d'un organisme """
organisme = models.ForeignKey(
Organisme,
organisme = models.ForeignKey(Organisme,
models.CASCADE,
related_name="professionnels",
blank=True, null=True # For now
)
def make_username(self):
""" Build the username for this Professionel instance. Must be overriden."""
raise NotImplementedError
class MaraudeurManager(UserManager):
""" Manager for Maraudeurs objects.
Updates `create`, `get_or_create` methods signatures : 'first_name', 'last_name'.
Add `set_referent` method (same signature).
"""
def create(self, first_name, last_name):
username = "%s.%s" % (first_name[0].lower(), last_name.lower())
data = {
'first_name': first_name,
'last_name': last_name,
'email': "%s@alsa68.org" % username,
'is_staff': True,
'is_active': True,
}
return super().create_user(username, **data)
def get_or_create(self, first_name, last_name):
try:
maraudeur = self.get(first_name=first_name, last_name=last_name)
created = False
except self.model.DoesNotExist:
created = True
maraudeur = self.create(first_name, last_name)
return (maraudeur, created)
def get_referent(self):
try:
return self.get(is_superuser=True)
except self.model.DoesNotExist:
return None
def set_referent(self, first_name, last_name):
maraudeur, created = self.get_or_create(first_name, last_name)
for previous in self.get_queryset().filter(is_superuser=True):
previous.is_superuser = False
previous.save()
maraudeur.is_superuser = True
maraudeur.save()
return maraudeur
def save(self, *args, **kwargs):
self.username = self.make_username()
if not self.pk:
self.email = "%s@%s" % (self.username, get_email_suffix(self.organisme))
return super().save(*args, **kwargs)
class Maraudeur(Professionnel):
""" Professionnels qui participent aux maraudes """
""" Professionnel qui participe aux maraudes """
# Donne accès aux vues "maraudes" et "suivi"
@staticmethod
def get_organisme():
return Organisme.objects.get_or_create(**settings.MARAUDEURS['organisme'])[0]
def est_referent(self):
return self.is_superuser
est_referent.boolean = True
est_referent.short_description = 'Référent Maraude'
objects = MaraudeurManager()
class Meta:
verbose_name = "Maraudeur"
def __str__(self):
return "%s %s" % (self.first_name, self.last_name[0])
def make_username(self):
return "%s.%s" % (self.first_name[0].lower(), self.last_name.lower())
def save(self, *args, **kwargs):
if not self.pk:
self.is_staff = True
self.organisme = Maraudeur.get_organisme()
self.set_password(settings.MARAUDEURS['password'])
return super().save(*args, **kwargs)
def __str__(self):
return "%s %s." % (self.first_name, self.last_name[0])

View File

@@ -1,2 +1,8 @@
{% extends "base.html" %}
{% block page_content %}
<h4 class="page-header">Profil</h4>
{{ user.first_name }}, {{ user.last_name }}
{% endblock %}

View File

@@ -1,33 +1,64 @@
from django.test import TestCase
from .models import Maraudeur, Professionnel
from utilisateurs.models import Maraudeur, Professionnel
# Create your tests here.
def generate_names():
i = 0
while True:
yield {'first_name': 'name%i' % i, 'last_name': 'family%i' % i}
i += 1
class ProfessionelTestCase(TestCase):
pass
#TODO: Un seul objet Maraudeur peut avoir la propriété vraie 'is_referent'
class MaraudeurTestCase(TestCase):
maraudeurs = generate_names()
def create(self):
return Maraudeur.objects.create(**next(self.maraudeurs))
def test_email_set_on_creation(self):
m = self.create()
self.assertIsNotNone(m.email)
def test_username_set_on_creation(self):
m = self.create()
self.assertEqual(m.username, Maraudeur.make_username(m))
def test_maraudeurs_is_staff(self):
m = self.create()
self.assertEqual(m.is_staff, True)
def test_username_set_on_update(self):
m = self.create()
m.last_name = "test01"
m.save()
self.assertEqual(m.username, "%s.test01" % (m.first_name[0]))
class MaraudeurManagerTestCase(TestCase):
names = [
('Arthur', 'Gerbaud'),
('Thibault', 'Huet'),
('Jacqueline', 'Julien'),
{'first_name': 'Astérix', 'last_name': 'Devinci'},
{'first_name': 'Obélix', 'last_name': 'Idéfix'},
]
def setUp(self):
for name in self.names:
Maraudeur.objects.create(*name)
Maraudeur.objects.create(**name)
def test_get_or_create_from_first_and_last_name(self):
# Existing Maraudeur
get_maraudeur = Maraudeur.objects.get(first_name="Thibault", last_name="Huet")
maraudeur, created = Maraudeur.objects.get_or_create('Thibault','Huet')
get_maraudeur = Maraudeur.objects.get(first_name="Obélix", last_name="Idéfix")
maraudeur, created = Maraudeur.objects.get_or_create(first_name='Obélix', last_name='Idéfix')
self.assertEqual(created, False)
self.assertEqual(maraudeur, get_maraudeur)
# Non-existing Maraudeur
with self.assertRaises(Maraudeur.DoesNotExist):
Maraudeur.objects.get(first_name="Thierry", last_name="Lhermitte")
maraudeur, created = Maraudeur.objects.get_or_create('Thierry', 'Lhermitte')
maraudeur, created = Maraudeur.objects.get_or_create(first_name='Thierry', last_name='Lhermitte')
self.assertEqual(created, True)
self.assertEqual(maraudeur, Maraudeur.objects.get(username="t.lhermitte"))
@@ -37,7 +68,7 @@ class MaraudeurTestCase(TestCase):
self.assertEqual(referent1.is_superuser, True)
self.assertEqual(referent1, Maraudeur.objects.get_referent())
# Set a new referent, existing Maraudeur
referent2 = Maraudeur.objects.set_referent("Arthur", "Gerbaud")
referent2 = Maraudeur.objects.set_referent("Astérix", "Devinci")
self.assertEqual(referent2.is_superuser, True)
self.assertEqual(referent2, Maraudeur.objects.get_referent())
self.test_referent_is_unique()

View File

@@ -1,10 +1,9 @@
from django.views import generic
from .apps import utilisateurs
from .models import Professionnel
from .mixins import MaraudeurMixin
@utilisateurs
class UtilisateurView(generic.DetailView):
class UtilisateurView(MaraudeurMixin, generic.DetailView):
template_name = "utilisateurs/details.html"
model = Professionnel