Add new version email and info box then new version is available
This commit is contained in:
		@@ -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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user