From 4289eed6d585e29c855450915dd581283408822b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Nov 2020 11:39:28 +0100 Subject: [PATCH 01/15] Small model tweaks --- .gitignore | 5 +++++ fiches/migrations/0008_auto_20201113_1038.py | 23 ++++++++++++++++++++ fiches/models.py | 8 +++++-- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 fiches/migrations/0008_auto_20201113_1038.py diff --git a/.gitignore b/.gitignore index a07b7d1..a5d3e4a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,11 +3,16 @@ *.swo *~ *# +*.log /media/picture /venv/ +.vagrant/ +static/ *.sqlite3 fiches/templates/fiches/base_old.html fiches/static/fiches/css_old/ + +.vscode diff --git a/fiches/migrations/0008_auto_20201113_1038.py b/fiches/migrations/0008_auto_20201113_1038.py new file mode 100644 index 0000000..fcf28e3 --- /dev/null +++ b/fiches/migrations/0008_auto_20201113_1038.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.17 on 2020-11-13 10:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('fiches', '0007_auto_20200917_1421'), + ] + + operations = [ + migrations.AlterField( + model_name='department', + name='name', + field=models.CharField(max_length=255, unique=True, verbose_name='nom du département'), + ), + migrations.AlterField( + model_name='profile', + name='birth_date', + field=models.DateField(blank=True, null=True, verbose_name='date de naissance'), + ), + ] diff --git a/fiches/models.py b/fiches/models.py index 3dbc3cf..8c7111a 100644 --- a/fiches/models.py +++ b/fiches/models.py @@ -23,7 +23,9 @@ class Profile(models.Model): promotion = models.IntegerField( validators=[MinValueValidator(1980)], verbose_name=_("promotion") ) - birth_date = models.DateField(blank=True, verbose_name=_("date de naissance")) + birth_date = models.DateField( + blank=True, null=True, verbose_name=_("date de naissance") + ) thurne = models.CharField(blank=True, max_length=100, verbose_name=_("thurne")) text_field = models.TextField(blank=True, verbose_name=_("champ libre")) printing = models.BooleanField( @@ -41,7 +43,9 @@ class Profile(models.Model): class Department(models.Model): - name = models.CharField(max_length=255, verbose_name=_("nom du département")) + name = models.CharField( + max_length=255, verbose_name=_("nom du département"), unique=True + ) def __str__(self): return self.name From d01d4d4d51ce1475b754e153d8f52f1e00d0c84a Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Nov 2020 11:43:34 +0100 Subject: [PATCH 02/15] Blackify settings (oups) --- annuaire/settings.py | 87 ++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/annuaire/settings.py b/annuaire/settings.py index b102e1c..b407743 100644 --- a/annuaire/settings.py +++ b/annuaire/settings.py @@ -20,7 +20,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '84=n(@@wl(04oc$(-+3surgrlf&uq3=m)=(hpg$immi1h69s)p' +SECRET_KEY = "84=n(@@wl(04oc$(-+3surgrlf&uq3=m)=(hpg$immi1h69s)p" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True @@ -30,59 +30,59 @@ ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django_cas_ng', - 'fiches' + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_cas_ng", + "fiches", ] MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = 'annuaire.urls' +ROOT_URLCONF = "annuaire.urls" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], }, }, ] AUTHENTICATION_BACKENDS = ( - 'django.contrib.auth.backends.ModelBackend', - 'django_cas_ng.backends.CASBackend', + "django.contrib.auth.backends.ModelBackend", + "django_cas_ng.backends.CASBackend", ) -WSGI_APPLICATION = 'annuaire.wsgi.application' +WSGI_APPLICATION = "annuaire.wsgi.application" # Database # https://docs.djangoproject.com/en/dev/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), } } @@ -92,17 +92,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': - 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -110,9 +109,9 @@ AUTH_PASSWORD_VALIDATORS = [ # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ -LANGUAGE_CODE = 'fr-fr' +LANGUAGE_CODE = "fr-fr" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" USE_I18N = True @@ -124,14 +123,14 @@ USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/dev/howto/static-files/ -STATIC_URL = '/static/' +STATIC_URL = "/static/" -MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_ROOT = os.path.join(BASE_DIR, "media") -MEDIA_URL = '/media/' +MEDIA_URL = "/media/" -CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' +CAS_SERVER_URL = "https://cas.eleves.ens.fr/" CAS_VERSION = "2" -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" From ef0076781e29fa4582a025395aee9c9192b8f46e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Nov 2020 11:44:53 +0100 Subject: [PATCH 03/15] Rajoute les serveurs ldap aux settings --- annuaire/settings.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/annuaire/settings.py b/annuaire/settings.py index b407743..4a86b9a 100644 --- a/annuaire/settings.py +++ b/annuaire/settings.py @@ -22,6 +22,26 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "84=n(@@wl(04oc$(-+3surgrlf&uq3=m)=(hpg$immi1h69s)p" +# À bouger dans un ficher secret quand il sera créé ? +LDAP = { + "SPI": { + "PROTOCOL": "ldaps", + "URL": "ldap.spi.ens.fr", + "PORT": 636, + }, + "CRI": { + "PROTOCOL": "ldap", + "URL": "annuaire.ens.fr", + "PORT": 389, + }, +} + +ANNUAIRE = { + "PROTOCOL": "http", + "URL": "annuaireweb.ens.fr", + "PORT": 80, +} + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True From bf05b0a7d4968b0d8e578b0d8e6392ded808396c Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Nov 2020 11:45:32 +0100 Subject: [PATCH 04/15] =?UTF-8?q?Commande=20pour=20ajouter=20les=20conscri?= =?UTF-8?q?t=C2=B7e=C2=B7s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fiches/management/commands/_ldap.py | 144 ++++++++++++++++++++ fiches/management/commands/add_conscrits.py | 109 +++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 fiches/management/commands/_ldap.py create mode 100644 fiches/management/commands/add_conscrits.py diff --git a/fiches/management/commands/_ldap.py b/fiches/management/commands/_ldap.py new file mode 100644 index 0000000..34a01ca --- /dev/null +++ b/fiches/management/commands/_ldap.py @@ -0,0 +1,144 @@ +from collections import namedtuple + +import ldap +import ldap.filter +from django.conf import settings + +ClipperAccount = namedtuple("ClipperAccount", ["uid", "name", "email", "year", "dept"]) + + +class LDAP: + """Classe pour se connecter à un LDAP ENS (CRI ou SPI). + Inspirée de `merle_scripts`. + """ + + # À renseigner + LDAP_SERVER = { + "PROTOCOL": "ldaps", + "URL": "ldap.example.com", + "PORT": 636, + } + + search_base = "" + attr_list = [] + + def __init__(self): + """ Initialize the LDAP object """ + + ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER) + self.ldap_obj = ldap.initialize( + "{PROTOCOL}://{URL}:{PORT}".format(**self.LDAP_SERVER) + ) + self.ldap_obj.set_option(ldap.OPT_REFERRALS, 0) + self.ldap_obj.set_option(ldap.OPT_PROTOCOL_VERSION, 3) + self.ldap_obj.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND) + self.ldap_obj.set_option(ldap.OPT_X_TLS_DEMAND, True) + self.ldap_obj.set_option(ldap.OPT_NETWORK_TIMEOUT, 10) + self.ldap_obj.set_option(ldap.OPT_TIMEOUT, 10) + + def search(self, filters="(objectClass=*)"): + """Do a ldap.search_s with the given filters, as specified for + ldap.search_s""" + + return self.ldap_obj.search_s( + self.search_base, ldap.SCOPE_SUBTREE, filters, self.attr_list + ) + + @staticmethod + def extract_ldap_info(entry, field): + """ Extract the given field from an LDAP entry as an UTF-8 string """ + return entry[1].get(field, [b""])[0].decode("utf-8") + + +class ClipperLDAP(LDAP): + + LDAP_SERVER = settings.LDAP["SPI"] + + search_base = "dc=spi,dc=ens,dc=fr" + attr_list = ["cn", "uid", "mail", "homeDirectory"] + + verbose_depts = { + "bio": "Biologie", + "chimie": "Chimie", + "dec": "Études Cognitives", + "geol": "Géosciences", + "guests": "Invité·e", + "info": "Informatique", + "litt": "Lettres", + "maths": "Mathématiques", + "pei": "PEI", + "phy": "Physique", + } + + def parse_dept(self, home_dir): + """Extrait le département d'entrée d'un·e élève à partir de son dossier. + Le dossier a le format `/users///`. + """ + users, promo, dept, login = home_dir.split("/")[1:] + + if users != "users": + raise ValueError("Invalid home directory") + + # Ça casse en 2100, mais le système de naming de sas aussi... + promo = "20" + promo + + return promo, self.verbose_depts.get(dept, None) + + def get_clipper_list(self, promo_filter=None): + """Extrait la liste des comptes clipper présents dans le LDAP. + Renvoie une liste de `namedTuple` contenant leur nom, prénom, adresse mail et + département/année d'entrée. + Si `promo_filter != None`, ne renvoie que les clippers d'une promotion donnée. + """ + search_res = self.search() + clipper_list = [] + + for entry in search_res: + uid = self.extract_ldap_info(entry, "uid") + # Il y a des comptes "bizarre" (e.g. `root`) avec uid vide + if len(uid) > 0: + try: + name = self.extract_ldap_info(entry, "cn") + email = self.extract_ldap_info(entry, "mail") + promo, dept = self.parse_dept( + self.extract_ldap_info(entry, "homeDirectory") + ) + + if promo_filter is None or promo == promo_filter: + clipper_list.append( + ClipperAccount(uid, name, email, promo, dept) + ) + except ValueError: + pass + + return clipper_list + + +class AnnuaireLDAP(LDAP): + + LDAP_SERVER = settings.LDAP["CRI"] + + search_base = "ou=people,dc=ens,dc=fr" + attr_list = ["uid", "sn", "givenName", "ou"] + + def try_match(self, clipper): + """Essaie de trouver une entrée correspondant au compte clipper donné dans + l'annuaire de l'ENS. L'heuristique est la suivante : il est très probable + que le prénom de la personne commence par le premier mot de `clipper.fullname`, + et que son nom de famille finisse par le dernier mot. + """ + given_name = clipper.name.split(" ")[0] + last_name = clipper.name.split(" ")[-1] + + search_name = self.search( + "(&(givenName={}*)(sn=*{}))".format(given_name, last_name) + ) + + if len(search_name) > 0: + if len(search_name) > 2: + print("Erreur : deux résultats trouvés pour {}".format(clipper.uid)) + return None + + return self.extract_ldap_info(search_name[0], "uid") + + return None diff --git a/fiches/management/commands/add_conscrits.py b/fiches/management/commands/add_conscrits.py new file mode 100644 index 0000000..d668e93 --- /dev/null +++ b/fiches/management/commands/add_conscrits.py @@ -0,0 +1,109 @@ +from datetime import date +from io import BytesIO +from urllib.error import HTTPError +from urllib.request import urlopen + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.files import File +from django.core.management.base import BaseCommand + +from fiches.models import Department, Profile + +from ._ldap import AnnuaireLDAP, ClipperLDAP + + +class Command(BaseCommand): + help = ( + "Importe les noms et (si possible) les photos des conscrit·e·s dans l'annuaire." + ) + + def add_arguments(self, parser): + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--all", action="store_true", help="Importe l'intégralité des promotions" + ) + group.add_argument("--promo", help="Spécifie la promotion à importer") + + parser.add_argument( + "--without-photos", + action="store_true", + help="N'essaie pas d'importer les photos associées", + ) + + def get_current_promo(self): + today = date.today() + year = today.year + if today.month < 9: + year -= 1 + + return str(year) + + def handle(self, *args, **options): + if options["all"]: + promo = None + elif options["promo"] is not None: + promo = options["promo"] + if len(promo) == 2: + promo = "20" + promo + else: + promo = self.get_current_promo() + + # On récupère la liste des élèves à créer + ldap = ClipperLDAP() + clipper_list = ldap.get_clipper_list(promo_filter=promo) + + # On vire les élèves déjà existants + existing_users = set(User.objects.values_list("username", flat=True)) + clippers = [ + clipper for clipper in clipper_list if clipper.uid not in existing_users + ] + + # Les départements sont créés à la main ; il y en a peu. + depts = { + dept: Department.objects.get_or_create(name=dept)[0].id + for dept in ldap.verbose_depts.values() + } + + users_to_create = [] + profiles_to_create = [] + dept_m2m_to_create = [] + + for clipper in clippers: + user = User(username=clipper.uid, email=clipper.email) + profile = Profile(user=user, full_name=clipper.name, promotion=clipper.year) + users_to_create.append(user) + profiles_to_create.append(profile) + dept_m2m_to_create.append( + Profile.department.through( + profile=profile, department_id=depts[clipper.dept] + ) + ) + + User.objects.bulk_create(users_to_create) + for profile in profiles_to_create: + profile.user_id = profile.user.id + profiles = Profile.objects.bulk_create(profiles_to_create) + for dept_m2m in dept_m2m_to_create: + dept_m2m.profile_id = dept_m2m.profile.id + Profile.department.through.objects.bulk_create(dept_m2m_to_create) + + # Gestion des images + + if not options["without_photos"]: + cri_ldap = AnnuaireLDAP() + base_annuaire = "{PROTOCOL}://{URL}:{PORT}".format(**settings.ANNUAIRE) + + for clipper, profile in zip(clippers, profiles): + cri_login = cri_ldap.try_match(clipper) + if cri_login is not None: + img_url = "{}/photos/{}.jpg".format(base_annuaire, cri_login) + try: + istream = urlopen(img_url) + profile.picture.save( + name="{}.jpg".format(profile.user.username), + content=File(BytesIO(istream.read())), + ) + except HTTPError: + # Parfois il y a une erreur 404 : dans ce cas, pas de photo + pass From df10e754a534da48aa67edbfe5f6d4126fb29089 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Nov 2020 11:46:14 +0100 Subject: [PATCH 05/15] requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 4e4fcaf..0192af9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ django==2.2.* Pillow django_cas_ng +python-ldap \ No newline at end of file From 1d24478c1df0b0758506bdc641d2d50cfe093461 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 13 Nov 2020 17:23:17 +0100 Subject: [PATCH 06/15] Split in 2 commands --- fiches/management/commands/_ldap.py | 16 +++-- fiches/management/commands/add_conscrits.py | 43 +++--------- fiches/management/commands/get_photos.py | 73 +++++++++++++++++++++ 3 files changed, 91 insertions(+), 41 deletions(-) create mode 100644 fiches/management/commands/get_photos.py diff --git a/fiches/management/commands/_ldap.py b/fiches/management/commands/_ldap.py index 34a01ca..8470f73 100644 --- a/fiches/management/commands/_ldap.py +++ b/fiches/management/commands/_ldap.py @@ -121,14 +121,14 @@ class AnnuaireLDAP(LDAP): search_base = "ou=people,dc=ens,dc=fr" attr_list = ["uid", "sn", "givenName", "ou"] - def try_match(self, clipper): - """Essaie de trouver une entrée correspondant au compte clipper donné dans + def try_match(self, profile): + """Essaie de trouver une entrée correspondant au profile donné dans l'annuaire de l'ENS. L'heuristique est la suivante : il est très probable - que le prénom de la personne commence par le premier mot de `clipper.fullname`, + que le prénom de la personne commence par le premier mot de `profile.full_name`, et que son nom de famille finisse par le dernier mot. """ - given_name = clipper.name.split(" ")[0] - last_name = clipper.name.split(" ")[-1] + given_name = profile.full_name.split(" ")[0] + last_name = profile.full_name.split(" ")[-1] search_name = self.search( "(&(givenName={}*)(sn=*{}))".format(given_name, last_name) @@ -136,7 +136,11 @@ class AnnuaireLDAP(LDAP): if len(search_name) > 0: if len(search_name) > 2: - print("Erreur : deux résultats trouvés pour {}".format(clipper.uid)) + print( + "Erreur : deux résultats trouvés pour {}".format( + profile.user.username + ) + ) return None return self.extract_ldap_info(search_name[0], "uid") diff --git a/fiches/management/commands/add_conscrits.py b/fiches/management/commands/add_conscrits.py index d668e93..0301f21 100644 --- a/fiches/management/commands/add_conscrits.py +++ b/fiches/management/commands/add_conscrits.py @@ -1,22 +1,15 @@ from datetime import date -from io import BytesIO -from urllib.error import HTTPError -from urllib.request import urlopen -from django.conf import settings from django.contrib.auth.models import User -from django.core.files import File from django.core.management.base import BaseCommand from fiches.models import Department, Profile -from ._ldap import AnnuaireLDAP, ClipperLDAP +from ._ldap import ClipperLDAP class Command(BaseCommand): - help = ( - "Importe les noms et (si possible) les photos des conscrit·e·s dans l'annuaire." - ) + help = "Crée les fiches annuaire des conscrit·e·s automatiquement" def add_arguments(self, parser): group = parser.add_mutually_exclusive_group() @@ -25,12 +18,6 @@ class Command(BaseCommand): ) group.add_argument("--promo", help="Spécifie la promotion à importer") - parser.add_argument( - "--without-photos", - action="store_true", - help="N'essaie pas d'importer les photos associées", - ) - def get_current_promo(self): today = date.today() year = today.year @@ -83,27 +70,13 @@ class Command(BaseCommand): User.objects.bulk_create(users_to_create) for profile in profiles_to_create: profile.user_id = profile.user.id - profiles = Profile.objects.bulk_create(profiles_to_create) + Profile.objects.bulk_create(profiles_to_create) for dept_m2m in dept_m2m_to_create: dept_m2m.profile_id = dept_m2m.profile.id Profile.department.through.objects.bulk_create(dept_m2m_to_create) - # Gestion des images - - if not options["without_photos"]: - cri_ldap = AnnuaireLDAP() - base_annuaire = "{PROTOCOL}://{URL}:{PORT}".format(**settings.ANNUAIRE) - - for clipper, profile in zip(clippers, profiles): - cri_login = cri_ldap.try_match(clipper) - if cri_login is not None: - img_url = "{}/photos/{}.jpg".format(base_annuaire, cri_login) - try: - istream = urlopen(img_url) - profile.picture.save( - name="{}.jpg".format(profile.user.username), - content=File(BytesIO(istream.read())), - ) - except HTTPError: - # Parfois il y a une erreur 404 : dans ce cas, pas de photo - pass + print( + "Création de {} utilisateur·ices et de {} profils effectuée avec succès".format( + len(users_to_create), len(profiles_to_create) + ) + ) diff --git a/fiches/management/commands/get_photos.py b/fiches/management/commands/get_photos.py new file mode 100644 index 0000000..c9e1bd5 --- /dev/null +++ b/fiches/management/commands/get_photos.py @@ -0,0 +1,73 @@ +from datetime import date +from io import BytesIO +from urllib.error import HTTPError +from urllib.request import urlopen + +from django.conf import settings +from django.core.files import File +from django.core.management.base import BaseCommand + +from fiches.models import Profile + +from ._ldap import AnnuaireLDAP + + +class Command(BaseCommand): + help = "Si possible, import les photos des conscrit·e·s dans l'annuaire." + + def add_arguments(self, parser): + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--all", action="store_true", help="Importe l'intégralité des promotions" + ) + group.add_argument("--promo", help="Spécifie la promotion à importer") + + def get_current_promo(self): + today = date.today() + year = today.year + if today.month < 9: + year -= 1 + + return year + + def handle(self, *args, **options): + if options["all"]: + promo = None + elif options["promo"] is not None: + promo = options["promo"] + if len(promo) == 2: + promo = "20" + promo + else: + promo = self.get_current_promo() + + no_images = Profile.objects.select_related("user").filter(picture="") + + if promo is not None: + no_images = no_images.filter(promotion=promo) + + cri_ldap = AnnuaireLDAP() + base_annuaire = "{PROTOCOL}://{URL}:{PORT}".format(**settings.ANNUAIRE) + + success = 0 + + for profile in no_images: + cri_login = cri_ldap.try_match(profile) + if cri_login is not None: + img_url = "{}/photos/{}.jpg".format(base_annuaire, cri_login) + try: + istream = urlopen(img_url) + profile.picture.save( + name="{}.jpg".format(profile.user.username), + content=File(BytesIO(istream.read())), + ) + success += 1 + except HTTPError: + # Parfois, même si on trouve un login CRI, il y a une erreur 404. + # Dans ce cas, pas de photo : on échoue gracieusement. + pass + + print( + "{} profils traités ; {} images importées.".format( + no_images.count(), success + ) + ) From d7263fc9e01d0ca674093d5b93538f625303839d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 18:39:29 +0100 Subject: [PATCH 07/15] Split requirements --- requirements-prod.txt | 5 +++++ requirements.txt | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 requirements-prod.txt diff --git a/requirements-prod.txt b/requirements-prod.txt new file mode 100644 index 0000000..1b3d939 --- /dev/null +++ b/requirements-prod.txt @@ -0,0 +1,5 @@ +-r requirements.txt + +psycopg2 +python-ldap +gunicorn \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0192af9..4e4fcaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ django==2.2.* Pillow django_cas_ng -python-ldap \ No newline at end of file From 43026cecd2260087f9fd3f1abeb4b51e31d8eefb Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 18:40:46 +0100 Subject: [PATCH 08/15] Split settings between local and prod --- annuaire/settings/.gitignore | 1 + annuaire/settings/__init__.py | 0 annuaire/{settings.py => settings/common.py} | 142 ++++++++----------- annuaire/settings/local.py | 62 ++++++++ annuaire/settings/prod.py | 81 +++++++++++ annuaire/settings/secret_example.py | 15 ++ manage.py | 4 +- 7 files changed, 219 insertions(+), 86 deletions(-) create mode 100644 annuaire/settings/.gitignore create mode 100644 annuaire/settings/__init__.py rename annuaire/{settings.py => settings/common.py} (50%) create mode 100644 annuaire/settings/local.py create mode 100644 annuaire/settings/prod.py create mode 100644 annuaire/settings/secret_example.py diff --git a/annuaire/settings/.gitignore b/annuaire/settings/.gitignore new file mode 100644 index 0000000..b47515c --- /dev/null +++ b/annuaire/settings/.gitignore @@ -0,0 +1 @@ +secret.py \ No newline at end of file diff --git a/annuaire/settings/__init__.py b/annuaire/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/annuaire/settings.py b/annuaire/settings/common.py similarity index 50% rename from annuaire/settings.py rename to annuaire/settings/common.py index 4a86b9a..d0bfe3e 100644 --- a/annuaire/settings.py +++ b/annuaire/settings/common.py @@ -1,53 +1,47 @@ """ -Django settings for annuaire project. - -Generated by 'django-admin startproject' using Django 2.2b1. - -For more information on this file, see -https://docs.djangoproject.com/en/dev/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/dev/ref/settings/ +Settings communs entre setups de dev et de production. """ import os +import sys -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# --- +# Secrets +# --- + +try: + from . import secret +except ImportError: + raise ImportError( + "The secret.py file is missing.\n" + "For a development environment, simply copy secret_example.py" + ) -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/dev/howto/deployment/checklist/ +def import_secret(name): + """ + Shorthand for importing a value from the secret module and raising an + informative exception if a secret is missing. + """ + try: + return getattr(secret, name) + except AttributeError: + raise RuntimeError("Secret missing: {}".format(name)) -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "84=n(@@wl(04oc$(-+3surgrlf&uq3=m)=(hpg$immi1h69s)p" -# À bouger dans un ficher secret quand il sera créé ? -LDAP = { - "SPI": { - "PROTOCOL": "ldaps", - "URL": "ldap.spi.ens.fr", - "PORT": 636, - }, - "CRI": { - "PROTOCOL": "ldap", - "URL": "annuaire.ens.fr", - "PORT": 389, - }, -} +SECRET_KEY = import_secret("SECRET_KEY") +ADMINS = import_secret("ADMINS") +SERVER_EMAIL = import_secret("SERVER_EMAIL") +EMAIL_HOST = import_secret("EMAIL_HOST") -ANNUAIRE = { - "PROTOCOL": "http", - "URL": "annuaireweb.ens.fr", - "PORT": 80, -} -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +# --- +# Défauts Django +# --- -ALLOWED_HOSTS = [] - -# Application definition +DEBUG = False # False by default feels safer +TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) INSTALLED_APPS = [ "django.contrib.admin", @@ -95,62 +89,42 @@ AUTHENTICATION_BACKENDS = ( WSGI_APPLICATION = "annuaire.wsgi.application" - -# Database -# https://docs.djangoproject.com/en/dev/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": os.path.join(BASE_DIR, "db.sqlite3"), - } -} - - -# Password validation -# https://docs.djangoproject.com/en/dev/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - - # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ LANGUAGE_CODE = "fr-fr" - TIME_ZONE = "UTC" - USE_I18N = True - USE_L10N = True - USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/dev/howto/static-files/ - -STATIC_URL = "/static/" - -MEDIA_ROOT = os.path.join(BASE_DIR, "media") - -MEDIA_URL = "/media/" +# --- +# Settings CAS +# --- CAS_SERVER_URL = "https://cas.eleves.ens.fr/" - CAS_VERSION = "2" -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +# --- +# LDAP et annuaire ENS +# --- + +# Est-ce vraiment nécessaire de les garder secrets ? +LDAP = { + "SPI": { + "PROTOCOL": "ldaps", + "URL": "ldap.spi.ens.fr", + "PORT": 636, + }, + "CRI": { + "PROTOCOL": "ldap", + "URL": "annuaire.ens.fr", + "PORT": 389, + }, +} + +ANNUAIRE = { + "PROTOCOL": "http", + "URL": "annuaireweb.ens.fr", + "PORT": 80, +} diff --git a/annuaire/settings/local.py b/annuaire/settings/local.py new file mode 100644 index 0000000..7275ef6 --- /dev/null +++ b/annuaire/settings/local.py @@ -0,0 +1,62 @@ +""" +Settings pour le dev local de l'annuaire (hors vagrant). +""" + +import os + +from .common import * # NOQA +from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING + +# --- +# Tweaks for debug/local development +# --- + +ALLOWED_HOSTS = [] + +DEBUG = True +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +STATIC_URL = "/static/" +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "db.sqlite3"), + } +} + +# Use the default cache backend for local development +CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}} + +# Pas besoin de sécurité en local +AUTH_PASSWORD_VALIDATORS = [] +PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] + + +# --- +# Debug tool bar +# --- + + +def show_toolbar(request): + """ + On active la debug-toolbar en mode développement local sauf : + - dans l'admin où ça ne sert pas à grand chose; + - si la variable d'environnement DJANGO_NO_DDT est à 1 → ça permet de la désactiver + sans modifier ce fichier en exécutant `export DJANGO_NO_DDT=1` dans le terminal + qui lance `./manage.py runserver`. + + Autre side effect de cette fonction : on ne fait pas la vérification de INTERNAL_IPS + que ferait la debug-toolbar par défaut, ce qui la fait fonctionner aussi à + l'intérieur de Vagrant (comportement non testé depuis un moment…) + """ + env_no_ddt = bool(os.environ.get("DJANGO_NO_DDT", None)) + return DEBUG and not env_no_ddt and not request.path.startswith("/admin/") + + +if not TESTING: + INSTALLED_APPS += ["debug_toolbar"] + MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} diff --git a/annuaire/settings/prod.py b/annuaire/settings/prod.py new file mode 100644 index 0000000..c5610a7 --- /dev/null +++ b/annuaire/settings/prod.py @@ -0,0 +1,81 @@ +""" +Settings pour la mise en production de l'annuaire. +""" + +import os + +from .common import * # NOQA +from .common import ( + BASE_DIR, + import_secret, +) + +# --- +# Prod-specific secrets +# --- + +REDIS_PASSWD = import_secret("REDIS_PASSWD") +REDIS_DB = import_secret("REDIS_DB") +REDIS_HOST = import_secret("REDIS_HOST") +REDIS_PORT = import_secret("REDIS_PORT") + +DBNAME = import_secret("DBNAME") +DBUSER = import_secret("DBUSER") +DBPASSWD = import_secret("DBPASSWD") + +# --- +# À modifier possiblement lors de la mise en production +# --- + +ALLOWED_HOSTS = ["annuaire.eleves.ens.fr", "www.annuaire.eleves.ens.fr"] + +STATIC_ROOT = os.path.join( + os.path.dirname(os.path.dirname(BASE_DIR)), "public", "annuaire", "static" +) + +STATIC_URL = "/static/" +MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") +MEDIA_URL = "/media/" + +# --- +# Cache settings +# --- + +CACHES = { + "default": { + "BACKEND": "redis_cache.RedisCache", + "LOCATION": "redis://:{passwd}@{host}:{port}/{db}".format( + passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB + ), + } +} + +# --- +# Prod database settings +# --- + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": DBNAME, + "USER": DBUSER, + "PASSWORD": DBPASSWD, + "HOST": os.environ.get("DBHOST", "localhost"), + } +} + + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", # noqa + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] diff --git a/annuaire/settings/secret_example.py b/annuaire/settings/secret_example.py new file mode 100644 index 0000000..fde5770 --- /dev/null +++ b/annuaire/settings/secret_example.py @@ -0,0 +1,15 @@ +SECRET_KEY = "$=kp$3e=xh)*4h8(_g#lprlmve_vs9_xv9hlgse%+uk9nhc==x" +ADMINS = None +SERVER_EMAIL = "root@localhost" +EMAIL_HOST = None + + +# Ne pas modifier si on utilise vagrant ! +DBUSER = "annuaire" +DBNAME = "annuaire" +DBPASSWD = "O1LxCADDA6Px5SiKvifjvdp3DSjfbp" + +REDIS_PASSWD = "dummy" +REDIS_PORT = 6379 +REDIS_DB = 0 +REDIS_HOST = "127.0.0.1" diff --git a/manage.py b/manage.py index ee859e7..ef76229 100755 --- a/manage.py +++ b/manage.py @@ -5,7 +5,7 @@ import sys def main(): - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'annuaire.settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "annuaire.settings.local") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -17,5 +17,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() From 764865c9d8350fb1640c7abd6e1999e05285bc28 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 18:41:06 +0100 Subject: [PATCH 09/15] Ajoute la debug_toolbar --- annuaire/urls.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/annuaire/urls.py b/annuaire/urls.py index a26909d..831235e 100644 --- a/annuaire/urls.py +++ b/annuaire/urls.py @@ -21,13 +21,16 @@ from fiches.views import BirthdayView, HomeView import django_cas_ng.views as cas_views urlpatterns = [ - path('admin/', admin.site.urls), - path('fiche/', include('fiches.urls')), - path('', HomeView.as_view(), name='home'), - path('birthday', BirthdayView.as_view(), name='birthday'), - path('accounts/login/', cas_views.LoginView.as_view(), name='cas_ng_login'), - path('logout', cas_views.LogoutView.as_view(), name='cas_ng_logout'), + path("admin/", admin.site.urls), + path("fiche/", include("fiches.urls")), + path("", HomeView.as_view(), name="home"), + path("birthday", BirthdayView.as_view(), name="birthday"), + path("accounts/login/", cas_views.LoginView.as_view(), name="cas_ng_login"), + path("logout", cas_views.LogoutView.as_view(), name="cas_ng_logout"), ] -if settings.DEBUG: +if settings.DEBUG and "debug_toolbar" in settings.INSTALLED_APPS: + import debug_toolbar + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] From 700272ac22e040003a0b2d8fec75ba888231c210 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 18:41:23 +0100 Subject: [PATCH 10/15] VAGRANT --- Vagrantfile | 47 +++++++++++++++++++ annuaire/settings/vagrant.py | 17 +++++++ provisioning/bootstrap.sh | 86 ++++++++++++++++++++++++++++++++++ provisioning/gunicorn.service | 15 ++++++ provisioning/nginx.conf | 49 +++++++++++++++++++ provisioning/package.list | 6 +++ provisioning/prepare_django.sh | 6 +++ 7 files changed, 226 insertions(+) create mode 100644 Vagrantfile create mode 100644 annuaire/settings/vagrant.py create mode 100644 provisioning/bootstrap.sh create mode 100644 provisioning/gunicorn.service create mode 100644 provisioning/nginx.conf create mode 100644 provisioning/package.list create mode 100644 provisioning/prepare_django.sh diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..4895fb7 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,47 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + config.vm.box = "ubuntu/focal64" + + # On associe le port 80 dans la machine virtuelle avec le port 8080 de notre + # ordinateur, et le port 8000 avec le port 8000. + config.vm.network :forwarded_port, guest: 80, host: 8080 + config.vm.network :forwarded_port, guest: 8000, host: 8000 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + # config.vm.provision "shell", inline: <<-SHELL + # sudo apt-get update + # sudo apt-get install -y apache2 + # SHELL + config.vm.provision :shell, path: "provisioning/bootstrap.sh", args: ENV['PWD'] +end diff --git a/annuaire/settings/vagrant.py b/annuaire/settings/vagrant.py new file mode 100644 index 0000000..7d3a092 --- /dev/null +++ b/annuaire/settings/vagrant.py @@ -0,0 +1,17 @@ +""" +Settings pour le développement de l'annuaire avec vagrant. +Essaie de rester le plus fidèle possible aux settings de production, +avec des différences les plus minimes possibles. +""" + +from .prod import * # noqa + +DEBUG = True + +MEDIA_ROOT = "/srv/annuaire/media" +STATIC_ROOT = "/srv/annuaire/static" + +EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" +EMAIL_FILE_PATH = "/var/mail/django" + +ALLOWED_HOSTS = ["127.0.0.1", "localhost", "0.0.0.0"] diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh new file mode 100644 index 0000000..b65df7c --- /dev/null +++ b/provisioning/bootstrap.sh @@ -0,0 +1,86 @@ +#!/bin/sh + +# Stop if an error is encountered +set -e + +PROJECTNAME=$(basename $1) +SETTINGS_MODULE="$PROJECTNAME.settings.vagrant" + +# Configuration de la base de données. Le mot de passe est constant car c'est +# pour une installation de dév locale qui ne sera accessible que depuis la +# machine virtuelle. +DBUSER=$PROJECTNAME +DBNAME=$PROJECTNAME +DBPASSWD="O1LxCADDA6Px5SiKvifjvdp3DSjfbp" + +# Installation de paquets utiles. +# Installe les paquets mentionnés dans `package.list`, en excluant les lignes +# commençant par #. +apt-get update && apt-get upgrade -y +apt-get install -y $(awk '! /^ *#/' /vagrant/provisioning/package.list) + +# Postgresql +# On teste si la db existe déjà pour ne pas essayer de la recréer +DB_EXISTS=$(sudo -u postgres psql -lqt | cut -d \| -f 1 | grep -cw $DBNAME || true) +if [ $DB_EXISTS -eq 0 ] +then + sudo -u postgres createdb $DBNAME + sudo -u postgres createuser -SdR $DBUSER + sudo -u postgres psql -c "ALTER USER $DBUSER WITH PASSWORD '$DBPASSWD';" + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DBNAME TO $DBUSER;" +fi + + +# Redis +REDIS_PASSWD="dummy" +redis-cli CONFIG SET requirepass $REDIS_PASSWD +redis-cli -a $REDIS_PASSWD CONFIG REWRITE + +# Contenu statique +mkdir -p /srv/$PROJECTNAME/static +ln -sf /vagrant/media /srv/$PROJECTNAME/media +chown -R vagrant:www-data /srv/$PROJECTNAME + +# Nginx +rm -f /etc/nginx/sites-enabled/default +sed "s/\_\_PROJECTNAME__/$PROJECTNAME/g" /vagrant/provisioning/nginx.conf > /etc/nginx/sites-enabled/$PROJECTNAME.conf +systemctl reload nginx + +# Environnement virtuel python +sudo -H -u vagrant python3 -m venv ~vagrant/venv +sudo -H -u vagrant ~vagrant/venv/bin/pip install -U pip +sudo -H -u vagrant ~vagrant/venv/bin/pip install -r /vagrant/requirements-prod.txt -r /vagrant/requirements-dev.txt + +# Préparation de Django +cd /vagrant +sudo -H -u vagrant \ + DJANGO_SETTINGS_MODULE=$SETTINGS_MODULE \ + bash -c ". ~/venv/bin/activate && bash provisioning/prepare_django.sh" +/home/vagrant/venv/bin/python manage.py collectstatic --noinput --settings $SETTINGS_MODULE + +# Mails +mkdir -p /var/mail/django +chown -R vagrant:www-data /var/mail/django + +# Service files +for file in /vagrant/provisioning/*.service +do + # failsafe si aucun fichier .service n'existe + [ -f $file ] || break + SERVICE=$(basename $file) + + # On copie en remplaçant si nécessaire le template + sed "s/\_\_PROJECTNAME__/$PROJECTNAME/g" $file > /etc/systemd/system/$SERVICE + systemctl enable $SERVICE + systemctl start $SERVICE +done + +# Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh` +cat >> ~vagrant/.bashrc < Date: Mon, 16 Nov 2020 18:50:45 +0100 Subject: [PATCH 11/15] Fix les permissions des fichiers statiques sous vagrant --- provisioning/bootstrap.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index b65df7c..9a5849d 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -56,7 +56,7 @@ cd /vagrant sudo -H -u vagrant \ DJANGO_SETTINGS_MODULE=$SETTINGS_MODULE \ bash -c ". ~/venv/bin/activate && bash provisioning/prepare_django.sh" -/home/vagrant/venv/bin/python manage.py collectstatic --noinput --settings $SETTINGS_MODULE +sudo -H -u vagrant /home/vagrant/venv/bin/python manage.py collectstatic --noinput --settings $SETTINGS_MODULE # Mails mkdir -p /var/mail/django From d3ec952de8fd681960a8d19155366c80808b634b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 19:03:24 +0100 Subject: [PATCH 12/15] Debug toolbar on vagrant --- annuaire/settings/vagrant.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/annuaire/settings/vagrant.py b/annuaire/settings/vagrant.py index 7d3a092..369a914 100644 --- a/annuaire/settings/vagrant.py +++ b/annuaire/settings/vagrant.py @@ -5,6 +5,8 @@ avec des différences les plus minimes possibles. """ from .prod import * # noqa +from .prod import TESTING, INSTALLED_APPS, MIDDLEWARE +from .local import show_toolbar DEBUG = True @@ -15,3 +17,8 @@ EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" EMAIL_FILE_PATH = "/var/mail/django" ALLOWED_HOSTS = ["127.0.0.1", "localhost", "0.0.0.0"] + +if not TESTING: + INSTALLED_APPS += ["debug_toolbar"] + MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} From 3fee57061d4ee52ca347866896ad045c0f60bb3a Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 19:07:10 +0100 Subject: [PATCH 13/15] Fix : side effects in local.py --- annuaire/settings/local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/annuaire/settings/local.py b/annuaire/settings/local.py index 7275ef6..22be900 100644 --- a/annuaire/settings/local.py +++ b/annuaire/settings/local.py @@ -57,6 +57,6 @@ def show_toolbar(request): if not TESTING: - INSTALLED_APPS += ["debug_toolbar"] + INSTALLED_APPS = INSTALLED_APPS + ["debug_toolbar"] MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} From 5ee66b453a595aae700ca8476fe5562541843953 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 19:10:02 +0100 Subject: [PATCH 14/15] README --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f9707b6..f170dab 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # Annuaire des élèves de l'ENS -## Installation +## Environnement de développement + +### Méthode facile : environnement virtuel python Il est fortement conseillé d'utiliser un environnement virtuel pour Python. @@ -33,6 +35,27 @@ Vous êtes prêts à développer ! Lancez l'annuaire avec : python manage.py runserver + +### Plus compliqué : machine virtuelle Vagrant + +Pour avoir une situation plus proche de la situation en production, il est possible de faire tourner le site depuis une +machine virtuelle Windows. Pour cela, il faut d'abord installer `vagrant` et `virtualbox` : + + sudo apt install vagrant virtualbox + +Ensuite, la commande `vagrant up` devrait créer et configurer la machine virtuelle ; un peu de patience, cela peut prendre +du temps ! Une fois fini, il y a deux possibilités : + +- `vagrant ssh` permet de se connecter à la machine, et d'effectuer des opérations à l'aide de `manage.py`. On peut effectuer +à peu près les mêmes opérations que pour un virtualenv classique, à une différence près : il faut utiliser + + python manage.py runserver 0.0.0.0:8000 + +pour lancer le serveur, afin d'y avoir accès depuis son navigateur. + +- un serveur normalement très proche de celui de production (avec `gunicorn` + `nginx`) tourne en permanence sur la machine ; +il suffit de visiter `127.0.0.1:8080` pour y avoir accès ! + ## Développement En manque d'inspiration ? N'hésitez pas à aller lire les issues ouvertes actuellement, il y en a pour tous les niveaux ! From 317077951b8b05a257f38cf62430b091b4eec7c0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 16 Nov 2020 19:11:53 +0100 Subject: [PATCH 15/15] Markdown fix --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f170dab..48b196a 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,9 @@ du temps ! Une fois fini, il y a deux possibilités : - `vagrant ssh` permet de se connecter à la machine, et d'effectuer des opérations à l'aide de `manage.py`. On peut effectuer à peu près les mêmes opérations que pour un virtualenv classique, à une différence près : il faut utiliser - python manage.py runserver 0.0.0.0:8000 +``` +python manage.py runserver 0.0.0.0:8000 +``` pour lancer le serveur, afin d'y avoir accès depuis son navigateur.