├── tests ├── __init__.py ├── test_app │ ├── __init__.py │ ├── models.py │ ├── views.py │ └── tests.py └── run_tests.py ├── active_users ├── __init__.py ├── api │ ├── urls.py │ ├── views.py │ └── __init__.py ├── middleware.py ├── settings.py └── keys.py ├── REQUIREMENTS ├── Dockerfile ├── .gitignore ├── MANIFEST.in ├── docker-compose.yml ├── .github └── workflows │ ├── publish_to_pypi.yaml │ └── test_on_push.yaml ├── CHANGELOG.md ├── tox.ini ├── LICENSE ├── setup.py └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /active_users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /REQUIREMENTS: -------------------------------------------------------------------------------- 1 | django>=1.11 2 | django-redis>=4.9.0 3 | six 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python 2 | 3 | RUN pip install tox 4 | COPY . /src 5 | 6 | VOLUME /src 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/* 2 | dist/* 3 | build/* 4 | *.egg-info 5 | *.pyc 6 | .tox/ 7 | .venv 8 | venv 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft active_users 2 | recursive-exclude active_users *.pyc 3 | include REQUIREMENTS 4 | include README.rst 5 | include LICENSE 6 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | redis: 2 | image: redis 3 | ports: 4 | - "6379:6379" 5 | 6 | app: 7 | build: . 8 | working_dir: /src 9 | restart: always 10 | links: 11 | - redis 12 | environment: 13 | - REDIS_HOST=redis://redis:6379/0 14 | command: tox 15 | -------------------------------------------------------------------------------- /active_users/api/urls.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from django.urls import re_path 3 | 4 | from . import views 5 | 6 | 7 | _patterns = [ 8 | re_path(r'^active-users-info/$', views.active_users_info_view) 9 | ] 10 | 11 | try: 12 | from django.conf.urls import patterns 13 | 14 | urlpatterns = patterns('', *_patterns) 15 | except ImportError: 16 | urlpatterns = _patterns 17 | -------------------------------------------------------------------------------- /active_users/api/views.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | 4 | from django.http import HttpResponse 5 | 6 | from . import get_active_users 7 | 8 | 9 | def active_users_info_view(request): 10 | """ View for info about active users """ 11 | data = get_active_users() 12 | # Compatible with Django 1.5, 1.6 13 | return HttpResponse( 14 | json.dumps({'data': data, 'count': len(data)}, ensure_ascii=False), 15 | content_type='application/json', 16 | ) 17 | -------------------------------------------------------------------------------- /.github/workflows/publish_to_pypi.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to PyPI 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build_publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | - name: Install wheel 17 | run: pip install wheel 18 | - name: Build package 19 | run: python setup.py bdist_wheel --universal 20 | - name: Publish to PyPI 21 | uses: pypa/gh-action-pypi-publish@release/v1 22 | with: 23 | password: ${{ secrets.pypi_token }} 24 | -------------------------------------------------------------------------------- /tests/test_app/views.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from django.conf.urls import include 3 | from django.http import HttpResponse 4 | from django.urls import re_path 5 | 6 | 7 | def active_view(request): 8 | return HttpResponse('

Test view has responded

') 9 | 10 | 11 | def excluded_view(request): 12 | return HttpResponse('

Excluded view has responded

') 13 | 14 | 15 | _patterns = [ 16 | re_path(r'^active-users/', include('active_users.api.urls')), 17 | re_path(r'^excluded/$', excluded_view), 18 | re_path(r'^$', active_view), 19 | ] 20 | 21 | try: 22 | from django.conf.urls import patterns 23 | urlpatterns = patterns('', *_patterns) 24 | except ImportError: 25 | urlpatterns = _patterns 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v0.4.1 4 | 5 | - Pycharm autoformat; 6 | - `ActiveUsersSessionMiddleware` using `pk` as a universal attribute instead of `id`; 7 | 8 | ## v0.4.0 9 | 10 | - Added support for Python 3.9, 3.10, 3.11 11 | - Added support for Django 4 12 | - Dropped support for Python 2.7, 3.4, 3.5 13 | - Dropped support for Django 1.x 14 | 15 | ## v0.3.0 16 | 17 | - Added support for Python 3.8 18 | - Added support for Django 3.0 19 | - Dropped support for Python 3.3 20 | - Dropped support for Django 1.5 and 1.6 21 | - Fixed documentation 22 | 23 | ## v0.2.2 24 | 25 | - Added support for latest version of django-redis package 26 | 27 | ## v0.2.1 28 | 29 | - Fixed bug with missing api folder in a wheel package 30 | 31 | ## v0.2 32 | 33 | - Added support for Django 2.0 34 | 35 | ## v0.1 36 | 37 | Initial release 38 | -------------------------------------------------------------------------------- /active_users/api/__init__.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | try: 3 | from django.utils.encoding import force_str 4 | except ImportError: 5 | # FIXME: Django 2 compatibility 6 | from django.utils.encoding import force_text as force_str 7 | 8 | from django_redis import get_redis_connection 9 | 10 | from active_users.settings import active_users_settings as settings 11 | 12 | 13 | def get_active_users_keys(pattern='au:*'): 14 | """ Get all keys from redis """ 15 | return [force_str(key) for key in get_redis_connection().keys(pattern)] 16 | 17 | 18 | def get_active_users_count(): 19 | """ Get redis keys count """ 20 | return len(get_active_users_keys()) 21 | 22 | 23 | def get_active_users(): 24 | """ Get list of all active users """ 25 | return [ 26 | settings.KEY_CLASS.key_to_dict(key) for key in get_active_users_keys() 27 | ] 28 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{36}-django{20,21,22,30,31,32} 3 | py{37}-django{20,21,22,30,31,32} 4 | py{38}-django{20,21,22,30,31,32,40,41,42} 5 | py{39}-django{20,21,22,30,31,32,40,41,42} 6 | py{310}-django{32,40,41,42} 7 | py{311}-django{41,42} 8 | skip_missing_interpreters = True 9 | 10 | [testenv] 11 | passenv = * 12 | 13 | basepython = 14 | py35: python3.5 15 | py36: python3.6 16 | py37: python3.7 17 | py38: python3.8 18 | py39: python3.9 19 | py310: python3.10 20 | py311: python3.11 21 | 22 | commands = python tests/run_tests.py 23 | 24 | deps = 25 | django20: Django>=2.0,<2.1 26 | django21: Django>=2.1,<2.2 27 | django22: Django>=2.2,<2.3 28 | django30: Django>=3.0,<3.1 29 | django31: Django>=3.1,<3.2 30 | django32: Django>=3.2,<3.3 31 | django40: Django>=4.0,<4.1 32 | django41: Django>=4.1,<4.2 33 | django42: Django>=4.2,<4.3 34 | -------------------------------------------------------------------------------- /active_users/middleware.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | import re 3 | 4 | from django.utils.functional import cached_property 5 | from django_redis import get_redis_connection 6 | 7 | from active_users.settings import active_users_settings as settings 8 | 9 | try: 10 | from django.utils.deprecation import MiddlewareMixin 11 | except ImportError: 12 | parent_class = object 13 | else: 14 | parent_class = MiddlewareMixin 15 | 16 | 17 | class ActiveUsersSessionMiddleware(parent_class): 18 | 19 | @cached_property 20 | def redis_client(self): 21 | return get_redis_connection() 22 | 23 | def process_request(self, request): 24 | if settings.EXCLUDE_URL_PATTERNS: 25 | if any(re.search(pat, request.path) 26 | for pat in settings.EXCLUDE_URL_PATTERNS): 27 | return 28 | if request.user.pk is not None: 29 | key = settings.KEY_CLASS.create_from_request(request) 30 | self.redis_client.setex( 31 | name=key.dump(), 32 | value=0, 33 | time=settings.KEY_EXPIRE 34 | ) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-2018 Nikita Ekaterinchuk 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='django-active-users', 5 | version='0.4.1', 6 | packages=['active_users', 'active_users.api'], 7 | url='https://github.com/n-elloco/django-active-users', 8 | license='MIT', 9 | author='Nikita Ekaterinchuk', 10 | author_email='en.elloco@gmail.com', 11 | description='Monitoring of active users in Django using Redis', 12 | long_description=open('README.rst').read(), 13 | install_requires=open('REQUIREMENTS').read(), 14 | classifiers=[ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Environment :: Web Environment', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | 'Programming Language :: Python', 21 | 'Programming Language :: Python :: 3.6', 22 | 'Programming Language :: Python :: 3.7', 23 | 'Programming Language :: Python :: 3.8', 24 | 'Programming Language :: Python :: 3.9', 25 | 'Programming Language :: Python :: 3.10', 26 | 'Programming Language :: Python :: 3.11', 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /active_users/settings.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from django.conf import settings 3 | from django.test.signals import setting_changed 4 | 5 | try: 6 | from django.utils.module_loading import import_string 7 | except ImportError: 8 | from django.utils.module_loading import import_by_path as import_string 9 | 10 | from active_users.keys import AbstractActiveUserEntry 11 | 12 | PREFIX = 'ACTIVE_USERS' 13 | 14 | DEFAULTS = { 15 | 'KEY_EXPIRE': 20, 16 | 'KEY_CLASS': 'active_users.keys.ActiveUserEntry', 17 | 'EXCLUDE_URL_PATTERNS': [] 18 | } 19 | 20 | 21 | class ActiveUsersSettings(object): 22 | 23 | def __init__(self): 24 | for key, default in DEFAULTS.items(): 25 | value = getattr(settings, '{0}_{1}'.format(PREFIX, key), default) 26 | self.set_setting(key, value) 27 | 28 | assert issubclass(self.KEY_CLASS, AbstractActiveUserEntry) 29 | 30 | def set_setting(self, key, value): 31 | setattr( 32 | self, key, import_string(value) if key == 'KEY_CLASS' else value) 33 | 34 | 35 | active_users_settings = ActiveUsersSettings() 36 | 37 | 38 | def reload_settings(*args, **kwargs): 39 | if kwargs['setting'].startswith(PREFIX): 40 | key = kwargs['setting'].replace(PREFIX + '_', '') 41 | if key in DEFAULTS: 42 | active_users_settings.set_setting( 43 | key, 44 | kwargs['value'] or DEFAULTS[key] 45 | ) 46 | 47 | 48 | setting_changed.connect(reload_settings) 49 | -------------------------------------------------------------------------------- /.github/workflows/test_on_push.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | tags: 6 | - '!refs/tags/*' 7 | branches: 8 | - '*' 9 | 10 | jobs: 11 | run_tests: 12 | runs-on: [ubuntu-20.04] 13 | 14 | services: 15 | redis: 16 | image: redis 17 | ports: 18 | - 6379:6379 19 | 20 | strategy: 21 | max-parallel: 4 22 | matrix: 23 | python-version: [3.7, 3.8, 3.9] 24 | django-version: ["2.0", 2.1, 2.2, "3.0", 3.1, 3.2, "4.0", 4.1, 4.2] 25 | exclude: 26 | - python-version: 3.7 27 | django-version: "4.0" 28 | - python-version: 3.7 29 | django-version: 4.1 30 | - python-version: 3.7 31 | django-version: 4.2 32 | 33 | include: 34 | - python-version: "3.10" 35 | django-version: 3.2 36 | - python-version: "3.10" 37 | django-version: "4.0" 38 | - python-version: "3.10" 39 | django-version: 4.1 40 | - python-version: "3.10" 41 | django-version: 4.2 42 | 43 | - python-version: 3.11 44 | django-version: 4.1 45 | - python-version: 3.11 46 | django-version: 4.2 47 | steps: 48 | - uses: actions/checkout@v1 49 | - uses: actions/setup-python@v1 50 | with: 51 | python-version: ${{ matrix.python-version }} 52 | - name: Install requirements 53 | run: | 54 | pip install -r REQUIREMENTS 55 | pip install -U Django~=${{ matrix.django-version }} 56 | - name: Tests 57 | run: python tests/run_tests.py 58 | -------------------------------------------------------------------------------- /tests/run_tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import sys 4 | 5 | from django.conf import settings 6 | 7 | 8 | MIDDLEWARE = ( 9 | 'django.middleware.common.CommonMiddleware', 10 | 'django.contrib.sessions.middleware.SessionMiddleware', 11 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 12 | 'active_users.middleware.ActiveUsersSessionMiddleware', 13 | ) 14 | 15 | 16 | def main(): 17 | current_dir = os.path.dirname(os.path.abspath(__file__)) 18 | sys.path.insert(0, os.path.join(current_dir, '..')) 19 | 20 | conf_kwargs = dict( 21 | CACHES={ 22 | 'default': { 23 | 'BACKEND': 'django_redis.cache.RedisCache', 24 | 'LOCATION': os.environ.get( 25 | 'REDIS_HOST', 'redis://127.0.0.1:6379/0'), 26 | } 27 | }, 28 | DATABASES={ 29 | 'default': { 30 | 'ENGINE': 'django.db.backends.sqlite3', 31 | 'NAME': 'test.db', 32 | 'TEST_NAME': 'test.db' 33 | } 34 | }, 35 | SITE_ID=1, 36 | MIDDLEWARE=MIDDLEWARE, 37 | INSTALLED_APPS=( 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.sites', 42 | 'test_app' 43 | ), 44 | ROOT_URLCONF='test_app.views', 45 | SECRET_KEY='secret', 46 | ) 47 | 48 | settings.configure(**conf_kwargs) 49 | 50 | try: 51 | # For django>=1.7 52 | from django import setup 53 | except ImportError: 54 | pass 55 | else: 56 | setup() 57 | 58 | from django.test.utils import get_runner 59 | runner = get_runner(settings)() 60 | return runner.run_tests(('test_app',)) 61 | 62 | 63 | if __name__ == '__main__': 64 | failures = main() 65 | sys.exit(failures) 66 | -------------------------------------------------------------------------------- /tests/test_app/tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import json 3 | import time 4 | 5 | from django.contrib.auth.models import User 6 | from django.test import TestCase, Client 7 | from django.test.utils import override_settings 8 | from django_redis import get_redis_connection 9 | 10 | from active_users.api import get_active_users_count, get_active_users 11 | from active_users.settings import active_users_settings 12 | 13 | 14 | class ActiveUsersTest(TestCase): 15 | 16 | username = 'test' 17 | user_password = 'secret' 18 | 19 | @classmethod 20 | def setUpClass(cls): 21 | super(ActiveUsersTest, cls).setUpClass() 22 | cls.redis_client = get_redis_connection() 23 | 24 | cls.key_class = active_users_settings.KEY_CLASS 25 | 26 | cls.client = Client() 27 | cls.user = User.objects.create_user( 28 | username=cls.username, 29 | email='test@test.com', 30 | password=cls.user_password 31 | ) 32 | 33 | def setUp(self): 34 | self.client.login(username=self.username, password=self.user_password) 35 | 36 | def tearDown(self): 37 | self.redis_client.flushdb() 38 | 39 | def test_view(self): 40 | self.url_request('/') 41 | self.assertEqual(get_active_users_count(), 1) 42 | 43 | users = get_active_users() 44 | user = users.pop() 45 | 46 | self.assertEqual(user['user_id'], str(self.user.id)) 47 | self.assertEqual(user['username'], self.username) 48 | 49 | @override_settings(ACTIVE_USERS_KEY_EXPIRE=3) 50 | def test_expire_setting(self): 51 | self.url_request('/') 52 | time.sleep(3) 53 | self.assertEqual(get_active_users_count(), 0) 54 | 55 | @override_settings(ACTIVE_USERS_EXCLUDE_URL_PATTERNS=['/excluded']) 56 | def test_excluded_view(self): 57 | self.url_request('/excluded/') 58 | self.assertEqual(get_active_users_count(), 0) 59 | 60 | def test_api_view(self): 61 | self.url_request('/') 62 | response = self.url_request('/active-users/active-users-info/') 63 | 64 | data = json.loads(response.content.decode()) 65 | self.assertIn('data', data) 66 | self.assertIn('count', data) 67 | 68 | def url_request(self, url): 69 | response = self.client.get(url) 70 | self.assertEqual(response.status_code, 200) 71 | 72 | return response 73 | -------------------------------------------------------------------------------- /active_users/keys.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from six import text_type 3 | 4 | 5 | class AbstractActiveUserEntry(object): 6 | """ Abstract class for redis key instance""" 7 | 8 | fields = tuple() 9 | key_prefix = 'au' 10 | 11 | def __init__(self): 12 | for field in self.fields: 13 | setattr(self, field, u'') 14 | 15 | @classmethod 16 | def create_from_request(cls, request): 17 | """ Fill object from request """ 18 | raise NotImplementedError 19 | 20 | @classmethod 21 | def create_from_key(cls, key): 22 | """ Load object from redis key """ 23 | key_items = key.split(u':') 24 | instance = cls() 25 | # First item is prefix 26 | for i, field in enumerate(cls.fields, start=1): 27 | setattr(instance, field, key_items.get(i, u'')) 28 | return instance 29 | 30 | def dump(self): 31 | """ Dump object to redis key """ 32 | key_items = [self.key_prefix] 33 | for field in self.fields: 34 | key_items.append(getattr(self, field, u'')) 35 | return u':'.join(map(text_type, key_items)) 36 | 37 | @classmethod 38 | def key_to_dict(cls, key): 39 | """ Convert redis key to dict directly """ 40 | key_items = key.split(u':')[1:] # First item is prefix 41 | return dict(zip(cls.fields, key_items)) 42 | 43 | def to_dict(self): 44 | """ Serialize object to dict """ 45 | return {field: getattr(self, field) for field in self.fields} 46 | 47 | 48 | class ActiveUserEntry(AbstractActiveUserEntry): 49 | """ Default key class""" 50 | fields = ('user_id', 'session_id', 'ip', 'username') 51 | 52 | @classmethod 53 | def create_from_request(cls, request): 54 | instance = cls() 55 | if request.user.id is not None: 56 | instance.user_id = request.user.id 57 | instance.username = request.user.username 58 | instance.ip = cls.get_client_ip(request) 59 | if request.session: 60 | instance.session_id = request.session.session_key 61 | return instance 62 | 63 | @staticmethod 64 | def get_client_ip(request): 65 | x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') 66 | if x_forwarded_for: 67 | ip = x_forwarded_for.split(',')[0] 68 | else: 69 | ip = request.META.get('REMOTE_ADDR', '127.0.0.1') 70 | return ip 71 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | MONITORING OF DJANGO ACTIVE USERS 2 | ================================= 3 | 4 | .. image:: https://img.shields.io/pypi/v/django-active-users.svg 5 | :target: https://pypi.python.org/pypi/django-active-users 6 | 7 | .. image:: https://github.com/n-elloco/django-active-users/workflows/Tests/badge.svg?branch=master 8 | :target: https://github.com/n-elloco/django-active-users/actions?query=workflow%3A%22Tests%22 9 | 10 | 11 | *Online monitoring of active users in Django using Redis* 12 | 13 | Collecting information about active users in the Django application 14 | for last specified time interval, using Redis cache. 15 | 16 | 17 | Requirements 18 | ------------ 19 | 20 | - Python: 3.6+ 21 | - Django: 2.0+ 22 | - Django-redis: 23 | 4.9.0 24 | 25 | 4.11.0 (for Django 3.0+) 26 | 27 | 28 | Install 29 | ------- 30 | 31 | .. code-block:: bash 32 | 33 | pip install django-active-users 34 | 35 | 36 | Setup 37 | ----- 38 | 39 | Your django application should have a Redis cache setting. 40 | See more in ``django-redis`` `official documentation `_. 41 | 42 | Add ``active_users.middleware.ActiveUsersSessionMiddleware`` to your project's 43 | ``MIDDLEWARE`` after the ``AuthenticationMiddleware``. 44 | 45 | .. code-block:: python 46 | 47 | MIDDLEWARE = ( 48 | ... 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 50 | 'active_users.middleware.ActiveUsersSessionMiddleware', 51 | ... 52 | ) 53 | 54 | 55 | Settings 56 | -------- 57 | 58 | ``ACTIVE_USERS_KEY_EXPIRE`` - Time of key expire (interval after the last request during which the visitor is considered as active) in seconds. Default is 20. 59 | 60 | ``ACTIVE_USERS_EXCLUDE_URL_PATTERNS`` - List of regular expressions for excluded URLs. If they are matched, the visitor (and pageview) key will not be create. 61 | 62 | ``ACTIVE_USERS_KEY_CLASS`` - Class of visitor key entry. It should be a descendant of ``active_users.keys.AbstractActiveUserEntry``. 63 | Default ``active_users.keys.ActiveUserEntry``. See more in `Custom dimensions in the keys`_ 64 | 65 | 66 | API 67 | --- 68 | 69 | You can use API for getting information about active users. 70 | You can also call methods from ``active_users.api`` module directly. 71 | 72 | **Methods:** 73 | 74 | - ``get_active_users_count`` - returns count of active users (users, who visited site for the last time interval, 75 | which is set by option ``ACTIVE_USERS_KEY_EXPIRE``) 76 | 77 | - ``get_active_users`` - returns list of dictionaries with information about active users; 78 | keys of dictionaries are set from field ``fields`` of entry class, which is set by option ``ACTIVE_USERS_KEY_CLASS`` 79 | 80 | 81 | Also, you can include special view in your Django application, adding the URL pattern to your ``urls.py`` file 82 | 83 | 84 | .. code-block:: python 85 | 86 | urlpatterns = [ 87 | ... 88 | url(r'^active-users/', include('active_users.api.urls')), 89 | ... 90 | ] 91 | 92 | 93 | Custom dimensions in the keys 94 | ----------------------------- 95 | 96 | By default, 4 dimensions are saved in the keys (``user_id``, ``session_id``, ``IP``, ``username``). 97 | This is provided by class ``ActiveUserEntry``, which inherits from abstract class ``AbstractActiveUserEntry``. 98 | You can use your dimensions, defined in your own class, which should be a descendant of class ``AbstractActiveUserEntry`` and 99 | you need to define the logic of using these dimensions in the class method ``create_from_request``. 100 | 101 | For example, we need to save information about service, which makes request, and this information we can take 102 | from request header. Also, we want use all dimensions from class ``ActiveUserEntry``. 103 | 104 | 105 | .. code-block:: python 106 | 107 | from active_users.keys import ActiveUserEntry 108 | 109 | class OurActiveUserEntry(ActiveUserEntry): 110 | 111 | fields = ('service_id',) + ActiveUserEntry.fields 112 | 113 | @classmethod 114 | def create_from_request(cls, request): 115 | instance = super(OurActiveUserEntry, cls).create_from_request(request) 116 | instance.service_id = request.META.get('HTTP_SERVICE_ID', u'') 117 | return instance 118 | 119 | 120 | At the end, we need to specify option ``ACTIVE_USERS_KEY_CLASS`` in the ``settings.py``. 121 | 122 | 123 | .. code-block:: python 124 | 125 | ACTIVE_USERS_KEY_CLASS = 'my_app.keys.OurActiveUserEntry' 126 | --------------------------------------------------------------------------------