This commit is contained in:
Julien Palard 2020-05-31 15:33:59 +02:00
parent f1a87fb77b
commit a539d9096f
12 changed files with 219 additions and 130 deletions

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from .models import Paste from .models import Paste
@admin.register(Paste) @admin.register(Paste)
class PasteAdmin(admin.ModelAdmin): class PasteAdmin(admin.ModelAdmin):
list_display = ('paste_time', 'slug', 'title', 'viewcount') list_display = ("paste_time", "slug", "title", "viewcount")

View File

@ -3,8 +3,9 @@ from django.conf import settings
def app_details(request): def app_details(request):
"""Passes settings details to the templates.""" """Passes settings details to the templates."""
return {'APP_NAME': settings.APP_NAME, return {
'APP_VERSION': settings.APP_VERSION, "APP_NAME": settings.APP_NAME,
'DISPLAY_NAME': settings.DISPLAY_NAME, "APP_VERSION": settings.APP_VERSION,
'PASTE': settings.PASTE, "DISPLAY_NAME": settings.DISPLAY_NAME,
"PASTE": settings.PASTE,
} }

View File

@ -6,23 +6,31 @@ from .models import Paste, Language, EXPIRE_CHOICES
class PasteForm(ModelForm): class PasteForm(ModelForm):
"""Paste model form.""" """Paste model form."""
content = CharField(max_length=settings.PASTE['max_characters'],
strip=False, content = CharField(
widget=forms.Textarea) max_length=settings.PASTE["max_characters"], strip=False, widget=forms.Textarea
)
class Meta: class Meta:
model = Paste model = Paste
fields = ['language', 'title', 'password', 'content', 'lifetime', fields = [
'lifecount', 'private'] "language",
"title",
"password",
"content",
"lifetime",
"lifecount",
"private",
]
def save(self, commit=True): def save(self, commit=True):
"""Overwrites save method.""" """Overwrites save method."""
paste = super(PasteForm, self).save(commit=False) paste = super(PasteForm, self).save(commit=False)
paste.compute_size() paste.compute_size()
if not self.cleaned_data['title']: if not self.cleaned_data["title"]:
paste.title = 'no title' paste.title = "no title"
if self.cleaned_data['password']: if self.cleaned_data["password"]:
paste.set_password(self.cleaned_data['password']) paste.set_password(self.cleaned_data["password"])
if commit: if commit:
paste.save() paste.save()
return paste return paste

View File

@ -1,16 +1,18 @@
from django.contrib.sessions.middleware import SessionMiddleware as DjangoSessionMiddleware from django.contrib.sessions.middleware import (
SessionMiddleware as DjangoSessionMiddleware,
)
class SessionMiddleware(DjangoSessionMiddleware): class SessionMiddleware(DjangoSessionMiddleware):
def process_request(self, request): def process_request(self, request):
if 'admin' in request.path: if "admin" in request.path:
session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME) session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
request.session = self.SessionStore(session_key) request.session = self.SessionStore(session_key)
else: else:
request.session = {} request.session = {}
def process_response(self, request, response): def process_response(self, request, response):
if 'admin' in request.path: if "admin" in request.path:
return super().process_response(request, response) return super().process_response(request, response)
else: else:
return response return response

View File

@ -10,37 +10,81 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Language', name="Language",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('name', models.CharField(max_length=200, unique=True)), "id",
('slug', models.SlugField(max_length=200, unique=True)), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=200, unique=True)),
("slug", models.SlugField(max_length=200, unique=True)),
], ],
), ),
migrations.CreateModel( migrations.CreateModel(
name='Paste', name="Paste",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('slug', models.SlugField(editable=False, unique=True)), "id",
('title', models.CharField(blank=True, max_length=200)), models.AutoField(
('content', models.TextField(validators=[django.core.validators.MaxLengthValidator(100000)])), auto_created=True,
('size', models.IntegerField(default=0, editable=False)), primary_key=True,
('paste_time', models.DateTimeField(default=datetime.datetime.now, editable=False)), serialize=False,
('paste_ip', models.GenericIPAddressField(editable=False)), verbose_name="ID",
('paste_agent', models.CharField(editable=False, max_length=200)), ),
('lifetime', models.IntegerField(choices=[(0, 'Never expire'), (5, '5 minutes'), (30, '30 minutes'), (60, '1 hour'), (1440, '1 day'), (10080, '1 week')], default=0)), ),
('lifecount', models.IntegerField(default=0)), ("slug", models.SlugField(editable=False, unique=True)),
('viewcount', models.IntegerField(default=0, editable=False)), ("title", models.CharField(blank=True, max_length=200)),
('expired', models.BooleanField(default=False, editable=False)), (
('private', models.BooleanField(default=False)), "content",
('password', models.CharField(blank=True, max_length=128)), models.TextField(
('salt', models.CharField(blank=True, max_length=36)), validators=[django.core.validators.MaxLengthValidator(100000)]
('language', models.ForeignKey(default=13, null=True, on_delete=django.db.models.deletion.SET_NULL, to='paste.Language')), ),
),
("size", models.IntegerField(default=0, editable=False)),
(
"paste_time",
models.DateTimeField(default=datetime.datetime.now, editable=False),
),
("paste_ip", models.GenericIPAddressField(editable=False)),
("paste_agent", models.CharField(editable=False, max_length=200)),
(
"lifetime",
models.IntegerField(
choices=[
(0, "Never expire"),
(5, "5 minutes"),
(30, "30 minutes"),
(60, "1 hour"),
(1440, "1 day"),
(10080, "1 week"),
],
default=0,
),
),
("lifecount", models.IntegerField(default=0)),
("viewcount", models.IntegerField(default=0, editable=False)),
("expired", models.BooleanField(default=False, editable=False)),
("private", models.BooleanField(default=False)),
("password", models.CharField(blank=True, max_length=128)),
("salt", models.CharField(blank=True, max_length=36)),
(
"language",
models.ForeignKey(
default=13,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="paste.Language",
),
),
], ],
), ),
] ]

View File

@ -7,31 +7,40 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('paste', '0001_initial'), ("paste", "0001_initial"),
] ]
operations = [ operations = [
migrations.RemoveField( migrations.RemoveField(model_name="paste", name="paste_agent",),
model_name='paste', migrations.RemoveField(model_name="paste", name="paste_ip",),
name='paste_agent', migrations.AlterField(
), model_name="paste",
migrations.RemoveField( name="language",
model_name='paste', field=models.ForeignKey(
name='paste_ip', default=14,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="paste.Language",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='paste', model_name="paste",
name='language', name="lifecount",
field=models.ForeignKey(default=14, null=True, on_delete=django.db.models.deletion.SET_NULL, to='paste.Language'),
),
migrations.AlterField(
model_name='paste',
name='lifecount',
field=models.IntegerField(blank=True, default=0), field=models.IntegerField(blank=True, default=0),
), ),
migrations.AlterField( migrations.AlterField(
model_name='paste', model_name="paste",
name='lifetime', name="lifetime",
field=models.IntegerField(choices=[(0, 'Never expire'), (60, '1 hour'), (1440, '1 day'), (10080, '1 week'), (302400, '1 month'), (3679200, '1 year')], default=3679200), field=models.IntegerField(
choices=[
(0, "Never expire"),
(60, "1 hour"),
(1440, "1 day"),
(10080, "1 week"),
(302400, "1 month"),
(3679200, "1 year"),
],
default=3679200,
),
), ),
] ]

View File

@ -9,17 +9,18 @@ import uuid
EXPIRE_CHOICES = ( EXPIRE_CHOICES = (
(0, _('Never expire')), (0, _("Never expire")),
(60, _('1 hour')), (60, _("1 hour")),
(60 * 24, _('1 day')), (60 * 24, _("1 day")),
(60 * 24 * 7, _('1 week')), (60 * 24 * 7, _("1 week")),
(60 * 24 * 7 * 30, _('1 month')), (60 * 24 * 7 * 30, _("1 month")),
(60 * 24 * 7 * 365, _('1 year')), (60 * 24 * 7 * 365, _("1 year")),
) )
class Language(models.Model): class Language(models.Model):
"""Language object.""" """Language object."""
name = models.CharField(max_length=200, unique=True) name = models.CharField(max_length=200, unique=True)
slug = models.SlugField(max_length=200, unique=True) slug = models.SlugField(max_length=200, unique=True)
@ -28,7 +29,8 @@ class Language(models.Model):
language = cls.objects.filter(name__icontains=name).first() language = cls.objects.filter(name__icontains=name).first()
if not language: if not language:
language = cls.objects.filter( language = cls.objects.filter(
name__iexact=settings.PASTE['default_language']).first() name__iexact=settings.PASTE["default_language"]
).first()
return language return language
def __unicode__(self): def __unicode__(self):
@ -41,15 +43,20 @@ class Language(models.Model):
class Paste(models.Model): class Paste(models.Model):
"""Paste object.""" """Paste object."""
language = models.ForeignKey(Language, default=14, on_delete=models.SET_NULL, null=True)
language = models.ForeignKey(
Language, default=14, on_delete=models.SET_NULL, null=True
)
slug = models.SlugField(unique=True, editable=False) slug = models.SlugField(unique=True, editable=False)
title = models.CharField(max_length=200, blank=True) title = models.CharField(max_length=200, blank=True)
content = models.TextField(validators=[MaxLengthValidator( content = models.TextField(
settings.PASTE['max_characters'])]) validators=[MaxLengthValidator(settings.PASTE["max_characters"])]
)
size = models.IntegerField(default=0, editable=False) size = models.IntegerField(default=0, editable=False)
paste_time = models.DateTimeField(default=datetime.now, editable=False) paste_time = models.DateTimeField(default=datetime.now, editable=False)
lifetime = models.IntegerField(default=settings.PASTE['default_lifetime'], lifetime = models.IntegerField(
choices=EXPIRE_CHOICES) default=settings.PASTE["default_lifetime"], choices=EXPIRE_CHOICES
)
lifecount = models.IntegerField(default=0, blank=True) lifecount = models.IntegerField(default=0, blank=True)
viewcount = models.IntegerField(default=0, editable=False) viewcount = models.IntegerField(default=0, editable=False)
expired = models.BooleanField(default=False, editable=False) expired = models.BooleanField(default=False, editable=False)
@ -109,7 +116,7 @@ class Paste(models.Model):
"""Return hashed string.""" """Return hashed string."""
if not self.salt: if not self.salt:
self.salt = str(uuid.uuid1()) self.salt = str(uuid.uuid1())
return hashlib.sha512((raw+self.salt).encode()).hexdigest() return hashlib.sha512((raw + self.salt).encode()).hexdigest()
def set_password(self, raw): def set_password(self, raw):
"""Define a hashed password.""" """Define a hashed password."""
@ -126,6 +133,5 @@ class Paste(models.Model):
return self.slug return self.slug
def __str__(self): def __str__(self):
excerpt = repr(self.content[:100]) + ( excerpt = repr(self.content[:100]) + ("..." if len(self.content) > 100 else "")
'...' if len(self.content) > 100 else '')
return "{} - {} - {}".format(self.slug, self.title, excerpt) return "{} - {} - {}".format(self.slug, self.title, excerpt)

View File

@ -9,27 +9,27 @@ from paste.tools import cache_exists, cache_fetch, cache_store
def render_pygments(request, paste, data): def render_pygments(request, paste, data):
"""Renders Pygments template.""" """Renders Pygments template."""
key = paste.slug+'_pygments.cache' key = paste.slug + "_pygments.cache"
if cache_exists(key): if cache_exists(key):
highlighted_content = cache_fetch(key) highlighted_content = cache_fetch(key)
else: else:
lexer = get_lexer_by_name(paste.language.slug) lexer = get_lexer_by_name(paste.language.slug)
formatter = HtmlFormatter(style='emacs') formatter = HtmlFormatter(style="emacs")
highlighted_content = highlight(paste.content, lexer, formatter) highlighted_content = highlight(paste.content, lexer, formatter)
cache_store(key, highlighted_content) cache_store(key, highlighted_content)
data['paste'] = paste data["paste"] = paste
data['highlighted'] = highlighted_content data["highlighted"] = highlighted_content
rendered = loader.render_to_string('paste/show-pygments.html', data, request) rendered = loader.render_to_string("paste/show-pygments.html", data, request)
return HttpResponse(rendered) return HttpResponse(rendered)
def render_form(request, paste, data): def render_form(request, paste, data):
"""Renders Form template.""" """Renders Form template."""
data['paste'] = paste data["paste"] = paste
rendered = loader.render_to_string('paste/show-form.html', data, request) rendered = loader.render_to_string("paste/show-form.html", data, request)
return HttpResponse(rendered) return HttpResponse(rendered)
def render_raw(request, paste, data): def render_raw(request, paste, data):
"""Renders RAW content.""" """Renders RAW content."""
return HttpResponse(paste.content, content_type='text/plain') return HttpResponse(paste.content, content_type="text/plain")

View File

@ -2,12 +2,14 @@ from django import template
register = template.Library() register = template.Library()
@register.filter(name='add_class')
@register.filter(name="add_class")
def add_class(value, arg): def add_class(value, arg):
value.field.widget.attrs.update({"class": arg}) value.field.widget.attrs.update({"class": arg})
return value return value
@register.filter(name='placeholder')
@register.filter(name="placeholder")
def placeholder(value, arg): def placeholder(value, arg):
value.field.widget.attrs.update({"placeholder": arg}) value.field.widget.attrs.update({"placeholder": arg})
return value return value

View File

@ -8,7 +8,7 @@ from .models import Paste
def random_id(model): def random_id(model):
"""Returns a short uuid for the slug of the given model.""" """Returns a short uuid for the slug of the given model."""
uuid = random.choice('0123456789') + shortuuid.uuid() uuid = random.choice("0123456789") + shortuuid.uuid()
for i in range(3, len(uuid)): for i in range(3, len(uuid)):
potential_uuid = uuid[:i] potential_uuid = uuid[:i]
if not model.objects.filter(slug=potential_uuid): if not model.objects.filter(slug=potential_uuid):
@ -28,11 +28,11 @@ def cache_exists(key):
def cache_store(key, value): def cache_store(key, value):
"""Store cache value for key.""" """Store cache value for key."""
with open(cache_get_filepath(key), 'w') as cache_file: with open(cache_get_filepath(key), "w") as cache_file:
cache_file.write(value) cache_file.write(value)
def cache_fetch(key): def cache_fetch(key):
"""Fetch cache value for key.""" """Fetch cache value for key."""
with open(cache_get_filepath(key), 'r') as cache_file: with open(cache_get_filepath(key), "r") as cache_file:
return cache_file.read() return cache_file.read()

View File

@ -5,9 +5,13 @@ from webtools import settings
urlpatterns = [ urlpatterns = [
url(r'^$', views.index, name='index'), url(r"^$", views.index, name="index"),
url(r'^history$', views.history, name='history'), url(r"^history$", views.history, name="history"),
url(r'^static/(?P<path>.*)', serve, {'document_root': settings.STATIC_ROOT}), url(r"^static/(?P<path>.*)", serve, {"document_root": settings.STATIC_ROOT}),
url(r'^paste/(?P<slug>[a-zA-Z0-9]+)/(?P<renderer>[a-z]+)?$', views.show, name='paste'), url(
url(r'^(?P<slug>[0-9][a-zA-Z0-9]+)$', views.show, name='short_paste'), r"^paste/(?P<slug>[a-zA-Z0-9]+)/(?P<renderer>[a-z]+)?$",
views.show,
name="paste",
),
url(r"^(?P<slug>[0-9][a-zA-Z0-9]+)$", views.show, name="short_paste"),
] ]

View File

@ -12,74 +12,86 @@ from webtools import settings
@csrf_exempt @csrf_exempt
def index(request): def index(request):
"""Displays form.""" """Displays form."""
data = {'menu': 'index', data = {"menu": "index", "max_characters": settings.PASTE["max_characters"]}
'max_characters': settings.PASTE['max_characters']} if request.method == "POST":
if request.method == 'POST':
paste = Paste(slug=random_id(Paste)) paste = Paste(slug=random_id(Paste))
if request.FILES: if request.FILES:
for language_name, any_file in request.FILES.items(): for language_name, any_file in request.FILES.items():
break break
language = Language.by_name(language_name) language = Language.by_name(language_name)
form = PasteForm({'language': language.id, form = PasteForm(
'title': any_file.name, {
'private': settings.PASTE['private_by_default'], "language": language.id,
'lifetime': settings.PASTE['default_lifetime'], "title": any_file.name,
'content': any_file.read().decode() "private": settings.PASTE["private_by_default"],
}, instance=paste) "lifetime": settings.PASTE["default_lifetime"],
"content": any_file.read().decode(),
},
instance=paste,
)
else: else:
form = PasteForm(request.POST, instance=paste) form = PasteForm(request.POST, instance=paste)
if not form.is_valid(): if not form.is_valid():
data['form'] = form data["form"] = form
return render(request, 'paste/index.html', data) return render(request, "paste/index.html", data)
form.save() # Some logic added to overrided method, see forms.py form.save() # Some logic added to overrided method, see forms.py
location = request.build_absolute_uri( location = request.build_absolute_uri(
reverse('short_paste', kwargs={'slug': paste.slug})) reverse("short_paste", kwargs={"slug": paste.slug})
return HttpResponseRedirect(location, content=location + "\n", )
content_type='text/plain') return HttpResponseRedirect(
data['form'] = PasteForm(initial={ location, content=location + "\n", content_type="text/plain"
'private': settings.PASTE['private_by_default'], )
'lifetime': settings.PASTE['default_lifetime'], data["form"] = PasteForm(
'language': Language.by_name(settings.PASTE['default_language']).id}) initial={
data['absolute_index_url'] = request.build_absolute_uri(reverse('index')) "private": settings.PASTE["private_by_default"],
return render(request, 'paste/index.html', data) "lifetime": settings.PASTE["default_lifetime"],
"language": Language.by_name(settings.PASTE["default_language"]).id,
}
)
data["absolute_index_url"] = request.build_absolute_uri(reverse("index"))
return render(request, "paste/index.html", data)
def show(request, slug, renderer='pygments'): def show(request, slug, renderer="pygments"):
"""Display paste.""" """Display paste."""
# Fetching object # Fetching object
paste = get_object_or_404(Paste, slug=slug) paste = get_object_or_404(Paste, slug=slug)
data = {'title': paste.title,'slug': slug} data = {"title": paste.title, "slug": slug}
# Handling expiration # Handling expiration
if paste.is_expired(): if paste.is_expired():
return render(request, 'paste/expired.html') return render(request, "paste/expired.html")
# Handling passwords # Handling passwords
if paste.password: if paste.password:
if 'password' in request.POST: if "password" in request.POST:
password = request.POST['password'] password = request.POST["password"]
elif 'password' in request.COOKIES: elif "password" in request.COOKIES:
password = request.COOKIES['password'] password = request.COOKIES["password"]
else: else:
password = None password = None
if not paste.pwd_match(password): if not paste.pwd_match(password):
return render(request, 'paste/locked.html', data) return render(request, "paste/locked.html", data)
# Before rendering actions # Before rendering actions
paste.incr_viewcount() paste.incr_viewcount()
# Handling rendering modes # Handling rendering modes
if not renderer or renderer not in settings.PASTE['enabled_renderers']: if not renderer or renderer not in settings.PASTE["enabled_renderers"]:
renderer = settings.PASTE['default_renderer'] renderer = settings.PASTE["default_renderer"]
data['current_renderer'] = renderer data["current_renderer"] = renderer
data['renderers'] = settings.PASTE['enabled_renderers'] data["renderers"] = settings.PASTE["enabled_renderers"]
render_method = getattr(renderers, 'render_%s' % renderer) render_method = getattr(renderers, "render_%s" % renderer)
response = render_method(request, paste, data) response = render_method(request, paste, data)
# Responding # Responding
if 'password' in request.POST: if "password" in request.POST:
response.set_cookie('password', request.POST['password']) response.set_cookie("password", request.POST["password"])
return response return response
def history(request): def history(request):
"""Display last 25 public non-expired pastes.""" """Display last 25 public non-expired pastes."""
pastes = Paste.objects.filter(private=False, expired=False).order_by('-pk')[:25] pastes = Paste.objects.filter(private=False, expired=False).order_by("-pk")[:25]
data = {'pastes': pastes, 'menu': 'history', 'default_renderer': settings.PASTE['default_renderer']} data = {
return render(request, 'paste/history.html', data) "pastes": pastes,
"menu": "history",
"default_renderer": settings.PASTE["default_renderer"],
}
return render(request, "paste/history.html", data)