Remaster (#38)

* setup new 'statistiques' module

* added 'graphos' package and created first test graph

* put graphos in requirements, deleted local folder

* added "load_csv" management command !

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

* added missing urls.py file

* added 'merge' action and view

* added 'info_completed' ratio

* linked sujets:merge views inside suivi:details

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

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

* small cleanup

* worked on Maraude and Sujet lists

* corrected missing line in notes.__init__

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

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

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

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

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

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

* added error-prone cases in paginator

* fixed non-working modals, added titles

* added UpdateStatistiques capacity in CompteRenduCreate view

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

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

* re-instated statistiques module link and first test page

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

* Added PieChart view for FicheStatistique fields

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

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

* small theme change

* removed some dead code

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

* added some tests

* added customised admin templates

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

* added django-nose for test coverage

* Corrected raising exception on first migration

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

* Better try block

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

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

View File

@@ -0,0 +1 @@
default_app_config = 'notes.apps.NotesConfig'

38
notes/actions.py Normal file
View File

@@ -0,0 +1,38 @@
from .models import Sujet
from statistiques.models import FicheStatistique, NSP
def merge_stats(main, merged):
""" Merge stats of two sujets according to priority order : main, then merged """
# TODO: replace hardcoded field names with more flexible getters
# Fields of 'Sujet' model
for field in ('nom', 'prenom', 'surnom', 'age',):
if not getattr(main, field):
setattr(main, field, getattr(merged, field, None))
# Première rencontre : retenir la plus ancienne
if merged.premiere_rencontre:
if not main.premiere_rencontre or main.premiere_rencontre > merged.premiere_rencontre:
main.premiere_rencontre = merged.premiere_rencontre
# Fields of 'FicheStatistique' model
# NullBoolean fields
for field in ('prob_psychiatrie', 'prob_somatique',
'prob_administratif', 'prob_addiction',
'connu_siao', 'lien_familial'):
if not getattr(main.statistiques, field): # Ignore if already filled
setattr(main.statistiques, field, getattr(merged.statistiques, field, None))
# Choice fields, None is NSP
for field in ('habitation', 'ressources', 'parcours_de_vie'):
if getattr(main.statistiques, field) == NSP: # Ignore if already filled
setattr(main.statistiques, field, getattr(merged.statistiques, field, NSP))
def merge_two(main, merged):
""" Merge 'merged' sujet into 'main' one """
merge_stats(main, merged) # Merge statistics and informations
for note in merged.notes.all(): # Move all notes
note.sujet = main
note.save()
main.save()
merged.delete()

View File

@@ -3,6 +3,15 @@ from django.contrib import admin
from .models import *
# Register your models here.
@admin.register(Sujet)
class SujetAdmin(admin.ModelAdmin):
fieldsets = [
('Identité', {'fields': [('nom', 'prenom'), 'genre']}),
('Informations', {'fields': ['age', ]}),
]
@admin.register(Note)
class NoteAdmin(admin.ModelAdmin):
@@ -16,4 +25,4 @@ class NoteAdmin(admin.ModelAdmin):
]
list_display = ['created_date', 'sujet', 'child_class', 'text']
list_filter = ('sujet', 'created_date', 'created_by')
list_filter = ('created_date', 'created_by')

View File

@@ -1,5 +1,9 @@
from django.apps import AppConfig
from watson import search as watson
class NotesConfig(AppConfig):
name = 'notes'
def ready(self):
Sujet = self.get_model("Sujet")
watson.register(Sujet, fields=('nom', 'prenom', 'surnom'))

View File

@@ -1,11 +1,12 @@
from .models import Note
import datetime
from .models import Note, Sujet
from utilisateurs.models import Professionnel
from django import forms
from django_select2.forms import Select2Widget
from django.forms import Textarea
### NOTES
class NoteForm(forms.ModelForm):
""" Generic Note form """
@@ -14,7 +15,7 @@ class NoteForm(forms.ModelForm):
fields = ['sujet', 'text', 'created_by', 'created_date', 'created_time']
widgets = {
'sujet': Select2Widget(),
'text': Textarea(attrs={'rows':4}),
'text': forms.Textarea(attrs={'rows':4}),
}
@@ -54,6 +55,8 @@ class UserNoteForm(NoteForm):
instance.save()
return instance
class AutoNoteForm(UserNoteForm):
class Meta(UserNoteForm.Meta):
fields = ['text']
@@ -68,3 +71,24 @@ class AutoNoteForm(UserNoteForm):
if commit:
inst.save()
return inst
### SUJETS
current_year = datetime.date.today().year
YEAR_CHOICE = tuple(year - 2 for year in range(current_year, current_year + 10))
class SujetCreateForm(forms.ModelForm):
class Meta:
model = Sujet
fields = ['nom', 'surnom', 'prenom', 'genre', 'premiere_rencontre']
widgets = {
'premiere_rencontre': forms.SelectDateWidget( empty_label=("Année", "Mois", "Jour"),
years = YEAR_CHOICE,
),
}
class SelectSujetForm(forms.Form):
sujet = forms.ModelChoiceField(queryset=Sujet.objects.all(), widget=Select2Widget)

