diff --git a/api/admin.py b/api/admin.py index 9cccfc7..e1243e5 100644 --- a/api/admin.py +++ b/api/admin.py @@ -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: diff --git a/api/apps.py b/api/apps.py index d87006d..14b89a8 100644 --- a/api/apps.py +++ b/api/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class ApiConfig(AppConfig): - name = 'api' + name = "api" diff --git a/api/client/apiclient.py b/api/client/apiclient.py index 2d01d7c..5ce8bc8 100644 --- a/api/client/apiclient.py +++ b/api/client/apiclient.py @@ -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() diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py index 12e0ddf..b34bb10 100644 --- a/api/migrations/0001_initial.py +++ b/api/migrations/0001_initial.py @@ -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", + ), + ), ], ), ] diff --git a/api/models.py b/api/models.py index 1ce36ca..9db7996 100644 --- a/api/models.py +++ b/api/models.py @@ -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) diff --git a/api/tests.py b/api/tests.py index 7ce503c..a39b155 100644 --- a/api/tests.py +++ b/api/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/api/urls.py b/api/urls.py index fcdf67a..bb84592 100644 --- a/api/urls.py +++ b/api/urls.py @@ -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), ] diff --git a/api/views.py b/api/views.py index f015373..530c440 100644 --- a/api/views.py +++ b/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 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() diff --git a/bocal_auth/__init__.py b/bocal_auth/__init__.py index 917eea3..d4953d3 100644 --- a/bocal_auth/__init__.py +++ b/bocal_auth/__init__.py @@ -1 +1 @@ -default_app_config = 'bocal_auth.apps.BocalAuthConfig' +default_app_config = "bocal_auth.apps.BocalAuthConfig" diff --git a/bocal_auth/admin.py b/bocal_auth/admin.py index 8c38f3f..846f6b4 100644 --- a/bocal_auth/admin.py +++ b/bocal_auth/admin.py @@ -1,3 +1 @@ -from django.contrib import admin - # Register your models here. diff --git a/bocal_auth/apps.py b/bocal_auth/apps.py index ea9408d..3958955 100644 --- a/bocal_auth/apps.py +++ b/bocal_auth/apps.py @@ -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 diff --git a/bocal_auth/cas_backend.py b/bocal_auth/cas_backend.py index af8e0b8..16d069b 100644 --- a/bocal_auth/cas_backend.py +++ b/bocal_auth/cas_backend.py @@ -1,4 +1,5 @@ from django_cas_ng.backends import CASBackend + from .models import CasUser diff --git a/bocal_auth/migrations/0001_initial.py b/bocal_auth/migrations/0001_initial.py index 9bca4fd..606bfdd 100644 --- a/bocal_auth/migrations/0001_initial.py +++ b/bocal_auth/migrations/0001_initial.py @@ -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, + ), + ), ], ), ] diff --git a/bocal_auth/models.py b/bocal_auth/models.py index c83b39a..0b1d37a 100644 --- a/bocal_auth/models.py +++ b/bocal_auth/models.py @@ -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) diff --git a/bocal_auth/rhosts.py b/bocal_auth/rhosts.py index bd6d9a3..ae1d80c 100644 --- a/bocal_auth/rhosts.py +++ b/bocal_auth/rhosts.py @@ -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 diff --git a/bocal_auth/signals.py b/bocal_auth/signals.py index bbba5a8..de22e19 100644 --- a/bocal_auth/signals.py +++ b/bocal_auth/signals.py @@ -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) diff --git a/bocal_auth/tests.py b/bocal_auth/tests.py index 7ce503c..a39b155 100644 --- a/bocal_auth/tests.py +++ b/bocal_auth/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/bocal_auth/views.py b/bocal_auth/views.py index 93378f1..995ad23 100644 --- a/bocal_auth/views.py +++ b/bocal_auth/views.py @@ -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): diff --git a/mainsite/admin.py b/mainsite/admin.py index e26b96f..a102fd9 100644 --- a/mainsite/admin.py +++ b/mainsite/admin.py @@ -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) diff --git a/mainsite/apps.py b/mainsite/apps.py index 8c76684..4f6018b 100644 --- a/mainsite/apps.py +++ b/mainsite/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class MainsiteConfig(AppConfig): - name = 'mainsite' + name = "mainsite" diff --git a/mainsite/context_processors.py b/mainsite/context_processors.py index afe7647..4f3630b 100644 --- a/mainsite/context_processors.py +++ b/mainsite/context_processors.py @@ -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, } diff --git a/mainsite/migrations/0001_initial.py b/mainsite/migrations/0001_initial.py index d7ab41e..ae5f3de 100644 --- a/mainsite/migrations/0001_initial.py +++ b/mainsite/migrations/0001_initial.py @@ -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é", + ), + ), ], ), ] diff --git a/mainsite/migrations/0002_auto_20170922_1438.py b/mainsite/migrations/0002_auto_20170922_1438.py index bf6dbb7..70c61a7 100644 --- a/mainsite/migrations/0002_auto_20170922_1438.py +++ b/mainsite/migrations/0002_auto_20170922_1438.py @@ -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"]}, ), ] diff --git a/mainsite/migrations/0003_siteconfiguration.py b/mainsite/migrations/0003_siteconfiguration.py index ffac5c9..f108d60 100644 --- a/mainsite/migrations/0003_siteconfiguration.py +++ b/mainsite/migrations/0003_siteconfiguration.py @@ -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", }, ), ] diff --git a/mainsite/migrations/0004_publication_in_year_view_anyway.py b/mainsite/migrations/0004_publication_in_year_view_anyway.py index e7bb78e..cd629db 100644 --- a/mainsite/migrations/0004_publication_in_year_view_anyway.py +++ b/mainsite/migrations/0004_publication_in_year_view_anyway.py @@ -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", + ), ), ] diff --git a/mainsite/migrations/0005_auto_20170922_1916.py b/mainsite/migrations/0005_auto_20170922_1916.py index 7c62cbf..3b0a424 100644 --- a/mainsite/migrations/0005_auto_20170922_1916.py +++ b/mainsite/migrations/0005_auto_20170922_1916.py @@ -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"]}, ), ] diff --git a/mainsite/migrations/0006_auto_20170923_1847.py b/mainsite/migrations/0006_auto_20170923_1847.py index ca36df8..7beba12 100644 --- a/mainsite/migrations/0006_auto_20170923_1847.py +++ b/mainsite/migrations/0006_auto_20170923_1847.py @@ -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)" + ), ), ] diff --git a/mainsite/migrations/0007_siteconfiguration_specialpublisdescr.py b/mainsite/migrations/0007_siteconfiguration_specialpublisdescr.py index 4944d88..0e7bcfd 100644 --- a/mainsite/migrations/0007_siteconfiguration_specialpublisdescr.py +++ b/mainsite/migrations/0007_siteconfiguration_specialpublisdescr.py @@ -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, ), ] diff --git a/mainsite/migrations/0008_publication_unknown_date.py b/mainsite/migrations/0008_publication_unknown_date.py index 810d600..1d74680 100644 --- a/mainsite/migrations/0008_publication_unknown_date.py +++ b/mainsite/migrations/0008_publication_unknown_date.py @@ -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", + ), ), ] diff --git a/mainsite/models.py b/mainsite/models.py index 9145e4a..a8bef7f 100644 --- a/mainsite/models.py +++ b/mainsite/models.py @@ -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"] diff --git a/mainsite/tests.py b/mainsite/tests.py index 7ce503c..a39b155 100644 --- a/mainsite/tests.py +++ b/mainsite/tests.py @@ -1,3 +1 @@ -from django.test import TestCase - # Create your tests here. diff --git a/mainsite/urls.py b/mainsite/urls.py index 478ab12..b98c4c7 100644 --- a/mainsite/urls.py +++ b/mainsite/urls.py @@ -1,4 +1,5 @@ from django.conf.urls import url + from . import views urlpatterns = [ diff --git a/mainsite/views.py b/mainsite/views.py index 07254d4..ddbd008 100644 --- a/mainsite/views.py +++ b/mainsite/views.py @@ -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) diff --git a/manage.py b/manage.py index 624d703..aa81b93 100755 --- a/manage.py +++ b/manage.py @@ -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 " diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..579121d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,5 @@ +[tool.isort] +profile = "black" + +[tool.ruff.lint] +ignore = ["F403", "F405"]