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:
@@ -41,8 +41,9 @@ class MaraudeAdmin(admin.ModelAdmin):
|
||||
]
|
||||
list_display = ('date', 'heure_debut', 'binome', 'est_passee', 'est_terminee')
|
||||
list_filter = ['date', 'binome']
|
||||
ordering = ['-date']
|
||||
|
||||
|
||||
@admin.register(Planning)
|
||||
class PlanningAdmin(admin.ModelAdmin):
|
||||
|
||||
list_display = ('week_day', 'horaire')
|
||||
|
||||
@@ -4,23 +4,5 @@ from django.apps import AppConfig
|
||||
class Config(AppConfig):
|
||||
name = 'maraudes'
|
||||
|
||||
index_url = "/maraudes/"
|
||||
menu_icon = "road"
|
||||
def get_index_url(self):
|
||||
return "/maraudes/"
|
||||
|
||||
|
||||
from utilisateurs.models import Maraudeur
|
||||
from website.decorators import Webpage
|
||||
|
||||
maraudes = Webpage('maraudes',
|
||||
icon="road",
|
||||
defaults={
|
||||
'users': [Maraudeur],
|
||||
'ajax': False,
|
||||
'title': ('Maraudes','app'),
|
||||
})
|
||||
# Setting up some links
|
||||
maraudes.app_menu.add_link(('Liste des maraudes', 'maraudes:liste', "list"))
|
||||
maraudes.app_menu.add_link(('Planning', 'maraudes:planning', "calendar"), admin=True)
|
||||
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
from .models import Maraude
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import datetime
|
||||
|
||||
def split_by_12h_blocks(iterable):
|
||||
""" Move object with given 'field' time under 12:00 to the end of stream.
|
||||
Apart from this, order is untouched.
|
||||
"""
|
||||
to_end = []
|
||||
for note in iterable:
|
||||
if getattr(note, "created_time") <= datetime.time(12):
|
||||
to_end.append(note)
|
||||
else:
|
||||
yield note
|
||||
|
||||
for note in to_end:
|
||||
yield note
|
||||
|
||||
class CompteRendu(Maraude):
|
||||
""" Proxy for Maraude objects.
|
||||
Gives access to related Observation and Rencontre
|
||||
"""
|
||||
|
||||
def rencontre_count(self):
|
||||
return self.rencontres.count()
|
||||
|
||||
def observation_count(self):
|
||||
count = 0
|
||||
for r in self:
|
||||
count += r.observations.count()
|
||||
return count
|
||||
|
||||
def get_observations(self, order="heure_debut", reverse=False):
|
||||
""" Returns list of all observations related to this instance """
|
||||
observations = []
|
||||
for r in self._iter(order=order, reverse=reverse):
|
||||
observations += r.observations.get_queryset()
|
||||
return list(split_by_12h_blocks(observations))
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterates on related 'rencontres' objects using default ordering """
|
||||
return self._iter()
|
||||
|
||||
def reversed(self, order="heure_debut"):
|
||||
return self._iter(order=order, reverse=True)
|
||||
|
||||
def _iter(self, order="heure_debut", reverse=False):
|
||||
""" Iterator on related 'rencontre' queryset.
|
||||
|
||||
Optionnal :
|
||||
- order : order by this field, default: 'heure_debut'
|
||||
- reversed : reversed ordering, default: False
|
||||
"""
|
||||
if reverse:
|
||||
order = "-" + order
|
||||
for rencontre in self.rencontres.get_queryset().order_by(order):
|
||||
yield rencontre
|
||||
|
||||
def as_list(self, **kwargs):
|
||||
return [r for r in self._iter(**kwargs)]
|
||||
|
||||
def as_dict(self, key_field="lieu"):
|
||||
""" Returns an 'OrderedDict' with given 'key_field' value as keys and
|
||||
the corresponding (rencontre, observations) tuple
|
||||
"""
|
||||
condensed = OrderedDict()
|
||||
for r, obs in self.__iter__():
|
||||
val = getattr(r, key_field, None)
|
||||
if not val:
|
||||
pass
|
||||
if not val in condensed:
|
||||
condensed[val] = [(r, obs)]
|
||||
else:
|
||||
condensed[val].append((r, obs))
|
||||
return condensed
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
from django import forms
|
||||
from django.forms import inlineformset_factory
|
||||
from notes.forms import *
|
||||
from django.utils import timezone
|
||||
|
||||
from django_select2.forms import Select2Widget
|
||||
|
||||
# Models
|
||||
from .models import *
|
||||
from .notes import *
|
||||
from notes.forms import UserNoteForm, SimpleNoteForm
|
||||
from notes.models import Sujet, GENRE_CHOICES
|
||||
|
||||
|
||||
class MaraudeAutoDateForm(forms.ModelForm):
|
||||
""" Maraude ModelForm with disabled 'date' field """
|
||||
def current_year_range():
|
||||
""" Returns a range from year -1 to year + 2 """
|
||||
year = timezone.now().date().year
|
||||
return (year - 1, year, year + 1, year + 2)
|
||||
|
||||
|
||||
|
||||
class MaraudeHiddenDateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Maraude
|
||||
fields = ['date', 'heure_debut', 'referent', 'binome']
|
||||
widgets = {'date': forms.HiddenInput()}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['date'].disabled = True
|
||||
|
||||
|
||||
|
||||
@@ -29,34 +35,36 @@ class RencontreForm(forms.ModelForm):
|
||||
}
|
||||
|
||||
|
||||
ObservationInlineFormSet = inlineformset_factory( Rencontre, Observation,
|
||||
ObservationInlineFormSet = forms.inlineformset_factory( Rencontre, Observation,
|
||||
form=SimpleNoteForm,
|
||||
extra = 1,
|
||||
)
|
||||
|
||||
RencontreInlineFormSet = inlineformset_factory(
|
||||
RencontreInlineFormSet = forms.inlineformset_factory(
|
||||
Maraude, Rencontre,
|
||||
form = RencontreForm,
|
||||
extra = 0,
|
||||
)
|
||||
|
||||
ObservationInlineFormSetNoExtra = inlineformset_factory(
|
||||
ObservationInlineFormSetNoExtra = forms.inlineformset_factory(
|
||||
Rencontre, Observation,
|
||||
form = SimpleNoteForm,
|
||||
extra = 0
|
||||
)
|
||||
|
||||
|
||||
|
||||
class MonthSelectForm(forms.Form):
|
||||
|
||||
month = forms.ChoiceField(
|
||||
month = forms.ChoiceField(label="Mois",
|
||||
choices=[
|
||||
(1, 'Janvier'), (2, 'Février'), (3, 'Mars'), (4, 'Avril'),
|
||||
(5, 'Mai'), (6, 'Juin'), (7, 'Juillet'), (8, 'Août'),
|
||||
(9, 'Septembre'),(10, 'Octobre'),(11, 'Novembre'),(12, 'Décembre')
|
||||
],
|
||||
)
|
||||
year = forms.ChoiceField(
|
||||
choices = [(y, y) for y in [2015, 2016, 2017, 2018]]
|
||||
year = forms.ChoiceField(label="Année",
|
||||
choices = [(y, y) for y in current_year_range()]
|
||||
)
|
||||
|
||||
def __init__(self, *args, month=None, year=None, **kwargs):
|
||||
@@ -64,3 +72,48 @@ class MonthSelectForm(forms.Form):
|
||||
self.fields['month'].initial = month
|
||||
self.fields['year'].initial = year
|
||||
|
||||
|
||||
|
||||
class AppelForm(UserNoteForm):
|
||||
class Meta(UserNoteForm.Meta):
|
||||
model = Appel
|
||||
fields = ['sujet', 'text', 'entrant', 'created_date', 'created_time']
|
||||
|
||||
|
||||
|
||||
class SignalementForm(UserNoteForm):
|
||||
|
||||
nom = forms.CharField(64, required=False)
|
||||
prenom = forms.CharField(64, required=False)
|
||||
age = forms.IntegerField(required=False)
|
||||
genre = forms.ChoiceField(choices=GENRE_CHOICES)
|
||||
|
||||
class Meta(UserNoteForm.Meta):
|
||||
model = Signalement
|
||||
fields = ['text', 'source', 'created_date', 'created_time']
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.cleaned_data['nom'] and not self.cleaned_data['prenom']:
|
||||
self.add_error('nom', '')
|
||||
self.add_error('prenom', '')
|
||||
raise forms.ValidationError("Entrez au moins un nom ou prénom")
|
||||
|
||||
def save(self, commit=True):
|
||||
sujet = Sujet.objects.create(
|
||||
nom=self.cleaned_data['nom'],
|
||||
prenom=self.cleaned_data['prenom'],
|
||||
genre=self.cleaned_data['genre'],
|
||||
age=self.cleaned_data['age']
|
||||
)
|
||||
instance = super().save(commit=False)
|
||||
instance.sujet = sujet
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class SendMailForm(forms.Form):
|
||||
|
||||
subject = forms.CharField(label="Objet")
|
||||
message = forms.CharField(widget=forms.Textarea, label="Contenu")
|
||||
|
||||
216
maraudes/management/commands/load_csv.py
Normal file
216
maraudes/management/commands/load_csv.py
Normal file
@@ -0,0 +1,216 @@
|
||||
import datetime
|
||||
import csv
|
||||
|
||||
def parse_rows(data_file):
|
||||
|
||||
reader = csv.DictReader(data_file, delimiter=";")
|
||||
|
||||
for i, row in enumerate(reader):
|
||||
date = datetime.datetime.strptime(row['DATE'] + ".2016", "%d.%m.%Y").date()
|
||||
lieu = row['LIEUX']
|
||||
nom, prenom = row['NOM'], row['PRENOM']
|
||||
prem_rencontre = True if row['1 ER CONTACT'].lower() == "oui" else False
|
||||
|
||||
yield i, date, lieu, nom, prenom, prem_rencontre
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = "Load data for rencontre from csv files"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('file', help="path to the files to load", nargs="+", type=str)
|
||||
parser.add_argument('--commit', help="commit changes to the database",
|
||||
action="store_true", dest="commit", default=False)
|
||||
parser.add_argument('--check', action='store_true', dest='check', default=False,
|
||||
help="Check that all lines from file are written into database")
|
||||
|
||||
@property
|
||||
def cache(self):
|
||||
if not hasattr(self, '_cache'):
|
||||
self._cache = {'maraude': {}, 'lieu': {}, 'sujet': {}, 'rencontre': [], 'observation': []}
|
||||
return self._cache
|
||||
|
||||
def new_object(self, model, data, cache_key=None):
|
||||
""" Create new object, add it to cache (in dict if cache_key is given, in list otherwise).
|
||||
Save it only if --commit option is given
|
||||
"""
|
||||
obj = model(**data)
|
||||
msg = "[%i]+ Created %s " % (self.cur_line, obj)
|
||||
if self._commit:
|
||||
obj.save()
|
||||
msg += " successfully saved to db"
|
||||
if cache_key:
|
||||
self.cache[model.__qualname__.lower()][cache_key] = obj
|
||||
msg += " and added to cache."
|
||||
self.stdout.write(self.style.SUCCESS(msg))
|
||||
return obj
|
||||
|
||||
@property
|
||||
def referent_maraude(self):
|
||||
if not hasattr(self, '_referent'):
|
||||
from utilisateurs.models import Maraudeur
|
||||
self._referent = Maraudeur.objects.get_referent()
|
||||
return self._referent
|
||||
|
||||
def find_maraude(self, date):
|
||||
from maraudes.models import Maraude
|
||||
try: # First, try to retrieve from database
|
||||
obj = Maraude.objects.get(date=date)
|
||||
except Maraude.DoesNotExist:
|
||||
# Try to retrieve from cache
|
||||
try:
|
||||
obj = self.cache['maraude'][date]
|
||||
except KeyError:
|
||||
# Create a new object and put it into cache
|
||||
obj = self.new_object(
|
||||
Maraude,
|
||||
{'date':date, 'referent':self.referent_maraude, 'binome':self.referent_maraude},
|
||||
cache_key=date)
|
||||
return obj
|
||||
|
||||
def find_sujet(self, nom, prenom):
|
||||
from sujets.models import Sujet
|
||||
from watson import search
|
||||
|
||||
search_text = "%s %s" % (nom, prenom)
|
||||
sujet = self.cache['sujet'].get(search_text, None)
|
||||
|
||||
while not sujet:
|
||||
create = False #Set to True if creation is needed at and of loop
|
||||
self.stdout.write(self.style.WARNING("In line %i, searching : %s. " % (self.cur_line, search_text)), ending='')
|
||||
results = search.filter(Sujet, search_text)
|
||||
|
||||
if results.count() == 1: # Always ask to review result a first time
|
||||
sujet = results[0]
|
||||
self.stdout.write(self.style.SUCCESS("Found %s '%s' %s" % (sujet.nom, sujet.surnom, sujet.prenom)))
|
||||
action = input("Confirm ? (y/n/type new search)> ")
|
||||
if action == "n":
|
||||
sujet = None
|
||||
search_text = "%s %s" % (nom, prenom)
|
||||
elif action == "y":
|
||||
continue
|
||||
else: # In case the result is no good at all !
|
||||
sujet = None
|
||||
search_text = action
|
||||
|
||||
elif results.count() > 1: # Ask to find the appropriate result
|
||||
self.stdout.write(self.style.WARNING("Multiple results for %s" % search_text))
|
||||
for i, result in enumerate(results):
|
||||
self.stdout.write("%i. %s '%s' %s" % (i, result.nom, result.surnom, result.prenom))
|
||||
choice = input("Choose the right number - Type new search - C to create '%s %s': " % (nom, prenom))
|
||||
if choice == "C":
|
||||
create = True
|
||||
else:
|
||||
try: sujet = results[int(choice)]
|
||||
except (IndexError, ValueError):
|
||||
search_text = str(choice) #New search
|
||||
continue
|
||||
|
||||
else: # No results, try with name only, or ask for new search
|
||||
if search_text == "%s %s" % (nom, prenom):
|
||||
# Search only with nom
|
||||
self.stdout.write(self.style.WARNING("Nothing, trying name only..."), ending='')
|
||||
search_text = nom if nom else prenom
|
||||
continue
|
||||
else:
|
||||
self.stdout.write(self.style.ERROR("No result !"))
|
||||
action = input("New search or C to create '%s %s': " % (nom, prenom))
|
||||
if action == "C":
|
||||
create = True
|
||||
else:
|
||||
search_text = str(action)
|
||||
|
||||
if create:
|
||||
sujet = self.new_object(
|
||||
Sujet,
|
||||
{'nom':nom, 'prenom':prenom}
|
||||
)
|
||||
self.stdout.write('Created, %s' % sujet)
|
||||
# Always store sujet in cache because it may or may not be updated, safer to save in all cases.
|
||||
self.cache['sujet']["%s %s" % (nom, prenom)] = sujet
|
||||
return sujet
|
||||
|
||||
def find_lieu(self, nom):
|
||||
from maraudes.models import Lieu
|
||||
|
||||
try:
|
||||
lieu = Lieu.objects.get(nom=nom)
|
||||
except Lieu.DoesNotExist:
|
||||
lieu = self.cache['lieu'].get(nom, None)
|
||||
while not lieu:
|
||||
self.stdout.write(self.style.WARNING("At line %i, le lieu '%s' n'a pas été trouvé" % (self.cur_line, nom)))
|
||||
action = input('%s (Créer/Sélectionner)> ' % nom)
|
||||
if action == "C":
|
||||
lieu = self.new_object(Lieu, {'nom': nom}, cache_key=nom)
|
||||
elif action == "S":
|
||||
choices = {l.pk:l.nom for l in Lieu.objects.all()}
|
||||
for key, name in choices.items():
|
||||
self.stdout.write("%i. %s" % (key, name))
|
||||
while not lieu:
|
||||
chosen_key = input('Choose a number: ')
|
||||
try:
|
||||
lieu = Lieu.objects.get(pk=chosen_key)
|
||||
confirm = input("Associer %s à %s ? (o/n)> " % (nom, lieu.nom))
|
||||
if confirm == "n":
|
||||
lieu = None
|
||||
else:
|
||||
self.cache['lieu'][nom] = lieu
|
||||
|
||||
except (Lieu.DoesNotExist, ValueError):
|
||||
lieu = None
|
||||
else:
|
||||
continue
|
||||
|
||||
return lieu
|
||||
|
||||
def add_rencontre(self, maraude, sujet, lieu):
|
||||
from maraudes.models import Rencontre
|
||||
from maraudes.notes import Observation
|
||||
|
||||
rencontre = self.new_object(Rencontre,
|
||||
{'maraude':maraude, 'lieu':lieu, 'heure_debut':datetime.time(20, 0),
|
||||
'duree':15})
|
||||
observation = self.new_object(Observation, {'rencontre':rencontre,
|
||||
'sujet':sujet,
|
||||
'text':"Chargé depuis '%s'" % self._file.name})
|
||||
self.cache['rencontre'].append(rencontre)
|
||||
self.cache['observation'].append(observation)
|
||||
|
||||
def handle(self, **options):
|
||||
""" Parsing all given files, look for existing objects and create new Rencontre
|
||||
and Observation objects. Ask for help finding related object, creating new ones
|
||||
if needed. All creation/updates are stored in cache and commited only after
|
||||
user confirmation
|
||||
"""
|
||||
|
||||
self._commit = options.get('commit', False)
|
||||
|
||||
for file_path in options['file']:
|
||||
with open(file_path, 'r') as data_file:
|
||||
self.stdout.write("Working with '%s'" % data_file.name)
|
||||
self._file = data_file
|
||||
for line, date, lieu, nom, prenom, prems in parse_rows(data_file):
|
||||
self.cur_line = line
|
||||
maraude = self.find_maraude(date)
|
||||
lieu = self.find_lieu(lieu)
|
||||
sujet = self.find_sujet(nom, prenom)
|
||||
assert sujet is not None
|
||||
assert lieu is not None
|
||||
assert maraude is not None
|
||||
if prems and self._commit:
|
||||
sujet.premiere_rencontre = date
|
||||
sujet.save()
|
||||
self.stdout.write(self.style.SUCCESS("[%i]* Updated premiere_rencontre on %s" % (self.cur_line, sujet)))
|
||||
|
||||
self.add_rencontre(maraude, sujet, lieu)
|
||||
|
||||
#Summary
|
||||
self.stdout.write(" ## %s : %i lines ##" % (data_file.name, self.cur_line))
|
||||
self.stdout.write("Trouvé %s nouvelles observations" % len(self.cache['observation']))
|
||||
self.stdout.write("Nécessite l'ajout/modification de : \n- %i maraudes\n- %i lieux\n- %i sujets" %
|
||||
(len(self.cache['maraude']), len(self.cache['lieu']), len(self.cache['sujet'])))
|
||||
|
||||
view = input('Voulez-vous voir la liste des changements ? (o/n)> ')
|
||||
if view == "o": self.stdout.write(" ## Changements ## \n%s" % self.cache)
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.db.models import Manager
|
||||
|
||||
import datetime
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
# TODO: What is really useful in there ??
|
||||
class MaraudeManager(Manager):
|
||||
""" Manager for Maraude objects """
|
||||
|
||||
@@ -20,24 +21,7 @@ class MaraudeManager(Manager):
|
||||
if not maraudes_ref:
|
||||
return maraudes_bin
|
||||
|
||||
cursor = 0
|
||||
complete_list = []
|
||||
for i, m in enumerate(maraudes_bin):
|
||||
if cursor >= 0 and maraudes_ref[cursor].date < m.date:
|
||||
complete_list.append(maraudes_ref[cursor])
|
||||
complete_list.append(m)
|
||||
if cursor < len(maraudes_ref) - 1:
|
||||
cursor += 1
|
||||
else:
|
||||
cursor = -1
|
||||
else:
|
||||
complete_list.append(m)
|
||||
# Don't lose remaining items of maraudes_ref
|
||||
if cursor >= 0:
|
||||
complete_list += maraudes_ref[cursor:]
|
||||
|
||||
return complete_list
|
||||
|
||||
return maraudes_bin | maraudes_ref
|
||||
|
||||
def get_next_of(self, maraudeur):
|
||||
""" Retourne la prochaine maraude de 'maraudeur' """
|
||||
@@ -47,22 +31,28 @@ class MaraudeManager(Manager):
|
||||
'date'
|
||||
).first()
|
||||
|
||||
def get_future(self):
|
||||
def get_future(self, date=None):
|
||||
""" Retourne la liste des prochaines maraudes """
|
||||
if not date: date = self.today
|
||||
return self.get_queryset().filter(
|
||||
date__gte=datetime.date.today()
|
||||
date__gte=date
|
||||
).order_by(
|
||||
'date'
|
||||
)
|
||||
|
||||
def get_past(self):
|
||||
def get_past(self, date=None):
|
||||
""" Retourne la liste des maraudes passées """
|
||||
if not date: date = self.today
|
||||
return self.get_queryset().filter(
|
||||
date__lt=datetime.date.today()
|
||||
date__lt=date
|
||||
).order_by(
|
||||
'date'
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def today(self):
|
||||
return timezone.localtime(timezone.now()).date()
|
||||
|
||||
@cached_property
|
||||
def next(self):
|
||||
""" Prochaine maraude """
|
||||
|
||||
@@ -1,27 +1,73 @@
|
||||
import calendar
|
||||
import datetime
|
||||
from django.utils import timezone
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils import timezone
|
||||
from django.db import models
|
||||
from django.db.models import Count
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from utilisateurs.models import Maraudeur
|
||||
|
||||
from . import managers
|
||||
|
||||
|
||||
## Fonctions utiles
|
||||
|
||||
def get_referent_maraude():
|
||||
""" Retourne l'administrateur et référent de la Maraude """
|
||||
return Maraudeur.objects.get_referent()
|
||||
|
||||
def split_by_12h_blocks(iterable):
|
||||
""" Move object with given 'field' time under 12:00 to the end of stream.
|
||||
Apart from this, order is untouched.
|
||||
"""
|
||||
to_end = []
|
||||
for note in iterable:
|
||||
if getattr(note, "created_time") <= datetime.time(12):
|
||||
to_end.append(note)
|
||||
else:
|
||||
yield note
|
||||
|
||||
for note in to_end:
|
||||
yield note
|
||||
|
||||
## Constantes
|
||||
|
||||
# Jours de la semaine
|
||||
WEEKDAYS = [
|
||||
(0, "Lundi"),
|
||||
(1, "Mardi"),
|
||||
(2, "Mercredi"),
|
||||
(3, "Jeudi"),
|
||||
(4, "Vendredi"),
|
||||
(5, "Samedi"),
|
||||
(6, "Dimanche")
|
||||
]
|
||||
|
||||
# Horaires
|
||||
HORAIRES_APRESMIDI = datetime.time(16, 0)
|
||||
HORAIRES_SOIREE = datetime.time(20, 0)
|
||||
HORAIRES_CHOICES = (
|
||||
(HORAIRES_APRESMIDI, 'Après-midi'),
|
||||
(HORAIRES_SOIREE, 'Soirée')
|
||||
)
|
||||
|
||||
# Durées
|
||||
DUREE_CHOICES = (
|
||||
(5, '5 min'),
|
||||
(10, '10 min'),
|
||||
(15, '15 min'),
|
||||
(20, '20 min'),
|
||||
(30, '30 min'),
|
||||
(45, '45 min'),
|
||||
(60, '1 heure'),
|
||||
)
|
||||
|
||||
## Modèles
|
||||
|
||||
|
||||
class Lieu(models.Model):
|
||||
""" Lieu de rencontre """
|
||||
|
||||
nom = models.CharField(max_length=128)
|
||||
|
||||
def __str__(self):
|
||||
@@ -32,9 +78,6 @@ class Lieu(models.Model):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Maraude(models.Model):
|
||||
""" Modèle pour une maraude
|
||||
- date : jour de la maraude
|
||||
@@ -53,13 +96,7 @@ class Maraude(models.Model):
|
||||
"Date",
|
||||
unique=True
|
||||
)
|
||||
# Horaires
|
||||
HORAIRES_APRESMIDI = datetime.time(16, 0)
|
||||
HORAIRES_SOIREE = datetime.time(20, 0)
|
||||
HORAIRES_CHOICES = (
|
||||
(HORAIRES_APRESMIDI, 'Après-midi'),
|
||||
(HORAIRES_SOIREE, 'Soirée')
|
||||
)
|
||||
|
||||
heure_debut = models.TimeField(
|
||||
"Horaire",
|
||||
choices=HORAIRES_CHOICES,
|
||||
@@ -98,13 +135,10 @@ class Maraude(models.Model):
|
||||
('view_maraudes', "Accès à l'application 'maraudes'"),
|
||||
)
|
||||
|
||||
# TODO: A remplacer !
|
||||
JOURS = ["Lundi", "Mardi", "Mercredi", "Jeudi",
|
||||
"Vendredi", "Samedi", "Dimanche"]
|
||||
MOIS = ["Jan.", "Fév.", "Mars", "Avr.", "Mai", "Juin",
|
||||
"Juil.", "Août", "Sept.", "Oct.", "Nov.", "Déc."]
|
||||
def __str__(self):
|
||||
return '%s %i %s' % (self.JOURS[self.date.weekday()],
|
||||
return '%s %i %s' % (WEEKDAYS[self.date.weekday()][1], # Retrieve text inside tuple
|
||||
self.date.day,
|
||||
self.MOIS[self.date.month - 1])
|
||||
|
||||
@@ -123,27 +157,14 @@ class Maraude(models.Model):
|
||||
est_passee.boolean = True
|
||||
est_passee.short_description = 'Passée ?'
|
||||
|
||||
def get_observations(self):
|
||||
raise Warning("Deprecated ! Should use CompteRendu proxy object")
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('maraudes:details', kwargs={'pk': self.id})
|
||||
return reverse('notes:details-maraude', kwargs={'pk': self.id})
|
||||
|
||||
|
||||
|
||||
class Rencontre(models.Model):
|
||||
""" Une Rencontre dans le cadre d'une maraude
|
||||
"""
|
||||
# Choices
|
||||
DUREE_CHOICES = (
|
||||
(5, '5 min'),
|
||||
(10, '10 min'),
|
||||
(15, '15 min'),
|
||||
(20, '20 min'),
|
||||
(30, '30 min'),
|
||||
(45, '45 min'),
|
||||
(60, '1 heure'),
|
||||
)
|
||||
|
||||
# Fields
|
||||
maraude = models.ForeignKey(
|
||||
@@ -195,14 +216,44 @@ class Rencontre(models.Model):
|
||||
return [o.sujet for o in self.observations.all()]
|
||||
|
||||
|
||||
WEEKDAYS = [
|
||||
(0, "Lundi"),
|
||||
(1, "Mardi"),
|
||||
(2, "Mercredi"),
|
||||
(3, "Jeudi"),
|
||||
(4, "Vendredi"),
|
||||
(5, "Samedi"),
|
||||
]
|
||||
class CompteRendu(Maraude):
|
||||
""" Proxy for Maraude objects.
|
||||
Gives access to related Observation and Rencontre
|
||||
"""
|
||||
|
||||
def observations_count(self):
|
||||
return self.rencontres.aggregate(Count("observations"))['observations__count']
|
||||
|
||||
def get_observations(self, order="heure_debut", reverse=False):
|
||||
""" Returns list of all observations related to this instance """
|
||||
observations = []
|
||||
for r in self._iter(order=order, reverse=reverse):
|
||||
observations += r.observations.get_queryset()
|
||||
return list(split_by_12h_blocks(observations))
|
||||
|
||||
def __iter__(self):
|
||||
""" Iterates on related 'rencontres' objects using default ordering """
|
||||
return self._iter()
|
||||
|
||||
def reversed(self, order="heure_debut"):
|
||||
return self._iter(order=order, reverse=True)
|
||||
|
||||
def _iter(self, order="heure_debut", reverse=False):
|
||||
""" Iterator on related 'rencontre' queryset.
|
||||
|
||||
Optionnal :
|
||||
- order : order by this field, default: 'heure_debut'
|
||||
- reversed : reversed ordering, default: False
|
||||
"""
|
||||
if reverse:
|
||||
order = "-" + order
|
||||
for rencontre in self.rencontres.get_queryset().order_by(order):
|
||||
yield rencontre
|
||||
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
|
||||
|
||||
class FoyerAccueil(Lieu):
|
||||
""" Foyer d'hébergement partenaire """
|
||||
@@ -220,11 +271,12 @@ class Planning(models.Model):
|
||||
"""
|
||||
|
||||
week_day = models.IntegerField(
|
||||
primary_key=True,
|
||||
choices=WEEKDAYS,
|
||||
)
|
||||
horaire = models.TimeField(
|
||||
"Horaire",
|
||||
choices=Maraude.HORAIRES_CHOICES,
|
||||
choices=HORAIRES_CHOICES,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
@@ -21,5 +21,18 @@ class Observation(Note):
|
||||
def note_labels(self): return [self.rencontre.lieu, self.rencontre.heure_debut]
|
||||
def note_bg_colors(self): return ("info", "info")
|
||||
|
||||
class Appel(Note):
|
||||
|
||||
entrant = models.BooleanField( "Appel entrant ?")
|
||||
|
||||
def note_labels(self): return ["Reçu" if self.entrant else "Émis", self.created_by]
|
||||
def note_bg_colors(self): return ("warning", "info")
|
||||
|
||||
|
||||
class Signalement(Note):
|
||||
|
||||
source = models.ForeignKey("utilisateurs.Organisme")
|
||||
|
||||
def note_labels(self): return [self.source, self.created_by]
|
||||
def note_bg_colors(self): return ('danger', 'info')
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{% load notes %}
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<table class="table table-bordered">
|
||||
{% for note in notes %}
|
||||
{% inline_table note header="sujet" %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<div class="well bg-info">
|
||||
<p><strong>Informations</strong></p>
|
||||
<p>Rencontres : {{ maraude.rencontre_count}}</p>
|
||||
<p>Personnes rencontrées : {{ maraude.observation_count}}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,104 +0,0 @@
|
||||
{% load bootstrap3 %}{% load staticfiles %}
|
||||
|
||||
<script type="text/javascript" src="{% static "scripts/jquery.formset.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
/* Dynamic Formsets */
|
||||
$(function() {
|
||||
$.fn.onAddForm = function(row) {
|
||||
// Load django_select2 fields
|
||||
row.find('.django-select2').djangoSelect2();
|
||||
var button = row.find('a.btn-delete')
|
||||
var text = button.text()
|
||||
button.html('<span class="glyphicon glyphicon-minus"></span> ' + text);
|
||||
};
|
||||
|
||||
$.fn.onDeleteForm = function(row) {
|
||||
/*
|
||||
* Custom code when deleting dynamic form
|
||||
*/
|
||||
};
|
||||
});
|
||||
$(function() {
|
||||
$('.dynamic-formset').formset({
|
||||
prefix: '{{ inline_formset.prefix }}',
|
||||
addText: 'Ajouter une personne',
|
||||
deleteText: 'Supprimer',
|
||||
addCssClass: 'btn btn-link btn-add',
|
||||
deleteCssClass: 'btn btn-link btn-delete',
|
||||
added: $.fn.onAddForm,
|
||||
removed: $.fn.onDeleteForm
|
||||
});
|
||||
|
||||
var text = $('a.btn-add').text()
|
||||
$('a.btn-add').html('<span class="glyphicon glyphicon-plus"></span> ' + text)
|
||||
text = $('a.btn-delete:first').text()
|
||||
$('a.btn-delete').html('<span class="glyphicon glyphicon-minus"></span> ' + text);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<!-- Modal buttons -->
|
||||
<div class="well well-sm text-right"><strong>Créer un nouvel objet :</strong>
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button id="new-sujet" class="btn btn-sm btn-primary">{% bootstrap_icon "user" %} Sujet</button>
|
||||
<button id="new-lieu" class="btn btn-sm btn-primary">{% bootstrap_icon "globe" %} Lieu</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'maraudes:create' maraude.pk %}?finalize=False">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-primary panel-collapse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Nouvelle rencontre</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% include "compte_rendu/compterendu_form.html" %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
{% bootstrap_button "Enregistrer" icon="save" button_type="submit" button_class="btn btn-success btn-sm" %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ form.media.js }}{{ form.media.css }}
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h4 class="panel-title">Enregistrées</h4></div>
|
||||
<table class="table">
|
||||
{% for rencontre in rencontres %}<tr><th colspan="2" class="active">{{ rencontre }}</th></tr>
|
||||
{% for observation in rencontre.observations.all %}<tr>
|
||||
<td>{{observation.sujet}}</td>
|
||||
<td>{{observation.text}}</td>
|
||||
</tr>{% endfor %}{% endfor %}
|
||||
</table>
|
||||
<div class="panel-footer text-right">
|
||||
<a class="btn btn-danger btn-sm" href="{% url 'maraudes:create' maraude.pk %}?finalize=True">
|
||||
{% bootstrap_icon "ok-circle" %} Finaliser</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal and button linking -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Modal title</h4>
|
||||
</div>
|
||||
<div id="form-modal-body" class="modal-body">
|
||||
<div class="alert alert-warning">Content should be there...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{% static 'scripts/bootstrap-modal.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
$.fn.openModalEvent('new-sujet',
|
||||
'{% url "sujets:create" %}?next={% url "maraudes:create" pk=maraude.pk %}',
|
||||
'Nouveau sujet');
|
||||
$.fn.openModalEvent('new-lieu',
|
||||
'{% url "maraudes:lieu-create" %}?next={% url "maraudes:create" pk=maraude.pk %}',
|
||||
'Nouveau lieu');
|
||||
</script>
|
||||
@@ -1,22 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
<form method="post" action="{% url 'maraudes:update' maraude.pk %}?continue=True">
|
||||
{% csrf_token %}
|
||||
{{ base_formset.management_form }}
|
||||
<div class="col-md-12 text-center well">
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-sm btn-success" formaction="{% url 'maraudes:update' maraude.pk %}?continue=False">
|
||||
{% bootstrap_icon "refresh" %} Mettre à jour</button>
|
||||
{% bootstrap_button "Enregistrer et quitter" button_type="submit" button_class="btn-primary btn-sm" icon="ok-circle" %}
|
||||
</div>
|
||||
</div>
|
||||
{% for form, inline_formset in forms %}
|
||||
<div class="col-md-6 col-sd-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% include "compte_rendu/compterendu_form.html" %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">{% bootstrap_field form.DELETE %}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</form>
|
||||
14
maraudes/templates/maraudes/base.html
Normal file
14
maraudes/templates/maraudes/base.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base_site.html" %}
|
||||
|
||||
{% block title %} Maraudes > {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "maraudes:index" %}">{{ date }}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% include "maraudes/menu.html" %}
|
||||
</div>
|
||||
</div>{% endblock %}
|
||||
169
maraudes/templates/maraudes/compterendu.html
Normal file
169
maraudes/templates/maraudes/compterendu.html
Normal file
@@ -0,0 +1,169 @@
|
||||
{% extends "maraudes/base.html" %}
|
||||
{% load bootstrap3 staticfiles %}
|
||||
|
||||
{% block title %} {{ block.super }} Compte-rendu du {{ object.date }} {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "maraudes:index" %}">{{ object.date }}</a></li>
|
||||
<li>Compte-rendu</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
<h4>{% bootstrap_icon "plus" %} Création</h4>
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button id="new-sujet" class="btn btn-default">{% bootstrap_icon "user" %} Sujet</button>
|
||||
<button id="new-lieu" class="btn btn-default">{% bootstrap_icon "globe" %} Lieu</button>
|
||||
</div>
|
||||
<hr />
|
||||
<h4>Finaliser</h4>
|
||||
<div class="pull-right"><a class="btn btn-primary" href="{% url 'maraudes:finalize' maraude.pk %}">
|
||||
{% bootstrap_icon "ok-circle" %} Finaliser</a></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<script type="text/javascript" src="{% static "scripts/jquery.formset.js" %}"></script>
|
||||
<script type="text/javascript">
|
||||
/* Dynamic Formsets */
|
||||
$(function() {
|
||||
$.fn.onAddForm = function(row) {
|
||||
// Load django_select2 fields
|
||||
row.find('.django-select2').djangoSelect2();
|
||||
var button = row.find('a.btn-delete')
|
||||
var text = button.text()
|
||||
button.html('<span class="glyphicon glyphicon-minus"></span> ' + text);
|
||||
};
|
||||
|
||||
$.fn.onDeleteForm = function(row) {
|
||||
/*
|
||||
* Custom code when deleting dynamic form
|
||||
*/
|
||||
};
|
||||
});
|
||||
$(function() {
|
||||
$('.dynamic-formset').formset({
|
||||
prefix: '{{ inline_formset.prefix }}',
|
||||
addText: 'Ajouter une personne',
|
||||
deleteText: 'Supprimer',
|
||||
addCssClass: 'btn btn-link btn-add',
|
||||
deleteCssClass: 'btn btn-link btn-delete',
|
||||
added: $.fn.onAddForm,
|
||||
removed: $.fn.onDeleteForm
|
||||
});
|
||||
|
||||
var text = $('a.btn-add').text()
|
||||
$('a.btn-add').html('<span class="glyphicon glyphicon-plus"></span> ' + text)
|
||||
text = $('a.btn-delete:first').text()
|
||||
$('a.btn-delete').html('<span class="glyphicon glyphicon-minus"></span> ' + text);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-7 col-md-12">
|
||||
<form method="post" action="{% url 'maraudes:create' maraude.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-primary panel-collapse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Nouvelle rencontre</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% include "maraudes/compterendu_form.html" %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
{% bootstrap_button "Enregistrer" icon="save" button_type="submit" button_class="btn btn-success btn-sm" %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ form.media.js }}{{ form.media.css }}
|
||||
</div>
|
||||
<div class="col-lg-5 col-md-12">
|
||||
<div id="saved-rencontres">
|
||||
<h4 class="page-header">Enregistrées</h4>
|
||||
<table class="table table-bordered">
|
||||
{% for rencontre in rencontres %}<tr><th colspan="2" class="active">{{ rencontre }}</th></tr>
|
||||
{% for observation in rencontre.observations.all %}<tr>
|
||||
<td>
|
||||
<a href="{% url "notes:details-sujet" observation.sujet.pk %}" id="sujet-name-{{observation.sujet.pk}}">{{observation.sujet}}</a>
|
||||
<a class="btn btn-link btn-xs show-stats-btn" href="#" value="{{observation.sujet.pk}}">
|
||||
{% bootstrap_icon "stats" %} Mise à jour</a>
|
||||
</td></tr>
|
||||
<tr><td>{{observation.text}}</td></tr>{% endfor %}{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div id="update-stats">
|
||||
<h4 class="page-header"><span id="sujet-name"></span> <small>Fiche statistiques</small>
|
||||
<div class="pull-right" id="update-buttons">
|
||||
<label for="submit-form" class="btn btn-primary" id="update-stats-btn" pk="">{% bootstrap_icon "floppy-save" %} Enregistrer</label>
|
||||
<span class="btn btn-primary btn-sm" id="cancel">{% bootstrap_icon "remove" %}Annuler</span>
|
||||
</div></h4>
|
||||
<div id="fiche-stats" class="well well-sm">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- Modal and button linking -->
|
||||
<div class="modal fade" id="form-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Modal title</h4>
|
||||
</div>
|
||||
<div id="form-modal-body" class="modal-body">
|
||||
<div class="alert alert-warning">Content should be there...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript" src="{% static 'scripts/bootstrap-modal.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
|
||||
function UpdateStats(pk) {
|
||||
var name = $("#sujet-name-" + pk).text();
|
||||
console.log("Update stats for ", pk, ":", name);
|
||||
$("#fiche-stats").load("/statistiques/update/" + pk);
|
||||
$("#sujet-name").text(name);
|
||||
$("#saved-rencontres").hide();
|
||||
$("#update-stats-btn").attr("pk", pk);
|
||||
$("#update-stats").show();
|
||||
};
|
||||
|
||||
$("#update-stats").hide();
|
||||
|
||||
$(".show-stats-btn").click(function(e) {
|
||||
var value = $(this).attr("value");
|
||||
UpdateStats(value);
|
||||
});
|
||||
|
||||
$("#update-stats-btn").click(function(e) {
|
||||
e.preventDefault();
|
||||
var pk = $(this).attr("pk");
|
||||
$.post("/statistiques/update/" + pk + "/", $("#update-stats-form").serialize());
|
||||
$("#fiche-stats").html("");
|
||||
$("#saved-rencontres").show();
|
||||
$("#update-stats").hide();
|
||||
});
|
||||
|
||||
$("#cancel").click(function() {
|
||||
$("#fiche-stats").html("");
|
||||
$("#saved-rencontres").show();
|
||||
$("#update-stats").hide();
|
||||
});
|
||||
|
||||
$.fn.openModalEvent('new-sujet',
|
||||
'{% url "notes:create-sujet" %}?next={% url "maraudes:create" pk=maraude.pk %}',
|
||||
'Nouveau sujet');
|
||||
|
||||
$.fn.openModalEvent('new-lieu',
|
||||
'{% url "maraudes:lieu-create" %}?next={% url "maraudes:create" pk=maraude.pk %}',
|
||||
'Nouveau lieu');
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,9 +6,11 @@
|
||||
<span class="input-group-btn">
|
||||
<button id="minus-5" class="btn btn-default btn-sm" type="button"><strong>-5</strong></button>
|
||||
<button id="plus-5" class="btn btn-default btn-sm" type="button"><strong>+5</strong></button>
|
||||
</span></div>
|
||||
</span>
|
||||
</div>
|
||||
{% bootstrap_field form.duree layout="inline" size="small" %}
|
||||
</div>
|
||||
|
||||
<div class="form-horizontal">
|
||||
{{ inline_formset.management_form }}
|
||||
{% for form in inline_formset %}
|
||||
@@ -23,5 +25,4 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="{% static "scripts/update_time.js" %}"></script>
|
||||
@@ -1,7 +0,0 @@
|
||||
{% if maraude.est_terminee %}
|
||||
{% include "compte_rendu/compterendu.html" %}
|
||||
{% else %}
|
||||
{% if perms.maraudes.can_add_compterendu %}<a class="btn btn-primary" href="{% url 'maraudes:create' maraude.pk %}">Écrire le compte-rendu</a>
|
||||
{% else %} <p class="alert alert-info">Le compte-rendu n'a pas encore été écrit</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
46
maraudes/templates/maraudes/finalize.html
Normal file
46
maraudes/templates/maraudes/finalize.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "maraudes/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %} {{ block.super }} Compte-rendu du {{ object.date }} {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "maraudes:index" %}">{{ object.date }}</a></li>
|
||||
<li>Transmission</li>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block page_content %}
|
||||
{% if object.est_terminee %}<div class="alert alert-warning"><p>Ce compte-rendu a déjà été finalisé !</p></div>
|
||||
{% else %}
|
||||
<div class="col-md-12 col-lg-7">
|
||||
<form method="post" action="{% url 'maraudes:finalize' maraude.pk %}">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-primary panel-collapse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Envoyer un message</h4>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% bootstrap_form form %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
{% bootstrap_button "Pas de message" icon="remove" button_type="link" href="?no_mail=True" button_class="btn btn-danger btn-sm" %}
|
||||
{% bootstrap_button "Envoyer" icon="send" button_type="submit" button_class="btn btn-success btn-sm" %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-12 col-lg-5">
|
||||
<h4 class="page-header">Rencontres</h4>
|
||||
<table class="table table-bordered">
|
||||
{% for rencontre in object.rencontres.all %}<tr><th colspan="2" class="active">{{ rencontre }}</th></tr>
|
||||
{% for observation in rencontre.observations.all %}<tr>
|
||||
<td>
|
||||
<a href="{% url "notes:details-sujet" observation.sujet.pk %}" id="sujet-name-{{observation.sujet.pk}}">{{observation.sujet}}</a>
|
||||
<a class="btn btn-link btn-xs" nohref onclick="UpdateStats({{observation.sujet.pk}});return false;">
|
||||
{% bootstrap_icon "stats" %} Mise à jour</a>
|
||||
</td></tr>
|
||||
<tr><td>{{observation.text}}</td></tr>{% endfor %}{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,15 @@
|
||||
{% extends "maraudes/base.html" %}
|
||||
|
||||
{% block title %} {{ block.super }} Tableau de bord {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li>Tableau de bord</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
{% load tables %}
|
||||
<div class="col-md-6">
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Votre prochaine maraude</h4>
|
||||
@@ -11,24 +21,32 @@
|
||||
avec {% if user.is_superuser %}{{prochaine_maraude.binome}}{%else%}{{prochaine_maraude.referent}}{%endif%}.
|
||||
</strong></p>
|
||||
<hr />
|
||||
{% if prochaine_maraude.est_terminee %}
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'maraudes:details' pk=prochaine_maraude.pk %}">
|
||||
Voir le compte-rendu
|
||||
</a>{%else%}
|
||||
<a class="btn btn-sm btn-primary" href="{% url 'maraudes:create' pk=prochaine_maraude.pk %}">
|
||||
Rédiger le compte-rendu
|
||||
</a>{% endif %}
|
||||
|
||||
{% else %}<p>Aucune maraude prévue.</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_superuser %}
|
||||
<div class="col-md-6">
|
||||
{% if derniers_sujets_rencontres %}
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Ces derniers temps...</h4>
|
||||
</div>
|
||||
{% table derniers_sujets_rencontres cols=3 cell_template="maraudes/table_cell_derniers_sujets.html" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if user.is_superuser and missing_cr %}
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Compte-rendus en retard</h4>
|
||||
</div>
|
||||
{% table missing_cr cols=2 cell_template="maraudes/missing_cr_table_cell.html" %}
|
||||
{% table missing_cr cols=2 cell_template="maraudes/table_cell_missing_cr.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-12 col-lg-6">
|
||||
<h4><strong>Nouvelle note :</strong></h4>
|
||||
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
{% include "notes/form_appel.html" with form=appel_form %}
|
||||
{% include "notes/form_signalement.html" with form=signalement_form %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
{% include "maraudes/lieu_create_inner.html" %}
|
||||
{% load bootstrap3 %}
|
||||
<form class="form form-horizontal" action="{% url "maraudes:lieu-create" %}" method="post">{% csrf_token %}
|
||||
{% bootstrap_form form layout="horizontal"%}
|
||||
<div class="pull-right">
|
||||
{% bootstrap_button "Ajouter un lieu" button_type="submit" button_class="btn btn-primary" icon="plus" %}
|
||||
</div>
|
||||
<input type="text" hidden=True name="next" value="{{ next }}" />
|
||||
</form>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
<form class="form form-horizontal" action="{% url "maraudes:lieu-create" %}" method="post">{% csrf_token %}
|
||||
{% bootstrap_form form layout="horizontal"%}
|
||||
<div class="pull-right">
|
||||
{% bootstrap_button "Ajouter un lieu" button_type="submit" button_class="btn btn-primary" icon="plus" %}
|
||||
</div>
|
||||
<input type="text" hidden=True name="next" value="{{ next }}" />
|
||||
</form>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{% if object.est_terminee %}<a href="{% url 'maraudes:details' object.id %}" class="btn btn-link">
|
||||
{% else %}<a href="#" class="btn btn-link disabled">{% endif %}
|
||||
{{object.date}}</a>
|
||||
<div class="pull-right">
|
||||
<span class="label label-info">{{ object.binome }} & {{ object.referent }}</span>
|
||||
{% if object.est_terminee %}
|
||||
<span class="label label-success">{{object.rencontres.count}} rencontres</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -1,32 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load tables %}
|
||||
<div class="col-lg-10">
|
||||
<div class="panel panel-primary">
|
||||
<!-- Default panel contents -->
|
||||
<div class="panel-heading text-center">
|
||||
<h3 class="panel-title">Maraudes passées</h3>
|
||||
</div>
|
||||
<!-- Table -->
|
||||
{% table object_list cols=3 cell_template="maraudes/list_table_cell.html" %}
|
||||
{% if is_paginated %}
|
||||
<div class="panel-footer text-center">
|
||||
<ul class="pagination">
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
<li {% if page_obj.number == num %} class="active" {%endif%}><a href="?page={{num}}">{{num}}</a></li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-left">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
Filtrer <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'maraudes:liste' %}">Pas de filtre</a></li>
|
||||
<li><a href="?filter=month-only">Ce mois-ci</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
25
maraudes/templates/maraudes/menu.html
Normal file
25
maraudes/templates/maraudes/menu.html
Normal file
@@ -0,0 +1,25 @@
|
||||
{% load navbar %}
|
||||
<ul class="nav nav-pills nav-stacked text-right">
|
||||
<li role="presentation" {% active namespace="maraudes" viewname="index" %}>
|
||||
<a href="{% url "maraudes:index" %}">Tableau de bord
|
||||
<span class="glyphicon glyphicon-dashboard"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" {% active namespace="maraudes" viewname="create" %}
|
||||
{% if not prochaine_maraude.date == date %} class="disabled">
|
||||
<a href="#">Compte-rendu
|
||||
<span class="glyphicon glyphicon-pencil"></span></a>
|
||||
</li> {% else %}>
|
||||
<a href="{% if prochaine_maraude.est_terminee %}
|
||||
{% url 'notes:details-maraude' pk=prochaine_maraude.pk %}
|
||||
{%else%}
|
||||
{% url 'maraudes:create' pk=prochaine_maraude.pk %}
|
||||
{% endif %}">Compte-rendu
|
||||
<span class="glyphicon glyphicon-pencil"></span></a>
|
||||
</li> {% endif %}
|
||||
<li role="presentation" {% active namespace="maraudes" viewname="planning" %}>
|
||||
<a href="{% url "maraudes:planning" %}">Planning
|
||||
<span class="glyphicon glyphicon-calendar"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -1,3 +0,0 @@
|
||||
<a href="{% url 'maraudes:create' pk=object.pk %}" class="btn btn-link">{{object.date}}</a>
|
||||
<span class="label label-info">{{object.referent}} & {{object.binome}}</span>
|
||||
|
||||
61
maraudes/templates/maraudes/planning.html
Normal file
61
maraudes/templates/maraudes/planning.html
Normal file
@@ -0,0 +1,61 @@
|
||||
{% extends "maraudes/base.html" %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %} {{ block.super }} Planning {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li>Planning</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<div class="panel panel-primary text-center">
|
||||
<div class="panel-body">
|
||||
<label for="submit-form" class="btn btn-primary">{% bootstrap_icon "floppy-save" %} Enregistrer</label>
|
||||
<hr />
|
||||
<form action="" method="get" class="form-horizontal">
|
||||
<strong>{% bootstrap_icon "calendar" %} Choisir une autre période : </strong>
|
||||
{% bootstrap_form select_form layout='horizontal' %}
|
||||
{% bootstrap_button "Choisir" button_type="submit" button_class="btn btn-primary btn-sm" %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<form method="post" action="{% url 'maraudes:planning' %}?month={{month}}&year={{year}}">
|
||||
<input type="submit" id="submit-form" class="hidden" />
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
<table class="table table-bordered">
|
||||
<tr class="active">
|
||||
{% for weekday in weekdays %}<th>{{weekday}}</th>{% endfor %}
|
||||
</tr>
|
||||
{% for week in weeks %}
|
||||
<tr>
|
||||
{% for day, form in week %}
|
||||
<td style="min-width:5%;" {% if form %}class="{% if form.instance.id %}success{%else%}warning{%endif%}"{% endif %}>{% if day %}{% if form %}
|
||||
<div class="row">
|
||||
<div class="col-lg-2">{% endif %}
|
||||
<strong>{{ day }}</strong>
|
||||
{% if form %}</div>
|
||||
<div class="col-lg-10">
|
||||
{% bootstrap_field form.id %}
|
||||
{% bootstrap_field form.date %}
|
||||
{% bootstrap_field form.heure_debut layout="inline" size="small" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field form.binome layout="horizontal" size="small" show_label=False %}
|
||||
{% bootstrap_field form.referent layout="horizontal" size="small" show_label=False %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1 @@
|
||||
<a href="{% url "notes:details-sujet" object.pk %}">{{ object }}</a>
|
||||
1
maraudes/templates/maraudes/table_cell_missing_cr.html
Normal file
1
maraudes/templates/maraudes/table_cell_missing_cr.html
Normal file
@@ -0,0 +1 @@
|
||||
<a class="btn btn-sm btn-primary" href="{% url "maraudes:create" pk=object.pk %}"><span class="glyphicon glyphicon-pencil"></span> {{ object }}</a>
|
||||
@@ -1,30 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
<div class="well col-md-12">
|
||||
<form action="" method="get" class="form-inline text-center">
|
||||
<label>Période : </label>
|
||||
{% bootstrap_form select_form layout='inline' %}
|
||||
{% bootstrap_button "Choisir" button_type="submit" %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'maraudes:planning' %}?month={{month}}&year={{year}}">
|
||||
{% csrf_token %}
|
||||
{{ formset.management_form }}
|
||||
{% for form in formset %}<div class="col-md-4 col-xs-6">
|
||||
<div class="panel {% if form.instance.pk %}panel-info{%else%}panel-warning{%endif%}">
|
||||
<div class="panel-heading text-center">
|
||||
<div class="form-inline">{% if form.id %}{{ form.id }}{% endif %}
|
||||
{% bootstrap_field form.date size="small" show_label=False %}
|
||||
{% bootstrap_field form.heure_debut layout="inline" size="small" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_field form.binome layout="horizontal" %}
|
||||
{% bootstrap_field form.referent layout="horizontal" size="small" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>{% endfor %}
|
||||
{% bootstrap_button "Enregistrer" button_type="submit" button_class="btn-primary" %}
|
||||
</form>
|
||||
@@ -4,7 +4,10 @@ import random
|
||||
from calendar import monthrange
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Maraude, Maraudeur
|
||||
from .models import (
|
||||
Maraude, Maraudeur, Planning,
|
||||
WEEKDAYS, HORAIRES_SOIREE,
|
||||
)
|
||||
# Create your tests here.
|
||||
|
||||
from maraudes_project.base_data import MARAUDEURS
|
||||
@@ -31,62 +34,101 @@ def get_maraude_days(start, end):
|
||||
|
||||
return maraude_days
|
||||
|
||||
class MaraudeManagerTestCase(TestCase):
|
||||
class PlanningTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
for maraudeur in MARAUDEURS:
|
||||
Maraudeur.objects.create(
|
||||
**maraudeur
|
||||
)
|
||||
self.maraudeurs = Maraudeur.objects.all()
|
||||
#Set up Référent de la Maraude
|
||||
ref = self.maraudeurs[0]
|
||||
Maraudeur.objects.set_referent(ref.first_name, ref.last_name)
|
||||
for i, is_maraude in enumerate(MARAUDE_DAYS):
|
||||
if is_maraude:
|
||||
Planning.objects.create(week_day=i, horaire=HORAIRES_SOIREE)
|
||||
|
||||
l = len(self.maraudeurs)
|
||||
today = datetime.date.today()
|
||||
start_date = today.replace(month=today.month - 1 if today.month > 1 else 12,
|
||||
day=1)
|
||||
end_date = today.replace(month=today.month + 1 if today.month < 12 else 1,
|
||||
day=28)
|
||||
for i, date in enumerate(get_maraude_days(start_date, end_date)):
|
||||
i = i % l
|
||||
if i == 0:
|
||||
replacement = random.randint(1, l-1)
|
||||
binome = random.randint(1, l-1)
|
||||
while binome == replacement:
|
||||
binome = random.randint(1, l-1)
|
||||
def test_get_planning(self):
|
||||
maraudes = {i for i in range(7) if MARAUDE_DAYS[i]}
|
||||
test_maraudes = set()
|
||||
for p in Planning.get_planning():
|
||||
test_maraudes.add(p.week_day)
|
||||
self.assertEqual(p.horaire, HORAIRES_SOIREE)
|
||||
self.assertEqual(maraudes, test_maraudes)
|
||||
|
||||
Maraude.objects.create(
|
||||
date=date,
|
||||
referent=self.maraudeurs[replacement],
|
||||
binome=self.maraudeurs[binome], # Avoid 0 = referent
|
||||
)
|
||||
def test_get_maraudes_days_for_month(self):
|
||||
test_values = [
|
||||
{'year': 2017, 'month': 2,
|
||||
'test': [(day, HORAIRES_SOIREE) for day in (2,3,6,7,9,10,13,14,16,17,20,21,23,24,27,28)] },
|
||||
{'year': 2016, 'month': 3,
|
||||
'test': [(day, HORAIRES_SOIREE) for day in (1,3,4,7,8,10,11,14,15,17,18,21,22,24,25,28,29,31)] },
|
||||
]
|
||||
|
||||
for test in test_values:
|
||||
self.assertEqual(test['test'], list(Planning.get_maraudes_days_for_month(test['year'], test['month'])))
|
||||
|
||||
|
||||
class MaraudeManagerTestCase(TestCase):
|
||||
|
||||
maraudeurs = [{"first_name": "Astérix", "last_name": "Le Gaulois"}, {"first_name": "Obélix", "last_name": "et Idéfix"}]
|
||||
|
||||
def setUp(self):
|
||||
first = True
|
||||
for maraudeur in self.maraudeurs:
|
||||
if first:
|
||||
first = False
|
||||
self.referent = Maraudeur.objects.set_referent(*list(maraudeur.values()))
|
||||
else:
|
||||
Maraude.objects.create(
|
||||
date=date,
|
||||
referent=ref,
|
||||
binome=self.maraudeurs[i]
|
||||
self.binome = Maraudeur.objects.create(
|
||||
**maraudeur
|
||||
)
|
||||
|
||||
def test_future_maraudes(self):
|
||||
self.today = datetime.date.today()
|
||||
self.past_dates = [self.today - datetime.timedelta(d) for d in (1, 3, 5)]
|
||||
self.future_dates = [self.today + datetime.timedelta(d) for d in (2, 4, 6)]
|
||||
|
||||
for date in [self.today,] + self.past_dates + self.future_dates:
|
||||
Maraude.objects.create(
|
||||
date = date,
|
||||
referent = self.referent,
|
||||
binome = self.binome
|
||||
)
|
||||
|
||||
def retrieve_date(self, maraude):
|
||||
return maraude.date
|
||||
|
||||
def test_all_of(self):
|
||||
_all = set([self.today, ] + self.past_dates + self.future_dates)
|
||||
for maraudeur in self.maraudeurs:
|
||||
maraudeur = Maraudeur.objects.get(**maraudeur)
|
||||
self.assertEqual(
|
||||
set(map(self.retrieve_date, Maraude.objects.all_of(maraudeur))),
|
||||
_all
|
||||
)
|
||||
|
||||
def test_future_maraudes_no_args(self):
|
||||
""" La liste des futures maraudes """
|
||||
pass
|
||||
test_set = set(self.future_dates + [self.today,])
|
||||
check_set = set(map(self.retrieve_date, Maraude.objects.get_future()))
|
||||
self.assertEqual(test_set, check_set)
|
||||
|
||||
def test_past_maraudes(self):
|
||||
pass
|
||||
def test_future_maraudes_are_sorted_by_date(self):
|
||||
check_generator = iter(sorted(self.future_dates + [self.today,]))
|
||||
for maraude in Maraude.objects.get_future():
|
||||
self.assertEqual(maraude.date, next(check_generator))
|
||||
|
||||
def test_get_next_maraude(self):
|
||||
pass
|
||||
def test_past_maraudes_are_sorted_by_date(self):
|
||||
check_generator = iter(sorted(self.past_dates))
|
||||
for maraude in Maraude.objects.get_past():
|
||||
self.assertEqual(maraude.date, next(check_generator))
|
||||
|
||||
def test_past_maraudes_no_args(self):
|
||||
check_set = set(self.past_dates)
|
||||
test_set = set(map(self.retrieve_date, Maraude.objects.get_past()))
|
||||
self.assertEqual(test_set, check_set)
|
||||
|
||||
def test_next_property(self):
|
||||
self.assertEqual(self.retrieve_date(Maraude.objects.next), self.today)
|
||||
|
||||
def test_last_property(self):
|
||||
self.assertEqual(self.retrieve_date(Maraude.objects.last), max(self.past_dates))
|
||||
|
||||
def test_get_next_of(self):
|
||||
pass
|
||||
self.assertEqual(self.retrieve_date(Maraude.objects.get_next_of(self.binome)), self.today)
|
||||
|
||||
def test_all_of_with_referent(self):
|
||||
pass
|
||||
|
||||
def test_all_of_with_maraudeur(self):
|
||||
pass
|
||||
|
||||
|
||||
class MaraudeTestCase(TestCase):
|
||||
|
||||
@@ -7,10 +7,7 @@ from . import views
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name="index"),
|
||||
url(r'^planning/$', views.PlanningView.as_view(), name="planning"),
|
||||
url(r'^liste/$', views.MaraudeListView.as_view(), name="liste"),
|
||||
url(r'^lieu/create/$', views.LieuCreateView.as_view(), name="lieu-create"),
|
||||
# Compte-rendus de maraude
|
||||
url(r'^(?P<pk>[0-9]+)/$', views.MaraudeDetailsView.as_view(), name="details"),
|
||||
url(r'^(?P<pk>[0-9]+)/update/$', views.CompteRenduUpdateView.as_view(), name="update"),
|
||||
url(r'^(?P<pk>[0-9]+)/create/$', views.CompteRenduCreateView.as_view(), name="create"),
|
||||
url(r'^(?P<pk>[0-9]+)/finalize/$', views.FinalizeView.as_view(), name="finalize"),
|
||||
]
|
||||
|
||||
@@ -1,116 +1,84 @@
|
||||
import datetime
|
||||
import calendar
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import render, redirect
|
||||
# Views
|
||||
from django.views import generic
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.views import generic
|
||||
from django.core.mail import send_mail
|
||||
from django.forms import modelformset_factory
|
||||
from django.contrib import messages
|
||||
|
||||
from utilisateurs.mixins import MaraudeurMixin
|
||||
|
||||
# Models
|
||||
from .models import ( Maraude, Maraudeur,
|
||||
CompteRendu,
|
||||
Rencontre, Lieu,
|
||||
Planning, )
|
||||
from .compte_rendu import CompteRendu
|
||||
from notes.models import Note
|
||||
# Forms
|
||||
from django import forms
|
||||
from django.forms import inlineformset_factory, modelformset_factory, modelform_factory
|
||||
from django.forms.extras import widgets
|
||||
from django_select2.forms import Select2Widget
|
||||
from .forms import ( RencontreForm, RencontreInlineFormSet,
|
||||
ObservationInlineFormSet, ObservationInlineFormSetNoExtra,
|
||||
MaraudeAutoDateForm, MonthSelectForm, )
|
||||
|
||||
from django.core.mail import send_mail
|
||||
|
||||
from .apps import maraudes
|
||||
from .forms import ( RencontreForm,
|
||||
ObservationInlineFormSet,
|
||||
MaraudeHiddenDateForm, MonthSelectForm,
|
||||
AppelForm, SignalementForm,
|
||||
SendMailForm )
|
||||
from notes.mixins import NoteFormMixin
|
||||
|
||||
|
||||
@maraudes.using(title=('La Maraude', 'Tableau de bord'))
|
||||
class IndexView(generic.TemplateView):
|
||||
def derniers_sujets_rencontres():
|
||||
""" Renvoie le 'set' des sujets rencontrés dans les deux dernières maraudes """
|
||||
sujets = set()
|
||||
for cr in list(CompteRendu.objects.filter(heure_fin__isnull=False))[-2:]:
|
||||
for obs in cr.get_observations():
|
||||
sujets.add(obs.sujet)
|
||||
return list(sujets)
|
||||
|
||||
|
||||
|
||||
class IndexView(NoteFormMixin, MaraudeurMixin, generic.TemplateView):
|
||||
|
||||
template_name = "maraudes/index.html"
|
||||
|
||||
#NoteFormMixin
|
||||
forms = {
|
||||
'appel': AppelForm,
|
||||
'signalement': SignalementForm,
|
||||
}
|
||||
|
||||
def get_initial(self):
|
||||
now = timezone.localtime(timezone.now())
|
||||
return {'created_date': now.date(),
|
||||
'created_time': now.time()}
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('maraudes:index')
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['prochaine_maraude_abs'] = self.get_prochaine_maraude()
|
||||
context['prochaine_maraude'] = self.get_prochaine_maraude_for_user()
|
||||
context['prochaine_maraude'] = Maraude.objects.get_next_of(self.request.user)
|
||||
context['derniers_sujets_rencontres'] = derniers_sujets_rencontres()
|
||||
|
||||
if self.request.user.is_superuser:
|
||||
context['missing_cr'] = CompteRendu.objects.get_queryset().filter(
|
||||
heure_fin__isnull=True,
|
||||
date__lte = timezone.localtime(timezone.now()).date()
|
||||
date__lt = timezone.localtime(timezone.now()).date()
|
||||
)
|
||||
return context
|
||||
|
||||
def get_prochaine_maraude_for_user(self):
|
||||
""" Retourne le prochain objet Maraude auquel
|
||||
l'utilisateur participe, ou None """
|
||||
try: #TODO: Clean up this ugly thing
|
||||
self.maraudeur = Maraudeur.objects.get(username=self.request.user.username)
|
||||
except:
|
||||
self.maraudeur = None
|
||||
|
||||
if self.maraudeur:
|
||||
return Maraude.objects.get_next_of(self.maraudeur)
|
||||
return None
|
||||
|
||||
def get_prochaine_maraude(self):
|
||||
return Maraude.objects.next
|
||||
|
||||
## MARAUDES
|
||||
@maraudes.using(title=('{{maraude.date}}', 'compte-rendu'))
|
||||
class MaraudeDetailsView(generic.DetailView):
|
||||
""" Vue détaillé d'un compte-rendu de maraude """
|
||||
|
||||
model = CompteRendu
|
||||
context_object_name = "maraude"
|
||||
template_name = "maraudes/details.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['notes'] = self.object.get_observations()
|
||||
return context
|
||||
|
||||
|
||||
@maraudes.using(title=('Liste des maraudes',))
|
||||
class MaraudeListView(generic.ListView):
|
||||
""" Vue de la liste des compte-rendus de maraude """
|
||||
|
||||
model = CompteRendu
|
||||
template_name = "maraudes/liste.html"
|
||||
paginate_by = 30
|
||||
|
||||
def get_queryset(self):
|
||||
current_date = timezone.localtime(timezone.now()).date()
|
||||
qs = super().get_queryset().filter(
|
||||
date__lte=current_date
|
||||
).order_by('-date')
|
||||
|
||||
filtre = self.request.GET.get('filter', None)
|
||||
if filtre == "month-only":
|
||||
return qs.filter(date__month=current_date.month)
|
||||
#Other cases...
|
||||
else:
|
||||
return qs
|
||||
|
||||
|
||||
## COMPTE-RENDU DE MARAUDE
|
||||
@maraudes.using(title=('{{maraude.date}}', 'rédaction'))
|
||||
class CompteRenduCreateView(generic.DetailView):
|
||||
class CompteRenduCreateView(MaraudeurMixin, generic.DetailView):
|
||||
""" Vue pour la création d'un compte-rendu de maraude """
|
||||
|
||||
model = CompteRendu
|
||||
template_name = "compte_rendu/compterendu_create.html"
|
||||
template_name = "maraudes/compterendu.html"
|
||||
context_object_name = "maraude"
|
||||
|
||||
form = None
|
||||
inline_formset = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
#WARNING: Overrides app_menu and replace it
|
||||
self._user_menu = ["compte_rendu/menu/creation.html"]
|
||||
|
||||
def get_forms(self, *args, initial=None):
|
||||
self.form = RencontreForm(*args,
|
||||
initial=initial)
|
||||
@@ -119,25 +87,6 @@ class CompteRenduCreateView(generic.DetailView):
|
||||
instance=self.form.instance
|
||||
)
|
||||
|
||||
def finalize(self):
|
||||
print('finalize !')
|
||||
maraude = self.get_object()
|
||||
maraude.heure_fin = timezone.now()
|
||||
maraude.save()
|
||||
# Redirect to a new view to edit mail ??
|
||||
# Add text to some mails ? Transmission, message à un référent, etc...
|
||||
# Send mail to Maraudeurs
|
||||
_from = maraude.referent.email
|
||||
# Shall select only Maraudeur where 'is_active' is True !
|
||||
recipients = [m for m in Maraudeur.objects.all() if m not in (maraude.referent, maraude.binome)]
|
||||
objet = "Compte-rendu de maraude : %s" % maraude.date
|
||||
message = "Sujets rencontrés : ..." #TODO: Mail content
|
||||
send_mail(objet, message, _from, recipients)
|
||||
|
||||
return redirect("maraudes:details",
|
||||
pk=maraude.pk
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.get_forms(request.POST, request.FILES)
|
||||
if self.form.has_changed():
|
||||
@@ -151,8 +100,6 @@ class CompteRenduCreateView(generic.DetailView):
|
||||
return redirect('maraudes:create', pk=self.get_object().pk)
|
||||
|
||||
def get(self, request, new_form=True, *args, **kwargs):
|
||||
if request.GET.get('finalize', False) == "True":
|
||||
return self.finalize()
|
||||
|
||||
def calculate_end_time(debut, duree):
|
||||
end_minute = debut.minute + duree
|
||||
@@ -186,76 +133,85 @@ class CompteRenduCreateView(generic.DetailView):
|
||||
context['form'] = self.form
|
||||
context['inline_formset'] = self.inline_formset
|
||||
context['rencontres'] = self.get_object().rencontres.order_by("-heure_debut")
|
||||
# Link there so that "Compte-rendu" menu item is not disabled
|
||||
context['prochaine_maraude'] = self.object
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@maraudes.using(title=('{{maraude.date}}', 'mise à jour'))
|
||||
class CompteRenduUpdateView(generic.DetailView):
|
||||
""" Vue pour mettre à jour le compte-rendu de la maraude """
|
||||
class FinalizeView( MaraudeurMixin,
|
||||
generic.detail.SingleObjectMixin,
|
||||
generic.edit.FormView):
|
||||
|
||||
model = CompteRendu
|
||||
context_object_name = "maraude"
|
||||
template_name = "compte_rendu/compterendu_update.html"
|
||||
template_name = "maraudes/finalize.html"
|
||||
model = Maraude
|
||||
form_class = SendMailForm
|
||||
success_url = "/maraudes/"
|
||||
|
||||
base_formset = None
|
||||
inline_formsets = []
|
||||
rencontres_queryset = None
|
||||
forms = None
|
||||
def get(self, *args, **kwargs):
|
||||
print(self.request.GET)
|
||||
if bool(self.request.GET.get("no_mail", False)) == True:
|
||||
messages.warning(self.request, "Aucun compte-rendu n'a été envoyé !")
|
||||
return self.finalize()
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_forms_with_inline(self, *args):
|
||||
self.base_formset = RencontreInlineFormSet(
|
||||
*args,
|
||||
instance=self.get_object(),
|
||||
prefix="rencontres"
|
||||
)
|
||||
def get_initial(self):
|
||||
maraude = self.get_object()
|
||||
objet = "%s - Compte-rendu de maraude" % maraude.date
|
||||
sujets_rencontres = set()
|
||||
for r in maraude.rencontres.all():
|
||||
for s in r.get_sujets():
|
||||
sujets_rencontres.add(s)
|
||||
message = "Nous avons rencontré : " + ", ".join(map(str, sujets_rencontres)) + ".\n\n"
|
||||
return {
|
||||
"subject": objet,
|
||||
"message": message
|
||||
}
|
||||
|
||||
self.inline_formsets = []
|
||||
for i, instance in enumerate(self.get_object()):
|
||||
inline_formset = ObservationInlineFormSetNoExtra(
|
||||
*args,
|
||||
instance = instance,
|
||||
prefix = "observation-%i" % i
|
||||
)
|
||||
self.inline_formsets.append(inline_formset)
|
||||
def finalize(self):
|
||||
maraude = self.get_object()
|
||||
maraude.heure_fin = timezone.localtime(timezone.now()).time()
|
||||
maraude.save()
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
# Aucun nouveau formulaire de 'Rencontre' n'est inclus.
|
||||
self.forms = [(self.base_formset[i], self.inline_formsets[i]) for i in range(len(self.inline_formsets))]
|
||||
def form_valid(self, form):
|
||||
# Send mail
|
||||
maraude = self.get_object()
|
||||
recipients = Maraudeur.objects.filter(
|
||||
is_active=True
|
||||
).exclude(
|
||||
pk__in=(maraude.referent.pk,
|
||||
maraude.binome.pk)
|
||||
)
|
||||
result = send_mail(
|
||||
form.cleaned_data['subject'],
|
||||
form.cleaned_data['message'],
|
||||
maraude.referent.email,
|
||||
[m.email for m in recipients],
|
||||
)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.get_forms_with_inline(request.POST, request.FILES)
|
||||
self.errors = False
|
||||
|
||||
if self.base_formset.is_valid():
|
||||
for inline_formset in self.inline_formsets:
|
||||
if inline_formset.is_valid():
|
||||
inline_formset.save()
|
||||
self.base_formset.save()
|
||||
if result == 1:
|
||||
messages.success(self.request, "Le compte-rendu a été transmis à %s" % ", ".join(map(str, recipients)))
|
||||
else:
|
||||
self.errors = True
|
||||
messages.error(self.request, "Erreur lors de l'envoi du message !")
|
||||
return self.finalize()
|
||||
|
||||
if self.errors or request.GET['continue'] == "False": # Load page to display errors
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
return redirect('maraudes:details', pk=self.get_object().pk)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.get_forms_with_inline()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
def get_context_data(self, **kwargs):
|
||||
self.object = self.get_object()
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['base_formset'] = self.base_formset
|
||||
context['forms'] = self.forms
|
||||
if self.object.est_terminee is True:
|
||||
context['form'] = None#Useless form
|
||||
return context
|
||||
# Link there so that "Compte-rendu" menu item is not disabled
|
||||
context['prochaine_maraude'] = self.object
|
||||
return context
|
||||
|
||||
|
||||
## PLANNING
|
||||
@maraudes.using(title=('Planning',))
|
||||
class PlanningView(generic.TemplateView):
|
||||
""" Display and edit the planning of next Maraudes """
|
||||
|
||||
template_name = "planning/planning.html"
|
||||
class PlanningView(MaraudeurMixin, generic.TemplateView):
|
||||
""" Vue d'édition du planning des maraudes """
|
||||
|
||||
template_name = "maraudes/planning.html"
|
||||
|
||||
def _parse_request(self):
|
||||
self.current_date = datetime.date.today()
|
||||
@@ -287,7 +243,7 @@ class PlanningView(generic.TemplateView):
|
||||
self._calculate_initials()
|
||||
return modelformset_factory(
|
||||
Maraude,
|
||||
form = MaraudeAutoDateForm,
|
||||
form = MaraudeHiddenDateForm,
|
||||
extra = len(self.initials),
|
||||
)(
|
||||
*args,
|
||||
@@ -300,24 +256,54 @@ class PlanningView(generic.TemplateView):
|
||||
for form in self.formset.forms:
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
return redirect('maraudes:index')
|
||||
else:
|
||||
logger.info("Form was ignored ! (%s)" % (form.errors.as_data()))
|
||||
return redirect('maraudes:planning')
|
||||
|
||||
def get(self, request):
|
||||
self.formset = self.get_formset()
|
||||
return super().get(request)
|
||||
|
||||
def get_weeks(self):
|
||||
""" List of (day, form) tuples, split by weeks """
|
||||
|
||||
def form_generator(forms):
|
||||
""" Yields None until the generator receives the day of
|
||||
next form.
|
||||
"""
|
||||
forms = iter(sorted(forms, key=lambda f: f.initial['date']))
|
||||
day = yield
|
||||
for form in forms:
|
||||
while day != form.initial['date'].day:
|
||||
day = yield None
|
||||
day = yield form
|
||||
|
||||
while True: # Avoid StopIteration
|
||||
day = yield None
|
||||
|
||||
form_or_none = form_generator(self.formset)
|
||||
form_or_none.send(None)
|
||||
|
||||
return [
|
||||
[(day, form_or_none.send(day)) for day in week]
|
||||
for week in calendar.monthcalendar(self.year, self.month)
|
||||
]
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['weekdays'] = ["Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"]
|
||||
context['weeks'] = self.get_weeks()
|
||||
|
||||
context['formset'] = self.formset
|
||||
context['select_form'] = MonthSelectForm(month=self.month, year=self.year)
|
||||
context['month'], context['year'] = self.month, self.year
|
||||
return context
|
||||
|
||||
|
||||
## LIEU
|
||||
|
||||
@maraudes.using(ajax=True)
|
||||
class LieuCreateView(generic.edit.CreateView):
|
||||
""" Vue de création d'un lieu """
|
||||
|
||||
model = Lieu
|
||||
template_name = "maraudes/lieu_create.html"
|
||||
fields = "__all__"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
default_app_config = 'notes.apps.NotesConfig'
|
||||
|
||||
38
notes/actions.py
Normal file
38
notes/actions.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from .models import Sujet
|
||||
from statistiques.models import FicheStatistique, NSP
|
||||
|
||||
|
||||
def merge_stats(main, merged):
|
||||
""" Merge stats of two sujets according to priority order : main, then merged """
|
||||
# TODO: replace hardcoded field names with more flexible getters
|
||||
|
||||
# Fields of 'Sujet' model
|
||||
for field in ('nom', 'prenom', 'surnom', 'age',):
|
||||
if not getattr(main, field):
|
||||
setattr(main, field, getattr(merged, field, None))
|
||||
|
||||
# Première rencontre : retenir la plus ancienne
|
||||
if merged.premiere_rencontre:
|
||||
if not main.premiere_rencontre or main.premiere_rencontre > merged.premiere_rencontre:
|
||||
main.premiere_rencontre = merged.premiere_rencontre
|
||||
|
||||
# Fields of 'FicheStatistique' model
|
||||
# NullBoolean fields
|
||||
for field in ('prob_psychiatrie', 'prob_somatique',
|
||||
'prob_administratif', 'prob_addiction',
|
||||
'connu_siao', 'lien_familial'):
|
||||
if not getattr(main.statistiques, field): # Ignore if already filled
|
||||
setattr(main.statistiques, field, getattr(merged.statistiques, field, None))
|
||||
# Choice fields, None is NSP
|
||||
for field in ('habitation', 'ressources', 'parcours_de_vie'):
|
||||
if getattr(main.statistiques, field) == NSP: # Ignore if already filled
|
||||
setattr(main.statistiques, field, getattr(merged.statistiques, field, NSP))
|
||||
|
||||
def merge_two(main, merged):
|
||||
""" Merge 'merged' sujet into 'main' one """
|
||||
merge_stats(main, merged) # Merge statistics and informations
|
||||
for note in merged.notes.all(): # Move all notes
|
||||
note.sujet = main
|
||||
note.save()
|
||||
main.save()
|
||||
merged.delete()
|
||||
@@ -3,6 +3,15 @@ from django.contrib import admin
|
||||
from .models import *
|
||||
# Register your models here.
|
||||
|
||||
@admin.register(Sujet)
|
||||
class SujetAdmin(admin.ModelAdmin):
|
||||
|
||||
fieldsets = [
|
||||
('Identité', {'fields': [('nom', 'prenom'), 'genre']}),
|
||||
('Informations', {'fields': ['age', ]}),
|
||||
]
|
||||
|
||||
|
||||
@admin.register(Note)
|
||||
class NoteAdmin(admin.ModelAdmin):
|
||||
|
||||
@@ -16,4 +25,4 @@ class NoteAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
list_display = ['created_date', 'sujet', 'child_class', 'text']
|
||||
list_filter = ('sujet', 'created_date', 'created_by')
|
||||
list_filter = ('created_date', 'created_by')
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
from watson import search as watson
|
||||
|
||||
class NotesConfig(AppConfig):
|
||||
name = 'notes'
|
||||
|
||||
def ready(self):
|
||||
Sujet = self.get_model("Sujet")
|
||||
watson.register(Sujet, fields=('nom', 'prenom', 'surnom'))
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from .models import Note
|
||||
import datetime
|
||||
|
||||
from .models import Note, Sujet
|
||||
from utilisateurs.models import Professionnel
|
||||
|
||||
from django import forms
|
||||
from django_select2.forms import Select2Widget
|
||||
from django.forms import Textarea
|
||||
|
||||
|
||||
### NOTES
|
||||
|
||||
class NoteForm(forms.ModelForm):
|
||||
""" Generic Note form """
|
||||
@@ -14,7 +15,7 @@ class NoteForm(forms.ModelForm):
|
||||
fields = ['sujet', 'text', 'created_by', 'created_date', 'created_time']
|
||||
widgets = {
|
||||
'sujet': Select2Widget(),
|
||||
'text': Textarea(attrs={'rows':4}),
|
||||
'text': forms.Textarea(attrs={'rows':4}),
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +55,8 @@ class UserNoteForm(NoteForm):
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
|
||||
class AutoNoteForm(UserNoteForm):
|
||||
class Meta(UserNoteForm.Meta):
|
||||
fields = ['text']
|
||||
@@ -68,3 +71,24 @@ class AutoNoteForm(UserNoteForm):
|
||||
if commit:
|
||||
inst.save()
|
||||
return inst
|
||||
|
||||
|
||||
|
||||
### SUJETS
|
||||
|
||||
current_year = datetime.date.today().year
|
||||
YEAR_CHOICE = tuple(year - 2 for year in range(current_year, current_year + 10))
|
||||
|
||||
class SujetCreateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Sujet
|
||||
fields = ['nom', 'surnom', 'prenom', 'genre', 'premiere_rencontre']
|
||||
widgets = {
|
||||
'premiere_rencontre': forms.SelectDateWidget( empty_label=("Année", "Mois", "Jour"),
|
||||
years = YEAR_CHOICE,
|
||||
),
|
||||
}
|
||||
|
||||
class SelectSujetForm(forms.Form):
|
||||
|
||||
sujet = forms.ModelChoiceField(queryset=Sujet.objects.all(), widget=Select2Widget)
|
||||
|
||||
@@ -1,9 +1,74 @@
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
|
||||
from django.db import models
|
||||
from . import managers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HOMME = 'M'
|
||||
FEMME = 'Mme'
|
||||
GENRE_CHOICES = (
|
||||
(HOMME, 'Homme'),
|
||||
(FEMME, 'Femme'),
|
||||
)
|
||||
|
||||
class Sujet(models.Model):
|
||||
""" Personne faisant l'objet d'un suivi par la maraude
|
||||
"""
|
||||
|
||||
genre = models.CharField("Genre",
|
||||
max_length=3,
|
||||
choices=GENRE_CHOICES,
|
||||
default=HOMME)
|
||||
nom = models.CharField(max_length=32, blank=True)
|
||||
prenom = models.CharField(max_length=32, blank=True)
|
||||
surnom = models.CharField(max_length=64, blank=True)
|
||||
|
||||
premiere_rencontre = models.DateField(
|
||||
blank=True, null=True,
|
||||
default=timezone.now
|
||||
)
|
||||
age = models.SmallIntegerField(
|
||||
blank=True, null=True
|
||||
)
|
||||
|
||||
# referent = models.ForeignKey("utilisateurs.Professionnel", related_name="suivis")
|
||||
|
||||
def __str__(self):
|
||||
string = '%s ' % self.genre
|
||||
if self.nom: string += '%s ' % self.nom
|
||||
if self.surnom: string += '"%s" ' % self.surnom
|
||||
if self.prenom: string += '%s' % self.prenom
|
||||
return string
|
||||
|
||||
def clean(self):
|
||||
if not any([self.nom, self.prenom, self.surnom]):
|
||||
raise ValidationError("Vous devez remplir au moins un nom, prénom ou surnom")
|
||||
return super().clean()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
if not self.id:
|
||||
from statistiques.models import FicheStatistique
|
||||
super().save(*args, **kwargs)
|
||||
fiche = FicheStatistique.objects.create(sujet=self)
|
||||
else:
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Sujet"
|
||||
ordering = ('surnom', 'nom', 'prenom')
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("notes:details-sujet", kwargs={"pk": self.pk })
|
||||
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
""" Note relative à un sujet.
|
||||
|
||||
@@ -19,11 +84,12 @@ class Note(models.Model):
|
||||
objects = managers.NoteManager()
|
||||
|
||||
sujet = models.ForeignKey(
|
||||
'sujets.Sujet',
|
||||
Sujet,
|
||||
related_name="notes",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
text = models.TextField("Texte")
|
||||
|
||||
created_by = models.ForeignKey(
|
||||
'utilisateurs.Professionnel',
|
||||
blank=True,
|
||||
@@ -42,7 +108,11 @@ class Note(models.Model):
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return "%s" % (self.child_class.__qualname__)
|
||||
return "<%s: %s>" % (self.child_class.__qualname__, self.sujet)
|
||||
|
||||
@classmethod
|
||||
def __str__(cls):
|
||||
return "<%s>" % cls.__qualname__
|
||||
|
||||
def note_author(self):
|
||||
return None
|
||||
|
||||
14
notes/templates/notes/base.html
Normal file
14
notes/templates/notes/base.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Notes >{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li>Notes</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% include "notes/menu.html" %}
|
||||
</div>
|
||||
</div>{% endblock %}
|
||||
33
notes/templates/notes/details.html
Normal file
33
notes/templates/notes/details.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "notes/base.html" %}
|
||||
{% load bootstrap3 notes %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="col-lg-8 col-md-12">
|
||||
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading" role="tab" id="notesHeading">
|
||||
<h3 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNotes" aria-expanded="true" aria-controls="collapseOne">
|
||||
Notes</a>
|
||||
</h3>
|
||||
</div>
|
||||
{% block pre_content %}{% endblock %}
|
||||
<table class="table table-striped table-bordered">
|
||||
{% for note in notes %}
|
||||
{% if maraude %}
|
||||
{% inline_table note header="sujet" %}
|
||||
{% elif sujet %}
|
||||
{% inline_table note header="date" %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% block post_content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-12">
|
||||
{% block right_column %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
47
notes/templates/notes/details_maraude.html
Normal file
47
notes/templates/notes/details_maraude.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends "notes/details.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ block.super }} {{ maraude }}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li><a href="{% url "notes:liste-maraude" %}">Maraudes</a></li>
|
||||
<li>{{ maraude }}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
<div class="panel panel-primary text-right">
|
||||
<div class="panel-body text-right">
|
||||
<h4 class="panel-title">Navigation</h4>
|
||||
<nav aria-label="Maraudes navigation">
|
||||
<ul class="pagination">
|
||||
<li {% if not prev_maraude %}class="disabled"{% endif %}>
|
||||
<a href="{% if prev_maraude %}{% url "notes:details-maraude" prev_maraude.pk %}{% else %}#{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true" class="glyphicon glyphicon-chevron-left"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li {% if not next_maraude %}class="disabled"{% endif %}>
|
||||
<a href="{% if next_maraude %}{% url "notes:details-maraude" next_maraude.pk %}{%else%}#{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true" class="glyphicon glyphicon-chevron-right"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block right_column %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="notesSujetHeading">
|
||||
<h3 class="panel-title">
|
||||
Informations</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p><strong>Maraudeurs :</strong> {{ maraude.binome }} & {{ maraude.referent }}</p>
|
||||
<p><strong>Nombre de rencontres</strong> {{ maraude.rencontres.count }}</p>
|
||||
<p><strong>Nombre de personnes rencontrées</strong> {{ maraude.observations_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
103
notes/templates/notes/details_sujet.html
Normal file
103
notes/templates/notes/details_sujet.html
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends "notes/details.html" %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}
|
||||
{{ block.super }} {{ sujet }}
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li><a href="{% url "notes:liste-sujet" %}">Sujets</a></li>
|
||||
<li>{{ sujet }}</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block pre_content %}
|
||||
<div id="collapseNotes" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="notesHeading">
|
||||
{% endblock %}
|
||||
|
||||
{% block post_content %}
|
||||
{% if notes.has_other_pages %}<div class="panel-footer text-center">
|
||||
<ul class="pagination">
|
||||
{% for num in notes.paginator.page_range %}
|
||||
<li {% if notes.number == num %} class="active" {%endif%}><a href="?page={{num}}">{{num}}</a></li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="notesAjoutHeading">
|
||||
<h4 class="panel-title">
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNotesAjout" aria-expanded="false" aria-controls="collapseTwo">
|
||||
{% bootstrap_icon "plus" %} Ajouter une note
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseNotesAjout" class="panel-collapse collapse" role="tabpanel" aria-labelledby="notesAjoutHeading">
|
||||
<div class="panel-body">
|
||||
<form method="POST" action="">{% csrf_token %}
|
||||
{% bootstrap_form note_form show_label=False %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
{% bootstrap_button "Enregistrer" button_type="submit" %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block right_column %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Informations</h4>
|
||||
</div>
|
||||
{% include "notes/details_sujet_inner.html" %}
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><h4 class="panel-title">Statistiques</h4></div>
|
||||
<div id="stats-content">
|
||||
{% include "statistiques/fiche_stats_details.html" with object=sujet.statistiques %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
<span class="text-right" id="normal-buttons">
|
||||
<p>{% bootstrap_icon "tasks" %} {{ sujet.statistiques.info_completed }}%
|
||||
<span class="btn btn-primary btn-sm" id="update-stats">Mettre à jour</span></p>
|
||||
</span>
|
||||
<span class="text-right" id="update-buttons">
|
||||
<label for="submit-form" class="btn btn-primary">{% bootstrap_icon "floppy-save" %} Enregistrer</label>
|
||||
<span class="btn btn-primary btn-sm" id="cancel">Annuler</span>
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#update-buttons").hide();
|
||||
$("#update-stats").click(function() {
|
||||
$("#stats-content").load("{% url "statistiques:update" sujet.pk %}");
|
||||
$("#normal-buttons").hide();
|
||||
$("#update-buttons").show();
|
||||
});
|
||||
$("#cancel").click(function() {
|
||||
$("#stats-content").load("{% url "statistiques:details" sujet.pk %}");
|
||||
$("#update-buttons").hide();
|
||||
$("#normal-buttons").show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<hr />
|
||||
{% if user.is_superuser %}
|
||||
<div class="panel panel-primary text-right"><div class="panel-heading"><h4 class="panel-title"><strong>Administration :</strong></h4></div>
|
||||
<div class="panel-body">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a href="{% url 'admin:notes_note_changelist' %}?sujet__exact={{sujet.pk}}" class="btn btn-primary">{% bootstrap_icon "wrench" %} Éditer les notes</a>
|
||||
<a href="{% url 'notes:sujets-merge' pk=object.pk %}" class="btn btn-default">{% bootstrap_icon "paste" %} Fusionner</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
22
notes/templates/notes/details_sujet_inner.html
Normal file
22
notes/templates/notes/details_sujet_inner.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<div id="sujet-content">
|
||||
<table class="table table-striped">
|
||||
{% with "-" as none %}
|
||||
<tr><th>Nom</th><th>Surnom</th><th>Prénom</th></tr>
|
||||
<tr><td>{{ sujet.nom|default:none }}</td><td>{{ sujet.surnom|default:none}}</td><td>{{ sujet.prenom|default:none}}</td></tr>
|
||||
<tr><th>Âge</th><th colspan="2">Première rencontre</th></tr>
|
||||
<tr><td>{{ sujet.age|default_if_none:none }}</td><td colspan="2">{{ sujet.premiere_rencontre|default_if_none:none }}</td></tr>
|
||||
{% endwith %}
|
||||
</table>
|
||||
<div class="panel-footer text-right" id="sujet-buttons">
|
||||
<span class="text-right"><span class="btn btn-primary btn-sm" id="update-sujet">Mettre à jour</a></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#update-sujet").click(function() {
|
||||
console.log("update !")
|
||||
$("#sujet-content").load("{% url "notes:update-sujet" sujet.pk %}");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
24
notes/templates/notes/details_sujet_update.html
Normal file
24
notes/templates/notes/details_sujet_update.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{% load bootstrap3 %}
|
||||
<form action="{% url "notes:update-sujet" form.instance.pk %}" method="post">{% csrf_token %}
|
||||
<table class="table table-striped">
|
||||
{% with "-" as none %}
|
||||
<tr><th>Nom</th><th>Surnom</th><th>Prénom</th></tr>
|
||||
<tr><td>{% bootstrap_field form.nom show_label=False %}</td><td>{% bootstrap_field form.surnom show_label=False %}</td><td>{% bootstrap_field form.prenom show_label=False %}</td></tr>
|
||||
<tr><th>Âge</th><th>Genre</th><th>Première rencontre</th></tr>
|
||||
<tr><td>{% bootstrap_field form.age show_label=False %}</td><td>{% bootstrap_field form.genre show_label=False %}</td><td>{% bootstrap_field form.premiere_rencontre show_label=False %}</td></tr>
|
||||
{% endwith %}
|
||||
</table>
|
||||
<div class="panel-footer text-right">
|
||||
<span class="text-right">{% bootstrap_button "Enregistrer" button_type="submit" %}
|
||||
<span class="btn btn-primary btn-sm" id="cancel">Annuler</span></span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
$("#cancel").click(function() {
|
||||
console.log("cancelled !")
|
||||
$("#sujet-content").load("{% url "notes:sujet" form.instance.pk %}");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="appelHeading">
|
||||
<h4 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#appelCollapse" aria-expanded="true" aria-controls="collapseOne">
|
||||
Appel</a></h4>
|
||||
<span class="glyphicon glyphicon-earphone"></span> Appel</a></h4>
|
||||
</div>
|
||||
<div id="appelCollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="appelHeading">
|
||||
<div class="panel-body">
|
||||
{% include "suivi/appel_form_inner.html" %}</div>
|
||||
{% include "notes/form_appel_inner.html" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% load bootstrap3 %}
|
||||
<form action="" method="POST">{% csrf_token %}
|
||||
{% with "inline" as layout %}
|
||||
<div class="form-{{layout}} well col-md-10 col-md-offset-2">
|
||||
<div class="form-{{layout}} well well-sm text-center">
|
||||
{% bootstrap_field form.created_date layout=layout %}
|
||||
{% bootstrap_field form.created_time layout=layout %}
|
||||
{% bootstrap_field form.entrant layout=layout %}
|
||||
@@ -14,3 +14,4 @@
|
||||
<div class="pull-right">{% bootstrap_button "Enregistrer l'appel" button_type="submit" %}</div>
|
||||
</form>
|
||||
{{ form.media.js }}{{ form.media.css }}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="signalementHeading">
|
||||
<h4 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#signalementCollapse" aria-expanded="true" aria-controls="collapseOne">
|
||||
Signalement</a></h4>
|
||||
<span class="glyphicon glyphicon-warning-sign"></span> Signalement</a></h4>
|
||||
</div>
|
||||
<div id="signalementCollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="signalementHeading">
|
||||
<div class="panel-body">
|
||||
{% include "suivi/signalement_form_inner.html" %}
|
||||
{% include "notes/form_signalement_inner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
16
notes/templates/notes/index.html
Normal file
16
notes/templates/notes/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "notes/base.html" %}
|
||||
|
||||
{% block title %}{{block.super}} Tableau de bord{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li>Tableau de bord</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="col-md-12 col-lg-6">
|
||||
<h2 class="page-header">TODO</h2>
|
||||
<p>Liste des sujets qui ont été ajoutés depuis la dernière connexion</p>
|
||||
<p>Liste des compte-rendus qui ont été ajoutés depuis la dernière connexion</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
34
notes/templates/notes/liste.html
Normal file
34
notes/templates/notes/liste.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "notes/base.html" %}
|
||||
{% load tables %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
{% block sidebar_insert %}{% endblock %}
|
||||
{% if filters %}
|
||||
<div class="well">
|
||||
<h4 class="text-right"><span class="glyphicon glyphicon-filter"></span> <strong>Filtres</strong></h4>
|
||||
<ul class="nav nav-pills nav-stacked text-right">
|
||||
{% for filter in filters %}
|
||||
<li role="presentation" {% if filter.active %} class="active" {% endif %}>
|
||||
<a href="?filter={{filter.parameter_name}}">{{ filter.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
{% block search %}{% endblock %}
|
||||
<!-- Table -->
|
||||
{% table object_list cols=3 cell_template=table_cell_template header=table_header %}
|
||||
{% if is_paginated %}
|
||||
<div class="text-center">
|
||||
<ul class="pagination">{% with request.GET.filter as filter %}
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
<li {% if page_obj.number == num %} class="active" {%endif%}><a href="?{% if filter %}filter={{ filter }}&{%endif%}page={{num}}">{{num}}</a></li>
|
||||
{%endfor%}{% endwith %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
14
notes/templates/notes/liste_maraudes.html
Normal file
14
notes/templates/notes/liste_maraudes.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "notes/liste.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ block.super }} Liste des maraudes
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{block.super}}
|
||||
<li><a href="{% url "notes:liste-maraude" %}">Maraudes</a></li>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
||||
39
notes/templates/notes/liste_sujets.html
Normal file
39
notes/templates/notes/liste_sujets.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "notes/liste.html" %}
|
||||
|
||||
{% block title %}{{block.super}} Liste des sujets {% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li><a href="{% url "notes:liste-sujet" %}">Sujets</a></li>
|
||||
{% if query_text %}<li>'{{query_text}}'</li>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar_insert %}
|
||||
<div class="panel panel-primary text-right">
|
||||
<div class="panel-body">
|
||||
<h4><strong>Rechercher</strong></h4>
|
||||
<form action="{% url "notes:liste-sujet" %}" method="POST" class="form form-group">{% csrf_token %}
|
||||
<div class="input-group">
|
||||
<input type="text" name="q" class="form-control" placeholder="Chercher un sujet" aria-describedby="basic-addon1">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" class="btn btn-primary"><span class=" glyphicon glyphicon-search"></span> </button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
<hr/>
|
||||
<h4><strong>Outils</strong></h4>
|
||||
<a class="btn btn-primary" href="{% url "notes:create-sujet" %}">
|
||||
<span class="glyphicon glyphicon-plus"></span> Ajouter un sujet</a> </div></div>
|
||||
{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% if query_text %}<div class="well well-sm text-center">
|
||||
<h4><span class="label label-primary">'{{query_text}}'</span>
|
||||
<span class="label label-danger">
|
||||
{% if not object_list %}Aucun résultat
|
||||
{% else %} {{ object_list.count }} résultats
|
||||
{% endif %}
|
||||
</span></h4>
|
||||
</div>{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
13
notes/templates/notes/menu.html
Normal file
13
notes/templates/notes/menu.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% load navbar %}
|
||||
<ul class="nav nav-pills nav-stacked text-right">
|
||||
<li role="presentation" {% active namespace="notes" viewname="liste-sujet" %}>
|
||||
<a href="{% url "notes:liste-sujet" %}">Par sujet
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" {% active namespace="notes" viewname="liste-maraude" %}>
|
||||
<a href="{% url "notes:liste-maraude" %}">Par maraude
|
||||
<span class="glyphicon glyphicon-road"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
9
notes/templates/notes/sujet_create.html
Normal file
9
notes/templates/notes/sujet_create.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "notes/base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include "notes/sujet_create_inner.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,5 +1,5 @@
|
||||
{% load bootstrap3 %}
|
||||
<form class="form-horizontal" action="{% url "sujets:create" %}" method="post">{% csrf_token %}
|
||||
<form class="form-horizontal" action="{% url "notes:create-sujet" %}" method="post">{% csrf_token %}
|
||||
{% bootstrap_form form layout="horizontal"%}
|
||||
<div class="pull-right">
|
||||
{% bootstrap_button "Ajouter un sujet" button_type="submit" button_class="btn btn-primary" icon="plus" %}
|
||||
12
notes/templates/notes/sujet_merge.html
Normal file
12
notes/templates/notes/sujet_merge.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{% extends "notes/base.html" %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li><a href="{% url "notes:liste-sujet" %}">Sujets</a></li>
|
||||
<li><a href="{% url "notes:details-sujet" object.pk %}">{{ object }}</a></li>
|
||||
<li>Fusionner vers...</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
{% include 'notes/sujet_merge_inner.html' %}
|
||||
{% endblock %}
|
||||
8
notes/templates/notes/sujet_merge_inner.html
Normal file
8
notes/templates/notes/sujet_merge_inner.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% load bootstrap3 %}
|
||||
<p> Vous allez fusionner la fiche de <strong>{{object}}</strong> et ses {{object.notes.count}} notes vers :</p>
|
||||
<form action="{% url 'notes:sujets-merge' pk=object.pk %}" method="POST">
|
||||
{% csrf_token %}
|
||||
{{ form.media }}
|
||||
{% bootstrap_field form.sujet %}
|
||||
<div class="pull-right">{% bootstrap_button 'Fusionner' button_type='submit' icon='paste' %}</div>
|
||||
</form>
|
||||
6
notes/templates/notes/table_cell_maraudes.html
Normal file
6
notes/templates/notes/table_cell_maraudes.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% if object.est_terminee %}<a href="{% url 'notes:details-maraude' object.id %}" class="btn btn-link">
|
||||
{% else %}<a href="#" class="btn btn-link disabled">{% endif %}{{ object }}</a>
|
||||
<div class="pull-right">
|
||||
<span class="label label-info">{{ object.binome }} & {{ object.referent }}</span>
|
||||
{% if object.est_terminee %}<span class="label label-success">{{object.rencontres.count}} rencontres</span>{% endif %}
|
||||
</div>
|
||||
9
notes/templates/notes/table_cell_sujets.html
Normal file
9
notes/templates/notes/table_cell_sujets.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<a href="{% url 'notes:details-sujet' object.pk %}" class="btn btn-link">{{object}}</a>
|
||||
<div class="pull-right" style="padding-right: 20px;">
|
||||
<span class="label label-info">{{ object.notes.count }} notes</span>
|
||||
{% with object.statistiques.info_completed as completed %}
|
||||
<span class="label label-{% if completed <= 80 %}warning{%else%}success{%endif%}">{{ completed }} %</span>
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ register = template.Library()
|
||||
|
||||
@register.inclusion_tag("notes/table_inline.html")
|
||||
def inline_table(note, header=None):
|
||||
from maraudes.models import Maraude
|
||||
bg_color, bg_label_color = note.bg_colors
|
||||
|
||||
if not header:
|
||||
@@ -14,10 +15,14 @@ def inline_table(note, header=None):
|
||||
|
||||
if header == "date":
|
||||
header_field = "created_date"
|
||||
link = None
|
||||
try:
|
||||
maraude = Maraude.objects.get(date=note.created_date)
|
||||
link = maraude.get_absolute_url()
|
||||
except Maraude.DoesNotExist:
|
||||
link = None
|
||||
elif header == "sujet":
|
||||
header_field = "sujet"
|
||||
link = reverse("suivi:details", kwargs={'pk': note.sujet.pk})
|
||||
link = note.sujet.get_absolute_url()
|
||||
|
||||
header = getattr(note, header_field)
|
||||
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
from .models import Note
|
||||
from .models import Note, Sujet
|
||||
# Create your tests here.
|
||||
|
||||
# TODO: test 'actions.py'
|
||||
|
||||
|
||||
class SujetModelTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test_statistiques_is_autocreated(self):
|
||||
new_sujet = Sujet.objects.create(prenom="Astérix")
|
||||
self.assertIsNotNone(new_sujet.statistiques)
|
||||
|
||||
def test_at_least_one_in_name_surname_firstname(self):
|
||||
self.assertIsInstance(Sujet.objects.create(nom="DeGaulle"), Sujet)
|
||||
self.assertIsInstance(Sujet.objects.create(surnom="Le Gaulois"), Sujet)
|
||||
self.assertIsInstance(Sujet.objects.create(prenom="Astérix"), Sujet)
|
||||
|
||||
def test_raises_validation_error_if_no_name(self):
|
||||
with self.assertRaises(ValidationError):
|
||||
Sujet.objects.create(age=25)
|
||||
|
||||
class NoteManagerTestCase(TestCase):
|
||||
""" managers.NoteManager Test Case """
|
||||
|
||||
16
notes/urls.py
Normal file
16
notes/urls.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name="index"),
|
||||
url(r'sujets/$', views.SujetListView.as_view(), name="liste-sujet"),
|
||||
url(r'sujets/(?P<pk>[0-9]+)/$', views.SuiviSujetView.as_view(), name="details-sujet"),
|
||||
url(r'sujets/(?P<pk>[0-9]+)/merge/$', views.MergeView.as_view(), name="sujets-merge"),
|
||||
url(r'maraudes/$', views.MaraudeListView.as_view(), name="liste-maraude"),
|
||||
url(r'maraudes/(?P<pk>[0-9]+)/$', views.CompteRenduDetailsView.as_view(), name="details-maraude"),
|
||||
# Manage Sujet
|
||||
url(r'sujets/create/$', views.SujetCreateView.as_view(), name="create-sujet"),
|
||||
url(r'sujet/(?P<pk>[0-9]+)/$', views.SujetAjaxDetailsView.as_view(), name="sujet"),
|
||||
url(r'sujet/(?P<pk>[0-9]+)/update/$', views.SujetAjaxUpdateView.as_view(), name="update-sujet"),
|
||||
]
|
||||
254
notes/views.py
Normal file
254
notes/views.py
Normal file
@@ -0,0 +1,254 @@
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from django.shortcuts import redirect, reverse
|
||||
from django.views import generic
|
||||
from django.utils import timezone
|
||||
from django.contrib import messages
|
||||
from django.http.response import HttpResponseNotAllowed
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
|
||||
from utilisateurs.mixins import MaraudeurMixin
|
||||
from maraudes.models import Maraude, CompteRendu
|
||||
from .models import Sujet, Note
|
||||
from .forms import SujetCreateForm, AutoNoteForm, SelectSujetForm
|
||||
from .mixins import NoteFormMixin
|
||||
from .actions import merge_two
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# Create your views here.
|
||||
|
||||
|
||||
|
||||
class IndexView(MaraudeurMixin, generic.TemplateView):
|
||||
template_name = "notes/index.html"
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return redirect("notes:liste-sujet")
|
||||
|
||||
class Filter:
|
||||
def __init__(self, title, name, filter_func):
|
||||
self.title = title
|
||||
self.parameter_name = name
|
||||
self.active = False
|
||||
self._filter_func = filter_func
|
||||
|
||||
def filter(self, qs):
|
||||
return self._filter_func(qs)
|
||||
|
||||
|
||||
|
||||
class ListView(MaraudeurMixin, generic.ListView):
|
||||
""" Base ListView for Maraude and Sujet lists """
|
||||
paginate_by = 30
|
||||
cell_template = None
|
||||
|
||||
filters = []
|
||||
active_filter = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._filters = {}
|
||||
|
||||
if self.filters:
|
||||
for i, (title, func) in enumerate(self.filters):
|
||||
_id = "filter_%i" % i
|
||||
self._filters[_id] = Filter(title, _id, func)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
filter_name = self.request.GET.get('filter', None)
|
||||
if filter_name:
|
||||
self.active_filter = self._filters.get(filter_name, None)
|
||||
if self.active_filter:
|
||||
self.active_filter.active = True
|
||||
|
||||
return super().get(request, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
qs = super().get_queryset()
|
||||
if self.active_filter:
|
||||
qs = self.active_filter.filter(qs)
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["filters"] = self._filters.values()
|
||||
context["table_cell_template"] = getattr(self, 'cell_template', None)
|
||||
context["table_header"] = getattr(self, 'table_header', None)
|
||||
return context
|
||||
|
||||
|
||||
class MaraudeListView(ListView):
|
||||
""" Vue de la liste des compte-rendus de maraude """
|
||||
|
||||
model = CompteRendu
|
||||
template_name = "notes/liste_maraudes.html"
|
||||
cell_template = "notes/table_cell_maraudes.html"
|
||||
table_header = "Liste des maraudes"
|
||||
|
||||
queryset = Maraude.objects.get_past().order_by("-date")
|
||||
|
||||
filters = [
|
||||
("Ce mois-ci", lambda qs: qs.filter(date__month=timezone.now().date().month)),
|
||||
]
|
||||
|
||||
|
||||
class SujetListView(ListView):
|
||||
#ListView
|
||||
model = Sujet
|
||||
template_name = "notes/liste_sujets.html"
|
||||
cell_template = "notes/table_cell_sujets.html"
|
||||
table_header = "Liste des sujets"
|
||||
|
||||
|
||||
def info_completed_filter(qs):
|
||||
excluded_set = set()
|
||||
for sujet in qs:
|
||||
if sujet.statistiques.info_completed >= 50:
|
||||
excluded_set.add(sujet.pk)
|
||||
|
||||
return qs.exclude(pk__in=excluded_set)
|
||||
|
||||
filters = [
|
||||
("Rencontré(e)s cette année", lambda qs: qs.filter(premiere_rencontre__year=timezone.now().date().year)),
|
||||
("Statistiques incomplètes", info_completed_filter),
|
||||
]
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
from watson import search as watson
|
||||
search_text = request.POST.get('q')
|
||||
results = watson.filter(Sujet, search_text)
|
||||
#logger.warning("SEARCH for %s : %s" % (search_text, results))
|
||||
if results.count() == 1:
|
||||
return redirect(results[0].get_absolute_url())
|
||||
self.queryset = results
|
||||
return self.get(request, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['query_text'] = self.request.POST.get('q', None)
|
||||
return context
|
||||
|
||||
|
||||
class DetailView(MaraudeurMixin, generic.DetailView):
|
||||
template_name = "notes/details.html"
|
||||
|
||||
class CompteRenduDetailsView(DetailView):
|
||||
""" Vue détaillé d'un compte-rendu de maraude """
|
||||
|
||||
model = CompteRendu
|
||||
context_object_name = "maraude"
|
||||
template_name = "notes/details_maraude.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['notes'] = sorted(Note.objects.get_queryset().filter(created_date=self.object.date), key=lambda n: n.created_time)
|
||||
context['next_maraude'] = Maraude.objects.get_future(
|
||||
date=self.object.date + datetime.timedelta(1)
|
||||
).filter(
|
||||
heure_fin__isnull=False
|
||||
).first()
|
||||
context['prev_maraude'] = Maraude.objects.get_past(
|
||||
date=self.object.date
|
||||
).filter(
|
||||
heure_fin__isnull=False
|
||||
).last()
|
||||
return context
|
||||
|
||||
|
||||
class SuiviSujetView(NoteFormMixin, DetailView):
|
||||
#NoteFormMixin
|
||||
forms = {
|
||||
'note': AutoNoteForm,
|
||||
}
|
||||
def get_success_url(self):
|
||||
return reverse('notes:details-sujet', kwargs={'pk': self.get_object().pk})
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['sujet'] = self.get_object()
|
||||
return kwargs
|
||||
#DetailView
|
||||
model = Sujet
|
||||
template_name = "notes/details_sujet.html"
|
||||
context_object_name = "sujet"
|
||||
# Paginator
|
||||
per_page = 5
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.paginator = Paginator(
|
||||
self.get_object().notes.by_date(reverse=True),
|
||||
self.per_page
|
||||
)
|
||||
self.page = self.request.GET.get('page', 1)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
try:
|
||||
notes = self.paginator.page(self.page)
|
||||
except PageNotAnInteger:
|
||||
notes = self.paginator.page(1)
|
||||
except EmptyPage:
|
||||
notes = self.paginator.page(self.paginator.num_pages)
|
||||
context['notes'] = notes
|
||||
return context
|
||||
|
||||
|
||||
### Sujet Management Views
|
||||
|
||||
class SujetAjaxDetailsView(generic.DetailView):
|
||||
#DetailView
|
||||
template_name = "notes/details_sujet_inner.html"
|
||||
model = Sujet
|
||||
|
||||
http_method_names = ["get"]
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
""" Redirect to complete details view if request is not ajax """
|
||||
if not self.request.is_ajax():
|
||||
return redirect("notes:details-sujet", pk=self.get_object().pk)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
class SujetAjaxUpdateView(generic.edit.UpdateView):
|
||||
#UpdateView
|
||||
template_name = "notes/details_sujet_update.html"
|
||||
model = Sujet
|
||||
fields = '__all__'
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("notes:details-sujet", kwargs={'pk': self.object.pk})
|
||||
|
||||
from website.mixins import AjaxTemplateMixin
|
||||
|
||||
class SujetCreateView(AjaxTemplateMixin, generic.edit.CreateView):
|
||||
#CreateView
|
||||
template_name = "notes/sujet_create.html"
|
||||
form_class = SujetCreateForm
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'next' in self.request.POST:
|
||||
self.success_url = self.request.POST["next"]
|
||||
return super().post(self, request, *args, **kwargs)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
try: context['next'] = self.request.GET['next']
|
||||
except:context['next'] = None
|
||||
return context
|
||||
|
||||
class MergeView(generic.DetailView, generic.FormView):
|
||||
""" Implement actions.merge_two as a view """
|
||||
|
||||
template_name = "notes/sujet_merge.html"
|
||||
model = Sujet
|
||||
form_class = SelectSujetForm
|
||||
|
||||
def form_valid(self, form):
|
||||
slave = self.get_object()
|
||||
master = form.cleaned_data['sujet']
|
||||
try:
|
||||
merge_two(master, slave)
|
||||
except Exception as e:
|
||||
logger.error("Merge: ", e)
|
||||
messages.error(self.request, "La fusion vers %s a échoué !" % master)
|
||||
return redirect(slave)
|
||||
messages.success(self.request, "%s vient d'être fusionné" % slave)
|
||||
return redirect(master)
|
||||
@@ -3,3 +3,5 @@
|
||||
django-bootstrap3
|
||||
django-select2
|
||||
django-watson
|
||||
django-graphos
|
||||
django-nose
|
||||
|
||||
81
settings.py
81
settings.py
@@ -1,26 +1,80 @@
|
||||
|
||||
import os
|
||||
from .base_settings import *
|
||||
|
||||
# Added settings
|
||||
""" These are the default settings.
|
||||
You may set them up to your needs.
|
||||
"""
|
||||
|
||||
# Localisation settings
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
|
||||
# Default settings for created Maraudeur objects.
|
||||
MARAUDEURS = {
|
||||
# Default password, TODO: users shall be asked to change it on first login.
|
||||
'password': "test",
|
||||
# The institution to which professionnals belongs.
|
||||
'organisme': {
|
||||
'nom': "ALSA",
|
||||
'email': "direction@alsa68.org"
|
||||
},
|
||||
}
|
||||
|
||||
# END OF SETTINGS
|
||||
|
||||
""" Custom settings for 'maraudes_project' application.
|
||||
DO NOT MODIFY the following settings,
|
||||
unless you know what you are doing.
|
||||
"""
|
||||
|
||||
LOGIN_URL = 'index'
|
||||
|
||||
if DEBUG:
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
else: #TODO: configure a real backend
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'utilisateurs.backends.CustomUserAuthentication'
|
||||
]
|
||||
|
||||
# Extra settings to default template engine.
|
||||
# Context processors
|
||||
TEMPLATES[0]['OPTIONS']['context_processors'] += [
|
||||
"website.context_processors.website_processor",
|
||||
]
|
||||
# Template directories
|
||||
TEMPLATES[0]['DIRS'] += [
|
||||
os.path.join(BASE_DIR, "templates"), # Custom admin templates
|
||||
]
|
||||
|
||||
# Applications
|
||||
INSTALLED_APPS += [
|
||||
# Design
|
||||
'bootstrap3',
|
||||
'django_select2',
|
||||
# Search Engine
|
||||
'watson',
|
||||
# Graph package
|
||||
'graphos',
|
||||
# Tests
|
||||
'django_nose',
|
||||
# Project apps
|
||||
'maraudes',
|
||||
'sujets',
|
||||
'notes',
|
||||
'suivi',
|
||||
'utilisateurs',
|
||||
'website',
|
||||
'maraudes',
|
||||
'notes',
|
||||
'utilisateurs',
|
||||
'statistiques',
|
||||
]
|
||||
|
||||
# django-nose
|
||||
TEST_RUNNER = "django_nose.NoseTestSuiteRunner"
|
||||
NOSE_ARGS = [
|
||||
"--with-coverage",
|
||||
"--cover-package=website,maraudes,notes,utilisateurs",
|
||||
]
|
||||
|
||||
LOGIN_URL = 'index'
|
||||
|
||||
# bootstrap3
|
||||
BOOTSTRAP3 = {
|
||||
# The URL to the jQuery JavaScript file
|
||||
'base_url': os.path.join(STATIC_URL, 'css', 'bootstrap/'),
|
||||
@@ -32,13 +86,6 @@ BOOTSTRAP3 = {
|
||||
'horizontal_field_class': 'col-md-10',
|
||||
}
|
||||
|
||||
# Django-select2 Configuration
|
||||
# django-select2
|
||||
SELECT2_JS = 'scripts/select2.min.js'
|
||||
SELECT2_CSS = 'css/select2.min.css'
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'website.backends.MyBackend'
|
||||
]
|
||||
|
||||
|
||||
|
||||
8
statistiques/apps.py
Normal file
8
statistiques/apps.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
class StatistiquesConfig(AppConfig):
|
||||
name = 'statistiques'
|
||||
|
||||
|
||||
|
||||
|
||||
70
statistiques/charts.py
Normal file
70
statistiques/charts.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from django.db.models import (Field, CharField, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from graphos.sources.simple import SimpleDataSource
|
||||
from graphos.renderers import gchart
|
||||
|
||||
|
||||
class PieWrapper(gchart.PieChart):
|
||||
""" A wrapper around gchart.PieChart that generates a graph from :
|
||||
|
||||
- a queryset and a model field (NullBooleanField or field with choices)
|
||||
- a data object and title
|
||||
"""
|
||||
|
||||
height=400
|
||||
width=800
|
||||
labels = {
|
||||
NullBooleanField: {True: "Oui", False: "Non", None:"Ne sait pas"},
|
||||
}
|
||||
|
||||
def __init__( self, queryset=None, field=None,
|
||||
data=None, title=None,
|
||||
null_values=[],**kwargs):
|
||||
if not data:
|
||||
if not isinstance(field, Field):
|
||||
raise TypeError(field, 'must be a child of django.models.db.fields.Field !')
|
||||
if not queryset:
|
||||
raise TypeError("You must give either a queryset and field or data")
|
||||
|
||||
if field.__class__ in PieWrapper.labels:
|
||||
labels = PieWrapper.labels[field.__class__]
|
||||
elif field.choices:
|
||||
labels = dict(field.choices)
|
||||
else:
|
||||
raise ValueError("Could not guess labels for", field)
|
||||
|
||||
data = ([(field.name, 'count')] + # Headers
|
||||
[(labels[item[field.name]],
|
||||
item['nbr']) for item in queryset.values(
|
||||
field.name
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
).order_by()
|
||||
if (not null_values
|
||||
or item[field] not in null_values)
|
||||
])
|
||||
|
||||
super().__init__(
|
||||
SimpleDataSource(
|
||||
data=data
|
||||
),
|
||||
options={
|
||||
'title': getattr(field, 'verbose_name', title),
|
||||
'is3D': True,
|
||||
'pieSliceText': 'value',
|
||||
'legend': {'position': 'labeled', 'maxLines': 3, 'textStyle': {'fontSize': 16,}},
|
||||
},
|
||||
width=kwargs.get('width', self.width), height=kwargs.get('height', self.height),
|
||||
)
|
||||
|
||||
def get_js_template(self):
|
||||
return "statistiques/gchart/pie_chart.html"
|
||||
|
||||
def get_html_template(self):
|
||||
return "statistiques/gchart/html.html"
|
||||
|
||||
|
||||
class ColumnWrapper(gchart.ColumnChart):
|
||||
|
||||
pass
|
||||
42
statistiques/forms.py
Normal file
42
statistiques/forms.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from django import forms
|
||||
from django.db.utils import OperationalError
|
||||
|
||||
from .models import FicheStatistique
|
||||
|
||||
class StatistiquesForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = FicheStatistique
|
||||
exclude = ["sujet"]
|
||||
|
||||
|
||||
def get_year_range():
|
||||
qs = FicheStatistique.objects.filter(
|
||||
sujet__premiere_rencontre__isnull=False
|
||||
).order_by(
|
||||
'sujet__premiere_rencontre'
|
||||
)
|
||||
year = lambda f: f.sujet.premiere_rencontre.year
|
||||
|
||||
# Need to call exists() in a try block
|
||||
# to avoid raising exception on first migration
|
||||
try:
|
||||
qs_is_not_empty = qs.exists()
|
||||
except OperationalError:
|
||||
qs_is_not_empty = False
|
||||
|
||||
if qs_is_not_empty:
|
||||
return range(year(qs.first()), year(qs.last()) + 1)
|
||||
else:
|
||||
return ()
|
||||
|
||||
class SelectRangeForm(forms.Form):
|
||||
|
||||
year = forms.ChoiceField(label="Année", choices=[(0, 'Toutes')] + [(i, str(i)) for i in get_year_range()])
|
||||
month = forms.ChoiceField(label="Mois",
|
||||
choices=[(0, 'Tous'),
|
||||
(1, 'Janvier'), (2, 'Février'), (3, 'Mars'), (4, 'Avril'),
|
||||
(5, 'Mai'), (6, 'Juin'), (7, 'Juillet'), (8, 'Août'),
|
||||
(9, 'Septembre'),(10, 'Octobre'),(11, 'Novembre'),(12, 'Décembre')
|
||||
],
|
||||
)
|
||||
88
statistiques/models.py
Normal file
88
statistiques/models.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.db import models
|
||||
from django.shortcuts import reverse
|
||||
# Create your models here.
|
||||
|
||||
NSP = "Ne sait pas"
|
||||
|
||||
# Item: Parcours institutionnel
|
||||
PARCOURS_INSTITUTIONNEL = "Institutionnel"
|
||||
PARCOURS_FAMILIAL = "Familial"
|
||||
PARCOURS_DE_VIE_CHOICES = (
|
||||
(NSP, "Ne sait pas"),
|
||||
(PARCOURS_FAMILIAL, "Parcours familial"),
|
||||
(PARCOURS_INSTITUTIONNEL, "Parcours institutionnel"),
|
||||
)
|
||||
|
||||
#Item: Type d'habitation
|
||||
HABITATION_SANS = "Sans Abri"
|
||||
HABITATION_LOGEMENT = "Logement"
|
||||
HABITATION_TIERS = "Hébergement"
|
||||
HABITATION_MAL_LOGEMENT = "Mal logé"
|
||||
HABITATION_CHOICES = (
|
||||
(NSP, "Ne sait pas"),
|
||||
(HABITATION_SANS, "Sans abri"),
|
||||
(HABITATION_TIERS, "Hébergé"),
|
||||
(HABITATION_LOGEMENT, "Logé"),
|
||||
(HABITATION_MAL_LOGEMENT, "Mal logé"),
|
||||
)
|
||||
|
||||
#Item: Ressources
|
||||
RESSOURCES_RSA = "RSA"
|
||||
RESSOURCES_AAH = "AAH"
|
||||
RESSOURCES_POLE_EMPLOI = "Pôle Emploi"
|
||||
RESSOURCES_AUTRES = "Autres"
|
||||
RESSOURCES_SANS = "Pas de ressources"
|
||||
RESSOURCES_CHOICES = (
|
||||
(NSP, "Ne sait pas"),
|
||||
(RESSOURCES_AAH, "AAH"),
|
||||
(RESSOURCES_RSA, "RSA"),
|
||||
(RESSOURCES_SANS, "Aucune"),
|
||||
(RESSOURCES_POLE_EMPLOI, "Pôle emploi"),
|
||||
(RESSOURCES_AUTRES, "Autres ressources"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class FicheStatistique(models.Model):
|
||||
|
||||
sujet = models.OneToOneField('notes.Sujet',
|
||||
on_delete=models.CASCADE,
|
||||
primary_key=True,
|
||||
related_name="statistiques")
|
||||
|
||||
# Logement
|
||||
habitation = models.CharField("Type d'habitat", max_length=64,
|
||||
choices=HABITATION_CHOICES,
|
||||
default=NSP)
|
||||
ressources = models.CharField("Ressources", max_length=64,
|
||||
choices=RESSOURCES_CHOICES,
|
||||
default=NSP)
|
||||
connu_siao = models.NullBooleanField("Connu du SIAO ?")
|
||||
|
||||
# Problématiques
|
||||
prob_psychiatrie = models.NullBooleanField("Psychiatrie")
|
||||
prob_administratif = models.NullBooleanField("Administratif")
|
||||
prob_addiction = models.NullBooleanField("Addiction")
|
||||
prob_somatique = models.NullBooleanField("Somatique")
|
||||
|
||||
lien_familial = models.NullBooleanField("Lien Familial")
|
||||
parcours_de_vie = models.CharField("Parcours de vie",
|
||||
max_length=64,
|
||||
choices=PARCOURS_DE_VIE_CHOICES,
|
||||
default=NSP)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('notes:details-sujet', kwargs={'pk': self.sujet.pk})
|
||||
|
||||
@property
|
||||
def info_completed(self):
|
||||
observed = ('prob_psychiatrie', 'prob_addiction',
|
||||
'prob_administratif', 'prob_somatique', 'habitation', 'ressources',
|
||||
'connu_siao', 'lien_familial', 'parcours_de_vie')
|
||||
completed = 0
|
||||
for f in observed:
|
||||
if getattr(self, f) == None or getattr(self, f) == NSP:
|
||||
continue
|
||||
else:
|
||||
completed += 1
|
||||
return int(completed / len(observed) * 100)
|
||||
20
statistiques/templates/statistiques/base.html
Normal file
20
statistiques/templates/statistiques/base.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block extrahead %}
|
||||
<script type="text/javascript" src="{% static "scripts/jquery.flot.min.js" %}"></script>
|
||||
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
|
||||
<script type="text/javascript">
|
||||
google.load("visualization", "1", {packages:["corechart"]});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Statistiques >{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li><a href="{% url "statistiques:index" %}">Statistiques</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{% include "statistiques/menu.html" %}
|
||||
{% endblock %}
|
||||
42
statistiques/templates/statistiques/fiche_stats_details.html
Normal file
42
statistiques/templates/statistiques/fiche_stats_details.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{% load boolean_icons bootstrap3 %}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th colspan="4" class="active">Problématiques</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Psychiatrique</th>
|
||||
<td>{{ object.prob_psychiatrie|as_icon }}</td>
|
||||
<th>Addiction</th>
|
||||
<td>{{ object.prob_addiction|as_icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Administratif</th>
|
||||
<td>{{ object.prob_administratif|as_icon }}</td>
|
||||
<th>Somatique</th>
|
||||
<td>{{ object.prob_somatique|as_icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Habitation</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{{ object.habitation }}</td>
|
||||
<th>Connu du SIAO</th>
|
||||
<td>{{ object.connu_siao|as_icon }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Ressources</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">{{ object.ressources }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Parcours de vie</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">{{ object.parcours_de_vie }}</td>
|
||||
<th>Lien familial</th>
|
||||
<td>{{ object.lien_familial|as_icon }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
44
statistiques/templates/statistiques/fiche_stats_update.html
Normal file
44
statistiques/templates/statistiques/fiche_stats_update.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{% load bootstrap3 %}
|
||||
<form action="{% url "statistiques:update" form.instance.pk %}" method="post" id="update-stats-form">{% csrf_token%}
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th colspan="4" class="active">Problématiques</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Psychiatrique</th>
|
||||
<td>{% bootstrap_field form.prob_psychiatrie show_label=False size="small" %}</td>
|
||||
<th>Addiction</th>
|
||||
<td>{% bootstrap_field form.prob_addiction show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Administratif</th>
|
||||
<td>{% bootstrap_field form.prob_administratif show_label=False size="small" %}</td>
|
||||
<th>Somatique</th>
|
||||
<td>{% bootstrap_field form.prob_somatique show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Habitation</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<td>{% bootstrap_field form.habitation show_label=False size="small" %}</td>
|
||||
<th>Connu du SIAO</th>
|
||||
<td>{% bootstrap_field form.connu_siao show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Ressources</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4">{% bootstrap_field form.ressources show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th colspan="4" class="active">Parcours de vie</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">{% bootstrap_field form.parcours_de_vie show_label=False size="small" %}</td>
|
||||
<th>Lien familial</th>
|
||||
<td>{% bootstrap_field form.lien_familial show_label=False size="small" %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type="submit" id="submit-form" class="hidden"></input>
|
||||
</form>
|
||||
7
statistiques/templates/statistiques/filter_form.html
Normal file
7
statistiques/templates/statistiques/filter_form.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{% load bootstrap3 %}
|
||||
|
||||
<h4>Période</h4>
|
||||
<form action="" method="get">
|
||||
{% bootstrap_form form layout="inline" %}
|
||||
{% bootstrap_button "Ok" button_type="submit" %}
|
||||
</form>
|
||||
5
statistiques/templates/statistiques/gchart/html.html
Normal file
5
statistiques/templates/statistiques/gchart/html.html
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="row text-center" id="wrapper-{{ chart.get_html_id}}">
|
||||
<div id="{{ chart.get_html_id }}" style="width: {{ chart.width }}px; height: {{ chart.height }}px;"></div>
|
||||
<a class="btn btn-sm btn-default" href="#" id="image-{{ chart.get_html_id }}"><span class="glyphicon glyphicon-save-file"></span> Télécharger l'image</a>
|
||||
</div>
|
||||
|
||||
13
statistiques/templates/statistiques/gchart/pie_chart.html
Normal file
13
statistiques/templates/statistiques/gchart/pie_chart.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "graphos/gchart/base.html" %}
|
||||
|
||||
{% block create_chart %}
|
||||
var chart_data = data
|
||||
var chart_div = document.getElementById('{{ chart.get_html_id }}');
|
||||
var chart = new google.visualization.PieChart(chart_div);
|
||||
|
||||
// Wait for the chart to finish drawing before calling the getImageURI() method.
|
||||
google.visualization.events.addListener(chart, 'ready', function () {
|
||||
$("#image-{{ chart.get_html_id }}").attr("href", chart.getImageURI());
|
||||
$("#wrapper-{{ chart.get_html_id}}").hide();
|
||||
});
|
||||
{% endblock %}
|
||||
60
statistiques/templates/statistiques/index.html
Normal file
60
statistiques/templates/statistiques/index.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% extends "statistiques/base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ block.super }} Maraudes{% endblock %}
|
||||
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
<li>Maraudes</li>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<div class="alert alert-info alert-dismissible">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<p>Voici les données permettant une analyse statistiques des maraudes.</p>
|
||||
<p>Vous pouvez sélectionner une période particulière ou l'ensemble des données</p>
|
||||
<p>Les données sont réparties en trois catégories, accessibles par le menu sur la gauche</p>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<h3 class="page-header">Données générales</h3>
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<span class="badge">{{ nbr_maraudes }}</span>
|
||||
Nombre de maraudes
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="badge">{{ nbr_maraudes_jour }}</span>
|
||||
dont, Maraudes de journée
|
||||
</li>
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<span class="badge">{{ nbr_rencontres }}</span>
|
||||
Nombre total de rencontres
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<span class="badge">{{ moy_rencontres }}</span>
|
||||
soit, en <strong>moyenne</strong> par maraude
|
||||
</li>
|
||||
<li class="list-group-item list-group-item-danger">
|
||||
<span class="badge">{{ nbr_sujets_rencontres }}</span>
|
||||
Nombre de sujets rencontrés
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% if rencontres_par_mois %}
|
||||
<div class="col-lg-8">
|
||||
<h3 class="page-header">Rencontres par mois</h3>
|
||||
{{ rencontres_par_mois.as_html }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
14
statistiques/templates/statistiques/menu.html
Normal file
14
statistiques/templates/statistiques/menu.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% load navbar %}
|
||||
<ul class="nav nav-pills nav-stacked text-right">
|
||||
<li role="presentation" {% active namespace="statistiques" viewname="index" %}>
|
||||
<a href="{% url "statistiques:index" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Maraudes
|
||||
<span class="glyphicon glyphicon-road"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" {% active namespace="statistiques" viewname="pies" %}>
|
||||
<a href="{% url "statistiques:pies" %}?year={{year|default:0}}{% if month %}&month={{month}}{% endif %}">Typologie du public
|
||||
<span class="glyphicon glyphicon-adjust"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
46
statistiques/templates/statistiques/typologie.html
Normal file
46
statistiques/templates/statistiques/typologie.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends "statistiques/base.html" %}
|
||||
|
||||
{% block title %}{{block.super}} Typologie{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}{{block.super}}<li>Typologie</li>{% endblock %}
|
||||
|
||||
{% block sidebar %}
|
||||
{{ block.super }}
|
||||
<hr />
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-body text-right">
|
||||
{% include "statistiques/filter_form.html" %}
|
||||
<hr />
|
||||
<p>Échantillon : {{ queryset.count }} sujets</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<script type="text/javascript">
|
||||
function hideAll() {
|
||||
{% for _, graph in graphs %}{% with graph.get_html_id as id %}
|
||||
$("#tab-{{id}}").attr("class", "");
|
||||
$("#wrapper-{{id}}").hide();
|
||||
{% endwith %}{% endfor %}
|
||||
}
|
||||
|
||||
function showGraph(id) {
|
||||
hideAll();
|
||||
$("#tab-" + id).attr("class", "active");
|
||||
$("#wrapper-" + id).show();
|
||||
}
|
||||
|
||||
/*$( function() {
|
||||
hideAll();
|
||||
});*/
|
||||
</script>
|
||||
<ul class="nav nav-tabs">
|
||||
{% for title, graph in graphs %}<li role="presentation" id="tab-{{graph.get_html_id}}"><a href="#" onclick="showGraph('{{graph.get_html_id}}');">{{ title }}</a></li>{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% for title, graph in graphs %}
|
||||
{{ graph.as_html }}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
7
statistiques/tests.py
Normal file
7
statistiques/tests.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
# MANDATORY FEATURES
|
||||
|
||||
# FicheStatistique primary key IS it's foreign sujet pk
|
||||
11
statistiques/urls.py
Normal file
11
statistiques/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url('^$', views.DashboardView.as_view(), name="index"),
|
||||
url('^charts/$', views.PieChartView.as_view(), name="pies"),
|
||||
url(r'^details/(?P<pk>[0-9]+)/$', views.StatistiquesDetailsView.as_view(), name="details"),
|
||||
url(r'^update/(?P<pk>[0-9]+)/$', views.StatistiquesUpdateView.as_view(), name="update"),
|
||||
]
|
||||
|
||||
204
statistiques/views.py
Normal file
204
statistiques/views.py
Normal file
@@ -0,0 +1,204 @@
|
||||
import datetime
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.contrib import messages
|
||||
from django.views import generic
|
||||
from django.db.models import (Field, CharField, NullBooleanField,
|
||||
Count,
|
||||
)
|
||||
from django.db.models.functions.datetime import ExtractMonth
|
||||
from graphos.sources.simple import SimpleDataSource
|
||||
from graphos.renderers import gchart
|
||||
|
||||
from .models import FicheStatistique
|
||||
from .forms import StatistiquesForm, SelectRangeForm
|
||||
from .charts import PieWrapper, ColumnWrapper
|
||||
|
||||
from maraudes.notes import Observation
|
||||
from maraudes.models import Maraude
|
||||
from notes.models import Sujet
|
||||
|
||||
###
|
||||
|
||||
|
||||
nom_mois = {
|
||||
1: "Janvier",
|
||||
2: "Février",
|
||||
3: "Mars",
|
||||
4: "Avril",
|
||||
5: "Mai",
|
||||
6: "Juin",
|
||||
7: "Juillet",
|
||||
8: "Août",
|
||||
9: "Septembre",
|
||||
10: "Octobre",
|
||||
11: "Novembre",
|
||||
12: "Décembre"
|
||||
}
|
||||
|
||||
|
||||
class FilterMixin(generic.edit.FormMixin):
|
||||
|
||||
form_class = SelectRangeForm
|
||||
|
||||
def get_initial(self):
|
||||
return {'month': self.request.GET.get('month', 0), 'year': self.request.GET.get('year', 0) }
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.year = int(self.request.GET.get('year', 0))
|
||||
self.month = int(self.request.GET.get('month', 0))
|
||||
return super().get(self, *args, **kwargs)
|
||||
|
||||
def _filters(self, prefix):
|
||||
return {'%s__%s' % (prefix, attr): getattr(self, attr) for attr in ('year', 'month')
|
||||
if getattr(self, attr) > 0 }
|
||||
|
||||
def get_observations_queryset(self):
|
||||
return Observation.objects.filter(**self._filters('created_date'))
|
||||
|
||||
def get_maraudes_queryset(self):
|
||||
return Maraude.objects.filter(**self._filters('date'))
|
||||
|
||||
def get_fichestatistiques_queryset(self):
|
||||
return FicheStatistique.objects.filter(pk__in=self.get_observations_queryset().values_list('sujet'))
|
||||
|
||||
def get_sujets_queryset(self):
|
||||
return Sujet.objects.filter(pk__in=self.get_observations_queryset().values_list('sujet'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['year'] = self.year
|
||||
context['month'] = self.month
|
||||
return context
|
||||
|
||||
|
||||
NO_DATA = "Aucune donnée"
|
||||
|
||||
class DashboardView(FilterMixin, generic.TemplateView):
|
||||
template_name = "statistiques/index.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
maraudes = self.get_maraudes_queryset()
|
||||
rencontres = self.get_observations_queryset()
|
||||
|
||||
context['nbr_maraudes'] = maraudes.count() or NO_DATA
|
||||
context['nbr_maraudes_jour'] = maraudes.filter(
|
||||
heure_debut=datetime.time(16,00)
|
||||
).count() or NO_DATA
|
||||
context['nbr_rencontres'] = rencontres.count() or NO_DATA
|
||||
try:
|
||||
context['moy_rencontres'] = int(context['nbr_rencontres'] / context['nbr_maraudes'])
|
||||
except (ZeroDivisionError, TypeError):
|
||||
context['moy_rencontres'] = NO_DATA
|
||||
|
||||
if self.year and not self.month: #Show rencontres_par_mois graph
|
||||
par_mois = rencontres.order_by().annotate(
|
||||
mois=ExtractMonth('created_date')
|
||||
).values(
|
||||
'mois'
|
||||
).annotate(
|
||||
nbr=Count('pk')
|
||||
)
|
||||
context['rencontres_par_mois'] = ColumnWrapper(
|
||||
SimpleDataSource(
|
||||
[("Mois", "Rencontres")] +
|
||||
[(nom_mois[item['mois']], item['nbr']) for item in par_mois]
|
||||
),
|
||||
options = {
|
||||
"title": "Nombre de rencontres par mois"
|
||||
}
|
||||
)
|
||||
|
||||
# Graph: Fréquence de rencontres par sujet
|
||||
|
||||
nbr_rencontres = rencontres.values('sujet').annotate(nbr=Count('pk')).order_by()
|
||||
context['nbr_sujets_rencontres'] = nbr_rencontres.count()
|
||||
|
||||
|
||||
categories = (
|
||||
('Rencontre unique', (1,)),
|
||||
('Entre 2 et 5 rencontres', range(2,6)),
|
||||
('Entre 6 et 20 rencontres', range(6,20)),
|
||||
('Plus de 20 rencontres', range(20,999)),
|
||||
)
|
||||
get_count_for_range = lambda rg: nbr_rencontres.filter(nbr__in=rg).count()
|
||||
context['graph_rencontres'] = PieWrapper(
|
||||
data= [('Type de rencontre', 'Nombre de sujets')] +
|
||||
[(label, get_count_for_range(rg)) for label, rg in categories],
|
||||
title= 'Fréquence de rencontres'
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
|
||||
class PieChartView(FilterMixin, generic.TemplateView):
|
||||
template_name = "statistiques/typologie.html"
|
||||
|
||||
def get_graphs(self):
|
||||
sujets = self.get_sujets_queryset()
|
||||
# Insertion des champs 'âge' et 'genre' du modèle notes.Sujet
|
||||
for field in Sujet._meta.fields:
|
||||
if field.name == 'genre':
|
||||
yield str(field.verbose_name), PieWrapper(sujets, field)
|
||||
if field.name == 'age':
|
||||
categories = (
|
||||
('Mineurs', range(0,18)),
|
||||
('18-24', range(18,25)),
|
||||
('25-34', range(25,35)),
|
||||
('35-44', range(35,45)),
|
||||
('45-54', range(45,55)),
|
||||
('+ de 55', range(55,110)),
|
||||
)
|
||||
nbr_sujets = lambda rg: sujets.filter(age__in=rg).count()
|
||||
|
||||
yield "Âge", PieWrapper(
|
||||
data=[("age", "count")] +
|
||||
[(label, nbr_sujets(rg))
|
||||
for label, rg in categories] +
|
||||
[("Ne sait pas", sujets.filter(age=None).count())],
|
||||
title="Âge des sujets")
|
||||
|
||||
# Puis des champs du modèle statistiques.FicheStatistique
|
||||
# dans leur ordre de déclaration
|
||||
queryset = self.get_fichestatistiques_queryset()
|
||||
for field in FicheStatistique._meta.fields:
|
||||
if field.__class__ in (NullBooleanField, CharField):
|
||||
yield str(field.verbose_name), PieWrapper(queryset, field)
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['graphs'] = [(title, graph) for title, graph in self.get_graphs()]
|
||||
context['queryset'] = self.get_fichestatistiques_queryset()
|
||||
return context
|
||||
|
||||
|
||||
|
||||
# AjaxMixin
|
||||
|
||||
class AjaxOrRedirectMixin:
|
||||
""" For view that should be retrieved by Ajax only. If not,
|
||||
redirects to the primary view where these are displayed """
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
""" Redirect to complete details view if request is not ajax """
|
||||
if not self.request.is_ajax():
|
||||
return redirect("notes:details-sujet", pk=self.get_object().pk)
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class StatistiquesDetailsView(AjaxOrRedirectMixin, generic.DetailView):
|
||||
|
||||
model = FicheStatistique
|
||||
template_name = "statistiques/fiche_stats_details.html"
|
||||
|
||||
|
||||
|
||||
class StatistiquesUpdateView(AjaxOrRedirectMixin, generic.UpdateView):
|
||||
|
||||
model = FicheStatistique
|
||||
form_class = StatistiquesForm
|
||||
template_name = "statistiques/fiche_stats_update.html"
|
||||
@@ -1,16 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SuiviConfig(AppConfig):
|
||||
name = 'suivi'
|
||||
|
||||
from utilisateurs.models import Maraudeur
|
||||
from website.decorators import Webpage
|
||||
suivi = Webpage("suivi", icon="eye-open", defaults={
|
||||
'restricted': [Maraudeur],
|
||||
'ajax': False,
|
||||
}
|
||||
)
|
||||
|
||||
suivi.app_menu.add_link(('Liste des sujets', 'suivi:liste', 'list'))
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
from .notes import *
|
||||
from notes.forms import *
|
||||
from sujets.models import Sujet, GENRE_CHOICES
|
||||
from django import forms
|
||||
|
||||
class AppelForm(UserNoteForm):
|
||||
class Meta(UserNoteForm.Meta):
|
||||
model = Appel
|
||||
fields = ['sujet', 'text', 'entrant', 'created_date', 'created_time']
|
||||
|
||||
class SignalementForm(UserNoteForm):
|
||||
|
||||
nom = forms.CharField(64, required=False)
|
||||
prenom = forms.CharField(64, required=False)
|
||||
age = forms.IntegerField(required=False)
|
||||
genre = forms.ChoiceField(choices=GENRE_CHOICES)
|
||||
|
||||
class Meta(UserNoteForm.Meta):
|
||||
model = Signalement
|
||||
fields = ['text', 'source', 'created_date', 'created_time']
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.cleaned_data['nom'] and not self.cleaned_data['prenom']:
|
||||
self.add_error('nom', '')
|
||||
self.add_error('prenom', '')
|
||||
raise forms.ValidationError("Entrez au moins un nom ou prénom")
|
||||
|
||||
def save(self, commit=True):
|
||||
sujet = Sujet.objects.create(
|
||||
nom=self.cleaned_data['nom'],
|
||||
prenom=self.cleaned_data['prenom'],
|
||||
genre=self.cleaned_data['genre'],
|
||||
age=self.cleaned_data['age']
|
||||
)
|
||||
instance = super().save(commit=False)
|
||||
instance.sujet = sujet
|
||||
if commit:
|
||||
instance.save()
|
||||
return instance
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -1,20 +0,0 @@
|
||||
from django.db import models
|
||||
from notes.models import Note
|
||||
|
||||
class Appel(Note):
|
||||
|
||||
entrant = models.BooleanField( "Appel entrant ?")
|
||||
|
||||
def note_labels(self): return ["Reçu" if self.entrant else "Émis", self.created_by]
|
||||
def note_bg_colors(self): return ("warning", "info")
|
||||
|
||||
|
||||
class Signalement(Note):
|
||||
|
||||
source = models.ForeignKey("utilisateurs.Organisme")
|
||||
|
||||
def note_labels(self): return [self.source, self.created_by]
|
||||
def note_bg_colors(self): return ('warning', 'info')
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
<div class="col-lg-6 col-md-12">
|
||||
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
{% include "suivi/sujet_suivi.html" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="notesAjoutHeading">
|
||||
<h4 class="panel-title">
|
||||
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNotesAjout" aria-expanded="false" aria-controls="collapseTwo">
|
||||
{% bootstrap_icon "plus" %} Ajouter une note
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapseNotesAjout" class="panel-collapse collapse" role="tabpanel" aria-labelledby="notesAjoutHeading">
|
||||
<div class="panel-body">
|
||||
<form method="POST" action="">{% csrf_token %}
|
||||
{% bootstrap_form note_form show_label=False %}
|
||||
</div>
|
||||
<div class="panel-footer text-right">
|
||||
{% bootstrap_button "Enregistrer" button_type="submit" %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-md-12"> {% include "sujets/sujet_details.html" %}</div>
|
||||
@@ -1,14 +0,0 @@
|
||||
<div class="col-md-12 col-lg-6">
|
||||
<h4>Ces derniers temps</h4>
|
||||
<p> Nous avons rencontré {{ derniers_sujets }}. </p>
|
||||
<h4>Vigilance</h4>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 col-lg-6">
|
||||
<h4><strong>Créer une note :</strong></h4>
|
||||
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
|
||||
{% include "suivi/appel_form.html" with form=appel_form %}
|
||||
{% include "suivi/signalement_form.html" with form=signalement_form %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
{% load bootstrap3 %}
|
||||
<li class="app-menu">
|
||||
<a href="{% url 'sujets:liste' %}">Liste des sujets
|
||||
<span class="pull-right">{% bootstrap_icon "list" %}</span></a>
|
||||
</li>
|
||||
{% if user.is_superuser %}
|
||||
<li class="dropdown app-menu">
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
Administration <b class="caret"></b>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{% url 'sujets:create' %}">{% bootstrap_icon "plus" %} Nouveau sujet</a></li>
|
||||
<li><a href="{% url 'admin:app_list' app_label='sujets' %}">
|
||||
{% bootstrap_icon "wrench" %} Gérer les sujets</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@@ -1,16 +0,0 @@
|
||||
{% load notes %}
|
||||
<div class="panel panel-primary">
|
||||
<div class="panel-heading" role="tab" id="notesSujetHeading">
|
||||
<h3 class="panel-title">
|
||||
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNotesSujet" aria-expanded="true" aria-controls="collapseOne">
|
||||
Notes</a><span class="pull-right label">Total : {{ notes.count }}</span></h3>
|
||||
</div>
|
||||
<div id="collapseNotesSujet" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="notesSujetHeading">
|
||||
<table class="table table-striped table-bordered">
|
||||
{% for note in notes %}
|
||||
{% inline_table note header="date" %}
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name="index"),
|
||||
url(r'liste/$', views.SujetListView.as_view(), name="liste"),
|
||||
url(r'(?P<pk>[0-9]+)/$', views.SuiviSujetView.as_view(), name="details"),
|
||||
]
|
||||
@@ -1,90 +0,0 @@
|
||||
from django.shortcuts import reverse, redirect
|
||||
from django.views import generic
|
||||
from django.utils import timezone
|
||||
|
||||
from sujets.models import Sujet
|
||||
from .forms import *
|
||||
from notes.mixins import NoteFormMixin
|
||||
from notes.forms import AutoNoteForm
|
||||
# Create your views here.
|
||||
|
||||
|
||||
from maraudes.compte_rendu import CompteRendu
|
||||
|
||||
def derniers_sujets_rencontres():
|
||||
""" Renvoie le 'set' des sujets rencontrés dans les deux dernières maraudes """
|
||||
sujets = set()
|
||||
|
||||
# Issue: Récupère des comptes-rendus, même s'il n'ont pas été rédigé. Ne devrait pas
|
||||
# être un souci si on reste à jour, mais sinon...
|
||||
for cr in list(CompteRendu.objects.all())[-2:]:
|
||||
for obs in cr.get_observations():
|
||||
sujets.add(obs.sujet)
|
||||
return sujets
|
||||
|
||||
from .apps import suivi
|
||||
|
||||
@suivi.using(title=("Suivi", "Tableau de bord"))
|
||||
class IndexView(NoteFormMixin, generic.TemplateView):
|
||||
#NoteFormMixin
|
||||
forms = {
|
||||
'appel': AppelForm,
|
||||
'signalement': SignalementForm,
|
||||
}
|
||||
def get_initial(self):
|
||||
return {'created_date': timezone.localtime(timezone.now()).date(),
|
||||
'created_time': timezone.localtime(timezone.now()).time()}
|
||||
def get_success_url(self):
|
||||
return reverse('suivi:index')
|
||||
#TemplateView
|
||||
template_name = "suivi/index.html"
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['derniers_sujets'] = ", ".join(map(str, derniers_sujets_rencontres()))
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@suivi.using(title=('Liste des sujets',))
|
||||
class SujetListView(generic.ListView):
|
||||
#ListView
|
||||
model = Sujet
|
||||
template_name = "sujets/sujet_liste.html"
|
||||
paginate_by = 30
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
from watson import search as watson
|
||||
search_text = request.POST.get('q')
|
||||
results = watson.filter(Sujet, search_text)
|
||||
if results.count() == 1:
|
||||
return redirect(results[0].get_absolute_url())
|
||||
self.queryset = results
|
||||
return self.get(request, **kwargs)
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['query_text'] = self.request.POST.get('q', None)
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@suivi.using(title=('{{sujet}}', 'suivi'))
|
||||
class SuiviSujetView(NoteFormMixin, generic.DetailView):
|
||||
#NoteFormMixin
|
||||
forms = {
|
||||
'note': AutoNoteForm,
|
||||
}
|
||||
def get_success_url(self):
|
||||
return reverse('suivi:details', kwargs={'pk': self.get_object().pk})
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['sujet'] = self.get_object()
|
||||
return kwargs
|
||||
#DetailView
|
||||
model = Sujet
|
||||
template_name = "suivi/details.html"
|
||||
context_object_name = "sujet"
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['notes'] = self.object.notes.by_date(reverse=True)
|
||||
return context
|
||||
@@ -1 +0,0 @@
|
||||
default_app_config = 'sujets.apps.SujetsConfig'
|
||||
@@ -1,14 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Sujet
|
||||
|
||||
|
||||
@admin.register(Sujet)
|
||||
class SujetAdmin(admin.ModelAdmin):
|
||||
|
||||
fieldsets = [
|
||||
('Identité', {'fields': [('nom', 'prenom'), 'genre']}),
|
||||
('Informations', {'fields': ['age', ]}),
|
||||
]
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from watson import search as watson
|
||||
|
||||
class SujetsConfig(AppConfig):
|
||||
name = 'sujets'
|
||||
|
||||
def ready(self):
|
||||
Sujet = self.get_model("Sujet")
|
||||
watson.register(Sujet, fields=('nom', 'prenom', 'surnom'))
|
||||
@@ -1,18 +0,0 @@
|
||||
from django.forms import ModelForm
|
||||
from django.forms.extras.widgets import SelectDateWidget
|
||||
from .models import Sujet
|
||||
|
||||
import datetime
|
||||
|
||||
current_year = datetime.date.today().year
|
||||
YEAR_CHOICE = tuple(year - 2 for year in range(current_year, current_year + 10))
|
||||
|
||||
class SujetCreateForm(ModelForm):
|
||||
class Meta:
|
||||
model = Sujet
|
||||
fields = ['nom', 'surnom', 'prenom', 'genre', 'premiere_rencontre']
|
||||
widgets = {
|
||||
'premiere_rencontre': SelectDateWidget( empty_label=("Année", "Mois", "Jour"),
|
||||
years = YEAR_CHOICE,
|
||||
),
|
||||
}
|
||||
135
sujets/models.py
135
sujets/models.py
@@ -1,135 +0,0 @@
|
||||
from django.utils import timezone
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from django.db import models
|
||||
|
||||
### Item choices
|
||||
|
||||
# Item: Parcours institutionnel
|
||||
PARCOURS_INSTITUTIONNEL = "Institutionnel"
|
||||
PARCOURS_FAMILIAL = "Familial"
|
||||
PARCOURS_NR = "Non renseigné"
|
||||
PARCOURS_DE_VIE_CHOICES = (
|
||||
(PARCOURS_FAMILIAL, "Parcours familial"),
|
||||
(PARCOURS_INSTITUTIONNEL, "Parcours institutionnel"),
|
||||
(PARCOURS_NR, "Ne sait pas"),
|
||||
)
|
||||
|
||||
#Item: Type d'habitation
|
||||
HABITATION_SANS = "Sans Abri"
|
||||
HABITATION_LOGEMENT = "Logement"
|
||||
HABITATION_TIERS = "Hébergement"
|
||||
HABITATION_MAL_LOGEMENT = "Mal logé"
|
||||
HABITATION_NR = "Non renseigné"
|
||||
HABITATION_CHOICES = (
|
||||
(HABITATION_SANS, "Sans abri"),
|
||||
(HABITATION_TIERS, "Hébergé"),
|
||||
(HABITATION_LOGEMENT, "Logé"),
|
||||
(HABITATION_MAL_LOGEMENT, "Mal logé"),
|
||||
(HABITATION_NR, "Ne sait pas"),
|
||||
)
|
||||
|
||||
#Item: Ressources
|
||||
RESSOURCES_RSA = "RSA"
|
||||
RESSOURCES_AAH = "AAH"
|
||||
RESSOURCES_POLE_EMPLOI = "Pôle Emploi"
|
||||
RESSOURCES_AUTRES = "Autres"
|
||||
RESSOURCES_SANS = "Pas de ressources"
|
||||
RESSOURCES_NR = "Non renseigné"
|
||||
RESSOURCES_CHOICES = (
|
||||
(RESSOURCES_AAH, "AAH"),
|
||||
(RESSOURCES_RSA, "RSA"),
|
||||
(RESSOURCES_SANS, "Aucune"),
|
||||
(RESSOURCES_POLE_EMPLOI, "Pôle emploi"),
|
||||
(RESSOURCES_AUTRES, "Autres ressources"),
|
||||
(RESSOURCES_NR, "Ne sait pas")
|
||||
)
|
||||
|
||||
|
||||
|
||||
### Models
|
||||
# - Personne
|
||||
# - Sujet
|
||||
|
||||
HOMME = 'M'
|
||||
FEMME = 'Mme'
|
||||
GENRE_CHOICES = (
|
||||
(HOMME, 'Homme'),
|
||||
(FEMME, 'Femme'),
|
||||
)
|
||||
|
||||
class Personne(models.Model):
|
||||
""" Modèle de base d'une personne
|
||||
- genre
|
||||
- nom
|
||||
- prénom
|
||||
"""
|
||||
|
||||
genre = models.CharField(max_length=3,
|
||||
choices=GENRE_CHOICES,
|
||||
default=HOMME)
|
||||
nom = models.CharField(max_length=32, blank=True)
|
||||
prenom = models.CharField(max_length=32, blank=True)
|
||||
surnom = models.CharField(max_length=64, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
string = '%s ' % self.genre
|
||||
if self.nom: string += '%s ' % self.nom
|
||||
if self.surnom: string += '"%s" ' % self.surnom
|
||||
if self.prenom: string += '%s' % self.prenom
|
||||
return string
|
||||
|
||||
def clean(self):
|
||||
if not any([self.nom, self.prenom, self.surnom]):
|
||||
raise ValidationError(_("Vous devez remplir au moins un nom, prénom ou surnom"))
|
||||
return super().clean()
|
||||
|
||||
#TODO:
|
||||
# Il serait préférable de séparer le Sujet (nom, prénom, age)
|
||||
# des données utilisées pour les statistiques
|
||||
# Solution : nouveau modèle "Informations" avec OneToOneRelation vers Sujet
|
||||
# Cette classe pourra être déplacée dans le module 'statistiques'
|
||||
|
||||
class Sujet(Personne):
|
||||
""" Personne faisant l'objet d'un suivi par la maraude
|
||||
"""
|
||||
# referent = models.ForeignKey("utilisateurs.Professionnel", related_name="suivis")
|
||||
premiere_rencontre = models.DateField(
|
||||
blank=True, null=True,
|
||||
default=timezone.now
|
||||
)
|
||||
age = models.SmallIntegerField(
|
||||
blank=True, null=True
|
||||
)
|
||||
|
||||
lien_familial = models.NullBooleanField("Lien Familial")
|
||||
parcours_de_vie = models.CharField(max_length=64,
|
||||
choices=PARCOURS_DE_VIE_CHOICES,
|
||||
default=PARCOURS_NR)
|
||||
|
||||
# Problématiques
|
||||
prob_psychiatrie = models.NullBooleanField("Psychiatrie")
|
||||
prob_administratif = models.NullBooleanField("Administratif")
|
||||
prob_addiction = models.NullBooleanField("Addiction")
|
||||
prob_somatique = models.NullBooleanField("Somatique")
|
||||
|
||||
# Logement
|
||||
habitation = models.CharField("Type d'habitat", max_length=64,
|
||||
choices=HABITATION_CHOICES,
|
||||
default=HABITATION_NR)
|
||||
ressources = models.CharField("Ressources", max_length=64,
|
||||
choices=RESSOURCES_CHOICES,
|
||||
default=RESSOURCES_NR)
|
||||
connu_siao = models.NullBooleanField("Connu du SIAO ?")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Sujet"
|
||||
ordering = ('surnom', 'nom', 'prenom')
|
||||
permissions = (
|
||||
('view_sujets', "Accès à l'application 'sujets'"),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('suivi:details', kwargs={'pk': self.id})
|
||||
@@ -1,6 +0,0 @@
|
||||
{% extends "base_site.html" %}
|
||||
|
||||
{% block title %}{% endblock %}
|
||||
|
||||
{% block page_header %}{% endblock %}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<a href="{% url 'suivi:details' object.pk %}" class="btn btn-link">{{object}}</a>
|
||||
<div class="pull-right" style="padding-right: 20px;">
|
||||
<span class="label label-info">{{ object.notes.count }} notes</span>
|
||||
</div>
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user