lots of changes
Added a creation_time field to thinks - using the modified time on the filesystem wasn't reliable. Styled the login page. The index shows the most recent thinks at the top. Allow authentication by an Authorization header, with a token specified in settings.py. Lots of improvements to the editor, including showing the log, and a form to install Elm packages when editing .elm files. The Makefile for a project is automatically run each time a file is saved.
This commit is contained in:
parent
3d5c0c6c73
commit
500eb38774
13 changed files with 1474 additions and 396 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@ gunicorn.conf.py
|
||||||
public/
|
public/
|
||||||
secret_key.txt
|
secret_key.txt
|
||||||
think_data/
|
think_data/
|
||||||
|
Makefile
|
||||||
|
|
|
@ -68,7 +68,7 @@ class SaveFileForm(forms.ModelForm):
|
||||||
path.parent.mkdir(exist_ok=True, parents=True)
|
path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
with open(path, 'w') as f:
|
with open(path, 'w') as f:
|
||||||
f.write(content)
|
f.write(content.replace('\r\n','\n'))
|
||||||
|
|
||||||
return super().save(commit)
|
return super().save(commit)
|
||||||
|
|
||||||
|
|
72
thinks/make.py
Normal file
72
thinks/make.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from filelock import FileLock, Timeout
|
||||||
|
import subprocess
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
class ThingMaker():
|
||||||
|
|
||||||
|
gap = 2
|
||||||
|
|
||||||
|
extensions = None
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'path': '.',
|
||||||
|
'default_make': [],
|
||||||
|
'extensions': ['.js'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, think):
|
||||||
|
self.think = think
|
||||||
|
self.load_config()
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(self,):
|
||||||
|
config_path = self.think.root / '.watchmakerc'
|
||||||
|
|
||||||
|
if config_path.exists():
|
||||||
|
with open(config_path) as f:
|
||||||
|
self.config.update(yaml.load(f.read(),Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
|
self.extensions = self.config.get('extensions')
|
||||||
|
|
||||||
|
|
||||||
|
def make(self, file_changed):
|
||||||
|
if file_changed.is_dir():
|
||||||
|
return {"error": f"{file_changed} is directory"}
|
||||||
|
|
||||||
|
root = self.think.root
|
||||||
|
|
||||||
|
t = datetime.now()
|
||||||
|
|
||||||
|
src_path = (root / file_changed).resolve()
|
||||||
|
|
||||||
|
if not src_path.is_relative_to(root.resolve()):
|
||||||
|
return {"error": f"{src_path} is not relative to root {root}"}
|
||||||
|
|
||||||
|
if src_path.name == '.watchmakerc' or self.extensions is None or src_path.suffix in self.extensions:
|
||||||
|
lock_path = str(root / '.make.lock')
|
||||||
|
lock = FileLock(lock_path, timeout=5)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with lock:
|
||||||
|
return self.run(src_path)
|
||||||
|
except Timeout:
|
||||||
|
return {"error": "Timed out"}
|
||||||
|
|
||||||
|
|
||||||
|
def run(self, p):
|
||||||
|
root = self.think.root
|
||||||
|
if (root / 'Makefile').exists():
|
||||||
|
command = ['make'] + self.config.get('default_make', [])
|
||||||
|
res = subprocess.run(command, cwd=root, capture_output=True, encoding='utf-8')
|
||||||
|
with open(root / '.make.log', 'w') as f:
|
||||||
|
f.write(f"{datetime.now()}\n")
|
||||||
|
f.write(res.stdout)
|
||||||
|
f.write(res.stderr)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'stdout': res.stdout,
|
||||||
|
'stderr': res.stderr,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {"error": "No make"}
|
20
thinks/migrations/0004_think_creation_time.py
Normal file
20
thinks/migrations/0004_think_creation_time.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 5.0.3 on 2024-12-07 12:03
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('thinks', '0003_think_category'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='think',
|
||||||
|
name='creation_time',
|
||||||
|
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2024, 12, 7, 12, 3, 5, 567875, tzinfo=datetime.timezone.utc)),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
|
@ -10,6 +10,7 @@ class Think(models.Model):
|
||||||
|
|
||||||
slug = models.SlugField()
|
slug = models.SlugField()
|
||||||
category = models.CharField(max_length=100, blank=True, null=True)
|
category = models.CharField(max_length=100, blank=True, null=True)
|
||||||
|
creation_time = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
is_template = models.BooleanField(default=False)
|
is_template = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
@ -58,6 +59,11 @@ class Think(models.Model):
|
||||||
|
|
||||||
return log
|
return log
|
||||||
|
|
||||||
@property
|
def as_json(self):
|
||||||
def creation_time(self):
|
return {
|
||||||
return make_aware(datetime.fromtimestamp(self.root.stat().st_ctime))
|
'slug': self.slug,
|
||||||
|
'category': self.category,
|
||||||
|
'absolute_url': self.get_absolute_url(),
|
||||||
|
'readme': self.get_readme(),
|
||||||
|
'creation_time': self.creation_time,
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,19 @@
|
||||||
import './code-editor.mjs';
|
import './code-editor.mjs';
|
||||||
|
|
||||||
export default async function init_app() {
|
export default async function init_app() {
|
||||||
const flags = JSON.parse(document.getElementById('think-editor-data').textContent);
|
const flags = JSON.parse(document.getElementById('think-editor-data').textContent);
|
||||||
flags.csrf_token = document.getElementById('csrftoken')?.textContent || '';
|
flags.csrf_token = document.getElementById('csrftoken')?.textContent || '';
|
||||||
const app = Elm.App.init({node: document.body, flags});
|
const app = Elm.App.init({node: document.body, flags});
|
||||||
}
|
|
||||||
|
app.ports.reload_preview.subscribe(() => {
|
||||||
|
console.log('reload preview');
|
||||||
|
const iframe = document.getElementById('preview-frame');
|
||||||
|
if(iframe) {
|
||||||
|
const src = iframe.src;
|
||||||
|
iframe.src = "";
|
||||||
|
setTimeout(() => {
|
||||||
|
iframe.src = src;
|
||||||
|
},10);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -1,201 +1,237 @@
|
||||||
: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));
|
--double-spacing: calc(2 * var(--spacing));
|
||||||
|
|
||||||
--editor-size: 50%;
|
--editor-size: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: sans-serif;
|
color-scheme: light dark;
|
||||||
|
font-family: sans-serif;
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
display: grid;
|
||||||
min-height: 100vh;
|
grid-template-rows: auto 1fr;
|
||||||
margin: 0;
|
min-height: 100vh;
|
||||||
padding: var(--half-spacing);
|
margin: 0;
|
||||||
|
padding: var(--half-spacing);
|
||||||
& > header {
|
|
||||||
padding: var(--spacing);
|
& > header {
|
||||||
& h1 {
|
padding: var(--spacing);
|
||||||
margin: 0;
|
& h1 {
|
||||||
}
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
}
|
||||||
header {
|
|
||||||
& #think-controls {
|
@media (prefers-color-scheme: dark) {
|
||||||
display: flex;
|
body {
|
||||||
gap: var(--spacing);
|
background: black;
|
||||||
margin: 0;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#editor-size-input + output {
|
header {
|
||||||
width: 5em;
|
& #think-controls {
|
||||||
}
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
.file-path {
|
margin: 0;
|
||||||
font-family: monospace;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editor-size-input + output {
|
||||||
.think-editor {
|
width: 5em;
|
||||||
display: flex;
|
}
|
||||||
gap: var(--spacing);
|
|
||||||
height: 100%;
|
.file-path {
|
||||||
|
font-family: monospace;
|
||||||
& > #main-nav > nav {
|
}
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--spacing);
|
.think-editor {
|
||||||
|
display: grid;
|
||||||
& #file-tree {
|
gap: var(--spacing);
|
||||||
margin: 0;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
flex-grow: 1;
|
--col-1-width: auto;
|
||||||
|
grid-template:
|
||||||
& > li {
|
"nav editor preview" min-content
|
||||||
margin-top: var(--half-spacing);
|
"log editor preview" 1fr
|
||||||
|
/ var(--col-1-width) var(--editor-size) var(--preview-size)
|
||||||
& > a {
|
;
|
||||||
text-decoration: none;
|
|
||||||
}
|
&:has(#main-nav[open], #log[open]) {
|
||||||
}
|
--col-1-width: 20em;
|
||||||
|
}
|
||||||
& .dir {
|
|
||||||
font-weight: bold;
|
& > #main-nav {
|
||||||
}
|
grid-area: nav;
|
||||||
|
}
|
||||||
& .dir + .file {
|
& > #log {
|
||||||
margin-top: var(--spacing);
|
grid-area: log;
|
||||||
}
|
width: 100%;
|
||||||
}
|
overflow: auto;
|
||||||
|
}
|
||||||
& form {
|
|
||||||
display: grid;
|
& .dragging {
|
||||||
grid-template-columns: 1fr 6em;
|
background: red;
|
||||||
align-content: start;
|
}
|
||||||
align-items: start;
|
|
||||||
}
|
& summary {
|
||||||
|
background: #eee;
|
||||||
& #make-log {
|
}
|
||||||
& > pre {
|
|
||||||
max-width: 20em;
|
& > #main-nav > nav {
|
||||||
overflow: auto;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
}
|
gap: var(--spacing);
|
||||||
}
|
|
||||||
|
& #file-tree {
|
||||||
& #log[open] {
|
margin: 0;
|
||||||
width: 80ch;
|
overflow: auto;
|
||||||
overflow: auto;
|
flex-grow: 1;
|
||||||
}
|
|
||||||
|
& > li {
|
||||||
& #editor {
|
margin-top: var(--half-spacing);
|
||||||
overflow: hidden;
|
|
||||||
flex-grow: 1;
|
& > a {
|
||||||
flex-basis: var(--editor-size);
|
text-decoration: none;
|
||||||
max-height: 85vh;
|
}
|
||||||
|
}
|
||||||
& #editor-controls {
|
|
||||||
display: flex;
|
& .dir {
|
||||||
gap: var(--spacing);
|
font-weight: bold;
|
||||||
justify-content: space-between;
|
}
|
||||||
|
|
||||||
& > details {
|
& .dir + .file {
|
||||||
text-align: right;
|
margin-top: var(--spacing);
|
||||||
|
}
|
||||||
& > summary {
|
}
|
||||||
user-select: none;
|
|
||||||
}
|
& form {
|
||||||
|
display: grid;
|
||||||
& button {
|
grid-template-columns: 1fr 6em;
|
||||||
margin: var(--half-spacing) 0;
|
align-content: start;
|
||||||
}
|
align-items: start;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
& #make-log {
|
||||||
& #code-editor {
|
& > pre {
|
||||||
display: block;
|
max-width: 20em;
|
||||||
max-width: 50vw;
|
overflow: auto;
|
||||||
padding-bottom: 10em;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& #preview {
|
& #editor {
|
||||||
display: flex;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
flex-grow: 1;
|
||||||
|
flex-basis: var(--editor-size);
|
||||||
&[open] {
|
max-height: 85vh;
|
||||||
flex-grow: 1;
|
grid-area: editor;
|
||||||
flex-shrink: 1;
|
|
||||||
flex-basis: calc(100% - var(--editor-size));
|
& #editor-controls {
|
||||||
}
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
& > summary {
|
justify-content: space-between;
|
||||||
text-align: right;
|
|
||||||
}
|
& > details {
|
||||||
|
text-align: right;
|
||||||
& > iframe {
|
|
||||||
width: 100%;
|
& > summary {
|
||||||
height: 100%;
|
user-select: none;
|
||||||
border: none;
|
}
|
||||||
}
|
|
||||||
|
& button {
|
||||||
&[closed] > iframe {
|
margin: var(--half-spacing) 0;
|
||||||
display: none;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
overflow: hidden;
|
|
||||||
}
|
& #code-editor {
|
||||||
|
display: block;
|
||||||
#file-form {
|
max-width: 50vw;
|
||||||
overflow: auto;
|
padding-bottom: 10em;
|
||||||
max-height: 100%;
|
}
|
||||||
max-width: 100%;
|
}
|
||||||
}
|
|
||||||
@media (max-width: 100ch) {
|
& #preview {
|
||||||
html {
|
display: flex;
|
||||||
font-size: min(3vw, 16px);
|
flex-direction: column;
|
||||||
}
|
grid-area: preview;
|
||||||
.think-editor {
|
|
||||||
flex-direction: column;
|
&[open] {
|
||||||
overflow: visible;
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
& > * ~ * {
|
flex-basis: calc(100% - var(--editor-size));
|
||||||
border-top: medium solid #888;
|
}
|
||||||
margin-top: var(--spacing);
|
|
||||||
padding-top: var(--spacing);
|
& > summary {
|
||||||
}
|
text-align: right;
|
||||||
|
}
|
||||||
& nav {
|
|
||||||
flex-direction: row;
|
& > iframe {
|
||||||
flex-wrap: wrap;
|
width: 100%;
|
||||||
|
height: calc(100% - 3em);
|
||||||
& form {
|
border: none;
|
||||||
flex-grow: 1;
|
}
|
||||||
}
|
|
||||||
|
&[closed] > iframe {
|
||||||
& #file-tree {
|
display: none;
|
||||||
max-height: 7em;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& #editor {
|
#file-form {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
& #code-editor {
|
max-height: 100%;
|
||||||
max-width: none;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
@media (max-width: 100ch) {
|
||||||
|
html {
|
||||||
& #preview {
|
font-size: min(3vw, 16px);
|
||||||
height: 100vh;
|
}
|
||||||
}
|
.think-editor {
|
||||||
}
|
overflow: visible;
|
||||||
|
grid-template:
|
||||||
|
"nav"
|
||||||
|
"log"
|
||||||
|
"editor"
|
||||||
|
"preview"
|
||||||
|
;
|
||||||
|
|
||||||
|
& > * ~ * {
|
||||||
|
border-top: medium solid #888;
|
||||||
|
margin-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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -18,6 +18,44 @@ body {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
body.login {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100svh;
|
||||||
|
margin: 0;
|
||||||
|
padding: var(--spacing);
|
||||||
|
|
||||||
|
& header {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
& form {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing);
|
||||||
|
|
||||||
|
& div {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
gap: var(--spacing);
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& label {
|
||||||
|
grid-column: 1;
|
||||||
|
justify-self: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
grid-template-rows: 2em 2em 2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
body.index {
|
body.index {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
|
@ -32,7 +70,7 @@ body.index {
|
||||||
gap: var(--double-spacing);
|
gap: var(--double-spacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
& #thinks-list {
|
& .thinks-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--double-spacing);
|
gap: var(--double-spacing);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
{% block body_class %}login {{block.super}}{% endblock %}
|
{% block body_class %}login {{block.super}}{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<h1>Login</h1>
|
<h1>Log in</h1>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
@ -15,9 +15,9 @@
|
||||||
{% if next %}
|
{% if next %}
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<p>Your account doesn't have access to this page. To proceed,
|
<p>Your account doesn't have access to this page. To proceed,
|
||||||
please login with an account that has access.</p>
|
please log in with an account that has access.</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Please login to see this page.</p>
|
<p>Please log in to see this page.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,32 @@
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="recent">
|
||||||
|
<h2>Recent</h2>
|
||||||
|
|
||||||
|
<ul class="thinks-list">
|
||||||
|
{% for think in recent_thinks %}
|
||||||
|
<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>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="thinks">
|
<section id="thinks">
|
||||||
<h2>Thinks</h2>
|
<h2>Thinks</h2>
|
||||||
<p><a href="{% url 'new_think' %}">New think</a></p>
|
<p><a href="{% url 'new_think' %}">New think</a></p>
|
||||||
{% regroup thinks by category as categories %}
|
{% regroup thinks by category as categories %}
|
||||||
<ul id="thinks-list">
|
<ul class="thinks-list">
|
||||||
{% for category in categories %}
|
{% for category in categories %}
|
||||||
<li>
|
<li>
|
||||||
<details {% if not category.grouper %}open{% endif %}>
|
<details open>
|
||||||
<summary>{% if category.grouper %}{{category.grouper}}{% else %}Uncategorised{% endif %}</summary>
|
<summary>{% if category.grouper %}{{category.grouper}}{% else %}Uncategorised{% endif %}</summary>
|
||||||
<ul>
|
<ul>
|
||||||
{% for think in category.list %}
|
{% for think in category.list %}
|
||||||
|
|
|
@ -1,19 +1,30 @@
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import AccessMixin
|
||||||
from django.http import HttpResponse, JsonResponse
|
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 itertools import groupby
|
||||||
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from . import forms
|
from . import forms
|
||||||
|
from .make import ThingMaker
|
||||||
from .models import Think
|
from .models import Think
|
||||||
from .random_slug import random_slug
|
from .random_slug import random_slug
|
||||||
|
|
||||||
|
class LoginRequiredMixin(AccessMixin):
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
token = request.META.get('HTTP_AUTHORIZATION')
|
||||||
|
if token != f'Bearer {settings.API_TOKEN}':
|
||||||
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
class ThinkMixin(LoginRequiredMixin):
|
class ThinkMixin(LoginRequiredMixin):
|
||||||
model = Think
|
model = Think
|
||||||
context_object_name = 'think'
|
context_object_name = 'think'
|
||||||
|
@ -26,10 +37,19 @@ class IndexView(ThinkMixin, generic.ListView):
|
||||||
|
|
||||||
context['templates'] = Think.objects.filter(is_template=True)
|
context['templates'] = Think.objects.filter(is_template=True)
|
||||||
|
|
||||||
|
context['recent_thinks'] = Think.objects.filter(is_template=False).order_by('-creation_time')[:3]
|
||||||
|
|
||||||
context['thinks'] = sorted(Think.objects.filter(is_template=False), key=lambda t: (t.category if t.category else '', -t.creation_time.timestamp()))
|
context['thinks'] = sorted(Think.objects.filter(is_template=False), key=lambda t: (t.category if t.category else '', -t.creation_time.timestamp()))
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
if request.accepts('application/json') and not request.accepts('text/html'):
|
||||||
|
self.object_list = self.get_queryset()
|
||||||
|
context = self.get_context_data()
|
||||||
|
return JsonResponse({'templates': [t.as_json() for t in context['templates']], 'thinks': [t.as_json() for t in context['thinks']]})
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
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
|
||||||
|
@ -82,17 +102,23 @@ class ThinkView(ThinkMixin, generic.DetailView):
|
||||||
else:
|
else:
|
||||||
directory = path.parent
|
directory = path.parent
|
||||||
|
|
||||||
files = [{'name': p.name, 'path': str(p.relative_to(root)), 'is_dir': p.is_dir()} for p in directory.iterdir()]
|
if directory.exists():
|
||||||
|
files = [{'name': p.name, 'path': str(p.relative_to(root)), 'is_dir': p.is_dir()} for p in directory.iterdir()]
|
||||||
|
else:
|
||||||
|
files = []
|
||||||
|
|
||||||
if directory != root:
|
if directory != root:
|
||||||
files.insert(0, {'name': '..', 'path': str(directory.parent.relative_to(root)), 'is_dir': True})
|
files.insert(0, {'name': '..', 'path': str(directory.parent.relative_to(root)), 'is_dir': True})
|
||||||
|
|
||||||
|
files = sorted(files, key=lambda x: x['name'].lower())
|
||||||
|
|
||||||
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:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
else:
|
else:
|
||||||
content = ''
|
content = ''
|
||||||
|
|
||||||
context['think_editor_data'] = {
|
data = context['think_editor_data'] = {
|
||||||
'preview_url': think.get_static_url(),
|
'preview_url': think.get_static_url(),
|
||||||
'slug': think.slug,
|
'slug': think.slug,
|
||||||
'files': files,
|
'files': files,
|
||||||
|
@ -102,6 +128,10 @@ class ThinkView(ThinkMixin, generic.DetailView):
|
||||||
'no_preview': self.request.GET.get('no-preview') is not None,
|
'no_preview': self.request.GET.get('no-preview') is not None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if path is not None and path.suffix == '.elm':
|
||||||
|
with open('public/elm-packages.json') as f:
|
||||||
|
data['elm_packages'] = json.load(f)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
class RenameThinkView(ThinkMixin, generic.UpdateView):
|
class RenameThinkView(ThinkMixin, generic.UpdateView):
|
||||||
|
@ -138,7 +168,13 @@ class SaveFileView(ThinkMixin, generic.UpdateView):
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
self.path = form.cleaned_data['path'].relative_to(self.object.root)
|
self.path = form.cleaned_data['path'].relative_to(self.object.root)
|
||||||
return super().form_valid(form)
|
|
||||||
|
thing = form.save()
|
||||||
|
|
||||||
|
maker = ThingMaker(thing)
|
||||||
|
result = maker.make(self.path)
|
||||||
|
|
||||||
|
return JsonResponse(result or {"error": "not built"})
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.object.get_absolute_url()+'?path='+str(self.path)
|
return self.object.get_absolute_url()+'?path='+str(self.path)
|
||||||
|
|
|
@ -129,3 +129,5 @@ THINKS_DIR = Path('think_data')
|
||||||
THINKS_DIR.mkdir(parents=True, exist_ok=True)
|
THINKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
THINKS_STATIC_URL = 'https://{slug}.think.somethingorotherwhatever.com'
|
THINKS_STATIC_URL = 'https://{slug}.think.somethingorotherwhatever.com'
|
||||||
|
|
||||||
|
API_TOKEN = ''
|
Loading…
Reference in a new issue