Compare commits

...

4 Commits

Author SHA1 Message Date
Gregory Trullemans d6c3df1c3b Adding rebuil_tree command, breadcrumb and PrerequisiteClosure model/table
continuous-integration/drone/push Build is failing Details
2022-01-12 21:03:43 +01:00
Gregory Trullemans 83245b1216 Updating skill tree view and template. 2022-01-12 14:48:59 +01:00
Gregory Trullemans 706343cc6a Adding html template for skill tree and update base.html 2022-01-12 14:42:07 +01:00
Gregory Trullemans 96f52f1cd4 Adding D3 DAG JS libraries. 2022-01-12 14:39:15 +01:00
9 changed files with 333 additions and 15 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

2
static/js/plugins/D3-dag/d3.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -46,6 +46,8 @@
<!-- Chart JS -->
<script src="{% static "js/plugins/moment_2.29.1.min.js" %}"></script>
<script src="{% static "js/plugins/chartjs_2.9.4.min.js" %}"></script>
{% block header %}{% endblock %}
</head>
<body class="sidebar-mini {% if request.session.template == 1 %}white-content{% endif %}">

View File

@ -0,0 +1,179 @@
{% extends "base.html" %}
{% block header %}
<style>
svg {
width: 50%;
height: 50%;
}
</style>
{% endblock %}
{% block content %}
<div class="card mb-0">
<svg></svg>
</div>
{% endblock %}
{% block footerscript %}
<script src="{% static "js/plugins/D3-dag/d3.min.js%}"></script>
<script src="{% static "js/plugins/D3-dag/d3-dag.0.8.2.min.js%}"></script>
<script>
(async () => {
// fetch data and render
// const resp = await fetch(
// "https://raw.githubusercontent.com/erikbrinkman/d3-dag/main/examples/grafo.json"
// );
const data = [{
// Rudy
"id": "43/",
"parentIds": ["-2", ".41/"]
},
{
// vrille
"id": "-2",
"parentIds": ["-1"]
},
{
// Barani
"id": ".41/",
"parentIds": ["-1", ".3-/"]
},
{
// 1/2 vrille
"id": "-1",
"parentIds": ["|"]
},
{
// Chandelle
"id": "|",
"parentIds": []
},
{
// 3/4 Avant Tendu
"id": ".3-/",
"parentIds": [".1"]
},
{
// Ventre
"id": ".1",
"parentIds": ["4p"]
},
{
// 4 pattes
"id": "4p",
"parentIds": []
}];
// const data = await resp.json();
const dag = d3.dagStratify()(data);
const nodeRadius = 20;
const layout = d3
//
// Base layout
.sugiyama()
//
// Layering
.layering(d3.layeringSimplex()) // Simplex (shortest edges)
// .layering(d3.layeringLongestPath()) // Longest Path (minimum height)
// .layering(d3.layeringCoffmanGraham()) // Coffman Graham (constrained width)
//
// Decrossing
.decross(d3.decrossOpt()) // Optimal (can be very slow)
// .decross(d3.decrossTwoLayer().order(d3.twolayerAgg())) // Two Layer Agg (fast)
// .decross(d3.decrossTwoLayer().order(d3.twolayerOpt())) // Two Layer Opt (can be very slow)
//
// Coords
// .coord(d3.coordCenter()) // Center (fast)
.coord(d3.coordGreedy()) // Greedy (fast)
// .coord(d3.coordQuad()) // Quadradtic (can be slow)
// .coord(d3.coordTopological()) //
.nodeSize(
(node) => [(node ? 3.6 : 0.25) * nodeRadius, 3 * nodeRadius]
); // set node size instead of constraining to fit
// .nodeSize((node) => {
// const size = node ? base : 5;
// return [1.2 * size, size];
// });
const { width, height } = layout(dag);
// --------------------------------
// This code only handles rendering
// --------------------------------
const svgSelection = d3.select("svg");
svgSelection.attr("viewBox", [0, 0, width, height].join(" "));
const defs = svgSelection.append("defs"); // For gradients
const steps = dag.size();
const interp = d3.interpolateRainbow;
const colorMap = new Map();
for (const [i, node] of dag.idescendants().entries()) {
colorMap.set(node.data.id, interp(i / steps));
}
// How to draw edges
const line = d3
.line()
.curve(d3.curveCatmullRom)
.x((d) => d.x)
.y((d) => d.y);
// Plot edges
svgSelection
.append("g")
.selectAll("path")
.data(dag.links())
.enter()
.append("path")
.attr("d", ({ points }) => line(points))
.attr("fill", "none")
.attr("stroke-width", 3)
.attr("stroke", ({ source, target }) => {
// encodeURIComponents for spaces, hope id doesn't have a `--` in it
const gradId = encodeURIComponent(`${source.data.id}--${target.data.id}`);
const grad = defs
.append("linearGradient")
.attr("id", gradId)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", source.x)
.attr("x2", target.x)
.attr("y1", source.y)
.attr("y2", target.y);
grad
.append("stop")
.attr("offset", "0%")
.attr("stop-color", colorMap.get(source.data.id));
grad
.append("stop")
.attr("offset", "100%")
.attr("stop-color", colorMap.get(target.data.id));
return `url(#${gradId})`;
});
// Select nodes
const nodes = svgSelection
.append("g")
.selectAll("g")
.data(dag.descendants())
.enter()
.append("g")
.attr("transform", ({ x, y }) => `translate(${x}, ${y})`);
// Plot node circles
nodes
.append("circle")
.attr("r", nodeRadius)
.attr("fill", (n) => colorMap.get(n.data.id));
// Add text to nodes
nodes
.append("text")
.text((d) => d.data.id)
.attr("font-weight", "bold")
.attr("font-family", "sans-serif")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("fill", "white");
})();
</script>
{% endblock %}

