Add galaxy (#562)
* style.scss: lint * style.scss: add 'th' padding * core: populate: add much more data for development * Add galaxy
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
BIN
core/fixtures/images/sas/Family/skia_sli.jpg
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
core/fixtures/images/sas/Family/skia_sli_krophil.jpg
Normal file
After Width: | Height: | Size: 96 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 30 KiB |
@ -6,8 +6,15 @@ from django.core.management.base import BaseCommand
|
|||||||
|
|
||||||
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
# see https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||||
# added "v?"
|
# added "v?"
|
||||||
|
# Please note that this does not match the version of the three.js library.
|
||||||
|
# Hence, you shall have to check this one by yourself
|
||||||
semver_regex = re.compile(
|
semver_regex = re.compile(
|
||||||
"""^v?(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"""
|
r"^v?"
|
||||||
|
r"(?P<major>\d+)"
|
||||||
|
r"\.(?P<minor>\d+)"
|
||||||
|
r"\.(?P<patch>\d+)"
|
||||||
|
r"(?:-(?P<prerelease>(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:\d+|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
|
||||||
|
r"(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding:utf-8 -*
|
# -*- coding:utf-8 -*
|
||||||
#
|
#
|
||||||
# Copyright 2016,2017
|
# Copyright 2016,2017,2023
|
||||||
# - Skia <skia@libskia.so>
|
# - Skia <skia@hya.sk>
|
||||||
#
|
#
|
||||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
# http://ae.utbm.fr.
|
# http://ae.utbm.fr.
|
||||||
@ -25,6 +25,7 @@
|
|||||||
import os
|
import os
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from io import StringIO, BytesIO
|
from io import StringIO, BytesIO
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from django.contrib.auth.models import Permission
|
from django.contrib.auth.models import Permission
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
@ -54,6 +55,7 @@ from com.models import Sith, Weekmail, News, NewsDate
|
|||||||
from election.models import Election, Role, Candidature, ElectionList
|
from election.models import Election, Role, Candidature, ElectionList
|
||||||
from forum.models import Forum, ForumTopic
|
from forum.models import Forum, ForumTopic
|
||||||
from pedagogy.models import UV
|
from pedagogy.models import UV
|
||||||
|
from sas.models import Album, Picture, PeoplePictureRelation
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -71,9 +73,7 @@ class Command(BaseCommand):
|
|||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
os.environ["DJANGO_COLORS"] = "nocolor"
|
os.environ["DJANGO_COLORS"] = "nocolor"
|
||||||
Site(id=4000, domain=settings.SITH_URL, name=settings.SITH_NAME).save()
|
Site(id=4000, domain=settings.SITH_URL, name=settings.SITH_NAME).save()
|
||||||
root_path = os.path.dirname(
|
root_path = Path(__file__).parent.parent.parent.parent
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
)
|
|
||||||
root_group, _ = Group.objects.get_or_create(name="Root")
|
root_group, _ = Group.objects.get_or_create(name="Root")
|
||||||
Group(name="Public").save()
|
Group(name="Public").save()
|
||||||
Group(name="Subscribers").save()
|
Group(name="Subscribers").save()
|
||||||
@ -84,7 +84,7 @@ class Command(BaseCommand):
|
|||||||
Group(name="Banned from buying alcohol").save()
|
Group(name="Banned from buying alcohol").save()
|
||||||
Group(name="Banned from counters").save()
|
Group(name="Banned from counters").save()
|
||||||
Group(name="Banned to subscribe").save()
|
Group(name="Banned to subscribe").save()
|
||||||
Group(name="SAS admin").save()
|
sas_admin, _ = Group.objects.get_or_create(name="SAS admin")
|
||||||
Group(name="Forum admin").save()
|
Group(name="Forum admin").save()
|
||||||
Group(name="Pedagogy admin").save()
|
Group(name="Pedagogy admin").save()
|
||||||
self.reset_index("core", "auth")
|
self.reset_index("core", "auth")
|
||||||
@ -119,7 +119,8 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
club_root = SithFile(parent=None, name="clubs", is_folder=True, owner=root)
|
club_root = SithFile(parent=None, name="clubs", is_folder=True, owner=root)
|
||||||
club_root.save()
|
club_root.save()
|
||||||
SithFile(parent=None, name="SAS", is_folder=True, owner=root).save()
|
sas = SithFile(parent=None, name="SAS", is_folder=True, owner=root)
|
||||||
|
sas.save()
|
||||||
main_club = Club(
|
main_club = Club(
|
||||||
id=1,
|
id=1,
|
||||||
name=settings.SITH_MAIN_CLUB["name"],
|
name=settings.SITH_MAIN_CLUB["name"],
|
||||||
@ -223,7 +224,15 @@ Welcome to the wiki page!
|
|||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
||||||
]
|
]
|
||||||
skia.save()
|
skia.save()
|
||||||
skia_profile_path = os.path.join(root_path, "core/fixtures/images/3.jpg")
|
skia_profile_path = (
|
||||||
|
root_path
|
||||||
|
/ "core"
|
||||||
|
/ "fixtures"
|
||||||
|
/ "images"
|
||||||
|
/ "sas"
|
||||||
|
/ "Family"
|
||||||
|
/ "skia.jpg"
|
||||||
|
)
|
||||||
with open(skia_profile_path, "rb") as f:
|
with open(skia_profile_path, "rb") as f:
|
||||||
name = str(skia.id) + "_profile.jpg"
|
name = str(skia.id) + "_profile.jpg"
|
||||||
skia_profile = SithFile(
|
skia_profile = SithFile(
|
||||||
@ -233,7 +242,7 @@ Welcome to the wiki page!
|
|||||||
owner=skia,
|
owner=skia,
|
||||||
is_folder=False,
|
is_folder=False,
|
||||||
mime_type="image/jpeg",
|
mime_type="image/jpeg",
|
||||||
size=os.path.getsize(skia_profile_path),
|
size=skia_profile_path.stat().st_size,
|
||||||
)
|
)
|
||||||
skia_profile.file.name = name
|
skia_profile.file.name = name
|
||||||
skia_profile.save()
|
skia_profile.save()
|
||||||
@ -351,23 +360,48 @@ Welcome to the wiki page!
|
|||||||
]
|
]
|
||||||
u.save()
|
u.save()
|
||||||
# Adding user Richard Batsbak
|
# Adding user Richard Batsbak
|
||||||
r = User(
|
richard = User(
|
||||||
username="rbatsbak",
|
username="rbatsbak",
|
||||||
last_name="Batsbak",
|
last_name="Batsbak",
|
||||||
first_name="Richard",
|
first_name="Richard",
|
||||||
email="richard@git.an",
|
email="richard@git.an",
|
||||||
date_of_birth="1982-06-12",
|
date_of_birth="1982-06-12",
|
||||||
)
|
)
|
||||||
r.set_password("plop")
|
richard.set_password("plop")
|
||||||
r.save()
|
richard.save()
|
||||||
r.view_groups = [
|
richard.godfathers.add(comptable)
|
||||||
|
richard_profile_path = (
|
||||||
|
root_path
|
||||||
|
/ "core"
|
||||||
|
/ "fixtures"
|
||||||
|
/ "images"
|
||||||
|
/ "sas"
|
||||||
|
/ "Family"
|
||||||
|
/ "richard.jpg"
|
||||||
|
)
|
||||||
|
with open(richard_profile_path, "rb") as f:
|
||||||
|
name = f"{richard.id}_profile.jpg"
|
||||||
|
richard_profile = SithFile(
|
||||||
|
parent=profiles_root,
|
||||||
|
name=name,
|
||||||
|
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"),
|
||||||
|
owner=richard,
|
||||||
|
is_folder=False,
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
size=richard_profile_path.stat().st_size,
|
||||||
|
)
|
||||||
|
richard_profile.file.name = name
|
||||||
|
richard_profile.save()
|
||||||
|
richard.profile_pict = richard_profile
|
||||||
|
richard.save()
|
||||||
|
richard.view_groups = [
|
||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
||||||
]
|
]
|
||||||
r.save()
|
richard.save()
|
||||||
# Adding syntax help page
|
# Adding syntax help page
|
||||||
p = Page(name="Aide_sur_la_syntaxe")
|
p = Page(name="Aide_sur_la_syntaxe")
|
||||||
p.save(force_lock=True)
|
p.save(force_lock=True)
|
||||||
with open(os.path.join(root_path) + "/doc/SYNTAX.md", "r") as rm:
|
with open(root_path / "doc" / "SYNTAX.md", "r") as rm:
|
||||||
PageRev(
|
PageRev(
|
||||||
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
||||||
).save()
|
).save()
|
||||||
@ -442,7 +476,7 @@ Welcome to the wiki page!
|
|||||||
s.save()
|
s.save()
|
||||||
# Richard
|
# Richard
|
||||||
s = Subscription(
|
s = Subscription(
|
||||||
member=User.objects.filter(pk=r.pk).first(),
|
member=User.objects.filter(pk=richard.pk).first(),
|
||||||
subscription_type=default_subscription,
|
subscription_type=default_subscription,
|
||||||
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||||
)
|
)
|
||||||
@ -514,7 +548,7 @@ Welcome to the wiki page!
|
|||||||
subscribers = Group.objects.get(name="Subscribers")
|
subscribers = Group.objects.get(name="Subscribers")
|
||||||
old_subscribers = Group.objects.get(name="Old subscribers")
|
old_subscribers = Group.objects.get(name="Old subscribers")
|
||||||
Customer(user=skia, account_id="6568j", amount=0).save()
|
Customer(user=skia, account_id="6568j", amount=0).save()
|
||||||
Customer(user=r, account_id="4000k", amount=0).save()
|
Customer(user=richard, account_id="4000k", amount=0).save()
|
||||||
p = ProductType(name="Bières bouteilles")
|
p = ProductType(name="Bières bouteilles")
|
||||||
p.save()
|
p.save()
|
||||||
c = ProductType(name="Cotisations")
|
c = ProductType(name="Cotisations")
|
||||||
@ -825,7 +859,15 @@ Welcome to the wiki page!
|
|||||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
||||||
]
|
]
|
||||||
sli.save()
|
sli.save()
|
||||||
sli_profile_path = os.path.join(root_path, "core/fixtures/images/5.jpg")
|
sli_profile_path = (
|
||||||
|
root_path
|
||||||
|
/ "core"
|
||||||
|
/ "fixtures"
|
||||||
|
/ "images"
|
||||||
|
/ "sas"
|
||||||
|
/ "Family"
|
||||||
|
/ "sli.jpg"
|
||||||
|
)
|
||||||
with open(sli_profile_path, "rb") as f:
|
with open(sli_profile_path, "rb") as f:
|
||||||
name = str(sli.id) + "_profile.jpg"
|
name = str(sli.id) + "_profile.jpg"
|
||||||
sli_profile = SithFile(
|
sli_profile = SithFile(
|
||||||
@ -835,7 +877,7 @@ Welcome to the wiki page!
|
|||||||
owner=sli,
|
owner=sli,
|
||||||
is_folder=False,
|
is_folder=False,
|
||||||
mime_type="image/jpeg",
|
mime_type="image/jpeg",
|
||||||
size=os.path.getsize(sli_profile_path),
|
size=sli_profile_path.stat().st_size,
|
||||||
)
|
)
|
||||||
sli_profile.file.name = name
|
sli_profile.file.name = name
|
||||||
sli_profile.save()
|
sli_profile.save()
|
||||||
@ -851,7 +893,15 @@ Welcome to the wiki page!
|
|||||||
)
|
)
|
||||||
krophil.set_password("plop")
|
krophil.set_password("plop")
|
||||||
krophil.save()
|
krophil.save()
|
||||||
krophil_profile_path = os.path.join(root_path, "core/fixtures/images/6.jpg")
|
krophil_profile_path = (
|
||||||
|
root_path
|
||||||
|
/ "core"
|
||||||
|
/ "fixtures"
|
||||||
|
/ "images"
|
||||||
|
/ "sas"
|
||||||
|
/ "Family"
|
||||||
|
/ "krophil.jpg"
|
||||||
|
)
|
||||||
with open(krophil_profile_path, "rb") as f:
|
with open(krophil_profile_path, "rb") as f:
|
||||||
name = str(krophil.id) + "_profile.jpg"
|
name = str(krophil.id) + "_profile.jpg"
|
||||||
krophil_profile = SithFile(
|
krophil_profile = SithFile(
|
||||||
@ -861,7 +911,7 @@ Welcome to the wiki page!
|
|||||||
owner=krophil,
|
owner=krophil,
|
||||||
is_folder=False,
|
is_folder=False,
|
||||||
mime_type="image/jpeg",
|
mime_type="image/jpeg",
|
||||||
size=os.path.getsize(krophil_profile_path),
|
size=krophil_profile_path.stat().st_size,
|
||||||
)
|
)
|
||||||
krophil_profile.file.name = name
|
krophil_profile.file.name = name
|
||||||
krophil_profile.save()
|
krophil_profile.save()
|
||||||
@ -1164,3 +1214,106 @@ Welcome to the wiki page!
|
|||||||
hours_THE=121,
|
hours_THE=121,
|
||||||
hours_TE=4,
|
hours_TE=4,
|
||||||
).save()
|
).save()
|
||||||
|
|
||||||
|
# SAS
|
||||||
|
skia.groups.add(sas_admin.id)
|
||||||
|
sas_fixtures_path = root_path / "core" / "fixtures" / "images" / "sas"
|
||||||
|
for f in sas_fixtures_path.glob("*"):
|
||||||
|
if f.is_dir():
|
||||||
|
album = Album(
|
||||||
|
parent=sas,
|
||||||
|
name=f.name,
|
||||||
|
owner=root,
|
||||||
|
is_folder=True,
|
||||||
|
is_in_sas=True,
|
||||||
|
is_moderated=True,
|
||||||
|
)
|
||||||
|
album.clean()
|
||||||
|
album.save()
|
||||||
|
for p in f.iterdir():
|
||||||
|
pict = Picture(
|
||||||
|
parent=album,
|
||||||
|
name=p.name,
|
||||||
|
file=resize_image(
|
||||||
|
Image.open(BytesIO(p.read_bytes())), 1000, "JPEG"
|
||||||
|
),
|
||||||
|
owner=root,
|
||||||
|
is_folder=False,
|
||||||
|
is_in_sas=True,
|
||||||
|
is_moderated=True,
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
size=p.stat().st_size,
|
||||||
|
)
|
||||||
|
pict.file.name = p.name
|
||||||
|
pict.clean()
|
||||||
|
pict.generate_thumbnails()
|
||||||
|
pict.save()
|
||||||
|
|
||||||
|
p = Picture.objects.get(name="skia.jpg")
|
||||||
|
PeoplePictureRelation(user=skia, picture=p).save()
|
||||||
|
p = Picture.objects.get(name="sli.jpg")
|
||||||
|
PeoplePictureRelation(user=sli, picture=p).save()
|
||||||
|
p = Picture.objects.get(name="krophil.jpg")
|
||||||
|
PeoplePictureRelation(user=krophil, picture=p).save()
|
||||||
|
p = Picture.objects.get(name="skia_sli.jpg")
|
||||||
|
PeoplePictureRelation(user=skia, picture=p).save()
|
||||||
|
PeoplePictureRelation(user=sli, picture=p).save()
|
||||||
|
p = Picture.objects.get(name="skia_sli_krophil.jpg")
|
||||||
|
PeoplePictureRelation(user=skia, picture=p).save()
|
||||||
|
PeoplePictureRelation(user=sli, picture=p).save()
|
||||||
|
PeoplePictureRelation(user=krophil, picture=p).save()
|
||||||
|
p = Picture.objects.get(name="richard.jpg")
|
||||||
|
PeoplePictureRelation(user=richard, picture=p).save()
|
||||||
|
|
||||||
|
with open(skia_profile_path, "rb") as f:
|
||||||
|
name = str(skia.id) + "_profile.jpg"
|
||||||
|
skia_profile = SithFile(
|
||||||
|
parent=profiles_root,
|
||||||
|
name=name,
|
||||||
|
file=resize_image(Image.open(BytesIO(f.read())), 400, "JPEG"),
|
||||||
|
owner=skia,
|
||||||
|
is_folder=False,
|
||||||
|
mime_type="image/jpeg",
|
||||||
|
size=skia_profile_path.stat().st_size,
|
||||||
|
)
|
||||||
|
skia_profile.file.name = name
|
||||||
|
skia_profile.save()
|
||||||
|
skia.profile_pict = skia_profile
|
||||||
|
skia.save()
|
||||||
|
|
||||||
|
# Create some additional data for galaxy to work with
|
||||||
|
root.godfathers.add(skia)
|
||||||
|
skia.godfathers.add(root)
|
||||||
|
sli.godfathers.add(skia)
|
||||||
|
richard.godchildren.add(subscriber)
|
||||||
|
richard.godchildren.add(public)
|
||||||
|
Membership(
|
||||||
|
user=sli,
|
||||||
|
club=troll,
|
||||||
|
role=9,
|
||||||
|
description="Padawan Troll",
|
||||||
|
start_date=timezone.now() - timedelta(days=17),
|
||||||
|
).save()
|
||||||
|
Membership(
|
||||||
|
user=krophil,
|
||||||
|
club=troll,
|
||||||
|
role=10,
|
||||||
|
description="Maitre Troll",
|
||||||
|
start_date=timezone.now() - timedelta(days=200),
|
||||||
|
).save()
|
||||||
|
Membership(
|
||||||
|
user=skia,
|
||||||
|
club=troll,
|
||||||
|
role=2,
|
||||||
|
description="Grand Ancien Troll",
|
||||||
|
start_date=timezone.now() - timedelta(days=400),
|
||||||
|
end_date=timezone.now() - timedelta(days=86),
|
||||||
|
).save()
|
||||||
|
Membership(
|
||||||
|
user=richard,
|
||||||
|
club=troll,
|
||||||
|
role=2,
|
||||||
|
description="",
|
||||||
|
start_date=timezone.now() - timedelta(days=200),
|
||||||
|
end_date=timezone.now() - timedelta(days=100),
|
||||||
|
).save()
|
||||||
|
@ -11,7 +11,6 @@ $primary-dark-color: hsl(203, 75%, 40%);
|
|||||||
$secondary-light-color: hsl(40, 68%, 65%);
|
$secondary-light-color: hsl(40, 68%, 65%);
|
||||||
$secondary-dark-color: hsl(40, 68%, 35%);
|
$secondary-dark-color: hsl(40, 68%, 35%);
|
||||||
|
|
||||||
|
|
||||||
$primary-neutral-color: hsl(219.6, 20.8%, 50%);
|
$primary-neutral-color: hsl(219.6, 20.8%, 50%);
|
||||||
$primary-neutral-light-color: hsl(0, 0%, 94%);
|
$primary-neutral-light-color: hsl(0, 0%, 94%);
|
||||||
$primary-neutral-dark-color: hsl(210, 29%, 29%);
|
$primary-neutral-dark-color: hsl(210, 29%, 29%);
|
||||||
@ -29,7 +28,7 @@ $pinktober: #ff5674;
|
|||||||
$pinktober-secondary: #8a2536;
|
$pinktober-secondary: #8a2536;
|
||||||
$pinktober-primary-text: white;
|
$pinktober-primary-text: white;
|
||||||
$pinktober-bar-closed: $pinktober-secondary;
|
$pinktober-bar-closed: $pinktober-secondary;
|
||||||
$pinktober-bar-opened: #388E3C;
|
$pinktober-bar-opened: #388e3c;
|
||||||
|
|
||||||
$shadow-color: rgb(223, 223, 223);
|
$shadow-color: rgb(223, 223, 223);
|
||||||
|
|
||||||
@ -49,7 +48,11 @@ body {
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
button, input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
button,
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="file"] {
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-button-color;
|
background-color: $background-button-color;
|
||||||
@ -62,15 +65,24 @@ button, input[type=button], input[type=submit], input[type=reset],input[type=fil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=button], input[type=submit], input[type=reset],input[type=file] {
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="file"] {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:not(:disabled), input[type=button]:not(:disabled), input[type=submit]:not(:disabled), input[type=reset]:not(:disabled),input[type=file]:not(:disabled) {
|
button:not(:disabled),
|
||||||
|
input[type="button"]:not(:disabled),
|
||||||
|
input[type="submit"]:not(:disabled),
|
||||||
|
input[type="reset"]:not(:disabled),
|
||||||
|
input[type="file"]:not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input,textarea[type=text],[type=number]{
|
input,
|
||||||
|
textarea[type="text"],
|
||||||
|
[type="number"] {
|
||||||
border: none;
|
border: none;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: $background-button-color;
|
background-color: $background-button-color;
|
||||||
@ -143,7 +155,8 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow {
|
.shadow {
|
||||||
box-shadow: rgba(60, 64, 67, .3) 0 1px 3px 0, rgba(60, 64, 67, .15) 0 4px 8px 3px;
|
box-shadow: rgba(60, 64, 67, 0.3) 0 1px 3px 0,
|
||||||
|
rgba(60, 64, 67, 0.15) 0 4px 8px 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w_big {
|
.w_big {
|
||||||
@ -162,7 +175,9 @@ a {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
[x-cloak] { display: none !important; }
|
[x-cloak] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
/*--------------------------------HEADER-------------------------------*/
|
/*--------------------------------HEADER-------------------------------*/
|
||||||
|
|
||||||
@ -195,7 +210,6 @@ header {
|
|||||||
background-color: $primary-neutral-dark-color;
|
background-color: $primary-neutral-dark-color;
|
||||||
border-radius: 0px 0px 10px 10px;
|
border-radius: 0px 0px 10px 10px;
|
||||||
|
|
||||||
|
|
||||||
#header_logo {
|
#header_logo {
|
||||||
background-color: $white-color;
|
background-color: $white-color;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
@ -338,7 +352,8 @@ header {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
#alert_box, #info_box {
|
#alert_box,
|
||||||
|
#info_box {
|
||||||
flex: 49%;
|
flex: 49%;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin: 0.2em;
|
margin: 0.2em;
|
||||||
@ -369,7 +384,6 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#page {
|
#page {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 20px auto 0;
|
margin: 20px auto 0;
|
||||||
@ -485,7 +499,6 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
padding: 1em 1%;
|
padding: 1em 1%;
|
||||||
box-shadow: $shadow-color 0 5px 10px;
|
box-shadow: $shadow-color 0 5px 10px;
|
||||||
@ -587,19 +600,22 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (max-width: $small-devices) {
|
@media screen and (max-width: $small-devices) {
|
||||||
#left_column, #right_column {
|
#left_column,
|
||||||
|
#right_column {
|
||||||
flex: 100%;
|
flex: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* AGENDA/BIRTHDAYS */
|
/* AGENDA/BIRTHDAYS */
|
||||||
#agenda,#birthdays {
|
#agenda,
|
||||||
|
#birthdays {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: white;
|
background: white;
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
#agenda_title,#birthdays_title {
|
#agenda_title,
|
||||||
|
#birthdays_title {
|
||||||
margin: 0em;
|
margin: 0em;
|
||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
@ -615,7 +631,8 @@ header {
|
|||||||
box-shadow: $shadow-color 1px 1px 1px;
|
box-shadow: $shadow-color 1px 1px 1px;
|
||||||
height: 20em;
|
height: 20em;
|
||||||
}
|
}
|
||||||
#agenda_content,#birthdays_content {
|
#agenda_content,
|
||||||
|
#birthdays_content {
|
||||||
.agenda_item {
|
.agenda_item {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
@ -867,7 +884,10 @@ header {
|
|||||||
|
|
||||||
/*---------------------------POSTERS----------------------------*/
|
/*---------------------------POSTERS----------------------------*/
|
||||||
|
|
||||||
#poster_list, #screen_list, #poster_edit, #screen_edit{
|
#poster_list,
|
||||||
|
#screen_list,
|
||||||
|
#poster_edit,
|
||||||
|
#screen_edit {
|
||||||
position: relative;
|
position: relative;
|
||||||
#title {
|
#title {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -907,16 +927,19 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#posters, #screens{
|
#posters,
|
||||||
|
#screens {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
#no-posters, #no-screens{
|
#no-posters,
|
||||||
|
#no-screens {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.poster, .screen{
|
.poster,
|
||||||
|
.screen {
|
||||||
min-width: 10%;
|
min-width: 10%;
|
||||||
max-width: 20%;
|
max-width: 20%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -976,7 +999,8 @@ header {
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
.begin, .end{
|
.begin,
|
||||||
|
.end {
|
||||||
width: 48%;
|
width: 48%;
|
||||||
}
|
}
|
||||||
.begin {
|
.begin {
|
||||||
@ -984,7 +1008,9 @@ header {
|
|||||||
padding-right: 2%;
|
padding-right: 2%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edit, .moderate, .slideshow{
|
.edit,
|
||||||
|
.moderate,
|
||||||
|
.slideshow {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background-color: hsl(40, 100%, 50%);
|
background-color: hsl(40, 100%, 50%);
|
||||||
@ -1017,8 +1043,7 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.not_moderated
|
&.not_moderated {
|
||||||
{
|
|
||||||
border: 1px solid red;
|
border: 1px solid red;
|
||||||
}
|
}
|
||||||
&:hover .tooltip {
|
&:hover .tooltip {
|
||||||
@ -1058,7 +1083,6 @@ header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*---------------------------ACCOUNTING----------------------------*/
|
/*---------------------------ACCOUNTING----------------------------*/
|
||||||
#accounting {
|
#accounting {
|
||||||
.journal-table {
|
.journal-table {
|
||||||
@ -1084,7 +1108,12 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*-----------------------------GENERAL-----------------------------*/
|
/*-----------------------------GENERAL-----------------------------*/
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
@ -1119,12 +1148,15 @@ h6 {
|
|||||||
margin-left: 50px;
|
margin-left: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
p, pre {
|
p,
|
||||||
|
pre {
|
||||||
margin-top: 0.8em;
|
margin-top: 0.8em;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol, dl {
|
ul,
|
||||||
|
ol,
|
||||||
|
dl {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
margin-left: 25px;
|
margin-left: 25px;
|
||||||
@ -1167,7 +1199,11 @@ blockquote h5:first-child {
|
|||||||
|
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-size: 0.90em;
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
@ -1207,11 +1243,13 @@ sub {
|
|||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
}
|
}
|
||||||
|
|
||||||
b, strong {
|
b,
|
||||||
|
strong {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
i, em {
|
i,
|
||||||
|
em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1220,7 +1258,8 @@ i, em {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
u, .underline {
|
u,
|
||||||
|
.underline {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1273,7 +1312,8 @@ u, .underline {
|
|||||||
#user_profile_infos_items {
|
#user_profile_infos_items {
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
}
|
}
|
||||||
.user_profile_infos_item, .user_profile_infos_item_value {
|
.user_profile_infos_item,
|
||||||
|
.user_profile_infos_item_value {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 49%;
|
width: 49%;
|
||||||
@ -1293,7 +1333,8 @@ u, .underline {
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
color: grey;
|
color: grey;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
&:after, &:before {
|
&:after,
|
||||||
|
&:before {
|
||||||
content: "\201C";
|
content: "\201C";
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
@ -1330,7 +1371,8 @@ u, .underline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (max-width: $small-devices) {
|
@media screen and (max-width: $small-devices) {
|
||||||
#user_profile_infos, #user_profile_pictures {
|
#user_profile_infos,
|
||||||
|
#user_profile_pictures {
|
||||||
flex-basis: 50%;
|
flex-basis: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1439,7 +1481,6 @@ u, .underline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*---------------------------------PAGE--------------------------------*/
|
/*---------------------------------PAGE--------------------------------*/
|
||||||
|
|
||||||
.page_content {
|
.page_content {
|
||||||
@ -1720,7 +1761,8 @@ label {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#cash_summary_form label, .inline {
|
#cash_summary_form label,
|
||||||
|
.inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1775,13 +1817,19 @@ label {
|
|||||||
|
|
||||||
/*--------------------------------JQuery-------------------------------*/
|
/*--------------------------------JQuery-------------------------------*/
|
||||||
|
|
||||||
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header
|
.ui-state-active,
|
||||||
.ui-state-active, a.ui-button:active, .ui-button:active,
|
.ui-widget-content .ui-state-active,
|
||||||
|
.ui-widget-header .ui-state-active,
|
||||||
|
a.ui-button:active,
|
||||||
|
.ui-button:active,
|
||||||
.ui-button.ui-state-active:hover {
|
.ui-button.ui-state-active:hover {
|
||||||
background: $primary-color;
|
background: $primary-color;
|
||||||
border-color: $primary-color;
|
border-color: $primary-color;
|
||||||
}
|
}
|
||||||
.ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-top,
|
.ui-corner-all,
|
||||||
|
.ui-corner-bottom,
|
||||||
|
.ui-corner-right,
|
||||||
|
.ui-corner-top,
|
||||||
.ui-corner-left {
|
.ui-corner-left {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
@ -1795,7 +1843,6 @@ label {
|
|||||||
max-width: 10em;
|
max-width: 10em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------pedagogy-----------------------------------*/
|
/* --------------------------------------pedagogy-----------------------------------*/
|
||||||
@ -1819,7 +1866,7 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.grade-without-star {
|
&.grade-without-star {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $large-devices) {
|
@media screen and (max-width: $large-devices) {
|
||||||
@ -1851,11 +1898,9 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#search_form {
|
#search_form {
|
||||||
|
|
||||||
.search-form-container {
|
.search-form-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
@ -1921,7 +1966,8 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
grid-area: radio-semester;
|
grid-area: radio-semester;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radio-guide input[type="radio"],input[type="checkbox"] {
|
.radio-guide input[type="radio"],
|
||||||
|
input[type="checkbox"] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.radio-guide {
|
.radio-guide {
|
||||||
@ -2111,7 +2157,7 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
"grade-block"
|
"grade-block"
|
||||||
"comment"
|
"comment"
|
||||||
"info"
|
"info"
|
||||||
"comment-end-bar"
|
"comment-end-bar";
|
||||||
}
|
}
|
||||||
|
|
||||||
.grade-block {
|
.grade-block {
|
||||||
@ -2261,7 +2307,6 @@ $pedagogy-white-text: #f0f0f0;
|
|||||||
a:hover {
|
a:hover {
|
||||||
color: $pedagogy-hover-blue;
|
color: $pedagogy-hover-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
|
@ -189,78 +189,70 @@ class UserTabsMixin(TabedViewMixin):
|
|||||||
return self.object.get_display_name()
|
return self.object.get_display_name()
|
||||||
|
|
||||||
def get_list_of_tabs(self):
|
def get_list_of_tabs(self):
|
||||||
tab_list = []
|
user: User = self.object
|
||||||
tab_list.append(
|
tab_list = [
|
||||||
{
|
{
|
||||||
"url": reverse("core:user_profile", kwargs={"user_id": self.object.id}),
|
"url": reverse("core:user_profile", kwargs={"user_id": user.id}),
|
||||||
"slug": "infos",
|
"slug": "infos",
|
||||||
"name": _("Infos"),
|
"name": _("Infos"),
|
||||||
}
|
},
|
||||||
)
|
|
||||||
tab_list.append(
|
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_godfathers", kwargs={"user_id": user.id}),
|
||||||
"core:user_godfathers", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "godfathers",
|
"slug": "godfathers",
|
||||||
"name": _("Family"),
|
"name": _("Family"),
|
||||||
}
|
},
|
||||||
)
|
|
||||||
tab_list.append(
|
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_pictures", kwargs={"user_id": user.id}),
|
||||||
"core:user_pictures", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "pictures",
|
"slug": "pictures",
|
||||||
"name": _("Pictures"),
|
"name": _("Pictures"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if self.request.user.was_subscribed:
|
||||||
|
tab_list.append(
|
||||||
|
{
|
||||||
|
"url": reverse("galaxy:user", kwargs={"user_id": user.id}),
|
||||||
|
"slug": "galaxy",
|
||||||
|
"name": _("Galaxy"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if self.request.user == self.object:
|
if self.request.user == user:
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{"url": reverse("core:user_tools"), "slug": "tools", "name": _("Tools")}
|
{"url": reverse("core:user_tools"), "slug": "tools", "name": _("Tools")}
|
||||||
)
|
)
|
||||||
if self.request.user.can_edit(self.object):
|
if self.request.user.can_edit(user):
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_edit", kwargs={"user_id": user.id}),
|
||||||
"core:user_edit", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "edit",
|
"slug": "edit",
|
||||||
"name": _("Edit"),
|
"name": _("Edit"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_prefs", kwargs={"user_id": user.id}),
|
||||||
"core:user_prefs", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "prefs",
|
"slug": "prefs",
|
||||||
"name": _("Preferences"),
|
"name": _("Preferences"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if self.request.user.can_view(self.object):
|
if self.request.user.can_view(user):
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_clubs", kwargs={"user_id": user.id}),
|
||||||
"core:user_clubs", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "clubs",
|
"slug": "clubs",
|
||||||
"name": _("Clubs"),
|
"name": _("Clubs"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if self.request.user.is_owner(self.object):
|
if self.request.user.is_owner(user):
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_groups", kwargs={"user_id": user.id}),
|
||||||
"core:user_groups", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "groups",
|
"slug": "groups",
|
||||||
"name": _("Groups"),
|
"name": _("Groups"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if self.object.customer and (
|
if user.customer and (
|
||||||
self.object == self.request.user
|
user == self.request.user
|
||||||
or self.request.user.is_in_group(
|
or self.request.user.is_in_group(
|
||||||
settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
|
settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
|
||||||
)
|
)
|
||||||
@ -271,9 +263,7 @@ class UserTabsMixin(TabedViewMixin):
|
|||||||
):
|
):
|
||||||
tab_list.append(
|
tab_list.append(
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse("core:user_stats", kwargs={"user_id": user.id}),
|
||||||
"core:user_stats", kwargs={"user_id": self.object.id}
|
|
||||||
),
|
|
||||||
"slug": "stats",
|
"slug": "stats",
|
||||||
"name": _("Stats"),
|
"name": _("Stats"),
|
||||||
}
|
}
|
||||||
@ -281,10 +271,10 @@ class UserTabsMixin(TabedViewMixin):
|
|||||||
tab_list.append(
|
tab_list.append(
|
||||||
{
|
{
|
||||||
"url": reverse(
|
"url": reverse(
|
||||||
"core:user_account", kwargs={"user_id": self.object.id}
|
"core:user_account", kwargs={"user_id": user.id}
|
||||||
),
|
),
|
||||||
"slug": "account",
|
"slug": "account",
|
||||||
"name": _("Account") + " (%s €)" % self.object.customer.amount,
|
"name": _("Account") + " (%s €)" % user.customer.amount,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
|
0
galaxy/__init__.py
Normal file
30
galaxy/apps.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2023
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyConfig(AppConfig):
|
||||||
|
default_auto_field = "django.db.models.BigAutoField"
|
||||||
|
name = "galaxy"
|
61
galaxy/management/commands/rule_galaxy.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2023
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import connection
|
||||||
|
|
||||||
|
from galaxy.models import Galaxy
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
|
"Rule the Galaxy! "
|
||||||
|
"Reset the whole galaxy and compute once again all the relation scores of all users. "
|
||||||
|
"As the sith's users are rather numerous, this command might be quite expensive in memory "
|
||||||
|
"and CPU time. Please keep this fact in mind when scheduling calls to this command in a production "
|
||||||
|
"environment."
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
logger = logging.getLogger("main")
|
||||||
|
if options["verbosity"] > 1:
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
elif options["verbosity"] > 0:
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
else:
|
||||||
|
logger.setLevel(logging.NOTSET)
|
||||||
|
|
||||||
|
logger.info("The Galaxy is being ruled by the Sith.")
|
||||||
|
Galaxy.rule()
|
||||||
|
logger.info(
|
||||||
|
"Caching current Galaxy state for a quicker display of the Empire's power."
|
||||||
|
)
|
||||||
|
Galaxy.make_state()
|
||||||
|
|
||||||
|
logger.info("Ruled the galaxy in {} queries.".format(len(connection.queries)))
|
||||||
|
if options["verbosity"] > 2:
|
||||||
|
for q in connection.queries:
|
||||||
|
logger.debug(q)
|
113
galaxy/migrations/0001_initial.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Generated by Django 3.2.16 on 2023-02-03 10:31
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Galaxy",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("state", models.JSONField(verbose_name="current state")),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="GalaxyStar",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"mass",
|
||||||
|
models.PositiveIntegerField(default=0, verbose_name="star mass"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owner",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="galaxy_user",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="galaxy user",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="GalaxyLane",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"distance",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0,
|
||||||
|
help_text="Distance separating star1 and star2",
|
||||||
|
verbose_name="distance",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"family",
|
||||||
|
models.PositiveIntegerField(default=0, verbose_name="family score"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pictures",
|
||||||
|
models.PositiveIntegerField(
|
||||||
|
default=0, verbose_name="pictures score"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"clubs",
|
||||||
|
models.PositiveIntegerField(default=0, verbose_name="clubs score"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"star1",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="lanes1",
|
||||||
|
to="galaxy.galaxystar",
|
||||||
|
verbose_name="galaxy lanes 1",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"star2",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="lanes2",
|
||||||
|
to="galaxy.galaxystar",
|
||||||
|
verbose_name="galaxy lanes 2",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
galaxy/migrations/__init__.py
Normal file
377
galaxy/models.py
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2023
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
import math
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q, Case, F, Value, When, Count
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from typing import List, TypedDict
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from club.models import Club
|
||||||
|
from sas.models import Picture
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyStar(models.Model):
|
||||||
|
"""
|
||||||
|
This class defines a star (vertex -> user) in the galaxy graph, storing a reference to its owner citizen, and being
|
||||||
|
referenced by GalaxyLane.
|
||||||
|
|
||||||
|
It also stores the individual mass of this star, used to push it towards the center of the galaxy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
owner = models.OneToOneField(
|
||||||
|
User,
|
||||||
|
verbose_name=_("galaxy user"),
|
||||||
|
related_name="galaxy_user",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
mass = models.PositiveIntegerField(
|
||||||
|
_("star mass"),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.owner)
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyLane(models.Model):
|
||||||
|
"""
|
||||||
|
This class defines a lane (edge -> link between galaxy citizen) in the galaxy map, storing a reference to both its
|
||||||
|
ends and the distance it covers.
|
||||||
|
Score details between citizen owning the stars is also stored here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
star1 = models.ForeignKey(
|
||||||
|
GalaxyStar,
|
||||||
|
verbose_name=_("galaxy lanes 1"),
|
||||||
|
related_name="lanes1",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
star2 = models.ForeignKey(
|
||||||
|
GalaxyStar,
|
||||||
|
verbose_name=_("galaxy lanes 2"),
|
||||||
|
related_name="lanes2",
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
distance = models.PositiveIntegerField(
|
||||||
|
_("distance"),
|
||||||
|
default=0,
|
||||||
|
help_text=_("Distance separating star1 and star2"),
|
||||||
|
)
|
||||||
|
family = models.PositiveIntegerField(
|
||||||
|
_("family score"),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
pictures = models.PositiveIntegerField(
|
||||||
|
_("pictures score"),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
clubs = models.PositiveIntegerField(
|
||||||
|
_("clubs score"),
|
||||||
|
default=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StarDict(TypedDict):
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
mass: int
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyDict(TypedDict):
|
||||||
|
nodes: List[StarDict]
|
||||||
|
links: List
|
||||||
|
|
||||||
|
|
||||||
|
class Galaxy(models.Model):
|
||||||
|
logger = logging.getLogger("main")
|
||||||
|
|
||||||
|
GALAXY_SCALE_FACTOR = 2_000
|
||||||
|
FAMILY_LINK_POINTS = 366 # Equivalent to a leap year together in a club, because.
|
||||||
|
PICTURE_POINTS = 2 # Equivalent to two days as random members of a club.
|
||||||
|
CLUBS_POINTS = 1 # One day together as random members in a club is one point.
|
||||||
|
|
||||||
|
state = models.JSONField("current state")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_state() -> GalaxyDict:
|
||||||
|
"""
|
||||||
|
Compute JSON structure to send to 3d-force-graph: https://github.com/vasturiano/3d-force-graph/
|
||||||
|
"""
|
||||||
|
without_nickname = Concat(
|
||||||
|
F("owner__first_name"), Value(" "), F("owner__last_name")
|
||||||
|
)
|
||||||
|
with_nickname = Concat(
|
||||||
|
F("owner__first_name"),
|
||||||
|
Value(" "),
|
||||||
|
F("owner__last_name"),
|
||||||
|
Value(" ("),
|
||||||
|
F("owner__nick_name"),
|
||||||
|
Value(")"),
|
||||||
|
)
|
||||||
|
stars = GalaxyStar.objects.annotate(
|
||||||
|
owner_name=Case(
|
||||||
|
When(owner__nick_name=None, then=without_nickname),
|
||||||
|
default=with_nickname,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lanes = GalaxyLane.objects.annotate(
|
||||||
|
star1_owner=F("star1__owner__id"),
|
||||||
|
star2_owner=F("star2__owner__id"),
|
||||||
|
)
|
||||||
|
json = GalaxyDict(
|
||||||
|
nodes=[
|
||||||
|
StarDict(id=star.owner_id, name=star.owner_name, mass=star.mass)
|
||||||
|
for star in stars
|
||||||
|
],
|
||||||
|
links=[],
|
||||||
|
)
|
||||||
|
# Make bidirectional links
|
||||||
|
# TODO: see if this impacts performance with a big graph
|
||||||
|
for path in lanes:
|
||||||
|
json["links"].append(
|
||||||
|
{
|
||||||
|
"source": path.star1_owner,
|
||||||
|
"target": path.star2_owner,
|
||||||
|
"value": path.distance,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
json["links"].append(
|
||||||
|
{
|
||||||
|
"source": path.star2_owner,
|
||||||
|
"target": path.star1_owner,
|
||||||
|
"value": path.distance,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Galaxy.objects.all().delete()
|
||||||
|
Galaxy(state=json).save()
|
||||||
|
|
||||||
|
###################
|
||||||
|
# User self score #
|
||||||
|
###################
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_user_score(cls, user):
|
||||||
|
"""
|
||||||
|
This compute an individual score for each citizen. It will later be used by the graph algorithm to push
|
||||||
|
higher scores towards the center of the galaxy.
|
||||||
|
|
||||||
|
Idea: This could be added to the computation:
|
||||||
|
- Forum posts
|
||||||
|
- Picture count
|
||||||
|
- Counter consumption
|
||||||
|
- Barman time
|
||||||
|
- ...
|
||||||
|
"""
|
||||||
|
user_score = 1
|
||||||
|
user_score += cls.query_user_score(user)
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# Scale that value with some magic number to accommodate to typical data
|
||||||
|
# Really active galaxy citizen after 5 years typically have a score of about XXX
|
||||||
|
# Citizen that were seen regularly without taking much part in organizations typically have a score of about XXX
|
||||||
|
# Citizen that only went to a few events typically score about XXX
|
||||||
|
user_score = int(math.log2(user_score))
|
||||||
|
|
||||||
|
return user_score
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def query_user_score(cls, user):
|
||||||
|
score_query = (
|
||||||
|
User.objects.filter(id=user.id)
|
||||||
|
.annotate(
|
||||||
|
godchildren_count=Count("godchildren", distinct=True)
|
||||||
|
* cls.FAMILY_LINK_POINTS,
|
||||||
|
godfathers_count=Count("godfathers", distinct=True)
|
||||||
|
* cls.FAMILY_LINK_POINTS,
|
||||||
|
pictures_score=Count("pictures", distinct=True) * cls.PICTURE_POINTS,
|
||||||
|
clubs_score=Count("memberships", distinct=True) * cls.CLUBS_POINTS,
|
||||||
|
)
|
||||||
|
.aggregate(
|
||||||
|
score=models.Sum(
|
||||||
|
F("godchildren_count")
|
||||||
|
+ F("godfathers_count")
|
||||||
|
+ F("pictures_score")
|
||||||
|
+ F("clubs_score")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return score_query.get("score")
|
||||||
|
|
||||||
|
####################
|
||||||
|
# Inter-user score #
|
||||||
|
####################
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_users_score(cls, user1, user2):
|
||||||
|
family = cls.compute_users_family_score(user1, user2)
|
||||||
|
pictures = cls.compute_users_pictures_score(user1, user2)
|
||||||
|
clubs = cls.compute_users_clubs_score(user1, user2)
|
||||||
|
score = family + pictures + clubs
|
||||||
|
return score, family, pictures, clubs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_users_family_score(cls, user1, user2):
|
||||||
|
link_count = User.objects.filter(
|
||||||
|
Q(id=user1.id, godfathers=user2) | Q(id=user2.id, godfathers=user1)
|
||||||
|
).count()
|
||||||
|
if link_count:
|
||||||
|
cls.logger.debug(
|
||||||
|
f"\t\t- '{user1}' and '{user2}' have {link_count} direct family link"
|
||||||
|
)
|
||||||
|
return link_count * cls.FAMILY_LINK_POINTS
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_users_pictures_score(cls, user1, user2):
|
||||||
|
picture_count = (
|
||||||
|
Picture.objects.filter(people__user__in=(user1,))
|
||||||
|
.filter(people__user__in=(user2,))
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
if picture_count:
|
||||||
|
cls.logger.debug(
|
||||||
|
f"\t\t- '{user1}' was pictured with '{user2}' {picture_count} times"
|
||||||
|
)
|
||||||
|
return picture_count * cls.PICTURE_POINTS
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def compute_users_clubs_score(cls, user1, user2):
|
||||||
|
common_clubs = Club.objects.filter(members__in=user1.memberships.all()).filter(
|
||||||
|
members__in=user2.memberships.all()
|
||||||
|
)
|
||||||
|
user1_memberships = user1.memberships.filter(club__in=common_clubs)
|
||||||
|
user2_memberships = user2.memberships.filter(club__in=common_clubs)
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
for user1_membership in user1_memberships:
|
||||||
|
if user1_membership.end_date is None:
|
||||||
|
user1_membership.end_date = timezone.now().date()
|
||||||
|
query = Q( # start2 <= start1 <= end2
|
||||||
|
start_date__lte=user1_membership.start_date,
|
||||||
|
end_date__gte=user1_membership.start_date,
|
||||||
|
)
|
||||||
|
query |= Q( # start2 <= start1 <= now
|
||||||
|
start_date__lte=user1_membership.start_date, end_date=None
|
||||||
|
)
|
||||||
|
query |= Q( # start1 <= start2 <= end2
|
||||||
|
start_date__gte=user1_membership.start_date,
|
||||||
|
start_date__lte=user1_membership.end_date,
|
||||||
|
)
|
||||||
|
for user2_membership in user2_memberships.filter(
|
||||||
|
query, club=user1_membership.club
|
||||||
|
):
|
||||||
|
if user2_membership.end_date is None:
|
||||||
|
user2_membership.end_date = timezone.now().date()
|
||||||
|
latest_start = max(
|
||||||
|
user1_membership.start_date, user2_membership.start_date
|
||||||
|
)
|
||||||
|
earliest_end = min(user1_membership.end_date, user2_membership.end_date)
|
||||||
|
cls.logger.debug(
|
||||||
|
"\t\t- '%s' was with '%s' in %s starting on %s until %s (%s days)"
|
||||||
|
% (
|
||||||
|
user1,
|
||||||
|
user2,
|
||||||
|
user2_membership.club,
|
||||||
|
latest_start,
|
||||||
|
earliest_end,
|
||||||
|
(earliest_end - latest_start).days,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
score += cls.CLUBS_POINTS * (earliest_end - latest_start).days
|
||||||
|
return score
|
||||||
|
|
||||||
|
###################
|
||||||
|
# Rule the galaxy #
|
||||||
|
###################
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def rule(cls):
|
||||||
|
GalaxyStar.objects.all().delete()
|
||||||
|
# The following is a no-op thanks to cascading, but in case that changes in the future, better keep it anyway.
|
||||||
|
GalaxyLane.objects.all().delete()
|
||||||
|
rulable_users = (
|
||||||
|
User.objects.filter(subscriptions__isnull=False)
|
||||||
|
.filter(
|
||||||
|
Q(godchildren__isnull=False)
|
||||||
|
| Q(godfathers__isnull=False)
|
||||||
|
| Q(pictures__isnull=False)
|
||||||
|
| Q(memberships__isnull=False)
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
# force fetch of the whole query to make sure there won't
|
||||||
|
# be any more db hits
|
||||||
|
# this is memory expensive but prevents a lot of db hits, therefore
|
||||||
|
# is far more time efficient
|
||||||
|
rulable_users = list(rulable_users)
|
||||||
|
while len(rulable_users) > 0:
|
||||||
|
user1 = rulable_users.pop()
|
||||||
|
for user2 in rulable_users:
|
||||||
|
cls.logger.debug("")
|
||||||
|
cls.logger.debug(f"\t> Ruling '{user1}' against '{user2}'")
|
||||||
|
star1, _ = GalaxyStar.objects.get_or_create(owner=user1)
|
||||||
|
star2, _ = GalaxyStar.objects.get_or_create(owner=user2)
|
||||||
|
if star1.mass == 0:
|
||||||
|
star1.mass = cls.compute_user_score(user1)
|
||||||
|
star1.save()
|
||||||
|
if star2.mass == 0:
|
||||||
|
star2.mass = cls.compute_user_score(user2)
|
||||||
|
star2.save()
|
||||||
|
users_score, family, pictures, clubs = cls.compute_users_score(
|
||||||
|
user1, user2
|
||||||
|
)
|
||||||
|
if users_score > 0:
|
||||||
|
GalaxyLane(
|
||||||
|
star1=star1,
|
||||||
|
star2=star2,
|
||||||
|
distance=cls.scale_distance(users_score),
|
||||||
|
family=family,
|
||||||
|
pictures=pictures,
|
||||||
|
clubs=clubs,
|
||||||
|
).save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def scale_distance(cls, value):
|
||||||
|
# TODO: this will need adjustements with the real, typical data on Taiste
|
||||||
|
|
||||||
|
cls.logger.debug(f"\t\t> Score: {value}")
|
||||||
|
# Invert score to draw close users together
|
||||||
|
value = 1 / value # Cannot be 0
|
||||||
|
value += 2 # We use log2 just below and need to stay above 1
|
||||||
|
value = ( # Let's get something in the range ]0; log2(3)-1≈0.58[ that we can multiply later
|
||||||
|
math.log2(value) - 1
|
||||||
|
)
|
||||||
|
value *= ( # Scale that value with a magic number to accommodate to typical data
|
||||||
|
# Really close galaxy citizen after 5 years typically have a score of about XXX
|
||||||
|
# Citizen that were in the same year without being really friends typically have a score of about XXX
|
||||||
|
# Citizen that have met once or twice only have a couple of pictures together typically score about XXX
|
||||||
|
cls.GALAXY_SCALE_FACTOR
|
||||||
|
)
|
||||||
|
cls.logger.debug(f"\t\t> Scaled distance: {value}")
|
||||||
|
return int(value)
|
5
galaxy/static/galaxy/js/3d-force-graph.min.js
vendored
Normal file
2
galaxy/static/galaxy/js/d3-force-3d.min.js
vendored
Normal file
2
galaxy/static/galaxy/js/three-spritetext.min.js
vendored
Normal file
6
galaxy/static/galaxy/js/three.min.js
vendored
Normal file
108
galaxy/templates/galaxy/user.jinja
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
{% extends "core/base.jinja" %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% trans user_name=user.get_display_name() %}{{ user_name }}'s Galaxy{% endtrans %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if object.galaxy_user %}
|
||||||
|
<p><a onclick="focus_node(get_node_from_id({{ object.id }}))">Reset on {{ object.get_display_name() }}</a></p>
|
||||||
|
<p>Self score: {{ object.galaxy_user.mass }}</p>
|
||||||
|
<table style="width: initial;">
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Citizen</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Distance</th>
|
||||||
|
<th>Family</th>
|
||||||
|
<th>Pictures</th>
|
||||||
|
<th>Clubs</th>
|
||||||
|
</tr>
|
||||||
|
{% for lane in lanes %}
|
||||||
|
<tr>
|
||||||
|
<td><a onclick="focus_node(get_node_from_id({{ lane.other_star_id }}))">Locate</a></td>
|
||||||
|
<td><a href="{{ url("galaxy:user", user_id=lane.other_star_id) }}">{{ lane.other_star_name }}</a></td>
|
||||||
|
<td>{{ lane.other_star_mass }}</td>
|
||||||
|
<td>{{ lane.distance }}</td>
|
||||||
|
<td>{{ lane.family }}</td>
|
||||||
|
<td>{{ lane.pictures }}</td>
|
||||||
|
<td>{{ lane.clubs }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="3d-graph" style="margin: 1em;"></div>
|
||||||
|
{% else %}
|
||||||
|
<p>This citizen has not yet joined the galaxy</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
{{ super() }}
|
||||||
|
|
||||||
|
<script src="{{ static('galaxy/js/three.min.js') }}" defer></script>
|
||||||
|
<script src="{{ static('galaxy/js/three-spritetext.min.js') }}" defer></script>
|
||||||
|
<script src="{{ static('galaxy/js/3d-force-graph.min.js') }}" defer></script>
|
||||||
|
<script src="{{ static('galaxy/js/d3-force-3d.min.js') }}" defer></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var Graph;
|
||||||
|
|
||||||
|
function get_node_from_id(id) {
|
||||||
|
return Graph.graphData().nodes.find(n => n.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focus_node(node) {
|
||||||
|
// Aim at node from outside it
|
||||||
|
const distance = 200;
|
||||||
|
const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
|
||||||
|
|
||||||
|
const newPos = node.x || node.y || node.z
|
||||||
|
? { x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }
|
||||||
|
: { x: 0, y: 0, z: distance }; // special case if node is in (0,0,0)
|
||||||
|
|
||||||
|
Graph.cameraPosition(
|
||||||
|
newPos, // new position
|
||||||
|
node, // lookAt ({ x, y, z })
|
||||||
|
3000 // ms transition duration
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
Graph = ForceGraph3D();
|
||||||
|
Graph(document.getElementById('3d-graph'));
|
||||||
|
Graph
|
||||||
|
.jsonUrl('{{ url("galaxy:data") }}')
|
||||||
|
.width(1000)
|
||||||
|
.height(700)
|
||||||
|
.nodeAutoColorBy('id')
|
||||||
|
.nodeLabel(node => `${node.name}`)
|
||||||
|
.onNodeClick(node => focus_node(node))
|
||||||
|
.linkDirectionalParticles(3)
|
||||||
|
.linkDirectionalParticleWidth(0.8)
|
||||||
|
.linkDirectionalParticleSpeed(0.006)
|
||||||
|
.nodeThreeObject(node => {
|
||||||
|
const sprite = new SpriteText(node.name);
|
||||||
|
sprite.material.depthWrite = false; // make sprite background transparent
|
||||||
|
sprite.color = node.color;
|
||||||
|
sprite.textHeight = 5;
|
||||||
|
return sprite;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set distance between stars
|
||||||
|
Graph.d3Force('link').distance(link => link.value);
|
||||||
|
|
||||||
|
// Set high masses nearer the center of the galaxy
|
||||||
|
// TODO: quick and dirty strength computation, this will need tuning.
|
||||||
|
Graph.d3Force('positionX', d3.forceX().strength(node => { return 1 - (1 / node.mass); }));
|
||||||
|
Graph.d3Force('positionY', d3.forceY().strength(node => { return 1 - (1 / node.mass); }));
|
||||||
|
Graph.d3Force('positionZ', d3.forceZ().strength(node => { return 1 - (1 / node.mass); }));
|
||||||
|
|
||||||
|
// Focus current user
|
||||||
|
setTimeout(() => focus_node(get_node_from_id({{ object.id }})), 1000);
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
129
galaxy/tests.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2023
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
from core.models import User
|
||||||
|
from galaxy.models import Galaxy
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
call_command("populate")
|
||||||
|
self.root = User.objects.get(username="root")
|
||||||
|
self.skia = User.objects.get(username="skia")
|
||||||
|
self.sli = User.objects.get(username="sli")
|
||||||
|
self.krophil = User.objects.get(username="krophil")
|
||||||
|
self.richard = User.objects.get(username="rbatsbak")
|
||||||
|
self.subscriber = User.objects.get(username="subscriber")
|
||||||
|
self.public = User.objects.get(username="public")
|
||||||
|
self.com = User.objects.get(username="comunity")
|
||||||
|
|
||||||
|
def test_user_self_score(self):
|
||||||
|
with self.assertNumQueries(8):
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.root), 9)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.skia), 10)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.sli), 8)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.krophil), 2)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.richard), 10)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.subscriber), 8)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.public), 8)
|
||||||
|
self.assertEqual(Galaxy.compute_user_score(self.com), 1)
|
||||||
|
|
||||||
|
def test_users_score(self):
|
||||||
|
expected_scores = {
|
||||||
|
"krophil": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"public": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"rbatsbak": {"clubs": 100, "family": 0, "pictures": 0, "score": 100},
|
||||||
|
"subscriber": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
},
|
||||||
|
"public": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0}
|
||||||
|
},
|
||||||
|
"rbatsbak": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"public": {"clubs": 0, "family": 366, "pictures": 0, "score": 366},
|
||||||
|
"subscriber": {"clubs": 0, "family": 366, "pictures": 0, "score": 366},
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"krophil": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"public": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"rbatsbak": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"skia": {"clubs": 0, "family": 732, "pictures": 0, "score": 732},
|
||||||
|
"sli": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"subscriber": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
},
|
||||||
|
"skia": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"krophil": {"clubs": 114, "family": 0, "pictures": 2, "score": 116},
|
||||||
|
"public": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"rbatsbak": {"clubs": 100, "family": 0, "pictures": 0, "score": 100},
|
||||||
|
"sli": {"clubs": 0, "family": 366, "pictures": 4, "score": 370},
|
||||||
|
"subscriber": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
},
|
||||||
|
"sli": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"krophil": {"clubs": 17, "family": 0, "pictures": 2, "score": 19},
|
||||||
|
"public": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"rbatsbak": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"subscriber": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
},
|
||||||
|
"subscriber": {
|
||||||
|
"comunity": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
"public": {"clubs": 0, "family": 0, "pictures": 0, "score": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
computed_scores = {}
|
||||||
|
users = [
|
||||||
|
self.root,
|
||||||
|
self.skia,
|
||||||
|
self.sli,
|
||||||
|
self.krophil,
|
||||||
|
self.richard,
|
||||||
|
self.subscriber,
|
||||||
|
self.public,
|
||||||
|
self.com,
|
||||||
|
]
|
||||||
|
|
||||||
|
with self.assertNumQueries(100):
|
||||||
|
while len(users) > 0:
|
||||||
|
user1 = users.pop(0)
|
||||||
|
for user2 in users:
|
||||||
|
score, family, pictures, clubs = Galaxy.compute_users_score(
|
||||||
|
user1, user2
|
||||||
|
)
|
||||||
|
u1 = computed_scores.get(user1.username, {})
|
||||||
|
u1[user2.username] = {
|
||||||
|
"score": score,
|
||||||
|
"family": family,
|
||||||
|
"pictures": pictures,
|
||||||
|
"clubs": clubs,
|
||||||
|
}
|
||||||
|
computed_scores[user1.username] = u1
|
||||||
|
|
||||||
|
self.maxDiff = None # Yes, we want to see the diff if any
|
||||||
|
self.assertDictEqual(expected_scores, computed_scores)
|
40
galaxy/urls.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2023
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from galaxy.views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path(
|
||||||
|
"<int:user_id>/",
|
||||||
|
GalaxyUserView.as_view(),
|
||||||
|
name="user",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"data.json",
|
||||||
|
GalaxyDataView.as_view(),
|
||||||
|
name="data",
|
||||||
|
),
|
||||||
|
]
|
97
galaxy/views.py
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# -*- coding:utf-8 -*
|
||||||
|
#
|
||||||
|
# Copyright 2023
|
||||||
|
# - Skia <skia@hya.sk>
|
||||||
|
#
|
||||||
|
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||||
|
# http://ae.utbm.fr.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License a published by the Free Software
|
||||||
|
# Foundation; either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||||
|
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
|
from django.views.generic import DetailView, View
|
||||||
|
from django.http import JsonResponse
|
||||||
|
from django.db.models import Q, Case, F, When, Value
|
||||||
|
from django.db.models.functions import Concat
|
||||||
|
|
||||||
|
from core.views import (
|
||||||
|
CanViewMixin,
|
||||||
|
FormerSubscriberMixin,
|
||||||
|
)
|
||||||
|
from core.models import User
|
||||||
|
from core.views import UserTabsMixin
|
||||||
|
from galaxy.models import Galaxy, GalaxyLane
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyUserView(CanViewMixin, UserTabsMixin, DetailView):
|
||||||
|
model = User
|
||||||
|
pk_url_kwarg = "user_id"
|
||||||
|
template_name = "galaxy/user.jinja"
|
||||||
|
current_tab = "galaxy"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super(GalaxyUserView, self).get_queryset().select_related("galaxy_user")
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
kwargs = super(GalaxyUserView, self).get_context_data(**kwargs)
|
||||||
|
kwargs["lanes"] = (
|
||||||
|
GalaxyLane.objects.filter(
|
||||||
|
Q(star1=self.object.galaxy_user) | Q(star2=self.object.galaxy_user)
|
||||||
|
)
|
||||||
|
.order_by("distance")
|
||||||
|
.annotate(
|
||||||
|
other_star_id=Case(
|
||||||
|
When(star1=self.object.galaxy_user, then=F("star2__owner__id")),
|
||||||
|
default=F("star1__owner__id"),
|
||||||
|
),
|
||||||
|
other_star_mass=Case(
|
||||||
|
When(star1=self.object.galaxy_user, then=F("star2__mass")),
|
||||||
|
default=F("star1__mass"),
|
||||||
|
),
|
||||||
|
other_star_name=Case(
|
||||||
|
When(
|
||||||
|
star1=self.object.galaxy_user,
|
||||||
|
then=Case(
|
||||||
|
When(
|
||||||
|
star2__owner__nick_name=None,
|
||||||
|
then=Concat(
|
||||||
|
F("star2__owner__first_name"),
|
||||||
|
Value(" "),
|
||||||
|
F("star2__owner__last_name"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
default=Case(
|
||||||
|
When(
|
||||||
|
star1__owner__nick_name=None,
|
||||||
|
then=Concat(
|
||||||
|
F("star1__owner__first_name"),
|
||||||
|
Value(" "),
|
||||||
|
F("star1__owner__last_name"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.select_related("star1", "star2")
|
||||||
|
)
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
|
class GalaxyDataView(FormerSubscriberMixin, View):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return JsonResponse(Galaxy.objects.first().state)
|
@ -97,6 +97,7 @@ INSTALLED_APPS = (
|
|||||||
"trombi",
|
"trombi",
|
||||||
"matmat",
|
"matmat",
|
||||||
"pedagogy",
|
"pedagogy",
|
||||||
|
"galaxy",
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
@ -210,6 +211,29 @@ DATABASES = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"simple": {"format": "%(levelname)s %(message)s"},
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"log_to_stdout": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "simple",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"main": {
|
||||||
|
"handlers": ["log_to_stdout"],
|
||||||
|
"level": "INFO",
|
||||||
|
"propagate": True,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||||
|
|
||||||
@ -690,4 +714,8 @@ SITH_FRONT_DEP_VERSIONS = {
|
|||||||
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
|
"https://github.com/getsentry/sentry-javascript/": "4.0.6",
|
||||||
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
||||||
"https://github.com/alpinejs/alpine": "3.10.5",
|
"https://github.com/alpinejs/alpine": "3.10.5",
|
||||||
|
"https://github.com/mrdoob/three.js/": "r148",
|
||||||
|
"https://github.com/vasturiano/three-spritetext": "1.6.5",
|
||||||
|
"https://github.com/vasturiano/3d-force-graph/": "1.70.19",
|
||||||
|
"https://github.com/vasturiano/d3-force-3d": "3.0.3",
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,7 @@ urlpatterns = [
|
|||||||
path("api/v1/", include(("api.urls", "api"), namespace="api")),
|
path("api/v1/", include(("api.urls", "api"), namespace="api")),
|
||||||
path("election/", include(("election.urls", "election"), namespace="election")),
|
path("election/", include(("election.urls", "election"), namespace="election")),
|
||||||
path("forum/", include(("forum.urls", "forum"), namespace="forum")),
|
path("forum/", include(("forum.urls", "forum"), namespace="forum")),
|
||||||
|
path("galaxy/", include(("galaxy.urls", "galaxy"), namespace="galaxy")),
|
||||||
path("trombi/", include(("trombi.urls", "trombi"), namespace="trombi")),
|
path("trombi/", include(("trombi.urls", "trombi"), namespace="trombi")),
|
||||||
path("matmatronch/", include(("matmat.urls", "matmat"), namespace="matmat")),
|
path("matmatronch/", include(("matmat.urls", "matmat"), namespace="matmat")),
|
||||||
path("pedagogy/", include(("pedagogy.urls", "pedagogy"), namespace="pedagogy")),
|
path("pedagogy/", include(("pedagogy.urls", "pedagogy"), namespace="pedagogy")),
|
||||||
|