core: add family graphs

Signed-off-by: Skia <skia@libskia.so>
This commit is contained in:
Skia 2017-10-11 12:30:33 +02:00
parent 47bace2057
commit 7879b6dd6b
5 changed files with 221 additions and 30 deletions

View File

@ -6,6 +6,8 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<p><a href="{{ url("core:user_godfathers_tree_pict", user_id=profile.id) }}?family">
{% trans %}Show family picture{% endtrans %}</a></p>
{% if profile.godfathers.exists() %} {% if profile.godfathers.exists() %}
<h4>{% trans %}Godfathers{% endtrans %}</h4> <h4>{% trans %}Godfathers{% endtrans %}</h4>
<ul> <ul>
@ -14,6 +16,8 @@
{{ u.get_mini_item()|safe }} </a>{{ delete_godfather(user, profile, u, True) }}</li> {{ u.get_mini_item()|safe }} </a>{{ delete_godfather(user, profile, u, True) }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<p><a href="{{ url("core:user_godfathers_tree", user_id=profile.id) }}">
{% trans %}Show ancestors tree{% endtrans %}</a></p>
{% else %} {% else %}
<p>{% trans %}No godfathers{% endtrans %} <p>{% trans %}No godfathers{% endtrans %}
{% endif %} {% endif %}
@ -25,6 +29,8 @@
{{ u.get_mini_item()|safe }} </a>{{ delete_godfather(user, profile, u, False) }}</li> {{ u.get_mini_item()|safe }} </a>{{ delete_godfather(user, profile, u, False) }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<p><a href="{{ url("core:user_godfathers_tree", user_id=profile.id) }}?descent">
{% trans %}Show descent tree{% endtrans %}</a></p>
{% else %} {% else %}
<p>{% trans %}No godchildren{% endtrans %} <p>{% trans %}No godchildren{% endtrans %}
{% endif %} {% endif %}

View File

@ -0,0 +1,54 @@
{% extends "core/base.jinja" %}
{% block title %}
{% if param == "godchildren" %}
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s godchildren{% endtrans %}
{% else %}
{% trans user_name=profile.get_display_name() %}{{ user_name }}'s godfathers{% endtrans %}
{% endif %}
{% endblock %}
{% macro display_members_list(user) %}
{% if user.__getattribute__(param).exists() %}
<ul>
{% for u in user.__getattribute__(param).all() %}
<li>
<a href="{{ url("core:user_godfathers", user_id=u.id) }}">
{{ u.get_short_name() }}
</a>
{% if u in members_set %}
{% trans %}Already seen (check above){% endtrans %}
{% else %}
{{ members_set.add(u) or "" }}
{{ display_members_list(u) }}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
{% endmacro %}
{% block content %}
<p><a href="{{ url("core:user_godfathers", user_id=profile.id) }}">
{% trans %}Back to family{% endtrans %}</a></p>
{% if profile.__getattribute__(param).exists() %}
{% if param == "godchildren" %}
<p><a href="{{ url("core:user_godfathers_tree_pict", user_id=profile.id) }}?descent">
{% trans %}Show a picture of the tree{% endtrans %}</a></p>
<h4>{% trans u=profile.get_short_name() %}Descent tree of {{ u }}{% endtrans %}</h4>
{% else %}
<p><a href="{{ url("core:user_godfathers_tree_pict", user_id=profile.id) }}?ancestors">
{% trans %}Show a picture of the tree{% endtrans %}</a></p>
<h4>{% trans u=profile.get_short_name() %}Ancestors tree of {{ u }}{% endtrans %}</h4>
{% endif %}
{{ members_set.add(profile) or "" }}
{{ display_members_list(profile) }}
{% else %}
{% if param == "godchildren" %}
<p>{% trans %}No godchildren{% endtrans %}
{% else %}
<p>{% trans %}No godfathers{% endtrans %}
{% endif %}
{% endif %}
{% endblock %}

View File

@ -61,6 +61,8 @@ urlpatterns = [
url(r'^user/(?P<user_id>[0-9]+)/$', UserView.as_view(), name='user_profile'), url(r'^user/(?P<user_id>[0-9]+)/$', UserView.as_view(), name='user_profile'),
url(r'^user/(?P<user_id>[0-9]+)/pictures$', UserPicturesView.as_view(), name='user_pictures'), url(r'^user/(?P<user_id>[0-9]+)/pictures$', UserPicturesView.as_view(), name='user_pictures'),
url(r'^user/(?P<user_id>[0-9]+)/godfathers$', UserGodfathersView.as_view(), name='user_godfathers'), url(r'^user/(?P<user_id>[0-9]+)/godfathers$', UserGodfathersView.as_view(), name='user_godfathers'),
url(r'^user/(?P<user_id>[0-9]+)/godfathers/tree$', UserGodfathersTreeView.as_view(), name='user_godfathers_tree'),
url(r'^user/(?P<user_id>[0-9]+)/godfathers/tree/pict$', UserGodfathersTreePictureView.as_view(), name='user_godfathers_tree_pict'),
url(r'^user/(?P<user_id>[0-9]+)/godfathers/(?P<godfather_id>[0-9]+)/(?P<is_father>(True)|(False))/delete$', DeleteUserGodfathers, name='user_godfathers_delete'), url(r'^user/(?P<user_id>[0-9]+)/godfathers/(?P<godfather_id>[0-9]+)/(?P<is_father>(True)|(False))/delete$', DeleteUserGodfathers, name='user_godfathers_delete'),
url(r'^user/(?P<user_id>[0-9]+)/edit$', UserUpdateProfileView.as_view(), name='user_edit'), url(r'^user/(?P<user_id>[0-9]+)/edit$', UserUpdateProfileView.as_view(), name='user_edit'),
url(r'^user/(?P<user_id>[0-9]+)/profile_upload$', UserUploadProfilePictView.as_view(), name='user_profile_upload'), url(r'^user/(?P<user_id>[0-9]+)/profile_upload$', UserUploadProfilePictView.as_view(), name='user_profile_upload'),

View File

@ -28,7 +28,7 @@ from django.contrib.auth import views
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.http import Http404 from django.http import Http404, HttpResponse
from django.views.generic.edit import UpdateView from django.views.generic.edit import UpdateView
from django.views.generic import ListView, DetailView, TemplateView from django.views.generic import ListView, DetailView, TemplateView
from django.forms.models import modelform_factory from django.forms.models import modelform_factory
@ -233,20 +233,6 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
current_tab = 'infos' current_tab = 'infos'
def DeleteUserGodfathers(request, user_id, godfather_id, is_father):
user = User.objects.get(id=user_id)
if ((user == request.user) or
request.user.is_root or
request.user.is_board_member):
ud = get_object_or_404(User, id=godfather_id)
if is_father == "True":
user.godfathers.remove(ud)
else:
user.godchildren.remove(ud)
else:
raise PermissionDenied
return redirect('core:user_godfathers', user_id=user_id)
class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView): class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
""" """
@ -274,6 +260,20 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
kwargs['pictures'][album.id].append(pict_relation.picture) kwargs['pictures'][album.id].append(pict_relation.picture)
return kwargs return kwargs
def DeleteUserGodfathers(request, user_id, godfather_id, is_father):
user = User.objects.get(id=user_id)
if ((user == request.user) or
request.user.is_root or
request.user.is_board_member):
ud = get_object_or_404(User, id=godfather_id)
if is_father == "True":
user.godfathers.remove(ud)
else:
user.godchildren.remove(ud)
else:
raise PermissionDenied
return redirect('core:user_godfathers', user_id=user_id)
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView): class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
""" """
Display a user's godfathers Display a user's godfathers
@ -305,6 +305,91 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
kwargs['form'] = UserGodfathersForm() kwargs['form'] = UserGodfathersForm()
return kwargs return kwargs
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
"""
Display a user's family tree
"""
model = User
pk_url_kwarg = "user_id"
context_object_name = "profile"
template_name = "core/user_godfathers_tree.jinja"
current_tab = 'godfathers'
def get_context_data(self, **kwargs):
kwargs = super(UserGodfathersTreeView, self).get_context_data(**kwargs)
if "descent" in self.request.GET:
kwargs['param'] = "godchildren"
else:
kwargs['param'] = "godfathers"
kwargs['members_set'] = set()
return kwargs
class UserGodfathersTreePictureView(CanViewMixin, DetailView):
"""
Display a user's tree as a picture
"""
model = User
pk_url_kwarg = "user_id"
def build_complex_graph(self):
import pygraphviz as pgv
self.depth = int(self.request.GET.get("depth", 4))
if self.param == "godfathers":
self.graph = pgv.AGraph(strict=False, directed=True, rankdir="BT")
else:
self.graph = pgv.AGraph(strict=False, directed=True)
family = set()
self.level = 1
# Since the tree isn't very deep, we can build it recursively
def crawl_family(user):
if self.level > self.depth: return
self.level += 1
for u in user.__getattribute__(self.param).all():
self.graph.add_edge(user.get_short_name(), u.get_short_name())
if u not in family:
family.add(u)
crawl_family(u)
self.level -= 1
self.graph.add_node(self.object.get_short_name())
family.add(self.object)
crawl_family(self.object)
def build_family_graph(self):
import pygraphviz as pgv
self.graph = pgv.AGraph(strict=False, directed=True)
self.graph.add_node(self.object.get_short_name())
for u in self.object.godfathers.all():
self.graph.add_edge(u.get_short_name(), self.object.get_short_name())
for u in self.object.godchildren.all():
self.graph.add_edge(self.object.get_short_name(), u.get_short_name())
def get(self, request, *args, **kwargs):
self.object = self.get_object()
if "descent" in self.request.GET:
self.param = "godchildren"
elif "ancestors" in self.request.GET:
self.param = "godfathers"
else:
self.param = "family"
if self.param == "family":
self.build_family_graph()
else:
self.build_complex_graph()
# Pimp the graph before display
self.graph.node_attr['color'] = "lightblue"
self.graph.node_attr['style'] = "filled"
main_node = self.graph.get_node(self.object.get_short_name())
main_node.attr['color'] = "sandybrown"
main_node.attr['shape'] = "rect"
if self.param == "godchildren":
self.graph.graph_attr['label'] = _("Godchildren")
elif self.param == "godfathers":
self.graph.graph_attr['label'] = _("Godfathers")
else:
self.graph.graph_attr['label'] = _("Family")
img = self.graph.draw(format="png", prog="dot")
return HttpResponse(img, content_type="image/png")
class UserStatsView(UserTabsMixin, CanViewMixin, DetailView): class UserStatsView(UserTabsMixin, CanViewMixin, DetailView):
""" """

View File

@ -6,7 +6,7 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-09 16:19+0200\n" "POT-Creation-Date: 2017-10-11 12:22+0200\n"
"PO-Revision-Date: 2016-07-18\n" "PO-Revision-Date: 2016-07-18\n"
"Last-Translator: Skia <skia@libskia.so>\n" "Last-Translator: Skia <skia@libskia.so>\n"
"Language-Team: AE info <ae.info@utbm.fr>\n" "Language-Team: AE info <ae.info@utbm.fr>\n"
@ -745,9 +745,10 @@ msgid ""
"Warning: if you select <em>Account</em>, the opposite operation will be " "Warning: if you select <em>Account</em>, the opposite operation will be "
"created in the target account. If you don't want that, select <em>Club</em> " "created in the target account. If you don't want that, select <em>Club</em> "
"instead of <em>Account</em>." "instead of <em>Account</em>."
msgstr "Attention : si vous sélectionnez <em>Compte</em>, l'opération inverse sera " msgstr ""
"créée dans le compte cible. Si vous ne le voulez pas, sélectionnez <em>Club</em> " "Attention : si vous sélectionnez <em>Compte</em>, l'opération inverse sera "
"à la place de <em>Compte</em>." "créée dans le compte cible. Si vous ne le voulez pas, sélectionnez <em>Club</"
"em> à la place de <em>Compte</em>."
#: accounting/templates/accounting/operation_edit.jinja:46 #: accounting/templates/accounting/operation_edit.jinja:46
msgid "Linked operation:" msgid "Linked operation:"
@ -760,7 +761,7 @@ msgstr "Opération liée : "
#: core/templates/core/file_edit.jinja:8 #: core/templates/core/file_edit.jinja:8
#: core/templates/core/macros_pages.jinja:26 #: core/templates/core/macros_pages.jinja:26
#: core/templates/core/page_prop.jinja:11 #: core/templates/core/page_prop.jinja:11
#: core/templates/core/user_godfathers.jinja:35 #: core/templates/core/user_godfathers.jinja:41
#: core/templates/core/user_preferences.jinja:12 #: core/templates/core/user_preferences.jinja:12
#: core/templates/core/user_preferences.jinja:19 #: core/templates/core/user_preferences.jinja:19
#: counter/templates/counter/cash_register_summary.jinja:22 #: counter/templates/counter/cash_register_summary.jinja:22
@ -896,16 +897,12 @@ msgid "logo"
msgstr "logo" msgstr "logo"
#: club/models.py:62 #: club/models.py:62
#, fuzzy
#| msgid "active"
msgid "is active" msgid "is active"
msgstr "actif" msgstr "actif"
#: club/models.py:63 #: club/models.py:63
#, fuzzy
#| msgid "description"
msgid "short description" msgid "short description"
msgstr "description" msgstr "description courte"
#: club/models.py:64 core/models.py:199 #: club/models.py:64 core/models.py:199
msgid "address" msgid "address"
@ -2895,26 +2892,69 @@ msgid "Change user password"
msgstr "Changer le mot de passe" msgstr "Changer le mot de passe"
#: core/templates/core/user_godfathers.jinja:5 #: core/templates/core/user_godfathers.jinja:5
#: core/templates/core/user_godfathers_tree.jinja:7
#, python-format #, python-format
msgid "%(user_name)s's godfathers" msgid "%(user_name)s's godfathers"
msgstr "Parrains de %(user_name)s" msgstr "Parrains de %(user_name)s"
#: core/templates/core/user_godfathers.jinja:10 core/views/user.py:169 #: core/templates/core/user_godfathers.jinja:10
msgid "Show family picture"
msgstr "Voir une image de la famille"
#: core/templates/core/user_godfathers.jinja:12 core/views/user.py:169
#: core/views/user.py:388
msgid "Godfathers" msgid "Godfathers"
msgstr "Parrains" msgstr "Parrains"
#: core/templates/core/user_godfathers.jinja:18 #: core/templates/core/user_godfathers.jinja:20
msgid "Show ancestors tree"
msgstr "Voir l'arbre des ancêtres"
#: core/templates/core/user_godfathers.jinja:22
#: core/templates/core/user_godfathers_tree.jinja:50
msgid "No godfathers" msgid "No godfathers"
msgstr "Pas de parrains" msgstr "Pas de parrains"
#: core/templates/core/user_godfathers.jinja:21 #: core/templates/core/user_godfathers.jinja:25 core/views/user.py:386
msgid "Godchildren" msgid "Godchildren"
msgstr "Fillots" msgstr "Fillots"
#: core/templates/core/user_godfathers.jinja:29 #: core/templates/core/user_godfathers.jinja:33
msgid "Show descent tree"
msgstr "Voir l'arbre de la descendance"
#: core/templates/core/user_godfathers.jinja:35
#: core/templates/core/user_godfathers_tree.jinja:48
msgid "No godchildren" msgid "No godchildren"
msgstr "Pas de fillots" msgstr "Pas de fillots"
#: core/templates/core/user_godfathers_tree.jinja:5
msgid "%(user_name)s's godchildren"
msgstr "Fillots de %(user_name)s"
#: core/templates/core/user_godfathers_tree.jinja:20
msgid "Already seen (check above)"
msgstr "Déjà vu (voir plus haut)"
#: core/templates/core/user_godfathers_tree.jinja:33
msgid "Back to family"
msgstr "Retour à la famille"
#: core/templates/core/user_godfathers_tree.jinja:37
#: core/templates/core/user_godfathers_tree.jinja:41
msgid "Show a picture of the tree"
msgstr "Voir une image de l'arbre"
#: core/templates/core/user_godfathers_tree.jinja:38
#, python-format
msgid "Descent tree of %(u)s"
msgstr "Descendants de %(u)s"
#: core/templates/core/user_godfathers_tree.jinja:42
#, python-format
msgid "Ancestors tree of %(u)s"
msgstr "Ancêtres de %(u)s"
#: core/templates/core/user_group.jinja:4 #: core/templates/core/user_group.jinja:4
#, python-format #, python-format
msgid "Edit user groups for %(user_name)s" msgid "Edit user groups for %(user_name)s"
@ -3181,7 +3221,11 @@ msgstr "Fillot"
msgid "Pictures" msgid "Pictures"
msgstr "Photos" msgstr "Photos"
#: core/views/user.py:388 #: core/views/user.py:390
msgid "Family"
msgstr "Famille"
#: core/views/user.py:473
msgid "User already has a profile picture" msgid "User already has a profile picture"
msgstr "L'utilisateur a déjà une photo de profil" msgstr "L'utilisateur a déjà une photo de profil"