Adding the core applications code to the repository
This commit is contained in:
0
maraudes/__init__.py
Normal file
0
maraudes/__init__.py
Normal file
BIN
maraudes/__pycache__/__init__.cpython-34.pyc
Normal file
BIN
maraudes/__pycache__/__init__.cpython-34.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/__init__.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/__init__.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/admin.cpython-34.pyc
Normal file
BIN
maraudes/__pycache__/admin.cpython-34.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/admin.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/admin.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/apps.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/apps.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/compte_rendu.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/compte_rendu.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/forms.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/forms.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/managers.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/managers.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/models.cpython-34.pyc
Normal file
BIN
maraudes/__pycache__/models.cpython-34.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/models.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/models.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/notes.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/notes.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/suivi.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/suivi.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/tests.cpython-34.pyc
Normal file
BIN
maraudes/__pycache__/tests.cpython-34.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/tests.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/tests.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/urls.cpython-34.pyc
Normal file
BIN
maraudes/__pycache__/urls.cpython-34.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/urls.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/urls.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/views.cpython-34.pyc
Normal file
BIN
maraudes/__pycache__/views.cpython-34.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/views.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/views.cpython-35.pyc
Normal file
Binary file not shown.
BIN
maraudes/__pycache__/widgets.cpython-35.pyc
Normal file
BIN
maraudes/__pycache__/widgets.cpython-35.pyc
Normal file
Binary file not shown.
47
maraudes/admin.py
Normal file
47
maraudes/admin.py
Normal file
@@ -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')
|
||||
10
maraudes/apps.py
Normal file
10
maraudes/apps.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class Config(AppConfig):
|
||||
name = 'maraudes'
|
||||
|
||||
index_url = "/maraudes/"
|
||||
|
||||
def get_index_url(self):
|
||||
return "/maraudes/"
|
||||
41
maraudes/compte_rendu.py
Normal file
41
maraudes/compte_rendu.py
Normal file
@@ -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
|
||||
|
||||
|
||||
63
maraudes/forms.py
Normal file
63
maraudes/forms.py
Normal file
@@ -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
|
||||
|
||||
102
maraudes/managers.py
Normal file
102
maraudes/managers.py
Normal file
@@ -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()
|
||||
40
maraudes/migrations/0001_initial.py
Normal file
40
maraudes/migrations/0001_initial.py
Normal file
@@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
61
maraudes/migrations/0002_auto_20160804_1221.py
Normal file
61
maraudes/migrations/0002_auto_20160804_1221.py
Normal file
@@ -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'),
|
||||
),
|
||||
]
|
||||
39
maraudes/migrations/0003_auto_20160804_1221.py
Normal file
39
maraudes/migrations/0003_auto_20160804_1221.py
Normal file
@@ -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',),
|
||||
),
|
||||
]
|
||||
0
maraudes/migrations/__init__.py
Normal file
0
maraudes/migrations/__init__.py
Normal file
BIN
maraudes/migrations/__pycache__/0001_initial.cpython-35.pyc
Normal file
BIN
maraudes/migrations/__pycache__/0001_initial.cpython-35.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
maraudes/migrations/__pycache__/__init__.cpython-35.pyc
Normal file
BIN
maraudes/migrations/__pycache__/__init__.cpython-35.pyc
Normal file
Binary file not shown.
235
maraudes/models.py
Normal file
235
maraudes/models.py
Normal file
@@ -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)
|
||||
27
maraudes/notes.py
Normal file
27
maraudes/notes.py
Normal file
@@ -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])
|
||||
29
maraudes/templates/compte_rendu/compterendu.html
Normal file
29
maraudes/templates/compte_rendu/compterendu.html
Normal file
@@ -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>
|
||||
144
maraudes/templates/compte_rendu/compterendu_create.html
Normal file
144
maraudes/templates/compte_rendu/compterendu_create.html
Normal file
@@ -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>
|
||||
62
maraudes/templates/compte_rendu/compterendu_form.html
Normal file
62
maraudes/templates/compte_rendu/compterendu_form.html
Normal file
@@ -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>
|
||||
21
maraudes/templates/compte_rendu/compterendu_update.html
Normal file
21
maraudes/templates/compte_rendu/compterendu_update.html
Normal file
@@ -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>
|
||||
6
maraudes/templates/maraudes/details.html
Normal file
6
maraudes/templates/maraudes/details.html
Normal file
@@ -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 %}
|
||||
45
maraudes/templates/maraudes/index.html
Normal file
45
maraudes/templates/maraudes/index.html
Normal file
@@ -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>
|
||||
|
||||
1
maraudes/templates/maraudes/lieu_create.html
Normal file
1
maraudes/templates/maraudes/lieu_create.html
Normal file
@@ -0,0 +1 @@
|
||||
{% include "maraudes/lieu_create_inner.html" %}
|
||||
8
maraudes/templates/maraudes/lieu_create_inner.html
Normal file
8
maraudes/templates/maraudes/lieu_create_inner.html
Normal file
@@ -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>
|
||||
49
maraudes/templates/maraudes/list.html
Normal file
49
maraudes/templates/maraudes/list.html
Normal file
@@ -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 %}
|
||||
16
maraudes/templates/maraudes/menu.html
Normal file
16
maraudes/templates/maraudes/menu.html
Normal file
@@ -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 %}
|
||||
30
maraudes/templates/planning/planning.html
Normal file
30
maraudes/templates/planning/planning.html
Normal file
@@ -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>
|
||||
101
maraudes/tests.py
Normal file
101
maraudes/tests.py
Normal file
@@ -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
|
||||
16
maraudes/urls.py
Normal file
16
maraudes/urls.py
Normal file
@@ -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"),
|
||||
]
|
||||
315
maraudes/views.py
Normal file
315
maraudes/views.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user