Compare commits
No commits in common. "main" and "Evarin/apparence" have entirely different histories.
main
...
Evarin/app
64 changed files with 690 additions and 1211 deletions
|
@ -1 +0,0 @@
|
|||
insecure-secret-key
|
1
.envrc
1
.envrc
|
@ -1 +0,0 @@
|
|||
use nix
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -107,5 +107,3 @@ ENV/
|
|||
# mypy
|
||||
.mypy_cache/
|
||||
rhosts_dev
|
||||
.direnv
|
||||
.pre-commit-config.yaml
|
||||
|
|
1
.rhosts
1
.rhosts
|
@ -1 +0,0 @@
|
|||
+@eleves thubrecht
|
|
@ -1,14 +1,13 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.ApiKey)
|
||||
class ApiKeyAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "last_used", "displayValue"]
|
||||
readonly_fields = ["keyId", "key", "last_used", "displayValue"]
|
||||
list_display = ['name', 'last_used', 'displayValue']
|
||||
readonly_fields = ['keyId', 'key', 'last_used', 'displayValue']
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
if not change:
|
||||
obj.initialFill()
|
||||
super().save_model(request, obj, form, change)
|
||||
super(ApiKeyAdmin, self).save_model(request, obj, form, change)
|
||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = "api"
|
||||
name = 'api'
|
||||
|
|
|
@ -1,93 +1,86 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
""" API client for bocal-site """
|
||||
''' API client for bocal-site '''
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import urllib.request
|
||||
import hmac
|
||||
import hashlib
|
||||
from datetime import datetime, date
|
||||
import argparse
|
||||
import os.path
|
||||
import sys
|
||||
import urllib.request
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
def sendReq(url):
|
||||
def send(payload, host):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
"https://{}/{}".format(host, url), json.dumps(payload).encode("ascii")
|
||||
)
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req = urllib.request.Request('https://{}/{}'.format(host, url),
|
||||
json.dumps(payload).encode('ascii'))
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
handle = urllib.request.urlopen(req)
|
||||
code = handle.getcode()
|
||||
content = handle.read()
|
||||
handle.close()
|
||||
return (code, content.decode("utf-8"))
|
||||
return (code, content.decode('utf-8'))
|
||||
except urllib.error.HTTPError as e:
|
||||
return (e.code, e.read().decode("utf-8"))
|
||||
return (e.code, e.read().decode('utf-8'))
|
||||
|
||||
def authentify(apiKey, payload):
|
||||
keyId, key = apiKey.split("$")
|
||||
keyId, key = apiKey.split('$')
|
||||
keyId = int(keyId)
|
||||
time = datetime.now().timestamp()
|
||||
mac = hmac.new(
|
||||
key.encode("utf-8"),
|
||||
msg=str(int(time)).encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
)
|
||||
mac = hmac.new(key.encode('utf-8'),
|
||||
msg=str(int(time)).encode('utf-8'),
|
||||
digestmod=hashlib.sha256)
|
||||
payload_enc = json.dumps(payload)
|
||||
mac.update(payload_enc.encode("utf-8"))
|
||||
mac.update(payload_enc.encode('utf-8'))
|
||||
|
||||
auth = {
|
||||
"keyId": keyId,
|
||||
"timestamp": time,
|
||||
"hmac": mac.hexdigest(),
|
||||
'keyId': keyId,
|
||||
'timestamp': time,
|
||||
'hmac': mac.hexdigest(),
|
||||
}
|
||||
|
||||
return {
|
||||
"auth": auth,
|
||||
"req": payload_enc,
|
||||
'auth': auth,
|
||||
'req': payload_enc,
|
||||
}
|
||||
|
||||
def decorator(fct):
|
||||
"""Decorator. Adds authentication layer."""
|
||||
|
||||
''' Decorator. Adds authentication layer. '''
|
||||
def wrap(host, apiKey, *args, **kwargs):
|
||||
innerReq = fct(*args, **kwargs)
|
||||
payload = authentify(apiKey, innerReq)
|
||||
return send(payload, host)
|
||||
|
||||
return wrap
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@sendReq(url="api/publish")
|
||||
@sendReq(url='api/publish')
|
||||
def publish(bocId, url, date):
|
||||
return {
|
||||
"id": bocId,
|
||||
"url": url,
|
||||
"date": date.strftime("%Y-%m-%d"),
|
||||
'id': bocId,
|
||||
'url': url,
|
||||
'date': date.strftime('%Y-%m-%d'),
|
||||
}
|
||||
|
||||
|
||||
###############################################################################
|
||||
|
||||
TOKEN_DFT_FILE = os.path.expanduser("~/.bocal_api_token")
|
||||
DFT_HOST = "bocal.cof.ens.fr"
|
||||
DFT_HOST = 'bocal.cof.ens.fr'
|
||||
|
||||
|
||||
def read_token(path):
|
||||
token = ""
|
||||
token = ''
|
||||
try:
|
||||
with open(path, "r") as handle:
|
||||
with open(path, 'r') as handle:
|
||||
token = handle.readline().strip()
|
||||
except FileNotFoundError:
|
||||
print(
|
||||
"[Erreur] Fichier d'identifiants absent (`{}`).".format(path),
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("[Erreur] Fichier d'identifiants absent (`{}`).".format(path),
|
||||
file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return token
|
||||
|
||||
|
@ -96,7 +89,6 @@ def cmd(func):
|
|||
def wrap(parse_args, *args, **kwargs):
|
||||
token = read_token(parse_args.creds)
|
||||
return func(token, parse_args, *args, **kwargs)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
|
@ -105,9 +97,13 @@ def cmd_publish(token, args):
|
|||
if not args.date:
|
||||
publish_date = date.today()
|
||||
else:
|
||||
year, month, day = [int(x) for x in args.date.strip().split("-")]
|
||||
year, month, day = [int(x) for x in args.date.strip().split('-')]
|
||||
publish_date = date(year=year, month=month, day=day)
|
||||
(ret_code, ret_str) = publish(args.host, token, args.numero, args.url, publish_date)
|
||||
(ret_code, ret_str) = publish(args.host,
|
||||
token,
|
||||
args.numero,
|
||||
args.url,
|
||||
publish_date)
|
||||
if ret_code == 200:
|
||||
print("Succès :)")
|
||||
else:
|
||||
|
@ -117,30 +113,29 @@ def cmd_publish(token, args):
|
|||
|
||||
def setup_argparse():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--host",
|
||||
help=("Adresse du site à contacter (par défaut, " "`{}`).".format(DFT_HOST)),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--creds",
|
||||
help=(
|
||||
"Fichier contenant le token API à utiliser "
|
||||
"(par défaut, `{}`)".format(TOKEN_DFT_FILE)
|
||||
),
|
||||
)
|
||||
parser.add_argument('--host',
|
||||
help=("Adresse du site à contacter (par défaut, "
|
||||
"`{}`).".format(DFT_HOST)))
|
||||
parser.add_argument('--creds',
|
||||
help=("Fichier contenant le token API à utiliser "
|
||||
"(par défaut, `{}`)".format(TOKEN_DFT_FILE)))
|
||||
parser.set_defaults(creds=TOKEN_DFT_FILE, host=DFT_HOST)
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
parser_publish = subparsers.add_parser("publier", help="Publier un numéro du BOcal")
|
||||
parser_publish.add_argument("numero", help="Numéro du BOcal")
|
||||
parser_publish.add_argument("url", help="Adresse (locale) du PDF du BOcal")
|
||||
parser_publish.add_argument("-d", "--date", help="Date de publication indiquée")
|
||||
parser_publish = subparsers.add_parser('publier',
|
||||
help='Publier un numéro du BOcal')
|
||||
parser_publish.add_argument('numero',
|
||||
help='Numéro du BOcal')
|
||||
parser_publish.add_argument('url',
|
||||
help='Adresse (locale) du PDF du BOcal')
|
||||
parser_publish.add_argument('-d', '--date',
|
||||
help="Date de publication indiquée")
|
||||
parser_publish.set_defaults(func=cmd_publish)
|
||||
|
||||
out_args = parser.parse_args()
|
||||
if "func" not in out_args: # No subcommand provided
|
||||
if 'func' not in out_args: # No subcommand provided
|
||||
print("You must provide a command.", file=sys.stderr)
|
||||
print(parser.parse_args(["-h"]), file=sys.stderr)
|
||||
print(parser.parse_args(['-h']), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return out_args
|
||||
|
||||
|
@ -150,5 +145,5 @@ def main():
|
|||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import datetime
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
@ -11,37 +10,17 @@ class Migration(migrations.Migration):
|
|||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ApiKey",
|
||||
name='ApiKey',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("key", models.CharField(max_length=128, verbose_name="API key")),
|
||||
(
|
||||
"name",
|
||||
models.CharField(
|
||||
help_text="Where is this key used from?",
|
||||
max_length=256,
|
||||
verbose_name="Key name",
|
||||
),
|
||||
),
|
||||
(
|
||||
"last_used",
|
||||
models.DateTimeField(
|
||||
default=datetime.datetime(1970, 1, 1, 1, 0),
|
||||
verbose_name="Last used",
|
||||
),
|
||||
),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.CharField(max_length=128, verbose_name='API key')),
|
||||
('name', models.CharField(help_text='Where is this key used from?', max_length=256, verbose_name='Key name')),
|
||||
('last_used', models.DateTimeField(default=datetime.datetime(1970, 1, 1, 1, 0), verbose_name='Last used')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,30 +1,30 @@
|
|||
import hashlib
|
||||
from django.db import models
|
||||
from datetime import datetime, timedelta
|
||||
import hmac
|
||||
import hashlib
|
||||
import random
|
||||
import string
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ApiKey(models.Model):
|
||||
"""An API key, to login using the API
|
||||
''' An API key, to login using the API
|
||||
|
||||
An API key consists in a somewhat long chunk of ascii text, *not*
|
||||
containing any dollar ($) sign. It is saved on the client's machine as
|
||||
a string "keyId$key".
|
||||
An API token (to authentify a request) is a triplet (ts, kid, hmac) of
|
||||
a timestamp `ts`, the key id `kid` and
|
||||
hmac = `HMAC(key, ts + data, sha256)`
|
||||
where `data` is the normalized (`json.dumps(json.loads(...))`) value of
|
||||
the data part of the request.
|
||||
"""
|
||||
|
||||
key = models.CharField("API key", max_length=128)
|
||||
name = models.CharField(
|
||||
"Key name", max_length=256, help_text="Where is this key used from?"
|
||||
)
|
||||
last_used = models.DateTimeField("Last used", default=datetime.fromtimestamp(0))
|
||||
An API key consists in a somewhat long chunk of ascii text, *not*
|
||||
containing any dollar ($) sign. It is saved on the client's machine as
|
||||
a string "keyId$key".
|
||||
An API token (to authentify a request) is a triplet (ts, kid, hmac) of
|
||||
a timestamp `ts`, the key id `kid` and
|
||||
hmac = `HMAC(key, ts + data, sha256)`
|
||||
where `data` is the normalized (`json.dumps(json.loads(...))`) value of
|
||||
the data part of the request.
|
||||
'''
|
||||
key = models.CharField("API key",
|
||||
max_length=128)
|
||||
name = models.CharField("Key name",
|
||||
max_length=256,
|
||||
help_text="Where is this key used from?")
|
||||
last_used = models.DateTimeField("Last used",
|
||||
default=datetime.fromtimestamp(0))
|
||||
|
||||
def everUsed(self):
|
||||
return self.last_used > datetime.fromtimestamp(0)
|
||||
|
@ -33,11 +33,12 @@ class ApiKey(models.Model):
|
|||
if not self.key:
|
||||
KEY_SIZE = 64
|
||||
KEY_CHARS = string.ascii_letters + string.digits
|
||||
self.key = "".join([random.choice(KEY_CHARS) for _ in range(KEY_SIZE)])
|
||||
self.key = ''.join(
|
||||
[random.choice(KEY_CHARS) for _ in range(KEY_SIZE)])
|
||||
|
||||
@property
|
||||
def keyId(self):
|
||||
return self.pk
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def displayValue(self):
|
||||
|
@ -51,11 +52,9 @@ class ApiKey(models.Model):
|
|||
if datetime.now() - timedelta(minutes=5) > claimedDate:
|
||||
return False
|
||||
|
||||
mac = hmac.new(
|
||||
self.key.encode("utf-8"),
|
||||
msg=str(int(claimedDate.timestamp())).encode("utf-8"),
|
||||
digestmod=hashlib.sha256,
|
||||
)
|
||||
mac.update(data.encode("utf-8"))
|
||||
mac = hmac.new(self.key.encode('utf-8'),
|
||||
msg=str(int(claimedDate.timestamp())).encode('utf-8'),
|
||||
digestmod=hashlib.sha256)
|
||||
mac.update(data.encode('utf-8'))
|
||||
|
||||
return hmac.compare_digest(mac.hexdigest(), inpMac)
|
||||
|
|
3
api/tests.py
Normal file
3
api/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,9 +1,8 @@
|
|||
from django.urls import path
|
||||
|
||||
from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
app_name = "manisite"
|
||||
app_name = 'manisite'
|
||||
|
||||
urlpatterns = [
|
||||
path("publish", views.publishApiView),
|
||||
url(r'^publish$', views.publishApiView),
|
||||
]
|
||||
|
|
54
api/views.py
54
api/views.py
|
@ -1,30 +1,27 @@
|
|||
import datetime
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import response
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
import mainsite.models as mainModels
|
||||
import json
|
||||
import datetime
|
||||
|
||||
from . import models
|
||||
import mainsite.models as mainModels
|
||||
|
||||
|
||||
def authentify(data, payload):
|
||||
"""returns whether the request's authentification is correct"""
|
||||
required = ["keyId", "timestamp", "hmac"]
|
||||
''' returns whether the request's authentification is correct '''
|
||||
required = ['keyId', 'timestamp', 'hmac']
|
||||
for field in required:
|
||||
if field not in data:
|
||||
return response.HttpResponseForbidden(
|
||||
'Missing required field "{}"'.format(field)
|
||||
)
|
||||
'Missing required field "{}"'.format(field))
|
||||
try:
|
||||
key = models.ApiKey.objects.get(id=data["keyId"])
|
||||
key = models.ApiKey.objects.get(id=data['keyId'])
|
||||
except models.ApiKey.DoesNotExist:
|
||||
return response.HttpResponseForbidden("Bad authentication")
|
||||
return response.HttpResponseForbidden('Bad authentication')
|
||||
|
||||
if not key.isCorrect(data["timestamp"], data["hmac"], payload):
|
||||
return response.HttpResponseForbidden("Bad authentication")
|
||||
if not key.isCorrect(data['timestamp'], data['hmac'], payload):
|
||||
return response.HttpResponseForbidden('Bad authentication')
|
||||
|
||||
|
||||
def apiView(required=[]):
|
||||
|
@ -32,15 +29,15 @@ def apiView(required=[]):
|
|||
@csrf_exempt
|
||||
def wrap(request, *args, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
data = json.loads(request.body.decode('utf-8'))
|
||||
except TypeError:
|
||||
return response.HttpResponseBadRequest("Bad packet format")
|
||||
except json.decoder.JSONDecodeError:
|
||||
return response.HttpResponseBadRequest("Bad json")
|
||||
|
||||
try:
|
||||
authData = data["auth"]
|
||||
reqDataOrig = data["req"]
|
||||
authData = data['auth']
|
||||
reqDataOrig = data['req']
|
||||
except KeyError:
|
||||
return response.HttpResponseBadRequest("Bad request format")
|
||||
|
||||
|
@ -54,40 +51,39 @@ def apiView(required=[]):
|
|||
for field in required:
|
||||
if field not in reqData:
|
||||
return response.HttpResponseBadRequest(
|
||||
"Missing field {}".format(field)
|
||||
)
|
||||
"Missing field {}".format(field))
|
||||
|
||||
authVal = authentify(authData, reqDataOrig)
|
||||
if authVal is not None:
|
||||
return authVal
|
||||
|
||||
return fct(request, reqData, *args, **kwargs)
|
||||
|
||||
return wrap
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@apiView(required=["id", "url", "date"])
|
||||
def publishApiView(request, data):
|
||||
"""Publish a BOcal, and create the corresponding year if needed"""
|
||||
if mainModels.Publication.objects.filter(num=data["id"]).count() > 0:
|
||||
''' Publish a BOcal, and create the corresponding year if needed '''
|
||||
if mainModels.Publication.objects.filter(num=data['id']).count() > 0:
|
||||
return response.HttpResponseBadRequest(
|
||||
"Un BOcal du même numéro est déjà présent ! Ajoutez celui-ci à la "
|
||||
"main si vous voulez vraiment faire ça."
|
||||
)
|
||||
"main si vous voulez vraiment faire ça.")
|
||||
|
||||
try:
|
||||
year, month, day = [int(x) for x in data["date"].split("-")]
|
||||
year, month, day = [int(x) for x in data['date'].split('-')]
|
||||
date = datetime.date(year, month, day)
|
||||
except Exception:
|
||||
except:
|
||||
return response.HttpResponseBadRequest("Bad date")
|
||||
|
||||
pub = mainModels.Publication(num=data["id"], url=data["url"], date=date)
|
||||
pub = mainModels.Publication(num=data['id'],
|
||||
url=data['url'],
|
||||
date=date)
|
||||
try:
|
||||
pub.full_clean()
|
||||
except ValidationError as e:
|
||||
return response.HttpResponseBadRequest("Invalid data: {}".format(e))
|
||||
return response.HttpResponseBadRequest(
|
||||
"Invalid data: {}".format(e))
|
||||
pub.save()
|
||||
|
||||
pub.createPubYear()
|
||||
|
|
193
app/settings.py
193
app/settings.py
|
@ -1,193 +0,0 @@
|
|||
"""
|
||||
Django settings for the bocal project
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from loadcredential import Credentials
|
||||
|
||||
credentials = Credentials(env_prefix="BOCAL_")
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = credentials["SECRET_KEY"]
|
||||
|
||||
# WARNING: don't run with debug turned on in production!
|
||||
DEBUG = credentials.get_json("DEBUG", False)
|
||||
|
||||
ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", [])
|
||||
|
||||
ADMINS = credentials.get_json("ADMINS", [])
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
|
||||
###
|
||||
# List the installed applications
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"solo",
|
||||
"markdownx",
|
||||
"django_cas_ng",
|
||||
"mainsite",
|
||||
"api",
|
||||
"bocal_auth",
|
||||
]
|
||||
|
||||
|
||||
###
|
||||
# List the installed middlewares
|
||||
|
||||
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_cas_ng.middleware.CASMiddleware",
|
||||
]
|
||||
|
||||
|
||||
###
|
||||
# The main url configuration
|
||||
|
||||
ROOT_URLCONF = "app.urls"
|
||||
|
||||
|
||||
###
|
||||
# Template configuration:
|
||||
# - Django Templating Language is used
|
||||
# - Application directories can be used
|
||||
|
||||
|
||||
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",
|
||||
"mainsite.context_processors.sidebar_years",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
###
|
||||
# Database configuration
|
||||
# -> https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||
|
||||
DATABASES = credentials.get_json(
|
||||
"DATABASES",
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": BASE_DIR / "db.sqlite3",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
CACHES = credentials.get_json(
|
||||
"CACHES",
|
||||
default={
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
###
|
||||
# WSGI application configuration
|
||||
|
||||
WSGI_APPLICATION = "app.wsgi.application"
|
||||
|
||||
|
||||
###
|
||||
# Staticfiles configuration
|
||||
|
||||
STATIC_ROOT = credentials["STATIC_ROOT"]
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
MEDIA_ROOT = credentials.get("MEDIA_ROOT", BASE_DIR / "media")
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
|
||||
###
|
||||
# Internationalization configuration
|
||||
# -> https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "fr-fr"
|
||||
TIME_ZONE = "Europe/Paris"
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
LANGUAGES = [
|
||||
("fr", _("Français")),
|
||||
]
|
||||
|
||||
|
||||
###
|
||||
# Authentication configuration
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"bocal_auth.cas_backend.BOcalCASBackend",
|
||||
]
|
||||
|
||||
CAS_ADMIN_PREFIX = "/yaes5eiS" # we don't want CAS to take over /admin auth
|
||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||
CAS_FORCE_CHANGE_USERNAME_CASE = "lower"
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_LOGOUT_COMPLETELY = False
|
||||
CAS_REDIRECT_URL = "/"
|
||||
CAS_SERVER_URL = "https://cas.eleves.ens.fr/"
|
||||
CAS_VERIFY_URL = "https://cas.eleves.ens.fr/"
|
||||
CAS_VERSION = "CAS_2_SAML_1_0"
|
||||
|
||||
LOGIN_URL = "/accounts/login"
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{"NAME": f"django.contrib.auth.password_validation.{v}"}
|
||||
for v in [
|
||||
"UserAttributeSimilarityValidator",
|
||||
"MinimumLengthValidator",
|
||||
"CommonPasswordValidator",
|
||||
"NumericPasswordValidator",
|
||||
]
|
||||
]
|
||||
|
||||
RHOSTS_PATH = credentials["RHOSTS_PATH"]
|
||||
|
||||
|
||||
###
|
||||
# MarkdownX configuration
|
||||
|
||||
MARKDOWNX_EDITOR_RESIZABLE = False
|
||||
|
||||
|
||||
# Development settings
|
||||
if DEBUG:
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
39
app/urls.py
39
app/urls.py
|
@ -1,39 +0,0 @@
|
|||
import django.contrib.auth.views as dj_auth_views
|
||||
import django_cas_ng.views
|
||||
import markdownx.urls
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import include, path
|
||||
|
||||
import api.urls
|
||||
import bocal_auth.views as auth_views
|
||||
import mainsite.urls
|
||||
from bocal_auth.rhosts import forceReevalRhosts
|
||||
|
||||
# Force the user to login through the custom login page
|
||||
admin.site.login = login_required(forceReevalRhosts(admin.site.login))
|
||||
|
||||
cas_patterns = [
|
||||
path("login/", django_cas_ng.views.LoginView.as_view(), name="cas_ng_login"),
|
||||
path("logout/", django_cas_ng.views.LogoutView.as_view(), name="cas_ng_logout"),
|
||||
path(
|
||||
"callback/",
|
||||
django_cas_ng.views.CallbackView.as_view(),
|
||||
name="cas_ng_proxy_callback",
|
||||
),
|
||||
]
|
||||
|
||||
accounts_patterns = [
|
||||
path("cas/", include(cas_patterns)),
|
||||
path("login/", auth_views.login, name="login"),
|
||||
path("logout/", auth_views.logout, name="logout"),
|
||||
path("password_login/", dj_auth_views.LoginView.as_view(), name="password_login"),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("markdownx/", include(markdownx.urls)),
|
||||
path("api/", include(api.urls)),
|
||||
path("accounts/", include(accounts_patterns)),
|
||||
path("", include(mainsite.urls)),
|
||||
]
|
1
bocal/.gitignore
vendored
Normal file
1
bocal/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
settings.py
|
103
bocal/settings_base.py
Normal file
103
bocal/settings_base.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
"""
|
||||
Django settings for bocal project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.11.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Public dir: a good base path for MEDIA_ROOT and STATIC_ROOT
|
||||
PUBLIC_DIR = os.path.join(BASE_DIR, 'public')
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'solo',
|
||||
'markdownx',
|
||||
'django_cas_ng',
|
||||
'mainsite',
|
||||
'api',
|
||||
'bocal_auth',
|
||||
]
|
||||
|
||||
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_cas_ng.middleware.CASMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'bocal.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',
|
||||
'mainsite.context_processors.sidebar_years',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'bocal.wsgi.application'
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.11/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',
|
||||
},
|
||||
]
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'bocal_auth.cas_backend.BOcalCASBackend',
|
||||
]
|
||||
|
||||
CAS_ADMIN_PREFIX = '/yaes5eiS' # we don't want CAS to take over /admin auth
|
||||
|
||||
LOGIN_URL = '/accounts/login'
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
MEDIA_URL = '/media/'
|
48
bocal/settings_dev.py
Normal file
48
bocal/settings_dev.py
Normal file
|
@ -0,0 +1,48 @@
|
|||
import os
|
||||
from .settings_base import *
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
# For production, generate a fresh one, eg. with
|
||||
# pwgen -sy 60 1
|
||||
SECRET_KEY = 'k340m-_mw#i#up8ajv9$$=$tgpji3f3j!jafj2+ken*@wo9u0%'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Paths
|
||||
|
||||
STATIC_ROOT = os.path.join(PUBLIC_DIR, 'static')
|
||||
MEDIA_ROOT = os.path.join(PUBLIC_DIR, 'media')
|
||||
|
||||
# Cas
|
||||
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
|
||||
CAS_VERIFY_URL = 'https://cas.eleves.ens.fr/'
|
||||
CAS_VERSION = 'CAS_2_SAML_1_0'
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_FORCE_CHANGE_USERNAME_CASE = 'lower'
|
||||
CAS_REDIRECT_URL = '/'
|
||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||
CAS_LOGOUT_COMPLETELY = False
|
||||
|
||||
# Auth
|
||||
RHOSTS_PATH = 'rhosts_dev'
|
59
bocal/settings_prod.py
Normal file
59
bocal/settings_prod.py
Normal file
|
@ -0,0 +1,59 @@
|
|||
import os
|
||||
from .settings_base import *
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
# For production, generate a fresh one, eg. with
|
||||
# pwgen -sy 60 1
|
||||
SECRET_KEY = 'CHANGEMEQUICKLY' # FIXME
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['localhost',
|
||||
] # FIXME: add your domain name(s) here.
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases
|
||||
DATABASES = {
|
||||
'default': { # FIXME add real settings
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': '', # DB name
|
||||
'USER': '', # DB user
|
||||
'PASSWORD': '', # user's password
|
||||
'HOST': 'localhost', # DB host -- change if DB is not local
|
||||
'PORT': '5432', # DB port -- 5432 is the default port for postgres
|
||||
},
|
||||
|
||||
# Alternatively, use sqlite3 (if you don't really have a choice…)
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||
# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
# }
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
LANGUAGE_CODE = 'fr-fr'
|
||||
TIME_ZONE = 'Europe/Paris'
|
||||
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Paths
|
||||
|
||||
STATIC_ROOT = os.path.join(PUBLIC_DIR, 'static')
|
||||
MEDIA_ROOT = os.path.join(PUBLIC_DIR, 'media')
|
||||
|
||||
# Cas
|
||||
CAS_SERVER_URL = 'https://example.com/' # FIXME
|
||||
CAS_VERIFY_URL = 'https://example.com/' # FIXME
|
||||
CAS_VERSION = 'CAS_2_SAML_1_0' # FIXME
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_FORCE_CHANGE_USERNAME_CASE = 'lower'
|
||||
CAS_REDIRECT_URL = '/'
|
||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" # FIXME
|
||||
CAS_LOGOUT_COMPLETELY = False
|
||||
|
||||
# Auth
|
||||
RHOSTS_PATH = '' # FIXME (path to BOcal's .rhosts)
|
54
bocal/urls.py
Normal file
54
bocal/urls.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
"""bocal URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.11/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
import django.contrib.auth.views as dj_auth_views
|
||||
|
||||
import mainsite.urls
|
||||
import bocal_auth.views as auth_views
|
||||
from bocal_auth.rhosts import forceReevalRhosts
|
||||
import markdownx.urls
|
||||
import api.urls
|
||||
|
||||
import django_cas_ng.views
|
||||
|
||||
|
||||
# Force the user to login through the custom login page
|
||||
admin.site.login = login_required(forceReevalRhosts(admin.site.login))
|
||||
|
||||
cas_patterns = [
|
||||
url(r'^login$', django_cas_ng.views.login, name='cas_ng_login'),
|
||||
url(r'^logout$', django_cas_ng.views.logout, name='cas_ng_logout'),
|
||||
url(r'^callback$', django_cas_ng.views.callback,
|
||||
name='cas_ng_proxy_callback'),
|
||||
]
|
||||
|
||||
accounts_patterns = [
|
||||
url(r'^cas/', include(cas_patterns)),
|
||||
url(r'^login$', auth_views.login, name='login'),
|
||||
url(r'^logout$', auth_views.logout, name='logout'),
|
||||
url(r'^password_login$', dj_auth_views.LoginView.as_view(),
|
||||
name='password_login'),
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^admin/', admin.site.urls),
|
||||
url(r'^markdownx/', include(markdownx.urls)),
|
||||
url(r'^api/', include(api.urls)),
|
||||
url(r'^accounts/', include(accounts_patterns)),
|
||||
url(r'^', include(mainsite.urls)),
|
||||
]
|
|
@ -1 +1 @@
|
|||
default_app_config = "bocal_auth.apps.BocalAuthConfig"
|
||||
default_app_config = 'bocal_auth.apps.BocalAuthConfig'
|
||||
|
|
|
@ -1 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class BocalAuthConfig(AppConfig):
|
||||
name = "bocal_auth"
|
||||
name = 'bocal_auth'
|
||||
|
||||
def ready(self):
|
||||
pass
|
||||
from . import signals
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django_cas_ng.backends import CASBackend
|
||||
|
||||
from .models import CasUser
|
||||
|
||||
|
||||
|
|
|
@ -1,70 +1 @@
|
|||
[
|
||||
{
|
||||
"model": "auth.group",
|
||||
"fields": {
|
||||
"name": "BOcal",
|
||||
"permissions": [
|
||||
[
|
||||
"add_apikey",
|
||||
"api",
|
||||
"apikey"
|
||||
],
|
||||
[
|
||||
"change_apikey",
|
||||
"api",
|
||||
"apikey"
|
||||
],
|
||||
[
|
||||
"delete_apikey",
|
||||
"api",
|
||||
"apikey"
|
||||
],
|
||||
[
|
||||
"add_publication",
|
||||
"mainsite",
|
||||
"publication"
|
||||
],
|
||||
[
|
||||
"change_publication",
|
||||
"mainsite",
|
||||
"publication"
|
||||
],
|
||||
[
|
||||
"delete_publication",
|
||||
"mainsite",
|
||||
"publication"
|
||||
],
|
||||
[
|
||||
"add_publicationyear",
|
||||
"mainsite",
|
||||
"publicationyear"
|
||||
],
|
||||
[
|
||||
"change_publicationyear",
|
||||
"mainsite",
|
||||
"publicationyear"
|
||||
],
|
||||
[
|
||||
"delete_publicationyear",
|
||||
"mainsite",
|
||||
"publicationyear"
|
||||
],
|
||||
[
|
||||
"add_siteconfiguration",
|
||||
"mainsite",
|
||||
"siteconfiguration"
|
||||
],
|
||||
[
|
||||
"change_siteconfiguration",
|
||||
"mainsite",
|
||||
"siteconfiguration"
|
||||
],
|
||||
[
|
||||
"delete_siteconfiguration",
|
||||
"mainsite",
|
||||
"siteconfiguration"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
[{"model": "auth.group", "fields": {"name": "BOcal", "permissions": [["add_apikey", "api", "apikey"], ["change_apikey", "api", "apikey"], ["delete_apikey", "api", "apikey"], ["add_publication", "mainsite", "publication"], ["change_publication", "mainsite", "publication"], ["delete_publication", "mainsite", "publication"], ["add_publicationyear", "mainsite", "publicationyear"], ["change_publicationyear", "mainsite", "publicationyear"], ["delete_publicationyear", "mainsite", "publicationyear"], ["add_siteconfiguration", "mainsite", "siteconfiguration"], ["change_siteconfiguration", "mainsite", "siteconfiguration"], ["delete_siteconfiguration", "mainsite", "siteconfiguration"]]}}]
|
|
@ -2,9 +2,9 @@
|
|||
# Generated by Django 1.11.5 on 2017-10-14 16:49
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -12,22 +12,14 @@ class Migration(migrations.Migration):
|
|||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("auth", "0008_alter_user_username_max_length"),
|
||||
('auth', '0008_alter_user_username_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CasUser",
|
||||
name='CasUser',
|
||||
fields=[
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class CasUser(models.Model):
|
||||
"""Describes a Django user that was created through CAS"""
|
||||
''' Describes a Django user that was created through CAS '''
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
user = models.OneToOneField(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
primary_key=True)
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
""" Reads a .rhosts file """
|
||||
''' Reads a .rhosts file '''
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from .models import CasUser
|
||||
|
||||
|
||||
def hasUser(user, allowed_domains=[]):
|
||||
"""Check that `user` appears in the rhosts file.
|
||||
''' Check that `user` appears in the rhosts file.
|
||||
If `allowed_domains` is not empty, also checks that the user belongs to one
|
||||
of the specified domains."""
|
||||
of the specified domains. '''
|
||||
|
||||
def clearLine(line):
|
||||
line = line.strip()
|
||||
hashPos = line.find("#")
|
||||
hashPos = line.find('#')
|
||||
if hashPos >= 0:
|
||||
line = line[:hashPos]
|
||||
return line
|
||||
|
||||
with open(settings.RHOSTS_PATH, "r") as handle:
|
||||
with open(settings.RHOSTS_PATH, 'r') as handle:
|
||||
for line in handle:
|
||||
line = clearLine(line)
|
||||
if not line:
|
||||
|
@ -32,7 +31,7 @@ def hasUser(user, allowed_domains=[]):
|
|||
if login != user: # Not the ones we're looking for
|
||||
continue
|
||||
|
||||
if domain[:2] != "+@": # Not a valid domain
|
||||
if domain[:2] != '+@': # Not a valid domain
|
||||
continue
|
||||
domain = domain[2:]
|
||||
|
||||
|
@ -44,16 +43,16 @@ def hasUser(user, allowed_domains=[]):
|
|||
|
||||
|
||||
def default_allowed(user):
|
||||
return hasUser(user, allowed_domains=["eleves"])
|
||||
return hasUser(user, allowed_domains=['eleves'])
|
||||
|
||||
|
||||
class NoBOcalException(Exception):
|
||||
def __str__(self):
|
||||
def __str__():
|
||||
return "The BOcal group was not created"
|
||||
|
||||
|
||||
def bocalGroup():
|
||||
qs = Group.objects.filter(name="BOcal")
|
||||
qs = Group.objects.filter(name='BOcal')
|
||||
if qs.count() != 1:
|
||||
raise NoBOcalException
|
||||
return qs[0]
|
||||
|
@ -73,15 +72,12 @@ def grantBOcalPrivileges(user):
|
|||
|
||||
def requireCasUser(fct):
|
||||
def hasCas(user):
|
||||
if user.is_anonymous:
|
||||
return False
|
||||
return CasUser.objects.filter(user=user).count() > 0
|
||||
|
||||
def wrap(user, *args, **kwargs):
|
||||
if not hasCas(user):
|
||||
return
|
||||
return fct(user, *args, **kwargs)
|
||||
|
||||
return wrap
|
||||
|
||||
|
||||
|
@ -95,12 +91,11 @@ def evalRhostsPrivileges(user):
|
|||
|
||||
@requireCasUser
|
||||
def logout(user):
|
||||
stripCasPrivileges(user)
|
||||
stripCasPrivileges()
|
||||
|
||||
|
||||
def forceReevalRhosts(fct):
|
||||
def wrap(request, *args, **kwargs):
|
||||
evalRhostsPrivileges(request.user)
|
||||
return fct(request, *args, **kwargs)
|
||||
|
||||
def wrap(req, *args, **kwargs):
|
||||
evalRhostsPrivileges(req.user)
|
||||
return fct(req, *args, **kwargs)
|
||||
return wrap
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
from django.dispatch import receiver
|
||||
from django_cas_ng.signals import cas_user_authenticated, cas_user_logout
|
||||
|
||||
from . import rhosts
|
||||
|
||||
|
||||
@receiver(cas_user_authenticated)
|
||||
def onCasLogin(sender, user, **kwargs):
|
||||
"""Called upon login of a user through CAS"""
|
||||
''' Called upon login of a user through CAS '''
|
||||
rhosts.evalRhostsPrivileges(user)
|
||||
|
||||
|
||||
@receiver(cas_user_logout)
|
||||
def onCasLogout(sender, user, **kwargs):
|
||||
"""Strip the user from their privileges — in case something goes wrong
|
||||
during the next authentication"""
|
||||
''' Strip the user from their privileges — in case something goes wrong
|
||||
during the next authentication '''
|
||||
rhosts.logout(user)
|
||||
|
|
3
bocal_auth/tests.py
Normal file
3
bocal_auth/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,49 +1,42 @@
|
|||
from urllib.parse import quote as urlquote
|
||||
|
||||
from django.shortcuts import render, redirect
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest
|
||||
from django.http.response import Http404
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
|
||||
from urllib.parse import quote as urlquote
|
||||
|
||||
|
||||
def login(request: HttpRequest):
|
||||
if request.user.is_authenticated:
|
||||
return redirect("homepage")
|
||||
def login(req):
|
||||
if req.user.is_authenticated():
|
||||
return redirect('homepage')
|
||||
|
||||
match request.method:
|
||||
case "GET":
|
||||
data = request.GET
|
||||
case "POST":
|
||||
data = request.POST
|
||||
case _:
|
||||
raise Http404
|
||||
|
||||
next = data.get("next")
|
||||
|
||||
if next is not None:
|
||||
if req.method == 'GET':
|
||||
reqDict = req.GET
|
||||
elif req.method == 'POST':
|
||||
reqDict = req.POST
|
||||
if 'next' in reqDict:
|
||||
nextUrl = reqDict['next']
|
||||
context = {
|
||||
"pass_url": "{}?next={}".format(
|
||||
reverse("password_login"), urlquote(next, safe="")
|
||||
),
|
||||
"cas_url": "{}?next={}".format(
|
||||
reverse("cas_ng_login"), urlquote(next, safe="")
|
||||
),
|
||||
'pass_url': '{}?next={}'.format(
|
||||
reverse('password_login'),
|
||||
urlquote(nextUrl, safe='')),
|
||||
'cas_url': '{}?next={}'.format(
|
||||
reverse('cas_ng_login'),
|
||||
urlquote(nextUrl, safe='')),
|
||||
}
|
||||
else:
|
||||
context = {
|
||||
"pass_url": reverse("password_login"),
|
||||
"cas_url": reverse("cas_ng_login"),
|
||||
'pass_url': reverse('password_login'),
|
||||
'cas_url': reverse('cas_ng_login'),
|
||||
}
|
||||
|
||||
return render(request, "mainsite/login.html", context=context)
|
||||
return render(req, 'mainsite/login.html', context=context)
|
||||
|
||||
|
||||
@login_required
|
||||
def logout(request: HttpRequest):
|
||||
if request.session["_auth_user_backend"] != "django_cas_ng.backends.CASBackend":
|
||||
auth_logout(request)
|
||||
return redirect("homepage")
|
||||
|
||||
return redirect("cas_ng_logout")
|
||||
def logout(req):
|
||||
CAS_BACKEND_NAME = 'django_cas_ng.backends.CASBackend'
|
||||
if req.session['_auth_user_backend'] != CAS_BACKEND_NAME:
|
||||
auth_logout(req)
|
||||
return redirect('homepage')
|
||||
return redirect('cas_ng_logout')
|
||||
|
|
78
default.nix
78
default.nix
|
@ -1,78 +0,0 @@
|
|||
{
|
||||
sources ? import ./npins,
|
||||
pkgs ? import sources.nixpkgs { },
|
||||
}:
|
||||
|
||||
let
|
||||
nix-pkgs = import sources.nix-pkgs { inherit pkgs; };
|
||||
|
||||
check = (import sources.git-hooks).run {
|
||||
src = ./.;
|
||||
|
||||
hooks = {
|
||||
# Python hooks
|
||||
black = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
isort = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
ruff = {
|
||||
enable = true;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
|
||||
# Misc Hooks
|
||||
commitizen.enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
python3 = pkgs.python3.override {
|
||||
packageOverrides = _: _: {
|
||||
inherit (nix-pkgs) django-cas-ng django-solo loadcredential;
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
devShell = pkgs.mkShell {
|
||||
name = "annuaire.dev";
|
||||
|
||||
packages = [
|
||||
(python3.withPackages (ps: [
|
||||
ps.django
|
||||
ps.django-cas-ng
|
||||
ps.django-markdownx
|
||||
ps.django-solo
|
||||
ps.markdown
|
||||
ps.pillow
|
||||
ps.loadcredential
|
||||
|
||||
# Dev packages
|
||||
ps.django-stubs
|
||||
]))
|
||||
];
|
||||
|
||||
env = {
|
||||
DJANGO_SETTINGS_MODULE = "app.settings";
|
||||
|
||||
CREDENTIALS_DIRECTORY = builtins.toString ./.credentials;
|
||||
|
||||
BOCAL_DEBUG = builtins.toJSON true;
|
||||
BOCAL_STATIC_ROOT = builtins.toString ./.static;
|
||||
BOCAL_RHOSTS_PATH = builtins.toString ./.rhosts;
|
||||
};
|
||||
|
||||
shellHook = ''
|
||||
${check.shellHook}
|
||||
|
||||
if [ ! -d .static ]; then
|
||||
mkdir .static
|
||||
fi
|
||||
'';
|
||||
};
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
from django.contrib import admin
|
||||
from solo.admin import SingletonModelAdmin
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
admin.site.register(models.SiteConfiguration, SingletonModelAdmin)
|
||||
admin.site.register(models.Publication)
|
||||
admin.site.register(models.PublicationYear)
|
||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
|||
|
||||
|
||||
class MainsiteConfig(AppConfig):
|
||||
name = "mainsite"
|
||||
name = 'mainsite'
|
||||
|
|
|
@ -7,11 +7,10 @@ def sidebar_years(req):
|
|||
avail_years = models.PublicationYear.objects.all()
|
||||
publi_years = [year for year in avail_years if year.publis().count() > 0]
|
||||
|
||||
num_special_publications = models.Publication.objects.filter(
|
||||
is_special=True
|
||||
).count()
|
||||
num_special_publications = models.Publication.objects\
|
||||
.filter(is_special=True).count()
|
||||
|
||||
return {
|
||||
"publication_years": publi_years,
|
||||
"has_special_publications": num_special_publications > 0,
|
||||
'publication_years': publi_years,
|
||||
'has_special_publications': num_special_publications > 0,
|
||||
}
|
||||
|
|
|
@ -9,57 +9,20 @@ class Migration(migrations.Migration):
|
|||
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Publication",
|
||||
name='Publication',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"num",
|
||||
models.CharField(max_length=128, verbose_name="Numéro du BOcal"),
|
||||
),
|
||||
(
|
||||
"url",
|
||||
models.CharField(
|
||||
max_length=512, verbose_name="Adresse sur le site"
|
||||
),
|
||||
),
|
||||
("date", models.DateField(verbose_name="Publication")),
|
||||
(
|
||||
"is_special",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text="Numéro du BOcal non-numéroté",
|
||||
verbose_name="Numéro spécial",
|
||||
),
|
||||
),
|
||||
(
|
||||
"descr",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
max_length=512,
|
||||
verbose_name="Description (optionnelle)",
|
||||
),
|
||||
),
|
||||
(
|
||||
"custom_name",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
help_text="Vide pour laisser le numéro seulement",
|
||||
max_length=128,
|
||||
verbose_name="Nom customisé",
|
||||
),
|
||||
),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('num', models.CharField(max_length=128, verbose_name='Numéro du BOcal')),
|
||||
('url', models.CharField(max_length=512, verbose_name='Adresse sur le site')),
|
||||
('date', models.DateField(verbose_name='Publication')),
|
||||
('is_special', models.BooleanField(default=False, help_text='Numéro du BOcal non-numéroté', verbose_name='Numéro spécial')),
|
||||
('descr', models.CharField(blank=True, max_length=512, verbose_name='Description (optionnelle)')),
|
||||
('custom_name', models.CharField(blank=True, help_text='Vide pour laisser le numéro seulement', max_length=128, verbose_name='Nom customisé')),
|
||||
],
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,27 +8,19 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0001_initial"),
|
||||
('mainsite', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PublicationYear",
|
||||
name='PublicationYear',
|
||||
fields=[
|
||||
(
|
||||
"startYear",
|
||||
models.IntegerField(
|
||||
help_text="Année scolaire à partir du 1/08",
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="Année de début",
|
||||
),
|
||||
),
|
||||
("descr", models.TextField(verbose_name="Accroche de l'année")),
|
||||
('startYear', models.IntegerField(help_text='Année scolaire à partir du 1/08', primary_key=True, serialize=False, verbose_name='Année de début')),
|
||||
('descr', models.TextField(verbose_name="Accroche de l'année")),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="publication",
|
||||
options={"ordering": ["date"]},
|
||||
name='publication',
|
||||
options={'ordering': ['date']},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,41 +8,20 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0002_auto_20170922_1438"),
|
||||
('mainsite', '0002_auto_20170922_1438'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SiteConfiguration",
|
||||
name='SiteConfiguration',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"homepageText",
|
||||
models.TextField(verbose_name="Texte de la page d'accueil (HTML)"),
|
||||
),
|
||||
(
|
||||
"writearticleText",
|
||||
models.TextField(verbose_name="Texte de la page « écrire » (HTML)"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.CharField(
|
||||
help_text="Attention au spam…",
|
||||
max_length=128,
|
||||
verbose_name="Adresse de contact du BOcal",
|
||||
),
|
||||
),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('homepageText', models.TextField(verbose_name="Texte de la page d'accueil (HTML)")),
|
||||
('writearticleText', models.TextField(verbose_name='Texte de la page « écrire » (HTML)')),
|
||||
('email', models.CharField(help_text='Attention au spam…', max_length=128, verbose_name='Adresse de contact du BOcal')),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Configuration du site",
|
||||
'verbose_name': 'Configuration du site',
|
||||
},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,17 +8,13 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0003_siteconfiguration"),
|
||||
('mainsite', '0003_siteconfiguration'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="publication",
|
||||
name="in_year_view_anyway",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Si le numéro est spécial, l'afficher quand même dans la page de l'année correspondante.",
|
||||
verbose_name="Aussi dans l'année",
|
||||
),
|
||||
model_name='publication',
|
||||
name='in_year_view_anyway',
|
||||
field=models.BooleanField(default=False, help_text="Si le numéro est spécial, l'afficher quand même dans la page de l'année correspondante.", verbose_name="Aussi dans l'année"),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,12 +8,12 @@ from django.db import migrations
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0004_publication_in_year_view_anyway"),
|
||||
('mainsite', '0004_publication_in_year_view_anyway'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="publicationyear",
|
||||
options={"ordering": ["-startYear"]},
|
||||
name='publicationyear',
|
||||
options={'ordering': ['-startYear']},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -2,36 +2,30 @@
|
|||
# Generated by Django 1.11.5 on 2017-09-23 16:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import markdownx.models
|
||||
from django.db import migrations
|
||||
import markdownx.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0005_auto_20170922_1916"),
|
||||
('mainsite', '0005_auto_20170922_1916'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="publicationyear",
|
||||
name="descr",
|
||||
field=markdownx.models.MarkdownxField(
|
||||
verbose_name="Accroche de l'année (Markdown)"
|
||||
),
|
||||
model_name='publicationyear',
|
||||
name='descr',
|
||||
field=markdownx.models.MarkdownxField(verbose_name="Accroche de l'année (Markdown)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="siteconfiguration",
|
||||
name="homepageText",
|
||||
field=markdownx.models.MarkdownxField(
|
||||
verbose_name="Texte de la page d'accueil (Markdown)"
|
||||
),
|
||||
model_name='siteconfiguration',
|
||||
name='homepageText',
|
||||
field=markdownx.models.MarkdownxField(verbose_name="Texte de la page d'accueil (Markdown)"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="siteconfiguration",
|
||||
name="writearticleText",
|
||||
field=markdownx.models.MarkdownxField(
|
||||
verbose_name="Texte de la page « écrire » (Markdown)"
|
||||
),
|
||||
model_name='siteconfiguration',
|
||||
name='writearticleText',
|
||||
field=markdownx.models.MarkdownxField(verbose_name='Texte de la page « écrire » (Markdown)'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -2,24 +2,21 @@
|
|||
# Generated by Django 1.11.5 on 2017-09-23 16:54
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import markdownx.models
|
||||
from django.db import migrations
|
||||
import markdownx.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0006_auto_20170923_1847"),
|
||||
('mainsite', '0006_auto_20170923_1847'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="siteconfiguration",
|
||||
name="specialPublisDescr",
|
||||
field=markdownx.models.MarkdownxField(
|
||||
default="",
|
||||
verbose_name="Texte de la page des publications spéciales (Markdown)",
|
||||
),
|
||||
model_name='siteconfiguration',
|
||||
name='specialPublisDescr',
|
||||
field=markdownx.models.MarkdownxField(default='', verbose_name='Texte de la page des publications spéciales (Markdown)'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -8,17 +8,13 @@ from django.db import migrations, models
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("mainsite", "0007_siteconfiguration_specialpublisdescr"),
|
||||
('mainsite', '0007_siteconfiguration_specialpublisdescr'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="publication",
|
||||
name="unknown_date",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="La date de publication du BOcal est inconnue parce qu'il est trop vieux. La date indiquée ne servira qu'à le ranger dans une année et à ordonner les BOcals.",
|
||||
verbose_name="Date inconnue",
|
||||
),
|
||||
model_name='publication',
|
||||
name='unknown_date',
|
||||
field=models.BooleanField(default=False, help_text="La date de publication du BOcal est inconnue parce qu'il est trop vieux. La date indiquée ne servira qu'à le ranger dans une année et à ordonner les BOcals.", verbose_name='Date inconnue'),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,63 +1,59 @@
|
|||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import BooleanField, CharField, DateField, IntegerField, Q
|
||||
from markdownx.models import MarkdownxField
|
||||
from django.db.models import Q
|
||||
from django.db.models import DateField, \
|
||||
CharField, \
|
||||
BooleanField, \
|
||||
IntegerField
|
||||
from solo.models import SingletonModel
|
||||
from markdownx.models import MarkdownxField
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
class SiteConfiguration(SingletonModel):
|
||||
homepageText = MarkdownxField("Texte de la page d'accueil (Markdown)")
|
||||
writearticleText = MarkdownxField("Texte de la page « écrire » (Markdown)")
|
||||
email = CharField(
|
||||
"Adresse de contact du BOcal", max_length=128, help_text="Attention au spam…"
|
||||
)
|
||||
specialPublisDescr = MarkdownxField(
|
||||
"Texte de la page des " "publications spéciales (Markdown)"
|
||||
)
|
||||
email = CharField("Adresse de contact du BOcal",
|
||||
max_length=128,
|
||||
help_text="Attention au spam…")
|
||||
specialPublisDescr = MarkdownxField("Texte de la page des "
|
||||
"publications spéciales (Markdown)")
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration du site"
|
||||
|
||||
class Meta: # pyright: ignore
|
||||
class Meta:
|
||||
verbose_name = "Configuration du site"
|
||||
|
||||
|
||||
class Publication(models.Model):
|
||||
num = CharField("Numéro du BOcal", max_length=128)
|
||||
url = CharField("Adresse sur le site", max_length=512)
|
||||
num = CharField('Numéro du BOcal', max_length=128)
|
||||
url = CharField('Adresse sur le site', max_length=512)
|
||||
# ^ This is not a URLField because we need internal URLS, eg `/static/blah`
|
||||
|
||||
date = DateField("Publication")
|
||||
unknown_date = BooleanField(
|
||||
"Date inconnue",
|
||||
help_text=(
|
||||
"La date de publication du BOcal "
|
||||
"est inconnue parce qu'il est "
|
||||
"trop vieux. La date indiquée ne "
|
||||
"servira qu'à le ranger dans une "
|
||||
"année et à ordonner les BOcals."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
is_special = BooleanField(
|
||||
"Numéro spécial", help_text="Numéro du BOcal non-numéroté", default=False
|
||||
)
|
||||
date = DateField('Publication')
|
||||
unknown_date = BooleanField('Date inconnue',
|
||||
help_text=("La date de publication du BOcal "
|
||||
"est inconnue parce qu'il est "
|
||||
"trop vieux. La date indiquée ne "
|
||||
"servira qu'à le ranger dans une "
|
||||
"année et à ordonner les BOcals."),
|
||||
default=False)
|
||||
is_special = BooleanField('Numéro spécial',
|
||||
help_text='Numéro du BOcal non-numéroté',
|
||||
default=False)
|
||||
in_year_view_anyway = BooleanField(
|
||||
"Aussi dans l'année",
|
||||
help_text=(
|
||||
"Si le numéro est spécial, l'afficher quand même dans la "
|
||||
"page de l'année correspondante."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
descr = CharField("Description (optionnelle)", max_length=512, blank=True)
|
||||
custom_name = CharField(
|
||||
"Nom customisé",
|
||||
help_text="Vide pour laisser le numéro seulement",
|
||||
max_length=128,
|
||||
blank=True,
|
||||
)
|
||||
help_text=("Si le numéro est spécial, l'afficher quand même dans la "
|
||||
"page de l'année correspondante."),
|
||||
default=False)
|
||||
descr = CharField('Description (optionnelle)',
|
||||
max_length=512,
|
||||
blank=True)
|
||||
custom_name = CharField('Nom customisé',
|
||||
help_text='Vide pour laisser le numéro seulement',
|
||||
max_length=128,
|
||||
blank=True)
|
||||
|
||||
class NoPublicationYear(Exception):
|
||||
def __str__(self):
|
||||
|
@ -71,8 +67,8 @@ class Publication(models.Model):
|
|||
return startYear
|
||||
|
||||
def publicationYear(self):
|
||||
"""Fetch corresponding publication year
|
||||
Raise `NoPublicationYear` if there is no such entry"""
|
||||
''' Fetch corresponding publication year
|
||||
Raise `NoPublicationYear` if there is no such entry '''
|
||||
startYear = self.numericPublicationYear
|
||||
try:
|
||||
return PublicationYear.objects.get(startYear=startYear)
|
||||
|
@ -80,13 +76,11 @@ class Publication(models.Model):
|
|||
raise self.NoPublicationYear
|
||||
|
||||
def createPubYear(self):
|
||||
"""Creates the corresponding publication year if needed."""
|
||||
if (
|
||||
PublicationYear.objects.filter(
|
||||
startYear=self.numericPublicationYear
|
||||
).count()
|
||||
) == 0:
|
||||
pubYear = PublicationYear(startYear=self.numericPublicationYear, descr="")
|
||||
''' Creates the corresponding publication year if needed. '''
|
||||
if (PublicationYear.objects
|
||||
.filter(startYear=self.numericPublicationYear).count()) == 0:
|
||||
pubYear = PublicationYear(startYear=self.numericPublicationYear,
|
||||
descr='')
|
||||
pubYear.save()
|
||||
return True
|
||||
return False
|
||||
|
@ -96,51 +90,50 @@ class Publication(models.Model):
|
|||
return self.custom_name
|
||||
elif self.is_special:
|
||||
return self.num
|
||||
return "BOcal n°{}".format(self.num)
|
||||
return 'BOcal n°{}'.format(self.num)
|
||||
|
||||
@staticmethod
|
||||
def latest():
|
||||
return Publication.objects.order_by("-date")[0]
|
||||
return Publication.objects.order_by('-date')[0]
|
||||
|
||||
class Meta:
|
||||
ordering = ["date"]
|
||||
ordering = ['date']
|
||||
|
||||
|
||||
class PublicationYear(models.Model):
|
||||
startYear = IntegerField(
|
||||
"Année de début", help_text="Année scolaire à partir du 15/08", primary_key=True
|
||||
)
|
||||
startYear = IntegerField('Année de début',
|
||||
help_text='Année scolaire à partir du 1/08',
|
||||
primary_key=True)
|
||||
descr = MarkdownxField("Accroche de l'année (Markdown)")
|
||||
|
||||
def __str__(self):
|
||||
return "{}-{}".format(self.startYear, self.startYear + 1)
|
||||
return '{}-{}'.format(self.startYear, self.startYear+1)
|
||||
|
||||
def beg(self):
|
||||
"""First day of this publication year (incl.)"""
|
||||
return datetime.date(self.startYear, 8, 15)
|
||||
''' First day of this publication year (incl.) '''
|
||||
return datetime.date(self.startYear, 8, 1)
|
||||
|
||||
def end(self):
|
||||
"""Last day of this publication year (excl.)"""
|
||||
return datetime.date(self.startYear + 1, 8, 15)
|
||||
''' Last day of this publication year (excl.) '''
|
||||
return datetime.date(self.startYear + 1, 8, 1)
|
||||
|
||||
def inYear(self, date):
|
||||
return self.beg() <= date < self.end()
|
||||
|
||||
def publis(self):
|
||||
"""List of publications from this year"""
|
||||
''' List of publications from this year '''
|
||||
return Publication.objects.filter(
|
||||
Q(is_special=False) | Q(in_year_view_anyway=True),
|
||||
date__gte=self.beg(),
|
||||
date__lt=self.end(),
|
||||
)
|
||||
date__lt=self.end())
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "/{}/".format(self)
|
||||
return '/{}/'.format(self)
|
||||
|
||||
@property
|
||||
def prettyName(self):
|
||||
return "{} – {}".format(self.startYear, self.startYear + 1)
|
||||
return '{} – {}'.format(self.startYear, self.startYear+1)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-startYear"]
|
||||
ordering = ['-startYear']
|
||||
|
|
|
@ -122,65 +122,30 @@ h2 {
|
|||
}
|
||||
|
||||
/* line 62, ../sass/screen.scss */
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
text-align: left;
|
||||
padding: 15px;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
/* line 69, ../sass/screen.scss */
|
||||
strong, b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* line 73, ../sass/screen.scss */
|
||||
/* line 66, ../sass/screen.scss */
|
||||
i, em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* line 77, ../sass/screen.scss */
|
||||
/* line 70, ../sass/screen.scss */
|
||||
a {
|
||||
color: #8c0e00;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* From http://adis.ca/entry/2011/pretty-code-block-in-css/ */
|
||||
/* line 84, ../sass/screen.scss */
|
||||
pre {
|
||||
font-family: "Courier 10 Pitch", Courier, monospace;
|
||||
font-size: 80%;
|
||||
line-height: 140%;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* From http://adis.ca/entry/2011/pretty-code-block-in-css/ */
|
||||
/* line 97, ../sass/screen.scss */
|
||||
code {
|
||||
font-family: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
|
||||
font-size: 80%;
|
||||
line-height: 140%;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* line 108, ../sass/screen.scss */
|
||||
/* line 76, ../sass/screen.scss */
|
||||
header {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
@media screen and (min-width: 600px) {
|
||||
/* line 108, ../sass/screen.scss */
|
||||
/* line 76, ../sass/screen.scss */
|
||||
header {
|
||||
border-bottom: 3px solid #000;
|
||||
position: relative;
|
||||
|
@ -188,7 +153,7 @@ header {
|
|||
margin: 30px auto;
|
||||
margin-bottom: 55px;
|
||||
}
|
||||
/* line 119, ../sass/screen.scss */
|
||||
/* line 87, ../sass/screen.scss */
|
||||
header:after {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
|
@ -200,14 +165,14 @@ header {
|
|||
}
|
||||
}
|
||||
|
||||
/* line 131, ../sass/screen.scss */
|
||||
/* line 99, ../sass/screen.scss */
|
||||
.container {
|
||||
margin: 0 auto;
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
}
|
||||
@media screen and (min-width: 600px) {
|
||||
/* line 131, ../sass/screen.scss */
|
||||
/* line 99, ../sass/screen.scss */
|
||||
.container {
|
||||
display: table;
|
||||
display: flex;
|
||||
|
@ -215,26 +180,26 @@ header {
|
|||
}
|
||||
}
|
||||
|
||||
/* line 142, ../sass/screen.scss */
|
||||
/* line 110, ../sass/screen.scss */
|
||||
.sidebar {
|
||||
width: 400px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
/* line 146, ../sass/screen.scss */
|
||||
/* line 114, ../sass/screen.scss */
|
||||
.sidebar .minimenu {
|
||||
display: none;
|
||||
}
|
||||
/* line 150, ../sass/screen.scss */
|
||||
/* line 118, ../sass/screen.scss */
|
||||
.sidebar ul.nav {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
/* line 153, ../sass/screen.scss */
|
||||
/* line 121, ../sass/screen.scss */
|
||||
.sidebar ul.nav li {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
margin: 7px 0;
|
||||
}
|
||||
/* line 158, ../sass/screen.scss */
|
||||
/* line 126, ../sass/screen.scss */
|
||||
.sidebar ul.nav li a {
|
||||
border: 1px solid rgba(140, 14, 0, 0.5);
|
||||
background: #fff;
|
||||
|
@ -244,27 +209,27 @@ header {
|
|||
text-align: center;
|
||||
}
|
||||
@media screen and (min-width: 600px) {
|
||||
/* line 158, ../sass/screen.scss */
|
||||
/* line 126, ../sass/screen.scss */
|
||||
.sidebar ul.nav li a {
|
||||
box-shadow: -2px 2px 0 #f06e00;
|
||||
transition: box-shadow 0.4s ease-out, transform 0.4s ease-out;
|
||||
}
|
||||
/* line 170, ../sass/screen.scss */
|
||||
/* line 138, ../sass/screen.scss */
|
||||
.sidebar ul.nav li a:hover {
|
||||
box-shadow: -6px 6px 0 #f06e00;
|
||||
transform: translateX(2px) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* line 181, ../sass/screen.scss */
|
||||
/* line 149, ../sass/screen.scss */
|
||||
.main {
|
||||
background: #fff;
|
||||
flex-grow: 1;
|
||||
padding: 20px 40px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(140, 14, 0, 0.7);
|
||||
}
|
||||
/* line 188, ../sass/screen.scss */
|
||||
/* line 156, ../sass/screen.scss */
|
||||
.main .last-publication a {
|
||||
display: block;
|
||||
max-width: 350px;
|
||||
|
@ -280,66 +245,35 @@ header {
|
|||
box-shadow: 0 0 0 rgba(140, 14, 0, 0);
|
||||
transition: box-shadow 1s ease-out;
|
||||
}
|
||||
/* line 203, ../sass/screen.scss */
|
||||
/* line 171, ../sass/screen.scss */
|
||||
.main .last-publication a:hover {
|
||||
box-shadow: 0 0 5px #8c0e00;
|
||||
}
|
||||
/* line 208, ../sass/screen.scss */
|
||||
.main .intro-text {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
/* line 212, ../sass/screen.scss */
|
||||
.main p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
/* line 216, ../sass/screen.scss */
|
||||
.main .md-text {
|
||||
text-align: justify;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
/* line 221, ../sass/screen.scss */
|
||||
.main ol, .main ul {
|
||||
list-style-position: outside;
|
||||
margin-left: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
/* line 226, ../sass/screen.scss */
|
||||
.main ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
/* line 229, ../sass/screen.scss */
|
||||
.main ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
/* line 232, ../sass/screen.scss */
|
||||
.main li {
|
||||
margin: 0.35em 0;
|
||||
}
|
||||
|
||||
/* line 237, ../sass/screen.scss */
|
||||
/* line 177, ../sass/screen.scss */
|
||||
.publication-list {
|
||||
margin: 10px 5px;
|
||||
margin-left: 30px;
|
||||
padding: 0 30px;
|
||||
max-width: 700px;
|
||||
}
|
||||
/* line 243, ../sass/screen.scss */
|
||||
/* line 183, ../sass/screen.scss */
|
||||
.publication-list .publication-entry {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
border-bottom: 1px solid #86abcb;
|
||||
}
|
||||
/* line 249, ../sass/screen.scss */
|
||||
/* line 189, ../sass/screen.scss */
|
||||
.publication-list .publication-entry:first-child {
|
||||
border-top: 1px solid #86abcb;
|
||||
}
|
||||
/* line 253, ../sass/screen.scss */
|
||||
/* line 193, ../sass/screen.scss */
|
||||
.publication-list .publication-entry > span {
|
||||
padding: 7px 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
/* line 257, ../sass/screen.scss */
|
||||
/* line 197, ../sass/screen.scss */
|
||||
.publication-list .publication-entry .publication-date {
|
||||
text-align: right;
|
||||
font-weight: light;
|
||||
|
@ -347,16 +281,16 @@ header {
|
|||
font-style: italic;
|
||||
width: 100px;
|
||||
}
|
||||
/* line 264, ../sass/screen.scss */
|
||||
/* line 204, ../sass/screen.scss */
|
||||
.publication-list .publication-entry .publication-descr {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
/* line 267, ../sass/screen.scss */
|
||||
/* line 207, ../sass/screen.scss */
|
||||
.publication-list .publication-entry .publication-descr:before {
|
||||
content: "– ";
|
||||
}
|
||||
/* line 271, ../sass/screen.scss */
|
||||
/* line 211, ../sass/screen.scss */
|
||||
.publication-list .publication-entry a.overlay {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
@ -367,12 +301,12 @@ header {
|
|||
}
|
||||
|
||||
@media screen and (max-width: 599px) {
|
||||
/* line 283, ../sass/screen.scss */
|
||||
/* line 223, ../sass/screen.scss */
|
||||
body {
|
||||
padding-top: 75px;
|
||||
}
|
||||
|
||||
/* line 286, ../sass/screen.scss */
|
||||
/* line 226, ../sass/screen.scss */
|
||||
header, .sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
@ -380,7 +314,7 @@ header {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* line 292, ../sass/screen.scss */
|
||||
/* line 232, ../sass/screen.scss */
|
||||
header {
|
||||
z-index: 15;
|
||||
right: 60px;
|
||||
|
@ -389,13 +323,13 @@ header {
|
|||
height: 60px;
|
||||
line-height: 60px;
|
||||
}
|
||||
/* line 299, ../sass/screen.scss */
|
||||
/* line 239, ../sass/screen.scss */
|
||||
header img {
|
||||
max-height: 55px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* line 304, ../sass/screen.scss */
|
||||
/* line 244, ../sass/screen.scss */
|
||||
.sidebar {
|
||||
z-index: 14;
|
||||
background: #f8ef78;
|
||||
|
@ -405,18 +339,18 @@ header {
|
|||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
/* line 313, ../sass/screen.scss */
|
||||
/* line 253, ../sass/screen.scss */
|
||||
.sidebar .minimenu {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 5px;
|
||||
display: block;
|
||||
}
|
||||
/* line 318, ../sass/screen.scss */
|
||||
/* line 258, ../sass/screen.scss */
|
||||
.sidebar .minimenu img {
|
||||
height: 50px;
|
||||
}
|
||||
/* line 322, ../sass/screen.scss */
|
||||
/* line 262, ../sass/screen.scss */
|
||||
.sidebar ul.nav {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
@ -425,17 +359,17 @@ header {
|
|||
font-size: 0.95em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
/* line 330, ../sass/screen.scss */
|
||||
/* line 270, ../sass/screen.scss */
|
||||
.sidebar ul.nav li {
|
||||
display: inline-block;
|
||||
min-width: 150px;
|
||||
margin: 5px;
|
||||
}
|
||||
/* line 334, ../sass/screen.scss */
|
||||
/* line 274, ../sass/screen.scss */
|
||||
.sidebar ul.nav li a {
|
||||
padding: 10px;
|
||||
}
|
||||
/* line 341, ../sass/screen.scss */
|
||||
/* line 281, ../sass/screen.scss */
|
||||
.sidebar.collapse ul {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,3 @@ $orange: #f06e00;
|
|||
$blanc: #fff;
|
||||
$pfont: "Source Sans Pro", sans-serif;
|
||||
$hfont: "Bitter", serif;
|
||||
$codeGray: #f0f0f0;
|
||||
$codeFont: Monaco, Consolas, "Andale Mono", "DejaVu Sans Mono", monospace;
|
||||
$preFont: "Courier 10 Pitch", Courier, monospace;
|
||||
|
|
|
@ -59,13 +59,6 @@ h2 {
|
|||
margin-bottom: 0.7em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5em;
|
||||
text-align: left;
|
||||
padding: 15px;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
strong, b {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -80,31 +73,6 @@ a {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* From http://adis.ca/entry/2011/pretty-code-block-in-css/ */
|
||||
pre {
|
||||
font-family: $preFont;
|
||||
font-size: 80%;
|
||||
line-height: 140%;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
background-color: $codeGray;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* From http://adis.ca/entry/2011/pretty-code-block-in-css/ */
|
||||
code {
|
||||
font-family: $codeFont;
|
||||
font-size: 80%;
|
||||
line-height: 140%;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
white-space: -moz-pre-wrap;
|
||||
white-space: -o-pre-wrap;
|
||||
background-color: $codeGray;
|
||||
}
|
||||
|
||||
header {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
|
@ -181,7 +149,7 @@ header {
|
|||
.main {
|
||||
background: #fff;
|
||||
flex-grow: 1;
|
||||
padding: 20px 40px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
border: 1px solid rgba($rouge, 0.7);
|
||||
|
||||
|
@ -204,34 +172,6 @@ header {
|
|||
box-shadow: 0 0 5px $rouge;
|
||||
}
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.md-text {
|
||||
text-align: justify;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
list-style-position: outside;
|
||||
margin-left: 20px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
ol {
|
||||
list-style-type: decimal;
|
||||
}
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
li {
|
||||
margin: 0.35em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.publication-list {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
Demandez la <br />dernière édition !</a>
|
||||
</div>
|
||||
|
||||
<div class="md-text">
|
||||
{{ site_config.homepageText | markdownify }}
|
||||
</div>
|
||||
{{ site_config.homepageText | markdownify }}
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -11,7 +11,7 @@ Millésime {{ year_range }}
|
|||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<div class="intro-text md-text">{{ intro_text | safe | markdownify }}</div>
|
||||
<div class="intro-text">{{ intro_text | markdownify }}</div>
|
||||
|
||||
<ul class="publication-list">
|
||||
{% for bocal in publications %}
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
{% block content %}
|
||||
{% get_solo 'mainsite.SiteConfiguration' as site_config %}
|
||||
|
||||
<div class="md-text">
|
||||
{{ site_config.writearticleText | markdownify }}
|
||||
</div>
|
||||
{{ site_config.writearticleText | markdownify }}
|
||||
|
||||
{% endblock content %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
<nav class="navbar">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="{% url "homepage" %}">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
<div id="sidebar" class="sidebar collapse">
|
||||
<a href="javascript:void(0);" onclick="document.getElementById('sidebar').classList.toggle('collapse');" class="minimenu"><img src="{% static "img/minimenu.svg" %}"></a>
|
||||
<ul class="nav nav-sidebar">
|
||||
|
|
3
mainsite/tests.py
Normal file
3
mainsite/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
|
@ -1,33 +1,12 @@
|
|||
from django.urls import path, register_converter
|
||||
|
||||
from django.conf.urls import url
|
||||
from . import views
|
||||
|
||||
|
||||
class FourDigitYearConverter:
|
||||
regex = "[0-9]{4}"
|
||||
|
||||
def to_python(self, value):
|
||||
return int(value)
|
||||
|
||||
def to_url(self, value):
|
||||
return "%04d" % value
|
||||
|
||||
|
||||
register_converter(FourDigitYearConverter, "yyyy")
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.HomeView.as_view(), name="homepage"),
|
||||
path("robots.txt", views.robots_view, name="robots"),
|
||||
path("ecrire/", views.WriteArticleView.as_view(), name="write_article"),
|
||||
path(
|
||||
"speciaux/",
|
||||
views.SpecialPublicationsView.as_view(),
|
||||
name="special_publications",
|
||||
),
|
||||
path(
|
||||
"<yyyy:year>-<yyyy:nYear>/",
|
||||
views.YearView.as_view(),
|
||||
name="year_view",
|
||||
),
|
||||
path("latest/", views.latestPublication, name="latestPublication"),
|
||||
url(r'^$', views.HomeView.as_view(), name='homepage'),
|
||||
url(r'^ecrire$', views.WriteArticleView.as_view(), name='write_article'),
|
||||
url(r'^speciaux/', views.SpecialPublicationsView.as_view(),
|
||||
name='special_publications'),
|
||||
url(r'^(?P<year>\d{4})-(?P<nYear>\d{4})/',
|
||||
views.YearView.as_view(), name='year_view'),
|
||||
url(r'^latest$', views.latestPublication, name='latestPublication'),
|
||||
]
|
||||
|
|
|
@ -1,87 +1,91 @@
|
|||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from django.views.generic import TemplateView
|
||||
from django.http import Http404
|
||||
|
||||
from mainsite.models import Publication, PublicationYear, SiteConfiguration
|
||||
|
||||
|
||||
def robots_view(request):
|
||||
"""Robots.txt view"""
|
||||
body = "User-Agent: *\nDisallow: /\nAllow: /$\n"
|
||||
return HttpResponse(body, content_type="text/plain")
|
||||
|
||||
|
||||
class HomeView(TemplateView):
|
||||
"""Website's homepage"""
|
||||
|
||||
template_name = "mainsite/homepage.html"
|
||||
''' Website's homepage '''
|
||||
template_name = 'mainsite/homepage.html'
|
||||
|
||||
|
||||
class WriteArticleView(TemplateView):
|
||||
"""Tell the readers how they can contribute to the BOcal"""
|
||||
|
||||
template_name = "mainsite/write_article.html"
|
||||
''' Tell the readers how they can contribute to the BOcal '''
|
||||
template_name = 'mainsite/write_article.html'
|
||||
|
||||
|
||||
class PublicationListView(ListView):
|
||||
"""
|
||||
Display a list of publications (generic class).
|
||||
"""
|
||||
class PublicationListView(TemplateView):
|
||||
''' Display a list of publications (generic class).
|
||||
|
||||
model = Publication
|
||||
context_object_name = "publications"
|
||||
template_name = "mainsite/publications_list_view.html"
|
||||
Reimplement `get_publications` (called with the url template args in a
|
||||
place where you can Http404) in subclasses to get it working. '''
|
||||
|
||||
template_name = 'mainsite/publications_list_view.html'
|
||||
|
||||
def initView(self):
|
||||
''' Cannot be __init__, we don't have **kwargs there '''
|
||||
pass
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
self.initView(**kwargs)
|
||||
|
||||
if len(ctx["publications"]) == 0:
|
||||
context = super(PublicationListView, self).get_context_data(**kwargs)
|
||||
context.update(self.additional_context(**kwargs))
|
||||
|
||||
publications = self.get_publications(**kwargs)
|
||||
|
||||
if len(publications) == 0:
|
||||
raise Http404
|
||||
context['publications'] = publications
|
||||
|
||||
return ctx
|
||||
return context
|
||||
|
||||
|
||||
class YearView(PublicationListView):
|
||||
"""Display a year worth of BOcals"""
|
||||
''' Display a year worth of BOcals '''
|
||||
|
||||
def get_queryset(self):
|
||||
year: int = self.kwargs.get("year")
|
||||
nYear: int = self.kwargs.get("nYear")
|
||||
|
||||
if nYear != year + 1:
|
||||
def initView(self, year, nYear):
|
||||
try:
|
||||
year, nYear = int(year), int(nYear)
|
||||
except ValueError:
|
||||
raise Http404
|
||||
if year + 1 != nYear:
|
||||
raise Http404
|
||||
self.year = year
|
||||
|
||||
self.publication_year = get_object_or_404(PublicationYear, startYear=year)
|
||||
self.pubYear = get_object_or_404(PublicationYear, startYear=year)
|
||||
|
||||
return self.publication_year.publis()
|
||||
def additional_context(self, year, nYear):
|
||||
return {
|
||||
'intro_text': self.pubYear.descr,
|
||||
'is_year_view': True,
|
||||
'year_range': self.pubYear.prettyName,
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
intro_text=self.publication_year.descr,
|
||||
is_year_view=True,
|
||||
year_range=self.publication_year.prettyName,
|
||||
**kwargs
|
||||
)
|
||||
def get_publications(self, year, nYear):
|
||||
return self.pubYear.publis()
|
||||
|
||||
|
||||
class SpecialPublicationsView(PublicationListView):
|
||||
"""Display the list of special publications"""
|
||||
''' Display the list of special publications '''
|
||||
|
||||
ordering = "-date"
|
||||
def additional_context(self):
|
||||
siteConf = SiteConfiguration.get_solo()
|
||||
return {
|
||||
'intro_text': siteConf.specialPublisDescr,
|
||||
'is_year_view': False,
|
||||
'list_title': "Numéros spéciaux",
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(is_special=True)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
intro_text=SiteConfiguration.get_solo().specialPublisDescr,
|
||||
is_year_view=False,
|
||||
list_title="Numéros spéciaux",
|
||||
**kwargs
|
||||
)
|
||||
def get_publications(self):
|
||||
publications = Publication.objects\
|
||||
.filter(is_special=True)\
|
||||
.order_by('-date')
|
||||
return publications
|
||||
|
||||
|
||||
def latestPublication(req):
|
||||
"""Redirects to the latest standard publication"""
|
||||
''' Redirects to the latest standard publication '''
|
||||
latestPubli = Publication.latest()
|
||||
return redirect(latestPubli.url)
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
from importlib.util import find_spec
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bocal.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
if find_spec("django") is None:
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
# Generated by npins. Do not modify; will be overwritten regularly
|
||||
let
|
||||
data = builtins.fromJSON (builtins.readFile ./sources.json);
|
||||
version = data.version;
|
||||
|
||||
mkSource =
|
||||
spec:
|
||||
assert spec ? type;
|
||||
let
|
||||
path =
|
||||
if spec.type == "Git" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "GitRelease" then
|
||||
mkGitSource spec
|
||||
else if spec.type == "PyPi" then
|
||||
mkPyPiSource spec
|
||||
else if spec.type == "Channel" then
|
||||
mkChannelSource spec
|
||||
else
|
||||
builtins.throw "Unknown source type ${spec.type}";
|
||||
in
|
||||
spec // { outPath = path; };
|
||||
|
||||
mkGitSource =
|
||||
{
|
||||
repository,
|
||||
revision,
|
||||
url ? null,
|
||||
hash,
|
||||
branch ? null,
|
||||
...
|
||||
}:
|
||||
assert repository ? type;
|
||||
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
|
||||
# In the latter case, there we will always be an url to the tarball
|
||||
if url != null then
|
||||
(builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash; # FIXME: check nix version & use SRI hashes
|
||||
})
|
||||
else
|
||||
assert repository.type == "Git";
|
||||
let
|
||||
urlToName =
|
||||
url: rev:
|
||||
let
|
||||
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
|
||||
|
||||
short = builtins.substring 0 7 rev;
|
||||
|
||||
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
|
||||
in
|
||||
"${if matched == null then "source" else builtins.head matched}${appendShort}";
|
||||
name = urlToName repository.url revision;
|
||||
in
|
||||
builtins.fetchGit {
|
||||
url = repository.url;
|
||||
rev = revision;
|
||||
inherit name;
|
||||
# hash = hash;
|
||||
};
|
||||
|
||||
mkPyPiSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchurl {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
|
||||
mkChannelSource =
|
||||
{ url, hash, ... }:
|
||||
builtins.fetchTarball {
|
||||
inherit url;
|
||||
sha256 = hash;
|
||||
};
|
||||
in
|
||||
if version == 3 then
|
||||
builtins.mapAttrs (_: mkSource) data.pins
|
||||
else
|
||||
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"
|
|
@ -1,34 +0,0 @@
|
|||
{
|
||||
"pins": {
|
||||
"git-hooks": {
|
||||
"type": "Git",
|
||||
"repository": {
|
||||
"type": "GitHub",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix"
|
||||
},
|
||||
"branch": "master",
|
||||
"revision": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
|
||||
"url": "https://github.com/cachix/git-hooks.nix/archive/3c3e88f0f544d6bb54329832616af7eb971b6be6.tar.gz",
|
||||
"hash": "04pwjz423iq2nkazkys905gvsm5j39722ngavrnx42b8msr5k555"
|
||||
},
|
||||
"nix-pkgs": {
|
||||
"type": "Git",
|
||||
"repository": {
|
||||
"type": "Git",
|
||||
"url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs"
|
||||
},
|
||||
"branch": "main",
|
||||
"revision": "e3fac77b062c9fe98dc1b5a367b0a8e70cde9624",
|
||||
"url": null,
|
||||
"hash": "12xqh19mv8zgvyrh4vfnc95acf45x81g398pyqsd1xy1l7030r7i"
|
||||
},
|
||||
"nixpkgs": {
|
||||
"type": "Channel",
|
||||
"name": "nixpkgs-unstable",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre694416.ccc0c2126893/nixexprs.tar.xz",
|
||||
"hash": "0cn1z4wzps8nfqxzr6l5mbn81adcqy2cy2ic70z13fhzicmxfsbx"
|
||||
}
|
||||
},
|
||||
"version": 3
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = ["F403", "F405"]
|
|
@ -1,9 +1,8 @@
|
|||
Django==2.2
|
||||
django-cas-ng==4.1.1
|
||||
django-markdownx==3.0.1
|
||||
django-solo==1.1.3
|
||||
Markdown==3.2.2
|
||||
olefile==0.46
|
||||
Pillow==7.1.2
|
||||
Django==1.11.5
|
||||
django-cas-ng==3.5.8
|
||||
django-markdownx==2.0.21
|
||||
django-solo==1.1.2
|
||||
Markdown==2.6.9
|
||||
olefile==0.44
|
||||
Pillow==4.2.1
|
||||
pytz==2017.2
|
||||
lxml==4.5.1
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
(import ./. { }).devShell
|
Loading…
Add table
Reference in a new issue