├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── online_users ├── __init__.py ├── admin.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py └── tests │ ├── __init__.py │ ├── test_middleware.py │ └── test_models.py ├── requirements.txt ├── setup.py └── test.db /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # IntelliJ 92 | .idea/ 93 | *.iml 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Lawrence Weikum 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include online_users *.py 4 | include requirements.txt -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-online-users 2 | =================== 3 | 4 | Tracks the time of each user's last action 5 | 6 | Using middleware, django-online-users will keep track of each user and the timestamp of their last action in the database. 7 | 8 | Admins can see this data in the admin portal, and the database can be queried using timedeltas. 9 | 10 | This is meant for smaller applications as each HTTP request will result in a database entry update. 11 | 12 | Requirements 13 | ------------ 14 | 15 | - Python: 2.7, 3.3, 3.4, 3.5, 3.6 16 | - Django: 1.11+ 17 | 18 | 19 | Setup 20 | ----------- 21 | 22 | 1. Add "online_users" to your ``INSTALLED_APPS`` 23 | 24 | 25 | .. code-block:: python 26 | 27 | INSTALLED_APPS = [ 28 | ... 29 | 'online_users', 30 | ] 31 | 32 | 33 | 2. Add the ``OnlineNowMiddleware`` to your ``MIDDLEWARE_CLASSES`` after the ``SessionMiddleware`` 34 | 35 | 36 | .. code-block:: python 37 | 38 | MIDDLEWARE_CLASSES = ( 39 | ... 40 | 'online_users.middleware.OnlineNowMiddleware', 41 | ) 42 | 43 | 44 | 3. Run ``python manage.py makemigrations`` and ``python manage.py migrate`` to create the tables in the database. 45 | 46 | 47 | Rollout 48 | ------- 49 | 50 | If this application is intended to be used in an application already in production, it's recommended that the middleware 51 | be added to the settings file *after* the application has been installed and the database has been migrated. Otherwise, 52 | HTTP requests may result in a 500 error. 53 | 54 | 55 | Use 56 | --- 57 | 58 | * The time of the last action for each user can be seen in the admin portal at http://127.0.0.1:8000/admin/online_users/onlineuser/ 59 | 60 | 61 | * To retrieve the current number of users online in the last 15 minutes 62 | 63 | 64 | .. code-block:: python 65 | 66 | from datetime import timedelta 67 | ... 68 | user_activity_objects = OnlineUserActivity.get_user_activities() 69 | number_of_active_users = user_activity_objects.count() 70 | 71 | 72 | * A timedelta can also be specified to the ``get_user_activities()`` with to find activity 73 | 74 | 75 | .. code-block:: python 76 | 77 | from datetime import timedelta 78 | ... 79 | user_activity_objects = OnlineUserActivity.get_user_activities(timedelta(minutes=60) 80 | users = (user for user in user_activity_objects) 81 | 82 | 83 | API 84 | --- 85 | 86 | OnlineUserActivity holds two values: `user`, the user in question, and `last_activity`, a timestamp stored as a DateTimeField. 87 | 88 | ``OnlineUserActivity.get_user_activities()`` will return OnlineUserActivity objects for the last 15 minutes by default. 89 | Developers can also pass in a time delta of a different length of time for the query. The response is a QuerySet containing 90 | the active users during that time period, and the items are sorted in descending chronological order. 91 | 92 | 93 | Change Log 94 | ------------ 95 | 96 | * 0.3 - Updating to have on_delete = models.CASCADE 97 | * 0.2 - Updating to Django 1.11. Changed from basic User model to settings.AUTH_USER_MODEL. -------------------------------------------------------------------------------- /online_users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lawrencemq/django-online-users/02cffe9bac8288b30e741d4e677af2682b5747dd/online_users/__init__.py -------------------------------------------------------------------------------- /online_users/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from online_users.models import OnlineUserActivity 4 | 5 | 6 | class OnlineUserActivityAdmin(admin.ModelAdmin): 7 | list_display = ('user', 'last_activity',) 8 | search_fields = ['user__username', ] 9 | list_filter = ['last_activity'] 10 | 11 | def get_ordering(self, request): 12 | return ['last_activity'] 13 | 14 | admin.site.register(OnlineUserActivity, OnlineUserActivityAdmin) 15 | -------------------------------------------------------------------------------- /online_users/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils.deprecation import MiddlewareMixin 2 | 3 | from online_users.models import OnlineUserActivity 4 | 5 | 6 | class OnlineNowMiddleware(MiddlewareMixin): 7 | """Updates the OnlineUserActivity database whenever an authenticated user makes an HTTP request.""" 8 | 9 | @staticmethod 10 | def process_request(request): 11 | user = request.user 12 | if not user.is_authenticated: 13 | return 14 | 15 | OnlineUserActivity.update_user_activity(user) 16 | -------------------------------------------------------------------------------- /online_users/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.1 on 2017-01-05 19:59 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='OnlineUserActivity', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('last_activity', models.DateTimeField()), 24 | ('user', models.OneToOneField( 25 | on_delete=django.db.models.deletion.CASCADE, 26 | to=settings.AUTH_USER_MODEL) 27 | ), 28 | ], 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /online_users/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lawrencemq/django-online-users/02cffe9bac8288b30e741d4e677af2682b5747dd/online_users/migrations/__init__.py -------------------------------------------------------------------------------- /online_users/models.py: -------------------------------------------------------------------------------- 1 | from datetime import timedelta 2 | 3 | from django.conf import settings 4 | from django.db import models 5 | from django.utils import timezone 6 | 7 | 8 | class OnlineUserActivity(models.Model): 9 | user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) 10 | last_activity = models.DateTimeField() 11 | 12 | @staticmethod 13 | def update_user_activity(user): 14 | """Updates the timestamp a user has for their last action. Uses UTC time.""" 15 | OnlineUserActivity.objects.update_or_create(user=user, defaults={'last_activity': timezone.now()}) 16 | 17 | @staticmethod 18 | def get_user_activities(time_delta=timedelta(minutes=15)): 19 | """ 20 | Gathers OnlineUserActivity objects from the database representing active users. 21 | 22 | :param time_delta: The amount of time in the past to classify a user as "active". Default is 15 minutes. 23 | :return: QuerySet of active users within the time_delta 24 | """ 25 | starting_time = timezone.now() - time_delta 26 | return OnlineUserActivity.objects.filter(last_activity__gte=starting_time).order_by('-last_activity') 27 | -------------------------------------------------------------------------------- /online_users/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import sys 4 | 5 | from django.conf import settings 6 | from django.conf.urls import url 7 | from django.core.management import call_command 8 | from django.http import HttpResponse 9 | 10 | current_dir = os.path.dirname(os.path.abspath(__file__)) 11 | sys.path.insert(0, os.path.join(current_dir, '..')) 12 | 13 | conf_kwargs = dict( 14 | ALLOWED_HOSTS=('testserver', '127.0.0.1', 'localhost', '::1'), 15 | DATABASES={ 16 | 'default': { 17 | 'ENGINE': 'django.db.backends.sqlite3', 18 | 'NAME': 'test.db', 19 | 'TEST_NAME': 'test.db' 20 | } 21 | }, 22 | SITE_ID=1, 23 | MIDDLEWARE=[ 24 | 'django.middleware.common.CommonMiddleware', 25 | 'django.contrib.sessions.middleware.SessionMiddleware', 26 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 27 | 'online_users.middleware.OnlineNowMiddleware', 28 | ], 29 | INSTALLED_APPS=( 30 | 'django.contrib.auth', 31 | 'django.contrib.contenttypes', 32 | 'django.contrib.sessions', 33 | 'django.contrib.sites', 34 | 'online_users' 35 | ), 36 | ROOT_URLCONF=( 37 | url(r'^', lambda _: HttpResponse('