View File

@ -0,0 +1,44 @@
"""This command manages Closure Tables implementation
It adds new levels and cleans links between Educatives.
This way, it's relatively easy to fetch an entire tree with just one tiny request.
"""
from django.core.management.base import BaseCommand
from ultron.objective.models import Educative, PrerequisiteClosure
class Command(BaseCommand):
def handle(self, *args, **options):
# educative_list = Educative.objects.all()
educative_list = [Educative.objects.get(pk=44)]
for educative in educative_list:
# print('__________________________________________________________________________')
# print('Traitement de ' + str(educative))
# breadcrumb = [node for node in educative.breadcrumb()]
breadcrumb = educative.breadcrumb()
for path in range(0, len(breadcrumb)):
# print(' ˪ ' + str(path + 1) + 'ème chemin')
tree = set(PrerequisiteClosure.objects.filter(descendant=educative, path=path))
# print(' ' + str(tree))
for position, ancestor in enumerate(breadcrumb[path]):
# print(' ˪ Traitement de ' + str(ancestor.long_label) + ' : ' + str(ancestor.long_label) + ' -> ' + str(educative.long_label) + ' | ' + str(position) + ' | ' + str(path))
tree_path, _ = PrerequisiteClosure.objects.get_or_create(
ancestor=ancestor, descendant=educative, level=position, path=path
)
# if _:
# print(' -> CREATION : ' + str(tree_path))
# else:
# print(' -> RECUPERATION : ' + str(tree_path))
if tree_path in tree:
tree.remove(tree_path)
for tree_path in tree:
# print(' DELETE : ' + str(tree_path))
tree_path.delete()

View File