View File

@@ -1,9 +1,74 @@
import logging
from django.utils import timezone
from django.utils.html import format_html
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.db import models
from . import managers
logger = logging.getLogger(__name__)
HOMME = 'M'
FEMME = 'Mme'
GENRE_CHOICES = (
(HOMME, 'Homme'),
(FEMME, 'Femme'),
)
class Sujet(models.Model):
""" Personne faisant l'objet d'un suivi par la maraude
"""
genre = models.CharField("Genre",
max_length=3,
choices=GENRE_CHOICES,
default=HOMME)
nom = models.CharField(max_length=32, blank=True)
prenom = models.CharField(max_length=32, blank=True)
surnom = models.CharField(max_length=64, blank=True)
premiere_rencontre = models.DateField(
blank=True, null=True,
default=timezone.now
)
age = models.SmallIntegerField(
blank=True, null=True
)
# referent = models.ForeignKey("utilisateurs.Professionnel", related_name="suivis")
def __str__(self):
string = '%s ' % self.genre
if self.nom: string += '%s ' % self.nom
if self.surnom: string += '"%s" ' % self.surnom
if self.prenom: string += '%s' % self.prenom
return string
def clean(self):
if not any([self.nom, self.prenom, self.surnom]):
raise ValidationError("Vous devez remplir au moins un nom, prénom ou surnom")
return super().clean()
def save(self, *args, **kwargs):
self.clean()
if not self.id:
from statistiques.models import FicheStatistique
super().save(*args, **kwargs)
fiche = FicheStatistique.objects.create(sujet=self)
else:
return super().save(*args, **kwargs)
class Meta:
verbose_name = "Sujet"
ordering = ('surnom', 'nom', 'prenom')
def get_absolute_url(self):
return reverse("notes:details-sujet", kwargs={"pk": self.pk })
class Note(models.Model):
""" Note relative à un sujet.
@@ -19,11 +84,12 @@ class Note(models.Model):
objects = managers.NoteManager()
sujet = models.ForeignKey(
'sujets.Sujet',
Sujet,
related_name="notes",
on_delete=models.CASCADE
)
text = models.TextField("Texte")
created_by = models.ForeignKey(
'utilisateurs.Professionnel',
blank=True,
@@ -42,7 +108,11 @@ class Note(models.Model):
return super().save(*args, **kwargs)
def __str__(self):
return "%s" % (self.child_class.__qualname__)
return "<%s: %s>" % (self.child_class.__qualname__, self.sujet)
@classmethod
def __str__(cls):
return "<%s>" % cls.__qualname__
def note_author(self):
return None

View File

@@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block title %}Notes >{% endblock %}
{% block breadcrumbs %}
<li>Notes</li>
{% endblock %}
{% block sidebar %}
<div class="panel panel-default">
<div class="panel-body">
{% include "notes/menu.html" %}
</div>
</div>{% endblock %}

View File

@@ -0,0 +1,33 @@
{% extends "notes/base.html" %}
{% load bootstrap3 notes %}
{% block page_content %}
<div class="col-lg-8 col-md-12">
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-primary">
<div class="panel-heading" role="tab" id="notesHeading">
<h3 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNotes" aria-expanded="true" aria-controls="collapseOne">
Notes</a>
</h3>
</div>
{% block pre_content %}{% endblock %}
<table class="table table-striped table-bordered">
{% for note in notes %}
{% if maraude %}
{% inline_table note header="sujet" %}
{% elif sujet %}
{% inline_table note header="date" %}
{% endif %}
{% endfor %}
</table>
{% block post_content %}{% endblock %}
</div>
</div>
</div>
<div class="col-lg-4 col-md-12">
{% block right_column %}{% endblock %}
</div>
{% endblock %}

View File

@@ -0,0 +1,47 @@
{% extends "notes/details.html" %}
{% block title %}
{{ block.super }} {{ maraude }}
{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li><a href="{% url "notes:liste-maraude" %}">Maraudes</a></li>
<li>{{ maraude }}</li>
{% endblock %}
{% block sidebar %}
<div class="panel panel-primary text-right">
<div class="panel-body text-right">
<h4 class="panel-title">Navigation</h4>
<nav aria-label="Maraudes navigation">
<ul class="pagination">
<li {% if not prev_maraude %}class="disabled"{% endif %}>
<a href="{% if prev_maraude %}{% url "notes:details-maraude" prev_maraude.pk %}{% else %}#{% endif %}" aria-label="Previous">
&nbsp; <span aria-hidden="true" class="glyphicon glyphicon-chevron-left"></span>
</a>
</li>
<li {% if not next_maraude %}class="disabled"{% endif %}>
<a href="{% if next_maraude %}{% url "notes:details-maraude" next_maraude.pk %}{%else%}#{% endif %}" aria-label="Next">
<span aria-hidden="true" class="glyphicon glyphicon-chevron-right"></span> &nbsp;
</a>
</li>
</ul>
</nav>
</div>
</div>
{% endblock %}
{% block right_column %}
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="notesSujetHeading">
<h3 class="panel-title">
Informations</h3>
</div>
<div class="panel-body">
<p><strong>Maraudeurs :</strong>&nbsp; {{ maraude.binome }} & {{ maraude.referent }}</p>
<p><strong>Nombre de rencontres</strong>&nbsp; {{ maraude.rencontres.count }}</p>
<p><strong>Nombre de personnes rencontrées</strong>&nbsp; {{ maraude.observations_count }}</p>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,103 @@
{% extends "notes/details.html" %}
{% load bootstrap3 %}
{% block title %}
{{ block.super }} {{ sujet }}
{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li><a href="{% url "notes:liste-sujet" %}">Sujets</a></li>
<li>{{ sujet }}</li>
{% endblock %}
{% block pre_content %}
<div id="collapseNotes" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="notesHeading">
{% endblock %}
{% block post_content %}
{% if notes.has_other_pages %}<div class="panel-footer text-center">
<ul class="pagination">
{% for num in notes.paginator.page_range %}
<li {% if notes.number == num %} class="active" {%endif%}><a href="?page={{num}}">{{num}}</a></li>
{%endfor%}
</ul>
</div>{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="notesAjoutHeading">
<h4 class="panel-title">
<a class="collapsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNotesAjout" aria-expanded="false" aria-controls="collapseTwo">
{% bootstrap_icon "plus" %} Ajouter une note
</a>
</h4>
</div>
<div id="collapseNotesAjout" class="panel-collapse collapse" role="tabpanel" aria-labelledby="notesAjoutHeading">
<div class="panel-body">
<form method="POST" action="">{% csrf_token %}
{% bootstrap_form note_form show_label=False %}
</div>
<div class="panel-footer text-right">
{% bootstrap_button "Enregistrer" button_type="submit" %}
</form>
</div>
</div>
{% endblock %}
{% block right_column %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">Informations</h4>
</div>
{% include "notes/details_sujet_inner.html" %}
</div>
<div class="panel panel-default">
<div class="panel-heading"><h4 class="panel-title">Statistiques</h4></div>
<div id="stats-content">
{% include "statistiques/fiche_stats_details.html" with object=sujet.statistiques %}
</div>
<div class="panel-footer text-right">
<span class="text-right" id="normal-buttons">
<p>{% bootstrap_icon "tasks" %} {{ sujet.statistiques.info_completed }}%&nbsp;
<span class="btn btn-primary btn-sm" id="update-stats">Mettre à jour</span></p>
</span>
<span class="text-right" id="update-buttons">
<label for="submit-form" class="btn btn-primary">{% bootstrap_icon "floppy-save" %} Enregistrer</label>
<span class="btn btn-primary btn-sm" id="cancel">Annuler</span>
</span>
</div>
</div>
<script type="text/javascript">
$(function() {
$("#update-buttons").hide();
$("#update-stats").click(function() {
$("#stats-content").load("{% url "statistiques:update" sujet.pk %}");
$("#normal-buttons").hide();
$("#update-buttons").show();
});
$("#cancel").click(function() {
$("#stats-content").load("{% url "statistiques:details" sujet.pk %}");
$("#update-buttons").hide();
$("#normal-buttons").show();
});
});
</script>
{% endblock %}
{% block sidebar %}
{{ block.super }}
<hr />
{% if user.is_superuser %}
<div class="panel panel-primary text-right"><div class="panel-heading"><h4 class="panel-title"><strong>Administration :</strong></h4></div>
<div class="panel-body">
<div class="btn-group" role="group" aria-label="...">
<a href="{% url 'admin:notes_note_changelist' %}?sujet__exact={{sujet.pk}}" class="btn btn-primary">{% bootstrap_icon "wrench" %} Éditer les notes</a>
<a href="{% url 'notes:sujets-merge' pk=object.pk %}" class="btn btn-default">{% bootstrap_icon "paste" %} Fusionner</a>
</div>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,22 @@
<div id="sujet-content">
<table class="table table-striped">
{% with "-" as none %}
<tr><th>Nom</th><th>Surnom</th><th>Prénom</th></tr>
<tr><td>{{ sujet.nom|default:none }}</td><td>{{ sujet.surnom|default:none}}</td><td>{{ sujet.prenom|default:none}}</td></tr>
<tr><th>Âge</th><th colspan="2">Première rencontre</th></tr>
<tr><td>{{ sujet.age|default_if_none:none }}</td><td colspan="2">{{ sujet.premiere_rencontre|default_if_none:none }}</td></tr>
{% endwith %}
</table>
<div class="panel-footer text-right" id="sujet-buttons">
<span class="text-right"><span class="btn btn-primary btn-sm" id="update-sujet">Mettre à jour</a></span>
</div>
</div>
<script type="text/javascript">
$(function() {
$("#update-sujet").click(function() {
console.log("update !")
$("#sujet-content").load("{% url "notes:update-sujet" sujet.pk %}");
});
});
</script>

View File

@@ -0,0 +1,24 @@
{% load bootstrap3 %}
<form action="{% url "notes:update-sujet" form.instance.pk %}" method="post">{% csrf_token %}
<table class="table table-striped">
{% with "-" as none %}
<tr><th>Nom</th><th>Surnom</th><th>Prénom</th></tr>
<tr><td>{% bootstrap_field form.nom show_label=False %}</td><td>{% bootstrap_field form.surnom show_label=False %}</td><td>{% bootstrap_field form.prenom show_label=False %}</td></tr>
<tr><th>Âge</th><th>Genre</th><th>Première rencontre</th></tr>
<tr><td>{% bootstrap_field form.age show_label=False %}</td><td>{% bootstrap_field form.genre show_label=False %}</td><td>{% bootstrap_field form.premiere_rencontre show_label=False %}</td></tr>
{% endwith %}
</table>
<div class="panel-footer text-right">
<span class="text-right">{% bootstrap_button "Enregistrer" button_type="submit" %}
<span class="btn btn-primary btn-sm" id="cancel">Annuler</span></span>
</div>
</form>
<script type="text/javascript">
$(function() {
$("#cancel").click(function() {
console.log("cancelled !")
$("#sujet-content").load("{% url "notes:sujet" form.instance.pk %}");
});
});
</script>

View File

@@ -0,0 +1,12 @@
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="appelHeading">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#appelCollapse" aria-expanded="true" aria-controls="collapseOne">
<span class="glyphicon glyphicon-earphone"></span> Appel</a></h4>
</div>
<div id="appelCollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="appelHeading">
<div class="panel-body">
{% include "notes/form_appel_inner.html" %}</div>
</div>
</div>

View File

@@ -0,0 +1,17 @@
{% load bootstrap3 %}
<form action="" method="POST">{% csrf_token %}
{% with "inline" as layout %}
<div class="form-{{layout}} well well-sm text-center">
{% bootstrap_field form.created_date layout=layout %}
{% bootstrap_field form.created_time layout=layout %}
{% bootstrap_field form.entrant layout=layout %}
</div> {% endwith %}
{% with "horizontal" as layout %}
<div class="form-{{layout}}">
{% bootstrap_field form.sujet layout=layout %}
{% bootstrap_field form.text layout=layout %}
</div> {% endwith %}
<div class="pull-right">{% bootstrap_button "Enregistrer l'appel" button_type="submit" %}</div>
</form>
{{ form.media.js }}{{ form.media.css }}

View File

@@ -0,0 +1,12 @@
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="signalementHeading">
<h4 class="panel-title">
<a role="button" data-toggle="collapse" data-parent="#accordion" href="#signalementCollapse" aria-expanded="true" aria-controls="collapseOne">
<span class="glyphicon glyphicon-warning-sign"></span> Signalement</a></h4>
</div>
<div id="signalementCollapse" class="panel-collapse collapse" role="tabpanel" aria-labelledby="signalementHeading">
<div class="panel-body">
{% include "notes/form_signalement_inner.html" %}
</div>
</div>
</div>

View File

@@ -0,0 +1,27 @@
{% load bootstrap3 %}
<form action="" method="POST">{% csrf_token %}
{% bootstrap_form_errors form %}
{% with "inline" as layout %}
<div class="form-group form-{{layout}} well">
<label class="control-label col-md-2" for="id_source">
Source
</label>
{% bootstrap_field form.source layout=layout %}
{% bootstrap_field form.created_date layout=layout %}
{% bootstrap_field form.created_time layout=layout %}
</div> {% endwith %}
<div class="well">
{% with "horizontal" as layout %}
<div class="form-group form-{{layout}}">
{% bootstrap_field form.nom layout=layout %}
{% bootstrap_field form.prenom layout=layout %}
{% bootstrap_field form.genre layout=layout %}
{% bootstrap_field form.age layout=layout %}
</div> {% endwith %}
</div>
{% with "horizontal" as layout %}
<div class="form-group form-{{layout}}">
{% bootstrap_field form.text layout=layout %}
</div> {% endwith %}
<div class="pull-right">{% bootstrap_button "Enregistrer le signalement" button_type="submit" %}</div>
</form>

View File

@@ -0,0 +1,16 @@
{% extends "notes/base.html" %}
{% block title %}{{block.super}} Tableau de bord{% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li>Tableau de bord</li>
{% endblock %}
{% block page_content %}
<div class="col-md-12 col-lg-6">
<h2 class="page-header">TODO</h2>
<p>Liste des sujets qui ont été ajoutés depuis la dernière connexion</p>
<p>Liste des compte-rendus qui ont été ajoutés depuis la dernière connexion</p>
</div>
{% endblock %}

View File

@@ -0,0 +1,34 @@
{% extends "notes/base.html" %}
{% load tables %}
{% block sidebar %}
{{ block.super }}
{% block sidebar_insert %}{% endblock %}
{% if filters %}
<div class="well">
<h4 class="text-right"><span class="glyphicon glyphicon-filter"></span> <strong>Filtres</strong></h4>
<ul class="nav nav-pills nav-stacked text-right">
{% for filter in filters %}
<li role="presentation" {% if filter.active %} class="active" {% endif %}>
<a href="?filter={{filter.parameter_name}}">{{ filter.title }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endblock %}
{% block page_content %}
{% block search %}{% endblock %}
<!-- Table -->
{% table object_list cols=3 cell_template=table_cell_template header=table_header %}
{% if is_paginated %}
<div class="text-center">
<ul class="pagination">{% with request.GET.filter as filter %}
{% for num in page_obj.paginator.page_range %}
<li {% if page_obj.number == num %} class="active" {%endif%}><a href="?{% if filter %}filter={{ filter }}&{%endif%}page={{num}}">{{num}}</a></li>
{%endfor%}{% endwith %}
</ul>
</div>
{% endif %}
{% endblock %}

View File

@@ -0,0 +1,14 @@
{% extends "notes/liste.html" %}
{% block title %}
{{ block.super }} Liste des maraudes
{% endblock %}
{% block breadcrumbs %}
{{block.super}}
<li><a href="{% url "notes:liste-maraude" %}">Maraudes</a></li>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends "notes/liste.html" %}
{% block title %}{{block.super}} Liste des sujets {% endblock %}
{% block breadcrumbs %}
{{ block.super }}
<li><a href="{% url "notes:liste-sujet" %}">Sujets</a></li>
{% if query_text %}<li>'{{query_text}}'</li>{% endif %}
{% endblock %}
{% block sidebar_insert %}
<div class="panel panel-primary text-right">
<div class="panel-body">
<h4><strong>Rechercher</strong></h4>
<form action="{% url "notes:liste-sujet" %}" method="POST" class="form form-group">{% csrf_token %}
<div class="input-group">
<input type="text" name="q" class="form-control" placeholder="Chercher un sujet" aria-describedby="basic-addon1">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary"><span class=" glyphicon glyphicon-search"></span> </button>
</span>
</div>
</form>
<hr/>
<h4><strong>Outils</strong></h4>
<a class="btn btn-primary" href="{% url "notes:create-sujet" %}">
<span class="glyphicon glyphicon-plus"></span> Ajouter un sujet</a> </div></div>
{% endblock %}
{% block search %}
{% if query_text %}<div class="well well-sm text-center">
<h4><span class="label label-primary">'{{query_text}}'</span>
<span class="label label-danger">
{% if not object_list %}Aucun résultat
{% else %} {{ object_list.count }} résultats
{% endif %}
</span></h4>
</div>{% endif %}
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% load navbar %}
<ul class="nav nav-pills nav-stacked text-right">
<li role="presentation" {% active namespace="notes" viewname="liste-sujet" %}>
<a href="{% url "notes:liste-sujet" %}">Par sujet&nbsp;
<span class="glyphicon glyphicon-user"></span>
</a>
</li>
<li role="presentation" {% active namespace="notes" viewname="liste-maraude" %}>
<a href="{% url "notes:liste-maraude" %}">Par maraude&nbsp;
<span class="glyphicon glyphicon-road"></span>
</a>
</li>
</ul>

View File

@@ -0,0 +1,9 @@
{% extends "notes/base.html" %}
{% block page_content %}
<div class="row">
<div class="col-md-12">
{% include "notes/sujet_create_inner.html" %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% load bootstrap3 %}
<form class="form-horizontal" action="{% url "notes:create-sujet" %}" method="post">{% csrf_token %}
{% bootstrap_form form layout="horizontal"%}
<div class="pull-right">
{% bootstrap_button "Ajouter un sujet" button_type="submit" button_class="btn btn-primary" icon="plus" %}
</div>
{% if next %}<input type="text" hidden=True name="next" value="{{ next }}" />{%endif%}
</form>

View File

@@ -0,0 +1,12 @@
{% extends "notes/base.html" %}
{% block breadcrumbs %}
{{ block.super }}
<li><a href="{% url "notes:liste-sujet" %}">Sujets</a></li>
<li><a href="{% url "notes:details-sujet" object.pk %}">{{ object }}</a></li>
<li>Fusionner vers...</li>
{% endblock %}
{% block page_content %}
{% include 'notes/sujet_merge_inner.html' %}
{% endblock %}

View File

@@ -0,0 +1,8 @@
{% load bootstrap3 %}
<p> Vous allez fusionner la fiche de <strong>{{object}}</strong> et ses {{object.notes.count}} notes vers :</p>
<form action="{% url 'notes:sujets-merge' pk=object.pk %}" method="POST">
{% csrf_token %}
{{ form.media }}
{% bootstrap_field form.sujet %}
<div class="pull-right">{% bootstrap_button 'Fusionner' button_type='submit' icon='paste' %}</div>
</form>

View File

@@ -0,0 +1,6 @@
{% if object.est_terminee %}<a href="{% url 'notes:details-maraude' object.id %}" class="btn btn-link">
{% else %}<a href="#" class="btn btn-link disabled">{% endif %}{{ object }}</a>
<div class="pull-right">
<span class="label label-info">{{ object.binome }} & {{ object.referent }}</span>
{% if object.est_terminee %}<span class="label label-success">{{object.rencontres.count}} rencontres</span>{% endif %}
</div>

View File

@@ -0,0 +1,9 @@
<a href="{% url 'notes:details-sujet' object.pk %}" class="btn btn-link">{{object}}</a>
<div class="pull-right" style="padding-right: 20px;">
<span class="label label-info">{{ object.notes.count }} notes</span>
{% with object.statistiques.info_completed as completed %}
<span class="label label-{% if completed <= 80 %}warning{%else%}success{%endif%}">{{ completed }} %</span>
{% endwith %}
</div>

View File

@@ -5,6 +5,7 @@ register = template.Library()
@register.inclusion_tag("notes/table_inline.html")
def inline_table(note, header=None):
from maraudes.models import Maraude
bg_color, bg_label_color = note.bg_colors
if not header:
@@ -14,10 +15,14 @@ def inline_table(note, header=None):
if header == "date":
header_field = "created_date"
link = None
try:
maraude = Maraude.objects.get(date=note.created_date)
link = maraude.get_absolute_url()
except Maraude.DoesNotExist:
link = None
elif header == "sujet":
header_field = "sujet"
link = reverse("suivi:details", kwargs={'pk': note.sujet.pk})
link = note.sujet.get_absolute_url()
header = getattr(note, header_field)

View File

@@ -1,7 +1,28 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
from .models import Note
from .models import Note, Sujet
# Create your tests here.
# TODO: test 'actions.py'
class SujetModelTestCase(TestCase):
def setUp(self):
pass
def test_statistiques_is_autocreated(self):
new_sujet = Sujet.objects.create(prenom="Astérix")
self.assertIsNotNone(new_sujet.statistiques)
def test_at_least_one_in_name_surname_firstname(self):
self.assertIsInstance(Sujet.objects.create(nom="DeGaulle"), Sujet)
self.assertIsInstance(Sujet.objects.create(surnom="Le Gaulois"), Sujet)
self.assertIsInstance(Sujet.objects.create(prenom="Astérix"), Sujet)
def test_raises_validation_error_if_no_name(self):
with self.assertRaises(ValidationError):
Sujet.objects.create(age=25)
class NoteManagerTestCase(TestCase):
""" managers.NoteManager Test Case """

16
notes/urls.py Normal file
View File

@@ -0,0 +1,16 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name="index"),
url(r'sujets/$', views.SujetListView.as_view(), name="liste-sujet"),
url(r'sujets/(?P<pk>[0-9]+)/$', views.SuiviSujetView.as_view(), name="details-sujet"),
url(r'sujets/(?P<pk>[0-9]+)/merge/$', views.MergeView.as_view(), name="sujets-merge"),
url(r'maraudes/$', views.MaraudeListView.as_view(), name="liste-maraude"),
url(r'maraudes/(?P<pk>[0-9]+)/$', views.CompteRenduDetailsView.as_view(), name="details-maraude"),
# Manage Sujet
url(r'sujets/create/$', views.SujetCreateView.as_view(), name="create-sujet"),
url(r'sujet/(?P<pk>[0-9]+)/$', views.SujetAjaxDetailsView.as_view(), name="sujet"),
url(r'sujet/(?P<pk>[0-9]+)/update/$', views.SujetAjaxUpdateView.as_view(), name="update-sujet"),
]

254
notes/views.py Normal file
View File

@@ -0,0 +1,254 @@
import logging
import datetime
from django.shortcuts import redirect, reverse
from django.views import generic
from django.utils import timezone
from django.contrib import messages
from django.http.response import HttpResponseNotAllowed
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from utilisateurs.mixins import MaraudeurMixin
from maraudes.models import Maraude, CompteRendu
from .models import Sujet, Note
from .forms import SujetCreateForm, AutoNoteForm, SelectSujetForm
from .mixins import NoteFormMixin
from .actions import merge_two
logger = logging.getLogger(__name__)
# Create your views here.
class IndexView(MaraudeurMixin, generic.TemplateView):
template_name = "notes/index.html"
def get(self, *args, **kwargs):
return redirect("notes:liste-sujet")
class Filter:
def __init__(self, title, name, filter_func):
self.title = title
self.parameter_name = name
self.active = False
self._filter_func = filter_func
def filter(self, qs):
return self._filter_func(qs)
class ListView(MaraudeurMixin, generic.ListView):
""" Base ListView for Maraude and Sujet lists """
paginate_by = 30
cell_template = None
filters = []
active_filter = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._filters = {}
if self.filters:
for i, (title, func) in enumerate(self.filters):
_id = "filter_%i" % i
self._filters[_id] = Filter(title, _id, func)
def get(self, request, **kwargs):
filter_name = self.request.GET.get('filter', None)
if filter_name:
self.active_filter = self._filters.get(filter_name, None)
if self.active_filter:
self.active_filter.active = True
return super().get(request, **kwargs)
def get_queryset(self):
qs = super().get_queryset()
if self.active_filter:
qs = self.active_filter.filter(qs)
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filters"] = self._filters.values()
context["table_cell_template"] = getattr(self, 'cell_template', None)
context["table_header"] = getattr(self, 'table_header', None)
return context
class MaraudeListView(ListView):
""" Vue de la liste des compte-rendus de maraude """
model = CompteRendu
template_name = "notes/liste_maraudes.html"
cell_template = "notes/table_cell_maraudes.html"
table_header = "Liste des maraudes"
queryset = Maraude.objects.get_past().order_by("-date")
filters = [
("Ce mois-ci", lambda qs: qs.filter(date__month=timezone.now().date().month)),
]
class SujetListView(ListView):
#ListView
model = Sujet
template_name = "notes/liste_sujets.html"
cell_template = "notes/table_cell_sujets.html"
table_header = "Liste des sujets"
def info_completed_filter(qs):
excluded_set = set()
for sujet in qs:
if sujet.statistiques.info_completed >= 50:
excluded_set.add(sujet.pk)
return qs.exclude(pk__in=excluded_set)
filters = [
("Rencontré(e)s cette année", lambda qs: qs.filter(premiere_rencontre__year=timezone.now().date().year)),
("Statistiques incomplètes", info_completed_filter),
]
def post(self, request, **kwargs):
from watson import search as watson
search_text = request.POST.get('q')
results = watson.filter(Sujet, search_text)
#logger.warning("SEARCH for %s : %s" % (search_text, results))
if results.count() == 1:
return redirect(results[0].get_absolute_url())
self.queryset = results
return self.get(request, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['query_text'] = self.request.POST.get('q', None)
return context
class DetailView(MaraudeurMixin, generic.DetailView):
template_name = "notes/details.html"
class CompteRenduDetailsView(DetailView):
""" Vue détaillé d'un compte-rendu de maraude """
model = CompteRendu
context_object_name = "maraude"
template_name = "notes/details_maraude.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['notes'] = sorted(Note.objects.get_queryset().filter(created_date=self.object.date), key=lambda n: n.created_time)
context['next_maraude'] = Maraude.objects.get_future(
date=self.object.date + datetime.timedelta(1)
).filter(
heure_fin__isnull=False
).first()
context['prev_maraude'] = Maraude.objects.get_past(
date=self.object.date
).filter(
heure_fin__isnull=False
).last()
return context
class SuiviSujetView(NoteFormMixin, DetailView):
#NoteFormMixin
forms = {
'note': AutoNoteForm,
}
def get_success_url(self):
return reverse('notes:details-sujet', kwargs={'pk': self.get_object().pk})
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['sujet'] = self.get_object()
return kwargs
#DetailView
model = Sujet
template_name = "notes/details_sujet.html"
context_object_name = "sujet"
# Paginator
per_page = 5
def get(self, *args, **kwargs):
self.paginator = Paginator(
self.get_object().notes.by_date(reverse=True),
self.per_page
)
self.page = self.request.GET.get('page', 1)
return super().get(*args, **kwargs)
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
try:
notes = self.paginator.page(self.page)
except PageNotAnInteger:
notes = self.paginator.page(1)
except EmptyPage:
notes = self.paginator.page(self.paginator.num_pages)
context['notes'] = notes
return context
### Sujet Management Views
class SujetAjaxDetailsView(generic.DetailView):
#DetailView
template_name = "notes/details_sujet_inner.html"
model = Sujet
http_method_names = ["get"]
def get(self, *args, **kwargs):
""" Redirect to complete details view if request is not ajax """
if not self.request.is_ajax():
return redirect("notes:details-sujet", pk=self.get_object().pk)
return super().get(*args, **kwargs)
class SujetAjaxUpdateView(generic.edit.UpdateView):
#UpdateView
template_name = "notes/details_sujet_update.html"
model = Sujet
fields = '__all__'
def get_success_url(self):
return reverse("notes:details-sujet", kwargs={'pk': self.object.pk})
from website.mixins import AjaxTemplateMixin
class SujetCreateView(AjaxTemplateMixin, generic.edit.CreateView):
#CreateView
template_name = "notes/sujet_create.html"
form_class = SujetCreateForm
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
class MergeView(generic.DetailView, generic.FormView):
""" Implement actions.merge_two as a view """
template_name = "notes/sujet_merge.html"
model = Sujet
form_class = SelectSujetForm
def form_valid(self, form):
slave = self.get_object()
master = form.cleaned_data['sujet']
try:
merge_two(master, slave)
except Exception as e:
logger.error("Merge: ", e)
messages.error(self.request, "La fusion vers %s a échoué !" % master)
return redirect(slave)
messages.success(self.request, "%s vient d'être fusionné" % slave)
return redirect(master)