├── .gitignore ├── .travis.yml ├── AUTHORS.md ├── LICENCE ├── MANIFEST.in ├── README.md ├── datapurge ├── __init__.py ├── compat.py ├── exceptions.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── purge.py ├── models.py ├── purge │ ├── __init__.py │ ├── actions.py │ ├── policies.py │ └── tasks.py ├── runtests │ ├── __init__.py │ ├── runtests.py │ ├── settings.py │ └── urls.py ├── settings.py ├── tests.py └── views.py ├── setup.cfg ├── setup.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.pot 3 | *.pyc 4 | local_settings.py 5 | .idea 6 | *.egg-info 7 | .tox -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | env: 4 | - TOX_ENV=py2.6-django1.3 5 | - TOX_ENV=py2.6-django1.4 6 | - TOX_ENV=py2.6-django1.5 7 | 8 | - TOX_ENV=py2.7-django1.3 9 | - TOX_ENV=py2.7-django1.4 10 | - TOX_ENV=py2.7-django1.5 11 | - TOX_ENV=py2.7-django1.6 12 | - TOX_ENV=py2.7-django1.7 13 | - TOX_ENV=py2.7-django1.8 14 | - TOX_ENV=py2.7-django1.9 15 | 16 | - TOX_ENV=py3.4-django1.8 17 | - TOX_ENV=py3.4-django1.9 18 | 19 | - TOX_ENV=py3.5-django1.8 20 | - TOX_ENV=py3.5-django1.9 21 | 22 | install: 23 | - pip install -U 'tox==2.2.1' 'pip<8.0.0' 'virtualenv<14.0.0' 24 | 25 | script: 26 | - tox -e $TOX_ENV 27 | 28 | notifications: 29 | email: 30 | recipients: 31 | - swistakm@gmail.com 32 | on_success: always 33 | on_failure: always 34 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | # Original Authors 2 | 3 | Michał Jaworski 4 | 5 | 6 | # Contributors 7 | 8 | Matthew Wilkes 9 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright © 2013 Michał Jaworski All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright notice, this 6 | list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this 8 | list of conditions and the following disclaimer in the documentation and/or 9 | other materials provided with the distribution. 10 | * Neither the name of Michał Jaworski nor the names of its contributors may be used to 11 | endorse or promote products derived from this software without specific prior 12 | written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/swistakm/django-datapurge.png)](https://travis-ci.org/swistakm/django-datapurge) 2 | 3 | 4 | # Django Datapurge 5 | 6 | Simple app to help purging old data like sesions, nonces, logs etc.. It's 7 | like `django-admin.py clearsessions` command but gives you possibility to 8 | purge anything. Simpliest way to use `datapurge` is to run management 9 | command (manually or via cron): 10 | 11 | python manage.py purge 12 | 13 | It's easy to integrate datapurge with `celery` or `kronos`. Just wrap 14 | `datapurge.actions.purge` function with code corresponding to your task 15 | backend and run it the way you want. 16 | 17 | # Requirements 18 | 19 | * Python (2.6, 2.7, 3.4, 3.5) 20 | * Django>=1.3.7 21 | 22 | # Installation 23 | 24 | Install from PyPI using pip: 25 | 26 | pip install django-datapurge 27 | 28 | Or clone this repo: 29 | 30 | git clone git@github.com:swistakm/django-datapurge.git 31 | 32 | Add `'datapurge'` to your `INSTALLED_APPS` setting. 33 | 34 | INSTALLED_APPS = ( 35 | ... 36 | 'datapurge', 37 | ) 38 | 39 | # Configuration 40 | 41 | Add `DATAPURGE_MODELS` to your settings file and specify which models should be purged: 42 | 43 | DATAPURGE_MODELS = { 44 | 'app_name.ModelName1': { 45 | # policy settings 46 | ... 47 | }, 48 | 'app_name.ModelName2': { 49 | ... 50 | }, 51 | } 52 | 53 | # Available purge policies 54 | 55 | There are a few available policies for your use. Use what you find most convienient. Policy is 56 | guessed from set parameters provided. 57 | 58 | 59 | ## ExpireFieldPolicy 60 | 61 | Deletes all objects which `expire_field` datetime is older than `timezone.now()`. 62 | 63 | Parameters: 64 | 65 | * `'expire_field'` - name of datetime field holding expiration date 66 | 67 | Example: 68 | 69 | DATAPURGE_MODELS = { 70 | "sessions.Session": { 71 | "expire_field": "expire_date", 72 | } 73 | } 74 | 75 | ## LifetimePolicy 76 | 77 | Deletes all objects which are older than specified `lifetime` 78 | 79 | Parameters: 80 | 81 | * `'lifetime'` - timedelta object specifying maximum lifetime of object 82 | * `'created_field'` - name of datetime field holding object creation time 83 | 84 | Example: 85 | 86 | from timezone import timedelta 87 | 88 | DATAPURGE_MODELS = { 89 | "oauth_provider.Nonce": { 90 | "lifetime": timedelta(seconds=300), 91 | "created_field": "timestamp", 92 | } 93 | 94 | ## CallablePolicy 95 | 96 | Deletes all objects from query returned by provided callable 97 | 98 | Parameters: 99 | 100 | * `'callable'` - function accepting model class and returning QuerySet 101 | 102 | Example: 103 | 104 | DATAPURGE_MODELS = { 105 | "some_app.Log": { 106 | "callable": lambda model: model.objects.all(), 107 | } 108 | 109 | -------------------------------------------------------------------------------- /datapurge/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 0, 2,) 2 | __version__ = '.'.join(map(str, VERSION[0:3])) + ''.join(VERSION[3:]) -------------------------------------------------------------------------------- /datapurge/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import partial 3 | import django 4 | 5 | if django.VERSION >= (1,9): 6 | from django.apps import apps 7 | get_model = apps.get_model 8 | elif django.VERSION >= (1,7): 9 | from django.db.models import get_model 10 | get_model = get_model 11 | elif django.VERSION >= (1,4): 12 | from django.db.models import get_model 13 | get_model = partial(get_model, seed_cache=False, only_installed=True) 14 | else: 15 | from django.db.models import get_model 16 | get_model = partial(get_model, seed_cache=False) -------------------------------------------------------------------------------- /datapurge/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | class BaseDatapurgeException(BaseException): 3 | """Base datapurge app exception""" 4 | 5 | class AmbiguousSettingsError(BaseException): 6 | """ Raises when datapurge settings appear to be ambiguous 7 | """ -------------------------------------------------------------------------------- /datapurge/management/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /datapurge/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /datapurge/management/commands/purge.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from django.core.management.base import BaseCommand, CommandError 3 | from datapurge.exceptions import AmbiguousSettingsError 4 | from datapurge.purge.actions import purge 5 | 6 | class Command(BaseCommand): 7 | help = 'Purges old data' 8 | 9 | def handle(self, *args, **options): 10 | try: 11 | purge() 12 | except AmbiguousSettingsError, err: 13 | raise CommandError(str(err)) 14 | -------------------------------------------------------------------------------- /datapurge/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /datapurge/purge/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /datapurge/purge/actions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from datapurge import settings 5 | from datapurge.purge.tasks import PurgeTask 6 | 7 | def purge(now=None): 8 | # todo: timezone handling 9 | models = settings.DATAPURGE_MODELS 10 | now = now or datetime.datetime.utcnow() 11 | 12 | for model, conf in models.items(): 13 | task = PurgeTask.create_from_conf(model, conf, now) 14 | task() 15 | -------------------------------------------------------------------------------- /datapurge/purge/policies.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from datapurge.exceptions import AmbiguousSettingsError 5 | from datapurge.settings import DATAPURGE_GRACEFULLY 6 | 7 | class BasePurgePolicy(object): 8 | """Base purge policy for purging old data""" 9 | 10 | def __init__(self, *args, **kwargs): 11 | self.grecefully = DATAPURGE_GRACEFULLY 12 | self.model = None 13 | 14 | def apply_policy(self, model, now=None): 15 | """ Applies purge policy to specified model. 16 | 17 | todo: creating self-purging (or not) logs 18 | :param model: Model class 19 | """ 20 | # todo: timezone handling 21 | self.now = now or datetime.datetime.utcnow() 22 | self.model = model 23 | self.purge() 24 | 25 | def purge(self): 26 | """purges data gracefully or not 27 | 28 | todo: do it gracefully 29 | 30 | """ 31 | if not self.grecefully: 32 | self.get_queryset().delete() 33 | else: 34 | raise NotImplemented 35 | 36 | def get_queryset(self): 37 | """ Gets model queryset 38 | :return: queryset to delete 39 | """ 40 | raise NotImplemented 41 | 42 | 43 | class CallablePolicy(BasePurgePolicy): 44 | 45 | def __init__(self, callable): 46 | super(CallablePolicy, self).__init__(callable) 47 | self.callable = callable 48 | 49 | def get_queryset(self): 50 | return self.callable(self.model) 51 | 52 | 53 | class ExpireFieldPolicy(BasePurgePolicy): 54 | 55 | def __init__(self, expire_field): 56 | super(ExpireFieldPolicy, self).__init__(expire_field) 57 | self.expire_field = expire_field 58 | 59 | def get_queryset(self): 60 | query = {self.expire_field + '__lte': self.now} 61 | return self.model.objects.filter(**query) 62 | 63 | 64 | class LifetimePolicy(BasePurgePolicy): 65 | 66 | def __init__(self, lifetime, created_field): 67 | super(LifetimePolicy, self).__init__(lifetime, created_field) 68 | self.lifetime = lifetime 69 | self.created_field = created_field 70 | 71 | def get_queryset(self): 72 | query = {self.created_field + '__lte': self.now - self.lifetime} 73 | return self.model.objects.filter(**query) 74 | 75 | 76 | class DummyPolicy(BasePurgePolicy): 77 | def purge(self): 78 | pass 79 | 80 | 81 | # Map settings kwargs to policy classes, use frozensets cause are hashable 82 | POLICY_KWARGS_MAPPING = { 83 | frozenset(("callable",)): CallablePolicy, 84 | frozenset(("expire_field",)): ExpireFieldPolicy, 85 | frozenset(("lifetime", "created_field",)): LifetimePolicy, 86 | frozenset(): DummyPolicy, 87 | } 88 | 89 | 90 | def create_policy(**kwargs): 91 | """ Guess policy class by provided kwargs and create it's object 92 | """ 93 | parameters = kwargs.keys() 94 | try: 95 | PolicyClass = POLICY_KWARGS_MAPPING.get(frozenset(parameters)) 96 | return PolicyClass(**kwargs) 97 | except KeyError: 98 | raise AmbiguousSettingsError("Can not initialize any policy class with parameters %s" % kwargs) 99 | 100 | -------------------------------------------------------------------------------- /datapurge/purge/tasks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datapurge.purge.policies import DummyPolicy, create_policy 3 | from datapurge.compat import get_model 4 | 5 | class PurgeTask(object): 6 | def __init__(self, model, policy=None, now=None): 7 | """ 8 | :type policy: datapurge.purge.policies.BasePurgePolicy 9 | """ 10 | self.model = model 11 | self.now = now 12 | self.policy = policy or DummyPolicy() 13 | 14 | def __call__(self, *args, **kwargs): 15 | self.policy.apply_policy(self.model, self.now) 16 | 17 | @classmethod 18 | def create_from_conf(cls, model_relation, options, force_now=None): 19 | app_label, model_name = model_relation.split(".") 20 | model = get_model(app_label, model_name) 21 | 22 | policy = create_policy(**options) 23 | return cls(model, policy, force_now) 24 | -------------------------------------------------------------------------------- /datapurge/runtests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- -------------------------------------------------------------------------------- /datapurge/runtests/runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/ 4 | # http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/ 5 | # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py 6 | import os 7 | import sys 8 | 9 | # fix sys path so we don't need to setup PYTHONPATH 10 | sys.path.append(os.path.join(os.path.dirname(__file__), "../..")) 11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'datapurge.runtests.settings' 12 | 13 | import django 14 | from django.conf import settings 15 | from django.test.utils import get_runner 16 | 17 | 18 | def main(): 19 | try: 20 | django.setup() 21 | except AttributeError: 22 | # Old django doesn't need a setup call 23 | pass 24 | 25 | TestRunner = get_runner(settings) 26 | 27 | test_runner = TestRunner(verbosity=2) 28 | failures = test_runner.run_tests(['datapurge']) 29 | 30 | sys.exit(failures) 31 | 32 | if __name__ == '__main__': 33 | main() -------------------------------------------------------------------------------- /datapurge/runtests/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example project. 2 | 3 | DEBUG = True 4 | TEMPLATE_DEBUG = DEBUG 5 | 6 | ADMINS = ( 7 | # ('Your Name', 'your_email@example.com'), 8 | ) 9 | 10 | MANAGERS = ADMINS 11 | 12 | DATABASES = { 13 | 'default': { 14 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 15 | 'NAME': 'example.db', # Or path to database file if using sqlite3. 16 | 'USER': '', # Not used with sqlite3. 17 | 'PASSWORD': '', # Not used with sqlite3. 18 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 19 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 20 | } 21 | } 22 | 23 | # Local time zone for this installation. Choices can be found here: 24 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 25 | # although not all choices may be available on all operating systems. 26 | # In a Windows environment this must be set to your system time zone. 27 | TIME_ZONE = 'America/Chicago' 28 | 29 | # Language code for this installation. All choices can be found here: 30 | # http://www.i18nguy.com/unicode/language-identifiers.html 31 | LANGUAGE_CODE = 'en-us' 32 | 33 | SITE_ID = 1 34 | 35 | # If you set this to False, Django will make some optimizations so as not 36 | # to load the internationalization machinery. 37 | USE_I18N = True 38 | 39 | # If you set this to False, Django will not format dates, numbers and 40 | # calendars according to the current locale. 41 | USE_L10N = True 42 | 43 | # If you set this to False, Django will not use timezone-aware datetimes. 44 | USE_TZ = False 45 | 46 | # Absolute filesystem path to the directory that will hold user-uploaded files. 47 | # Example: "/home/media/media.lawrence.com/media/" 48 | MEDIA_ROOT = '' 49 | 50 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 51 | # trailing slash. 52 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 53 | MEDIA_URL = '' 54 | 55 | # Absolute path to the directory static files should be collected to. 56 | # Don't put anything in this directory yourself; store your static files 57 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 58 | # Example: "/home/media/media.lawrence.com/static/" 59 | STATIC_ROOT = '' 60 | 61 | # URL prefix for static files. 62 | # Example: "http://media.lawrence.com/static/" 63 | STATIC_URL = '/static/' 64 | 65 | # Additional locations of static files 66 | STATICFILES_DIRS = ( 67 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 68 | # Always use forward slashes, even on Windows. 69 | # Don't forget to use absolute paths, not relative paths. 70 | ) 71 | 72 | # List of finder classes that know how to find static files in 73 | # various locations. 74 | STATICFILES_FINDERS = ( 75 | 'django.contrib.staticfiles.finders.FileSystemFinder', 76 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 77 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 78 | ) 79 | 80 | # Make this unique, and don't share it with anybody. 81 | SECRET_KEY = 'ktmmr74&5=c%h!9c96_iuzhgsce!npq3a8uj$swa3-)v(ad%w#' 82 | 83 | # List of callables that know how to import templates from various sources. 84 | TEMPLATE_LOADERS = ( 85 | 'django.template.loaders.filesystem.Loader', 86 | 'django.template.loaders.app_directories.Loader', 87 | # 'django.template.loaders.eggs.Loader', 88 | ) 89 | 90 | MIDDLEWARE_CLASSES = ( 91 | 'django.middleware.common.CommonMiddleware', 92 | 'django.contrib.sessions.middleware.SessionMiddleware', 93 | 'django.middleware.csrf.CsrfViewMiddleware', 94 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 95 | 'django.contrib.messages.middleware.MessageMiddleware', 96 | # Uncomment the next line for simple clickjacking protection: 97 | # 'django.middleware.clickjacking.XFrameOptionsMiddleware', 98 | ) 99 | 100 | ROOT_URLCONF = 'example.urls' 101 | 102 | # Python dotted path to the WSGI application used by Django's runserver. 103 | WSGI_APPLICATION = 'example.wsgi.application' 104 | 105 | TEMPLATE_DIRS = ( 106 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 107 | # Always use forward slashes, even on Windows. 108 | # Don't forget to use absolute paths, not relative paths. 109 | ) 110 | 111 | INSTALLED_APPS = ( 112 | 'django.contrib.auth', 113 | 'django.contrib.contenttypes', 114 | 'django.contrib.sessions', 115 | 'django.contrib.sites', 116 | 'django.contrib.messages', 117 | 'django.contrib.staticfiles', 118 | 119 | #external 120 | 'datapurge', 121 | 122 | ) 123 | 124 | # A sample logging configuration. The only tangible logging 125 | # performed by this configuration is to send an email to 126 | # the site admins on every HTTP 500 error when DEBUG=False. 127 | # See http://docs.djangoproject.com/en/dev/topics/logging for 128 | # more details on how to customize your logging configuration. 129 | LOGGING = { 130 | 'version': 1, 131 | 'disable_existing_loggers': False, 132 | 'filters': { 133 | }, 134 | 'handlers': { 135 | 'mail_admins': { 136 | 'level': 'ERROR', 137 | 'class': 'django.utils.log.AdminEmailHandler' 138 | } 139 | }, 140 | 'loggers': { 141 | 'django.request': { 142 | 'handlers': ['mail_admins'], 143 | 'level': 'ERROR', 144 | 'propagate': True, 145 | }, 146 | } 147 | } -------------------------------------------------------------------------------- /datapurge/runtests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns 2 | 3 | urlpatterns = patterns('', 4 | ) 5 | -------------------------------------------------------------------------------- /datapurge/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | from django.conf import settings 4 | 5 | DATAPURGE_GRACEFULLY = getattr(settings, "DATAPURGE_GRACEFULLY", False) 6 | DATAPURGE_GRACE_STEP = getattr(settings, "DATAPURGE_GRACE_STEP", 100) 7 | DATAPURGE_GRACE_WAIT = getattr(settings, "DATAPURGE_GRACE_WAIT", datetime.timedelta(seconds=0.3)) 8 | 9 | DATAPURGE_MODELS = getattr(settings, "DATAPURGE_MODELS", {}) -------------------------------------------------------------------------------- /datapurge/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | import datetime 8 | 9 | from django.test import TestCase 10 | from django.contrib.auth.models import User 11 | from datapurge.purge.actions import purge 12 | from datapurge.purge.policies import CallablePolicy, ExpireFieldPolicy, LifetimePolicy 13 | from datapurge.purge.tasks import PurgeTask 14 | 15 | 16 | class TestWith100Users(TestCase): 17 | def setUp(self): 18 | dt = datetime.datetime.now() 19 | self.users = [] 20 | 21 | # we use User model for test because it's in contrib and has datetime field we want 22 | for number in range(100): 23 | new_user = User.objects.create( 24 | username="testUser%s" % number, 25 | date_joined=dt - datetime.timedelta(days=number)) 26 | self.users.append(new_user) 27 | 28 | #reverse to preserve chronological order 29 | self.users.reverse() 30 | 31 | self.assertEqual(User.objects.count(), 100) 32 | 33 | class PoliciesTest(TestWith100Users): 34 | def test_CallablePolicy(self): 35 | expire_date = self.users[59].date_joined 36 | callable = lambda model: model.objects.filter(date_joined__lte=expire_date) 37 | 38 | policy = CallablePolicy(callable=callable) 39 | policy.apply_policy(User) 40 | 41 | self.assertEqual(User.objects.count(), 40) 42 | 43 | def test_ExpireFieldPolicy(self): 44 | fake_now = self.users[59].date_joined 45 | 46 | policy = ExpireFieldPolicy(expire_field="date_joined") 47 | policy.apply_policy(model=User, now=fake_now) 48 | 49 | self.assertEqual(User.objects.count(), 40) 50 | 51 | def test_LifetimePolicy(self): 52 | lifetime = self.users[-1].date_joined - self.users[59].date_joined 53 | fake_now = self.users[-1].date_joined 54 | 55 | policy = LifetimePolicy(lifetime=lifetime, created_field="date_joined") 56 | policy.apply_policy(model=User, now=fake_now) 57 | 58 | self.assertEqual(User.objects.count(), 40) 59 | 60 | class PurgeTaskTest(TestWith100Users): 61 | def test_purge_by_expire_field(self): 62 | fake_now = self.users[59].date_joined 63 | 64 | relation = "auth.User" 65 | params = { 66 | "expire_field": "date_joined", 67 | } 68 | 69 | task = PurgeTask.create_from_conf(relation, params, fake_now) 70 | task() 71 | 72 | self.assertEqual(User.objects.count(), 40) 73 | 74 | def test_purge_by_lifetime_field(self): 75 | lifetime = self.users[-1].date_joined - self.users[59].date_joined 76 | fake_now = self.users[-1].date_joined 77 | 78 | relation = "auth.User" 79 | params = { 80 | "lifetime": lifetime, 81 | "created_field": "date_joined" 82 | } 83 | 84 | task = PurgeTask.create_from_conf(relation, params, fake_now) 85 | task() 86 | 87 | self.assertEqual(User.objects.count(), 40) 88 | 89 | def test_purge_with_callable(self): 90 | expire_date = self.users[59].date_joined 91 | 92 | relation = "auth.User" 93 | params = { 94 | "callable": lambda model: model.objects.filter(date_joined__lte=expire_date) 95 | } 96 | 97 | task = PurgeTask.create_from_conf(relation, params) 98 | task() 99 | 100 | self.assertEqual(User.objects.count(), 40) 101 | 102 | class PurgeActionTest(TestWith100Users): 103 | def test_purge_action(self): 104 | expire_date = self.users[59].date_joined 105 | 106 | # monkeypatch datapurge settings 107 | from datapurge import settings 108 | settings.DATAPURGE_MODELS = { 109 | "auth.User": { 110 | "callable": lambda model: model.objects.filter(date_joined__lte=expire_date) 111 | } 112 | } 113 | 114 | purge() 115 | 116 | self.assertEqual(User.objects.count(), 40) 117 | -------------------------------------------------------------------------------- /datapurge/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | from datapurge import __version__ as version 6 | 7 | # allow setup.py to be run from any path 8 | os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) 9 | 10 | try: 11 | from pypandoc import convert 12 | 13 | def read_md(f): 14 | return convert(f, 'rst') 15 | 16 | except ImportError: 17 | print( 18 | "warning: pypandoc module not found, could not convert Markdown to RST" 19 | ) 20 | 21 | def read_md(f): 22 | return open(f, 'r').read() # noqa 23 | 24 | 25 | setup( 26 | name='django-datapurge', 27 | version=version, 28 | packages=find_packages(), 29 | include_package_data=True, 30 | license='BSD License', 31 | description=( 32 | 'A simple Django app to easily handle ' 33 | 'cleanup of old data (sessions, nonces, etc.)' 34 | ), 35 | long_description=read_md('README.md'), 36 | url='https://github.com/swistakm/django-datapurge', 37 | author = 'Michał Jaworski', 38 | author_email = 'swistakm@gmail.com', 39 | classifiers = [ 40 | 'Environment :: Web Environment', 41 | 'Framework :: Django', 42 | 'Framework :: Django :: 1.4', 43 | 'Framework :: Django :: 1.5', 44 | 'Framework :: Django :: 1.6', 45 | 'Framework :: Django :: 1.7', 46 | 'Framework :: Django :: 1.8', 47 | 'Framework :: Django :: 1.9', 48 | 'Intended Audience :: Developers', 49 | 'License :: OSI Approved :: BSD License', 50 | 'Operating System :: OS Independent', 51 | 'Programming Language :: Python', 52 | 'Programming Language :: Python :: 2', 53 | 'Programming Language :: Python :: 3', 54 | 'Programming Language :: Python :: 2.6', 55 | 'Programming Language :: Python :: 2.7', 56 | 'Programming Language :: Python :: 3.4', 57 | 'Programming Language :: Python :: 3.5', 58 | 'Topic :: Internet :: WWW/HTTP', 59 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 60 | ], 61 | test_suite='datapurge.runtests.runtests.main', 62 | ) 63 | 64 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | downloadcache = {toxworkdir}/cache/ 3 | envlist = 4 | py3.5-django{1.8,1.9} 5 | py3.4-django{1.8,1.9} 6 | py2.7-django{1.3,1.4,1.5,1.6,1.7,1.8,1.9} 7 | py2.6-django{1.3,1.4,1.5} 8 | 9 | [testenv] 10 | commands={envpython} setup.py test 11 | deps = 12 | django1.3: django>=1.3,<1.4 13 | django1.4: django>=1.4,<1.5 14 | django1.5: django>=1.5,<1.6 15 | django1.6: django>=1.6,<1.7 16 | django1.7: django>=1.7,<1.8 17 | django1.8: django>=1.8,<1.9 18 | django1.9: django>=1.9,<1.10 19 | 20 | basepython = 21 | py3.5: python3.5 22 | py3.4: python3.4 23 | py2.7: python2.7 24 | py2.6: python2.6 25 | 26 | --------------------------------------------------------------------------------