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;
 | 
				
			||||||
@@ -80,7 +92,7 @@ input,textarea[type=text],[type=number]{
 | 
				
			|||||||
  border-radius: 5px;
 | 
					  border-radius: 5px;
 | 
				
			||||||
  max-width: 95%;
 | 
					  max-width: 95%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
textarea{
 | 
					textarea {
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  background-color: $background-button-color;
 | 
					  background-color: $background-button-color;
 | 
				
			||||||
@@ -88,7 +100,7 @@ textarea{
 | 
				
			|||||||
  font-size: 1.2em;
 | 
					  font-size: 1.2em;
 | 
				
			||||||
  border-radius: 5px;
 | 
					  border-radius: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
select{
 | 
					select {
 | 
				
			||||||
  border: none;
 | 
					  border: none;
 | 
				
			||||||
  text-decoration: none;
 | 
					  text-decoration: none;
 | 
				
			||||||
  font-size: 1.2em;
 | 
					  font-size: 1.2em;
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
@@ -470,11 +484,11 @@ header {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.btn-grey.clickable:not(:disabled):hover {
 | 
					    &.btn-grey.clickable:not(:disabled):hover {
 | 
				
			||||||
      background-color:hsl(210,5%,30%);
 | 
					      background-color: hsl(210, 5%, 30%);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*--------------------------------CONTENT------------------------------*/
 | 
					  /*--------------------------------CONTENT------------------------------*/
 | 
				
			||||||
  #quick_notif {
 | 
					  #quick_notif {
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    margin: 0 auto;
 | 
					    margin: 0 auto;
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
@@ -510,7 +523,7 @@ header {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.alert-red {
 | 
					    &.alert-red {
 | 
				
			||||||
      background-color: rgb(255,245,245);
 | 
					      background-color: rgb(255, 245, 245);
 | 
				
			||||||
      color: #c53030;
 | 
					      color: #c53030;
 | 
				
			||||||
      border: #fc8181 1px solid;
 | 
					      border: #fc8181 1px solid;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -554,7 +567,7 @@ header {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*---------------------------------NEWS--------------------------------*/
 | 
					  /*---------------------------------NEWS--------------------------------*/
 | 
				
			||||||
  #news {
 | 
					  #news {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
@@ -586,20 +599,23 @@ 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;
 | 
				
			||||||
@@ -636,7 +653,7 @@ header {
 | 
				
			|||||||
          margin: 0em;
 | 
					          margin: 0em;
 | 
				
			||||||
          list-style-type: none;
 | 
					          list-style-type: none;
 | 
				
			||||||
          font-weight: bold;
 | 
					          font-weight: bold;
 | 
				
			||||||
          >li {
 | 
					          > li {
 | 
				
			||||||
            padding: 0.5em;
 | 
					            padding: 0.5em;
 | 
				
			||||||
            &:nth-child(even) {
 | 
					            &:nth-child(even) {
 | 
				
			||||||
              background: $secondary-neutral-light-color;
 | 
					              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 {
 | 
					    .news_events_group {
 | 
				
			||||||
      box-shadow: $shadow-color 1px 1px 1px;
 | 
					      box-shadow: $shadow-color 1px 1px 1px;
 | 
				
			||||||
      margin-left: 1em;
 | 
					      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 {
 | 
					    .news_coming_soon {
 | 
				
			||||||
      display: list-item;
 | 
					      display: list-item;
 | 
				
			||||||
      list-style-type: square;
 | 
					      list-style-type: square;
 | 
				
			||||||
@@ -750,9 +767,9 @@ header {
 | 
				
			|||||||
        font-size: 0.9em;
 | 
					        font-size: 0.9em;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
/* END COMING SOON */
 | 
					    /* END COMING SOON */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* NOTICES */
 | 
					    /* NOTICES */
 | 
				
			||||||
    .news_notice {
 | 
					    .news_notice {
 | 
				
			||||||
      margin: 0em 0em 1em 1em;
 | 
					      margin: 0em 0em 1em 1em;
 | 
				
			||||||
      padding: 0.4em;
 | 
					      padding: 0.4em;
 | 
				
			||||||
@@ -767,9 +784,9 @@ header {
 | 
				
			|||||||
        margin-left: 1em;
 | 
					        margin-left: 1em;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
/* END NOTICES */
 | 
					    /* END NOTICES */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* CALLS */
 | 
					    /* CALLS */
 | 
				
			||||||
    .news_call {
 | 
					    .news_call {
 | 
				
			||||||
      margin: 0em 0em 1em 1em;
 | 
					      margin: 0em 0em 1em 1em;
 | 
				
			||||||
      padding: 0.4em;
 | 
					      padding: 0.4em;
 | 
				
			||||||
@@ -787,7 +804,7 @@ header {
 | 
				
			|||||||
        margin-left: 1em;
 | 
					        margin-left: 1em;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
/* END CALLS */
 | 
					    /* END CALLS */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .news_empty {
 | 
					    .news_empty {
 | 
				
			||||||
      margin-left: 1em;
 | 
					      margin-left: 1em;
 | 
				
			||||||
@@ -798,7 +815,7 @@ header {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (max-width: $small-devices){
 | 
					@media screen and (max-width: $small-devices) {
 | 
				
			||||||
  #page {
 | 
					  #page {
 | 
				
			||||||
    width: 98%;
 | 
					    width: 98%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -867,29 +884,32 @@ 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;
 | 
				
			||||||
    padding: 10px;
 | 
					    padding: 10px;
 | 
				
			||||||
    margin: 10px;
 | 
					    margin: 10px;
 | 
				
			||||||
    border-bottom: 2px solid black;
 | 
					    border-bottom: 2px solid black;
 | 
				
			||||||
        h3{
 | 
					    h3 {
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      justify-content: center;
 | 
					      justify-content: center;
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
        #links{
 | 
					    #links {
 | 
				
			||||||
      position: absolute;
 | 
					      position: absolute;
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      bottom: 5px;
 | 
					      bottom: 5px;
 | 
				
			||||||
            &.left{
 | 
					      &.left {
 | 
				
			||||||
        left: 0;
 | 
					        left: 0;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            &.right{
 | 
					      &.right {
 | 
				
			||||||
        right: 0;
 | 
					        right: 0;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            .link{
 | 
					      .link {
 | 
				
			||||||
        padding: 5px;
 | 
					        padding: 5px;
 | 
				
			||||||
        padding-left: 20px;
 | 
					        padding-left: 20px;
 | 
				
			||||||
        padding-right: 20px;
 | 
					        padding-right: 20px;
 | 
				
			||||||
@@ -897,26 +917,29 @@ header {
 | 
				
			|||||||
        border-radius: 20px;
 | 
					        border-radius: 20px;
 | 
				
			||||||
        background-color: hsl(40, 100%, 50%);
 | 
					        background-color: hsl(40, 100%, 50%);
 | 
				
			||||||
        color: black;
 | 
					        color: black;
 | 
				
			||||||
                &:hover{
 | 
					        &:hover {
 | 
				
			||||||
          color: black;
 | 
					          color: black;
 | 
				
			||||||
          background-color: hsl(40, 58%, 50%);
 | 
					          background-color: hsl(40, 58%, 50%);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                &.delete{
 | 
					        &.delete {
 | 
				
			||||||
          background-color: hsl(0, 100%, 40%);
 | 
					          background-color: hsl(0, 100%, 40%);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    #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;
 | 
				
			||||||
@@ -926,28 +949,28 @@ header {
 | 
				
			|||||||
      border-radius: 4px;
 | 
					      border-radius: 4px;
 | 
				
			||||||
      padding: 10px;
 | 
					      padding: 10px;
 | 
				
			||||||
      background-color: lightgrey;
 | 
					      background-color: lightgrey;
 | 
				
			||||||
            *{
 | 
					      * {
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        justify-content: center;
 | 
					        justify-content: center;
 | 
				
			||||||
        align-items: center;
 | 
					        align-items: center;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            .name{
 | 
					      .name {
 | 
				
			||||||
        padding-bottom: 5px;
 | 
					        padding-bottom: 5px;
 | 
				
			||||||
        margin-bottom: 5px;
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
        border-bottom: 1px solid whitesmoke;
 | 
					        border-bottom: 1px solid whitesmoke;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            .image{
 | 
					      .image {
 | 
				
			||||||
        flex-grow: 1;
 | 
					        flex-grow: 1;
 | 
				
			||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
        padding-bottom: 5px;
 | 
					        padding-bottom: 5px;
 | 
				
			||||||
        margin-bottom: 5px;
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
        border-bottom: 1px solid whitesmoke;
 | 
					        border-bottom: 1px solid whitesmoke;
 | 
				
			||||||
                img{
 | 
					        img {
 | 
				
			||||||
          max-height: 20vw;
 | 
					          max-height: 20vw;
 | 
				
			||||||
          max-width: 100%;
 | 
					          max-width: 100%;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                &:hover{
 | 
					        &:hover {
 | 
				
			||||||
                    &::before{
 | 
					          &::before {
 | 
				
			||||||
            position: absolute;
 | 
					            position: absolute;
 | 
				
			||||||
            width: 100%;
 | 
					            width: 100%;
 | 
				
			||||||
            height: 100%;
 | 
					            height: 100%;
 | 
				
			||||||
@@ -964,11 +987,11 @@ header {
 | 
				
			|||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            .dates{
 | 
					      .dates {
 | 
				
			||||||
        padding-bottom: 5px;
 | 
					        padding-bottom: 5px;
 | 
				
			||||||
        margin-bottom: 5px;
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
        border-bottom: 1px solid whitesmoke;
 | 
					        border-bottom: 1px solid whitesmoke;
 | 
				
			||||||
                *{
 | 
					        * {
 | 
				
			||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          justify-content: center;
 | 
					          justify-content: center;
 | 
				
			||||||
          align-items: center;
 | 
					          align-items: center;
 | 
				
			||||||
@@ -976,29 +999,32 @@ header {
 | 
				
			|||||||
          margin-left: 5px;
 | 
					          margin-left: 5px;
 | 
				
			||||||
          margin-right: 5px;
 | 
					          margin-right: 5px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                .begin, .end{
 | 
					        .begin,
 | 
				
			||||||
 | 
					        .end {
 | 
				
			||||||
          width: 48%;
 | 
					          width: 48%;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                .begin{
 | 
					        .begin {
 | 
				
			||||||
          border-right: 1px solid whitesmoke;
 | 
					          border-right: 1px solid whitesmoke;
 | 
				
			||||||
          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%);
 | 
				
			||||||
        color: black;
 | 
					        color: black;
 | 
				
			||||||
                &:hover{
 | 
					        &:hover {
 | 
				
			||||||
          color: black;
 | 
					          color: black;
 | 
				
			||||||
          background-color: hsl(40, 58%, 50%);
 | 
					          background-color: hsl(40, 58%, 50%);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                &:nth-child(2n){
 | 
					        &:nth-child(2n) {
 | 
				
			||||||
          margin-top: 5px;
 | 
					          margin-top: 5px;
 | 
				
			||||||
          margin-bottom: 5px;
 | 
					          margin-bottom: 5px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            .tooltip{
 | 
					      .tooltip {
 | 
				
			||||||
        visibility: hidden;
 | 
					        visibility: hidden;
 | 
				
			||||||
        width: 120px;
 | 
					        width: 120px;
 | 
				
			||||||
        background-color: hsl(210, 20%, 98%);
 | 
					        background-color: hsl(210, 20%, 98%);
 | 
				
			||||||
@@ -1008,25 +1034,24 @@ header {
 | 
				
			|||||||
        border-radius: 6px;
 | 
					        border-radius: 6px;
 | 
				
			||||||
        position: absolute;
 | 
					        position: absolute;
 | 
				
			||||||
        z-index: 10;
 | 
					        z-index: 10;
 | 
				
			||||||
                ul{
 | 
					        ul {
 | 
				
			||||||
          margin-left: 0;
 | 
					          margin-left: 0;
 | 
				
			||||||
          display: inline-block;
 | 
					          display: inline-block;
 | 
				
			||||||
                    li{
 | 
					          li {
 | 
				
			||||||
            display: list-item;
 | 
					            display: list-item;
 | 
				
			||||||
            list-style-type: none;
 | 
					            list-style-type: none;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            &.not_moderated
 | 
					      &.not_moderated {
 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
        border: 1px solid red;
 | 
					        border: 1px solid red;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
            &:hover .tooltip{
 | 
					      &:hover .tooltip {
 | 
				
			||||||
        visibility: visible;
 | 
					        visibility: visible;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    #view{
 | 
					  #view {
 | 
				
			||||||
    position: fixed;
 | 
					    position: fixed;
 | 
				
			||||||
    width: 100vw;
 | 
					    width: 100vw;
 | 
				
			||||||
    height: 100vh;
 | 
					    height: 100vh;
 | 
				
			||||||
@@ -1039,10 +1064,10 @@ header {
 | 
				
			|||||||
    visibility: hidden;
 | 
					    visibility: hidden;
 | 
				
			||||||
    background-color: rgba(10, 10, 10, 0.9);
 | 
					    background-color: rgba(10, 10, 10, 0.9);
 | 
				
			||||||
    overflow: hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
        &.active{
 | 
					    &.active {
 | 
				
			||||||
      visibility: visible;
 | 
					      visibility: visible;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
        #placeholder{
 | 
					    #placeholder {
 | 
				
			||||||
      width: 80vw;
 | 
					      width: 80vw;
 | 
				
			||||||
      height: 80vh;
 | 
					      height: 80vh;
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
@@ -1050,7 +1075,7 @@ header {
 | 
				
			|||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
      top: 0;
 | 
					      top: 0;
 | 
				
			||||||
      left: 0;
 | 
					      left: 0;
 | 
				
			||||||
            img{
 | 
					      img {
 | 
				
			||||||
        max-width: 100%;
 | 
					        max-width: 100%;
 | 
				
			||||||
        max-height: 100%;
 | 
					        max-height: 100%;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -1329,8 +1370,9 @@ 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 {
 | 
				
			||||||
@@ -1510,7 +1551,7 @@ textarea {
 | 
				
			|||||||
  .last_message span {
 | 
					  .last_message span {
 | 
				
			||||||
    white-space: nowrap;
 | 
					    white-space: nowrap;
 | 
				
			||||||
    text-overflow: ellipsis;
 | 
					    text-overflow: ellipsis;
 | 
				
			||||||
    overflow:hidden;
 | 
					    overflow: hidden;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -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-----------------------------------*/
 | 
				
			||||||
@@ -1808,7 +1855,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.pedagogy {
 | 
					.pedagogy {
 | 
				
			||||||
  &.star-not-checked {
 | 
					  &.star-not-checked {
 | 
				
			||||||
      color : #f7f7f7;
 | 
					    color: #f7f7f7;
 | 
				
			||||||
    margin-bottom: 0;
 | 
					    margin-bottom: 0;
 | 
				
			||||||
    margin-top: 0;
 | 
					    margin-top: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -1819,10 +1866,10 @@ $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) {
 | 
				
			||||||
    &.star-not-checked {
 | 
					    &.star-not-checked {
 | 
				
			||||||
      margin-left: 5px;
 | 
					      margin-left: 5px;
 | 
				
			||||||
      margin-right: 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 {
 | 
					    &.grade-without-star {
 | 
				
			||||||
      display: block;
 | 
					      display: block;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
@@ -1879,13 +1924,13 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
      grid-template-rows: auto;
 | 
					      grid-template-rows: auto;
 | 
				
			||||||
      grid-template-areas: "search-bar-input search-bar-button";
 | 
					      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-columns: auto auto;
 | 
				
			||||||
        grid-template-rows: auto;
 | 
					        grid-template-rows: auto;
 | 
				
			||||||
        grid-template-areas: "search-bar-input search-bar-button";
 | 
					        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-columns: auto;
 | 
				
			||||||
        grid-template-rows: auto;
 | 
					        grid-template-rows: auto;
 | 
				
			||||||
        grid-template-areas: "search-bar-input";
 | 
					        grid-template-areas: "search-bar-input";
 | 
				
			||||||
@@ -1921,8 +1966,9 @@ $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"],
 | 
				
			||||||
      display:none;
 | 
					    input[type="checkbox"] {
 | 
				
			||||||
 | 
					      display: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    .radio-guide {
 | 
					    .radio-guide {
 | 
				
			||||||
      margin-top: 10px;
 | 
					      margin-top: 10px;
 | 
				
			||||||
@@ -1956,7 +2002,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
      grid-template-rows: auto auto;
 | 
					      grid-template-rows: auto auto;
 | 
				
			||||||
      grid-template-areas:
 | 
					      grid-template-areas:
 | 
				
			||||||
        "hours-cm hours-td hours-tp hours-te hours-the"
 | 
					        "hours-cm hours-td hours-tp hours-te hours-the"
 | 
				
			||||||
        "department credit-type semester . ." ;
 | 
					        "department credit-type semester . .";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .department {
 | 
					    .department {
 | 
				
			||||||
@@ -2005,7 +2051,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
        grid-template-rows: 100%;
 | 
					        grid-template-rows: 100%;
 | 
				
			||||||
        grid-template-areas: "stars comment";
 | 
					        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-columns: 100%;
 | 
				
			||||||
          grid-template-rows: auto auto;
 | 
					          grid-template-rows: auto auto;
 | 
				
			||||||
          grid-template-areas:
 | 
					          grid-template-areas:
 | 
				
			||||||
@@ -2033,7 +2079,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
        color: $pedagogy-white-text;
 | 
					        color: $pedagogy-white-text;
 | 
				
			||||||
        clip-path: polygon(0 0%, 0 100%, 30% 100%, 33% 0);
 | 
					        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;
 | 
					          clip-path: none;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -2060,7 +2106,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
        "grade grade-stars uv-infos"
 | 
					        "grade grade-stars uv-infos"
 | 
				
			||||||
        ". . 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-columns: 50% 50%;
 | 
				
			||||||
        grid-template-rows: auto auto;
 | 
					        grid-template-rows: auto auto;
 | 
				
			||||||
        grid-template-areas:
 | 
					        grid-template-areas:
 | 
				
			||||||
@@ -2104,14 +2150,14 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
      margin-bottom: 30px;
 | 
					      margin-bottom: 30px;
 | 
				
			||||||
      margin-top: 10px;
 | 
					      margin-top: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      @media screen and (max-width: $large-devices){
 | 
					      @media screen and (max-width: $large-devices) {
 | 
				
			||||||
        grid-template-columns: auto;
 | 
					        grid-template-columns: auto;
 | 
				
			||||||
        grid-template-rows: auto auto auto auto;
 | 
					        grid-template-rows: auto auto auto auto;
 | 
				
			||||||
        grid-template-areas:
 | 
					        grid-template-areas:
 | 
				
			||||||
          "grade-block"
 | 
					          "grade-block"
 | 
				
			||||||
          "comment"
 | 
					          "comment"
 | 
				
			||||||
          "info"
 | 
					          "info"
 | 
				
			||||||
          "comment-end-bar"
 | 
					          "comment-end-bar";
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .grade-block {
 | 
					      .grade-block {
 | 
				
			||||||
@@ -2131,10 +2177,10 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        background-color: $pedagogy-blue;
 | 
					        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-columns: 50% auto;
 | 
				
			||||||
          grid-template-rows: auto;
 | 
					          grid-template-rows: auto;
 | 
				
			||||||
          grid-template-areas:"grade-type grade-stars";
 | 
					          grid-template-areas: "grade-type grade-stars";
 | 
				
			||||||
          width: auto;
 | 
					          width: auto;
 | 
				
			||||||
          clip-path: none;
 | 
					          clip-path: none;
 | 
				
			||||||
          align-content: space-evenly;
 | 
					          align-content: space-evenly;
 | 
				
			||||||
@@ -2171,7 +2217,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
          "anchor"
 | 
					          "anchor"
 | 
				
			||||||
          "markdown";
 | 
					          "markdown";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @media screen and (max-width: $large-devices){
 | 
					        @media screen and (max-width: $large-devices) {
 | 
				
			||||||
          border-left: solid;
 | 
					          border-left: solid;
 | 
				
			||||||
          border-right: solid;
 | 
					          border-right: solid;
 | 
				
			||||||
          border-color: $pedagogy-blue;
 | 
					          border-color: $pedagogy-blue;
 | 
				
			||||||
@@ -2199,7 +2245,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
        grid-area: info;
 | 
					        grid-area: info;
 | 
				
			||||||
        padding-bottom: 10px;
 | 
					        padding-bottom: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @media screen and (max-width: $large-devices){
 | 
					        @media screen and (max-width: $large-devices) {
 | 
				
			||||||
          border-left: solid;
 | 
					          border-left: solid;
 | 
				
			||||||
          border-right: solid;
 | 
					          border-right: solid;
 | 
				
			||||||
          border-color: $pedagogy-blue;
 | 
					          border-color: $pedagogy-blue;
 | 
				
			||||||
@@ -2227,7 +2273,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
        background-color: $pedagogy-blue;
 | 
					        background-color: $pedagogy-blue;
 | 
				
			||||||
        margin-top: -1px;
 | 
					        margin-top: -1px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @media screen and (max-width: $large-devices){
 | 
					        @media screen and (max-width: $large-devices) {
 | 
				
			||||||
          grid-template-columns: auto;
 | 
					          grid-template-columns: auto;
 | 
				
			||||||
          grid-template-rows: auto auto auto;
 | 
					          grid-template-rows: auto auto auto;
 | 
				
			||||||
          grid-template-areas:
 | 
					          grid-template-areas:
 | 
				
			||||||
@@ -2247,7 +2293,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
          background-color: $pedagogy-orange;
 | 
					          background-color: $pedagogy-orange;
 | 
				
			||||||
          clip-path: polygon(0 10px, 0 100%, 350px 200%, 300px 10px);
 | 
					          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;
 | 
					            clip-path: none;
 | 
				
			||||||
            padding: 0;
 | 
					            padding: 0;
 | 
				
			||||||
            padding-bottom: 7px;
 | 
					            padding-bottom: 7px;
 | 
				
			||||||
@@ -2261,14 +2307,13 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
          a:hover {
 | 
					          a:hover {
 | 
				
			||||||
            color: $pedagogy-hover-blue;
 | 
					            color: $pedagogy-hover-blue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .date {
 | 
					        .date {
 | 
				
			||||||
          grid-area: date;
 | 
					          grid-area: date;
 | 
				
			||||||
          color: $pedagogy-white-text;
 | 
					          color: $pedagogy-white-text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          @media screen and (max-width: $large-devices){
 | 
					          @media screen and (max-width: $large-devices) {
 | 
				
			||||||
            padding-bottom: 7px;
 | 
					            padding-bottom: 7px;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -2287,7 +2332,7 @@ $pedagogy-white-text: #f0f0f0;
 | 
				
			|||||||
            color: $pedagogy-hover-blue;
 | 
					            color: $pedagogy-hover-blue;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          @media screen and (max-width: $large-devices){
 | 
					          @media screen and (max-width: $large-devices) {
 | 
				
			||||||
            text-align: center;
 | 
					            text-align: center;
 | 
				
			||||||
            justify-self: inherit;
 | 
					            justify-self: inherit;
 | 
				
			||||||
            padding-bottom: 7px;
 | 
					            padding-bottom: 7px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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")),
 | 
				
			||||||
 
 | 
				
			|||||||