diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..c2b8749 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,68 @@ +name: Python Tests + +on: [push] + +jobs: + test: + + runs-on: ubuntu-latest + strategy: + max-parallel: 4 + matrix: + tox_env: + - py36-django111 + - py37-django111 + - py36-django22 + - py37-django22 + include: + - python-version: "3.6" + tox_env: py36-django111 + - python-version: "3.7" + tox_env: py37-django111 + - python-version: "3.6" + tox_env: py36-django22 + - python-version: "3.7" + tox_env: py37-django22 + steps: + - name: Install system dependencies + run: | + sudo apt-get install libpq-dev + - uses: actions/checkout@v1 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip tox + - name: Test with tox + run: | + tox -e ${{ matrix.tox_env }} + - name: Prepare Coverage report + run: tox -e coverage-report + - name: Upload to codecov + uses: codecov/codecov-action@v1.0.2 + with: + token: ${{secrets.CODECOV_TOKEN}} + flags: unittests + + release: + runs-on: ubuntu-latest + needs: [test] + steps: + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: "3.7" + - name: Install build requirements + run: | + python -m pip install wheel + - uses: actions/checkout@v1 + - name: Build package + run: python setup.py sdist bdist_wheel + - name: Publish package + if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/.travis.yml b/.travis.yml index 72c8122..6555111 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,16 +4,20 @@ language: python matrix: include: - - python: 2.7 - env: TOXENV=py27-django110 - python: 2.7 env: TOXENV=py27-django111 - - python: 3.6 - env: TOXENV=py36-django110 - python: 3.6 env: TOXENV=py36-django111 + - python: 3.7 + env: TOXENV=py37-django111 + dist: xenial + sudo: true - python: 3.6 env: TOXENV=py36-django2 + - python: 3.7 + env: TOXENV=py37-django2 + dist: xenial + sudo: true before_cache: - rm -rf $HOME/.cache/pip/log diff --git a/CHANGES b/CHANGES index e251929..10231d5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +0.0.4 (2019-04-01) +================== + - added grace period to SESSION_EXPIRE_AFTER_LAST_ACTIVITY + + 0.0.3 (2017-12-14) ================== - Redirect user to the login page after session timeout instead of the root page diff --git a/Makefile b/Makefile index a171a35..50c44f2 100644 --- a/Makefile +++ b/Makefile @@ -20,3 +20,9 @@ release: rm -rf dist/* python setup.py sdist bdist_wheel twine upload dist/* + +BLACK_EXCLUDE="/(\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist)/" +black: + pip install --upgrade black + black --verbose --exclude $(BLACK_EXCLUDE) ./src + black --verbose --exclude $(BLACK_EXCLUDE) ./tests diff --git a/README.rst b/README.rst index 7d0565f..d2cd432 100644 --- a/README.rst +++ b/README.rst @@ -1,16 +1,7 @@ -====================== -django-session-timeout -====================== - -.. image:: https://readthedocs.org/projects/django-session-timeout/badge/?version=latest - :target: https://django-session-timeout.readthedocs.io/en/latest/?badge=latest - :alt: Documentation Status +.. start-no-pypi - -Status -====== -.. image:: https://travis-ci.org/labd/django-session-timeout.svg?branch=master - :target: https://travis-ci.org/labd/django-session-timeout +.. image:: https://dev.azure.com/lab-digital-opensource/django-session-timeout/_apis/build/status/labd.django-session-timeout?branchName=master + :target: https://dev.azure.com/lab-digital-opensource/django-session-timeout/_build/latest?definitionId=2&branchName=master .. image:: http://codecov.io/github/LabD/django-session-timeout/coverage.svg?branch=master :target: http://codecov.io/github/LabD/django-session-timeout?branch=master @@ -18,6 +9,20 @@ Status .. image:: https://img.shields.io/pypi/v/django-session-timeout.svg :target: https://pypi.python.org/pypi/django-session-timeout/ +.. image:: https://readthedocs.org/projects/django-session-timeout/badge/?version=stable + :target: https://django-session-timeout.readthedocs.io/en/stable/?badge=stable + :alt: Documentation Status + +.. image:: https://img.shields.io/github/stars/labd/django-session-timeout.svg?style=social&logo=github + :target: https://github.com/Labd/django-session-timeout/stargazers + +.. end-no-pypi + +====================== +django-session-timeout +====================== + +Add timestamp to sessions to expire them independently Installation ============ @@ -56,3 +61,11 @@ To expire the session X seconds after the `last activity`, use the following set .. code-block:: python SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True + + +By default, `last activiy` will be grouped per second. +To group by different period use the following setting: + +.. code-block:: python + + SESSION_EXPIRE_AFTER_LAST_ACTIVITY_GRACE_PERIOD = 60 # group by minute diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 0000000..c552231 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,56 @@ +jobs: + - job: UpdateBuildNumber + pool: + vmImage: ubuntu-16.04 + steps: + - script: | + pip3 install --upgrade setuptools + echo "##vso[build.UpdateBuildNumber]`python3 setup.py --version`-$BUILD_BUILDID" + displayName: Set Version number + + - job: Test + pool: + vmImage: "ubuntu-16.04" + strategy: + matrix: + python 3.6: + python.version: "3.6" + TOXENV: "py36" + python 3.7: + python.version: "3.7" + TOXENV: "py37" + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: "$(python.version)" + - script: | + python -m pip install --upgrade pip + pip3 install --upgrade setuptools + pip3 install --upgrade tox + pip3 install --upgrade codecov + displayName: "Install testing requirements" + - script: tox -e $(TOXENV) + displayName: "Run tox with TOXENV=$(TOXENV)" + - script: | + tox -e coverage-report + codecov + condition: succeeded() + displayName: "Upload coverage information to codecov.io" + + - job: Publish + pool: + vmImage: "ubuntu-16.04" + dependsOn: Test + condition: in(variables['Build.SourceBranch'], 'refs/heads/master') + displayName: "Publish artifacts" + steps: + - task: CopyFiles@2 + displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' + inputs: + SourceFolder: '$(Build.SourcesDirectory)' + Contents: | + **/* + !.git/**/* + TargetFolder: '$(Build.ArtifactStagingDirectory)' + - task: PublishBuildArtifacts@1 + displayName: drop diff --git a/docs/conf.py b/docs/conf.py index 7012e8f..7f1b2eb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -version = '0.0.3' +version = '0.0.4' release = version # The language for content autogenerated by Sphinx. Refer to documentation @@ -139,7 +139,7 @@ # The name for this set of Sphinx documents. # " v documentation" by default. # -# html_title = u'django-session-timeout v0.0.3' +# html_title = u'django-session-timeout v0.0.4' # A shorter title for the navigation bar. Default is the same as html_title. # diff --git a/setup.cfg b/setup.cfg index 99268f5..54b96d8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.0.3 +current_version = 0.0.4 commit = true tag = true tag_name = {new_version} diff --git a/setup.py b/setup.py index 11f87f7..529b5e3 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ +import re + from setuptools import find_packages, setup docs_require = [ @@ -5,29 +7,35 @@ ] tests_require = [ - 'coverage==.4.2', - 'freezegun==0.3.9', - 'pytest==3.0.5', - 'pytest-django==3.1.2', + 'coverage==4.5.3', + 'freezegun==0.3.11', + 'pytest==4.3.1', + 'pytest-django==3.4.8', + 'pytest-cov==2.6.1', # Linting - 'isort==4.2.5', - 'flake8==3.0.3', + 'isort==4.3.15', + 'flake8==3.7.7', 'flake8-blind-except==0.1.1', - 'flake8-debugger==1.4.0', + 'flake8-debugger==3.1.0', ] +with open('README.rst') as fh: + long_description = re.sub( + '^.. start-no-pypi.*^.. end-no-pypi', '', fh.read(), flags=re.M | re.S) + + setup( name='django-session-timeout', - version='0.0.3', + version='0.0.4', description="Middleware to expire sessions after specific amount of time", - long_description=open('README.rst', 'r').read(), + long_description=long_description, url='https://github.com/LabD/django-session-timeout', author="Lab Digital", author_email="opensource@labdigital.nl", install_requires=[ - 'Django>=1.8', - 'six>=1.1', + 'Django>=1.11', + 'six>=1.12', ], tests_require=tests_require, extras_require={ diff --git a/src/django_session_timeout/__init__.py b/src/django_session_timeout/__init__.py index ffcc925..81f0fde 100644 --- a/src/django_session_timeout/__init__.py +++ b/src/django_session_timeout/__init__.py @@ -1 +1 @@ -__version__ = '0.0.3' +__version__ = "0.0.4" diff --git a/src/django_session_timeout/middleware.py b/src/django_session_timeout/middleware.py index a8dc586..d6a9e41 100644 --- a/src/django_session_timeout/middleware.py +++ b/src/django_session_timeout/middleware.py @@ -9,18 +9,19 @@ MiddlewareMixin = object -SESSION_TIMEOUT_KEY = '_session_init_timestamp_' +SESSION_TIMEOUT_KEY = "_session_init_timestamp_" class SessionTimeoutMiddleware(MiddlewareMixin): def process_request(self, request): - if not hasattr(request, 'session') or request.session.is_empty(): + if not hasattr(request, "session") or request.session.is_empty(): return init_time = request.session.setdefault(SESSION_TIMEOUT_KEY, time.time()) expire_seconds = getattr( - settings, 'SESSION_EXPIRE_SECONDS', settings.SESSION_COOKIE_AGE) + settings, "SESSION_EXPIRE_SECONDS", settings.SESSION_COOKIE_AGE + ) session_is_expired = time.time() - init_time > expire_seconds @@ -29,6 +30,11 @@ def process_request(self, request): return redirect_to_login(next=request.path) expire_since_last_activity = getattr( - settings, 'SESSION_EXPIRE_AFTER_LAST_ACTIVITY', False) - if expire_since_last_activity: + settings, "SESSION_EXPIRE_AFTER_LAST_ACTIVITY", False + ) + grace_period = getattr( + settings, "SESSION_EXPIRE_AFTER_LAST_ACTIVITY_GRACE_PERIOD", 1 + ) + + if expire_since_last_activity and time.time() - init_time > grace_period: request.session[SESSION_TIMEOUT_KEY] = time.time() diff --git a/tests/conftest.py b/tests/conftest.py index c71ff96..605428e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,23 +4,20 @@ def pytest_configure(): settings.configure( INSTALLED_APPS=[ - 'django.contrib.contenttypes', - 'django.contrib.auth', - 'django.contrib.sessions' + "django.contrib.contenttypes", + "django.contrib.auth", + "django.contrib.sessions", ], MIDDLEWARE_CLASSES=[], - ROOT_URLCONF='tests.urls', + ROOT_URLCONF="tests.urls", CACHES={ - 'default': { - 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', - 'LOCATION': 'unique-snowflake', + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "unique-snowflake", } }, - SESSION_ENGINE='django.contrib.sessions.backends.cache', + SESSION_ENGINE="django.contrib.sessions.backends.cache", DATABASES={ - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'db.sqlite', - }, - } + "default": {"ENGINE": "django.db.backends.sqlite3", "NAME": "db.sqlite"} + }, ) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 25660f9..1f14474 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -6,12 +6,12 @@ from django_session_timeout.middleware import SessionTimeoutMiddleware -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def r(rf): - req = rf.get('/') + req = rf.get("/") middleware = SessionMiddleware() middleware.process_request(req) - req.session['example_key'] = '1' + req.session["example_key"] = "1" req.session.save() yield req @@ -35,32 +35,32 @@ def test_session_expire(r, settings): settings.SESSION_EXPIRE_SECONDS = 3600 middleware = SessionTimeoutMiddleware() - with freeze_time('2017-08-31 21:46:00'): + with freeze_time("2017-08-31 21:46:00"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 22:45:00'): + with freeze_time("2017-08-31 22:45:00"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 22:46:01'): + with freeze_time("2017-08-31 22:46:01"): response = middleware.process_request(r) assert SESSION_TIMEOUT_KEY not in r.session - assert response['location'] == '/accounts/login/?next=/' + assert response["location"] == "/accounts/login/?next=/" def test_session_expire_no_expire_setting(r, settings): settings.SESSION_COOKIE_AGE = 3600 middleware = SessionTimeoutMiddleware() - with freeze_time('2017-08-31 21:46:00'): + with freeze_time("2017-08-31 21:46:00"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 22:45:00'): + with freeze_time("2017-08-31 22:45:00"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 22:46:01'): + with freeze_time("2017-08-31 22:46:01"): response = middleware.process_request(r) assert SESSION_TIMEOUT_KEY not in r.session - assert response['location'] == '/accounts/login/?next=/' + assert response["location"] == "/accounts/login/?next=/" def test_session_expire_last_activity(r, settings): @@ -68,16 +68,64 @@ def test_session_expire_last_activity(r, settings): settings.SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True middleware = SessionTimeoutMiddleware() - with freeze_time('2017-08-31 20:46:00'): + with freeze_time("2017-08-31 20:46:00"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 21:45:00'): + with freeze_time("2017-08-31 21:45:00"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 21:46:01'): + with freeze_time("2017-08-31 21:46:01"): assert middleware.process_request(r) is None - with freeze_time('2017-08-31 23:46:02'): + with freeze_time("2017-08-31 23:46:02"): response = middleware.process_request(r) assert SESSION_TIMEOUT_KEY not in r.session - assert response['location'] == '/accounts/login/?next=/' \ No newline at end of file + assert response["location"] == "/accounts/login/?next=/" + + +def test_session_expire_last_activity_grace_(r, settings): + settings.SESSION_COOKIE_AGE = 3600 + settings.SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True + settings.SESSION_EXPIRE_AFTER_LAST_ACTIVITY_GRACE_PERIOD = 90 + middleware = SessionTimeoutMiddleware() + + value = None + + with freeze_time("2017-08-31 20:46:00"): + assert middleware.process_request(r) is None + value = r.session[SESSION_TIMEOUT_KEY] + + with freeze_time("2017-08-31 20:47:00"): + assert middleware.process_request(r) is None + assert r.session[SESSION_TIMEOUT_KEY] is value + + with freeze_time("2017-08-31 20:47:31"): + assert middleware.process_request(r) is None + assert r.session[SESSION_TIMEOUT_KEY] is not value + + with freeze_time("2017-08-31 21:47:32"): + response = middleware.process_request(r) + assert SESSION_TIMEOUT_KEY not in r.session + assert response["location"] == "/accounts/login/?next=/" + + +def test_session_expire_last_activity_grace_not_update(r, settings): + settings.SESSION_COOKIE_AGE = 3600 + settings.SESSION_EXPIRE_AFTER_LAST_ACTIVITY = True + settings.SESSION_EXPIRE_AFTER_LAST_ACTIVITY_GRACE_PERIOD = 90 + middleware = SessionTimeoutMiddleware() + + value = None + + with freeze_time("2017-08-31 20:46:00"): + assert middleware.process_request(r) is None + value = r.session[SESSION_TIMEOUT_KEY] + + with freeze_time("2017-08-31 20:47:00"): + assert middleware.process_request(r) is None + assert r.session[SESSION_TIMEOUT_KEY] is value + + with freeze_time("2017-08-31 21:46:01"): + response = middleware.process_request(r) + assert SESSION_TIMEOUT_KEY not in r.session + assert response["location"] == "/accounts/login/?next=/" diff --git a/tox.ini b/tox.ini index 8fd447a..5e71544 100644 --- a/tox.ini +++ b/tox.ini @@ -1,21 +1,19 @@ [tox] envlist = - py{27,36}-django{110, 111} - py{36}-django2 + py{36,37}-django111 + py{36,37}-django22 [testenv] -commands = coverage run --parallel -m pytest {posargs} +commands = coverage run --source django_session_timeout --parallel -m pytest {posargs} deps = - django110: Django>=1.10,<1.11 django111: Django>=1.11,<1.12 - django2: Django>=2.0,<3 + django22: Django>=2.2,<2.3 extras = test -# Uses default basepython otherwise reporting doesn't work on Travis where -# Python 3.5 is only available in 3.5 jobs. [testenv:coverage-report] deps = coverage skip_install = true commands = coverage combine + coverage xml coverage report