chore: Run formatters and fix lint errors

This commit is contained in:
Tom Hubrecht 2024-10-23 11:06:57 +02:00
parent 09ad2ca896
commit 430b3b40bb
Signed by: thubrecht
SSH key fingerprint: SHA256:r+nK/SIcWlJ0zFZJGHtlAoRwq1Rm+WcKAm5ADYMoQPc
35 changed files with 415 additions and 284 deletions

View file

@ -1,11 +1,12 @@
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:

View file

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

View file

@ -1,86 +1,93 @@
#!/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 hashlib
import hmac
import json
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
@ -89,6 +96,7 @@ def cmd(func):
def wrap(parse_args, *args, **kwargs):
token = read_token(parse_args.creds)
return func(token, parse_args, *args, **kwargs)
return wrap
@ -97,13 +105,9 @@ 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:
@ -113,29 +117,30 @@ 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
@ -145,5 +150,5 @@ def main():
args.func(args)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

@ -3,6 +3,7 @@
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
@ -10,17 +11,37 @@ 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 @@
from django.db import models
from datetime import datetime, timedelta
import hmac
import hashlib
import hmac
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,8 +33,7 @@ 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):
@ -52,9 +51,11 @@ 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)

View file

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

View file

@ -1,8 +1,9 @@
from django.conf.urls import url
from django.urls import path
from . import views
app_name = 'manisite'
app_name = "manisite"
urlpatterns = [
url(r'^publish$', views.publishApiView),
path("publish", views.publishApiView),
]

View file

@ -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 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
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=[]):
@ -29,15 +32,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")
@ -51,39 +54,40 @@ 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:
except Exception:
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 +1 @@
default_app_config = 'bocal_auth.apps.BocalAuthConfig'
default_app_config = "bocal_auth.apps.BocalAuthConfig"

View file

@ -1,3 +1 @@
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):
from . import signals
pass

View file

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

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,14 +12,22 @@ 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,11 +1,8 @@
from django.db import models
from django.contrib.auth.models import User
from django.db import models
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,23 +1,24 @@
''' 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:
@ -31,7 +32,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:]
@ -43,16 +44,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__():
def __str__(self):
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]
@ -80,6 +81,7 @@ def requireCasUser(fct):
if not hasCas(user):
return
return fct(user, *args, **kwargs)
return wrap
@ -100,4 +102,5 @@ def forceReevalRhosts(fct):
def wrap(req, *args, **kwargs):
evalRhostsPrivileges(req.user)
return fct(req, *args, **kwargs)
return wrap

View file

@ -1,16 +1,17 @@
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)

View file

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

View file

@ -1,9 +1,9 @@
from django.shortcuts import render, redirect
from django.urls import reverse
from urllib.parse import quote as urlquote
from django.contrib.auth import logout as auth_logout
from django.contrib.auth.decorators import login_required
from urllib.parse import quote as urlquote
from django.shortcuts import redirect, render
from django.urls import reverse
def login(req):

View file

@ -1,7 +1,7 @@
from django.contrib import admin
from solo.admin import SingletonModelAdmin
from . import models
from . import models
admin.site.register(models.SiteConfiguration, SingletonModelAdmin)
admin.site.register(models.Publication)

View file

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

View file

@ -7,10 +7,11 @@ 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,20 +9,57 @@ 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,19 +8,27 @@ 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,20 +8,41 @@ 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,13 +8,17 @@ 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,30 +2,36 @@
# Generated by Django 1.11.5 on 2017-09-23 16:47
from __future__ import unicode_literals
from django.db import migrations
import markdownx.models
from django.db import migrations
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,21 +2,24 @@
# Generated by Django 1.11.5 on 2017-09-23 16:54
from __future__ import unicode_literals
from django.db import migrations
import markdownx.models
from django.db import migrations
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,13 +8,17 @@ 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,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
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):
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"
@ -27,33 +24,40 @@ class SiteConfiguration(SingletonModel):
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):
@ -67,8 +71,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)
@ -76,11 +80,13 @@ 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
@ -90,50 +96,51 @@ 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 15/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.) '''
"""First day of this publication year (incl.)"""
return datetime.date(self.startYear, 8, 15)
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)
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

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

View file

@ -1,4 +1,5 @@
from django.conf.urls import url
from . import views
urlpatterns = [

View file

@ -1,38 +1,38 @@
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect
from django.views.generic import TemplateView
from django.http import Http404, HttpResponse
from mainsite.models import Publication, PublicationYear, SiteConfiguration
def robots_view(request):
""" Robots.txt view """
"""Robots.txt view"""
body = "User-Agent: *\nDisallow: /\nAllow: /$\n"
return HttpResponse(body, content_type="text/plain")
class HomeView(TemplateView):
""" Website's homepage """
"""Website's homepage"""
template_name = "mainsite/homepage.html"
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"
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
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"
def initView(self):
""" Cannot be __init__, we don't have **kwargs there """
"""Cannot be __init__, we don't have **kwargs there"""
pass
def get_context_data(self, **kwargs):
@ -51,7 +51,7 @@ class PublicationListView(TemplateView):
class YearView(PublicationListView):
""" Display a year worth of BOcals """
"""Display a year worth of BOcals"""
def initView(self, year, nYear):
try:
@ -76,7 +76,7 @@ class YearView(PublicationListView):
class SpecialPublicationsView(PublicationListView):
""" Display the list of special publications """
"""Display the list of special publications"""
def additional_context(self):
siteConf = SiteConfiguration.get_solo()
@ -92,6 +92,6 @@ class SpecialPublicationsView(PublicationListView):
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,18 +1,17 @@
#!/usr/bin/env python
import os
import sys
from importlib.util import find_spec
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "bocal.settings")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.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.
try:
import django
except ImportError:
if find_spec("django") is None:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "

5
pyproject.toml Normal file
View file

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