Commit b6cffcf4 authored by Valentin Samir's avatar Valentin Samir

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

parent 6eea76d9
......@@ -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()
# -*- 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)
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">&#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 %}
<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
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()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment