Add new version email and info box then new version is available
This commit is contained in:
parent
6eea76d984
commit
b6cffcf482
15 changed files with 301 additions and 3 deletions
10
README.rst
10
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
|
||||
-------------------------
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -23,3 +23,4 @@ class Command(BaseCommand):
|
|||
|
||||
def handle(self, *args, **options):
|
||||
models.User.clean_deleted_sessions()
|
||||
models.NewVersionWarning.send_mails()
|
||||
|
|
22
cas_server/migrations/0008_newversionwarning.py
Normal file
22
cas_server/migrations/0008_newversionwarning.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
25
cas_server/static/cas_server/alert-version.js
Normal file
25
cas_server/static/cas_server/alert-version.js
Normal 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();
|
||||
}
|
||||
});
|
||||
}
|
|
@ -31,8 +31,14 @@
|
|||
<div class="row">
|
||||
<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">
|
||||
{% block ante_messages %}{% endblock %}
|
||||
{% 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">×</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 %}
|
||||
<div {% spaceless %}
|
||||
{% if message.level == message_levels.DEBUG %}
|
||||
|
@ -58,5 +64,9 @@
|
|||
</div> <!-- /container -->
|
||||
<script src="{{settings.CAS_COMPONENT_URLS.jquery}}"></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>
|
||||
</html>
|
||||
|
|
|
@ -97,3 +97,6 @@ USE_TZ = True
|
|||
# https://docs.djangoproject.com/en/1.9/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
CAS_NEW_VERSION_HTML_WARNING = False
|
||||
CAS_NEW_VERSION_EMAIL_WARNING = False
|
||||
|
|
|
@ -16,7 +16,9 @@ import django
|
|||
from django.test import TestCase, Client
|
||||
from django.test.utils import override_settings
|
||||
from django.utils import timezone
|
||||
from django.core import mail
|
||||
|
||||
import mock
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
|
@ -271,3 +273,39 @@ class TicketTestCase(TestCase, UserModels, BaseServicePattern):
|
|||
)
|
||||
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)
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
from django.test import TestCase, RequestFactory
|
||||
|
||||
import six
|
||||
import warnings
|
||||
|
||||
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, 'toto'), 'toto')
|
||||
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())
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.utils import timezone
|
|||
|
||||
import random
|
||||
import json
|
||||
import mock
|
||||
from lxml import etree
|
||||
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
|
||||
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):
|
||||
"""Test a successul login"""
|
||||
# we get a client who fetch a frist time the login page and the login form default
|
||||
|
|
|
@ -25,11 +25,19 @@ import hashlib
|
|||
import crypt
|
||||
import base64
|
||||
import six
|
||||
import requests
|
||||
import time
|
||||
import logging
|
||||
|
||||
from importlib import import_module
|
||||
from datetime import datetime, timedelta
|
||||
from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode
|
||||
|
||||
from . import VERSION
|
||||
|
||||
#: logger facility
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def json_encode(obj):
|
||||
"""Encode a python object to json"""
|
||||
|
@ -51,6 +59,16 @@ def context(params):
|
|||
"""
|
||||
params["settings"] = settings
|
||||
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
|
||||
|
||||
|
||||
|
@ -603,3 +621,51 @@ def check_password(method, password, hashed_password, charset):
|
|||
)(password).hexdigest().encode("ascii") == hashed_password.lower()
|
||||
else:
|
||||
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)
|
||||
|
|
|
@ -9,3 +9,4 @@ requests>=2.4
|
|||
requests_futures>=0.9.5
|
||||
lxml>=3.4
|
||||
six>=1
|
||||
mock>=1
|
||||
|
|
3
setup.py
3
setup.py
|
@ -1,8 +1,7 @@
|
|||
import os
|
||||
import pkg_resources
|
||||
from setuptools import setup
|
||||
|
||||
VERSION = '0.6.1'
|
||||
from cas_server import VERSION
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme:
|
||||
README = readme.read()
|
||||
|
|
Loading…
Reference in a new issue