From b168e0a6423c53de31aae6c444fa1d1c5083afa6 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Wed, 27 Jul 2016 16:03:11 +0200 Subject: [PATCH 01/31] typo in setup.py classifiers --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b27f5b4..eb0af62 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ if __name__ == '__main__': author_email='valentin.samir@crans.org', classifiers=[ 'Environment :: Web Environment', - 'evelopment Status :: 5 - Production/Stable', + 'Development Status :: 5 - Production/Stable', 'Framework :: Django', 'Framework :: Django :: 1.7', 'Framework :: Django :: 1.8', From 6eea76d984ae920c9c33185d3a945f5e5ac9333e Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Fri, 29 Jul 2016 13:56:23 +0200 Subject: [PATCH 02/31] Add pytest-warning to tests and correct some warnings, complete coverage (essentially branch) --- Makefile | 2 +- cas_server/forms.py | 2 +- .../cas_server/{login.css => styles.css} | 0 cas_server/templates/cas_server/base.html | 13 +++--- cas_server/tests/settings.py | 16 ++++++++ cas_server/tests/test_models.py | 40 +++++++++++++++++++ cas_server/tests/utils.py | 11 +++-- cas_server/urls.py | 4 +- requirements-dev.txt | 1 + setup.py | 2 +- tox.ini | 2 +- 11 files changed, 78 insertions(+), 15 deletions(-) rename cas_server/static/cas_server/{login.css => styles.css} (100%) diff --git a/Makefile b/Makefile index c719834..d802dc0 100644 --- a/Makefile +++ b/Makefile @@ -62,7 +62,7 @@ run_server: test_project run_tests: test_venv python setup.py check --restructuredtext --stric - test_venv/bin/py.test --cov=cas_server --cov-report html + test_venv/bin/py.test -rw -x --cov=cas_server --cov-report html rm htmlcov/coverage_html.js # I am really pissed off by those keybord shortcuts test_venv/bin/sphinx-build: test_venv diff --git a/cas_server/forms.py b/cas_server/forms.py index 03c7515..2f18ed9 100644 --- a/cas_server/forms.py +++ b/cas_server/forms.py @@ -32,7 +32,7 @@ class BootsrapForm(forms.Form): self[name].checkbox = True else: attrs['class'] = "form-control" - if field.label: + if field.label: # pragma: no branch (currently all field are hidden or labeled) attrs["placeholder"] = field.label if field.required: attrs["required"] = "required" diff --git a/cas_server/static/cas_server/login.css b/cas_server/static/cas_server/styles.css similarity index 100% rename from cas_server/static/cas_server/login.css rename to cas_server/static/cas_server/styles.css diff --git a/cas_server/templates/cas_server/base.html b/cas_server/templates/cas_server/base.html index db61e1b..bd8663a 100644 --- a/cas_server/templates/cas_server/base.html +++ b/cas_server/templates/cas_server/base.html @@ -15,7 +15,7 @@ - +
@@ -36,18 +36,17 @@ {% for message in messages %}
- {{ message }}
{% endfor %} diff --git a/cas_server/tests/settings.py b/cas_server/tests/settings.py index 4e17ceb..c873ea2 100644 --- a/cas_server/tests/settings.py +++ b/cas_server/tests/settings.py @@ -51,6 +51,22 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.locale.LocaleMiddleware', ] +TEMPLATES = [ + { + 'APP_DIRS': True, + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages' + ] + } + } +] + ROOT_URLCONF = 'cas_server.tests.urls' # Database diff --git a/cas_server/tests/test_models.py b/cas_server/tests/test_models.py index 7a4403c..49f9c75 100644 --- a/cas_server/tests/test_models.py +++ b/cas_server/tests/test_models.py @@ -60,6 +60,24 @@ class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel) with self.assertRaises(models.FederatedUser.DoesNotExist): models.FederatedUser.objects.get(username="test2") + def test_json_attributes(self): + """test the json storage of ``atrributs`` in ``_attributs``""" + provider = models.FederatedIendityProvider.objects.get(suffix="example.com") + user = models.FederatedUser.objects.create( + username=settings.CAS_TEST_USER, + provider=provider, + attributs=settings.CAS_TEST_ATTRIBUTES, + ticket="" + ) + self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), user._attributs) + user.delete() + user = models.FederatedUser.objects.create( + username=settings.CAS_TEST_USER, + provider=provider, + ticket="" + ) + self.assertIsNone(user._attributs) + self.assertIsNone(user.attributs) class FederateSLOTestCase(TestCase, UserModels): """test for the federated SLO model""" @@ -231,3 +249,25 @@ class TicketTestCase(TestCase, UserModels, BaseServicePattern): self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest']) # only 1 ticket remain in the db self.assertEqual(len(models.ServiceTicket.objects.all()), 1) + + def test_json_attributes(self): + """test the json storage of ``atrributs`` in ``_attributs``""" + # ge an authenticated client + client = get_auth_client() + # get the user associated to the client + user = self.get_user(client) + ticket = models.ServiceTicket.objects.create( + user=user, + service=self.service, + attributs=settings.CAS_TEST_ATTRIBUTES, + service_pattern=self.service_pattern + ) + self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), ticket._attributs) + ticket.delete() + ticket = models.ServiceTicket.objects.create( + user=user, + service=self.service, + service_pattern=self.service_pattern + ) + self.assertIsNone(ticket._attributs) + self.assertIsNone(ticket.attributs) diff --git a/cas_server/tests/utils.py b/cas_server/tests/utils.py index 515b653..d020724 100644 --- a/cas_server/tests/utils.py +++ b/cas_server/tests/utils.py @@ -12,16 +12,21 @@ """Some utils functions for tests""" from cas_server.default_settings import settings +import django from django.test import Client -from django.template import loader, Context +from django.template import loader from django.utils import timezone +if django.VERSION < (1, 8): + from django.template import Context +else: + Context = lambda x:x import cgi import six from threading import Thread from lxml import etree from six.moves import BaseHTTPServer -from six.moves.urllib.parse import urlparse, parse_qsl +from six.moves.urllib.parse import urlparse, parse_qsl, parse_qs from datetime import timedelta from cas_server import models @@ -166,7 +171,7 @@ class HttpParamsHandler(BaseHTTPServer.BaseHTTPRequestHandler): postvars = cgi.parse_multipart(self.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': length = int(self.headers.get('content-length')) - postvars = cgi.parse_qs(self.rfile.read(length), keep_blank_values=1) + postvars = parse_qs(self.rfile.read(length), keep_blank_values=1) else: postvars = {} self.server.PARAMS = postvars diff --git a/cas_server/urls.py b/cas_server/urls.py index aa014f2..d4e691c 100644 --- a/cas_server/urls.py +++ b/cas_server/urls.py @@ -16,8 +16,10 @@ from django.views.decorators.debug import sensitive_post_parameters, sensitive_v from cas_server import views +app_name = "cas_server" + urlpatterns = [ - url(r'^$', RedirectView.as_view(pattern_name="cas_server:login")), + url(r'^$', RedirectView.as_view(pattern_name="cas_server:login", permanent=False)), url( '^login$', sensitive_post_parameters('password')( diff --git a/requirements-dev.txt b/requirements-dev.txt index c394fb1..d2d3902 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,6 +3,7 @@ tox>=1.8.1 pytest>=2.6.4 pytest-django>=2.8.0 pytest-pythonpath>=0.3 +pytest-warnings pytest-cov>=2.2.1 requests>=2.4 requests_futures>=0.9.5 diff --git a/setup.py b/setup.py index eb0af62..a8a5a8d 100644 --- a/setup.py +++ b/setup.py @@ -66,5 +66,5 @@ if __name__ == '__main__': download_url="https://github.com/nitmir/django-cas-server/releases", zip_safe=False, setup_requires=['pytest-runner'], - tests_require=['pytest', 'pytest-django', 'pytest-pythonpath'], + tests_require=['pytest', 'pytest-django', 'pytest-pythonpath', 'pytest-warnings', 'mock>=1'], ) diff --git a/tox.ini b/tox.ini index bdf50f0..401c249 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ whitelist_externals= [testenv] commands= - py.test {posargs:cas_server/tests/} + py.test -rw {posargs:cas_server/tests/} {[post_cmd]commands} whitelist_externals={[post_cmd]whitelist_externals} From b6cffcf482ed7df0ba4ebb5f946c474debc6d15e Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Fri, 29 Jul 2016 14:33:02 +0200 Subject: [PATCH 03/31] Add new version email and info box then new version is available --- README.rst | 10 +++ cas_server/__init__.py | 4 ++ cas_server/default_settings.py | 9 +++ .../management/commands/cas_clean_sessions.py | 1 + .../migrations/0008_newversionwarning.py | 22 +++++++ cas_server/models.py | 61 +++++++++++++++++ cas_server/static/cas_server/alert-version.js | 25 +++++++ cas_server/templates/cas_server/base.html | 12 +++- cas_server/tests/settings.py | 3 + cas_server/tests/test_models.py | 38 +++++++++++ cas_server/tests/test_utils.py | 26 ++++++++ cas_server/tests/test_view.py | 23 +++++++ cas_server/utils.py | 66 +++++++++++++++++++ requirements-dev.txt | 1 + setup.py | 3 +- 15 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 cas_server/migrations/0008_newversionwarning.py create mode 100644 cas_server/static/cas_server/alert-version.js diff --git a/README.rst b/README.rst index 59b9ba1..9cb5573 100644 --- a/README.rst +++ b/README.rst @@ -219,6 +219,16 @@ Federation settings ``_remember_provider``. +New version warnings settings +----------------------------- + +* ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new + version of the application is avaible. Once closed by a user, it is not displayed to this user + until the next new version. The default is ``True``. +* ``CAS_NEW_VERSION_EMAIL_WARNING``: A bolean sot sending a email to ``settings.ADMINS`` when a new + version is available. The default is ``True``. + + Tickets validity settings ------------------------- diff --git a/cas_server/__init__.py b/cas_server/__init__.py index 085927b..c138255 100644 --- a/cas_server/__init__.py +++ b/cas_server/__init__.py @@ -9,5 +9,9 @@ # # (c) 2015-2016 Valentin Samir """A django CAS server application""" + +#: version of the application +VERSION = '0.6.1' + #: path the the application configuration class default_app_config = 'cas_server.apps.CasAppConfig' diff --git a/cas_server/default_settings.py b/cas_server/default_settings.py index c7b2b12..69a2fdf 100644 --- a/cas_server/default_settings.py +++ b/cas_server/default_settings.py @@ -140,6 +140,15 @@ CAS_FEDERATE = False #: Time after witch the cookie use for “remember my identity provider” expire (one week). CAS_FEDERATE_REMEMBER_TIMEOUT = 604800 +#: A :class:`bool` for diplaying a warning on html pages then a new version of the application +#: is avaible. Once closed by a user, it is not displayed to this user until the next new version. +CAS_NEW_VERSION_HTML_WARNING = True +#: A :class:`bool` for sending emails to ``settings.ADMINS`` when a new version is available. +CAS_NEW_VERSION_EMAIL_WARNING = True +#: URL to the pypi json of the application. Used to retreive the version number of the last version. +#: You should not change it. +CAS_NEW_VERSION_JSON_URL = "https://pypi.python.org/pypi/django-cas-server/json" + GLOBALS = globals().copy() for name, default_value in GLOBALS.items(): # get the current setting value, falling back to default_value diff --git a/cas_server/management/commands/cas_clean_sessions.py b/cas_server/management/commands/cas_clean_sessions.py index 437bcb5..5de4ebf 100644 --- a/cas_server/management/commands/cas_clean_sessions.py +++ b/cas_server/management/commands/cas_clean_sessions.py @@ -23,3 +23,4 @@ class Command(BaseCommand): def handle(self, *args, **options): models.User.clean_deleted_sessions() + models.NewVersionWarning.send_mails() diff --git a/cas_server/migrations/0008_newversionwarning.py b/cas_server/migrations/0008_newversionwarning.py new file mode 100644 index 0000000..f5e4b19 --- /dev/null +++ b/cas_server/migrations/0008_newversionwarning.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-07-27 21:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cas_server', '0007_auto_20160723_2252'), + ] + + operations = [ + migrations.CreateModel( + name='NewVersionWarning', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.CharField(max_length=255)), + ], + ), + ] diff --git a/cas_server/models.py b/cas_server/models.py index 6e87d40..0c44f6f 100644 --- a/cas_server/models.py +++ b/cas_server/models.py @@ -18,15 +18,19 @@ from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible +from django.core.exceptions import ValidationError +from django.core.mail import send_mail import re import sys +import smtplib import logging from datetime import timedelta from concurrent.futures import ThreadPoolExecutor from requests_futures.sessions import FuturesSession import cas_server.utils as utils +from . import VERSION #: logger facility logger = logging.getLogger(__name__) @@ -1003,3 +1007,60 @@ class Proxy(models.Model): def __str__(self): return self.url + + +class NewVersionWarning(models.Model): + """ + Bases: :class:`django.db.models.Model` + + The last new version available version sent + """ + version = models.CharField(max_length=255) + + @classmethod + def send_mails(cls): + """ + For each new django-cas-server version, if the current instance is not up to date + send one mail to ``settings.ADMINS``. + """ + if settings.CAS_NEW_VERSION_EMAIL_WARNING and settings.ADMINS: + try: + obj = cls.objects.get() + except cls.DoesNotExist: + obj = NewVersionWarning.objects.create(version=VERSION) + LAST_VERSION = utils.last_version() + if LAST_VERSION is not None and LAST_VERSION != obj.version: + if utils.decode_version(VERSION) < utils.decode_version(LAST_VERSION): + try: + send_mail( + ( + '%sA new version of django-cas-server is available' + ) % settings.EMAIL_SUBJECT_PREFIX, + u''' +A new version of the django-cas-server is available. + +Your version: %s +New version: %s + +Upgrade using: + * pip install -U django-cas-server + * fetching the last release on + https://github.com/nitmir/django-cas-server/ or on + https://pypi.python.org/pypi/django-cas-server + +After upgrade, do not forget to run: + * ./manage.py migrate + * ./manage.py collectstatic +and to reload your wsgi server (apache2, uwsgi, gunicord, etc…) + +--\u0020 +django-cas-server +'''.strip() % (VERSION, LAST_VERSION), + settings.SERVER_EMAIL, + ["%s <%s>" % admin for admin in settings.ADMINS], + fail_silently=False, + ) + obj.version = LAST_VERSION + obj.save() + except smtplib.SMTPException as error: # pragma: no cover (should not happen) + logger.error("Unable to send new version mail: %s" % error) diff --git a/cas_server/static/cas_server/alert-version.js b/cas_server/static/cas_server/alert-version.js new file mode 100644 index 0000000..fb277a1 --- /dev/null +++ b/cas_server/static/cas_server/alert-version.js @@ -0,0 +1,25 @@ +function alert_version(last_version){ + jQuery(function( $ ){ + $('#alert-version').click(function( e ){ + e.preventDefault(); + var date = new Date(); + date.setTime(date.getTime()+(10*365*24*60*60*1000)); + var expires = "; expires="+date.toGMTString(); + document.cookie = "cas-alert-version=" + last_version + expires + "; path=/"; + }); + + var nameEQ="cas-alert-version=" + var ca = document.cookie.split(';'); + var value; + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') + c = c.substring(1,c.length); + if (c.indexOf(nameEQ) == 0) + value = c.substring(nameEQ.length,c.length); + } + if(value === last_version){ + $('#alert-version').parent().hide(); + } + }); +} diff --git a/cas_server/templates/cas_server/base.html b/cas_server/templates/cas_server/base.html index bd8663a..5abc8bd 100644 --- a/cas_server/templates/cas_server/base.html +++ b/cas_server/templates/cas_server/base.html @@ -31,8 +31,14 @@
- {% block ante_messages %}{% endblock %} {% if auto_submit %}