diff --git a/maraudes/admin.py b/maraudes/admin.py
index ee5d207..67c7678 100644
--- a/maraudes/admin.py
+++ b/maraudes/admin.py
@@ -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')
diff --git a/maraudes/apps.py b/maraudes/apps.py
index 1b48106..c051295 100644
--- a/maraudes/apps.py
+++ b/maraudes/apps.py
@@ -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)
-
diff --git a/maraudes/compte_rendu.py b/maraudes/compte_rendu.py
deleted file mode 100644
index 5dd6aac..0000000
--- a/maraudes/compte_rendu.py
+++ /dev/null
@@ -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
-
-
diff --git a/maraudes/forms.py b/maraudes/forms.py
index d2eb6d4..e1fb0b0 100644
--- a/maraudes/forms.py
+++ b/maraudes/forms.py
@@ -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")
diff --git a/maraudes/management/commands/load_csv.py b/maraudes/management/commands/load_csv.py
new file mode 100644
index 0000000..1b32026
--- /dev/null
+++ b/maraudes/management/commands/load_csv.py
@@ -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)
diff --git a/maraudes/managers.py b/maraudes/managers.py
index 1ec40ef..6e47710 100644
--- a/maraudes/managers.py
+++ b/maraudes/managers.py
@@ -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 """
diff --git a/maraudes/models.py b/maraudes/models.py
index 10b10dd..d3a6e15 100644
--- a/maraudes/models.py
+++ b/maraudes/models.py
@@ -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:
diff --git a/maraudes/notes.py b/maraudes/notes.py
index 5931fe4..6d317e0 100644
--- a/maraudes/notes.py
+++ b/maraudes/notes.py
@@ -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')
+
diff --git a/maraudes/templates/compte_rendu/compterendu.html b/maraudes/templates/compte_rendu/compterendu.html
deleted file mode 100644
index b13e306..0000000
--- a/maraudes/templates/compte_rendu/compterendu.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% load notes %}
-
-
- {% for note in notes %}
- {% inline_table note header="sujet" %}
- {% endfor %}
-
-
-
-
-
Informations
-
Rencontres : {{ maraude.rencontre_count}}
-
Personnes rencontrées : {{ maraude.observation_count}}
-
-
diff --git a/maraudes/templates/compte_rendu/compterendu_create.html b/maraudes/templates/compte_rendu/compterendu_create.html
deleted file mode 100644
index c807071..0000000
--- a/maraudes/templates/compte_rendu/compterendu_create.html
+++ /dev/null
@@ -1,104 +0,0 @@
-{% load bootstrap3 %}{% load staticfiles %}
-
-
-
-
-
-
-
-
Créer un nouvel objet :
-
-
-
-
-
-
-
- {{ form.media.js }}{{ form.media.css }}
-
-
-
-
Enregistrées
-
- {% for rencontre in rencontres %}| {{ rencontre }} |
- {% for observation in rencontre.observations.all %}
- | {{observation.sujet}} |
- {{observation.text}} |
-
{% endfor %}{% endfor %}
-
-
-
-
-
-
-
-
-
diff --git a/maraudes/templates/compte_rendu/compterendu_update.html b/maraudes/templates/compte_rendu/compterendu_update.html
deleted file mode 100644
index 5cfb17c..0000000
--- a/maraudes/templates/compte_rendu/compterendu_update.html
+++ /dev/null
@@ -1,22 +0,0 @@
-{% load bootstrap3 %}
-
diff --git a/maraudes/templates/maraudes/base.html b/maraudes/templates/maraudes/base.html
new file mode 100644
index 0000000..e64052c
--- /dev/null
+++ b/maraudes/templates/maraudes/base.html
@@ -0,0 +1,14 @@
+{% extends "base_site.html" %}
+
+{% block title %} Maraudes > {% endblock %}
+
+{% block breadcrumbs %}
+ {{ date }}
+{% endblock %}
+
+{% block sidebar %}
+
+
+ {% include "maraudes/menu.html" %}
+
+
{% endblock %}
diff --git a/maraudes/templates/maraudes/compterendu.html b/maraudes/templates/maraudes/compterendu.html
new file mode 100644
index 0000000..db123ce
--- /dev/null
+++ b/maraudes/templates/maraudes/compterendu.html
@@ -0,0 +1,169 @@
+{% extends "maraudes/base.html" %}
+{% load bootstrap3 staticfiles %}
+
+{% block title %} {{ block.super }} Compte-rendu du {{ object.date }} {% endblock %}
+
+{% block breadcrumbs %}
+ {{ object.date }}
+ Compte-rendu
+{% endblock %}
+
+{% block sidebar %}
+ {{ block.super }}
+
+
+
{% bootstrap_icon "plus" %} Création
+
+
+
+
+
+
Finaliser
+
+
+
+{% endblock %}
+
+{% block page_content %}
+
+
+
+
+
+
+ {{ form.media.js }}{{ form.media.css }}
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/maraudes/templates/compte_rendu/compterendu_form.html b/maraudes/templates/maraudes/compterendu_form.html
similarity index 98%
rename from maraudes/templates/compte_rendu/compterendu_form.html
rename to maraudes/templates/maraudes/compterendu_form.html
index e53bb8e..6e76dcd 100644
--- a/maraudes/templates/compte_rendu/compterendu_form.html
+++ b/maraudes/templates/maraudes/compterendu_form.html
@@ -6,9 +6,11 @@
-
+
+
{% bootstrap_field form.duree layout="inline" size="small" %}
+
{{ inline_formset.management_form }}
{% for form in inline_formset %}
@@ -23,5 +25,4 @@
{% endfor %}
-
diff --git a/maraudes/templates/maraudes/details.html b/maraudes/templates/maraudes/details.html
deleted file mode 100644
index b5ff6a1..0000000
--- a/maraudes/templates/maraudes/details.html
+++ /dev/null
@@ -1,7 +0,0 @@
-{% if maraude.est_terminee %}
- {% include "compte_rendu/compterendu.html" %}
-{% else %}
- {% if perms.maraudes.can_add_compterendu %}Écrire le compte-rendu
- {% else %} Le compte-rendu n'a pas encore été écrit
{% endif %}
-{% endif %}
-
diff --git a/maraudes/templates/maraudes/finalize.html b/maraudes/templates/maraudes/finalize.html
new file mode 100644
index 0000000..8b6cd35
--- /dev/null
+++ b/maraudes/templates/maraudes/finalize.html
@@ -0,0 +1,46 @@
+{% extends "maraudes/base.html" %}
+{% load bootstrap3 %}
+{% block title %} {{ block.super }} Compte-rendu du {{ object.date }} {% endblock %}
+
+{% block breadcrumbs %}
+ {{ object.date }}
+ Transmission
+{% endblock %}
+
+
+{% block page_content %}
+{% if object.est_terminee %}Ce compte-rendu a déjà été finalisé !
+{% else %}
+
+
+{% endif %}
+
+{% endblock %}
diff --git a/maraudes/templates/maraudes/index.html b/maraudes/templates/maraudes/index.html
index 029568c..5a66747 100644
--- a/maraudes/templates/maraudes/index.html
+++ b/maraudes/templates/maraudes/index.html
@@ -1,5 +1,15 @@
+{% extends "maraudes/base.html" %}
+
+{% block title %} {{ block.super }} Tableau de bord {% endblock %}
+
+{% block breadcrumbs %}
+ {{ block.super }}
+ Tableau de bord
+{% endblock %}
+
+{% block page_content %}
{% load tables %}
-
+
Votre prochaine maraude
@@ -11,24 +21,32 @@
avec {% if user.is_superuser %}{{prochaine_maraude.binome}}{%else%}{{prochaine_maraude.referent}}{%endif%}.
- {% if prochaine_maraude.est_terminee %}
-
- Voir le compte-rendu
- {%else%}
-
- Rédiger le compte-rendu
- {% endif %}
+
{% else %}
Aucune maraude prévue.
{% endif %}
-
-{% if user.is_superuser %}
-
+{% if derniers_sujets_rencontres %}
+
+
+
Ces derniers temps...
+
+ {% table derniers_sujets_rencontres cols=3 cell_template="maraudes/table_cell_derniers_sujets.html" %}
+
+{% endif %}
+{% if user.is_superuser and missing_cr %}
Compte-rendus en retard
- {% 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" %}
-
{% endif %}
+
+
+
Nouvelle note :
+
+ {% include "notes/form_appel.html" with form=appel_form %}
+ {% include "notes/form_signalement.html" with form=signalement_form %}
+
+
+{% endblock %}
diff --git a/maraudes/templates/maraudes/lieu_create.html b/maraudes/templates/maraudes/lieu_create.html
index 0664c35..bd75054 100644
--- a/maraudes/templates/maraudes/lieu_create.html
+++ b/maraudes/templates/maraudes/lieu_create.html
@@ -1 +1,8 @@
-{% include "maraudes/lieu_create_inner.html" %}
+{% load bootstrap3 %}
+
diff --git a/maraudes/templates/maraudes/lieu_create_inner.html b/maraudes/templates/maraudes/lieu_create_inner.html
index bd75054..e69de29 100644
--- a/maraudes/templates/maraudes/lieu_create_inner.html
+++ b/maraudes/templates/maraudes/lieu_create_inner.html
@@ -1,8 +0,0 @@
-{% load bootstrap3 %}
-
diff --git a/maraudes/templates/maraudes/list_table_cell.html b/maraudes/templates/maraudes/list_table_cell.html
deleted file mode 100644
index d320b5d..0000000
--- a/maraudes/templates/maraudes/list_table_cell.html
+++ /dev/null
@@ -1,9 +0,0 @@
-{% if object.est_terminee %}
-{% else %}{% endif %}
- {{object.date}}
-
-{{ object.binome }} & {{ object.referent }}
-{% if object.est_terminee %}
-{{object.rencontres.count}} rencontres
-{% endif %}
-
diff --git a/maraudes/templates/maraudes/liste.html b/maraudes/templates/maraudes/liste.html
deleted file mode 100644
index 7ac9cb1..0000000
--- a/maraudes/templates/maraudes/liste.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% load bootstrap3 %}
-{% load tables %}
-
-
-
-
-
Maraudes passées
-
-
- {% table object_list cols=3 cell_template="maraudes/list_table_cell.html" %}
- {% if is_paginated %}
-
- {% endif %}
-
-
-
diff --git a/maraudes/templates/maraudes/menu.html b/maraudes/templates/maraudes/menu.html
new file mode 100644
index 0000000..77cb8bc
--- /dev/null
+++ b/maraudes/templates/maraudes/menu.html
@@ -0,0 +1,25 @@
+{% load navbar %}
+
diff --git a/maraudes/templates/maraudes/missing_cr_table_cell.html b/maraudes/templates/maraudes/missing_cr_table_cell.html
deleted file mode 100644
index 6a277b9..0000000
--- a/maraudes/templates/maraudes/missing_cr_table_cell.html
+++ /dev/null
@@ -1,3 +0,0 @@
-{{object.date}}
-{{object.referent}} & {{object.binome}}
-
diff --git a/maraudes/templates/maraudes/planning.html b/maraudes/templates/maraudes/planning.html
new file mode 100644
index 0000000..2dcbb6e
--- /dev/null
+++ b/maraudes/templates/maraudes/planning.html
@@ -0,0 +1,61 @@
+{% extends "maraudes/base.html" %}
+{% load bootstrap3 %}
+
+{% block title %} {{ block.super }} Planning {% endblock %}
+
+{% block breadcrumbs %}
+ {{ block.super }}
+ Planning
+{% endblock %}
+
+{% block sidebar %}
+ {{ block.super }}
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block page_content %}
+
+{% endblock %}
diff --git a/maraudes/templates/maraudes/table_cell_derniers_sujets.html b/maraudes/templates/maraudes/table_cell_derniers_sujets.html
new file mode 100644
index 0000000..3f5c0f7
--- /dev/null
+++ b/maraudes/templates/maraudes/table_cell_derniers_sujets.html
@@ -0,0 +1 @@
+{{ object }}
diff --git a/maraudes/templates/maraudes/table_cell_missing_cr.html b/maraudes/templates/maraudes/table_cell_missing_cr.html
new file mode 100644
index 0000000..4465147
--- /dev/null
+++ b/maraudes/templates/maraudes/table_cell_missing_cr.html
@@ -0,0 +1 @@
+ {{ object }}
diff --git a/maraudes/templates/planning/planning.html b/maraudes/templates/planning/planning.html
deleted file mode 100644
index 5525dca..0000000
--- a/maraudes/templates/planning/planning.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{% load bootstrap3 %}
-
-
-
-
-
diff --git a/maraudes/tests.py b/maraudes/tests.py
index 7ec1850..4d1b3f9 100644
--- a/maraudes/tests.py
+++ b/maraudes/tests.py
@@ -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):
diff --git a/maraudes/urls.py b/maraudes/urls.py
index 7087ffa..c97ecba 100644
--- a/maraudes/urls.py
+++ b/maraudes/urls.py
@@ -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[0-9]+)/$', views.MaraudeDetailsView.as_view(), name="details"),
- url(r'^(?P[0-9]+)/update/$', views.CompteRenduUpdateView.as_view(), name="update"),
url(r'^(?P[0-9]+)/create/$', views.CompteRenduCreateView.as_view(), name="create"),
+ url(r'^(?P[0-9]+)/finalize/$', views.FinalizeView.as_view(), name="finalize"),
]
diff --git a/maraudes/views.py b/maraudes/views.py
index 86c0b65..6404696 100644
--- a/maraudes/views.py
+++ b/maraudes/views.py
@@ -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__"
diff --git a/notes/__init__.py b/notes/__init__.py
index e69de29..8ad3b03 100644
--- a/notes/__init__.py
+++ b/notes/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'notes.apps.NotesConfig'
diff --git a/notes/actions.py b/notes/actions.py
new file mode 100644
index 0000000..7d99369
--- /dev/null
+++ b/notes/actions.py
@@ -0,0 +1,38 @@
+from .models import Sujet
+from statistiques.models import FicheStatistique, NSP
+
+
+def merge_stats(main, merged):
+ """ Merge stats of two sujets according to priority order : main, then merged """
+ # TODO: replace hardcoded field names with more flexible getters
+
+ # Fields of 'Sujet' model
+ for field in ('nom', 'prenom', 'surnom', 'age',):
+ if not getattr(main, field):
+ setattr(main, field, getattr(merged, field, None))
+
+ # Première rencontre : retenir la plus ancienne
+ if merged.premiere_rencontre:
+ if not main.premiere_rencontre or main.premiere_rencontre > merged.premiere_rencontre:
+ main.premiere_rencontre = merged.premiere_rencontre
+
+ # Fields of 'FicheStatistique' model
+ # NullBoolean fields
+ for field in ('prob_psychiatrie', 'prob_somatique',
+ 'prob_administratif', 'prob_addiction',
+ 'connu_siao', 'lien_familial'):
+ if not getattr(main.statistiques, field): # Ignore if already filled
+ setattr(main.statistiques, field, getattr(merged.statistiques, field, None))
+ # Choice fields, None is NSP
+ for field in ('habitation', 'ressources', 'parcours_de_vie'):
+ if getattr(main.statistiques, field) == NSP: # Ignore if already filled
+ setattr(main.statistiques, field, getattr(merged.statistiques, field, NSP))
+
+def merge_two(main, merged):
+ """ Merge 'merged' sujet into 'main' one """
+ merge_stats(main, merged) # Merge statistics and informations
+ for note in merged.notes.all(): # Move all notes
+ note.sujet = main
+ note.save()
+ main.save()
+ merged.delete()
diff --git a/notes/admin.py b/notes/admin.py
index 8132060..609932c 100644
--- a/notes/admin.py
+++ b/notes/admin.py
@@ -3,6 +3,15 @@ from django.contrib import admin
from .models import *
# Register your models here.
+@admin.register(Sujet)
+class SujetAdmin(admin.ModelAdmin):
+
+ fieldsets = [
+ ('Identité', {'fields': [('nom', 'prenom'), 'genre']}),
+ ('Informations', {'fields': ['age', ]}),
+ ]
+
+
@admin.register(Note)
class NoteAdmin(admin.ModelAdmin):
@@ -16,4 +25,4 @@ class NoteAdmin(admin.ModelAdmin):
]
list_display = ['created_date', 'sujet', 'child_class', 'text']
- list_filter = ('sujet', 'created_date', 'created_by')
+ list_filter = ('created_date', 'created_by')
diff --git a/notes/apps.py b/notes/apps.py
index b6155ac..a92cd8b 100644
--- a/notes/apps.py
+++ b/notes/apps.py
@@ -1,5 +1,9 @@
from django.apps import AppConfig
-
+from watson import search as watson
class NotesConfig(AppConfig):
name = 'notes'
+
+ def ready(self):
+ Sujet = self.get_model("Sujet")
+ watson.register(Sujet, fields=('nom', 'prenom', 'surnom'))
diff --git a/notes/forms.py b/notes/forms.py
index 7c8bb51..494da59 100644
--- a/notes/forms.py
+++ b/notes/forms.py
@@ -1,11 +1,12 @@
-from .models import Note
+import datetime
+
+from .models import Note, Sujet
from utilisateurs.models import Professionnel
from django import forms
from django_select2.forms import Select2Widget
-from django.forms import Textarea
-
+### NOTES
class NoteForm(forms.ModelForm):
""" Generic Note form """
@@ -14,7 +15,7 @@ class NoteForm(forms.ModelForm):
fields = ['sujet', 'text', 'created_by', 'created_date', 'created_time']
widgets = {
'sujet': Select2Widget(),
- 'text': Textarea(attrs={'rows':4}),
+ 'text': forms.Textarea(attrs={'rows':4}),
}
@@ -54,6 +55,8 @@ class UserNoteForm(NoteForm):
instance.save()
return instance
+
+
class AutoNoteForm(UserNoteForm):
class Meta(UserNoteForm.Meta):
fields = ['text']
@@ -68,3 +71,24 @@ class AutoNoteForm(UserNoteForm):
if commit:
inst.save()
return inst
+
+
+
+### SUJETS
+
+current_year = datetime.date.today().year
+YEAR_CHOICE = tuple(year - 2 for year in range(current_year, current_year + 10))
+
+class SujetCreateForm(forms.ModelForm):
+ class Meta:
+ model = Sujet
+ fields = ['nom', 'surnom', 'prenom', 'genre', 'premiere_rencontre']
+ widgets = {
+ 'premiere_rencontre': forms.SelectDateWidget( empty_label=("Année", "Mois", "Jour"),
+ years = YEAR_CHOICE,
+ ),
+ }
+
+class SelectSujetForm(forms.Form):
+
+ sujet = forms.ModelChoiceField(queryset=Sujet.objects.all(), widget=Select2Widget)
diff --git a/notes/models.py b/notes/models.py
index 03ca167..8fcbc55 100644
--- a/notes/models.py
+++ b/notes/models.py
@@ -1,9 +1,74 @@
+import logging
+
from django.utils import timezone
from django.utils.html import format_html
+from django.core.exceptions import ValidationError
+from django.urls import reverse
from django.db import models
from . import managers
+logger = logging.getLogger(__name__)
+
+HOMME = 'M'
+FEMME = 'Mme'
+GENRE_CHOICES = (
+ (HOMME, 'Homme'),
+ (FEMME, 'Femme'),
+ )
+
+class Sujet(models.Model):
+ """ Personne faisant l'objet d'un suivi par la maraude
+ """
+
+ genre = models.CharField("Genre",
+ max_length=3,
+ choices=GENRE_CHOICES,
+ default=HOMME)
+ nom = models.CharField(max_length=32, blank=True)
+ prenom = models.CharField(max_length=32, blank=True)
+ surnom = models.CharField(max_length=64, blank=True)
+
+ premiere_rencontre = models.DateField(
+ blank=True, null=True,
+ default=timezone.now
+ )
+ age = models.SmallIntegerField(
+ blank=True, null=True
+ )
+
+ # referent = models.ForeignKey("utilisateurs.Professionnel", related_name="suivis")
+
+ def __str__(self):
+ string = '%s ' % self.genre
+ if self.nom: string += '%s ' % self.nom
+ if self.surnom: string += '"%s" ' % self.surnom
+ if self.prenom: string += '%s' % self.prenom
+ return string
+
+ def clean(self):
+ if not any([self.nom, self.prenom, self.surnom]):
+ raise ValidationError("Vous devez remplir au moins un nom, prénom ou surnom")
+ return super().clean()
+
+ def save(self, *args, **kwargs):
+ self.clean()
+ if not self.id:
+ from statistiques.models import FicheStatistique
+ super().save(*args, **kwargs)
+ fiche = FicheStatistique.objects.create(sujet=self)
+ else:
+ return super().save(*args, **kwargs)
+
+ class Meta:
+ verbose_name = "Sujet"
+ ordering = ('surnom', 'nom', 'prenom')
+
+ def get_absolute_url(self):
+ return reverse("notes:details-sujet", kwargs={"pk": self.pk })
+
+
+
class Note(models.Model):
""" Note relative à un sujet.
@@ -19,11 +84,12 @@ class Note(models.Model):
objects = managers.NoteManager()
sujet = models.ForeignKey(
- 'sujets.Sujet',
+ Sujet,
related_name="notes",
on_delete=models.CASCADE
)
text = models.TextField("Texte")
+
created_by = models.ForeignKey(
'utilisateurs.Professionnel',
blank=True,
@@ -42,7 +108,11 @@ class Note(models.Model):
return super().save(*args, **kwargs)
def __str__(self):
- return "%s" % (self.child_class.__qualname__)
+ return "<%s: %s>" % (self.child_class.__qualname__, self.sujet)
+
+ @classmethod
+ def __str__(cls):
+ return "<%s>" % cls.__qualname__
def note_author(self):
return None
diff --git a/notes/templates/notes/base.html b/notes/templates/notes/base.html
new file mode 100644
index 0000000..1e71127
--- /dev/null
+++ b/notes/templates/notes/base.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+
+{% block title %}Notes >{% endblock %}
+
+{% block breadcrumbs %}
+ Notes
+{% endblock %}
+
+{% block sidebar %}
+
+
+ {% include "notes/menu.html" %}
+
+
{% endblock %}
diff --git a/notes/templates/notes/details.html b/notes/templates/notes/details.html
new file mode 100644
index 0000000..773ceac
--- /dev/null
+++ b/notes/templates/notes/details.html
@@ -0,0 +1,33 @@
+{% extends "notes/base.html" %}
+{% load bootstrap3 notes %}
+
+{% block page_content %}
+
+
+
+
+{% block pre_content %}{% endblock %}
+
+ {% for note in notes %}
+ {% if maraude %}
+ {% inline_table note header="sujet" %}
+ {% elif sujet %}
+ {% inline_table note header="date" %}
+ {% endif %}
+ {% endfor %}
+
+{% block post_content %}{% endblock %}
+
+
+
+
+
+ {% block right_column %}{% endblock %}
+
+{% endblock %}
+
diff --git a/notes/templates/notes/details_maraude.html b/notes/templates/notes/details_maraude.html
new file mode 100644
index 0000000..acac101
--- /dev/null
+++ b/notes/templates/notes/details_maraude.html
@@ -0,0 +1,47 @@
+{% extends "notes/details.html" %}
+
+{% block title %}
+ {{ block.super }} {{ maraude }}
+{% endblock %}
+
+{% block breadcrumbs %}
+ {{ block.super }}
+ Maraudes
+ {{ maraude }}
+{% endblock %}
+
+{% block sidebar %}
+
+{% endblock %}
+
+{% block right_column %}
+
+
+
+ Informations
+
+
+
Maraudeurs : {{ maraude.binome }} & {{ maraude.referent }}
+
Nombre de rencontres {{ maraude.rencontres.count }}
+
Nombre de personnes rencontrées {{ maraude.observations_count }}
+
+
+{% endblock %}
diff --git a/notes/templates/notes/details_sujet.html b/notes/templates/notes/details_sujet.html
new file mode 100644
index 0000000..6143824
--- /dev/null
+++ b/notes/templates/notes/details_sujet.html
@@ -0,0 +1,103 @@
+{% extends "notes/details.html" %}
+{% load bootstrap3 %}
+
+{% block title %}
+ {{ block.super }} {{ sujet }}
+{% endblock %}
+
+{% block breadcrumbs %}
+ {{ block.super }}
+ Sujets
+ {{ sujet }}
+{% endblock %}
+
+{% block pre_content %}
+
+{% endblock %}
+
+{% block post_content %}
+ {% if notes.has_other_pages %}{% endif %}
+
+
+
+
+
+{% endblock %}
+
+{% block right_column %}
+
+
+
Informations
+
+ {% include "notes/details_sujet_inner.html" %}
+
+
+
+
Statistiques
+
+ {% include "statistiques/fiche_stats_details.html" with object=sujet.statistiques %}
+
+
+
+
+{% endblock %}
+
+{% block sidebar %}
+{{ block.super }}
+
+{% if user.is_superuser %}
+
+{% endif %}
+{% endblock %}
diff --git a/notes/templates/notes/details_sujet_inner.html b/notes/templates/notes/details_sujet_inner.html
new file mode 100644
index 0000000..65d811a
--- /dev/null
+++ b/notes/templates/notes/details_sujet_inner.html
@@ -0,0 +1,22 @@
+
+
+ {% with "-" as none %}
+ | Nom | Surnom | Prénom |
+ | {{ sujet.nom|default:none }} | {{ sujet.surnom|default:none}} | {{ sujet.prenom|default:none}} |
+ | Âge | Première rencontre |
+ | {{ sujet.age|default_if_none:none }} | {{ sujet.premiere_rencontre|default_if_none:none }} |
+ {% endwith %}
+
+
+
+
+
diff --git a/notes/templates/notes/details_sujet_update.html b/notes/templates/notes/details_sujet_update.html
new file mode 100644
index 0000000..f218fec
--- /dev/null
+++ b/notes/templates/notes/details_sujet_update.html
@@ -0,0 +1,24 @@
+{% load bootstrap3 %}
+
+
+
diff --git a/suivi/templates/suivi/appel_form.html b/notes/templates/notes/form_appel.html
similarity index 71%
rename from suivi/templates/suivi/appel_form.html
rename to notes/templates/notes/form_appel.html
index 2795f3a..dd80699 100644
--- a/suivi/templates/suivi/appel_form.html
+++ b/notes/templates/notes/form_appel.html
@@ -1,12 +1,12 @@
-
+
- {% include "suivi/appel_form_inner.html" %}
+ {% include "notes/form_appel_inner.html" %}
diff --git a/suivi/templates/suivi/appel_form_inner.html b/notes/templates/notes/form_appel_inner.html
similarity index 90%
rename from suivi/templates/suivi/appel_form_inner.html
rename to notes/templates/notes/form_appel_inner.html
index ce3b1a6..49186b0 100644
--- a/suivi/templates/suivi/appel_form_inner.html
+++ b/notes/templates/notes/form_appel_inner.html
@@ -1,7 +1,7 @@
{% load bootstrap3 %}