Compare commits

..

No commits in common. "main" and "Evarin/apparence" have entirely different histories.

64 changed files with 690 additions and 1211 deletions

View file

@ -1 +0,0 @@
insecure-secret-key

1
.envrc
View file

@ -1 +0,0 @@
use nix

2
.gitignore vendored
View file

@ -107,5 +107,3 @@ ENV/
# mypy
.mypy_cache/
rhosts_dev
.direnv
.pre-commit-config.yaml

View file

@ -1 +0,0 @@
+@eleves thubrecht

View file

@ -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)

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
name = "api"
name = 'api'

View file

@ -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()

View file

@ -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')),
],
),
]

View file

@ -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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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),
]

View file

@ -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()

View file

@ -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"

View file

@ -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
View file

@ -0,0 +1 @@
settings.py

103
bocal/settings_base.py Normal file
View 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
View 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
View 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
View 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)),
]

View file

@ -1 +1 @@
default_app_config = "bocal_auth.apps.BocalAuthConfig"
default_app_config = 'bocal_auth.apps.BocalAuthConfig'

View file

@ -1 +1,3 @@
from django.contrib import admin
# Register your models here.

View file

@ -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

View file

@ -1,5 +1,4 @@
from django_cas_ng.backends import CASBackend
from .models import CasUser

View file

@ -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"]]}}]

View file

@ -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)),
],
),
]

View file

@ -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)

View file

@ -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

View file

@ -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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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')

View file

@ -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
'';
};
}

View file

@ -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)

View file

@ -2,4 +2,4 @@ from django.apps import AppConfig
class MainsiteConfig(AppConfig):
name = "mainsite"
name = 'mainsite'

View file

@ -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,
}

View file

@ -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é')),
],
),
]

View file

@ -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']},
),
]

View file

@ -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',
},
),
]

View file

@ -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"),
),
]

View file

@ -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']},
),
]

View file

@ -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)'),
),
]

View file

@ -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,
),
]

View file

@ -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'),
),
]

View file

@ -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']

View file

@ -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;
}

View file

@ -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;

View file

@ -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 {

View file

@ -1,4 +1,4 @@
{% load static %}
{% load staticfiles %}
<!DOCTYPE html>
<html lang="fr">

View file

@ -10,8 +10,6 @@
Demandez la <br />dernière édition&nbsp;!</a>
</div>
<div class="md-text">
{{ site_config.homepageText | markdownify }}
</div>
{{ site_config.homepageText | markdownify }}
{% endblock content %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -1,4 +1,4 @@
{% load static %}
{% load staticfiles %}
<nav class="navbar">
<div class="navbar-header">
<a class="navbar-brand" href="{% url "homepage" %}">

View file

@ -1,4 +1,4 @@
{% load static %}
{% load staticfiles %}
<!DOCTYPE html>
<html lang="fr">

View file

@ -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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View file

@ -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'),
]

View file

@ -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)

View file

@ -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 "

View file

@ -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`"

View file

@ -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
}

View file

@ -1,5 +0,0 @@
[tool.isort]
profile = "black"
[tool.ruff.lint]
ignore = ["F403", "F405"]

View file

@ -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

View file

@ -1 +0,0 @@
(import ./. { }).devShell