Compare commits

..

25 commits

Author SHA1 Message Date
fc36d0cf34 feat(authens): Release 0.2.0 2024-07-04 23:15:21 +02:00
9eacf8c19b Merge pull request 'Rename get_next_page to get_success_url' (#59) from update into master
Reviewed-on: #59
2024-07-04 23:14:29 +02:00
sinavir
2fdb73e485 Rename get_next_page to get_success_url and update pyproject
From django 4.1, this function has been renamed
(https://docs.djangoproject.com/en/4.2/releases/4.1/)
2024-07-04 23:12:50 +02:00
3d0b9e578d feat: Move code under src, add nix tooling and switch to pyproject.toml 2024-07-04 22:46:55 +02:00
Martin Pepin
58747e57b3 Merge branch 'dorian/improve-pwd-reset' into 'master'
Dorian/improve pwd reset

See merge request klub-dev-ens/authens!36
2022-12-01 16:31:20 +01:00
Dorian Lesbre
83d63845ae
Fix typo 2022-11-02 10:26:52 +01:00
Dorian Lesbre
018fd5249f
Clearer password reset error message 2022-11-02 10:26:26 +01:00
ba1fa8c82c We need to use a string 2022-08-27 17:53:32 +02:00
0b0926ba76 Specify default auto field to avoid creating migrations by other apps using authens 2022-08-27 17:44:12 +02:00
Tom Hubrecht
e1eb6cc577 Version 0.1b5 2021-10-10 19:19:56 +02:00
Tom Hubrecht
a5682f0674 Merge branch 'kerl/bump-ci-versions' into 'master'
Bump python and Django versions in CI

See merge request klub-dev-ens/authens!35
2021-10-10 19:16:11 +02:00
Martin Pépin
6ea557e76a
Drop python3.5 support 2021-10-10 17:37:23 +02:00
Martin Pépin
cec9edba12
Fix Django version in the django22 tox env 2021-10-08 00:03:24 +02:00
Tom Hubrecht
22b4d440c3 Merge branch 'kerl/issue20' into 'master'
Handle LOGOUT_URL=None in the logout view

Closes #20

See merge request klub-dev-ens/authens!34
2021-10-03 17:50:10 +02:00
Martin Pépin
7809a3c639
Apply Tomate's suggestion: not x → x is not None 2021-10-03 17:34:25 +02:00
Martin Pépin
7b7a51ec5f
Bump python and Django versions in CI 2021-10-02 17:27:50 +02:00
Martin Pépin
acea5a6f4e
Handle LOGOUT_URL=None in the logout view
The view used to crash when `LOGOUT_URL` was not set and the user was
connect via CAS.

Cause: we assumed the result of `LogoutView.get_next_page()` to be a
  string and tried to prepend the domain name of the site to it.

Fix: redirect to the CAS' logout view without any `next_page` parameter.
2021-10-02 17:02:30 +02:00
Tom Hubrecht
f24395ea41 Version 0.1b4 2021-05-31 15:40:12 +00:00
Martin Pepin
577ecd677c Merge branch 'thubrecht/apromides' into 'master'
Gère les membres `staffs`

See merge request klub-dev-ens/authens!32
2021-05-25 09:19:21 +00:00
cb3a1b232f On définit STAFF_ENTRANCE_YEAR dans le modèle CASAccount 2021-05-25 11:10:34 +02:00
e6bc3d25dc Last isort fix 2021-05-23 13:22:30 +02:00
ca5659b0a2 Update README 2021-05-23 13:20:01 +02:00
26e4b7c1af Fix isort 2021-05-23 13:18:46 +02:00
f362bb1a60 Rajoute la possibilité d'autoriser l'accès aux membres de staffs, avec une promotion valant 0 2021-05-23 13:11:17 +02:00
8492d95040 Update isort config 2021-05-22 22:27:38 +02:00
47 changed files with 292 additions and 87 deletions

View file

@ -35,32 +35,37 @@ before_script:
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq python3-dev libldap2-dev libsasl2-dev - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq python3-dev libldap2-dev libsasl2-dev
- pip install tox - pip install tox
python35:
image: python:3.5
stage: tests
script:
- tox -e py35-django22
python36: python36:
image: python:3.6 image: python:3.6
stage: tests stage: tests
script: script:
- tox -e py36-django22 - tox -e py36-django22
- tox -e py36-django30 - tox -e py36-django31
- tox -e py36-django32
python37: python37:
image: python:3.7 image: python:3.7
stage: tests stage: tests
script: script:
- tox -e py37-django22 - tox -e py37-django22
- tox -e py37-django30 - tox -e py37-django31
- tox -e py37-django32
python38: python38:
image: python:3.8 image: python:3.8
stage: tests stage: tests
script: script:
- tox -e py38-django22 - tox -e py38-django22
- tox -e py38-django30 - tox -e py38-django31
- tox -e py38-django32
python39:
image: python:3.9
stage: tests
script:
- tox -e py39-django22
- tox -e py39-django31
- tox -e py39-django32
# --- # ---
# Release artifacts # Release artifacts

View file

@ -1,3 +0,0 @@
include LICENSE
recursive-include authens/static *
recursive-include authens/templates *

View file

@ -74,6 +74,16 @@ AUTHENS_USE_OLDCAS = False
AUTHENS_USE_PASSWORD = False AUTHENS_USE_PASSWORD = False
``` ```
- (Optionnel) Il est possible d'autoriser la connexion via CAS pour les membres
de `staffs`, lorsque cette option est activée, leur promotion est fixée à 0,
lorsque l'option est désactivée, une tentative de connexion renvoie une erreur
car le format de `$HOME` n'est pas valide.
```python
AUTHENS_ALLOW_STAFF = True
```
- (Optionnel) AuthENS utilise le paramètre Django standard - (Optionnel) AuthENS utilise le paramètre Django standard
[`LOGIN_REDIRECT_URL`](https://docs.djangoproject.com/en/3.0/ref/settings/#login-redirect-url) [`LOGIN_REDIRECT_URL`](https://docs.djangoproject.com/en/3.0/ref/settings/#login-redirect-url)
par défaut pour rediriger l'utilisateurice en cas de connexion réussie. par défaut pour rediriger l'utilisateurice en cas de connexion réussie.
@ -86,7 +96,7 @@ AUTHENS_USE_PASSWORD = False
### Création d'utilisateurices ### Création d'utilisateurices
AuthENS maintient une tables des comptes clipper connus. AuthENS maintient une table des comptes clipper connus.
Cette table est automatiquement mise à jour lors qu'une personne se connecte via Cette table est automatiquement mise à jour lors qu'une personne se connecte via
le CAS pour la première fois. le CAS pour la première fois.
En revanche lorsqu'un nouveau compte est créé manuellement et que ce compte En revanche lorsqu'un nouveau compte est créé manuellement et que ce compte

53
default.nix Normal file
View file

@ -0,0 +1,53 @@
{
sources ? import ./npins,
pkgs ? import sources.nixpkgs { },
}:
let
nix-pkgs = import sources.nix-pkgs { inherit pkgs; };
python3 = pkgs.python3.override { packageOverrides = _: _: { inherit (nix-pkgs) python-cas; }; };
deploy-pypi = pkgs.writeShellApplication {
name = "deploy-pypi";
runtimeInputs = [
(pkgs.python3.withPackages (ps: [
ps.setuptools
ps.build
ps.twine
]))
];
text = ''
# Clean the repository
rm -rf dist
python -m build
twine upload dist/*
'';
};
in
{
devShell = pkgs.mkShell {
name = "cas-eleves.dev";
packages = [
(python3.withPackages (ps: [
ps.django
ps.python-ldap
ps.python-cas
]))
pkgs.gettext
pkgs.gtranslator
];
};
publishShell = pkgs.mkShell {
name = "loadcredential.publish";
packages = [ deploy-pypi ];
};
}

View file

@ -13,9 +13,10 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path 1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from example import views
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from example import views
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),

80
npins/default.nix Normal file
View file

@ -0,0 +1,80 @@
# Generated by npins. Do not modify; will be overwritten regularly
let
data = builtins.fromJSON (builtins.readFile ./sources.json);
version = data.version;
mkSource =
spec:
assert spec ? type;
let
path =
if spec.type == "Git" then
mkGitSource spec
else if spec.type == "GitRelease" then
mkGitSource spec
else if spec.type == "PyPi" then
mkPyPiSource spec
else if spec.type == "Channel" then
mkChannelSource spec
else
builtins.throw "Unknown source type ${spec.type}";
in
spec // { outPath = path; };
mkGitSource =
{
repository,
revision,
url ? null,
hash,
branch ? null,
...
}:
assert repository ? type;
# At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository
# In the latter case, there we will always be an url to the tarball
if url != null then
(builtins.fetchTarball {
inherit url;
sha256 = hash;
})
else
assert repository.type == "Git";
let
urlToName =
url: rev:
let
matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url;
short = builtins.substring 0 7 rev;
appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else "";
in
"${if matched == null then "source" else builtins.head matched}${appendShort}";
name = urlToName repository.url revision;
in
builtins.fetchGit {
url = repository.url;
rev = revision;
inherit name;
narHash = hash;
};
mkPyPiSource =
{ url, hash, ... }:
builtins.fetchurl {
inherit url;
sha256 = hash;
};
mkChannelSource =
{ url, hash, ... }:
builtins.fetchTarball {
inherit url;
sha256 = hash;
};
in
if version == 4 then
builtins.mapAttrs (_: mkSource) data.pins
else
throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`"

22
npins/sources.json Normal file
View file

@ -0,0 +1,22 @@
{
"pins": {
"nix-pkgs": {
"type": "Git",
"repository": {
"type": "Git",
"url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs.git"
},
"branch": "main",
"revision": "46879d052e4a694ceb3027dbcff641c44e0ae1bd",
"url": null,
"hash": "sha256-/Yn3NDYA76bv8x06jahLAJ2z54L0vFeAtQKzyW3MfGA="
},
"nixpkgs": {
"type": "Channel",
"name": "nixpkgs-unstable",
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-24.11pre647329.6842b061970b/nixexprs.tar.xz",
"hash": "04inyzaffqclmymaphc4cwhywnyk51hi44508kfbsj9h57875hb6"
}
},
"version": 4
}

49
pyproject.toml Normal file
View file

@ -0,0 +1,49 @@
[build-system]
requires = ["setuptools", "setuptools_scm"]
build-backend = "setuptools.build_meta"
[project]
name = "authens"
dynamic = ["version"]
authors = [
{name = "Klub Dev ENS", email = "klub-dev@ens.fr"},
{name = "Tom Hubrecht", email = "pypi@mail.hubrecht.ovh"},
]
description = "CAS Authentication Client at the ENS."
license = {file = "LICENSE"}
readme = "README.md"
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 4.1",
"Framework :: Django :: 4.2",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
]
dependencies = [
"django >= 4.1",
"python-ldap >= 3, < 4",
"python-cas >= 1.5, < 2",
]
[project.urls]
Homepage = "https://git.dgnum.eu/DGNum/authens"
Repository = "https://git.dgnum.eu/DGNum/authens"
Issues = "https://git.dgnum.eu/DGNum/authens/issues"
[tool.setuptools.dynamic]
version = {attr = "authens.__VERSION__"}
[tool.setuptools.packages.find]
where = ["src"]

View file

@ -14,5 +14,4 @@ profile = black
combine_as_imports = true combine_as_imports = true
known_django = django known_django = django
known_first_party = authens,tests known_first_party = authens,tests
line_length = 88
sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER sections = FUTURE,STDLIB,THIRDPARTY,DJANGO,FIRSTPARTY,LOCALFOLDER

View file

@ -1,37 +0,0 @@
import setuptools
with open("README.md", "r") as file:
long_description = file.read()
setuptools.setup(
name="authens",
version="0.1b3",
author="Klub Dev ENS",
author_email="klub-dev@ens.fr",
description="CAS Authentication at the ENS",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://git.eleves.ens.fr/klub-dev-ens/authens",
packages=setuptools.find_packages(),
include_package_data=True,
classifiers=[
"Environment :: Web Environment",
"Framework :: Django",
"Framework :: Django :: 2.2",
"Framework :: Django :: 3.0",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
],
python_requires=">=3.5",
install_requires=["Django>=2.2", "python-ldap>=3,<4", "python-cas>=1.5,<2"],
)

1
shell.nix Normal file
View file

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

5
src/authens/__init__.py Normal file
View file

@ -0,0 +1,5 @@
__VERSION__ = "0.2.0"
__all__ = [
"__VERSION__",
]

View file

@ -3,3 +3,4 @@ from django.apps import AppConfig
class AuthEnsConfig(AppConfig): class AuthEnsConfig(AppConfig):
name = "authens" name = "authens"
default_auto_field = "django.db.models.AutoField"

View file

@ -1,5 +1,5 @@
LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636" LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636"
AUTHENS_USE_OLDCAS = True AUTHENS_USE_OLDCAS = True
AUTHENS_USE_PASSWORD = True AUTHENS_USE_PASSWORD = True
USE_LDAP = True AUTHENS_ALLOW_STAFF = False
# TODO: CAS_SERVER_URL # TODO: CAS_SERVER_URL

Binary file not shown.

View file

@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-28 20:40+0100\n" "POT-Creation-Date: 2022-11-02 10:22+0100\n"
"PO-Revision-Date: 2021-01-30 20:48+0100\n" "PO-Revision-Date: 2021-01-30 20:48+0100\n"
"Last-Translator: Tom Hubrecht <tom.hubrecht@ens.fr>\n" "Last-Translator: Tom Hubrecht <tom.hubrecht@ens.fr>\n"
"Language-Team: \n" "Language-Team: \n"
@ -22,7 +22,7 @@ msgstr ""
msgid "Ancien login clipper" msgid "Ancien login clipper"
msgstr "Old clipper username" msgstr "Old clipper username"
#: forms.py:18 templates/authens/login_switch.html:14 #: forms.py:18 templates/authens/login_switch.html:15
msgid "Mot de passe" msgid "Mot de passe"
msgstr "Password" msgstr "Password"
@ -39,7 +39,7 @@ msgstr ""
"No user exists with this username, this entrance year and/or this password. " "No user exists with this username, this entrance year and/or this password. "
"Please check your entry. Attention, all fields are case sensitive!" "Please check your entry. Attention, all fields are case sensitive!"
#: models.py:19 models.py:57 #: models.py:19 models.py:60
msgid "utilisateurice" msgid "utilisateurice"
msgstr "user" msgstr "user"
@ -47,36 +47,36 @@ msgstr "user"
msgid "login CAS" msgid "login CAS"
msgstr "CAS username" msgstr "CAS username"
#: models.py:30 models.py:67 #: models.py:30 models.py:70
msgid "année de création du compte CAS" msgid "année de création du compte CAS"
msgstr "year of CAS account creation" msgstr "year of CAS account creation"
#: models.py:34 #: models.py:37
msgid "Compte CAS" msgid "Compte CAS"
msgstr "CAS account" msgstr "CAS account"
#: models.py:35 #: models.py:38
msgid "Comptes CAS" msgid "Comptes CAS"
msgstr "CAS accounts" msgstr "CAS accounts"
#: models.py:39 #: models.py:42
#, python-format #, python-format
msgid "compte CAS %(cas_login)s (promo %(entrance_year)s) lié à %(user)s" msgid "compte CAS %(cas_login)s (promo %(entrance_year)s) lié à %(user)s"
msgstr "CAS account %(cas_login)s (year %(entrance_year)s) linked to %(user)s" msgstr "CAS account %(cas_login)s (year %(entrance_year)s) linked to %(user)s"
#: models.py:63 #: models.py:66
msgid "ancien login CAS" msgid "ancien login CAS"
msgstr "old CAS username" msgstr "old CAS username"
#: models.py:78 #: models.py:81
msgid "Ancien compte CAS" msgid "Ancien compte CAS"
msgstr "Old CAS account" msgstr "Old CAS account"
#: models.py:79 #: models.py:82
msgid "Anciens comptes CAS" msgid "Anciens comptes CAS"
msgstr "Old CAS accounts" msgstr "Old CAS accounts"
#: models.py:83 #: models.py:86
#, python-format #, python-format
msgid "" msgid ""
"Ancien compte CAS %(cas_login)s (promo %(entrance_year)s) lié à %(user)s" "Ancien compte CAS %(cas_login)s (promo %(entrance_year)s) lié à %(user)s"
@ -91,11 +91,11 @@ msgstr "Connection method"
msgid "Clipper" msgid "Clipper"
msgstr "Clipper" msgstr "Clipper"
#: templates/authens/login_switch.html:18 #: templates/authens/login_switch.html:20
msgid "Vieilleux" msgid "Vieilleux"
msgstr "Alumni" msgstr "Alumni"
#: templates/authens/login_switch.html:25 #: templates/authens/login_switch.html:27
#, python-format #, python-format
msgid "" msgid ""
"Si votre fin de scolarité approche, créez un mot de passe pour votre compte " "Si votre fin de scolarité approche, créez un mot de passe pour votre compte "
@ -153,16 +153,25 @@ msgstr "For your information, your username is the following: %(username)s"
msgid "L'équipe %(site_name)s" msgid "L'équipe %(site_name)s"
msgstr "The %(site_name)s team" msgstr "The %(site_name)s team"
#: views.py:86 #: views.py:89
msgid "Connexion échouée !" #, fuzzy
#| msgid "Connexion échouée !"
msgid "Connection échouée !"
msgstr "Connection failed!" msgstr "Connection failed!"
#: views.py:102 #: views.py:105
msgid "" msgid ""
"Un email de réinitialisation du mot de passe vient d'être envoyé à l'adresse " "Si un compte avec cet email existe, un email de réinitialisation vient de "
"indiquée !" "lui être envoyé !"
msgstr "A password reset email has just been sent to the indicated address!" msgstr ""
"A password reset email has just been send to the inidcated address, "
"provided an account with this email exists)"
#: views.py:112 #: views.py:115
msgid "Mot de passe modifié avec succès !" msgid "Mot de passe modifié avec succès !"
msgstr "Password changed successfully!" msgstr "Password changed successfully!"
#~ msgid ""
#~ "Un email de réinitialisation du mot de passe vient d'être envoyé à "
#~ "l'adresse indiquée !"
#~ msgstr "A password reset email has just been sent to the indicated address!"

View file

@ -30,6 +30,9 @@ class CASAccount(models.Model):
verbose_name=_("année de création du compte CAS"), blank=False, null=False verbose_name=_("année de création du compte CAS"), blank=False, null=False
) )
# The entrance year 0 is used for members of staff
STAFF_ENTRANCE_YEAR = 0
class Meta: class Meta:
verbose_name = _("Compte CAS") verbose_name = _("Compte CAS")
verbose_name_plural = _("Comptes CAS") verbose_name_plural = _("Comptes CAS")

View file

@ -1,7 +1,5 @@
"""Helper functions to get CAS metadata and create CAS accounts.""" """Helper functions to get CAS metadata and create CAS accounts."""
import warnings
# TODO: make the python-ldap dependency optional # TODO: make the python-ldap dependency optional
import ldap import ldap
@ -27,10 +25,6 @@ def fetch_cas_account(cas_login):
if not cas_login.isalnum(): if not cas_login.isalnum():
raise ValueError("Illegal CAS login: {}".format(cas_login)) raise ValueError("Illegal CAS login: {}".format(cas_login))
if not getattr(settings, "USE_LDAP", default_conf.USE_LDAP):
warnings.warn("Use of LDAP is disabled", RuntimeWarning)
return None
ldap_url = getattr(settings, "LDAP_SERVER_URL", default_conf.LDAP_SERVER_URL) ldap_url = getattr(settings, "LDAP_SERVER_URL", default_conf.LDAP_SERVER_URL)
ldap_obj = ldap.initialize(ldap_url) ldap_obj = ldap.initialize(ldap_url)
res = ldap_obj.search_s( res = ldap_obj.search_s(

View file

@ -3,6 +3,11 @@ from urllib.parse import urlunparse
from cas import CASClient from cas import CASClient
from django.conf import settings
from authens import conf as default_conf
from authens.models import CASAccount
def get_cas_client(request): def get_cas_client(request):
"""Return a CAS client configured for SPI's CAS.""" """Return a CAS client configured for SPI's CAS."""
@ -26,6 +31,14 @@ def parse_entrance_year(home_dir):
return None return None
dirs = home_dir.split("/") dirs = home_dir.split("/")
allow_staff = getattr(
settings, "AUTHENS_ALLOW_STAFF", default_conf.AUTHENS_ALLOW_STAFF
)
if allow_staff and dirs[:3] == ["", "users", "staffs"]:
return CASAccount.STAFF_ENTRANCE_YEAR
if len(dirs) < 3 or not dirs[2].isdecimal() or dirs[1] != "users": if len(dirs) < 3 or not dirs[2].isdecimal() or dirs[1] != "users":
raise ValueError("Invalid home directory: {}".format(home_dir)) raise ValueError("Invalid home directory: {}".format(home_dir))

View file

@ -102,7 +102,7 @@ class PasswordResetView(SuccessMessageMixin, auth_views.PasswordResetView):
success_url = reverse_lazy("authens:login") success_url = reverse_lazy("authens:login")
success_message = _( success_message = _(
"Un email de réinitialisation vient d'être envoyé à l'adresse indiquée !" "Si un compte avec cet email existe, un email de réinitialisation vient de lui être envoyé !"
) )
@ -138,14 +138,14 @@ class LogoutView(auth_views.LogoutView):
else: else:
self.cas_connected = False self.cas_connected = False
def get_next_page(self): def get_success_url(self):
next_page = super().get_next_page() next_page = super().get_success_url()
if self.cas_connected: if self.cas_connected:
cas_client = get_cas_client(self.request) cas_client = get_cas_client(self.request)
# If the next_url is local (no hostname), make it absolute so that the user # If the next_url is local (no hostname), make it absolute so that the user
# is correctly redirected from CAS. # is correctly redirected from CAS.
if not urlparse(next_page).netloc: if next_page is not None and not urlparse(next_page).netloc:
request = self.request request = self.request
next_page = urlunparse( next_page = urlunparse(
(request.scheme, request.get_host(), next_page, "", "", "") (request.scheme, request.get_host(), next_page, "", "", "")

View file

@ -1,12 +1,12 @@
[tox] [tox]
envlist = envlist =
py{35,36,37,38}-django22, py{36,37,38,39}-django{22,31,32}
py{36,37,38}-django30
[testenv] [testenv]
deps = deps =
django22: Django==2.2.* django22: Django>=2.2.17,<3
django30: Django==3.0.* django31: Django==3.1.*
django32: Django==3.2.*
python-cas python-cas
python-ldap python-ldap
commands = commands =