text = models.TextField()
edit_key = models.CharField(max_length=43)
- def get_absolute_url(self):
- pk = base64.urlsafe_b64encode(
+ def get_pk(self):
+ return base64.urlsafe_b64encode(
self.pk.to_bytes(math.ceil(self.pk.bit_length() / 8), 'big'),
).decode('utf-8')
+ def get_view_url(self):
+ pk = self.get_pk()
+
return reverse('text-file', kwargs={ 'pk': pk })
+ def get_edit_url(self):
+ pk = self.get_pk()
+
+ return reverse('edit', kwargs={ 'pk': pk })
+
admin.site.register(TextFile)
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>txt.house</title>
+
+ <style>
+ html {
+ margin: 0;
+ padding: 0;
+
+ font-size: 16px;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ body {
+ margin: 0 3rem;
+ padding: 0;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ font-family: Candara, Roboto, "Lucida Sans Unicode", "Lucida Grande", Verdana, sans-serif;
+
+ width: calc(100% - 6rem);
+ max-width: 40rem;
+ }
+
+ nav {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ width: 40rem;
+ height: 5rem;
+ }
+
+ nav a {
+ margin-right: 2rem;
+ }
+
+ nav a:active,
+ nav a:hover,
+ nav a:link,
+ nav a:visited {
+ color: black;
+ }
+
+ nav label {
+ margin-right: 0.2rem;
+ }
+
+ nav select {
+ border: none;
+ font-size: 16px;
+ font-family: Candara, Roboto, "Lucida Sans Unicode", "Lucida Grande", Verdana, sans-serif;
+ }
+
+ form.editor {
+ margin-top: 3.6rem;
+ width: 100%;
+
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ form.editor span {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ margin-bottom: 1rem;
+ width: 100%;
+ }
+
+ form.editor label {
+ margin-right: 0.5rem;
+ }
+
+ form.editor input[type=text] {
+ flex-grow: 1;
+ border: 1.5px solid black;
+ border-radius: 0;
+ padding: 0.3rem;
+ }
+
+ form.editor textarea {
+ width: calc(100% - 3px - 1rem);
+ height: 20rem;
+
+ border: 1.5px solid black;
+
+ padding: 0.5rem;
+ margin: 0;
+ margin-bottom: 3rem;
+ }
+
+ form.editor input[type=submit] {
+ border: 1.5px solid black;
+ border-radius: 8px;
+ padding: 1rem;
+
+ font-family: Roboto, "Lucida Sans Unicode", "Lucida Grande", Verdana, sans-serif;
+ font-size: 16px;
+
+ color: black;
+ background: white;
+
+ cursor: pointer;
+ }
+
+ form.editor input[type=submit]:hover {
+ background: #dddddd;
+ }
+ </style>
</head>
<body>
--- /dev/null
+{% extends "base.html" %}
+
+{% block body %}
+
+<nav>
+ <a href='{% url "index" %}' title='Home'>Home</a>
+ <a href='{{ text_file.get_view_url }}' title='View'>View</a>
+</nav>
+
+<style>
+ .copy-area {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ margin-top: 1.5rem;
+ }
+
+ .copy-area label {
+ font-weight: bold;
+ margin-right: 1rem;
+ }
+
+ small {
+ margin-top: 0.5rem;
+ }
+
+ .error {
+ color: #bb0000;
+ text-align: center;
+ font-weight: bold;
+ }
+</style>
+
+{% if edit_key %}
+ <span class='copy-area'>
+ <label for='edit-key-display'>Edit key:</label>
+ <span id='edit-key-display'>{{ edit_key }}</span>
+ </span>
+ <small>Save this key in order to edit this file in the future.</small>
+{% endif %}
+
+<form class='editor' action='{{ text_file.get_edit_url }}' method='post'>
+ {% csrf_token %}
+ {% if edit_key %}
+ <input id='edit_key' name='edit_key' type='hidden' value='{{ edit_key }}'></input>
+ {% else %}
+ <span>
+ <label for='edit_key'>Edit key:</label>
+ <input id='edit_key' name='edit_key' type='text'></input>
+ </span>
+
+ {% if incorrect_key %}
+ <span class='error'>You must enter the correct edit key to save your changes.</span>
+ {% endif %}
+ {% endif %}
+ <textarea name='text'>{{ text }}</textarea>
+ <input type='submit'></input>
+</form>
+
+{% endblock %}
{% extends 'base.html' %}
{% block body %}
-<form action='create' method='post'>
+<style>
+ h1 {
+ margin: 3rem 0;
+ }
+
+ p {
+ margin: 0;
+ }
+</style>
+
+<h1>txt.house</h1>
+
+<p>Paste or edit in text and submit to get a permanent link for it.</p>
+
+<form class='editor' action='{% url "create" %}' method='post'>
{% csrf_token %}
<textarea name='text'></textarea>
<input type='submit'></input>
</form>
+
{% endblock %}
--- /dev/null
+{% extends 'base.html' %}
+
+{% block body %}
+ <style>
+ .sans {
+ font-family: Candara, Roboto, "Lucida Sans Unicode", "Lucida Grande", Verdana, sans-serif;
+ line-height: 1.6;
+ }
+
+ .serif {
+ font-family: Bookman, Garamond, "Palatino Linotype", "Book Antiqua", Palatino, serif;
+ font-size: 1.2rem;
+ line-height: 1.6;
+ }
+
+ .mono {
+ font-family: Monaco, "Courier New", Courier, monospace;
+ line-height: 1.6;
+ }
+
+ main {
+ max-width: 40rem;
+ white-space: pre-wrap;
+ margin: 0 0 3rem;
+ }
+ </style>
+
+ <nav>
+ <a href='{% url "index" %}' title='Home'>Home</a>
+ <a href='{{ text_file.get_edit_url }}' title='Edit'>Edit</a>
+
+ <label for='font-select'>Font:</label>
+
+ <select id='font-select'>
+ <option value='sans' {% if font == 'sans' %}selected{% endif %}>Sans</option>
+ <option value='serif' {% if font == 'serif' %}selected{% endif %}>Serif</option>
+ <option value='mono' {% if font == 'mono' %}selected{% endif %}>Mono</option>
+ </select>
+ </nav>
+
+ <script type='text/javascript'>
+ function updateQueryParameter(key, value) {
+ if(window.location.search === '') {
+ window.location.search = '?' + key + '=' + value;
+ return;
+ }
+
+ var queryParameters = window.location.search.substring(1).split('&').map(function(kvp) {
+ return kvp.split('=');
+ });
+
+ queryParameters.forEach(function(kvp) {
+ if(kvp[0] === key) {
+ kvp[1] = value;
+ }
+ });
+
+ window.location.search = '?' + queryParameters.map(function(kvp) {
+ return kvp.join('=');
+ }).join('&');
+ }
+
+ (function(fn) {
+ if (document.readyState != 'loading'){
+ fn();
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+ })(function() {
+ var main = document.querySelector('main');
+ var fontSelect = document.querySelector('#font-select');
+
+ fontSelect.addEventListener('change', function(e) {
+ updateQueryParameter('font', e.target.value);
+
+ main.classList.add(e.target.value);
+
+ ['mono', 'sans', 'serif'].forEach(function(c) {
+ if(c != e.target.value) main.classList.remove(c);
+ });
+ });
+ });
+ </script>
+
+ <main class='{{ font }}'>{{ text }}</main>
+
+{% endblock %}
path('', views.index, name='index'),
path('create', views.create, name='create'),
path('t/<str:pk>', views.text_file, name='text-file'),
+ path('t/<str:pk>/edit', views.edit, name='edit'),
]
import secrets
from django.http import HttpResponse, Http404
-from django.shortcuts import get_object_or_404, redirect, render
+from django.shortcuts import get_object_or_404, render
from . import models
tf = models.TextFile(text=text, edit_key=secrets.token_urlsafe())
tf.save()
- return redirect(tf.get_absolute_url())
+ return render(
+ request,
+ 'edit.html',
+ {
+ 'text_file': tf,
+ 'text': tf.text,
+ 'edit_key': tf.edit_key,
+ },
+ )
def text_file(request, pk):
if request.method != 'GET':
tf = get_object_or_404(models.TextFile, pk=pk_int)
- return HttpResponse(tf.text)
+ if request.GET.get('raw'):
+ return HttpResponse(tf.text, content_type='text/plain')
+
+ font = request.GET.get('font', 'sans')
+
+ return render(
+ request,
+ 'text_file.html',
+ {
+ 'text_file': tf,
+ 'font': font,
+ 'text': tf.text.strip(),
+ },
+ )
+
+def edit(request, pk):
+ pk_int = int.from_bytes(base64.urlsafe_b64decode(pk), 'big')
+
+ tf = get_object_or_404(models.TextFile, pk=pk_int)
+
+ context = {
+ 'text_file': tf,
+ }
+
+ if request.method == 'POST':
+ edit_key = request.POST.get('edit_key','')
+ text = request.POST.get('text','')
+
+ context['text'] = text
+
+ if secrets.compare_digest(edit_key, tf.edit_key):
+ context['edit_key'] = edit_key
+ tf.text = text
+ tf.save()
+
+ else:
+ context['incorrect_key'] = True
+
+ elif request.method == 'GET':
+ context['text'] = tf.text
+
+ return render(
+ request,
+ 'edit.html',
+ context,
+ )