Remaster (#38)

* setup new 'statistiques' module

* added 'graphos' package and created first test graph

* put graphos in requirements, deleted local folder

* added "load_csv" management command !

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

* added missing urls.py file

* added 'merge' action and view

* added 'info_completed' ratio

* linked sujets:merge views inside suivi:details

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

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

* small cleanup

* worked on Maraude and Sujet lists

* corrected missing line in notes.__init__

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

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

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

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

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

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

* added error-prone cases in paginator

* fixed non-working modals, added titles

* added UpdateStatistiques capacity in CompteRenduCreate view

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

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

* re-instated statistiques module link and first test page

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

* Added PieChart view for FicheStatistique fields

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

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

* small theme change

* removed some dead code

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

* added some tests

* added customised admin templates

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

* added django-nose for test coverage

* Corrected raising exception on first migration

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

* Better try block

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

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

View File

@@ -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')

View File

@@ -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)

View File

@@ -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

View File

@@ -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")

View 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)

View File

@@ -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 """

View File

@@ -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:

View File

@@ -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')

View File

@@ -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>

View File

@@ -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">&times;</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>

View File

@@ -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>

View 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 %}

View 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">&times;</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 %}

View File

@@ -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>

View File

@@ -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 %}

View 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 %}

View File

@@ -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 %}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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&nbsp;
<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&nbsp;
<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&nbsp;
<span class="glyphicon glyphicon-pencil"></span></a>
</li> {% endif %}
<li role="presentation" {% active namespace="maraudes" viewname="planning" %}>
<a href="{% url "maraudes:planning" %}">Planning&nbsp;
<span class="glyphicon glyphicon-calendar"></span>
</a>
</li>
</ul>

View File

@@ -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>

View 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 %}

View File

@@ -0,0 +1 @@
<a href="{% url "notes:details-sujet" object.pk %}">{{ object }}</a>

View 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>

View File

@@ -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>

View File

@@ -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):

View File

@@ -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"),
]

View File

@@ -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__"