diff --git a/notes/actions.py b/notes/actions.py index 7d99369..1d44e4c 100644 --- a/notes/actions.py +++ b/notes/actions.py @@ -1,5 +1,4 @@ -from .models import Sujet -from statistiques.models import FicheStatistique, NSP +from statistiques.models import NSP def merge_stats(main, merged): @@ -21,17 +20,18 @@ def merge_stats(main, merged): 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 + 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 + 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 + merge_stats(main, merged) # Merge statistics and informations + for note in merged.notes.all(): # Move all notes note.sujet = main note.save() main.save() diff --git a/notes/apps.py b/notes/apps.py index a92cd8b..1941e64 100644 --- a/notes/apps.py +++ b/notes/apps.py @@ -1,9 +1,10 @@ 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')) + sujet_model = self.get_model("Sujet") + watson.register(sujet_model, fields=('nom', 'prenom', 'surnom')) diff --git a/notes/forms.py b/notes/forms.py index 494da59..f9b9890 100644 --- a/notes/forms.py +++ b/notes/forms.py @@ -6,8 +6,8 @@ from utilisateurs.models import Professionnel from django import forms from django_select2.forms import Select2Widget -### NOTES +# NOTES class NoteForm(forms.ModelForm): """ Generic Note form """ class Meta: @@ -15,11 +15,12 @@ class NoteForm(forms.ModelForm): fields = ['sujet', 'text', 'created_by', 'created_date', 'created_time'] widgets = { 'sujet': Select2Widget(), - 'text': forms.Textarea(attrs={'rows':4}), + 'text': forms.Textarea( + attrs={'rows': 4} + ), } - class SimpleNoteForm(forms.ModelForm): """ Simple note with only 'sujet' and 'text' fields. @@ -30,7 +31,6 @@ class SimpleNoteForm(forms.ModelForm): fields = ['sujet', 'text'] - class UserNoteForm(NoteForm): """ Form that sets 'created_by' with current user id. @@ -56,7 +56,6 @@ class UserNoteForm(NoteForm): return instance - class AutoNoteForm(UserNoteForm): class Meta(UserNoteForm.Meta): fields = ['text'] @@ -73,21 +72,21 @@ class AutoNoteForm(UserNoteForm): return inst - -### SUJETS - +# 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, - ), - } + widgets = {'premiere_rencontre': forms.SelectDateWidget( + empty_label=("Année", "Mois", "Jour"), + years=YEAR_CHOICE, + ), + } + class SelectSujetForm(forms.Form): diff --git a/notes/managers.py b/notes/managers.py index db76b63..02e512a 100644 --- a/notes/managers.py +++ b/notes/managers.py @@ -5,10 +5,7 @@ from django.db.models.query import QuerySet class NoteQuerySet(QuerySet): def _ordered_by_field(self, field, reverse=False): - return self.order_by( '%s%s' % ( "-" if reverse else "", - field - ) - ) + return self.order_by('%s%s' % ("-" if reverse else "", field)) def by_date(self, reverse=False): return self._ordered_by_field('created_date', reverse=reverse) @@ -16,6 +13,7 @@ class NoteQuerySet(QuerySet): def by_time(self, reverse=False): return self._ordered_by_field('created_time', reverse=reverse) + class NoteManager(Manager): def get_queryset(self): @@ -28,5 +26,3 @@ class NoteManager(Manager): def by_time(self, **kwargs): return self.get_queryset().by_time(**kwargs) - - diff --git a/notes/mixins.py b/notes/mixins.py index 678cf2d..81b83f5 100644 --- a/notes/mixins.py +++ b/notes/mixins.py @@ -1,34 +1,39 @@ from django.views.generic.edit import FormMixin from django.contrib import messages -from .forms import * class NoteFormMixin(FormMixin): + """ A mixin that allows to easily embed multiple distinct forms on a view. + Only one form can be processed by request ! + """ + # 'forms' shall be a dict of form classes, indexed by a prefix forms = None - def get_form(self, prefix): - kwargs = self.get_form_kwargs() - kwargs['prefix'] = prefix + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() kwargs['request'] = self.request - form_class = self.forms[prefix] - return form_class(**kwargs) + return kwargs + + def get_form_by_prefix(self, prefix): + try: + return self.get_form(form_class=self.forms[prefix]) + except KeyError: + raise ValueError("The form with prefix %s is not declared in %s" % (prefix, self.forms)) def post(self, request, **kwargs): + """ Save the first valid form found or reload page displaying a warning """ for prefix in self.forms.keys(): - form = self.get_form(prefix) + form = self.get_form_by_prefix(prefix) if form.is_valid(): form.save() + messages.success(self.request, "Un nouveau %s a été enregistré" % prefix) return self.form_valid(form) - return self.form_invalid(form) - - def form_valid(self, form): - messages.success(self.request, "Un nouveau %s a été enregistré" % form.prefix) - return super().form_valid(form) + messages.warning(request, "Il y a eu une erreur lors du traitement du formulaire") + return self.get(request, **kwargs) def get_context_data(self, **kwargs): context = super(FormMixin, self).get_context_data(**kwargs) for prefix in self.forms.keys(): - context['%s_form' % prefix] = self.get_form(prefix) + context['%s_form' % prefix] = self.get_form_by_prefix(prefix) return context - diff --git a/notes/models.py b/notes/models.py index 4035a61..01044e1 100644 --- a/notes/models.py +++ b/notes/models.py @@ -16,6 +16,7 @@ GENRE_CHOICES = ( (FEMME, 'Femme'), ) + class Sujet(models.Model): """ Personne faisant l'objet d'un suivi par la maraude """ @@ -38,9 +39,12 @@ class Sujet(models.Model): 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 + 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): @@ -65,6 +69,27 @@ class Sujet(models.Model): return reverse("notes:details-sujet", kwargs={"pk": self.pk}) +# Attributes used by 'notes' template tags +# bg_color : background color of header +# labels : list of strings to put in labels +def cached_attr(name): + """ Cached property set on return value of 'note_ATTR' method on + child instance. + """ + private_name = '_%s' % name + + def getter(self): + if not hasattr(self, private_name): + setattr(self, + private_name, + # Call *child instance* method + getattr(self.cast(), 'note_%s' % name)() + ) + return getattr(self, private_name) + + return getter + + class Note(models.Model): """ Note relative à un sujet. @@ -121,7 +146,7 @@ class Note(models.Model): """ Returns (header background color, labels color). Values must be valid bootstrap color name """ - return ("default", "info") + return "default", "info" def note_labels(self): """ Returns list of objects that are printed as bootstrap labels """ @@ -144,7 +169,6 @@ class Note(models.Model): self._child_class = self._child_instance.__class__ return - @property def type_name(self): return self.child_class.__qualname__ @@ -160,25 +184,9 @@ class Note(models.Model): self._get_child_class_and_instance() return self._child_instance - ## Attributes used by 'notes' template tags - # bg_color : background color of header - # labels : list of strings to put in labels - def cached_attr(name): - """ Cached property set on return value of 'note_ATTR' method on - child instance. - """ - private_name = '_%s' % name - def getter(self): - if not hasattr(self, private_name): - setattr(self, - private_name, - # Call *child instance* method - getattr(self.cast(), 'note_%s' % name)() - ) - return getattr(self, private_name) - return getter + # Define specials properties bg_colors = property(cached_attr('bg_colors'), doc="background color of header") labels = property(cached_attr('labels'), doc="list of string to display as labels") def get_absolute_url(self): - return reverse("notes:details-sujet", kwargs={"pk": self.sujet.pk}) \ No newline at end of file + return reverse("notes:details-sujet", kwargs={"pk": self.sujet.pk}) diff --git a/notes/templates/notes/table_inline.html b/notes/templates/notes/table_inline.html index e0d9711..2e9af99 100644 --- a/notes/templates/notes/table_inline.html +++ b/notes/templates/notes/table_inline.html @@ -1,16 +1,14 @@ - + {% if link %} {{header}} {% else %} {{header}} {% endif %} {{small}} - +
{% for label in labels %} - {{label}} - {% endfor %}
+ {{label}}
+ {% endfor %} - -

