Add new version email and info box then new version is available

This commit is contained in:
Valentin Samir 2016-07-29 14:33:02 +02:00
parent 6eea76d984
commit b6cffcf482
15 changed files with 301 additions and 3 deletions

View File

@ -219,6 +219,16 @@ Federation settings
``_remember_provider``. ``_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 Tickets validity settings
------------------------- -------------------------

View File

@ -9,5 +9,9 @@
# #
# (c) 2015-2016 Valentin Samir # (c) 2015-2016 Valentin Samir
"""A django CAS server application""" """A django CAS server application"""
#: version of the application
VERSION = '0.6.1'
#: path the the application configuration class #: path the the application configuration class
default_app_config = 'cas_server.apps.CasAppConfig' default_app_config = 'cas_server.apps.CasAppConfig'

View File

@ -140,6 +140,15 @@ CAS_FEDERATE = False
#: Time after witch the cookie use for “remember my identity provider” expire (one week). #: Time after witch the cookie use for “remember my identity provider” expire (one week).
CAS_FEDERATE_REMEMBER_TIMEOUT = 604800 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() GLOBALS = globals().copy()
for name, default_value in GLOBALS.items(): for name, default_value in GLOBALS.items():
# get the current setting value, falling back to default_value # get the current setting value, falling back to default_value

View File

@ -23,3 +23,4 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
models.User.clean_deleted_sessions() models.User.clean_deleted_sessions()
models.NewVersionWarning.send_mails()

View File

@ -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)),
],
),
]

View File

@ -18,15 +18,19 @@ from django.contrib import messages
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible 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 re
import sys import sys
import smtplib
import logging import logging
from datetime import timedelta from datetime import timedelta
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from requests_futures.sessions import FuturesSession from requests_futures.sessions import FuturesSession
import cas_server.utils as utils import cas_server.utils as utils
from . import VERSION
#: logger facility #: logger facility
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -1003,3 +1007,60 @@ class Proxy(models.Model):
def __str__(self): def __str__(self):
return self.url 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)

View File

@ -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();
}
});
}

View File

@ -31,8 +31,14 @@
<div class="row"> <div class="row">
<div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div> <div class="col-lg-3 col-md-3 col-sm-2 col-xs-12"></div>
<div class="col-lg-6 col-md-6 col-sm-8 col-xs-12"> <div class="col-lg-6 col-md-6 col-sm-8 col-xs-12">
{% block ante_messages %}{% endblock %}
{% if auto_submit %}<noscript>{% endif %} {% if auto_submit %}<noscript>{% endif %}
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<div class="alert alert-info alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true" id="alert-version">&#215;</button>
{% blocktrans %}A new version of the application is available. This instance runs {{VERSION}} and the last version is {{LAST_VERSION}}. Please consider upgrading.{% endblocktrans %}
</div>
{% endif %}
{% block ante_messages %}{% endblock %}
{% for message in messages %} {% for message in messages %}
<div {% spaceless %} <div {% spaceless %}
{% if message.level == message_levels.DEBUG %} {% if message.level == message_levels.DEBUG %}
@ -58,5 +64,9 @@
</div> <!-- /container --> </div> <!-- /container -->
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script> <script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></script>
<script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script> <script src="{{settings.CAS_COMPONENT_URLS.bootstrap3_js}}"></script>
{% if settings.CAS_NEW_VERSION_HTML_WARNING and upgrade_available %}
<script src="{% static "cas_server/alert-version.js" %}"></script>
<script>alert_version("{{LAST_VERSION}}")</script>
{% endif %}
</body> </body>
</html> </html>

View File

@ -97,3 +97,6 @@ USE_TZ = True
# https://docs.djangoproject.com/en/1.9/howto/static-files/ # https://docs.djangoproject.com/en/1.9/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
CAS_NEW_VERSION_HTML_WARNING = False
CAS_NEW_VERSION_EMAIL_WARNING = False

