From 964e1b64c75018394198bace5e31827698a25c1d Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Sat, 11 Jan 2014 16:00:11 +0100 Subject: [PATCH] Added option to keep snippets forever. --- CHANGELOG | 5 ++ docs/index.rst | 2 +- docs/integration.rst | 3 ++ docs/settings.rst | 9 ++++ dpaste/forms.py | 16 ++++-- .../management/commands/cleanup_snippets.py | 5 +- ...__chg_field_snippet_secret_id__chg_fiel.py | 49 +++++++++++++++++++ dpaste/models.py | 11 ++--- dpaste/tests/test_snippet.py | 16 ++++++ requirements.txt | 6 +++ 10 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 dpaste/migrations/0004_auto__chg_field_snippet_expires__chg_field_snippet_secret_id__chg_fiel.py diff --git a/CHANGELOG b/CHANGELOG index a2a733f..72c2156 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ Changelog ========= +2.4 (dev) +---------------- + +* Added an option to keep snippets forever + 2.3 (2014-01-07) ---------------- diff --git a/docs/index.rst b/docs/index.rst index e2801e5..8074ebd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,7 +9,7 @@ possible to be installed into an existing Django project like a regular app. Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 testing integration diff --git a/docs/integration.rst b/docs/integration.rst index 160f69a..acaa4b6 100644 --- a/docs/integration.rst +++ b/docs/integration.rst @@ -35,6 +35,9 @@ Finally just ``syncdb`` or if you use South, migrate:: manage.py migrate dpaste +Purge expired snippets +====================== + Do not forget to setup a cron job to purge expired snippets. You need to run the management command ``cleanup_snippets``. A cron job I use looks like:: diff --git a/docs/settings.rst b/docs/settings.rst index 7593a03..d3eb319 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -55,6 +55,15 @@ behavior without touching the code: (3600 * 24 * 30 * 12 * 100, ugettext(u'100 Years')), ) + You can keep snippets forever when you set the choice key to ``never``. + The management command will ignore these snippets:: + + ugettext = lambda s: s + DPASTE_EXPIRE_CHOICES = ( + (3600, ugettext(u'In one hour')), + (u'never', ugettext(u'Never')), + ) + ``DPASTE_EXPIRE_DEFAULT`` The key of the default value of ``DPASTE_EXPIRE_CHOICES``. Default: ``3600 * 24 * 30 * 12 * 100`` or simpler: ``DPASTE_EXPIRE_CHOICES[2][0]``. diff --git a/dpaste/forms.py b/dpaste/forms.py index 1849cec..d7d4462 100644 --- a/dpaste/forms.py +++ b/dpaste/forms.py @@ -61,7 +61,6 @@ class SnippetForm(forms.ModelForm): if 'l' in request.GET and request.GET['l'] in LEXER_KEYS: self.fields['lexer'].initial = request.GET['l'] - def clean_content(self): content = self.cleaned_data.get('content', '') if content.strip() == '': @@ -75,6 +74,12 @@ class SnippetForm(forms.ModelForm): raise forms.ValidationError('This snippet was identified as Spam.') return self.cleaned_data + def clean_expire_options(self): + expires = self.cleaned_data['expire_options'] + if expires == u'never': + return None + return expires + def save(self, parent=None, *args, **kwargs): MAX_SNIPPETS_PER_USER = getattr(settings, 'DPASTE_MAX_SNIPPETS_PER_USER', 15) @@ -82,9 +87,12 @@ class SnippetForm(forms.ModelForm): if parent: self.instance.parent = parent - # Add expire datestamp - self.instance.expires = datetime.datetime.now() + \ - datetime.timedelta(seconds=int(self.cleaned_data['expire_options'])) + # Add expire datestamp. None indicates 'keep forever', use the default + # null state of the db column for that. + expires = self.cleaned_data['expire_options'] + if expires: + self.instance.expires = datetime.datetime.now() + \ + datetime.timedelta(seconds=int(expires)) # Save snippet in the db super(SnippetForm, self).save(*args, **kwargs) diff --git a/dpaste/management/commands/cleanup_snippets.py b/dpaste/management/commands/cleanup_snippets.py index e8938a0..62d5c2f 100644 --- a/dpaste/management/commands/cleanup_snippets.py +++ b/dpaste/management/commands/cleanup_snippets.py @@ -12,7 +12,10 @@ class Command(LabelCommand): help = "Purges snippets that are expired" def handle(self, *args, **options): - deleteable_snippets = Snippet.objects.filter(expires__lte=datetime.datetime.now()) + deleteable_snippets = Snippet.objects.filter( + expires__isnull=False, + expires__lte=datetime.datetime.now() + ) sys.stdout.write(u"%s snippets gets deleted:\n" % deleteable_snippets.count()) for d in deleteable_snippets: sys.stdout.write(u"- %s (%s)\n" % (d.secret_id, d.expires)) diff --git a/dpaste/migrations/0004_auto__chg_field_snippet_expires__chg_field_snippet_secret_id__chg_fiel.py b/dpaste/migrations/0004_auto__chg_field_snippet_expires__chg_field_snippet_secret_id__chg_fiel.py new file mode 100644 index 0000000..b73cd01 --- /dev/null +++ b/dpaste/migrations/0004_auto__chg_field_snippet_expires__chg_field_snippet_secret_id__chg_fiel.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'Snippet.expires' + db.alter_column('dpaste_snippet', 'expires', self.gf('django.db.models.fields.DateTimeField')(null=True)) + + # Changing field 'Snippet.secret_id' + db.alter_column('dpaste_snippet', 'secret_id', self.gf('django.db.models.fields.CharField')(max_length=255, null=True)) + + # Changing field 'Snippet.published' + db.alter_column('dpaste_snippet', 'published', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True)) + + def backwards(self, orm): + + # Changing field 'Snippet.expires' + db.alter_column('dpaste_snippet', 'expires', self.gf('django.db.models.fields.DateTimeField')(default=None)) + + # Changing field 'Snippet.secret_id' + db.alter_column('dpaste_snippet', 'secret_id', self.gf('django.db.models.fields.CharField')(default='', max_length=255)) + + # Changing field 'Snippet.published' + db.alter_column('dpaste_snippet', 'published', self.gf('django.db.models.fields.DateTimeField')()) + + models = { + u'dpaste.snippet': { + 'Meta': {'ordering': "('-published',)", 'object_name': 'Snippet'}, + 'content': ('django.db.models.fields.TextField', [], {}), + 'expires': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + u'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'lexer': ('django.db.models.fields.CharField', [], {'default': "'python'", 'max_length': '30'}), + u'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['dpaste.Snippet']"}), + 'published': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}), + 'secret_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['dpaste'] \ No newline at end of file diff --git a/dpaste/models.py b/dpaste/models.py index 8532780..aeb000c 100644 --- a/dpaste/models.py +++ b/dpaste/models.py @@ -18,11 +18,11 @@ def generate_secret_id(length=L, alphabet=T): return ''.join([R.choice(alphabet) for i in range(length)]) class Snippet(models.Model): - secret_id = models.CharField(_(u'Secret ID'), max_length=255, blank=True) - content = models.TextField(_(u'Content'), ) + secret_id = models.CharField(_(u'Secret ID'), max_length=255, blank=True, null=True) + content = models.TextField(_(u'Content')) lexer = models.CharField(_(u'Lexer'), max_length=30, default=LEXER_DEFAULT) - published = models.DateTimeField(_(u'Published'), blank=True) - expires = models.DateTimeField(_(u'Expires'), blank=True) + published = models.DateTimeField(_(u'Published'), auto_now_add=True) + expires = models.DateTimeField(_(u'Expires'), blank=True, null=True) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class Meta: @@ -37,8 +37,7 @@ class Snippet(models.Model): return self.is_root_node() and not self.get_children() def save(self, *args, **kwargs): - if not self.pk: - self.published = datetime.now() + if not self.secret_id: self.secret_id = generate_secret_id() super(Snippet, self).save(*args, **kwargs) diff --git a/dpaste/tests/test_snippet.py b/dpaste/tests/test_snippet.py index 30d401c..acd1441 100644 --- a/dpaste/tests/test_snippet.py +++ b/dpaste/tests/test_snippet.py @@ -310,6 +310,22 @@ class SnippetTestCase(TestCase): management.call_command('cleanup_snippets') self.assertEqual(Snippet.objects.count(), 1) + def test_delete_management_snippet_that_never_expires_will_not_get_deleted(self): + """ + Snippets without an expiration date wont get deleted automatically. + """ + data = self.valid_form_data() + self.client.post(self.new_url, data, follow=True) + + self.assertEqual(Snippet.objects.count(), 1) + + s = Snippet.objects.all()[0] + s.expires = None + s.save() + + management.call_command('cleanup_snippets') + self.assertEqual(Snippet.objects.count(), 1) + def test_highlighting(self): # You can pass any lexer to the pygmentize function and it will # never fail loudly. diff --git a/requirements.txt b/requirements.txt index 1dd5c02..c571d69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ +# ----------------------------------------------------------------------------- +# These requirements are only required for local testing or development. +# To use dpaste it's enough to install the package, all, and only the +# necessary dependencies are installed automatically. +# ----------------------------------------------------------------------------- + # Project dependencies django==1.6.1 django-mptt==0.6.0