{{text | linebreaks }}

diff --git a/notes/templatetags/notes.py b/notes/templatetags/notes.py index c8c55d2..faece84 100644 --- a/notes/templatetags/notes.py +++ b/notes/templatetags/notes.py @@ -1,8 +1,8 @@ from django import template -from django.urls import reverse register = template.Library() + @register.inclusion_tag("notes/table_inline.html") def inline_table(note, header=None): from maraudes.models import Maraude @@ -10,7 +10,7 @@ def inline_table(note, header=None): if not header: header = "date" - if not header in ['sujet', 'date']: + if header not in ['sujet', 'date']: raise ValueError('header must be "sujet" or "date"') if header == "date": @@ -24,10 +24,8 @@ def inline_table(note, header=None): header_field = "sujet" link = note.sujet.get_absolute_url() - header = getattr(note, header_field) - return { - 'header': header, + 'header': getattr(note, header_field), 'link': link, 'small': note.child_class.__qualname__, 'bg_color': bg_color or "default", diff --git a/notes/tests.py b/notes/tests.py index 55ea0c2..dc8499e 100644 --- a/notes/tests.py +++ b/notes/tests.py @@ -24,6 +24,7 @@ class SujetModelTestCase(TestCase): with self.assertRaises(ValidationError): Sujet.objects.create(age=25) + class NoteManagerTestCase(TestCase): """ managers.NoteManager Test Case """ @@ -32,8 +33,11 @@ class NoteManagerTestCase(TestCase): self.note_manager = NoteManager def test_note_default_manager_is_NoteManager(self): - self.assertIsInstance(Note.objects, self.note_manager, - msg="%s is not Note default manager" % self.note_manager) + self.assertIsInstance( + Note.objects, + self.note_manager, + msg="%s is not Note default manager" % self.note_manager + ) def test_by_date(self): prev_note = None @@ -41,8 +45,11 @@ class NoteManagerTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertLessEqual(prev_note.created_date, note.created_date, - msg="%s is not same date or prior to %s" % (prev_note, note)) + self.assertLessEqual( + prev_note.created_date, + note.created_date, + msg="%s is not same date or prior to %s" % (prev_note, note) + ) prev_note = note def test_by_date_reversed(self): @@ -51,8 +58,11 @@ class NoteManagerTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertGreaterEqual(prev_note.created_date, note.created_date, - msg="%s is not same date or later to %s" % (prev_note, note)) + self.assertGreaterEqual( + prev_note.created_date, + note.created_date, + msg="%s is not same date or later to %s" % (prev_note, note) + ) prev_note = note def test_by_time(self): @@ -61,8 +71,11 @@ class NoteManagerTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertLessEqual(prev_note.created_time, note.created_time, - msg="%s is not same time or prior to %s" % (prev_note, note)) + self.assertLessEqual( + prev_note.created_time, + note.created_time, + msg="%s is not same time or prior to %s" % (prev_note, note) + ) prev_note = note def test_by_time_reversed(self): @@ -71,10 +84,13 @@ class NoteManagerTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertLessEqual(prev_note.created_time, note.created_time, - msg="%s is not same time or later to %s" % (prev_note, note)) + self.assertLessEqual( + prev_note.created_time, + note.created_time, + msg="%s is not same time or later to %s" % (prev_note, note)) prev_note = note + class NoteQuerySetTestCase(TestCase): def setUp(self): @@ -86,7 +102,9 @@ class NoteQuerySetTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertLessEqual(prev_note.created_date, note.created_date, + self.assertLessEqual( + prev_note.created_date, + note.created_date, msg="%s is not same date or prior to %s" % (prev_note, note)) prev_note = note @@ -96,8 +114,11 @@ class NoteQuerySetTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertGreaterEqual(prev_note.created_date, note.created_date, - msg="%s is not same date or later to %s" % (prev_note, note)) + self.assertGreaterEqual( + prev_note.created_date, + note.created_date, + msg="%s is not same date or later to %s" % (prev_note, note) + ) prev_note = note def test_by_time(self): @@ -106,8 +127,11 @@ class NoteQuerySetTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertLessEqual(prev_note.created_time, note.created_time, - msg="%s is not same time or prior to %s" % (prev_note, note)) + self.assertLessEqual( + prev_note.created_time, + note.created_time, + msg="%s is not same time or prior to %s" % (prev_note, note) + ) prev_note = note def test_by_time_reversed(self): @@ -116,6 +140,9 @@ class NoteQuerySetTestCase(TestCase): if not prev_note: prev_note = note else: - self.assertLessEqual(prev_note.created_time, note.created_time, - msg="%s is not same time or later to %s" % (prev_note, note)) + self.assertLessEqual( + prev_note.created_time, + note.created_time, + msg="%s is not same time or later to %s" % (prev_note, note) + ) prev_note = note diff --git a/notes/views.py b/notes/views.py index 7eb7350..bcce6ca 100644 --- a/notes/views.py +++ b/notes/views.py @@ -1,13 +1,12 @@ 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 watson import search as watson +from website.mixins import AjaxTemplateMixin from utilisateurs.mixins import MaraudeurMixin from maraudes.models import Maraude, CompteRendu from maraudes.notes import Observation, Signalement @@ -20,13 +19,13 @@ 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 @@ -38,7 +37,6 @@ class Filter: return self._filter_func(qs) - class ListView(MaraudeurMixin, generic.ListView): """ Base ListView for Maraude and Sujet lists """ paginate_by = 30 @@ -47,8 +45,8 @@ class ListView(MaraudeurMixin, generic.ListView): filters = [] active_filter = None - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self, **kwargs): + super().__init__(**kwargs) self._filters = {} if self.filters: @@ -95,38 +93,37 @@ class MaraudeListView(ListView): class SujetListView(ListView): - #ListView + # ListView model = Sujet template_name = "notes/liste_sujets.html" cell_template = "notes/table_cell_sujets.html" table_header = "Liste des sujets" - + @staticmethod def info_completed_filter(qs): - COMPLETED_RATIO = 70 # % of total fields completed + completed_ratio = 70 # % of total fields completed excluded_set = set() for sujet in qs: - if sujet.statistiques.info_completed >= COMPLETED_RATIO: + if sujet.statistiques.info_completed >= completed_ratio: excluded_set.add(sujet.pk) return qs.exclude(pk__in=excluded_set) + @staticmethod def rencontre_dans_le_mois(qs): """ Renvoie les sujets du queryset pour lesquelles une observation a été enregistrée au cours des 30 derniers jours """ - DAYS_NUMBER = 30 - LIMIT_DATE = timezone.now().date() - datetime.timedelta(DAYS_NUMBER) - + days_number = 30 + limit_date = timezone.now().date() - datetime.timedelta(days_number) included_set = set() for sujet in qs: # Try to find an observation in the range most_recent_obs = Observation.objects.filter(sujet=sujet).order_by("-created_date").first() - if most_recent_obs and most_recent_obs.created_date >= LIMIT_DATE: + if most_recent_obs and most_recent_obs.created_date >= limit_date: included_set.add(sujet.pk) return qs.filter(pk__in=included_set) - filters = [ ("Connu(e)s cette année", lambda qs: qs.filter( @@ -138,10 +135,9 @@ class SujetListView(ListView): ] 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)) + # logger.warning("SEARCH for %s : %s" % (search_text, results)) if results.count() == 1: return redirect(results[0].get_absolute_url()) self.queryset = results @@ -156,6 +152,7 @@ class SujetListView(ListView): class DetailView(MaraudeurMixin, generic.DetailView): template_name = "notes/details.html" + class CompteRenduDetailsView(DetailView): """ Vue détaillé d'un compte-rendu de maraude """ @@ -165,32 +162,38 @@ class CompteRenduDetailsView(DetailView): 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['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() + 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() + date=self.object.date + ).filter( + heure_fin__isnull=False + ).last() return context class SuiviSujetView(NoteFormMixin, DetailView): - #NoteFormMixin + # 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 + + # DetailView model = Sujet template_name = "notes/details_sujet.html" context_object_name = "sujet" @@ -205,8 +208,8 @@ class SuiviSujetView(NoteFormMixin, DetailView): 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) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) try: notes = self.paginator.page(self.page) except PageNotAnInteger: @@ -217,10 +220,9 @@ class SuiviSujetView(NoteFormMixin, DetailView): return context -### Sujet Management Views - +# Sujet Management Views class SujetAjaxDetailsView(generic.DetailView): - #DetailView + # DetailView template_name = "notes/details_sujet_inner.html" model = Sujet @@ -232,8 +234,9 @@ class SujetAjaxDetailsView(generic.DetailView): return redirect("notes:details-sujet", pk=self.get_object().pk) return super().get(*args, **kwargs) + class SujetAjaxUpdateView(generic.edit.UpdateView): - #UpdateView + """ View for 'sujet' updates, can be retrieved by ajax requests. """ template_name = "notes/details_sujet_update.html" model = Sujet fields = '__all__' @@ -241,24 +244,28 @@ class SujetAjaxUpdateView(generic.edit.UpdateView): 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 + """ View for 'sujet' creation, can be retrieved by ajax requests. """ 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 + 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 """ + """ Merge two 'sujet' objects by implementing actions.merge_two as a view """ template_name = "notes/sujet_merge.html" model = Sujet