View File

@ -16,7 +16,9 @@ import django
from django.test import TestCase, Client from django.test import TestCase, Client
from django.test.utils import override_settings from django.test.utils import override_settings
from django.utils import timezone from django.utils import timezone
from django.core import mail
import mock
from datetime import timedelta from datetime import timedelta
from importlib import import_module from importlib import import_module
@ -271,3 +273,39 @@ class TicketTestCase(TestCase, UserModels, BaseServicePattern):
) )
self.assertIsNone(ticket._attributs) self.assertIsNone(ticket._attributs)
self.assertIsNone(ticket.attributs) self.assertIsNone(ticket.attributs)
@mock.patch("cas_server.utils.last_version", lambda:"1.2.3")
@override_settings(ADMINS=[("Ano Nymous", "ano.nymous@example.net")])
@override_settings(CAS_NEW_VERSION_EMAIL_WARNING=True)
class NewVersionWarningTestCase(TestCase):
"""tests for the new version warning model"""
@mock.patch("cas_server.models.VERSION", "0.1.2")
def test_send_mails(self):
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 1)
self.assertEqual(
mail.outbox[0].subject,
'%sA new version of django-cas-server is available' % settings.EMAIL_SUBJECT_PREFIX
)
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 1)
@mock.patch("cas_server.models.VERSION", "1.2.3")
def test_send_mails_same_version(self):
models.NewVersionWarning.objects.create(version="0.1.2")
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 0)
@override_settings(ADMINS=[])
def test_send_mails_no_admins(self):
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 0)
@override_settings(CAS_NEW_VERSION_EMAIL_WARNING=False)
def test_send_mails_disabled(self):
models.NewVersionWarning.send_mails()
self.assertEqual(len(mail.outbox), 0)

View File

@ -13,6 +13,7 @@
from django.test import TestCase, RequestFactory from django.test import TestCase, RequestFactory
import six import six
import warnings
from cas_server import utils from cas_server import utils
@ -208,3 +209,28 @@ class UtilsTestCase(TestCase):
self.assertEqual(utils.get_tuple(test_tuple, 3), None) self.assertEqual(utils.get_tuple(test_tuple, 3), None)
self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto') self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto')
self.assertEqual(utils.get_tuple(None, 3), None) self.assertEqual(utils.get_tuple(None, 3), None)
def test_last_version(self):
"""
test the function last_version. An internet connection is needed, if you do not have
one, this test will fail and you should ignore it.
"""
try:
# first check if pypi is available
utils.requests.get("https://pypi.python.org/simple/django-cas-server/")
except utils.requests.exceptions.RequestException:
warnings.warn(
(
"Pypi seems not available, perhaps you do not have internet access. "
"Consequently, the test cas_server.tests.test_utils.UtilsTestCase.test_last_"
"version is ignored"
),
RuntimeWarning
)
else:
version = utils.last_version()
self.assertIsInstance(version, six.text_type)
self.assertEqual(len(version.split('.')), 3)
# version is cached 24h so calling it a second time should return the save value
self.assertEqual(version, utils.last_version())

View File

