From 88be3a6b1f2bb0eff529cbc8f598ffdbbd49e40e Mon Sep 17 00:00:00 2001 From: Martin Mahner Date: Tue, 21 Jan 2014 12:10:44 +0100 Subject: [PATCH] Support for onetime snippets. --- dpaste/forms.py | 18 +++++-- .../management/commands/cleanup_snippets.py | 3 +- ...pire_type__add_field_snippet_view_count.py | 49 +++++++++++++++++++ dpaste/models.py | 22 ++++++++- dpaste/static/dpaste/theme.css | 7 +++ dpaste/templates/dpaste/snippet_details.html | 16 ++++++ dpaste/templates/dpaste/snippet_form.html | 4 +- dpaste/tests/test_snippet.py | 2 +- dpaste/views.py | 20 ++++++-- 9 files changed, 129 insertions(+), 12 deletions(-) create mode 100644 dpaste/migrations/0005_auto__add_field_snippet_expire_type__add_field_snippet_view_count.py diff --git a/dpaste/forms.py b/dpaste/forms.py index d7d4462..1ad5429 100644 --- a/dpaste/forms.py +++ b/dpaste/forms.py @@ -28,7 +28,7 @@ class SnippetForm(forms.ModelForm): choices=LEXER_LIST, ) - expire_options = forms.ChoiceField( + expires = forms.ChoiceField( label=_(u'Expires'), choices=EXPIRE_CHOICES, initial=EXPIRE_DEFAULT, @@ -74,10 +74,18 @@ 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'] + def clean_expires(self): + expires = self.cleaned_data['expires'] + if expires == u'never': + self.cleaned_data['expire_type'] = Snippet.EXPIRE_KEEP return None + + if expires == u'onetime': + self.cleaned_data['expire_type'] = Snippet.EXPIRE_ONETIME + return None + + self.cleaned_data['expire_type'] = Snippet.EXPIRE_TIME return expires def save(self, parent=None, *args, **kwargs): @@ -89,7 +97,9 @@ class SnippetForm(forms.ModelForm): # Add expire datestamp. None indicates 'keep forever', use the default # null state of the db column for that. - expires = self.cleaned_data['expire_options'] + self.instance.expire_type = self.cleaned_data['expire_type'] + + expires = self.cleaned_data['expires'] if expires: self.instance.expires = datetime.datetime.now() + \ datetime.timedelta(seconds=int(expires)) diff --git a/dpaste/management/commands/cleanup_snippets.py b/dpaste/management/commands/cleanup_snippets.py index 62d5c2f..59993f8 100644 --- a/dpaste/management/commands/cleanup_snippets.py +++ b/dpaste/management/commands/cleanup_snippets.py @@ -14,12 +14,13 @@ class Command(LabelCommand): def handle(self, *args, **options): deleteable_snippets = Snippet.objects.filter( expires__isnull=False, + expire_type__in=[Snippet.EXPIRE_TIME, Snippet.EXPIRE_ONETIME], 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)) if options.get('dry_run'): - sys.stdout.write(u'Dry run - Doing nothing! *crossingfingers*\n') + sys.stdout.write(u'Dry run - Not actually deleting snippets!\n') else: deleteable_snippets.delete() diff --git a/dpaste/migrations/0005_auto__add_field_snippet_expire_type__add_field_snippet_view_count.py b/dpaste/migrations/0005_auto__add_field_snippet_expire_type__add_field_snippet_view_count.py new file mode 100644 index 0000000..2a5e275 --- /dev/null +++ b/dpaste/migrations/0005_auto__add_field_snippet_expire_type__add_field_snippet_view_count.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): + # Adding field 'Snippet.expire_type' + db.add_column('dpaste_snippet', 'expire_type', + self.gf('django.db.models.fields.PositiveSmallIntegerField')(default=1), + keep_default=False) + + # Adding field 'Snippet.view_count' + db.add_column('dpaste_snippet', 'view_count', + self.gf('django.db.models.fields.PositiveIntegerField')(default=0), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Snippet.expire_type' + db.delete_column('dpaste_snippet', 'expire_type') + + # Deleting field 'Snippet.view_count' + db.delete_column('dpaste_snippet', 'view_count') + + + models = { + u'dpaste.snippet': { + 'Meta': {'ordering': "('-published',)", 'object_name': 'Snippet'}, + 'content': ('django.db.models.fields.TextField', [], {}), + 'expire_type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '1'}), + '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'}), + 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}) + } + } + + complete_apps = ['dpaste'] \ No newline at end of file diff --git a/dpaste/models.py b/dpaste/models.py index aeb000c..78ea56f 100644 --- a/dpaste/models.py +++ b/dpaste/models.py @@ -1,4 +1,3 @@ -from datetime import datetime from random import SystemRandom from django.db import models @@ -14,15 +13,29 @@ L = getattr(settings, 'DPASTE_SLUG_LENGTH', 4) T = getattr(settings, 'DPASTE_SLUG_CHOICES', 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ1234567890') +ONETIME_LIMIT = getattr(settings, 'DPASTE_ONETIME_LIMIT', 2) + def generate_secret_id(length=L, alphabet=T): return ''.join([R.choice(alphabet) for i in range(length)]) class Snippet(models.Model): + EXPIRE_TIME = 1 + EXPIRE_KEEP = 2 + EXPIRE_ONETIME = 3 + EXPIRE_CHOICES = ( + (EXPIRE_TIME, _(u'Expire by timestamp')), + (EXPIRE_KEEP, _(u'Keep Forever')), + (EXPIRE_ONETIME, _(u'One time snippet')), + ) + 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'), auto_now_add=True) + expire_type = models.PositiveSmallIntegerField(_(u'Expire Type'), + choices=EXPIRE_CHOICES, default=EXPIRE_CHOICES[0][0]) expires = models.DateTimeField(_(u'Expires'), blank=True, null=True) + view_count = models.PositiveIntegerField(_('View count'), default=0) parent = models.ForeignKey('self', null=True, blank=True, related_name='children') class Meta: @@ -32,6 +45,13 @@ class Snippet(models.Model): def get_linecount(self): return len(self.content.splitlines()) + @property + def remaining_views(self): + if self.expire_type == self.EXPIRE_ONETIME: + remaining = ONETIME_LIMIT - self.view_count + return remaining > 0 and remaining or 0 + return None + @property def is_single(self): return self.is_root_node() and not self.get_children() diff --git a/dpaste/static/dpaste/theme.css b/dpaste/static/dpaste/theme.css index 86c93ab..8138854 100644 --- a/dpaste/static/dpaste/theme.css +++ b/dpaste/static/dpaste/theme.css @@ -12,6 +12,13 @@ tt strong { outline: 3px solid #ffe699; } +.message { + margin: 0; + padding: 10px 30px; + background-color: gold; + text-shadow: 0 1px 0 #ffea90; +} + /* Custom container */ .container-fluid { margin: 0 auto; diff --git a/dpaste/templates/dpaste/snippet_details.html b/dpaste/templates/dpaste/snippet_details.html index 73dc293..1393f3b 100644 --- a/dpaste/templates/dpaste/snippet_details.html +++ b/dpaste/templates/dpaste/snippet_details.html @@ -51,6 +51,9 @@ ======================================================================= -->
+ + {% blocktrans count counter=snippet.view_count %}{{ counter }} View{% plural %}{{ counter }} Views{% endblocktrans %} + {% if snippet.pk|in_list:request.session.snippet_list %} {% trans "Delete Now" %} {% endif %} @@ -62,6 +65,19 @@ rel="nofollow" title="Create a secret Gist"> {% trans "Gist" %}
+ {% if snippet.expire_type == 3 %} +

+ {% trans "This is a one-time snippet." %} + {% if snippet.remaining_views > 1 %} + {% trans "It will automatically get deleted after {{ remaining }} further views." %} + {% elif snippet.remaining_views == 1 %} + {% trans "It will automatically get deleted after the next view." %} + {% else %} + {% trans "It was automatically deleted and cannot be viewed again." %} + {% endif %} +

+ {% endif %} + diff --git a/dpaste/templates/dpaste/snippet_form.html b/dpaste/templates/dpaste/snippet_form.html index 2600d1e..1c2366f 100644 --- a/dpaste/templates/dpaste/snippet_form.html +++ b/dpaste/templates/dpaste/snippet_form.html @@ -27,10 +27,10 @@ {% endfor %}
- {{ snippet_form.expire_options.errors }} + {{ snippet_form.expires.errors }}
- {{ snippet_form.expire_options }} + {{ snippet_form.expires }}
diff --git a/dpaste/tests/test_snippet.py b/dpaste/tests/test_snippet.py index acd1441..57e1840 100644 --- a/dpaste/tests/test_snippet.py +++ b/dpaste/tests/test_snippet.py @@ -23,7 +23,7 @@ class SnippetTestCase(TestCase): return { 'content': u"Hello Wörld.\n\tGood Bye", 'lexer': LEXER_DEFAULT, - 'expire_options': EXPIRE_DEFAULT, + 'expires': EXPIRE_DEFAULT, } diff --git a/dpaste/views.py b/dpaste/views.py index 6c86d7a..8377578 100644 --- a/dpaste/views.py +++ b/dpaste/views.py @@ -17,7 +17,7 @@ from django.views.defaults import (page_not_found as django_page_not_found, from django.views.decorators.csrf import csrf_exempt from dpaste.forms import SnippetForm -from dpaste.models import Snippet +from dpaste.models import Snippet, ONETIME_LIMIT from dpaste.highlight import LEXER_WORDWRAP, LEXER_LIST from dpaste.highlight import LEXER_DEFAULT, LEXER_KEYS @@ -58,6 +58,15 @@ def snippet_details(request, snippet_id, template_name='dpaste/snippet_details.h """ snippet = get_object_or_404(Snippet, secret_id=snippet_id) + # One time snippet get deleted if the view count matches our limit + if snippet.view_count >= ONETIME_LIMIT: + snippet.delete() + raise Http404() + + # Increase the view count of the snippet + snippet.view_count += 1 + snippet.save() + tree = snippet.get_root() tree = tree.get_descendants(include_self=True) @@ -67,13 +76,18 @@ def snippet_details(request, snippet_id, template_name='dpaste/snippet_details.h } if request.method == "POST": - snippet_form = SnippetForm(data=request.POST, request=request, initial=new_snippet_initial) + snippet_form = SnippetForm( + data=request.POST, + request=request, + initial=new_snippet_initial) if snippet_form.is_valid(): new_snippet = snippet_form.save(parent=snippet) url = new_snippet.get_absolute_url() return HttpResponseRedirect(url) else: - snippet_form = SnippetForm(initial=new_snippet_initial, request=request) + snippet_form = SnippetForm( + initial=new_snippet_initial, + request=request) template_context = { 'snippet_form': snippet_form,