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__"
|
||||
|
||||
Reference in New Issue
Block a user