chore: Run formatters and fix lint errors
This commit is contained in:
parent
09ad2ca896
commit
430b3b40bb
35 changed files with 415 additions and 284 deletions
|
@ -1,11 +1,12 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.ApiKey)
|
@admin.register(models.ApiKey)
|
||||||
class ApiKeyAdmin(admin.ModelAdmin):
|
class ApiKeyAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'last_used', 'displayValue']
|
list_display = ["name", "last_used", "displayValue"]
|
||||||
readonly_fields = ['keyId', 'key', 'last_used', 'displayValue']
|
readonly_fields = ["keyId", "key", "last_used", "displayValue"]
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if not change:
|
if not change:
|
||||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class ApiConfig(AppConfig):
|
class ApiConfig(AppConfig):
|
||||||
name = 'api'
|
name = "api"
|
||||||
|
|
|
@ -1,86 +1,93 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
''' API client for bocal-site '''
|
""" API client for bocal-site """
|
||||||
|
|
||||||
import json
|
|
||||||
import urllib.request
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
from datetime import datetime, date
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
|
||||||
def sendReq(url):
|
def sendReq(url):
|
||||||
def send(payload, host):
|
def send(payload, host):
|
||||||
try:
|
try:
|
||||||
req = urllib.request.Request('https://{}/{}'.format(host, url),
|
req = urllib.request.Request(
|
||||||
json.dumps(payload).encode('ascii'))
|
"https://{}/{}".format(host, url), json.dumps(payload).encode("ascii")
|
||||||
req.add_header('Content-Type', 'application/json')
|
)
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
handle = urllib.request.urlopen(req)
|
handle = urllib.request.urlopen(req)
|
||||||
code = handle.getcode()
|
code = handle.getcode()
|
||||||
content = handle.read()
|
content = handle.read()
|
||||||
handle.close()
|
handle.close()
|
||||||
return (code, content.decode('utf-8'))
|
return (code, content.decode("utf-8"))
|
||||||
except urllib.error.HTTPError as e:
|
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):
|
def authentify(apiKey, payload):
|
||||||
keyId, key = apiKey.split('$')
|
keyId, key = apiKey.split("$")
|
||||||
keyId = int(keyId)
|
keyId = int(keyId)
|
||||||
time = datetime.now().timestamp()
|
time = datetime.now().timestamp()
|
||||||
mac = hmac.new(key.encode('utf-8'),
|
mac = hmac.new(
|
||||||
msg=str(int(time)).encode('utf-8'),
|
key.encode("utf-8"),
|
||||||
digestmod=hashlib.sha256)
|
msg=str(int(time)).encode("utf-8"),
|
||||||
|
digestmod=hashlib.sha256,
|
||||||
|
)
|
||||||
payload_enc = json.dumps(payload)
|
payload_enc = json.dumps(payload)
|
||||||
mac.update(payload_enc.encode('utf-8'))
|
mac.update(payload_enc.encode("utf-8"))
|
||||||
|
|
||||||
auth = {
|
auth = {
|
||||||
'keyId': keyId,
|
"keyId": keyId,
|
||||||
'timestamp': time,
|
"timestamp": time,
|
||||||
'hmac': mac.hexdigest(),
|
"hmac": mac.hexdigest(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'auth': auth,
|
"auth": auth,
|
||||||
'req': payload_enc,
|
"req": payload_enc,
|
||||||
}
|
}
|
||||||
|
|
||||||
def decorator(fct):
|
def decorator(fct):
|
||||||
''' Decorator. Adds authentication layer. '''
|
"""Decorator. Adds authentication layer."""
|
||||||
|
|
||||||
def wrap(host, apiKey, *args, **kwargs):
|
def wrap(host, apiKey, *args, **kwargs):
|
||||||
innerReq = fct(*args, **kwargs)
|
innerReq = fct(*args, **kwargs)
|
||||||
payload = authentify(apiKey, innerReq)
|
payload = authentify(apiKey, innerReq)
|
||||||
return send(payload, host)
|
return send(payload, host)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@sendReq(url='api/publish')
|
@sendReq(url="api/publish")
|
||||||
def publish(bocId, url, date):
|
def publish(bocId, url, date):
|
||||||
return {
|
return {
|
||||||
'id': bocId,
|
"id": bocId,
|
||||||
'url': url,
|
"url": url,
|
||||||
'date': date.strftime('%Y-%m-%d'),
|
"date": date.strftime("%Y-%m-%d"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
TOKEN_DFT_FILE = os.path.expanduser("~/.bocal_api_token")
|
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):
|
def read_token(path):
|
||||||
token = ''
|
token = ""
|
||||||
try:
|
try:
|
||||||
with open(path, 'r') as handle:
|
with open(path, "r") as handle:
|
||||||
token = handle.readline().strip()
|
token = handle.readline().strip()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("[Erreur] Fichier d'identifiants absent (`{}`).".format(path),
|
print(
|
||||||
file=sys.stderr)
|
"[Erreur] Fichier d'identifiants absent (`{}`).".format(path),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
@ -89,6 +96,7 @@ def cmd(func):
|
||||||
def wrap(parse_args, *args, **kwargs):
|
def wrap(parse_args, *args, **kwargs):
|
||||||
token = read_token(parse_args.creds)
|
token = read_token(parse_args.creds)
|
||||||
return func(token, parse_args, *args, **kwargs)
|
return func(token, parse_args, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
@ -97,13 +105,9 @@ def cmd_publish(token, args):
|
||||||
if not args.date:
|
if not args.date:
|
||||||
publish_date = date.today()
|
publish_date = date.today()
|
||||||
else:
|
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)
|
publish_date = date(year=year, month=month, day=day)
|
||||||
(ret_code, ret_str) = publish(args.host,
|
(ret_code, ret_str) = publish(args.host, token, args.numero, args.url, publish_date)
|
||||||
token,
|
|
||||||
args.numero,
|
|
||||||
args.url,
|
|
||||||
publish_date)
|
|
||||||
if ret_code == 200:
|
if ret_code == 200:
|
||||||
print("Succès :)")
|
print("Succès :)")
|
||||||
else:
|
else:
|
||||||
|
@ -113,29 +117,30 @@ def cmd_publish(token, args):
|
||||||
|
|
||||||
def setup_argparse():
|
def setup_argparse():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--host',
|
parser.add_argument(
|
||||||
help=("Adresse du site à contacter (par défaut, "
|
"--host",
|
||||||
"`{}`).".format(DFT_HOST)))
|
help=("Adresse du site à contacter (par défaut, " "`{}`).".format(DFT_HOST)),
|
||||||
parser.add_argument('--creds',
|
)
|
||||||
help=("Fichier contenant le token API à utiliser "
|
parser.add_argument(
|
||||||
"(par défaut, `{}`)".format(TOKEN_DFT_FILE)))
|
"--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)
|
parser.set_defaults(creds=TOKEN_DFT_FILE, host=DFT_HOST)
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
parser_publish = subparsers.add_parser('publier',
|
parser_publish = subparsers.add_parser("publier", help="Publier un numéro du BOcal")
|
||||||
help='Publier un numéro du BOcal')
|
parser_publish.add_argument("numero", help="Numéro du BOcal")
|
||||||
parser_publish.add_argument('numero',
|
parser_publish.add_argument("url", help="Adresse (locale) du PDF du BOcal")
|
||||||
help='Numéro du BOcal')
|
parser_publish.add_argument("-d", "--date", help="Date de publication indiquée")
|
||||||
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)
|
parser_publish.set_defaults(func=cmd_publish)
|
||||||
|
|
||||||
out_args = parser.parse_args()
|
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("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)
|
sys.exit(1)
|
||||||
return out_args
|
return out_args
|
||||||
|
|
||||||
|
@ -145,5 +150,5 @@ def main():
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,17 +11,37 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = []
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ApiKey',
|
name="ApiKey",
|
||||||
fields=[
|
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')),
|
"id",
|
||||||
('name', models.CharField(help_text='Where is this key used from?', max_length=256, verbose_name='Key name')),
|
models.AutoField(
|
||||||
('last_used', models.DateTimeField(default=datetime.datetime(1970, 1, 1, 1, 0), verbose_name='Last used')),
|
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 @@
|
||||||
from django.db import models
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class ApiKey(models.Model):
|
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*
|
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
|
containing any dollar ($) sign. It is saved on the client's machine as
|
||||||
a string "keyId$key".
|
a string "keyId$key".
|
||||||
An API token (to authentify a request) is a triplet (ts, kid, hmac) of
|
An API token (to authentify a request) is a triplet (ts, kid, hmac) of
|
||||||
a timestamp `ts`, the key id `kid` and
|
a timestamp `ts`, the key id `kid` and
|
||||||
hmac = `HMAC(key, ts + data, sha256)`
|
hmac = `HMAC(key, ts + data, sha256)`
|
||||||
where `data` is the normalized (`json.dumps(json.loads(...))`) value of
|
where `data` is the normalized (`json.dumps(json.loads(...))`) value of
|
||||||
the data part of the request.
|
the data part of the request.
|
||||||
'''
|
"""
|
||||||
key = models.CharField("API key",
|
|
||||||
max_length=128)
|
key = models.CharField("API key", max_length=128)
|
||||||
name = models.CharField("Key name",
|
name = models.CharField(
|
||||||
max_length=256,
|
"Key name", max_length=256, help_text="Where is this key used from?"
|
||||||
help_text="Where is this key used from?")
|
)
|
||||||
last_used = models.DateTimeField("Last used",
|
last_used = models.DateTimeField("Last used", default=datetime.fromtimestamp(0))
|
||||||
default=datetime.fromtimestamp(0))
|
|
||||||
|
|
||||||
def everUsed(self):
|
def everUsed(self):
|
||||||
return self.last_used > datetime.fromtimestamp(0)
|
return self.last_used > datetime.fromtimestamp(0)
|
||||||
|
@ -33,8 +33,7 @@ class ApiKey(models.Model):
|
||||||
if not self.key:
|
if not self.key:
|
||||||
KEY_SIZE = 64
|
KEY_SIZE = 64
|
||||||
KEY_CHARS = string.ascii_letters + string.digits
|
KEY_CHARS = string.ascii_letters + string.digits
|
||||||
self.key = ''.join(
|
self.key = "".join([random.choice(KEY_CHARS) for _ in range(KEY_SIZE)])
|
||||||
[random.choice(KEY_CHARS) for _ in range(KEY_SIZE)])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def keyId(self):
|
def keyId(self):
|
||||||
|
@ -52,9 +51,11 @@ class ApiKey(models.Model):
|
||||||
if datetime.now() - timedelta(minutes=5) > claimedDate:
|
if datetime.now() - timedelta(minutes=5) > claimedDate:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
mac = hmac.new(self.key.encode('utf-8'),
|
mac = hmac.new(
|
||||||
msg=str(int(claimedDate.timestamp())).encode('utf-8'),
|
self.key.encode("utf-8"),
|
||||||
digestmod=hashlib.sha256)
|
msg=str(int(claimedDate.timestamp())).encode("utf-8"),
|
||||||
mac.update(data.encode('utf-8'))
|
digestmod=hashlib.sha256,
|
||||||
|
)
|
||||||
|
mac.update(data.encode("utf-8"))
|
||||||
|
|
||||||
return hmac.compare_digest(mac.hexdigest(), inpMac)
|
return hmac.compare_digest(mac.hexdigest(), inpMac)
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
from django.conf.urls import url
|
from django.urls import path
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
app_name = 'manisite'
|
app_name = "manisite"
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^publish$', views.publishApiView),
|
path("publish", views.publishApiView),
|
||||||
]
|
]
|
||||||
|
|
56
api/views.py
56
api/views.py
|
@ -1,27 +1,30 @@
|
||||||
from django.http import response
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
import json
|
|
||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.http import response
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
import mainsite.models as mainModels
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
import mainsite.models as mainModels
|
|
||||||
|
|
||||||
|
|
||||||
def authentify(data, payload):
|
def authentify(data, payload):
|
||||||
''' returns whether the request's authentification is correct '''
|
"""returns whether the request's authentification is correct"""
|
||||||
required = ['keyId', 'timestamp', 'hmac']
|
required = ["keyId", "timestamp", "hmac"]
|
||||||
for field in required:
|
for field in required:
|
||||||
if field not in data:
|
if field not in data:
|
||||||
return response.HttpResponseForbidden(
|
return response.HttpResponseForbidden(
|
||||||
'Missing required field "{}"'.format(field))
|
'Missing required field "{}"'.format(field)
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
key = models.ApiKey.objects.get(id=data['keyId'])
|
key = models.ApiKey.objects.get(id=data["keyId"])
|
||||||
except models.ApiKey.DoesNotExist:
|
except models.ApiKey.DoesNotExist:
|
||||||
return response.HttpResponseForbidden('Bad authentication')
|
return response.HttpResponseForbidden("Bad authentication")
|
||||||
|
|
||||||
if not key.isCorrect(data['timestamp'], data['hmac'], payload):
|
if not key.isCorrect(data["timestamp"], data["hmac"], payload):
|
||||||
return response.HttpResponseForbidden('Bad authentication')
|
return response.HttpResponseForbidden("Bad authentication")
|
||||||
|
|
||||||
|
|
||||||
def apiView(required=[]):
|
def apiView(required=[]):
|
||||||
|
@ -29,15 +32,15 @@ def apiView(required=[]):
|
||||||
@csrf_exempt
|
@csrf_exempt
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.body.decode('utf-8'))
|
data = json.loads(request.body.decode("utf-8"))
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return response.HttpResponseBadRequest("Bad packet format")
|
return response.HttpResponseBadRequest("Bad packet format")
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
return response.HttpResponseBadRequest("Bad json")
|
return response.HttpResponseBadRequest("Bad json")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
authData = data['auth']
|
authData = data["auth"]
|
||||||
reqDataOrig = data['req']
|
reqDataOrig = data["req"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return response.HttpResponseBadRequest("Bad request format")
|
return response.HttpResponseBadRequest("Bad request format")
|
||||||
|
|
||||||
|
@ -51,39 +54,40 @@ def apiView(required=[]):
|
||||||
for field in required:
|
for field in required:
|
||||||
if field not in reqData:
|
if field not in reqData:
|
||||||
return response.HttpResponseBadRequest(
|
return response.HttpResponseBadRequest(
|
||||||
"Missing field {}".format(field))
|
"Missing field {}".format(field)
|
||||||
|
)
|
||||||
|
|
||||||
authVal = authentify(authData, reqDataOrig)
|
authVal = authentify(authData, reqDataOrig)
|
||||||
if authVal is not None:
|
if authVal is not None:
|
||||||
return authVal
|
return authVal
|
||||||
|
|
||||||
return fct(request, reqData, *args, **kwargs)
|
return fct(request, reqData, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@apiView(required=["id", "url", "date"])
|
@apiView(required=["id", "url", "date"])
|
||||||
def publishApiView(request, data):
|
def publishApiView(request, data):
|
||||||
''' Publish a BOcal, and create the corresponding year if needed '''
|
"""Publish a BOcal, and create the corresponding year if needed"""
|
||||||
if mainModels.Publication.objects.filter(num=data['id']).count() > 0:
|
if mainModels.Publication.objects.filter(num=data["id"]).count() > 0:
|
||||||
return response.HttpResponseBadRequest(
|
return response.HttpResponseBadRequest(
|
||||||
"Un BOcal du même numéro est déjà présent ! Ajoutez celui-ci à la "
|
"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:
|
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)
|
date = datetime.date(year, month, day)
|
||||||
except:
|
except Exception:
|
||||||
return response.HttpResponseBadRequest("Bad date")
|
return response.HttpResponseBadRequest("Bad date")
|
||||||
|
|
||||||
pub = mainModels.Publication(num=data['id'],
|
pub = mainModels.Publication(num=data["id"], url=data["url"], date=date)
|
||||||
url=data['url'],
|
|
||||||
date=date)
|
|
||||||
try:
|
try:
|
||||||
pub.full_clean()
|
pub.full_clean()
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return response.HttpResponseBadRequest(
|
return response.HttpResponseBadRequest("Invalid data: {}".format(e))
|
||||||
"Invalid data: {}".format(e))
|
|
||||||
pub.save()
|
pub.save()
|
||||||
|
|
||||||
pub.createPubYear()
|
pub.createPubYear()
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
default_app_config = 'bocal_auth.apps.BocalAuthConfig'
|
default_app_config = "bocal_auth.apps.BocalAuthConfig"
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class BocalAuthConfig(AppConfig):
|
class BocalAuthConfig(AppConfig):
|
||||||
name = 'bocal_auth'
|
name = "bocal_auth"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
from . import signals
|
pass
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django_cas_ng.backends import CASBackend
|
from django_cas_ng.backends import CASBackend
|
||||||
|
|
||||||
from .models import CasUser
|
from .models import CasUser
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
# Generated by Django 1.11.5 on 2017-10-14 16:49
|
# Generated by Django 1.11.5 on 2017-10-14 16:49
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
import django.db.models.deletion
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -12,14 +12,22 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0008_alter_user_username_max_length'),
|
("auth", "0008_alter_user_username_max_length"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='CasUser',
|
name="CasUser",
|
||||||
fields=[
|
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,11 +1,8 @@
|
||||||
from django.db import models
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class CasUser(models.Model):
|
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 = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||||
User,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
primary_key=True)
|
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
''' Reads a .rhosts file '''
|
""" Reads a .rhosts file """
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
from .models import CasUser
|
from .models import CasUser
|
||||||
|
|
||||||
|
|
||||||
def hasUser(user, allowed_domains=[]):
|
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
|
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):
|
def clearLine(line):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
hashPos = line.find('#')
|
hashPos = line.find("#")
|
||||||
if hashPos >= 0:
|
if hashPos >= 0:
|
||||||
line = line[:hashPos]
|
line = line[:hashPos]
|
||||||
return line
|
return line
|
||||||
|
|
||||||
with open(settings.RHOSTS_PATH, 'r') as handle:
|
with open(settings.RHOSTS_PATH, "r") as handle:
|
||||||
for line in handle:
|
for line in handle:
|
||||||
line = clearLine(line)
|
line = clearLine(line)
|
||||||
if not line:
|
if not line:
|
||||||
|
@ -31,7 +32,7 @@ def hasUser(user, allowed_domains=[]):
|
||||||
if login != user: # Not the ones we're looking for
|
if login != user: # Not the ones we're looking for
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if domain[:2] != '+@': # Not a valid domain
|
if domain[:2] != "+@": # Not a valid domain
|
||||||
continue
|
continue
|
||||||
domain = domain[2:]
|
domain = domain[2:]
|
||||||
|
|
||||||
|
@ -43,16 +44,16 @@ def hasUser(user, allowed_domains=[]):
|
||||||
|
|
||||||
|
|
||||||
def default_allowed(user):
|
def default_allowed(user):
|
||||||
return hasUser(user, allowed_domains=['eleves'])
|
return hasUser(user, allowed_domains=["eleves"])
|
||||||
|
|
||||||
|
|
||||||
class NoBOcalException(Exception):
|
class NoBOcalException(Exception):
|
||||||
def __str__():
|
def __str__(self):
|
||||||
return "The BOcal group was not created"
|
return "The BOcal group was not created"
|
||||||
|
|
||||||
|
|
||||||
def bocalGroup():
|
def bocalGroup():
|
||||||
qs = Group.objects.filter(name='BOcal')
|
qs = Group.objects.filter(name="BOcal")
|
||||||
if qs.count() != 1:
|
if qs.count() != 1:
|
||||||
raise NoBOcalException
|
raise NoBOcalException
|
||||||
return qs[0]
|
return qs[0]
|
||||||
|
@ -80,6 +81,7 @@ def requireCasUser(fct):
|
||||||
if not hasCas(user):
|
if not hasCas(user):
|
||||||
return
|
return
|
||||||
return fct(user, *args, **kwargs)
|
return fct(user, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,4 +102,5 @@ def forceReevalRhosts(fct):
|
||||||
def wrap(req, *args, **kwargs):
|
def wrap(req, *args, **kwargs):
|
||||||
evalRhostsPrivileges(req.user)
|
evalRhostsPrivileges(req.user)
|
||||||
return fct(req, *args, **kwargs)
|
return fct(req, *args, **kwargs)
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django_cas_ng.signals import cas_user_authenticated, cas_user_logout
|
from django_cas_ng.signals import cas_user_authenticated, cas_user_logout
|
||||||
|
|
||||||
from . import rhosts
|
from . import rhosts
|
||||||
|
|
||||||
|
|
||||||
@receiver(cas_user_authenticated)
|
@receiver(cas_user_authenticated)
|
||||||
def onCasLogin(sender, user, **kwargs):
|
def onCasLogin(sender, user, **kwargs):
|
||||||
''' Called upon login of a user through CAS '''
|
"""Called upon login of a user through CAS"""
|
||||||
rhosts.evalRhostsPrivileges(user)
|
rhosts.evalRhostsPrivileges(user)
|
||||||
|
|
||||||
|
|
||||||
@receiver(cas_user_logout)
|
@receiver(cas_user_logout)
|
||||||
def onCasLogout(sender, user, **kwargs):
|
def onCasLogout(sender, user, **kwargs):
|
||||||
''' Strip the user from their privileges — in case something goes wrong
|
"""Strip the user from their privileges — in case something goes wrong
|
||||||
during the next authentication '''
|
during the next authentication"""
|
||||||
rhosts.logout(user)
|
rhosts.logout(user)
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from django.shortcuts import render, redirect
|
from urllib.parse import quote as urlquote
|
||||||
from django.urls import reverse
|
|
||||||
from django.contrib.auth import logout as auth_logout
|
from django.contrib.auth import logout as auth_logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.shortcuts import redirect, render
|
||||||
from urllib.parse import quote as urlquote
|
from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
def login(req):
|
def login(req):
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from solo.admin import SingletonModelAdmin
|
from solo.admin import SingletonModelAdmin
|
||||||
from . import models
|
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
admin.site.register(models.SiteConfiguration, SingletonModelAdmin)
|
admin.site.register(models.SiteConfiguration, SingletonModelAdmin)
|
||||||
admin.site.register(models.Publication)
|
admin.site.register(models.Publication)
|
||||||
|
|
|
@ -2,4 +2,4 @@ from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class MainsiteConfig(AppConfig):
|
class MainsiteConfig(AppConfig):
|
||||||
name = 'mainsite'
|
name = "mainsite"
|
||||||
|
|
|
@ -7,10 +7,11 @@ def sidebar_years(req):
|
||||||
avail_years = models.PublicationYear.objects.all()
|
avail_years = models.PublicationYear.objects.all()
|
||||||
publi_years = [year for year in avail_years if year.publis().count() > 0]
|
publi_years = [year for year in avail_years if year.publis().count() > 0]
|
||||||
|
|
||||||
num_special_publications = models.Publication.objects\
|
num_special_publications = models.Publication.objects.filter(
|
||||||
.filter(is_special=True).count()
|
is_special=True
|
||||||
|
).count()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'publication_years': publi_years,
|
"publication_years": publi_years,
|
||||||
'has_special_publications': num_special_publications > 0,
|
"has_special_publications": num_special_publications > 0,
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,20 +9,57 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = []
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='Publication',
|
name="Publication",
|
||||||
fields=[
|
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')),
|
"id",
|
||||||
('url', models.CharField(max_length=512, verbose_name='Adresse sur le site')),
|
models.AutoField(
|
||||||
('date', models.DateField(verbose_name='Publication')),
|
auto_created=True,
|
||||||
('is_special', models.BooleanField(default=False, help_text='Numéro du BOcal non-numéroté', verbose_name='Numéro spécial')),
|
primary_key=True,
|
||||||
('descr', models.CharField(blank=True, max_length=512, verbose_name='Description (optionnelle)')),
|
serialize=False,
|
||||||
('custom_name', models.CharField(blank=True, help_text='Vide pour laisser le numéro seulement', max_length=128, verbose_name='Nom customisé')),
|
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,19 +8,27 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0001_initial'),
|
("mainsite", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='PublicationYear',
|
name="PublicationYear",
|
||||||
fields=[
|
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(
|
migrations.AlterModelOptions(
|
||||||
name='publication',
|
name="publication",
|
||||||
options={'ordering': ['date']},
|
options={"ordering": ["date"]},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,20 +8,41 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0002_auto_20170922_1438'),
|
("mainsite", "0002_auto_20170922_1438"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='SiteConfiguration',
|
name="SiteConfiguration",
|
||||||
fields=[
|
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)")),
|
"id",
|
||||||
('writearticleText', models.TextField(verbose_name='Texte de la page « écrire » (HTML)')),
|
models.AutoField(
|
||||||
('email', models.CharField(help_text='Attention au spam…', max_length=128, verbose_name='Adresse de contact du BOcal')),
|
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={
|
options={
|
||||||
'verbose_name': 'Configuration du site',
|
"verbose_name": "Configuration du site",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,13 +8,17 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0003_siteconfiguration'),
|
("mainsite", "0003_siteconfiguration"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='publication',
|
model_name="publication",
|
||||||
name='in_year_view_anyway',
|
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"),
|
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):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0004_publication_in_year_view_anyway'),
|
("mainsite", "0004_publication_in_year_view_anyway"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterModelOptions(
|
migrations.AlterModelOptions(
|
||||||
name='publicationyear',
|
name="publicationyear",
|
||||||
options={'ordering': ['-startYear']},
|
options={"ordering": ["-startYear"]},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,30 +2,36 @@
|
||||||
# Generated by Django 1.11.5 on 2017-09-23 16:47
|
# Generated by Django 1.11.5 on 2017-09-23 16:47
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
import markdownx.models
|
import markdownx.models
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0005_auto_20170922_1916'),
|
("mainsite", "0005_auto_20170922_1916"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='publicationyear',
|
model_name="publicationyear",
|
||||||
name='descr',
|
name="descr",
|
||||||
field=markdownx.models.MarkdownxField(verbose_name="Accroche de l'année (Markdown)"),
|
field=markdownx.models.MarkdownxField(
|
||||||
|
verbose_name="Accroche de l'année (Markdown)"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='siteconfiguration',
|
model_name="siteconfiguration",
|
||||||
name='homepageText',
|
name="homepageText",
|
||||||
field=markdownx.models.MarkdownxField(verbose_name="Texte de la page d'accueil (Markdown)"),
|
field=markdownx.models.MarkdownxField(
|
||||||
|
verbose_name="Texte de la page d'accueil (Markdown)"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='siteconfiguration',
|
model_name="siteconfiguration",
|
||||||
name='writearticleText',
|
name="writearticleText",
|
||||||
field=markdownx.models.MarkdownxField(verbose_name='Texte de la page « écrire » (Markdown)'),
|
field=markdownx.models.MarkdownxField(
|
||||||
|
verbose_name="Texte de la page « écrire » (Markdown)"
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,21 +2,24 @@
|
||||||
# Generated by Django 1.11.5 on 2017-09-23 16:54
|
# Generated by Django 1.11.5 on 2017-09-23 16:54
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
import markdownx.models
|
import markdownx.models
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0006_auto_20170923_1847'),
|
("mainsite", "0006_auto_20170923_1847"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='siteconfiguration',
|
model_name="siteconfiguration",
|
||||||
name='specialPublisDescr',
|
name="specialPublisDescr",
|
||||||
field=markdownx.models.MarkdownxField(default='', verbose_name='Texte de la page des publications spéciales (Markdown)'),
|
field=markdownx.models.MarkdownxField(
|
||||||
|
default="",
|
||||||
|
verbose_name="Texte de la page des publications spéciales (Markdown)",
|
||||||
|
),
|
||||||
preserve_default=False,
|
preserve_default=False,
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,13 +8,17 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('mainsite', '0007_siteconfiguration_specialpublisdescr'),
|
("mainsite", "0007_siteconfiguration_specialpublisdescr"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='publication',
|
model_name="publication",
|
||||||
name='unknown_date',
|
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'),
|
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,23 +1,20 @@
|
||||||
from django.db import models
|
|
||||||
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
|
import datetime
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import BooleanField, CharField, DateField, IntegerField, Q
|
||||||
|
from markdownx.models import MarkdownxField
|
||||||
|
from solo.models import SingletonModel
|
||||||
|
|
||||||
|
|
||||||
class SiteConfiguration(SingletonModel):
|
class SiteConfiguration(SingletonModel):
|
||||||
homepageText = MarkdownxField("Texte de la page d'accueil (Markdown)")
|
homepageText = MarkdownxField("Texte de la page d'accueil (Markdown)")
|
||||||
writearticleText = MarkdownxField("Texte de la page « écrire » (Markdown)")
|
writearticleText = MarkdownxField("Texte de la page « écrire » (Markdown)")
|
||||||
email = CharField("Adresse de contact du BOcal",
|
email = CharField(
|
||||||
max_length=128,
|
"Adresse de contact du BOcal", max_length=128, help_text="Attention au spam…"
|
||||||
help_text="Attention au spam…")
|
)
|
||||||
specialPublisDescr = MarkdownxField("Texte de la page des "
|
specialPublisDescr = MarkdownxField(
|
||||||
"publications spéciales (Markdown)")
|
"Texte de la page des " "publications spéciales (Markdown)"
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return "Configuration du site"
|
return "Configuration du site"
|
||||||
|
@ -27,33 +24,40 @@ class SiteConfiguration(SingletonModel):
|
||||||
|
|
||||||
|
|
||||||
class Publication(models.Model):
|
class Publication(models.Model):
|
||||||
num = CharField('Numéro du BOcal', max_length=128)
|
num = CharField("Numéro du BOcal", max_length=128)
|
||||||
url = CharField('Adresse sur le site', max_length=512)
|
url = CharField("Adresse sur le site", max_length=512)
|
||||||
# ^ This is not a URLField because we need internal URLS, eg `/static/blah`
|
# ^ This is not a URLField because we need internal URLS, eg `/static/blah`
|
||||||
|
|
||||||
date = DateField('Publication')
|
date = DateField("Publication")
|
||||||
unknown_date = BooleanField('Date inconnue',
|
unknown_date = BooleanField(
|
||||||
help_text=("La date de publication du BOcal "
|
"Date inconnue",
|
||||||
"est inconnue parce qu'il est "
|
help_text=(
|
||||||
"trop vieux. La date indiquée ne "
|
"La date de publication du BOcal "
|
||||||
"servira qu'à le ranger dans une "
|
"est inconnue parce qu'il est "
|
||||||
"année et à ordonner les BOcals."),
|
"trop vieux. La date indiquée ne "
|
||||||
default=False)
|
"servira qu'à le ranger dans une "
|
||||||
is_special = BooleanField('Numéro spécial',
|
"année et à ordonner les BOcals."
|
||||||
help_text='Numéro du BOcal non-numéroté',
|
),
|
||||||
default=False)
|
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(
|
in_year_view_anyway = BooleanField(
|
||||||
"Aussi dans l'année",
|
"Aussi dans l'année",
|
||||||
help_text=("Si le numéro est spécial, l'afficher quand même dans la "
|
help_text=(
|
||||||
"page de l'année correspondante."),
|
"Si le numéro est spécial, l'afficher quand même dans la "
|
||||||
default=False)
|
"page de l'année correspondante."
|
||||||
descr = CharField('Description (optionnelle)',
|
),
|
||||||
max_length=512,
|
default=False,
|
||||||
blank=True)
|
)
|
||||||
custom_name = CharField('Nom customisé',
|
descr = CharField("Description (optionnelle)", max_length=512, blank=True)
|
||||||
help_text='Vide pour laisser le numéro seulement',
|
custom_name = CharField(
|
||||||
max_length=128,
|
"Nom customisé",
|
||||||
blank=True)
|
help_text="Vide pour laisser le numéro seulement",
|
||||||
|
max_length=128,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
class NoPublicationYear(Exception):
|
class NoPublicationYear(Exception):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
@ -67,8 +71,8 @@ class Publication(models.Model):
|
||||||
return startYear
|
return startYear
|
||||||
|
|
||||||
def publicationYear(self):
|
def publicationYear(self):
|
||||||
''' Fetch corresponding publication year
|
"""Fetch corresponding publication year
|
||||||
Raise `NoPublicationYear` if there is no such entry '''
|
Raise `NoPublicationYear` if there is no such entry"""
|
||||||
startYear = self.numericPublicationYear
|
startYear = self.numericPublicationYear
|
||||||
try:
|
try:
|
||||||
return PublicationYear.objects.get(startYear=startYear)
|
return PublicationYear.objects.get(startYear=startYear)
|
||||||
|
@ -76,11 +80,13 @@ class Publication(models.Model):
|
||||||
raise self.NoPublicationYear
|
raise self.NoPublicationYear
|
||||||
|
|
||||||
def createPubYear(self):
|
def createPubYear(self):
|
||||||
''' Creates the corresponding publication year if needed. '''
|
"""Creates the corresponding publication year if needed."""
|
||||||
if (PublicationYear.objects
|
if (
|
||||||
.filter(startYear=self.numericPublicationYear).count()) == 0:
|
PublicationYear.objects.filter(
|
||||||
pubYear = PublicationYear(startYear=self.numericPublicationYear,
|
startYear=self.numericPublicationYear
|
||||||
descr='')
|
).count()
|
||||||
|
) == 0:
|
||||||
|
pubYear = PublicationYear(startYear=self.numericPublicationYear, descr="")
|
||||||
pubYear.save()
|
pubYear.save()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
@ -90,50 +96,51 @@ class Publication(models.Model):
|
||||||
return self.custom_name
|
return self.custom_name
|
||||||
elif self.is_special:
|
elif self.is_special:
|
||||||
return self.num
|
return self.num
|
||||||
return 'BOcal n°{}'.format(self.num)
|
return "BOcal n°{}".format(self.num)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def latest():
|
def latest():
|
||||||
return Publication.objects.order_by('-date')[0]
|
return Publication.objects.order_by("-date")[0]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['date']
|
ordering = ["date"]
|
||||||
|
|
||||||
|
|
||||||
class PublicationYear(models.Model):
|
class PublicationYear(models.Model):
|
||||||
startYear = IntegerField('Année de début',
|
startYear = IntegerField(
|
||||||
help_text='Année scolaire à partir du 15/08',
|
"Année de début", help_text="Année scolaire à partir du 15/08", primary_key=True
|
||||||
primary_key=True)
|
)
|
||||||
descr = MarkdownxField("Accroche de l'année (Markdown)")
|
descr = MarkdownxField("Accroche de l'année (Markdown)")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '{}-{}'.format(self.startYear, self.startYear+1)
|
return "{}-{}".format(self.startYear, self.startYear + 1)
|
||||||
|
|
||||||
def beg(self):
|
def beg(self):
|
||||||
''' First day of this publication year (incl.) '''
|
"""First day of this publication year (incl.)"""
|
||||||
return datetime.date(self.startYear, 8, 15)
|
return datetime.date(self.startYear, 8, 15)
|
||||||
|
|
||||||
def end(self):
|
def end(self):
|
||||||
''' Last day of this publication year (excl.) '''
|
"""Last day of this publication year (excl.)"""
|
||||||
return datetime.date(self.startYear + 1, 8, 15)
|
return datetime.date(self.startYear + 1, 8, 15)
|
||||||
|
|
||||||
def inYear(self, date):
|
def inYear(self, date):
|
||||||
return self.beg() <= date < self.end()
|
return self.beg() <= date < self.end()
|
||||||
|
|
||||||
def publis(self):
|
def publis(self):
|
||||||
''' List of publications from this year '''
|
"""List of publications from this year"""
|
||||||
return Publication.objects.filter(
|
return Publication.objects.filter(
|
||||||
Q(is_special=False) | Q(in_year_view_anyway=True),
|
Q(is_special=False) | Q(in_year_view_anyway=True),
|
||||||
date__gte=self.beg(),
|
date__gte=self.beg(),
|
||||||
date__lt=self.end())
|
date__lt=self.end(),
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def url(self):
|
def url(self):
|
||||||
return '/{}/'.format(self)
|
return "/{}/".format(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prettyName(self):
|
def prettyName(self):
|
||||||
return '{} – {}'.format(self.startYear, self.startYear+1)
|
return "{} – {}".format(self.startYear, self.startYear + 1)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['-startYear']
|
ordering = ["-startYear"]
|
||||||
|
|
|
@ -1,3 +1 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
# Create your tests here.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
|
from django.http import Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from django.http import Http404, HttpResponse
|
|
||||||
|
|
||||||
from mainsite.models import Publication, PublicationYear, SiteConfiguration
|
from mainsite.models import Publication, PublicationYear, SiteConfiguration
|
||||||
|
|
||||||
|
|
||||||
def robots_view(request):
|
def robots_view(request):
|
||||||
""" Robots.txt view """
|
"""Robots.txt view"""
|
||||||
body = "User-Agent: *\nDisallow: /\nAllow: /$\n"
|
body = "User-Agent: *\nDisallow: /\nAllow: /$\n"
|
||||||
return HttpResponse(body, content_type="text/plain")
|
return HttpResponse(body, content_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
class HomeView(TemplateView):
|
class HomeView(TemplateView):
|
||||||
""" Website's homepage """
|
"""Website's homepage"""
|
||||||
|
|
||||||
template_name = "mainsite/homepage.html"
|
template_name = "mainsite/homepage.html"
|
||||||
|
|
||||||
|
|
||||||
class WriteArticleView(TemplateView):
|
class WriteArticleView(TemplateView):
|
||||||
""" Tell the readers how they can contribute to the BOcal """
|
"""Tell the readers how they can contribute to the BOcal"""
|
||||||
|
|
||||||
template_name = "mainsite/write_article.html"
|
template_name = "mainsite/write_article.html"
|
||||||
|
|
||||||
|
|
||||||
class PublicationListView(TemplateView):
|
class PublicationListView(TemplateView):
|
||||||
""" Display a list of publications (generic class).
|
"""Display a list of publications (generic class).
|
||||||
|
|
||||||
Reimplement `get_publications` (called with the url template args in a
|
Reimplement `get_publications` (called with the url template args in a
|
||||||
place where you can Http404) in subclasses to get it working. """
|
place where you can Http404) in subclasses to get it working."""
|
||||||
|
|
||||||
template_name = "mainsite/publications_list_view.html"
|
template_name = "mainsite/publications_list_view.html"
|
||||||
|
|
||||||
def initView(self):
|
def initView(self):
|
||||||
""" Cannot be __init__, we don't have **kwargs there """
|
"""Cannot be __init__, we don't have **kwargs there"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
@ -51,7 +51,7 @@ class PublicationListView(TemplateView):
|
||||||
|
|
||||||
|
|
||||||
class YearView(PublicationListView):
|
class YearView(PublicationListView):
|
||||||
""" Display a year worth of BOcals """
|
"""Display a year worth of BOcals"""
|
||||||
|
|
||||||
def initView(self, year, nYear):
|
def initView(self, year, nYear):
|
||||||
try:
|
try:
|
||||||
|
@ -76,7 +76,7 @@ class YearView(PublicationListView):
|
||||||
|
|
||||||
|
|
||||||
class SpecialPublicationsView(PublicationListView):
|
class SpecialPublicationsView(PublicationListView):
|
||||||
""" Display the list of special publications """
|
"""Display the list of special publications"""
|
||||||
|
|
||||||
def additional_context(self):
|
def additional_context(self):
|
||||||
siteConf = SiteConfiguration.get_solo()
|
siteConf = SiteConfiguration.get_solo()
|
||||||
|
@ -92,6 +92,6 @@ class SpecialPublicationsView(PublicationListView):
|
||||||
|
|
||||||
|
|
||||||
def latestPublication(req):
|
def latestPublication(req):
|
||||||
""" Redirects to the latest standard publication """
|
"""Redirects to the latest standard publication"""
|
||||||
latestPubli = Publication.latest()
|
latestPubli = Publication.latest()
|
||||||
return redirect(latestPubli.url)
|
return redirect(latestPubli.url)
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bocal.settings")
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# The above import may fail for some other reason. Ensure that the
|
# The above import may fail for some other reason. Ensure that the
|
||||||
# issue is really that Django is missing to avoid masking other
|
# issue is really that Django is missing to avoid masking other
|
||||||
# exceptions on Python 2.
|
# exceptions on Python 2.
|
||||||
try:
|
if find_spec("django") is None:
|
||||||
import django
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
"available on your PYTHONPATH environment variable? Did you "
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
|
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
ignore = ["F403", "F405"]
|
Loading…
Reference in a new issue