Adding the core applications code to the repository
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,47 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
from .notes import Observation
|
||||||
|
|
||||||
|
|
||||||
|
# Basic registration
|
||||||
|
admin.site.register(Lieu)
|
||||||
|
|
||||||
|
# Inlines
|
||||||
|
class ObservationInline(admin.StackedInline):
|
||||||
|
model = Observation
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Rencontre)
|
||||||
|
class RencontreAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
fieldsets = [
|
||||||
|
('Contexte', {'fields': ['maraude', ('heure_debut', 'duree'), 'lieu']})
|
||||||
|
]
|
||||||
|
|
||||||
|
inlines = [ObservationInline]
|
||||||
|
|
||||||
|
list_display = ('maraude', 'lieu', 'heure_debut', 'groupe_ou_individu')
|
||||||
|
list_filter = ['lieu']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Maraude)
|
||||||
|
class MaraudeAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
fieldsets = [
|
||||||
|
('Planning', {'fields': [('date', 'heure_debut'), ('referent', 'binome')]}),
|
||||||
|
(None, {'fields': ['heure_fin']}),
|
||||||
|
]
|
||||||
|
list_display = ('date', 'heure_debut', 'binome', 'est_passee', 'est_terminee')
|
||||||
|
list_filter = ['date', 'binome']
|
||||||
|
|
||||||
|
@admin.register(Planning)
|
||||||
|
class PlanningAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ('week_day', 'horaire')
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class Config(AppConfig):
|
||||||
|
name = 'maraudes'
|
||||||
|
|
||||||
|
index_url = "/maraudes/"
|
||||||
|
|
||||||
|
def get_index_url(self):
|
||||||
|
return "/maraudes/"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
from .models import Maraude
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
class CompteRendu(Maraude):
|
||||||
|
""" Proxy for Maraude objects.
|
||||||
|
Gives access to related Observation and Rencontre
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self._iter()
|
||||||
|
|
||||||
|
def reversed(self):
|
||||||
|
return self._iter(order="-heure_debut")
|
||||||
|
|
||||||
|
def _iter(self, order="heure_debut"):
|
||||||
|
for r in self.rencontres.get_queryset().order_by(order):
|
||||||
|
yield (r, [o for o in r.observations.all()])
|
||||||
|
|
||||||
|
def as_list(self, **kwargs):
|
||||||
|
return [t for t 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
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.forms import inlineformset_factory
|
||||||
|
from notes.forms import NoteForm
|
||||||
|
# Models
|
||||||
|
from .models import Maraude, Rencontre
|
||||||
|
from .notes import Observation
|
||||||
|
|
||||||
|
|
||||||
|
class MaraudeAutoDateForm(forms.ModelForm):
|
||||||
|
""" Maraude ModelForm with disabled 'date' field """
|
||||||
|
class Meta:
|
||||||
|
model = Maraude
|
||||||
|
fields = ['date', 'heure_debut', 'referent', 'binome']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['date'].disabled = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RencontreForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Rencontre
|
||||||
|
fields = ['lieu', 'heure_debut', 'duree']
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ObservationInlineFormSet = inlineformset_factory( Rencontre, Observation,
|
||||||
|
form=NoteForm,
|
||||||
|
extra = 0,
|
||||||
|
min_num = 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
RencontreInlineFormSet = inlineformset_factory(
|
||||||
|
Maraude, Rencontre,
|
||||||
|
form = RencontreForm,
|
||||||
|
extra = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ObservationInlineFormSetNoExtra = inlineformset_factory(
|
||||||
|
Rencontre, Observation,
|
||||||
|
form = NoteForm,
|
||||||
|
extra = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
class MonthSelectForm(forms.Form):
|
||||||
|
|
||||||
|
month = forms.ChoiceField(
|
||||||
|
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]]
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, month=None, year=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.fields['month'].initial = month
|
||||||
|
self.fields['year'].initial = year
|
||||||
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
from django.db.models import Manager
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
|
||||||
|
class MaraudeManager(Manager):
|
||||||
|
""" Manager for Maraude objects """
|
||||||
|
|
||||||
|
def all_of(self, maraudeur):
|
||||||
|
""" Retourne la liste des maraudes de 'maraudeur' """
|
||||||
|
# Le référent ne peut participer qu'en tant que référent
|
||||||
|
if maraudeur.is_superuser:
|
||||||
|
return self.get_queryset().filter(referent=maraudeur.id)
|
||||||
|
|
||||||
|
# Un maraudeur peut occasionnellement être référent
|
||||||
|
maraudes_ref = self.get_queryset().filter(referent=maraudeur.id)
|
||||||
|
maraudes_bin = self.get_queryset().filter(binome=maraudeur.id)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_of(self, maraudeur):
|
||||||
|
""" Retourne la prochaine maraude de 'maraudeur' """
|
||||||
|
return self.all_of(maraudeur).filter(
|
||||||
|
date__gte=datetime.date.today()
|
||||||
|
).order_by(
|
||||||
|
'date'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
def get_future(self):
|
||||||
|
""" Retourne la liste des prochaines maraudes """
|
||||||
|
return self.get_queryset().filter(
|
||||||
|
date__gte=datetime.date.today()
|
||||||
|
).order_by(
|
||||||
|
'date'
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_past(self):
|
||||||
|
""" Retourne la liste des maraudes passées """
|
||||||
|
return self.get_queryset().filter(
|
||||||
|
date__lt=datetime.date.today()
|
||||||
|
).order_by(
|
||||||
|
'date'
|
||||||
|
)
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def next(self):
|
||||||
|
""" Prochaine maraude """
|
||||||
|
return self.get_future().first()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def last(self):
|
||||||
|
""" Dernière maraude """
|
||||||
|
return self.get_past().last()
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def in_progress(self):
|
||||||
|
""" Retourne la maraude en cours, ou None """
|
||||||
|
d, t = timezone.now().date(), timezone.now().time()
|
||||||
|
|
||||||
|
# Prendre le jour précédent s'il est entre minuit et 2h du matin
|
||||||
|
depassement = False
|
||||||
|
if t <= datetime.time(2):
|
||||||
|
d = d - datetime.timedelta(days=1)
|
||||||
|
depassement = True
|
||||||
|
|
||||||
|
maraude_du_jour = self.get(date=d)
|
||||||
|
|
||||||
|
if maraude_du_jour:
|
||||||
|
if depassement or t >= maraude_du_jour.heure_debut:
|
||||||
|
return maraude_du_jour
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class ObservationManager(Manager):
|
||||||
|
|
||||||
|
def get_for_sujet(self, sujet):
|
||||||
|
return self.filter(sujet=sujet)
|
||||||
|
|
||||||
|
def get_first_for_sujet(self, sujet):
|
||||||
|
return self.filter(sujet=sujet).order_by('date').first()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-08-04 10:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Lieu',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('nom', models.CharField(max_length=128)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Lieu de rencontre',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Maraude',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('date', models.DateField(unique=True, verbose_name='Date')),
|
||||||
|
('heure_debut', models.TimeField(choices=[(datetime.time(16, 0), 'Après-midi'), (datetime.time(20, 0), 'Soirée')], default=datetime.time(20, 0), verbose_name='Horaire')),
|
||||||
|
('heure_fin', models.TimeField(blank=True, null=True, verbose_name='Terminée à')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['date'],
|
||||||
|
'verbose_name': 'Maraude',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-08-04 10:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('maraudes', '0001_initial'),
|
||||||
|
('notes', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Observation',
|
||||||
|
fields=[
|
||||||
|
('note_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='notes.Note')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Observation',
|
||||||
|
},
|
||||||
|
bases=('notes.note',),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Planning',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('week_day', models.IntegerField(choices=[(0, 'Lundi'), (1, 'Mardi'), (2, 'Mercredi'), (3, 'Jeudi'), (4, 'Vendredi'), (5, 'Samedi')])),
|
||||||
|
('horaire', models.TimeField(choices=[(datetime.time(16, 0), 'Après-midi'), (datetime.time(20, 0), 'Soirée')], verbose_name='Horaire')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name_plural': 'Planning',
|
||||||
|
'verbose_name': 'Jour de maraude',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Rencontre',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('heure_debut', models.TimeField(verbose_name='Heure')),
|
||||||
|
('duree', models.SmallIntegerField(choices=[(5, '5 min'), (10, '10 min'), (15, '15 min'), (20, '20 min'), (30, '30 min'), (45, '45 min'), (60, '1 heure')], verbose_name='Durée')),
|
||||||
|
('lieu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='maraudes.Lieu')),
|
||||||
|
('maraude', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='rencontres', to='maraudes.Maraude')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['maraude', 'heure_debut'],
|
||||||
|
'verbose_name': 'Rencontre',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='observation',
|
||||||
|
name='rencontre',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='observations', to='maraudes.Rencontre'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-08-04 10:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import maraudes.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('maraudes', '0002_auto_20160804_1221'),
|
||||||
|
('utilisateurs', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='maraude',
|
||||||
|
name='binome',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='maraudes', to='utilisateurs.Maraudeur', verbose_name='Binôme'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='maraude',
|
||||||
|
name='referent',
|
||||||
|
field=models.ForeignKey(default=maraudes.models.get_referent_maraude, on_delete=django.db.models.deletion.CASCADE, related_name='references', to='utilisateurs.Maraudeur', verbose_name='Référent'),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CompteRendu',
|
||||||
|
fields=[
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'proxy': True,
|
||||||
|
},
|
||||||
|
bases=('maraudes.maraude',),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,235 @@
|
|||||||
|
import calendar
|
||||||
|
import datetime
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from utilisateurs.models import Maraudeur, ReferentMaraude
|
||||||
|
|
||||||
|
from . import managers
|
||||||
|
|
||||||
|
## Fonctions utiles
|
||||||
|
|
||||||
|
def get_referent_maraude():
|
||||||
|
""" Retourne l'administrateur et référent de la Maraude """
|
||||||
|
return Maraudeur.objects.filter(is_superuser=True).first()
|
||||||
|
|
||||||
|
|
||||||
|
## Modèles
|
||||||
|
|
||||||
|
|
||||||
|
class Lieu(models.Model):
|
||||||
|
""" Lieu de rencontre """
|
||||||
|
|
||||||
|
nom = models.CharField(max_length=128)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.nom
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Lieu de rencontre"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Maraude(models.Model):
|
||||||
|
""" Modèle pour une maraude
|
||||||
|
- date : jour de la maraude
|
||||||
|
- heure_debut :
|
||||||
|
- heure_fin :
|
||||||
|
- referent : maraudeur 1
|
||||||
|
- binome : maraudeur 2
|
||||||
|
|
||||||
|
Méthodes :
|
||||||
|
- est_terminee : True/False
|
||||||
|
- est_passee : True/false
|
||||||
|
"""
|
||||||
|
objects = managers.MaraudeManager()
|
||||||
|
|
||||||
|
date = models.DateField(
|
||||||
|
"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,
|
||||||
|
default=HORAIRES_CHOICES[1][0]
|
||||||
|
)
|
||||||
|
# Lorsque l'heure de fin est renseignée, la maraude est terminée
|
||||||
|
heure_fin = models.TimeField(
|
||||||
|
"Terminée à",
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
# Maraudeurs
|
||||||
|
referent = models.ForeignKey(
|
||||||
|
"utilisateurs.Maraudeur",
|
||||||
|
models.CASCADE,
|
||||||
|
verbose_name="Référent",
|
||||||
|
related_name="references",
|
||||||
|
default=get_referent_maraude
|
||||||
|
)
|
||||||
|
binome = models.ForeignKey(
|
||||||
|
"utilisateurs.Maraudeur",
|
||||||
|
models.CASCADE,
|
||||||
|
verbose_name="Binôme",
|
||||||
|
related_name="maraudes",
|
||||||
|
limit_choices_to={
|
||||||
|
'is_superuser': False,
|
||||||
|
'is_staff': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Maraude"
|
||||||
|
ordering = ['date']
|
||||||
|
permissions = (
|
||||||
|
('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()],
|
||||||
|
self.date.day,
|
||||||
|
self.MOIS[self.date.month - 1])
|
||||||
|
|
||||||
|
def est_terminee(self):
|
||||||
|
""" Indique si la maraude est considérée comme terminée """
|
||||||
|
if self.heure_fin is not None:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
est_terminee.admin_order_field = 'date'
|
||||||
|
est_terminee.boolean = True
|
||||||
|
est_terminee.short_description = 'Terminée ?'
|
||||||
|
|
||||||
|
def est_passee(self):
|
||||||
|
return self.date < datetime.date.today()
|
||||||
|
est_passee.admin_order_field = 'date'
|
||||||
|
est_passee.boolean = True
|
||||||
|
est_passee.short_description = 'Passée ?'
|
||||||
|
|
||||||
|
def rencontre_count(self):
|
||||||
|
return self.rencontres.count()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('maraudes:details', 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(
|
||||||
|
Maraude,
|
||||||
|
models.CASCADE,
|
||||||
|
related_name = 'rencontres',
|
||||||
|
limit_choices_to={'heure_fin__isnull': False}
|
||||||
|
)
|
||||||
|
lieu = models.ForeignKey(
|
||||||
|
Lieu,
|
||||||
|
models.CASCADE
|
||||||
|
)
|
||||||
|
heure_debut = models.TimeField("Heure")
|
||||||
|
duree = models.SmallIntegerField(
|
||||||
|
"Durée",
|
||||||
|
choices=DUREE_CHOICES
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Rencontre"
|
||||||
|
ordering = ['maraude', 'heure_debut']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s à %s (%imin)" % (
|
||||||
|
self.lieu,
|
||||||
|
self.heure_debut.strftime("%Hh%M"),
|
||||||
|
self.duree
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
return self.maraude.date
|
||||||
|
|
||||||
|
INDIVIDU = "Individu"
|
||||||
|
GROUPE = "Groupe"
|
||||||
|
def groupe_ou_individu(self):
|
||||||
|
""" Retourne le type de rencontre : 'groupe'/'individu' """
|
||||||
|
nb = self.observations.count()
|
||||||
|
if nb == 1:
|
||||||
|
return self.INDIVIDU
|
||||||
|
elif nb > 1:
|
||||||
|
return self.GROUPE
|
||||||
|
else:
|
||||||
|
return "Aucun"
|
||||||
|
|
||||||
|
def get_sujets(self):
|
||||||
|
""" Renvoie la liste des sujets rencontrés """
|
||||||
|
return [o.sujet for o in self.observations.all()]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Planning(models.Model):
|
||||||
|
""" Plannification des maraudes. Chaque instance représente un jour de la
|
||||||
|
semaine et un horaire par défaut.
|
||||||
|
"""
|
||||||
|
WEEKDAYS = [
|
||||||
|
(0, "Lundi"),
|
||||||
|
(1, "Mardi"),
|
||||||
|
(2, "Mercredi"),
|
||||||
|
(3, "Jeudi"),
|
||||||
|
(4, "Vendredi"),
|
||||||
|
(5, "Samedi"),
|
||||||
|
]
|
||||||
|
|
||||||
|
week_day = models.IntegerField(
|
||||||
|
choices=WEEKDAYS,
|
||||||
|
)
|
||||||
|
horaire = models.TimeField(
|
||||||
|
"Horaire",
|
||||||
|
choices=Maraude.HORAIRES_CHOICES,
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Jour de maraude"
|
||||||
|
verbose_name_plural = "Planning"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_planning(cls):
|
||||||
|
""" Renvoie l'ensemble des objets enregistrés """
|
||||||
|
return cls.objects.all()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_maraudes_days_for_month(cls, year, month):
|
||||||
|
""" Renvoie le jour et l'horaire prévu de maraude, comme un tuple,
|
||||||
|
pour l'année et le mois donnés.
|
||||||
|
"""
|
||||||
|
planning = Planning.get_planning()
|
||||||
|
for week in calendar.monthcalendar(year, month):
|
||||||
|
for planned in cls.get_planning():
|
||||||
|
day_of_maraude = week[planned.week_day]
|
||||||
|
if day_of_maraude:
|
||||||
|
yield (day_of_maraude, planned.horaire)
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from notes.models import Note
|
||||||
|
from . import managers
|
||||||
|
|
||||||
|
# Extends 'notes' module
|
||||||
|
|
||||||
|
class Observation(Note):
|
||||||
|
""" Note dans le cadre d'une rencontre """
|
||||||
|
|
||||||
|
objects = managers.ObservationManager()
|
||||||
|
rencontre = models.ForeignKey( 'maraudes.Rencontre',
|
||||||
|
related_name="observations",
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Observation"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "<Observation: %s>" % self.sujet
|
||||||
|
|
||||||
|
def get_date(self):
|
||||||
|
return self.rencontre.date
|
||||||
|
|
||||||
|
def get_header(self):
|
||||||
|
return ('Rencontre', [self.rencontre.lieu, "%smin" % self.rencontre.duree])
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<div class="panel panel-primary">
|
||||||
|
<!-- Default panel contents -->
|
||||||
|
<div class="panel-heading"><h3 class="panel-title">Compte-Rendu</h3>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p class="bg-info">Maraudeurs: {{ maraude.binome.first_name }}, {{ maraude.referent.first_name }}</p>
|
||||||
|
{% if maraude.est_terminee %}<p>Terminée à {{ maraude.heure_fin}} </p>
|
||||||
|
<p>{{maraude.rencontre_count}} rencontres</p>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
{% for rencontre, observations in maraude %}
|
||||||
|
<tr>
|
||||||
|
<th>{{ rencontre.lieu }}</th>
|
||||||
|
<th>{{ rencontre.heure_debut }}</th>
|
||||||
|
<th style="text-align:right" width="150">Durée: {{ rencontre.duree }}min</th>
|
||||||
|
</tr>
|
||||||
|
{% for observation in observations %}
|
||||||
|
<tr><td>{{ observation.sujet }}</td><td colspan="2"> {{ observation.note }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
{% if user.is_superuser and maraude.est_terminee %}<div class="panel-footer">
|
||||||
|
<a class="btn btn-primary" href="{% url 'maraudes:update' maraude.pk %}">Modifier</a>
|
||||||
|
</div>{%endif%}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{{ form.media.js }}{{ form.media.css }}
|
||||||
|
<script type="text/javascript" src="{% static "jquery.formset.js" %}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
$.fn.onAddForm = function(row) {
|
||||||
|
/*
|
||||||
|
* Custom code to integrate with django-select2 and bootstrap3
|
||||||
|
*/
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/* Lier les boutons de création
|
||||||
|
* Thanks to Derek Morgan, https://dmorgan.info/posts/django-views-bootstrap-modals/
|
||||||
|
*/
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
var formAjaxSubmit = function(form, modal) {
|
||||||
|
$(form).submit(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$.ajax({
|
||||||
|
type: $(this).attr('method'),
|
||||||
|
url: $(this).attr('action'),
|
||||||
|
data: $(this).serialize(),
|
||||||
|
success: function (xhr, ajaxOptions, thrownError) {
|
||||||
|
if ( $(xhr).find('.has-error').length > 0 ) {
|
||||||
|
$(modal).find('.modal-body').html(xhr);
|
||||||
|
formAjaxSubmit(form, modal);
|
||||||
|
} else {
|
||||||
|
$(modal).modal('toggle');
|
||||||
|
// Reload page ?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xhr, ajaxOptions, thrownError) {
|
||||||
|
// handle response errors here
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/* TODO: Use formAjaxSubmit above, but reload page on form success */
|
||||||
|
$('#new-sujet').click(function() {
|
||||||
|
$('#form-modal-body').load('{% url "sujets:create" %}?next={% url "maraudes:create" pk=maraude.id %}', function () {
|
||||||
|
$('.modal-title').text("Nouveau sujet");
|
||||||
|
$('#form-modal').modal('toggle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$('#new-lieu').click(function() {
|
||||||
|
$('#form-modal-body').load('{% url "maraudes:lieu-create" %}?next={% url "maraudes:create" pk=maraude.id %}', function () {
|
||||||
|
$('.modal-title').text("Nouveau lieu");
|
||||||
|
$('#form-modal').modal('toggle');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="modal fade" id="form-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Modal title</h4>
|
||||||
|
</div>
|
||||||
|
<div id="form-modal-body" class="modal-body">
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 col-sd-12">
|
||||||
|
<form method="post" action="{% url 'maraudes:create' maraude.pk %}?finalize=False">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="panel panel-default panel-collapse">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-header">Nouvelle rencontre
|
||||||
|
{% bootstrap_button "Enregistrer" icon="save" button_type="submit" button_class="btn btn-success btn-sm pull-right" %}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% include "compte_rendu/compterendu_form.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="panel-footer text-right">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class= "btn btn-primary" id="new-sujet">
|
||||||
|
{% bootstrap_icon "user" %} Nouveau sujet</a>
|
||||||
|
<a class="btn btn-primary" id="new-lieu">
|
||||||
|
{% bootstrap_icon "globe" %} Nouveau lieu</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-6 col-sd-12">
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading"><h4 class="panel-header">Enregistrées
|
||||||
|
<a class="btn btn-danger btn-sm pull-right" href="{% url 'maraudes:create' maraude.pk %}?finalize=True">
|
||||||
|
{% bootstrap_icon "ok-circle" %} Finaliser</a></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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{% load bootstrap3 %}
|
||||||
|
<div class="form-inline well well-sm text-center">
|
||||||
|
{% if form.id %}{% bootstrap_field form.id %}{% endif %}
|
||||||
|
{% bootstrap_field form.lieu layout="inline" size="small" %}
|
||||||
|
<div class="input-group">{% bootstrap_field form.heure_debut layout="inline" size="small" %}
|
||||||
|
<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>
|
||||||
|
{% bootstrap_field form.duree layout="inline" size="small" %}
|
||||||
|
</div>
|
||||||
|
<div class="form-horizontal">
|
||||||
|
{{ inline_formset.management_form }}
|
||||||
|
{{ inline_formset.media.js }}
|
||||||
|
{{ inline_formset.media.css }}
|
||||||
|
{% for form in inline_formset %}
|
||||||
|
<div class="dynamic-formset">
|
||||||
|
{% if form.id %}{% bootstrap_field form.id %}{% endif %}
|
||||||
|
{% if form.instance.pk %}{% bootstrap_field form.note_ptr %}{% endif %}
|
||||||
|
|
||||||
|
{% bootstrap_field form.sujet size="small" layout="horizontal" %}
|
||||||
|
{% if inline_formset.instance.pk %}
|
||||||
|
{% bootstrap_field form.DELETE layout="horizontal" %}
|
||||||
|
{% endif %}
|
||||||
|
{% bootstrap_field form.text size="small" layout="horizontal" %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
var input = $('#id_heure_debut')
|
||||||
|
var min_value = input.attr('value').split(":")
|
||||||
|
|
||||||
|
$.fn.editHeureValue = function(mod) {
|
||||||
|
var input = $('#id_heure_debut');
|
||||||
|
var value = input.attr('value').split(":");
|
||||||
|
value[1] = parseInt(value[1]) + mod;
|
||||||
|
|
||||||
|
do_change = true
|
||||||
|
for (i=0; i < 3; i++){
|
||||||
|
if (value[i] < min_value[i]){
|
||||||
|
do_change = false
|
||||||
|
};
|
||||||
|
};
|
||||||
|
new_value = value.join(":");
|
||||||
|
if (do_change){
|
||||||
|
input.attr('value', new_value);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#minus-5').click(function() {
|
||||||
|
$.fn.editHeureValue(-5)
|
||||||
|
console.log('minus 5')
|
||||||
|
});
|
||||||
|
$('#plus-5').click(function() {
|
||||||
|
$.fn.editHeureValue(5)
|
||||||
|
console.log('plus 5')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
<form method="post" action="{% url 'maraudes:update' maraude.pk %}?continue=True">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ base_formset.management_form }}
|
||||||
|
{% for form, inline_formset in forms %}
|
||||||
|
<div class="col-md-6 col-sd-12">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading text-right">
|
||||||
|
{% bootstrap_field form.DELETE field_class="col-md-1"%}
|
||||||
|
<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 class="panel-body">
|
||||||
|
{% include "compte_rendu/compterendu_form.html" %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% if maraude.est_terminee %}
|
||||||
|
{% include "compte_rendu/compterendu.html" with maraude=compte_rendu %}
|
||||||
|
{% else %}
|
||||||
|
{% if user.is_superuser %}<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 %}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6" id="prochaine-maraudes">
|
||||||
|
<div class="panel panel-primary">
|
||||||
|
<div class="panel-heading">Votre prochaine maraude</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if prochaine_maraude %}<p>
|
||||||
|
<span class="glyphicon glyphicon-calendar"></span>
|
||||||
|
<strong>{{ prochaine_maraude.date }} à {{ prochaine_maraude.heure_debut }}
|
||||||
|
avec {% if user.is_superuser %}{{prochaine_maraude.binome}}{%else%}{{prochaine_maraude.referent}}{%endif%}.
|
||||||
|
</strong></p>
|
||||||
|
<hr />
|
||||||
|
<p><mark>Informations, notes, rendez-vous ?</mark></p>
|
||||||
|
{% else %}<p>Aucune maraude prévue.</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if dernieres_maraudes %}<div class="col-md-6" id="dernieres-maraudes">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<p>Dernières maraudes
|
||||||
|
<span class="pull-right"><a href="{% url 'maraudes:liste' %}" class="btn btn-primary btn-sm">Aller à la liste</a></span></p>
|
||||||
|
</div>
|
||||||
|
<div class="list-group">
|
||||||
|
{% for maraude in dernieres_maraudes %}
|
||||||
|
<a href="{% url 'maraudes:details' maraude.pk %}" class="list-group-item">
|
||||||
|
<strong>{{ maraude }}</strong> <small>{{maraude.binome}} & {{maraude.referent}}</small>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>{% endif %}
|
||||||
|
|
||||||
|
{% if user.is_superuser %}<div class="col-md-6" id="administration">
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading">Administration</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="{% url 'maraudes:planning' %}" class="list-group-item">Planning</a>
|
||||||
|
<a href="{% url 'admin:maraudes_maraude_changelist' %}" class="list-group-item">Gérer les maraudes</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{% include "maraudes/lieu_create_inner.html" %}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{% load bootstrap3 %}
|
||||||
|
<div class="row"><div class="col-md-12">
|
||||||
|
<form action="{% url "maraudes:lieu-create" %}" method="post">{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
{% bootstrap_button "Ajouter un lieu" button_type="submit" button_class="btn btn-primary" icon="plus" %}
|
||||||
|
<input type="text" hidden=True name="next" value="{{ next }}" />
|
||||||
|
</form>
|
||||||
|
</div></div>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{% load bootstrap3 %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<!-- Default panel contents -->
|
||||||
|
<div class="panel-heading text-center">
|
||||||
|
<form action="" method="GET">
|
||||||
|
<label>TODO : Filtrer par date, terminée/non, et autres ?</label>
|
||||||
|
<button class="btn btn-warning" type="submit">Filtrer</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<table class="table table-striped">
|
||||||
|
<tr>
|
||||||
|
<th>Maraudes</th>
|
||||||
|
</tr>
|
||||||
|
{% for maraude in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
{% if maraude.est_terminee %}
|
||||||
|
<a href="{% url 'maraudes:details' maraude.id %}" class="btn btn-primary">
|
||||||
|
{% elif user.is_superuser %}
|
||||||
|
<a href="{% url 'maraudes:create' maraude.id %}" class="btn btn-warning">
|
||||||
|
{% else %}
|
||||||
|
<a href="#" class="btn btn-default disabled">
|
||||||
|
{% endif %}
|
||||||
|
{{maraude.date}}
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
</a><a class="btn btn-danger" href="/admin/maraudes/maraude/{{maraude.id}}/change/">{% bootstrap_icon "edit" %}
|
||||||
|
{% endif %}</a></div>
|
||||||
|
<div class="pull-right">
|
||||||
|
<span class="label label-info">{{ maraude.binome }} & {{ maraude.referent }}</span>
|
||||||
|
<span class="label label-success">{{maraude.rencontres.count}} rencontres</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% if is_paginated %}
|
||||||
|
<div class="col-md-12 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 %}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{% if dernieres_maraudes %}<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Maraudes</div>
|
||||||
|
<div class="list-group">
|
||||||
|
{% for maraude in dernieres_maraudes %}
|
||||||
|
<a href="{% url 'maraudes:details' maraude.pk %}" class="list-group-item">{{ maraude }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
<a href="{% url 'maraudes:liste' %}" class="list-group-item"><b>Autres...</b></a>
|
||||||
|
</div>
|
||||||
|
</div>{% endif %}
|
||||||
|
|
||||||
|
{% if user.is_superuser %}<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Administration</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="{% url 'maraudes:planning' %}" class="list-group-item">Planning</a>
|
||||||
|
</div>
|
||||||
|
</div>{% endif %}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{% 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">
|
||||||
|
<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>
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import datetime
|
||||||
|
import random
|
||||||
|
|
||||||
|
from calendar import monthrange
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from .models import Maraude, Maraudeur, ReferentMaraude
|
||||||
|
# Create your tests here.
|
||||||
|
|
||||||
|
from alsa.base_data import MARAUDEURS
|
||||||
|
|
||||||
|
MARAUDE_DAYS = [
|
||||||
|
True, True, False, True, True, False, False
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_maraude_days(start, end):
|
||||||
|
""" Iterator that returns date of maraude within start-end range """
|
||||||
|
maraude_days = []
|
||||||
|
first_loop = True
|
||||||
|
for m in range(start.month, end.month + 1):
|
||||||
|
start_day = 1
|
||||||
|
if first_loop:
|
||||||
|
start_day = start.day
|
||||||
|
first_loop = False
|
||||||
|
|
||||||
|
month_range = monthrange(start.year, m)[1]
|
||||||
|
for d in range(start_day, month_range + 1):
|
||||||
|
date = datetime.date(start.year, m, d)
|
||||||
|
if MARAUDE_DAYS[date.weekday()]:
|
||||||
|
maraude_days.append(date)
|
||||||
|
|
||||||
|
return maraude_days
|
||||||
|
|
||||||
|
class MaraudeManagerTestCase(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]
|
||||||
|
ReferentMaraude.objects.create(
|
||||||
|
maraudeur=ref
|
||||||
|
)
|
||||||
|
|
||||||
|
l = len(self.maraudeurs)
|
||||||
|
today = datetime.date.today()
|
||||||
|
start_date = today.replace(month=today.month - 1,
|
||||||
|
day=1)
|
||||||
|
end_date = today.replace(month=today.month + 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)
|
||||||
|
|
||||||
|
Maraude.objects.create(
|
||||||
|
date=date,
|
||||||
|
referent=self.maraudeurs[replacement],
|
||||||
|
binome=self.maraudeurs[binome], # Avoid 0 = referent
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
Maraude.objects.create(
|
||||||
|
date=date,
|
||||||
|
referent=ref,
|
||||||
|
binome=self.maraudeurs[i]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_future_maraudes(self):
|
||||||
|
""" La liste des futures maraudes """
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_past_maraudes(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_next_maraude(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_next_of(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_all_of_with_referent(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_all_of_with_maraudeur(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MaraudeTestCase(TestCase):
|
||||||
|
|
||||||
|
def test_est_terminee(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class RencontreTestCase(TestCase):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Maraudes URLconf
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
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]+)/cr/$', views.CompteRenduCreateView.as_view(), name="create"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
import datetime
|
||||||
|
import calendar
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.functional import cached_property
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
# Views
|
||||||
|
from django.views import generic
|
||||||
|
from website import views
|
||||||
|
# Models
|
||||||
|
from .models import ( Maraude, Maraudeur,
|
||||||
|
Rencontre, Lieu,
|
||||||
|
Planning, )
|
||||||
|
from .compte_rendu import CompteRendu
|
||||||
|
# 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, )
|
||||||
|
|
||||||
|
|
||||||
|
class MaraudesView(views.WebsiteProtectedMixin):
|
||||||
|
title = "Maraudes"
|
||||||
|
header = "Maraudes"
|
||||||
|
|
||||||
|
permissions = ['maraudes.view_maraudes']
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(MaraudesView, generic.TemplateView):
|
||||||
|
header = "La Maraude"
|
||||||
|
header_small = "Tableau de bord"
|
||||||
|
|
||||||
|
template_name = "maraudes/index.html"
|
||||||
|
count = 5
|
||||||
|
@cached_property
|
||||||
|
def dernieres_maraudes(self):
|
||||||
|
""" Renvoie la liste des 'Maraude' passées et terminées """
|
||||||
|
return Maraude.objects.get_past().filter(
|
||||||
|
heure_fin__isnull=False
|
||||||
|
).order_by(
|
||||||
|
'-date'
|
||||||
|
)[:self.count]
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['dernieres_maraudes'] = self.dernieres_maraudes
|
||||||
|
return context
|
||||||
|
|
||||||
|
## MARAUDES
|
||||||
|
|
||||||
|
class MaraudeDetailsView(MaraudesView, generic.DetailView):
|
||||||
|
model = Maraude
|
||||||
|
context_object_name = "maraude"
|
||||||
|
template_name = "maraudes/details.html"
|
||||||
|
|
||||||
|
# Template
|
||||||
|
header = "Maraude"
|
||||||
|
header_small = "Celle-ci"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['compte_rendu'] = CompteRendu.objects.get(pk=self.object.pk)
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MaraudeListView(MaraudesView, generic.ListView):
|
||||||
|
model = Maraude
|
||||||
|
template_name = "maraudes/list.html"
|
||||||
|
paginate_by = 10
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
today = datetime.date.today()
|
||||||
|
return super().get_queryset().filter(
|
||||||
|
date__lte=datetime.date.today()
|
||||||
|
).order_by('-date')
|
||||||
|
|
||||||
|
|
||||||
|
## COMPTE-RENDU DE MARAUDE
|
||||||
|
|
||||||
|
class CompteRenduCreateView(MaraudesView, generic.DetailView):
|
||||||
|
model = Maraude
|
||||||
|
template_name = "compte_rendu/compterendu_create.html"
|
||||||
|
|
||||||
|
form = None
|
||||||
|
inline_formset = None
|
||||||
|
|
||||||
|
def get_forms(self, *args, initial=None):
|
||||||
|
self.form = RencontreForm(*args,
|
||||||
|
initial=initial)
|
||||||
|
self.inline_formset = ObservationInlineFormSet(
|
||||||
|
*args,
|
||||||
|
instance=self.form.instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
# TODO: check for errors to avoid last entry to be lost
|
||||||
|
# Save 'heure_fin' on related Maraude object
|
||||||
|
maraude = self.get_object()
|
||||||
|
maraude.heure_fin = timezone.now()
|
||||||
|
maraude.save()
|
||||||
|
#TODO: send email to all Maraudeurs
|
||||||
|
return redirect("maraudes:details",
|
||||||
|
pk=self.get_object().pk
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
self.get_forms(request.POST, request.FILES)
|
||||||
|
if self.form.has_changed():
|
||||||
|
if not self.inline_formset.has_changed():
|
||||||
|
if request.GET['finalize'] == "True":
|
||||||
|
return self.finalize()
|
||||||
|
messages.warning(request, "Vous devez ajouter une observation !")
|
||||||
|
return self.get(request, new_form=False)
|
||||||
|
|
||||||
|
if not self.form.is_valid() or not self.inline_formset.is_valid():
|
||||||
|
return self.get(request, new_form=False)
|
||||||
|
rencontre = self.form.save(commit=False)
|
||||||
|
rencontre.maraude = self.get_object()
|
||||||
|
rencontre.save()
|
||||||
|
self.inline_formset.save()
|
||||||
|
|
||||||
|
return self.get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, request, new_form=True, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
if request.GET['finalize'] == "True":
|
||||||
|
return self.finalize()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def calculate_end_time(debut, duree):
|
||||||
|
end_minute = debut.minute + duree
|
||||||
|
hour = debut.hour + end_minute // 60
|
||||||
|
minute = end_minute % 60
|
||||||
|
return datetime.time(
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
debut.second
|
||||||
|
)
|
||||||
|
if new_form:
|
||||||
|
last_rencontre = self.get_object().rencontres.last()
|
||||||
|
initial = None
|
||||||
|
if last_rencontre:
|
||||||
|
initial = {
|
||||||
|
'lieu': last_rencontre.lieu,
|
||||||
|
'heure_debut': calculate_end_time(
|
||||||
|
last_rencontre.heure_debut,
|
||||||
|
last_rencontre.duree),
|
||||||
|
}
|
||||||
|
self.get_forms(initial=initial)
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['form'] = self.form
|
||||||
|
context['inline_formset'] = self.inline_formset
|
||||||
|
context['rencontres'] = self.get_object().rencontres.order_by("-heure_debut")
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CompteRenduUpdateView(MaraudesView, generic.DetailView):
|
||||||
|
""" Mettre à jour le compte-rendu de la maraude """
|
||||||
|
model = Maraude
|
||||||
|
context_object_name = "maraude"
|
||||||
|
template_name = "compte_rendu/compterendu_update.html"
|
||||||
|
|
||||||
|
base_formset = None
|
||||||
|
inline_formsets = []
|
||||||
|
rencontres_queryset = None
|
||||||
|
forms = None
|
||||||
|
|
||||||
|
def get_rencontres_queryset(self):
|
||||||
|
return self.get_object().rencontres.all()
|
||||||
|
|
||||||
|
def get_forms_with_inline(self, *args):
|
||||||
|
self.base_formset = RencontreInlineFormSet(
|
||||||
|
*args,
|
||||||
|
instance=self.get_object(),
|
||||||
|
prefix="rencontres"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.inline_formsets = []
|
||||||
|
for i, instance in enumerate(self.get_rencontres_queryset()):
|
||||||
|
inline_formset = ObservationInlineFormSetNoExtra(
|
||||||
|
*args,
|
||||||
|
instance = instance,
|
||||||
|
prefix = "observation-%i" % i
|
||||||
|
)
|
||||||
|
self.inline_formsets.append(inline_formset)
|
||||||
|
|
||||||
|
# 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 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()
|
||||||
|
else:
|
||||||
|
self.errors = True
|
||||||
|
|
||||||
|
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):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['base_formset'] = self.base_formset
|
||||||
|
context['forms'] = self.forms
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
## PLANNING
|
||||||
|
|
||||||
|
class PlanningView(MaraudesView, generic.TemplateView):
|
||||||
|
""" Display and edit the planning of next Maraudes """
|
||||||
|
|
||||||
|
template_name = "planning/planning.html"
|
||||||
|
|
||||||
|
title = "Planning"
|
||||||
|
header = "Plannification des maraudes"
|
||||||
|
header_small = "Mois Année"
|
||||||
|
|
||||||
|
def _parse_request(self):
|
||||||
|
self.current_date = datetime.date.today()
|
||||||
|
try: self.month = int(self.request.GET['month'])
|
||||||
|
except: self.month = self.current_date.month
|
||||||
|
try: self.year = int(self.request.GET['year'])
|
||||||
|
except: self.year = self.current_date.year
|
||||||
|
|
||||||
|
def _calculate_initials(self):
|
||||||
|
self._parse_request()
|
||||||
|
self.initials = []
|
||||||
|
for day, time in Planning.get_maraudes_days_for_month(self.year, self.month):
|
||||||
|
date = datetime.date(self.year, self.month, day)
|
||||||
|
try:
|
||||||
|
maraude = Maraude.objects.get(date=date)
|
||||||
|
except Maraude.DoesNotExist:
|
||||||
|
self.initials.append({
|
||||||
|
'date': date,
|
||||||
|
'heure_debut': time,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Maraude.objects.filter(
|
||||||
|
date__month=self.month,
|
||||||
|
date__year=self.year,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_formset(self, *args):
|
||||||
|
self._calculate_initials()
|
||||||
|
return modelformset_factory(
|
||||||
|
Maraude,
|
||||||
|
form = MaraudeAutoDateForm,
|
||||||
|
extra = len(self.initials),
|
||||||
|
)(
|
||||||
|
*args,
|
||||||
|
queryset = self.get_queryset(),
|
||||||
|
initial = self.initials
|
||||||
|
)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
self.formset = self.get_formset(request.POST, request.FILES)
|
||||||
|
for form in self.formset.forms:
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
return redirect('maraudes:index')
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
self.formset = self.get_formset()
|
||||||
|
return super().get(request)
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
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
|
||||||
|
|
||||||
|
class LieuCreateView(MaraudesView, views.AjaxTemplateMixin, generic.edit.CreateView):
|
||||||
|
model = Lieu
|
||||||
|
ajax_template_name = "maraudes/lieu_create_inner.html"
|
||||||
|
template_name = "maraudes/lieu_create.html"
|
||||||
|
fields = "__all__"
|
||||||
|
success_url = "/maraudes/"
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
if 'next' in self.request.POST:
|
||||||
|
self.success_url = self.request.POST["next"]
|
||||||
|
return super().post(self, request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
try:
|
||||||
|
context['next'] = self.request.GET['next']
|
||||||
|
except:
|
||||||
|
context['next'] = None
|
||||||
|
return context
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,10 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import *
|
||||||
|
# Register your models here.
|
||||||
|
|
||||||
|
@admin.register(Note)
|
||||||
|
class NoteAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
list_display = ['id', 'sujet']
|
||||||
|
list_filter = ('sujet',)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class NotesConfig(AppConfig):
|
||||||
|
name = 'notes'
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from .models import Note
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
from django_select2.forms import Select2Widget
|
||||||
|
from django.forms import Textarea
|
||||||
|
|
||||||
|
class NoteForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Note
|
||||||
|
fields = ['sujet', 'text']
|
||||||
|
widgets = {
|
||||||
|
'sujet': Select2Widget(),
|
||||||
|
'text': Textarea(attrs={'rows':4}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Get data for extra fields of Note
|
||||||
|
|
||||||
|
return super().save(*args, **kwargs)
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-08-04 10:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Note',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('text', models.TextField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-08-04 10:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('notes', '0001_initial'),
|
||||||
|
('utilisateurs', '0001_initial'),
|
||||||
|
('sujets', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='note',
|
||||||
|
name='created_by',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='utilisateurs.Professionnel'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='note',
|
||||||
|
name='sujet',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='sujets.Sujet'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,39 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Note(models.Model):
|
||||||
|
""" Note relative à un sujet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
sujet = models.ForeignKey(
|
||||||
|
'sujets.Sujet',
|
||||||
|
related_name="notes",
|
||||||
|
on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
text = models.TextField()
|
||||||
|
created_by = models.ForeignKey(
|
||||||
|
'utilisateurs.Professionnel',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
#date_created = models.DateField('Crée le')
|
||||||
|
|
||||||
|
|
||||||
|
def as_table(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_header(self):
|
||||||
|
""" Informations included in headers """
|
||||||
|
return ('Note', [])
|
||||||
|
|
||||||
|
def get_date(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def header_label(self):
|
||||||
|
return self.get_header()[0]
|
||||||
|
|
||||||
|
def header_infos(self):
|
||||||
|
return self.get_header()[1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
return self.get_date()
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Requirements
|
||||||
|
|
||||||
|
* django-bootstrap3
|
||||||
|
* django_select2
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SuiviConfig(AppConfig):
|
||||||
|
name = 'suivi'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<ul class="nav nav-pills nav-justified" role="tablist" style="padding-bottom:20px;float:bottom;">
|
||||||
|
<li role="presentation" class="active"><a href="#suivi" aria-controls="suivi" role="tab" data-toggle="tab">Suivi</a></li>
|
||||||
|
<li role="presentation"><a href="#fiche" aria-controls="fiche" role="tab" data-toggle="tab">Fiche</a></li>
|
||||||
|
</ul>
|
||||||
|
<!-- Tab panes -->
|
||||||
|
<div class="tab-content">
|
||||||
|
<div role="tabpanel" class="tab-pane active" id="suivi">{% include "suivis/sujet_suivi.html" %}</div>
|
||||||
|
<div role="tabpanel" class="tab-pane" id="fiche">{% include "sujets/sujet_details_inner.html" %}</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">Sujets</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="{% url 'sujets:liste' %}" class="list-group-item">Liste complète</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if user.is_superuser %}
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-danger">
|
||||||
|
<div class="panel-heading">Administration</div>
|
||||||
|
<div class="list-group">
|
||||||
|
<a href="{% url 'sujets:create' %}" class="list-group-item">Nouveau sujet</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
{% for note in notes %}
|
||||||
|
<tr><th>{{note.date}}
|
||||||
|
<div class="pull-right">
|
||||||
|
<span class="label label-primary">{{ note.header_label }}</span>
|
||||||
|
{% for info in note.header_infos %}<span class="label label-info">{{ info }}</span>
|
||||||
|
{%endfor%}
|
||||||
|
</div>
|
||||||
|
</th></tr>
|
||||||
|
<tr><td>{{note.note}}</td></tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
from sujets import views as sujets_views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(r'^$', views.IndexView.as_view(), name="index"),
|
||||||
|
url(r'(?P<pk>[0-9]+)/$', views.SuiviSujetView.as_view(), name="details"),
|
||||||
|
]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from django.views import generic
|
||||||
|
from website import views
|
||||||
|
|
||||||
|
from sujets.models import Sujet
|
||||||
|
|
||||||
|
# Create your views here.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class SuivisView(views.WebsiteProtectedMixin):
|
||||||
|
title = "Suivi des bénéficiaires"
|
||||||
|
header = "Suivi"
|
||||||
|
|
||||||
|
permissions = ['sujets.view_sujets']
|
||||||
|
|
||||||
|
|
||||||
|
class IndexView(SuivisView, generic.TemplateView):
|
||||||
|
template_name = "suivis/index.html"
|
||||||
|
header_small = "Tableau de bord"
|
||||||
|
|
||||||
|
|
||||||
|
class SuiviSujetView(SuivisView, generic.DetailView):
|
||||||
|
model = Sujet
|
||||||
|
template_name = "suivis/details.html"
|
||||||
|
context_object_name = "sujet"
|
||||||
|
|
||||||
|
def get_context_date(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['notes'] = self.object.notes.all()
|
||||||
|
return context
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,14 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Sujet
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Sujet)
|
||||||
|
class SujetAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
fieldsets = [
|
||||||
|
('Identité', {'fields': [('nom', 'prenom'), 'genre']}),
|
||||||
|
('Informations', {'fields': ['age', ]}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class SujetsConfig(AppConfig):
|
||||||
|
name = 'sujets'
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.7 on 2016-08-04 10:21
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Personne',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('genre', models.CharField(choices=[('M', 'Homme'), ('Mme', 'Femme')], default='M', max_length=3)),
|
||||||
|
('nom', models.CharField(blank=True, max_length=32)),
|
||||||
|
('prenom', models.CharField(blank=True, max_length=32)),
|
||||||
|
('surnom', models.CharField(blank=True, max_length=64)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Sujet',
|
||||||
|
fields=[
|
||||||
|
('personne_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='sujets.Personne')),
|
||||||
|
('premiere_rencontre', models.DateField(default=django.utils.timezone.now)),
|
||||||
|
('age', models.SmallIntegerField(blank=True, null=True)),
|
||||||
|
('lien_familial', models.NullBooleanField(verbose_name='Lien Familial')),
|
||||||
|
('parcours_de_vie', models.CharField(choices=[('Familial', 'Parcours familial'), ('Institutionnel', 'Parcours institutionnel'), ('Non renseigné', 'Ne sait pas')], default='Non renseigné', max_length=64)),
|
||||||
|
('prob_psychiatrie', models.NullBooleanField(verbose_name='Psychiatrie')),
|
||||||
|
('prob_administratif', models.NullBooleanField(verbose_name='Administratif')),
|
||||||
|
('prob_addiction', models.NullBooleanField(verbose_name='Addiction')),
|
||||||
|
('prob_somatique', models.NullBooleanField(verbose_name='Somatique')),
|
||||||
|
('habitation', models.CharField(choices=[('Sans Abri', 'Sans abri'), ('Hébergement', 'Hébergé'), ('Logement', 'Logé'), ('Mal logé', 'Mal logé'), ('Non renseigné', 'Ne sait pas')], default='Non renseigné', max_length=64, verbose_name="Type d'habitat")),
|
||||||
|
('ressources', models.CharField(choices=[('AAH', 'AAH'), ('RSA', 'RSA'), ('Pas de ressources', 'Aucune'), ('Pôle Emploi', 'Pôle emploi'), ('Autres', 'Autres ressources'), ('Non renseigné', 'Ne sait pas')], default='Non renseigné', max_length=64, verbose_name='Ressources')),
|
||||||
|
('connu_siao', models.NullBooleanField(verbose_name='Connu du SIAO ?')),
|
||||||
|
],
|
||||||
|
bases=('sujets.personne',),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,124 @@
|
|||||||
|
from django.utils import timezone
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
|
||||||
|
class Personne(models.Model):
|
||||||
|
""" Modèle de base d'une personne
|
||||||
|
- genre
|
||||||
|
- nom
|
||||||
|
- prénom
|
||||||
|
"""
|
||||||
|
|
||||||
|
HOMME = 'M'
|
||||||
|
FEMME = 'Mme'
|
||||||
|
GENRE_CHOICES = (
|
||||||
|
(HOMME, 'Homme'),
|
||||||
|
(FEMME, 'Femme'),
|
||||||
|
)
|
||||||
|
genre = models.CharField(max_length=3,
|
||||||
|
choices=GENRE_CHOICES,
|
||||||
|
default=HOMME)
|
||||||
|
nom = models.CharField(max_length=32, blank=True)
|
||||||
|
prenom = models.CharField(max_length=32, blank=True)
|
||||||
|
surnom = models.CharField(max_length=64, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
string = '%s ' % self.genre
|
||||||
|
if self.nom: string += '%s ' % self.nom
|
||||||
|
if self.surnom: string += '"%s" ' % self.surnom
|
||||||
|
if self.prenom: string += '%s' % self.prenom
|
||||||
|
return string
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not any([self.nom, self.prenom, self.surnom]):
|
||||||
|
raise ValidationError(_("Vous devez remplir au moins un nom, prénom ou surnom"))
|
||||||
|
return super().clean()
|
||||||
|
|
||||||
|
# Item: Parcours institutionnel
|
||||||
|
PARCOURS_INSTITUTIONNEL = "Institutionnel"
|
||||||
|
PARCOURS_FAMILIAL = "Familial"
|
||||||
|
PARCOURS_NR = "Non renseigné"
|
||||||
|
PARCOURS_DE_VIE_CHOICES = (
|
||||||
|
(PARCOURS_FAMILIAL, "Parcours familial"),
|
||||||
|
(PARCOURS_INSTITUTIONNEL, "Parcours institutionnel"),
|
||||||
|
(PARCOURS_NR, "Ne sait pas"),
|
||||||
|
)
|
||||||
|
|
||||||
|
#Item: Type d'habitation
|
||||||
|
HABITATION_SANS = "Sans Abri"
|
||||||
|
HABITATION_LOGEMENT = "Logement"
|
||||||
|
HABITATION_TIERS = "Hébergement"
|
||||||
|
HABITATION_MAL_LOGEMENT = "Mal logé"
|
||||||
|
HABITATION_NR = "Non renseigné"
|
||||||
|
HABITATION_CHOICES = (
|
||||||
|
(HABITATION_SANS, "Sans abri"),
|
||||||
|
(HABITATION_TIERS, "Hébergé"),
|
||||||
|
(HABITATION_LOGEMENT, "Logé"),
|
||||||
|
(HABITATION_MAL_LOGEMENT, "Mal logé"),
|
||||||
|
(HABITATION_NR, "Ne sait pas"),
|
||||||
|
)
|
||||||
|
|
||||||
|
#Item: Ressources
|
||||||
|
RESSOURCES_RSA = "RSA"
|
||||||
|
RESSOURCES_AAH = "AAH"
|
||||||
|
RESSOURCES_POLE_EMPLOI = "Pôle Emploi"
|
||||||
|
RESSOURCES_AUTRES = "Autres"
|
||||||
|
RESSOURCES_SANS = "Pas de ressources"
|
||||||
|
RESSOURCES_NR = "Non renseigné"
|
||||||
|
RESSOURCES_CHOICES = (
|
||||||
|
(RESSOURCES_AAH, "AAH"),
|
||||||
|
(RESSOURCES_RSA, "RSA"),
|
||||||
|
(RESSOURCES_SANS, "Aucune"),
|
||||||
|
(RESSOURCES_POLE_EMPLOI, "Pôle emploi"),
|
||||||
|
(RESSOURCES_AUTRES, "Autres ressources"),
|
||||||
|
(RESSOURCES_NR, "Ne sait pas")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Sujet(Personne):
|
||||||
|
""" Personne faisant l'objet d'un suivi par la maraude
|
||||||
|
|
||||||
|
"""
|
||||||
|
# referent = models.ForeignKey("utilisateurs.Professionnel", related_name="suivis")
|
||||||
|
|
||||||
|
premiere_rencontre = models.DateField(default=timezone.now)
|
||||||
|
age = models.SmallIntegerField(blank=True, null=True)
|
||||||
|
|
||||||
|
lien_familial = models.NullBooleanField("Lien Familial")
|
||||||
|
parcours_de_vie = models.CharField(max_length=64,
|
||||||
|
choices=PARCOURS_DE_VIE_CHOICES,
|
||||||
|
default=PARCOURS_NR)
|
||||||
|
|
||||||
|
# Problématiques
|
||||||
|
prob_psychiatrie = models.NullBooleanField("Psychiatrie")
|
||||||
|
prob_administratif = models.NullBooleanField("Administratif")
|
||||||
|
prob_addiction = models.NullBooleanField("Addiction")
|
||||||
|
prob_somatique = models.NullBooleanField("Somatique")
|
||||||
|
|
||||||
|
# Logement
|
||||||
|
habitation = models.CharField("Type d'habitat", max_length=64,
|
||||||
|
choices=HABITATION_CHOICES,
|
||||||
|
default=HABITATION_NR)
|
||||||
|
ressources = models.CharField("Ressources", max_length=64,
|
||||||
|
choices=RESSOURCES_CHOICES,
|
||||||
|
default=RESSOURCES_NR)
|
||||||
|
connu_siao = models.NullBooleanField("Connu du SIAO ?")
|
||||||
|
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Sujet"
|
||||||
|
permissions = (
|
||||||
|
('view_sujets', "Accès à l'application 'sujets'"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('suivis:details', kwargs={'pk': self.id})
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% extends "base_site.html" %}
|
||||||
|
|
||||||
|
{% block title %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}{% endblock %}
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
{% include "sujets/sujet_create_inner.html" %}
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{% load bootstrap3 %}
|
||||||
|
<div class="row"><div class="col-md-12">
|
||||||
|
<form class="form-horizontal" action="{% url "sujets:create" %}" method="post">{% csrf_token %}
|
||||||
|
{% bootstrap_form form layout="horizontal"%}
|
||||||
|
{% bootstrap_button "Ajouter un sujet" button_type="submit" button_class="btn btn-primary" %}
|
||||||
|
{% if next %}<input type="text" hidden=True name="next" value="{{ next }}" />{%endif%}
|
||||||
|
</form>
|
||||||
|
</div></div>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{% include 'sujets/sujet_details_inner.html' %}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4 text-center">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p>Première rencontre le {{sujet.premiere_rencontre}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 col-md-offset-4">
|
||||||
|
<a href="{% url "sujets:update" pk=sujet.id %}" class="btn btn-primary">Mettre à jour</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
État-civil
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Nom : {{ sujet.nom|default:'?' }}</p>
|
||||||
|
<p>Prénom : {{ sujet.prenom|default:'?' }}</p>
|
||||||
|
<p>Sexe : {{ sujet.genre }}</p>
|
||||||
|
<p>Âge : {{ sujet.age|default:'?' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Problématiques
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Psychiatrique : {{ sujet.prob_psychiatrie }}</p>
|
||||||
|
<p>Administratif : {{ sujet.prob_administratif }}</p>
|
||||||
|
<p>Addiction : {{ sujet.prob_addiction }}</p>
|
||||||
|
<p>Somatique : {{ sujet.prob_somatique }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!- Hébergement ->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Habitation
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<h4></h4>
|
||||||
|
<p>Type : {{ sujet.habitation }}</p>
|
||||||
|
<p>Connu du SIAO : {{ sujet.connu_siao }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!- Ressources ->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Ressources
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>{{ sujet.ressources }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!- Parcours de vie ->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
Parcours de vie
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Parcours : {{ sujet.parcours_de_vie }}</p>
|
||||||
|
<p>Lien familial : {{ sujet.lien_familial }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!- row ->
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user