{"action":"created","issue":{"url":"https://api.github.com/repos/revsys/django-tos/issues/21","repository_url":"https://api.github.com/repos/revsys/django-tos","labels_url":"https://api.github.com/repos/revsys/django-tos/issues/21/labels{/name}","comments_url":"https://api.github.com/repos/revsys/django-tos/issues/21/comments","events_url":"https://api.github.com/repos/revsys/django-tos/issues/21/events","html_url":"https://github.com/revsys/django-tos/pull/21","id":167199756,"number":21,"title":"Add cache middleware and check on every request","user":{"login":"blag","id":597113,"avatar_url":"https://avatars.githubusercontent.com/u/597113?v=3","gravatar_id":"","url":"https://api.github.com/users/blag","html_url":"https://github.com/blag","followers_url":"https://api.github.com/users/blag/followers","following_url":"https://api.github.com/users/blag/following{/other_user}","gists_url":"https://api.github.com/users/blag/gists{/gist_id}","starred_url":"https://api.github.com/users/blag/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/blag/subscriptions","organizations_url":"https://api.github.com/users/blag/orgs","repos_url":"https://api.github.com/users/blag/repos","events_url":"https://api.github.com/users/blag/events{/privacy}","received_events_url":"https://api.github.com/users/blag/received_events","type":"User","site_admin":false},"labels":[],"state":"closed","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":4,"created_at":"2016-07-23T19:37:44Z","updated_at":"2016-08-03T14:56:15Z","closed_at":"2016-08-03T14:56:10Z","pull_request":{"url":"https://api.github.com/repos/revsys/django-tos/pulls/21","html_url":"https://github.com/revsys/django-tos/pull/21","diff_url":"https://github.com/revsys/django-tos/pull/21.diff","patch_url":"https://github.com/revsys/django-tos/pull/21.patch"},"body":"Overriding the login view doesn't work for all use cases. Also if you have particularly long-lived sessions, signed in users may be able to use your site for days/weeks/months after you change your TOS without agreeing to the new one.\r\n\r\nThis adds optional middleware that uses a configured cache to check for TOS agreement on every GET request.\r\n\r\nAdvantages\r\n----------\r\n\r\n* Is 100% optional\r\n* Get Django >= 1.9 support for free! (`apps.AppConfig`)\r\n* Allows sites to allow users to skip TOS check (admins, staff, etc.) by setting cache keys (see example below)\r\n* Uses `request.session` to get user ID without querying database\r\n* Cache keys use user ID instead of username or email because the user ID will never change\r\n* Uses signals to invalidate cached agreements\r\n* Uses Django's built-in [cache versioning](https://docs.djangoproject.com/en/1.9/topics/cache/#cache-versioning) to invalidate cache keys\r\n\r\nDisadvantages\r\n-------------\r\n\r\n* Requires a cache key for all users and (optionally) additional keys for all staff users\r\n* Doesn't clear out old versions of cache keys\r\n* ~~Requires a named cache specifically for `tos` because...~~ Not anymore!\r\n* ~~Invalidates **ALL** keys in cache when `TermsOfService` is saved~~ Not anymore!\r\n* Hasn't been used in production yet\r\n* ~~I haven't written documentation~~ Done!\r\n* I haven't written tests for this code yet\r\n\r\nEfficiency\r\n----------\r\n\r\n* Best case for staff users: 2 cache hits\r\n* Best case for non-staff users: 1 cache miss, 2 cache hits\r\n* Worst case: 1 cache hit, 1 cache miss, a database query, 1 cache set\r\n\r\nNotes\r\n-----\r\n\r\nWe prefix our cache keys manually (with `django:tos:`) because we don't want to step on other apps' cache keys if it's configured to use a common cache. I'm kind of surprised Django doesn't have a way to do this on a per-app basis.\r\n\r\nExample\r\n-------\r\n\r\nThis is how a site could allow staff users and superusers to skip the TOS check.\r\n```python\r\nfrom django.apps import AppConfig, apps\r\nfrom django.conf import settings\r\nfrom django.contrib.auth import get_user_model\r\nfrom django.core.cache import caches\r\nfrom django.db.models import Q\r\nfrom django.db.models.signals import post_save, pre_save\r\nfrom django.dispatch import receiver\r\n\r\n\r\nclass MyAppConfig(AppConfig):\r\n    def ready(self):\r\n        if 'tos' in settings.INSTALLED_APPS:\r\n            cache = caches[getattr(settings, 'TOS_CACHE_NAME', 'default')]\r\n            tos_app = apps.get_app_config('tos')\r\n            TermsOfService = tos_app.get_model('TermsOfService')\r\n\r\n            @receiver(post_save, sender=get_user_model(), dispatch_uid='set_staff_in_cache_for_tos')\r\n            def set_staff_in_cache_for_tos(user, instance, **kwargs):\r\n                if kwargs.get('raw', False):\r\n                    return\r\n\r\n                # Get the cache prefix\r\n                key_version = cache.get('django:tos:key_version')\r\n\r\n                # If the user is staff allow them to skip the TOS agreement check\r\n                if instance.is_staff or instance.is_superuser:\r\n                    cache.set('django:tos:skip_tos_check:{}'.format(instance.id), version=key_version)\r\n\r\n                # But if they aren't make sure we invalidate them from the cache\r\n                elif cache.get('django:tos:skip_tos_check:{}'.format(instance.id), False):\r\n                    cache.delete('django:tos:skip_tos_check:{}'.format(instance.id), version=key_version)\r\n\r\n            @receiver(post_save, sender=TermsOfService, dispatch_uid='add_staff_users_to_tos_cache')\r\n            def add_staff_users_to_tos_cache(*args, **kwargs):\r\n                if kwargs.get('raw', False):\r\n                    return\r\n\r\n                # Get the cache prefix\r\n                key_version = cache.get('django:tos:key_version')\r\n\r\n                # Efficiently cache all of the users who are allowed to skip the TOS\r\n                # agreement check\r\n                cache.set_many({\r\n                    'django:tos:skip_tos_check:{}'.format(staff_user.id): True\r\n                    for staff_user in get_user_model().objects.filter(\r\n                        Q(is_staff=True) | Q(is_superuser=True))\r\n                }, version=key_version)\r\n\r\n            add_staff_users_to_tos_cache(TermsOfService, None)\r\n"},"comment":{"url":"https://api.github.com/repos/revsys/django-tos/issues/comments/237261037","html_url":"https://github.com/revsys/django-tos/pull/21#issuecomment-237261037","issue_url":"https://api.github.com/repos/revsys/django-tos/issues/21","id":237261037,"user":{"login":"nicholasserra","id":512288,"avatar_url":"https://avatars.githubusercontent.com/u/512288?v=3","gravatar_id":"","url":"https://api.github.com/users/nicholasserra","html_url":"https://github.com/nicholasserra","followers_url":"https://api.github.com/users/nicholasserra/followers","following_url":"https://api.github.com/users/nicholasserra/following{/other_user}","gists_url":"https://api.github.com/users/nicholasserra/gists{/gist_id}","starred_url":"https://api.github.com/users/nicholasserra/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nicholasserra/subscriptions","organizations_url":"https://api.github.com/users/nicholasserra/orgs","repos_url":"https://api.github.com/users/nicholasserra/repos","events_url":"https://api.github.com/users/nicholasserra/events{/privacy}","received_events_url":"https://api.github.com/users/nicholasserra/received_events","type":"User","site_admin":false},"created_at":"2016-08-03T14:56:15Z","updated_at":"2016-08-03T14:56:15Z","body":"Thanks!"}}
{"actor":{"display_login":"nicholasserra"}}