@ -20,6 +20,7 @@ from django.utils import timezone
import random import random
import json import json
import mock
from lxml import etree from lxml import etree
from six.moves import range from six.moves import range
@ -47,6 +48,28 @@ class LoginTestCase(TestCase, BaseServicePattern, CanLogin):
# we prepare a bunch a service url and service patterns for tests # we prepare a bunch a service url and service patterns for tests
self.setup_service_patterns() self.setup_service_patterns()
@override_settings(CAS_NEW_VERSION_HTML_WARNING=True)
@mock.patch("cas_server.utils.last_version", lambda:"1.2.3")
@mock.patch("cas_server.utils.VERSION", "0.1.2")
def test_new_version_available_ok(self):
client = Client()
response = client.get("/login")
self.assertIn(b"A new version of the application is available", response.content)
@override_settings(CAS_NEW_VERSION_HTML_WARNING=True)
@mock.patch("cas_server.utils.last_version", lambda:None)
@mock.patch("cas_server.utils.VERSION", "0.1.2")
def test_new_version_available_badpypi(self):
client = Client()
response = client.get("/login")
self.assertNotIn(b"A new version of the application is available", response.content)
@override_settings(CAS_NEW_VERSION_HTML_WARNING=False)
def test_new_version_available_disabled(self):
client = Client()
response = client.get("/login")
self.assertNotIn(b"A new version of the application is available", response.content)
def test_login_view_post_goodpass_goodlt(self): def test_login_view_post_goodpass_goodlt(self):
"""Test a successul login""" """Test a successul login"""
# we get a client who fetch a frist time the login page and the login form default # we get a client who fetch a frist time the login page and the login form default

View File

@ -25,11 +25,19 @@ import hashlib
import crypt import crypt
import base64 import base64
import six import six
import requests
import time
import logging
from importlib import import_module from importlib import import_module
from datetime import datetime, timedelta from datetime import datetime, timedelta
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
from . import VERSION
#: logger facility
logger = logging.getLogger(__name__)
def json_encode(obj): def json_encode(obj):
"""Encode a python object to json""" """Encode a python object to json"""
@ -51,6 +59,16 @@ def context(params):
""" """
params["settings"] = settings params["settings"] = settings
params["message_levels"] = DEFAULT_MESSAGE_LEVELS params["message_levels"] = DEFAULT_MESSAGE_LEVELS
if settings.CAS_NEW_VERSION_HTML_WARNING:
LAST_VERSION = last_version()
params["VERSION"] = VERSION
params["LAST_VERSION"] = LAST_VERSION
if LAST_VERSION is not None:
t_version = decode_version(VERSION)
t_last_version = decode_version(LAST_VERSION)
params["upgrade_available"] = t_version < t_last_version
else:
params["upgrade_available"] = False
return params return params
@ -603,3 +621,51 @@ def check_password(method, password, hashed_password, charset):
)(password).hexdigest().encode("ascii") == hashed_password.lower() )(password).hexdigest().encode("ascii") == hashed_password.lower()
else: else:
raise ValueError("Unknown password method check %r" % method) raise ValueError("Unknown password method check %r" % method)
def decode_version(version):
"""
decode a version string following version semantic http://semver.org/ input a tuple of int
:param unicode version: A dotted version
:return: A tuple a int
:rtype: tuple
"""
return tuple(int(sub_version) for sub_version in version.split('.'))
def last_version():
"""
Fetch the last version from pypi and return it. On successful fetch from pypi, the response
is cached 24h, on error, it is cached 10 min.
:return: the last django-cas-server version
:rtype: unicode
"""
try:
last_update, version, success = last_version._cache
except AttributeError:
last_update = 0
version = None
success = False
cache_delta = 24 * 3600 if success else 600
if (time.time() - last_update) < cache_delta:
return version
else:
try:
req = requests.get(settings.CAS_NEW_VERSION_JSON_URL)
data = json.loads(req.content)
versions = data["releases"].keys()
versions.sort()
version = versions[-1]
last_version._cache = (time.time(), version, True)
return version
except (
KeyError,
ValueError,
requests.exceptions.RequestException
) as error: # pragma: no cover (should not happen unless pypi is not available)
logger.error(
"Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error)
)
last_version._cache = (time.time(), version, False)

View File

@ -9,3 +9,4 @@ requests>=2.4
requests_futures>=0.9.5 requests_futures>=0.9.5
lxml>=3.4 lxml>=3.4
six>=1 six>=1
mock>=1

View File

@ -1,8 +1,7 @@
import os import os
import pkg_resources import pkg_resources
from setuptools import setup from setuptools import setup
from cas_server import VERSION
VERSION = '0.6.1'
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
README = readme.read() README = readme.read()