diff --git a/core/templates/core/user_godfathers.jinja b/core/templates/core/user_godfathers.jinja
index 80331c5e..fa2bbca6 100644
--- a/core/templates/core/user_godfathers.jinja
+++ b/core/templates/core/user_godfathers.jinja
@@ -6,6 +6,8 @@
{% endblock %}
{% block content %}
+
+ {% trans %}Show family picture{% endtrans %}
{% if profile.godfathers.exists() %}
{% trans %}Godfathers{% endtrans %}
@@ -14,6 +16,8 @@
{{ u.get_mini_item()|safe }} {{ delete_godfather(user, profile, u, True) }}
{% endfor %}
+
+ {% trans %}Show ancestors tree{% endtrans %}
{% else %}
{% trans %}No godfathers{% endtrans %}
{% endif %}
@@ -25,6 +29,8 @@
{{ u.get_mini_item()|safe }} {{ delete_godfather(user, profile, u, False) }}
{% endfor %}
+
+ {% trans %}Show descent tree{% endtrans %}
{% else %}
{% trans %}No godchildren{% endtrans %}
{% endif %}
diff --git a/core/templates/core/user_godfathers_tree.jinja b/core/templates/core/user_godfathers_tree.jinja
new file mode 100644
index 00000000..9c1791f4
--- /dev/null
+++ b/core/templates/core/user_godfathers_tree.jinja
@@ -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() %}
+
+ {% for u in user.__getattribute__(param).all() %}
+ -
+
+ {{ u.get_short_name() }}
+
+ {% if u in members_set %}
+ {% trans %}Already seen (check above){% endtrans %}
+ {% else %}
+ {{ members_set.add(u) or "" }}
+ {{ display_members_list(u) }}
+ {% endif %}
+
+ {% endfor %}
+
+{% endif %}
+{% endmacro %}
+
+{% block content %}
+
+ {% trans %}Back to family{% endtrans %}
+ {% if profile.__getattribute__(param).exists() %}
+ {% if param == "godchildren" %}
+
+ {% trans %}Show a picture of the tree{% endtrans %}
+ {% trans u=profile.get_short_name() %}Descent tree of {{ u }}{% endtrans %}
+ {% else %}
+
+ {% trans %}Show a picture of the tree{% endtrans %}
+ {% trans u=profile.get_short_name() %}Ancestors tree of {{ u }}{% endtrans %}
+ {% endif %}
+ {{ members_set.add(profile) or "" }}
+ {{ display_members_list(profile) }}
+ {% else %}
+ {% if param == "godchildren" %}
+ {% trans %}No godchildren{% endtrans %}
+ {% else %}
+
{% trans %}No godfathers{% endtrans %}
+ {% endif %}
+ {% endif %}
+{% endblock %}
+
diff --git a/core/urls.py b/core/urls.py
index a056b4b7..216ec064 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -61,6 +61,8 @@ urlpatterns = [
url(r'^user/(?P[0-9]+)/$', UserView.as_view(), name='user_profile'),
url(r'^user/(?P[0-9]+)/pictures$', UserPicturesView.as_view(), name='user_pictures'),
url(r'^user/(?P[0-9]+)/godfathers$', UserGodfathersView.as_view(), name='user_godfathers'),
+ url(r'^user/(?P[0-9]+)/godfathers/tree$', UserGodfathersTreeView.as_view(), name='user_godfathers_tree'),
+ url(r'^user/(?P[0-9]+)/godfathers/tree/pict$', UserGodfathersTreePictureView.as_view(), name='user_godfathers_tree_pict'),
url(r'^user/(?P[0-9]+)/godfathers/(?P[0-9]+)/(?P(True)|(False))/delete$', DeleteUserGodfathers, name='user_godfathers_delete'),
url(r'^user/(?P[0-9]+)/edit$', UserUpdateProfileView.as_view(), name='user_edit'),
url(r'^user/(?P[0-9]+)/profile_upload$', UserUploadProfilePictView.as_view(), name='user_profile_upload'),
diff --git a/core/views/user.py b/core/views/user.py
index 6bbbccef..25f7a7d5 100644
--- a/core/views/user.py
+++ b/core/views/user.py
@@ -28,7 +28,7 @@ from django.contrib.auth import views
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
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 import ListView, DetailView, TemplateView
from django.forms.models import modelform_factory
@@ -233,20 +233,6 @@ class UserView(UserTabsMixin, CanViewMixin, DetailView):
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):
"""
@@ -274,6 +260,20 @@ class UserPicturesView(UserTabsMixin, CanViewMixin, DetailView):
kwargs['pictures'][album.id].append(pict_relation.picture)
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):
"""
Display a user's godfathers
@@ -305,6 +305,91 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
kwargs['form'] = UserGodfathersForm()
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):
"""
diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po
index fb2af5e0..3378a0f8 100644
--- a/locale/fr/LC_MESSAGES/django.po
+++ b/locale/fr/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@
msgid ""
msgstr ""
"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"
"Last-Translator: Skia \n"
"Language-Team: AE info \n"
@@ -745,9 +745,10 @@ msgid ""
"Warning: if you select Account, the opposite operation will be "
"created in the target account. If you don't want that, select Club "
"instead of Account."
-msgstr "Attention : si vous sélectionnez Compte, l'opération inverse sera "
-"créée dans le compte cible. Si vous ne le voulez pas, sélectionnez Club "
-"à la place de Compte."
+msgstr ""
+"Attention : si vous sélectionnez Compte, l'opération inverse sera "
+"créée dans le compte cible. Si vous ne le voulez pas, sélectionnez Club"
+"em> à la place de Compte."
#: accounting/templates/accounting/operation_edit.jinja:46
msgid "Linked operation:"
@@ -760,7 +761,7 @@ msgstr "Opération liée : "
#: core/templates/core/file_edit.jinja:8
#: core/templates/core/macros_pages.jinja:26
#: 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:19
#: counter/templates/counter/cash_register_summary.jinja:22
@@ -896,16 +897,12 @@ msgid "logo"
msgstr "logo"
#: club/models.py:62
-#, fuzzy
-#| msgid "active"
msgid "is active"
msgstr "actif"
#: club/models.py:63
-#, fuzzy
-#| msgid "description"
msgid "short description"
-msgstr "description"
+msgstr "description courte"
#: club/models.py:64 core/models.py:199
msgid "address"
@@ -2895,26 +2892,69 @@ msgid "Change user password"
msgstr "Changer le mot de passe"
#: core/templates/core/user_godfathers.jinja:5
+#: core/templates/core/user_godfathers_tree.jinja:7
#, python-format
msgid "%(user_name)s's godfathers"
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"
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"
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"
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"
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
#, python-format
msgid "Edit user groups for %(user_name)s"
@@ -3181,7 +3221,11 @@ msgstr "Fillot"
msgid "Pictures"
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"
msgstr "L'utilisateur a déjà une photo de profil"