Compare commits
4 Commits
547f4a048e
...
d6c3df1c3b
Author | SHA1 | Date |
---|---|---|
Gregory Trullemans | d6c3df1c3b | |
Gregory Trullemans | 83245b1216 | |
Gregory Trullemans | 706343cc6a | |
Gregory Trullemans | 96f52f1cd4 |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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 %}">
|
||||
|
|
|
@ -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 %}
|
|
@ -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()
|
|
@ -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):
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue