Support for onetime snippets.

This commit is contained in:
Martin Mahner 2014-01-21 12:10:44 +01:00
parent 8a55139ae0
commit 88be3a6b1f
9 changed files with 129 additions and 12 deletions

View file

@ -28,7 +28,7 @@ class SnippetForm(forms.ModelForm):
choices=LEXER_LIST, choices=LEXER_LIST,
) )
expire_options = forms.ChoiceField( expires = forms.ChoiceField(
label=_(u'Expires'), label=_(u'Expires'),
choices=EXPIRE_CHOICES, choices=EXPIRE_CHOICES,
initial=EXPIRE_DEFAULT, initial=EXPIRE_DEFAULT,
@ -74,10 +74,18 @@ class SnippetForm(forms.ModelForm):
raise forms.ValidationError('This snippet was identified as Spam.') raise forms.ValidationError('This snippet was identified as Spam.')
return self.cleaned_data return self.cleaned_data
def clean_expire_options(self): def clean_expires(self):
expires = self.cleaned_data['expire_options'] expires = self.cleaned_data['expires']
if expires == u'never': if expires == u'never':
self.cleaned_data['expire_type'] = Snippet.EXPIRE_KEEP
return None 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 return expires
def save(self, parent=None, *args, **kwargs): 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 # Add expire datestamp. None indicates 'keep forever', use the default
# null state of the db column for that. # 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: if expires:
self.instance.expires = datetime.datetime.now() + \ self.instance.expires = datetime.datetime.now() + \
datetime.timedelta(seconds=int(expires)) datetime.timedelta(seconds=int(expires))

View file

@ -14,12 +14,13 @@ class Command(LabelCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
deleteable_snippets = Snippet.objects.filter( deleteable_snippets = Snippet.objects.filter(
expires__isnull=False, expires__isnull=False,
expire_type__in=[Snippet.EXPIRE_TIME, Snippet.EXPIRE_ONETIME],
expires__lte=datetime.datetime.now() expires__lte=datetime.datetime.now()
) )
sys.stdout.write(u"%s snippets gets deleted:\n" % deleteable_snippets.count()) sys.stdout.write(u"%s snippets gets deleted:\n" % deleteable_snippets.count())
for d in deleteable_snippets: for d in deleteable_snippets:
sys.stdout.write(u"- %s (%s)\n" % (d.secret_id, d.expires)) sys.stdout.write(u"- %s (%s)\n" % (d.secret_id, d.expires))
if options.get('dry_run'): 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: else:
deleteable_snippets.delete() deleteable_snippets.delete()

View file

@ -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']

View file

@ -1,4 +1,3 @@
from datetime import datetime
from random import SystemRandom from random import SystemRandom
from django.db import models from django.db import models
@ -14,15 +13,29 @@ L = getattr(settings, 'DPASTE_SLUG_LENGTH', 4)
T = getattr(settings, 'DPASTE_SLUG_CHOICES', T = getattr(settings, 'DPASTE_SLUG_CHOICES',
'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ1234567890') 'abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ1234567890')
ONETIME_LIMIT = getattr(settings, 'DPASTE_ONETIME_LIMIT', 2)
def generate_secret_id(length=L, alphabet=T): def generate_secret_id(length=L, alphabet=T):
return ''.join([R.choice(alphabet) for i in range(length)]) return ''.join([R.choice(alphabet) for i in range(length)])
class Snippet(models.Model): 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) secret_id = models.CharField(_(u'Secret ID'), max_length=255, blank=True, null=True)
content = models.TextField(_(u'Content')) content = models.TextField(_(u'Content'))
lexer = models.CharField(_(u'Lexer'), max_length=30, default=LEXER_DEFAULT) lexer = models.CharField(_(u'Lexer'), max_length=30, default=LEXER_DEFAULT)
published = models.DateTimeField(_(u'Published'), auto_now_add=True) 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) 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') parent = models.ForeignKey('self', null=True, blank=True, related_name='children')
class Meta: class Meta:
@ -32,6 +45,13 @@ class Snippet(models.Model):
def get_linecount(self): def get_linecount(self):
return len(self.content.splitlines()) 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 @property
def is_single(self): def is_single(self):
return self.is_root_node() and not self.get_children() return self.is_root_node() and not self.get_children()

View file

@ -12,6 +12,13 @@ tt strong {
outline: 3px solid #ffe699; outline: 3px solid #ffe699;
} }
.message {
margin: 0;
padding: 10px 30px;
background-color: gold;
text-shadow: 0 1px 0 #ffea90;
}
/* Custom container */ /* Custom container */
.container-fluid { .container-fluid {
margin: 0 auto; margin: 0 auto;

View file

@ -51,6 +51,9 @@
======================================================================= --> ======================================================================= -->
<div class="btn-group snippet-options"> <div class="btn-group snippet-options">
<button disabled class="btn">Expires in: {{ snippet.expires|timeuntil }}</button> <button disabled class="btn">Expires in: {{ snippet.expires|timeuntil }}</button>
<span class="btn disabled">
{% blocktrans count counter=snippet.view_count %}{{ counter }} View{% plural %}{{ counter }} Views{% endblocktrans %}
</span>
{% if snippet.pk|in_list:request.session.snippet_list %} {% if snippet.pk|in_list:request.session.snippet_list %}
<a class="btn" href="{% url "snippet_delete" snippet.secret_id %}" onclick="return confirm('{% trans "Really delete this snippet?" %}');"><i class="icon-trash"></i> {% trans "Delete Now" %}</a> <a class="btn" href="{% url "snippet_delete" snippet.secret_id %}" onclick="return confirm('{% trans "Really delete this snippet?" %}');"><i class="icon-trash"></i> {% trans "Delete Now" %}</a>
{% endif %} {% endif %}
@ -62,6 +65,19 @@
rel="nofollow" title="Create a secret Gist"><i class="icon-share"></i> {% trans "Gist" %}</a> rel="nofollow" title="Create a secret Gist"><i class="icon-share"></i> {% trans "Gist" %}</a>
</div> </div>
{% if snippet.expire_type == 3 %}
<p class="message">
{% 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 %}
</p>
{% endif %}
<!-- ====================================================================== <!-- ======================================================================
Snippet Snippet
======================================================================= --> ======================================================================= -->

View file

@ -27,10 +27,10 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="form-options-expire"> <div class="form-options-expire">
{{ snippet_form.expire_options.errors }} {{ snippet_form.expires.errors }}
<div class="input-prepend"> <div class="input-prepend">
<span class="add-on"><i class="icon-trash" title="{% trans "Expire in" %}"></i></span> <span class="add-on"><i class="icon-trash" title="{% trans "Expire in" %}"></i></span>
{{ snippet_form.expire_options }} {{ snippet_form.expires }}
</div> </div>
</div> </div>
</div> </div>

View file

@ -23,7 +23,7 @@ class SnippetTestCase(TestCase):
return { return {
'content': u"Hello Wörld.\n\tGood Bye", 'content': u"Hello Wörld.\n\tGood Bye",
'lexer': LEXER_DEFAULT, 'lexer': LEXER_DEFAULT,
'expire_options': EXPIRE_DEFAULT, 'expires': EXPIRE_DEFAULT,
} }

View file

@ -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 django.views.decorators.csrf import csrf_exempt
from dpaste.forms import SnippetForm 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_WORDWRAP, LEXER_LIST
from dpaste.highlight import LEXER_DEFAULT, LEXER_KEYS 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) 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 = snippet.get_root()
tree = tree.get_descendants(include_self=True) 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": 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(): if snippet_form.is_valid():
new_snippet = snippet_form.save(parent=snippet) new_snippet = snippet_form.save(parent=snippet)
url = new_snippet.get_absolute_url() url = new_snippet.get_absolute_url()
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
else: else:
snippet_form = SnippetForm(initial=new_snippet_initial, request=request) snippet_form = SnippetForm(
initial=new_snippet_initial,
request=request)
template_context = { template_context = {
'snippet_form': snippet_form, 'snippet_form': snippet_form,