mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-09 19:40:19 +00:00
Graph de famille en frontend (#820)
* Remove graphviz and use cytoscape.js instead * Frontend generated graphs * Make installation easier and faster * Better user experience * Family api and improved interface * Fix url history when using 0, improve button selection and reset reverse with reset button * Use klay layout * Add js translations and apply review comments
This commit is contained in:
committed by
GitHub
parent
bf96d8a10c
commit
f624b7c66d
@ -335,9 +335,40 @@ class UserGodfathersForm(forms.Form):
|
||||
label=_("Add"),
|
||||
)
|
||||
user = AutoCompleteSelectField(
|
||||
"users", required=True, label=_("Select user"), help_text=None
|
||||
"users", required=True, label=_("Select user"), help_text=""
|
||||
)
|
||||
|
||||
def __init__(self, *args, user: User, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.target_user = user
|
||||
|
||||
def clean_user(self):
|
||||
other_user = self.cleaned_data.get("user")
|
||||
if not other_user:
|
||||
raise ValidationError(_("This user does not exist"))
|
||||
if other_user == self.target_user:
|
||||
raise ValidationError(_("You cannot be related to yourself"))
|
||||
return other_user
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
if not self.is_valid():
|
||||
return self.cleaned_data
|
||||
other_user = self.cleaned_data["user"]
|
||||
if self.cleaned_data["type"] == "godfather":
|
||||
if self.target_user.godfathers.contains(other_user):
|
||||
self.add_error(
|
||||
"user",
|
||||
_("%s is already your godfather") % (other_user.get_short_name()),
|
||||
)
|
||||
else:
|
||||
if self.target_user.godchildren.contains(other_user):
|
||||
self.add_error(
|
||||
"user",
|
||||
_("%s is already your godchild") % (other_user.get_short_name()),
|
||||
)
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class PagePropForm(forms.ModelForm):
|
||||
error_css_class = "error"
|
||||
|
@ -34,7 +34,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.forms.models import modelform_factory
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.response import TemplateResponse
|
||||
@ -323,7 +323,7 @@ def delete_user_godfather(request, user_id, godfather_id, is_father):
|
||||
return redirect("core:user_godfathers", user_id=user_id)
|
||||
|
||||
|
||||
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView, FormView):
|
||||
"""Display a user's godfathers."""
|
||||
|
||||
model = User
|
||||
@ -331,27 +331,23 @@ class UserGodfathersView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
context_object_name = "profile"
|
||||
template_name = "core/user_godfathers.jinja"
|
||||
current_tab = "godfathers"
|
||||
form_class = UserGodfathersForm
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.form = UserGodfathersForm(request.POST)
|
||||
if self.form.is_valid() and self.form.cleaned_data["user"] != self.object:
|
||||
if self.form.cleaned_data["type"] == "godfather":
|
||||
self.object.godfathers.add(self.form.cleaned_data["user"])
|
||||
self.object.save()
|
||||
else:
|
||||
self.object.godchildren.add(self.form.cleaned_data["user"])
|
||||
self.object.save()
|
||||
self.form = UserGodfathersForm()
|
||||
return super().get(request, *args, **kwargs)
|
||||
def get_form_kwargs(self):
|
||||
return super().get_form_kwargs() | {"user": self.object}
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.cleaned_data["type"] == "godfather":
|
||||
self.object.godfathers.add(form.cleaned_data["user"])
|
||||
else:
|
||||
self.object.godchildren.add(form.cleaned_data["user"])
|
||||
return redirect("core:user_godfathers", user_id=self.object.id)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
try:
|
||||
kwargs["form"] = self.form
|
||||
except:
|
||||
kwargs["form"] = UserGodfathersForm()
|
||||
return kwargs
|
||||
return super().get_context_data(**kwargs) | {
|
||||
"godfathers": list(self.object.godfathers.select_related("profile_pict")),
|
||||
"godchildren": list(self.object.godchildren.select_related("profile_pict")),
|
||||
}
|
||||
|
||||
|
||||
class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
@ -365,86 +361,12 @@ class UserGodfathersTreeView(UserTabsMixin, CanViewMixin, DetailView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
if "descent" in self.request.GET:
|
||||
kwargs["param"] = "godchildren"
|
||||
else:
|
||||
kwargs["param"] = "godfathers"
|
||||
kwargs["members_set"] = set()
|
||||
kwargs["api_url"] = reverse(
|
||||
"api:family_graph", kwargs={"user_id": self.object.id}
|
||||
)
|
||||
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"] = _("Family")
|
||||
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):
|
||||
"""Display a user's stats."""
|
||||
|
||||
|
Reference in New Issue
Block a user