lots more stuff
This commit is contained in:
parent
46c9f0a447
commit
c4f250573a
19 changed files with 7619 additions and 149 deletions
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
__pycache__
|
||||||
|
db.sqlite3
|
||||||
|
gunicorn.conf.py
|
||||||
|
public/
|
||||||
|
secret_key.txt
|
||||||
|
think_data/
|
|
@ -23,7 +23,10 @@ class RemixThinkForm(forms.ModelForm):
|
||||||
class RenameThinkForm(forms.ModelForm):
|
class RenameThinkForm(forms.ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Think
|
model = Think
|
||||||
fields = ['slug']
|
fields = ['slug', 'category', 'is_template']
|
||||||
|
widgets = {
|
||||||
|
'category': forms.TextInput(attrs={'list': 'categories'})
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, *args, instance=None, **kwargs):
|
def __init__(self, *args, instance=None, **kwargs):
|
||||||
self.original_root = None if instance is None else instance.root
|
self.original_root = None if instance is None else instance.root
|
||||||
|
|
|
@ -104,6 +104,7 @@ class MakeHandler(FileSystemEventHandler):
|
||||||
command = ['make'] + config.get('default_make', [])
|
command = ['make'] + config.get('default_make', [])
|
||||||
res = subprocess.run(command, cwd=d, capture_output=True, encoding='utf-8')
|
res = subprocess.run(command, cwd=d, capture_output=True, encoding='utf-8')
|
||||||
with open(d / '.make.log', 'w') as f:
|
with open(d / '.make.log', 'w') as f:
|
||||||
|
f.write(f"{datetime.now()}\n")
|
||||||
f.write(res.stdout)
|
f.write(res.stdout)
|
||||||
f.write(res.stderr)
|
f.write(res.stderr)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Generated by Django 5.0.3 on 2024-03-05 19:22
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('thinks', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='think',
|
||||||
|
options={'ordering': ('slug',)},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='think',
|
||||||
|
name='is_template',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
18
thinks/migrations/0003_think_category.py
Normal file
18
thinks/migrations/0003_think_category.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 5.0.3 on 2024-04-26 07:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('thinks', '0002_alter_think_options_think_is_template'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='think',
|
||||||
|
name='category',
|
||||||
|
field=models.CharField(blank=True, max_length=100, null=True),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,6 +1,7 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.timezone import datetime, make_aware
|
||||||
|
|
||||||
THINKS_DIR = settings.THINKS_DIR
|
THINKS_DIR = settings.THINKS_DIR
|
||||||
|
|
||||||
|
@ -8,6 +9,9 @@ THINKS_DIR = settings.THINKS_DIR
|
||||||
class Think(models.Model):
|
class Think(models.Model):
|
||||||
|
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
|
category = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
|
||||||
|
is_template = models.BooleanField(default=False)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('slug',)
|
ordering = ('slug',)
|
||||||
|
@ -43,3 +47,17 @@ class Think(models.Model):
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_log(self):
|
||||||
|
log_file = self.file_path('.make.log')
|
||||||
|
if not log_file.exists():
|
||||||
|
return ''
|
||||||
|
|
||||||
|
with open(log_file) as f:
|
||||||
|
log = f.read()
|
||||||
|
|
||||||
|
return log
|
||||||
|
|
||||||
|
@property
|
||||||
|
def creation_time(self):
|
||||||
|
return make_aware(datetime.fromtimestamp(self.root.stat().st_ctime))
|
||||||
|
|
7
thinks/static/thinks/load-think-editor.mjs
Normal file
7
thinks/static/thinks/load-think-editor.mjs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import './code-editor.mjs';
|
||||||
|
|
||||||
|
export default async function init_app() {
|
||||||
|
const flags = JSON.parse(document.getElementById('think-editor-data').textContent);
|
||||||
|
flags.csrf_token = document.getElementById('csrftoken')?.textContent || '';
|
||||||
|
const app = Elm.App.init({node: document.body, flags});
|
||||||
|
}
|
201
thinks/static/thinks/think-editor.css
Normal file
201
thinks/static/thinks/think-editor.css
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
:root {
|
||||||
|
--spacing: 1em;
|
||||||
|
--half-spacing: calc(0.5 * var(--spacing));
|
||||||
|
--double-spacing: calc(2 * var(--spacing));
|
||||||
|
|
||||||
|
--editor-size: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--half-spacing);
|
||||||
|
|
||||||
|
& > header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
& h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
& #think-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#editor-size-input + output {
|
||||||
|
width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-path {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.think-editor {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& > #main-nav > nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing);
|
||||||
|
|
||||||
|
& #file-tree {
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
margin-top: var(--half-spacing);
|
||||||
|
|
||||||
|
& > a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dir {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dir + .file {
|
||||||
|
margin-top: var(--spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 6em;
|
||||||
|
align-content: start;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #make-log {
|
||||||
|
& > pre {
|
||||||
|
max-width: 20em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #log[open] {
|
||||||
|
width: 80ch;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #editor {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: var(--editor-size);
|
||||||
|
max-height: 85vh;
|
||||||
|
|
||||||
|
& #editor-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
& > details {
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
& > summary {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
margin: var(--half-spacing) 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #code-editor {
|
||||||
|
display: block;
|
||||||
|
max-width: 50vw;
|
||||||
|
padding-bottom: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-basis: calc(100% - var(--editor-size));
|
||||||
|
}
|
||||||
|
|
||||||
|
& > summary {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[closed] > iframe {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-form {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
@media (max-width: 100ch) {
|
||||||
|
html {
|
||||||
|
font-size: min(3vw, 16px);
|
||||||
|
}
|
||||||
|
.think-editor {
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
& > * ~ * {
|
||||||
|
border-top: medium solid #888;
|
||||||
|
margin-top: var(--spacing);
|
||||||
|
padding-top: var(--spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
& nav {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& form {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #file-tree {
|
||||||
|
max-height: 7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #editor {
|
||||||
|
overflow: auto;
|
||||||
|
& #code-editor {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #preview {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7074
thinks/static/thinks/think-editor.js
Normal file
7074
thinks/static/thinks/think-editor.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,31 +1,64 @@
|
||||||
:root {
|
:root {
|
||||||
--spacing: 1em;
|
--spacing: 1em;
|
||||||
--half-spacing: calc(0.5 * var(--spacing));
|
--half-spacing: calc(0.5 * var(--spacing));
|
||||||
|
--double-spacing: calc(2 * var(--spacing));
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body > header {
|
body {
|
||||||
padding: var(--spacing);
|
font-family: sans-serif;
|
||||||
& h1 {
|
& > header {
|
||||||
margin: 0;
|
padding: var(--spacing);
|
||||||
|
& h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body.index {
|
body.index {
|
||||||
& #thinks-list {
|
font-size: 20px;
|
||||||
padding: 0;
|
|
||||||
list-style: none;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing);
|
|
||||||
|
|
||||||
& > .think {
|
& main {
|
||||||
& .readme {
|
padding: 0 var(--spacing);
|
||||||
max-width: 80ch;
|
|
||||||
white-space: pre-wrap;
|
& #templates-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--double-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
& #thinks-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--double-spacing);
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
& details {
|
||||||
|
&[open] > summary {
|
||||||
|
margin-bottom: var(--spacing);
|
||||||
|
}
|
||||||
|
& ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .think {
|
||||||
|
& time {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .readme {
|
||||||
|
max-width: 80ch;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +90,19 @@ body.thing-editor {
|
||||||
& #file-tree {
|
& #file-tree {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
& > li {
|
||||||
|
margin-top: var(--half-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dir {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .dir + .file {
|
||||||
|
margin-top: var(--spacing);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& form {
|
& form {
|
||||||
|
@ -65,6 +111,13 @@ body.thing-editor {
|
||||||
align-content: start;
|
align-content: start;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& #make-log {
|
||||||
|
& > pre {
|
||||||
|
max-width: 20em;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& #editor {
|
& #editor {
|
||||||
|
@ -89,7 +142,9 @@ body.thing-editor {
|
||||||
& #code-editor {
|
& #code-editor {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 50vw;
|
max-width: 50vw;
|
||||||
|
padding-bottom: 10em;
|
||||||
}
|
}
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
& #preview {
|
& #preview {
|
||||||
|
@ -101,12 +156,13 @@ body.thing-editor {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#file-form {
|
#file-form {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
/*! max-height: 100%; */
|
max-height: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@media (max-width: 100ch) {
|
@media (max-width: 100ch) {
|
||||||
|
@ -116,17 +172,33 @@ body.thing-editor {
|
||||||
body.thing-editor {
|
body.thing-editor {
|
||||||
& main {
|
& main {
|
||||||
grid-template:
|
grid-template:
|
||||||
"nav" min-content "editor" 40vh "preview";
|
"nav" min-content "editor" 40vh "preview"
|
||||||
|
;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
& nav {
|
& nav {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
& form {
|
& form {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& #file-tree {
|
||||||
|
max-height: 7em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #editor {
|
||||||
|
overflow: auto;
|
||||||
|
& #code-editor {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #preview {
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
31
thinks/templates/registration/login.html
Normal file
31
thinks/templates/registration/login.html
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block body_class %}login {{block.super}}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Login</h1>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% if form.errors %}
|
||||||
|
<p>Your username and password didn't match. Please try again.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if next %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p>Your account doesn't have access to this page. To proceed,
|
||||||
|
please login with an account that has access.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Please login to see this page.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'login' %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{form}}
|
||||||
|
<button type="submit">Log in</button>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -8,7 +8,7 @@
|
||||||
<title>{% block title %}Thinks{% endblock %}</title>
|
<title>{% block title %}Thinks{% endblock %}</title>
|
||||||
|
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
<link rel="stylesheet" href="{% static "thinks/thinks.css" %}">
|
<link rel="stylesheet" href="{% static "thinks/thinks.css" %}?{% now "U" %}">
|
||||||
{% endblock stylesheets %}
|
{% endblock stylesheets %}
|
||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
|
|
@ -7,18 +7,45 @@
|
||||||
{% endblock header %}
|
{% endblock header %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<ul id="thinks-list">
|
<section id="templates">
|
||||||
{% for think in object_list %}
|
<h2>Templates</h2>
|
||||||
<li class="think">
|
<ul id="templates-list">
|
||||||
<a href="{% url 'think' think.slug %}">{{think.slug}}</a>
|
{% for think in templates %}
|
||||||
{% with readme=think.get_readme %}
|
<li class="think">
|
||||||
{% if readme %}
|
<a class="remix" href="{% url 'remix_think' think.slug %}">{{think.slug}}</a>
|
||||||
<pre class="readme">{{readme}}
|
<br>
|
||||||
</pre>
|
<small>(<a href="{% url 'think' think.slug %}">edit</a>)</small>
|
||||||
{% endif %}
|
</li>
|
||||||
{% endwith %}
|
{% endfor %}
|
||||||
</li>
|
</ul>
|
||||||
{% endfor %}
|
</section>
|
||||||
</ul>
|
|
||||||
<a href="{% url 'new_think' %}">New think</a>
|
<section id="thinks">
|
||||||
|
<h2>Thinks</h2>
|
||||||
|
<p><a href="{% url 'new_think' %}">New think</a></p>
|
||||||
|
{% regroup thinks by category as categories %}
|
||||||
|
<ul id="thinks-list">
|
||||||
|
{% for category in categories %}
|
||||||
|
<li>
|
||||||
|
<details {% if not category.grouper %}open{% endif %}>
|
||||||
|
<summary>{% if category.grouper %}{{category.grouper}}{% else %}Uncategorised{% endif %}</summary>
|
||||||
|
<ul>
|
||||||
|
{% for think in category.list %}
|
||||||
|
<li class="think">
|
||||||
|
<a href="{% url 'think' think.slug %}">{{think.slug}}</a>
|
||||||
|
<time datetime="{{think.creation_time|date:"c"}}">{{think.creation_time}}</time>
|
||||||
|
{% with readme=think.get_readme %}
|
||||||
|
{% if readme %}
|
||||||
|
<pre class="readme">{{readme|safe}}</pre>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</dl>
|
||||||
|
</section>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
|
|
30
thinks/templates/thinks/new_think.html
Normal file
30
thinks/templates/thinks/new_think.html
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{path}} - {{think.slug}} - {{block.super}}{% endblock title %}
|
||||||
|
|
||||||
|
{% block body_class %}thing-editor {{block.super}}{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{% static "thinks/think-editor.js" %}"></script>
|
||||||
|
|
||||||
|
{{think_editor_data|json_script:"think-editor-data"}}
|
||||||
|
<script id="csrftoken" type="text/plain">{{csrf_token}}</script>
|
||||||
|
|
||||||
|
<script type="module"">
|
||||||
|
import init_app from "{% static "thinks/load-think-editor.mjs" %}";
|
||||||
|
init_app();
|
||||||
|
</script>
|
||||||
|
{% endblock scripts %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
<link rel="stylesheet" href="{% static "thinks/think-editor.css" %}?{% now "U" %}">
|
||||||
|
{% endblock stylesheets %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<a href="/">thinks</a>
|
||||||
|
<h1>{{think.slug}}</h1>
|
||||||
|
{% endblock header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% endblock main %}
|
|
@ -8,6 +8,11 @@
|
||||||
|
|
||||||
<form method="post" >
|
<form method="post" >
|
||||||
{{form}}
|
{{form}}
|
||||||
|
<datalist id="categories">
|
||||||
|
{% for category in categories %}
|
||||||
|
<option value="{{category}}"></option>
|
||||||
|
{% endfor %}
|
||||||
|
</datalist>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit">Rename</button>
|
<button type="submit">Rename</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,106 +1,30 @@
|
||||||
{% extends "thinks/base.html" %}
|
{% extends "thinks/base.html" %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{{path}} - {{think.slug}} - {{block.super}}{% endblock title %}
|
||||||
|
|
||||||
{% block body_class %}thing-editor {{block.super}}{% endblock %}
|
{% block body_class %}thing-editor {{block.super}}{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{% static "thinks/think-editor.js" %}"></script>
|
||||||
|
|
||||||
|
{{think_editor_data|json_script:"think-editor-data"}}
|
||||||
|
<script id="csrftoken" type="text/plain">{{csrf_token}}</script>
|
||||||
|
|
||||||
|
<script type="module"">
|
||||||
|
import init_app from "{% static "thinks/load-think-editor.mjs" %}";
|
||||||
|
init_app();
|
||||||
|
</script>
|
||||||
|
{% endblock scripts %}
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
<link rel="stylesheet" href="{% static "thinks/think-editor.css" %}?{% now "U" %}">
|
||||||
|
{% endblock stylesheets %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<a href="/">thinks</a>
|
<a href="/">thinks</a>
|
||||||
<h1>{{think.slug}}</h1>
|
<h1>{{think.slug}}</h1>
|
||||||
<a href="{% url 'rename_think' slug=think.slug %}">Rename</a>
|
|
||||||
<a href="{% url 'delete_think' slug=think.slug %}">Delete</a>
|
|
||||||
<a href="{% url 'remix_think' slug=think.slug %}">Remix</a>
|
|
||||||
{% endblock header %}
|
{% endblock header %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
<nav>
|
|
||||||
<a target="preview" href="{{think.get_static_url}}">Preview</a>
|
|
||||||
|
|
||||||
<ul id="file-tree">
|
|
||||||
{% for name, path in files %}
|
|
||||||
<li><a href="?path={{path}}">{{name}}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<form id="new-file-form" method="post" action="{% url 'save_file' slug=think.slug %}" enctype="multipart/form-data">
|
|
||||||
<input aria-labelledby="new-file-button" id="new_file_path" type="text" name="path">
|
|
||||||
<button id="new-file-button" type="submit">New file</button>
|
|
||||||
{% csrf_token %}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form method="post" action="{% url 'run_command' slug=think.slug %}">
|
|
||||||
<input aria-labelledby="run-command-button" name="command">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button id="run-command-button" type="submit">Run</button>
|
|
||||||
</form>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<section id="editor">
|
|
||||||
{% if path is not None and not path.is_dir %}
|
|
||||||
<nav id="editor-controls">
|
|
||||||
{{path}}
|
|
||||||
<details>
|
|
||||||
<summary>actions</summary>
|
|
||||||
<form method="post" action="{% url 'delete_file' slug=think.slug %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="path" value="{{path}}">
|
|
||||||
<button type="submit">Delete</button>
|
|
||||||
</form>
|
|
||||||
<form method="post" action="{% url 'rename_file' slug=think.slug %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="path" value="{{path}}">
|
|
||||||
<input type="text" name="newpath" value="{{path}}">
|
|
||||||
<button type="submit">Rename</button>
|
|
||||||
</form>
|
|
||||||
</details>
|
|
||||||
</nav>
|
|
||||||
<form id="file-form" method="post" action="{% url 'save_file' slug=think.slug %}" enctype="multipart/form-data">
|
|
||||||
{{file_form.path.as_hidden}}
|
|
||||||
{{file_form.content.as_hidden}}
|
|
||||||
<code-editor id="code-editor">{{content}}</code-editor>
|
|
||||||
{% csrf_token %}
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="preview">
|
|
||||||
<button type="button" id="reload-preview">Reload</button>
|
|
||||||
<iframe id="preview-frame" src="{{think.get_static_url}}"></iframe>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const file_form = document.getElementById('file-form');
|
|
||||||
const preview_frame = document.getElementById('preview-frame');
|
|
||||||
const content_input = document.getElementById('code-editor');
|
|
||||||
|
|
||||||
function debounce(fn) {
|
|
||||||
let last = null;
|
|
||||||
return function() {
|
|
||||||
if(last) {
|
|
||||||
clearTimeout(last);
|
|
||||||
}
|
|
||||||
last = setTimeout(fn, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function reload_preview() {
|
|
||||||
preview_frame.src = preview_frame.src;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(content_input) {
|
|
||||||
const save_content = debounce(async() => {
|
|
||||||
const value = content_input.value;
|
|
||||||
console.log('save', value);
|
|
||||||
file_form.querySelector('[name="content"]').value = value;
|
|
||||||
await fetch(file_form.action, {method: 'POST', body: new FormData(file_form)});
|
|
||||||
reload_preview();
|
|
||||||
});
|
|
||||||
content_input.addEventListener('change', save_content);
|
|
||||||
}
|
|
||||||
console.log('hey');
|
|
||||||
|
|
||||||
document.getElementById('reload-preview').addEventListener('click', () => {
|
|
||||||
reload_preview();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
|
|
|
@ -4,7 +4,7 @@ from .views import *
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', IndexView.as_view(), name='index'),
|
path('', IndexView.as_view(), name='index'),
|
||||||
path('think/<slug:slug>', ThinkView.as_view(), name='think'),
|
path('think/<slug:slug>/edit', ThinkView.as_view(), name='think'),
|
||||||
path('think/<slug:slug>/rename', RenameThinkView.as_view(), name='rename_think'),
|
path('think/<slug:slug>/rename', RenameThinkView.as_view(), name='rename_think'),
|
||||||
path('think/<slug:slug>/delete', DeleteThinkView.as_view(), name='delete_think'),
|
path('think/<slug:slug>/delete', DeleteThinkView.as_view(), name='delete_think'),
|
||||||
path('think/<slug:slug>/file/<path:path>', ReadFileView.as_view(), name='read_file'),
|
path('think/<slug:slug>/file/<path:path>', ReadFileView.as_view(), name='read_file'),
|
||||||
|
@ -12,6 +12,7 @@ urlpatterns = [
|
||||||
path('think/<slug:slug>/rename-file', RenameFileView.as_view(), name='rename_file'),
|
path('think/<slug:slug>/rename-file', RenameFileView.as_view(), name='rename_file'),
|
||||||
path('think/<slug:slug>/delete-file', DeleteFileView.as_view(), name='delete_file'),
|
path('think/<slug:slug>/delete-file', DeleteFileView.as_view(), name='delete_file'),
|
||||||
path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'),
|
path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'),
|
||||||
|
path('think/<slug:slug>/log', LogView.as_view(), name='log'),
|
||||||
path('new', CreateThinkView.as_view(), name='new_think'),
|
path('new', CreateThinkView.as_view(), name='new_think'),
|
||||||
path('new/<slug:slug>', RemixThinkView.as_view(), name='remix_think'),
|
path('new/<slug:slug>', RemixThinkView.as_view(), name='remix_think'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -4,8 +4,10 @@ from django.http import HttpResponse, JsonResponse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from itertools import groupby
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from . import forms
|
from . import forms
|
||||||
|
@ -19,6 +21,15 @@ class ThinkMixin(LoginRequiredMixin):
|
||||||
class IndexView(ThinkMixin, generic.ListView):
|
class IndexView(ThinkMixin, generic.ListView):
|
||||||
template_name = 'thinks/index.html'
|
template_name = 'thinks/index.html'
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
|
context['templates'] = Think.objects.filter(is_template=True)
|
||||||
|
|
||||||
|
context['thinks'] = sorted(Think.objects.filter(is_template=False), key=lambda t: (t.category if t.category else '', -t.creation_time.timestamp()))
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
class CreateThinkView(ThinkMixin, generic.CreateView):
|
class CreateThinkView(ThinkMixin, generic.CreateView):
|
||||||
template_name = 'thinks/new.html'
|
template_name = 'thinks/new.html'
|
||||||
form_class = forms.CreateThinkForm
|
form_class = forms.CreateThinkForm
|
||||||
|
@ -49,34 +60,31 @@ class RemixThinkView(ThinkMixin, generic.UpdateView):
|
||||||
|
|
||||||
|
|
||||||
class ThinkView(ThinkMixin, generic.DetailView):
|
class ThinkView(ThinkMixin, generic.DetailView):
|
||||||
template_name = 'thinks/think.html'
|
template_name = "thinks/think.html"
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, *args, **kwargs):
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
think = self.object
|
think = self.object
|
||||||
|
|
||||||
|
root = think.root
|
||||||
|
|
||||||
strpath = self.request.GET.get('path')
|
strpath = self.request.GET.get('path')
|
||||||
|
|
||||||
path = think.file_path(strpath)
|
path = think.file_path(strpath)
|
||||||
|
|
||||||
relpath = path.relative_to(think.root) if path is not None else None
|
relpath = path.relative_to(root) if path is not None else None
|
||||||
|
|
||||||
if path is None:
|
if path is None:
|
||||||
directory = think.root
|
directory = root
|
||||||
elif path.is_dir():
|
elif path.is_dir():
|
||||||
directory = path
|
directory = path
|
||||||
else:
|
else:
|
||||||
directory = path.parent
|
directory = path.parent
|
||||||
|
|
||||||
files = [(p.name, p.relative_to(think.root)) for p in directory.iterdir()]
|
files = [{'name': p.name, 'path': str(p.relative_to(root)), 'is_dir': p.is_dir()} for p in directory.iterdir()]
|
||||||
if directory != think.root:
|
if directory != root:
|
||||||
files.insert(0, ('..', directory.parent.relative_to(think.root)))
|
files.insert(0, {'name': '..', 'path': str(directory.parent.relative_to(root)), 'is_dir': True})
|
||||||
context['files'] = files
|
|
||||||
|
|
||||||
context['directory'] = directory
|
|
||||||
|
|
||||||
context['path'] = relpath
|
|
||||||
|
|
||||||
if path is not None and path.is_file():
|
if path is not None and path.is_file():
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
|
@ -84,9 +92,15 @@ class ThinkView(ThinkMixin, generic.DetailView):
|
||||||
else:
|
else:
|
||||||
content = ''
|
content = ''
|
||||||
|
|
||||||
context['content'] = content
|
context['think_editor_data'] = {
|
||||||
|
'preview_url': think.get_static_url(),
|
||||||
context['file_form'] = forms.SaveFileForm(instance=think, initial={'content': content, 'path': relpath})
|
'slug': think.slug,
|
||||||
|
'files': files,
|
||||||
|
'file_path': str(relpath),
|
||||||
|
'file_content': content,
|
||||||
|
'is_dir': path is None or path.is_dir(),
|
||||||
|
'no_preview': self.request.GET.get('no-preview') is not None,
|
||||||
|
}
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
@ -97,6 +111,13 @@ class RenameThinkView(ThinkMixin, generic.UpdateView):
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.object.get_absolute_url()
|
return self.object.get_absolute_url()
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
|
context['categories'] = sorted(Think.objects.exclude(category=None).order_by('category').values_list('category',flat=True).distinct())
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
class DeleteThinkView(ThinkMixin, generic.DeleteView):
|
class DeleteThinkView(ThinkMixin, generic.DeleteView):
|
||||||
template_name = 'thinks/delete.html'
|
template_name = 'thinks/delete.html'
|
||||||
|
|
||||||
|
@ -152,9 +173,17 @@ class RunCommandView(ThinkMixin, generic.UpdateView):
|
||||||
think = self.object
|
think = self.object
|
||||||
command = form.cleaned_data['command']
|
command = form.cleaned_data['command']
|
||||||
res = subprocess.run(
|
res = subprocess.run(
|
||||||
command.split(' '),
|
['bash','-c',command],
|
||||||
cwd=think.root,
|
cwd=think.root,
|
||||||
encoding='utf8',
|
encoding='utf8',
|
||||||
capture_output=True
|
capture_output=True
|
||||||
)
|
)
|
||||||
return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr})
|
return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr})
|
||||||
|
|
||||||
|
class LogView(ThinkMixin, generic.DetailView):
|
||||||
|
template_name = 'thinks/think.html'
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
think = self.get_object()
|
||||||
|
|
||||||
|
return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8')
|
||||||
|
|
|
@ -20,7 +20,8 @@ BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = 'django-insecure-s8dq=40yx#rhlq@j$#i3^naz9&yx6tut#ikr80uu!r(1ze@3#@'
|
with open('/srv/think.somethingorotherwhatever.com/secret_key.txt') as f:
|
||||||
|
SECRET_KEY = f.read()
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
@ -127,4 +128,4 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
THINKS_DIR = Path('think_data')
|
THINKS_DIR = Path('think_data')
|
||||||
THINKS_DIR.mkdir(parents=True, exist_ok=True)
|
THINKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
THINKS_STATIC_URL = 'http://{slug}.think.somethingorotherwhatever.com'
|
THINKS_STATIC_URL = 'https://{slug}.think.somethingorotherwhatever.com'
|
||||||
|
|
Loading…
Reference in a new issue