Adding the core applications code to the repository

This commit is contained in:
artus
2016-08-05 10:41:43 +02:00
parent 243ff9153e
commit 5f4faf46ec
155 changed files with 13176 additions and 0 deletions

0
maraudes/__init__.py Normal file
View File

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.

47
maraudes/admin.py Normal file
View 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
View 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
View 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
View 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
View 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()

View 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',
},
),
]

View 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'),
),
]

View 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',),
),
]

View File

235
maraudes/models.py Normal file
View 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
View 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])

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

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

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

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

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

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

View File

@@ -0,0 +1 @@
{% include "maraudes/lieu_create_inner.html" %}

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

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

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

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