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
|
||||
# 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(
|
||||
"""^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 -*
|
||||
#
|
||||
# Copyright 2016,2017
|
||||
# - Skia <skia@libskia.so>
|
||||
# Copyright 2016,2017,2023
|
||||
# - Skia <skia@hya.sk>
|
||||
#
|
||||
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
|
||||
# http://ae.utbm.fr.
|
||||
@ -25,6 +25,7 @@
|
||||
import os
|
||||
from datetime import date, datetime, timedelta
|
||||
from io import StringIO, BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
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 forum.models import Forum, ForumTopic
|
||||
from pedagogy.models import UV
|
||||
from sas.models import Album, Picture, PeoplePictureRelation
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -71,9 +73,7 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
os.environ["DJANGO_COLORS"] = "nocolor"
|
||||
Site(id=4000, domain=settings.SITH_URL, name=settings.SITH_NAME).save()
|
||||
root_path = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
root_path = Path(__file__).parent.parent.parent.parent
|
||||
root_group, _ = Group.objects.get_or_create(name="Root")
|
||||
Group(name="Public").save()
|
||||
Group(name="Subscribers").save()
|
||||
@ -84,7 +84,7 @@ class Command(BaseCommand):
|
||||
Group(name="Banned from buying alcohol").save()
|
||||
Group(name="Banned from counters").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="Pedagogy admin").save()
|
||||
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.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(
|
||||
id=1,
|
||||
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
|
||||
]
|
||||
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:
|
||||
name = str(skia.id) + "_profile.jpg"
|
||||
skia_profile = SithFile(
|
||||
@ -233,7 +242,7 @@ Welcome to the wiki page!
|
||||
owner=skia,
|
||||
is_folder=False,
|
||||
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.save()
|
||||
@ -351,23 +360,48 @@ Welcome to the wiki page!
|
||||
]
|
||||
u.save()
|
||||
# Adding user Richard Batsbak
|
||||
r = User(
|
||||
richard = User(
|
||||
username="rbatsbak",
|
||||
last_name="Batsbak",
|
||||
first_name="Richard",
|
||||
email="richard@git.an",
|
||||
date_of_birth="1982-06-12",
|
||||
)
|
||||
r.set_password("plop")
|
||||
r.save()
|
||||
r.view_groups = [
|
||||
richard.set_password("plop")
|
||||
richard.save()
|
||||
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
|
||||
]
|
||||
r.save()
|
||||
richard.save()
|
||||
# Adding syntax help page
|
||||
p = Page(name="Aide_sur_la_syntaxe")
|
||||
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(
|
||||
page=p, title="Aide sur la syntaxe", author=skia, content=rm.read()
|
||||
).save()
|
||||
@ -442,7 +476,7 @@ Welcome to the wiki page!
|
||||
s.save()
|
||||
# Richard
|
||||
s = Subscription(
|
||||
member=User.objects.filter(pk=r.pk).first(),
|
||||
member=User.objects.filter(pk=richard.pk).first(),
|
||||
subscription_type=default_subscription,
|
||||
payment_method=settings.SITH_SUBSCRIPTION_PAYMENT_METHOD[0][0],
|
||||
)
|
||||
@ -514,7 +548,7 @@ Welcome to the wiki page!
|
||||
subscribers = Group.objects.get(name="Subscribers")
|
||||
old_subscribers = Group.objects.get(name="Old subscribers")
|
||||
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.save()
|
||||
c = ProductType(name="Cotisations")
|
||||
@ -825,7 +859,15 @@ Welcome to the wiki page!
|
||||
Group.objects.filter(name=settings.SITH_MAIN_MEMBERS_GROUP).first().id
|
||||
]
|
||||
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:
|
||||
name = str(sli.id) + "_profile.jpg"
|
||||
sli_profile = SithFile(
|
||||
@ -835,7 +877,7 @@ Welcome to the wiki page!
|
||||
owner=sli,
|
||||
is_folder=False,
|
||||
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.save()
|
||||
@ -851,7 +893,15 @@ Welcome to the wiki page!
|
||||
)
|
||||
krophil.set_password("plop")
|
||||
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:
|
||||
name = str(krophil.id) + "_profile.jpg"
|
||||
krophil_profile = SithFile(
|
||||
@ -861,7 +911,7 @@ Welcome to the wiki page!
|
||||
owner=krophil,
|
||||
is_folder=False,
|
||||
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.save()
|
||||
@ -1164,3 +1214,106 @@ Welcome to the wiki page!
|
||||
hours_THE=121,
|
||||
hours_TE=4,
|
||||
).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-dark-color: hsl(40, 68%, 35%);
|
||||
|
||||
|
||||
$primary-neutral-color: hsl(219.6, 20.8%, 50%);
|
||||
$primary-neutral-light-color: hsl(0, 0%, 94%);
|
||||
$primary-neutral-dark-color: hsl(210, 29%, 29%);
|
||||
@ -29,7 +28,7 @@ $pinktober: #ff5674;
|
||||
$pinktober-secondary: #8a2536;
|
||||
$pinktober-primary-text: white;
|
||||
$pinktober-bar-closed: $pinktober-secondary;
|
||||
$pinktober-bar-opened: #388E3C;
|
||||
$pinktober-bar-opened: #388e3c;
|
||||
|
||||
$shadow-color: rgb(223, 223, 223);
|
||||
|
||||
@ -49,7 +48,11 @@ body {
|
||||
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;
|
||||
text-decoration: none;
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
input,textarea[type=text],[type=number]{
|
||||
input,
|
||||
textarea[type="text"],
|
||||
[type="number"] {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
background-color: $background-button-color;
|
||||
@ -80,7 +92,7 @@ input,textarea[type=text],[type=number]{
|
||||
border-radius: 5px;
|
||||
max-width: 95%;
|
||||
}
|
||||
textarea{
|
||||
textarea {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
background-color: $background-button-color;
|
||||
@ -88,7 +100,7 @@ textarea{
|
||||
font-size: 1.2em;
|
||||
border-radius: 5px;
|
||||
}
|
||||
select{
|
||||
select {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
font-size: 1.2em;
|
||||
@ -143,7 +155,8 @@ a {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@ -162,7 +175,9 @@ a {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[x-cloak] { display: none !important; }
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*--------------------------------HEADER-------------------------------*/
|
||||
|
||||
@ -195,7 +210,6 @@ header {
|
||||
background-color: $primary-neutral-dark-color;
|
||||
border-radius: 0px 0px 10px 10px;
|
||||
|
||||
|
||||
#header_logo {
|
||||
background-color: $white-color;
|
||||
padding: 0.2em;
|
||||
@ -338,7 +352,8 @@ header {
|
||||
flex-wrap: wrap;
|
||||
width: 90%;
|
||||
margin: 1em auto;
|
||||
#alert_box, #info_box {
|
||||
#alert_box,
|
||||
#info_box {
|
||||
flex: 49%;
|
||||
font-size: 0.9em;
|
||||
margin: 0.2em;
|
||||
@ -369,7 +384,6 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#page {
|
||||
width: 90%;
|
||||
margin: 20px auto 0;
|
||||
@ -470,11 +484,11 @@ header {
|
||||
}
|
||||
|
||||
&.btn-grey.clickable:not(:disabled):hover {
|
||||
background-color:hsl(210,5%,30%);
|
||||
background-color: hsl(210, 5%, 30%);
|
||||
}
|
||||
}
|
||||
|
||||
/*--------------------------------CONTENT------------------------------*/
|
||||
/*--------------------------------CONTENT------------------------------*/
|
||||
#quick_notif {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
@ -485,7 +499,6 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#content {
|
||||
padding: 1em 1%;
|
||||
box-shadow: $shadow-color 0 5px 10px;
|
||||
@ -510,7 +523,7 @@ header {
|
||||
}
|
||||
|
||||
&.alert-red {
|
||||
background-color: rgb(255,245,245);
|
||||
background-color: rgb(255, 245, 245);
|
||||
color: #c53030;
|
||||
border: #fc8181 1px solid;
|
||||
}
|
||||
@ -554,7 +567,7 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
/*---------------------------------NEWS--------------------------------*/
|
||||
/*---------------------------------NEWS--------------------------------*/
|
||||
#news {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@ -586,20 +599,23 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $small-devices){
|
||||
#left_column, #right_column {
|
||||
@media screen and (max-width: $small-devices) {
|
||||
#left_column,
|
||||
#right_column {
|
||||
flex: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* AGENDA/BIRTHDAYS */
|
||||
#agenda,#birthdays {
|
||||
/* AGENDA/BIRTHDAYS */
|
||||
#agenda,
|
||||
#birthdays {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: white;
|
||||
font-size: 70%;
|
||||
margin-bottom: 1em;
|
||||
#agenda_title,#birthdays_title {
|
||||
#agenda_title,
|
||||
#birthdays_title {
|
||||
margin: 0em;
|
||||
border-radius: 5px 5px 0 0;
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
@ -615,7 +631,8 @@ header {
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
height: 20em;
|
||||
}
|
||||
#agenda_content,#birthdays_content {
|
||||
#agenda_content,
|
||||
#birthdays_content {
|
||||
.agenda_item {
|
||||
padding: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
@ -636,7 +653,7 @@ header {
|
||||
margin: 0em;
|
||||
list-style-type: none;
|
||||
font-weight: bold;
|
||||
>li {
|
||||
> li {
|
||||
padding: 0.5em;
|
||||
&:nth-child(even) {
|
||||
background: $secondary-neutral-light-color;
|
||||
@ -652,9 +669,9 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* END AGENDA/BIRTHDAYS */
|
||||
/* END AGENDA/BIRTHDAYS */
|
||||
|
||||
/* EVENTS TODAY AND NEXT FEW DAYS */
|
||||
/* EVENTS TODAY AND NEXT FEW DAYS */
|
||||
.news_events_group {
|
||||
box-shadow: $shadow-color 1px 1px 1px;
|
||||
margin-left: 1em;
|
||||
@ -733,9 +750,9 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* END EVENTS TODAY AND NEXT FEW DAYS */
|
||||
/* END EVENTS TODAY AND NEXT FEW DAYS */
|
||||
|
||||
/* COMING SOON */
|
||||
/* COMING SOON */
|
||||
.news_coming_soon {
|
||||
display: list-item;
|
||||
list-style-type: square;
|
||||
@ -750,9 +767,9 @@ header {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
/* END COMING SOON */
|
||||
/* END COMING SOON */
|
||||
|
||||
/* NOTICES */
|
||||
/* NOTICES */
|
||||
.news_notice {
|
||||
margin: 0em 0em 1em 1em;
|
||||
padding: 0.4em;
|
||||
@ -767,9 +784,9 @@ header {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
/* END NOTICES */
|
||||
/* END NOTICES */
|
||||
|
||||
/* CALLS */
|
||||
/* CALLS */
|
||||
.news_call {
|
||||
margin: 0em 0em 1em 1em;
|
||||
padding: 0.4em;
|
||||
@ -787,7 +804,7 @@ header {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
/* END CALLS */
|
||||
/* END CALLS */
|
||||
|
||||
.news_empty {
|
||||
margin-left: 1em;
|
||||
@ -798,7 +815,7 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices){
|
||||
@media screen and (max-width: $small-devices) {
|
||||
#page {
|
||||
width: 98%;
|
||||
}
|
||||
@ -867,29 +884,32 @@ header {
|
||||
|
||||
/*---------------------------POSTERS----------------------------*/
|
||||
|
||||
#poster_list, #screen_list, #poster_edit, #screen_edit{
|
||||
#poster_list,
|
||||
#screen_list,
|
||||
#poster_edit,
|
||||
#screen_edit {
|
||||
position: relative;
|
||||
#title{
|
||||
#title {
|
||||
position: relative;
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
border-bottom: 2px solid black;
|
||||
h3{
|
||||
h3 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
#links{
|
||||
#links {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
bottom: 5px;
|
||||
&.left{
|
||||
&.left {
|
||||
left: 0;
|
||||
}
|
||||
&.right{
|
||||
&.right {
|
||||
right: 0;
|
||||
}
|
||||
.link{
|
||||
.link {
|
||||
padding: 5px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
@ -897,26 +917,29 @@ header {
|
||||
border-radius: 20px;
|
||||
background-color: hsl(40, 100%, 50%);
|
||||
color: black;
|
||||
&:hover{
|
||||
&:hover {
|
||||
color: black;
|
||||
background-color: hsl(40, 58%, 50%);
|
||||
}
|
||||
&.delete{
|
||||
&.delete {
|
||||
background-color: hsl(0, 100%, 40%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#posters, #screens{
|
||||
#posters,
|
||||
#screens {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
#no-posters, #no-screens{
|
||||
#no-posters,
|
||||
#no-screens {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.poster, .screen{
|
||||
.poster,
|
||||
.screen {
|
||||
min-width: 10%;
|
||||
max-width: 20%;
|
||||
display: flex;
|
||||
@ -926,28 +949,28 @@ header {
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: lightgrey;
|
||||
*{
|
||||
* {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.name{
|
||||
.name {
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
}
|
||||
.image{
|
||||
.image {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
img{
|
||||
img {
|
||||
max-height: 20vw;
|
||||
max-width: 100%;
|
||||
}
|
||||
&:hover{
|
||||
&::before{
|
||||
&:hover {
|
||||
&::before {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@ -964,11 +987,11 @@ header {
|
||||
}
|
||||
}
|
||||
}
|
||||
.dates{
|
||||
.dates {
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid whitesmoke;
|
||||
*{
|
||||
* {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@ -976,29 +999,32 @@ header {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.begin, .end{
|
||||
.begin,
|
||||
.end {
|
||||
width: 48%;
|
||||
}
|
||||
.begin{
|
||||
.begin {
|
||||
border-right: 1px solid whitesmoke;
|
||||
padding-right: 2%;
|
||||
}
|
||||
}
|
||||
.edit, .moderate, .slideshow{
|
||||
.edit,
|
||||
.moderate,
|
||||
.slideshow {
|
||||
padding: 5px;
|
||||
border-radius: 20px;
|
||||
background-color: hsl(40, 100%, 50%);
|
||||
color: black;
|
||||
&:hover{
|
||||
&:hover {
|
||||
color: black;
|
||||
background-color: hsl(40, 58%, 50%);
|
||||
}
|
||||
&:nth-child(2n){
|
||||
&:nth-child(2n) {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
.tooltip{
|
||||
.tooltip {
|
||||
visibility: hidden;
|
||||
width: 120px;
|
||||
background-color: hsl(210, 20%, 98%);
|
||||
@ -1008,25 +1034,24 @@ header {
|
||||
border-radius: 6px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
ul{
|
||||
ul {
|
||||
margin-left: 0;
|
||||
display: inline-block;
|
||||
li{
|
||||
li {
|
||||
display: list-item;
|
||||
list-style-type: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.not_moderated
|
||||
{
|
||||
&.not_moderated {
|
||||
border: 1px solid red;
|
||||
}
|
||||
&:hover .tooltip{
|
||||
&:hover .tooltip {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
#view{
|
||||
#view {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
@ -1039,10 +1064,10 @@ header {
|
||||
visibility: hidden;
|
||||
background-color: rgba(10, 10, 10, 0.9);
|
||||
overflow: hidden;
|
||||
&.active{
|
||||
&.active {
|
||||
visibility: visible;
|
||||
}
|
||||
#placeholder{
|
||||
#placeholder {
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
@ -1050,7 +1075,7 @@ header {
|
||||
align-items: center;
|
||||
top: 0;
|
||||
left: 0;
|
||||
img{
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
@ -1058,7 +1083,6 @@ header {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*---------------------------ACCOUNTING----------------------------*/
|
||||
#accounting {
|
||||
.journal-table {
|
||||
@ -1084,7 +1108,12 @@ header {
|
||||
}
|
||||
|
||||
/*-----------------------------GENERAL-----------------------------*/
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: bold;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
@ -1119,12 +1148,15 @@ h6 {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
p, pre {
|
||||
p,
|
||||
pre {
|
||||
margin-top: 0.8em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
ul, ol, dl {
|
||||
ul,
|
||||
ol,
|
||||
dl {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
margin-left: 25px;
|
||||
@ -1167,7 +1199,11 @@ blockquote h5:first-child {
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
font-size: 0.90em;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
td {
|
||||
@ -1207,11 +1243,13 @@ sub {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
i, em {
|
||||
i,
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@ -1220,7 +1258,8 @@ i, em {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
u, .underline {
|
||||
u,
|
||||
.underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@ -1273,7 +1312,8 @@ u, .underline {
|
||||
#user_profile_infos_items {
|
||||
margin-top: 3em;
|
||||
}
|
||||
.user_profile_infos_item, .user_profile_infos_item_value {
|
||||
.user_profile_infos_item,
|
||||
.user_profile_infos_item_value {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
width: 49%;
|
||||
@ -1293,7 +1333,8 @@ u, .underline {
|
||||
text-align: right;
|
||||
color: grey;
|
||||
font-style: italic;
|
||||
&:after, &:before {
|
||||
&:after,
|
||||
&:before {
|
||||
content: "\201C";
|
||||
vertical-align: middle;
|
||||
}
|
||||
@ -1329,8 +1370,9 @@ u, .underline {
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: $small-devices){
|
||||
#user_profile_infos, #user_profile_pictures {
|
||||
@media screen and (max-width: $small-devices) {
|
||||
#user_profile_infos,
|
||||
#user_profile_pictures {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
}
|
||||
@ -1439,7 +1481,6 @@ u, .underline {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*---------------------------------PAGE--------------------------------*/
|
||||
|
||||
.page_content {
|
||||
@ -1510,7 +1551,7 @@ textarea {
|
||||
.last_message span {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow:hidden;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
@ -1720,7 +1761,8 @@ label {
|
||||
}
|
||||
}
|
||||
|
||||
#cash_summary_form label, .inline {
|
||||
#cash_summary_form label,
|
||||
.inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@ -1775,13 +1817,19 @@ label {
|
||||
|
||||
/*--------------------------------JQuery-------------------------------*/
|
||||
|
||||
.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header
|
||||
.ui-state-active, a.ui-button:active, .ui-button:active,
|
||||
.ui-state-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 {
|
||||
background: $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 {
|
||||
border-radius: 0;
|
||||
}
|
||||
@ -1795,7 +1843,6 @@ label {
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* --------------------------------------pedagogy-----------------------------------*/
|
||||
@ -1808,7 +1855,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
|
||||
.pedagogy {
|
||||
&.star-not-checked {
|
||||
color : #f7f7f7;
|
||||
color: #f7f7f7;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -1819,10 +1866,10 @@ $pedagogy-white-text: #f0f0f0;
|
||||
}
|
||||
|
||||
&.grade-without-star {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
&.star-not-checked {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
@ -1833,7 +1880,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices){
|
||||
@media screen and (max-width: $small-devices) {
|
||||
&.grade-without-star {
|
||||
display: block;
|
||||
}
|
||||
@ -1851,11 +1898,9 @@ $pedagogy-white-text: #f0f0f0;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#search_form {
|
||||
|
||||
.search-form-container {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
@ -1879,13 +1924,13 @@ $pedagogy-white-text: #f0f0f0;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input search-bar-button";
|
||||
|
||||
@media screen and (max-width: $medium-devices){
|
||||
@media screen and (max-width: $medium-devices) {
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input search-bar-button";
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-devices){
|
||||
@media screen and (max-width: $small-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas: "search-bar-input";
|
||||
@ -1921,8 +1966,9 @@ $pedagogy-white-text: #f0f0f0;
|
||||
grid-area: radio-semester;
|
||||
}
|
||||
|
||||
.radio-guide input[type="radio"],input[type="checkbox"] {
|
||||
display:none;
|
||||
.radio-guide input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
.radio-guide {
|
||||
margin-top: 10px;
|
||||
@ -1956,7 +2002,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
"hours-cm hours-td hours-tp hours-te hours-the"
|
||||
"department credit-type semester . ." ;
|
||||
"department credit-type semester . .";
|
||||
}
|
||||
|
||||
.department {
|
||||
@ -2005,7 +2051,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
grid-template-rows: 100%;
|
||||
grid-template-areas: "stars comment";
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 100%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
@ -2033,7 +2079,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
color: $pedagogy-white-text;
|
||||
clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
clip-path: none;
|
||||
}
|
||||
}
|
||||
@ -2060,7 +2106,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
"grade grade-stars uv-infos"
|
||||
". . uv-infos";
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 50% 50%;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-areas:
|
||||
@ -2104,14 +2150,14 @@ $pedagogy-white-text: #f0f0f0;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 10px;
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-template-areas:
|
||||
"grade-block"
|
||||
"comment"
|
||||
"info"
|
||||
"comment-end-bar"
|
||||
"comment-end-bar";
|
||||
}
|
||||
|
||||
.grade-block {
|
||||
@ -2131,10 +2177,10 @@ $pedagogy-white-text: #f0f0f0;
|
||||
|
||||
background-color: $pedagogy-blue;
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: 50% auto;
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:"grade-type grade-stars";
|
||||
grid-template-areas: "grade-type grade-stars";
|
||||
width: auto;
|
||||
clip-path: none;
|
||||
align-content: space-evenly;
|
||||
@ -2171,7 +2217,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
"anchor"
|
||||
"markdown";
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
@ -2199,7 +2245,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
grid-area: info;
|
||||
padding-bottom: 10px;
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
border-left: solid;
|
||||
border-right: solid;
|
||||
border-color: $pedagogy-blue;
|
||||
@ -2227,7 +2273,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
background-color: $pedagogy-blue;
|
||||
margin-top: -1px;
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto auto auto;
|
||||
grid-template-areas:
|
||||
@ -2247,7 +2293,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
background-color: $pedagogy-orange;
|
||||
clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
clip-path: none;
|
||||
padding: 0;
|
||||
padding-bottom: 7px;
|
||||
@ -2261,14 +2307,13 @@ $pedagogy-white-text: #f0f0f0;
|
||||
a:hover {
|
||||
color: $pedagogy-hover-blue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.date {
|
||||
grid-area: date;
|
||||
color: $pedagogy-white-text;
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
}
|
||||
@ -2287,7 +2332,7 @@ $pedagogy-white-text: #f0f0f0;
|
||||
color: $pedagogy-hover-blue;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $large-devices){
|
||||
@media screen and (max-width: $large-devices) {
|
||||
text-align: center;
|
||||
justify-self: inherit;
|
||||
padding-bottom: 7px;
|
||||
|
@ -2,7 +2,12 @@
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h3>{% trans %}404, Not Found{% endtrans %}</h3>
|
||||
<div id="page">
|
||||
<h3>{% trans %}404, Not Found{% endtrans %}</h3>
|
||||
<p class="alert alert-red">
|
||||
{{ exception }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
@ -72,7 +72,9 @@ def forbidden(request, exception):
|
||||
|
||||
|
||||
def not_found(request, exception):
|
||||
return HttpResponseNotFound(render(request, "core/404.jinja"))
|
||||
return HttpResponseNotFound(
|
||||
render(request, "core/404.jinja", context={"exception": exception})
|
||||
)
|
||||
|
||||
|
||||
def internal_servor_error(request):
|
||||
|
@ -189,78 +189,72 @@ class UserTabsMixin(TabedViewMixin):
|
||||
return self.object.get_display_name()
|
||||
|
||||
def get_list_of_tabs(self):
|
||||
tab_list = []
|
||||
tab_list.append(
|
||||
user: User = self.object
|
||||
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",
|
||||
"name": _("Infos"),
|
||||
}
|
||||
)
|
||||
tab_list.append(
|
||||
},
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_godfathers", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_godfathers", kwargs={"user_id": user.id}),
|
||||
"slug": "godfathers",
|
||||
"name": _("Family"),
|
||||
}
|
||||
)
|
||||
tab_list.append(
|
||||
},
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_pictures", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_pictures", kwargs={"user_id": user.id}),
|
||||
"slug": "pictures",
|
||||
"name": _("Pictures"),
|
||||
},
|
||||
]
|
||||
if (
|
||||
False and self.request.user.was_subscribed
|
||||
): # TODO: display galaxy once it's ready
|
||||
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(
|
||||
{"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(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_edit", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_edit", kwargs={"user_id": user.id}),
|
||||
"slug": "edit",
|
||||
"name": _("Edit"),
|
||||
}
|
||||
)
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_prefs", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_prefs", kwargs={"user_id": user.id}),
|
||||
"slug": "prefs",
|
||||
"name": _("Preferences"),
|
||||
}
|
||||
)
|
||||
if self.request.user.can_view(self.object):
|
||||
if self.request.user.can_view(user):
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_clubs", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_clubs", kwargs={"user_id": user.id}),
|
||||
"slug": "clubs",
|
||||
"name": _("Clubs"),
|
||||
}
|
||||
)
|
||||
if self.request.user.is_owner(self.object):
|
||||
if self.request.user.is_owner(user):
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_groups", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_groups", kwargs={"user_id": user.id}),
|
||||
"slug": "groups",
|
||||
"name": _("Groups"),
|
||||
}
|
||||
)
|
||||
try:
|
||||
if self.object.customer and (
|
||||
self.object == self.request.user
|
||||
if user.customer and (
|
||||
user == self.request.user
|
||||
or self.request.user.is_in_group(
|
||||
settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
|
||||
)
|
||||
@ -271,9 +265,7 @@ class UserTabsMixin(TabedViewMixin):
|
||||
):
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_stats", kwargs={"user_id": self.object.id}
|
||||
),
|
||||
"url": reverse("core:user_stats", kwargs={"user_id": user.id}),
|
||||
"slug": "stats",
|
||||
"name": _("Stats"),
|
||||
}
|
||||
@ -281,10 +273,10 @@ class UserTabsMixin(TabedViewMixin):
|
||||
tab_list.append(
|
||||
{
|
||||
"url": reverse(
|
||||
"core:user_account", kwargs={"user_id": self.object.id}
|
||||
"core:user_account", kwargs={"user_id": user.id}
|
||||
),
|
||||
"slug": "account",
|
||||
"name": _("Account") + " (%s €)" % self.object.customer.amount,
|
||||
"name": _("Account") + " (%s €)" % user.customer.amount,
|
||||
}
|
||||
)
|
||||
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-03-02 10:07
|
||||
|
||||
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="star owner",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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 star 1",
|
||||
),
|
||||
),
|
||||
(
|
||||
"star2",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="lanes2",
|
||||
to="galaxy.galaxystar",
|
||||
verbose_name="galaxy star 2",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
galaxy/migrations/__init__.py
Normal file
379
galaxy/models.py
Normal file
@ -0,0 +1,379 @@
|
||||
# -*- 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 typing import Tuple
|
||||
|
||||
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=_("star owner"),
|
||||
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 star 1"),
|
||||
related_name="lanes1",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
star2 = models.ForeignKey(
|
||||
GalaxyStar,
|
||||
verbose_name=_("galaxy star 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() -> None:
|
||||
"""
|
||||
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) -> int:
|
||||
"""
|
||||
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) -> int:
|
||||
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) -> Tuple[int, int, int, int]:
|
||||
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) -> int:
|
||||
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) -> int:
|
||||
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) -> int:
|
||||
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) -> None:
|
||||
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) -> int:
|
||||
# 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 %}
|
||||
|
||||
|
149
galaxy/tests.py
Normal file
@ -0,0 +1,149 @@
|
||||
# -*- 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)
|
||||
|
||||
def test_page_is_citizen(self):
|
||||
Galaxy.rule()
|
||||
self.client.login(username="root", password="plop")
|
||||
response = self.client.get("/galaxy/1/")
|
||||
self.assertContains(
|
||||
response,
|
||||
'<a onclick="focus_node(get_node_from_id(8))">Locate</a>',
|
||||
status_code=200,
|
||||
)
|
||||
|
||||
def test_page_not_citizen(self):
|
||||
Galaxy.rule()
|
||||
self.client.login(username="root", password="plop")
|
||||
response = self.client.get("/galaxy/2/")
|
||||
self.assertContains(
|
||||
response,
|
||||
"Ce citoyen n'a pas encore rejoint la galaxie",
|
||||
status_code=404,
|
||||
)
|
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",
|
||||
),
|
||||
]
|
104
galaxy/views.py
Normal file
@ -0,0 +1,104 @@
|
||||
# -*- 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, Http404
|
||||
from django.db.models import Q, Case, F, When, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
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_object(self, *args, **kwargs):
|
||||
user: User = super(GalaxyUserView, self).get_object(*args, **kwargs)
|
||||
if not hasattr(user, "galaxy_user"):
|
||||
raise Http404(_("This citizen has not yet joined the galaxy"))
|
||||
return user
|
||||
|
||||
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",
|
||||
"matmat",
|
||||
"pedagogy",
|
||||
"galaxy",
|
||||
)
|
||||
|
||||
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
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
|
||||
@ -691,4 +715,8 @@ SITH_FRONT_DEP_VERSIONS = {
|
||||
"https://github.com/jhuckaby/webcamjs/": "1.0.0",
|
||||
"https://github.com/vuejs/vue-next": "3.2.18",
|
||||
"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("election/", include(("election.urls", "election"), namespace="election")),
|
||||
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("matmatronch/", include(("matmat.urls", "matmat"), namespace="matmat")),
|
||||
path("pedagogy/", include(("pedagogy.urls", "pedagogy"), namespace="pedagogy")),
|
||||
|