@ -78,6 +78,55 @@ class Educative(Markdownizable):
self.level,
self.difficulty,
)
def breadcrumb(self, path=[]):
"""
Renvoie le breadcrumb pour l'édutatif courant.
Exemple :
>>> s = Skill.objects.get(pk=44)
>>> s.breadcrumb()
"""
# print('________________________________________________________________')
# print(self)
path = path + [self]
if self.prerequisites.all().count() == 0:
# print('Plus d\'ancetres')
return [path]
# print('# ancetres : ' + str(self.prerequisites.all().count()))
path_list = []
for prerequisite in self.prerequisites.all():
# print(' ' + str(prerequisite))
# Permet de gérer les cas de récursivité (qui ne devraient pas se produire dans notre cas)
if prerequisite.id == self.id:
return [self]
new_paths = prerequisite.breadcrumb(path)
for new_path in new_paths:
path_list.append(new_path)
return path_list
class PrerequisiteClosure(models.Model):
"""
Closure table de prérequis
"""
class Meta:
unique_together = ("descendant", "ancestor", "level", "path")
descendant = models.ForeignKey(Educative, on_delete=models.CASCADE, related_name="ancestor")
ancestor = models.ForeignKey(Educative, on_delete=models.CASCADE, related_name="descendants")
level = models.PositiveIntegerField()
path = models.PositiveIntegerField()
def __str__(self):
return "%s -> %s (%s|%s)" % (
self.ancestor.long_label,
self.descendant.long_label,
self.level,
self.path
)
class TouchPosition(models.Model):

View File

@ -10,7 +10,8 @@ skill_urlpatterns = [
),
path(r"lookup/", views.skill_lookup),
path(r"search/", views.skill_listing),
path(r"<int:skillid>/", views.skill_details, name="skill_details"),
path(r"<int:skill_id>/", views.skill_details, name="skill_details"),
path(r"<int:skill_id>/tree/", views.skill_tree, name="skill_tree"),
path(r"", views.skill_listing, name="skill_list"),
]

View File

@ -8,7 +8,12 @@ from django.urls import reverse
from ultron.people.models import Gymnast
from .forms import RoutineForm
from .models import Skill, Routine, RoutineSkill
from .models import (
Skill,
Routine,
RoutineSkill,
PrerequisiteClosure,
)
@login_required
@ -66,27 +71,35 @@ def skill_listing(request, field=None, expression=None, value=None, level=None):
@login_required
@require_http_methods(["GET"])
def skill_details(request, skillid):
def skill_tree(request, skill_id):
"""
"""
skill = get_object_or_404(Skill, pk=skill_id)
print(skill)
skill_tree = PrerequisiteClosure.objects.filter(descendant=skill).order_by("path", "level")
print(skill_tree)
context = {"skill": skill}
return render(request, "objectives/skills/tree.html", context)
@login_required
@require_http_methods(["GET"])
def skill_details(request, skill_id):
"""Récupère toutes les informations d'un skill.
La méthode en profite pour vérifier les champs level, rank, age_boy et age_girl
par rapport aux pré-requis.
:param skillig: id d'un `skill`
:type skillid: int
:param skill_id: id d'un `skill`
:type skill_id: int
:return: skill
"""
skill = get_object_or_404(Skill, pk=skillid)
skill = get_object_or_404(Skill, pk=skill_id)
for prerequisite in skill.prerequisites.all():
skill.level = max(prerequisite.level + 1, skill.level)
# if prerequisite.level >= skill.level:
# skill.level = prerequisite.level + 1
skill.rank = max(prerequisite.rank + 1, skill.rank)
# if prerequisite.rank >= skill.rank:
# skill.rank = prerequisite.rank + 1
skill.age_boy_with_help = max(skill.age_boy_with_help, prerequisite.age_boy_with_help)
skill.age_boy_without_help = max(
@ -95,8 +108,6 @@ def skill_details(request, skillid):
)
skill.age_boy_chained = max(skill.age_boy_chained, prerequisite.age_boy_chained)
skill.age_boy_masterised = max(skill.age_boy_masterised, prerequisite.age_boy_masterised)
# if prerequisite.age_boy > skill.age_boy:
# skill.age_boy = prerequisite.age_boy
skill.age_girl_with_help = max(skill.age_girl_with_help, prerequisite.age_girl_with_help)
skill.age_girl_without_help = max(
@ -105,8 +116,6 @@ def skill_details(request, skillid):
)
skill.age_girl_chained = max(skill.age_girl_chained, prerequisite.age_girl_chained)
skill.age_girl_masterised = max(skill.age_girl_masterised, prerequisite.age_girl_masterised)
# if prerequisite.age_girl > skill.age_girl:
# skill.age_girl = prerequisite.age_girl
skill.save()