first commit
This commit is contained in:
commit
52ab7aa331
33 changed files with 33447 additions and 0 deletions
13
codemirror-element/Makefile
Normal file
13
codemirror-element/Makefile
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
NODE_BIN=node_modules/.bin
|
||||||
|
|
||||||
|
DEST=../thinks/static/thinks
|
||||||
|
|
||||||
|
$(DEST)/code-editor.mjs: code-editor.bundle.mjs
|
||||||
|
cp $< $@
|
||||||
|
|
||||||
|
code-editor.terser.mjs: code-editor.bundle.mjs
|
||||||
|
$(NODE_BIN)/terser --compress --mangle -- $< > $@
|
||||||
|
|
||||||
|
code-editor.bundle.mjs: code-editor.mjs
|
||||||
|
$(NODE_BIN)/rollup $< -f es -p @rollup/plugin-node-resolve -o $@
|
||||||
|
|
73
codemirror-element/code-editor.mjs
Normal file
73
codemirror-element/code-editor.mjs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
import {basicSetup} from "codemirror";
|
||||||
|
import {EditorView, keymap} from "@codemirror/view";
|
||||||
|
import {EditorState} from "@codemirror/state";
|
||||||
|
import {python} from "@codemirror/lang-python";
|
||||||
|
import {r} from "codemirror-lang-r";
|
||||||
|
import {vim} from "@replit/codemirror-vim";
|
||||||
|
import {indentWithTab} from "@codemirror/commands";
|
||||||
|
|
||||||
|
window.EditorView = EditorView;
|
||||||
|
|
||||||
|
const languages = {
|
||||||
|
'python': python,
|
||||||
|
'r': r
|
||||||
|
}
|
||||||
|
|
||||||
|
export function codemirror_editor(language, options) {
|
||||||
|
const language_plugin = languages[language];
|
||||||
|
|
||||||
|
options = Object.assign({
|
||||||
|
extensions: [
|
||||||
|
vim(),
|
||||||
|
basicSetup,
|
||||||
|
keymap.of([indentWithTab]),
|
||||||
|
EditorView.updateListener.of(update => {
|
||||||
|
if(!options?.onChange || update.changes.desc.empty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
options.onChange(update);
|
||||||
|
})
|
||||||
|
]
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
let editor = new EditorView(options);
|
||||||
|
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class CodeEditorElement extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.language = this.getAttribute('language') || '';
|
||||||
|
const shadowRoot = this.attachShadow({mode: 'open'});
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.init_editor();
|
||||||
|
}
|
||||||
|
|
||||||
|
init_editor() {
|
||||||
|
const code = this.textContent;
|
||||||
|
const code_tag = this.shadowRoot;
|
||||||
|
|
||||||
|
this.codeMirror = codemirror_editor(
|
||||||
|
this.language,
|
||||||
|
{
|
||||||
|
doc: code,
|
||||||
|
parent: code_tag,
|
||||||
|
root: this.shadowRoot,
|
||||||
|
onChange: update => this.onChange(update)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange() {
|
||||||
|
const code = this.codeMirror.state.doc.toString();
|
||||||
|
this.value = code;
|
||||||
|
this.dispatchEvent(new CustomEvent('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("code-editor", CodeEditorElement);
|
20
codemirror-element/package.json
Normal file
20
codemirror-element/package.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "codemirror-element",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@codemirror/lang-python": "^6.1.2",
|
||||||
|
"@replit/codemirror-vim": "^6.2.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^15.0.2",
|
||||||
|
"codemirror": "^6.0.1",
|
||||||
|
"codemirror-lang-r": "^0.1.0-2",
|
||||||
|
"rollup": "^3.20.6",
|
||||||
|
"terser": "^5.17.1"
|
||||||
|
}
|
||||||
|
}
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
"""Django's command-line utility for administrative tasks."""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Run administrative tasks."""
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'thinkserver.settings')
|
||||||
|
try:
|
||||||
|
from django.core.management import execute_from_command_line
|
||||||
|
except ImportError as exc:
|
||||||
|
raise ImportError(
|
||||||
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
"available on your PYTHONPATH environment variable? Did you "
|
||||||
|
"forget to activate a virtual environment?"
|
||||||
|
) from exc
|
||||||
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
django
|
||||||
|
pyyaml
|
||||||
|
watchdog
|
0
thinks/__init__.py
Normal file
0
thinks/__init__.py
Normal file
3
thinks/admin.py
Normal file
3
thinks/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# Register your models here.
|
6
thinks/apps.py
Normal file
6
thinks/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ThinksConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'thinks'
|
126
thinks/forms.py
Normal file
126
thinks/forms.py
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
from django import forms
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .models import Think
|
||||||
|
|
||||||
|
class CreateThinkForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = ['slug']
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
print("SAVE", commit)
|
||||||
|
instance = super().save(commit)
|
||||||
|
if commit:
|
||||||
|
instance.root.mkdir(exist_ok=True,parents=True)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
class RemixThinkForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
class RenameThinkForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = ['slug']
|
||||||
|
|
||||||
|
def __init__(self, *args, instance=None, **kwargs):
|
||||||
|
self.original_root = None if instance is None else instance.root
|
||||||
|
super().__init__(*args, instance=instance, **kwargs)
|
||||||
|
|
||||||
|
def clean_slug(self):
|
||||||
|
slug = self.cleaned_data['slug']
|
||||||
|
root = settings.THINKS_DIR / slug
|
||||||
|
if root.exists() and slug != self.instance.slug:
|
||||||
|
raise forms.ValidationError("A think with the slug {slug} already exists")
|
||||||
|
|
||||||
|
return slug
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
slug = self.cleaned_data['slug']
|
||||||
|
root = settings.THINKS_DIR / slug
|
||||||
|
|
||||||
|
self.original_root.rename(root)
|
||||||
|
|
||||||
|
instance = super().save(commit)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
class SaveFileForm(forms.ModelForm):
|
||||||
|
path = forms.CharField()
|
||||||
|
content = forms.CharField(required=False, widget=forms.Textarea)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
def clean_path(self):
|
||||||
|
return self.instance.file_path(self.cleaned_data['path'])
|
||||||
|
|
||||||
|
def save(self, commit=False):
|
||||||
|
path = self.cleaned_data['path']
|
||||||
|
content = self.cleaned_data['content']
|
||||||
|
|
||||||
|
if not path.parent.exists():
|
||||||
|
path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
|
with open(path, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
|
||||||
|
return super().save(commit)
|
||||||
|
|
||||||
|
class RenameFileForm(forms.ModelForm):
|
||||||
|
path = forms.CharField()
|
||||||
|
newpath = forms.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
def clean_path(self):
|
||||||
|
path = self.instance.file_path(self.cleaned_data['path'])
|
||||||
|
return path
|
||||||
|
|
||||||
|
def clean_newpath(self):
|
||||||
|
path = self.instance.file_path(self.cleaned_data['newpath'])
|
||||||
|
if path.exists():
|
||||||
|
raise forms.ValidationError("The file {path} already exists")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def save(self, commit=False):
|
||||||
|
oldpath = self.cleaned_data['path']
|
||||||
|
newpath = self.cleaned_data['newpath']
|
||||||
|
|
||||||
|
print(oldpath,">",newpath)
|
||||||
|
|
||||||
|
oldpath.rename(newpath)
|
||||||
|
|
||||||
|
return super().save(commit)
|
||||||
|
|
||||||
|
class DeleteFileForm(forms.ModelForm):
|
||||||
|
path = forms.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = []
|
||||||
|
|
||||||
|
def clean_path(self):
|
||||||
|
path = self.instance.file_path(self.cleaned_data['path'])
|
||||||
|
if path == self.instance.root:
|
||||||
|
raise forms.ValidationError("Can't delete the think's root")
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
|
def save(self, commit=False):
|
||||||
|
path = self.cleaned_data['path']
|
||||||
|
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
return super().save(commit)
|
||||||
|
|
||||||
|
class RunCommandForm(forms.ModelForm):
|
||||||
|
command = forms.CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Think
|
||||||
|
fields = []
|
0
thinks/management/__init__.py
Normal file
0
thinks/management/__init__.py
Normal file
0
thinks/management/commands/__init__.py
Normal file
0
thinks/management/commands/__init__.py
Normal file
135
thinks/management/commands/watchmake.py
Normal file
135
thinks/management/commands/watchmake.py
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
"""
|
||||||
|
Requires the ``watchdog`` package.
|
||||||
|
|
||||||
|
Watch the current directory and its children for changes to files, and run ``make`` when certain files are changed.
|
||||||
|
|
||||||
|
Can be configured by a .watchmakerc file, containing settings in YAML format:
|
||||||
|
|
||||||
|
path = <the path to watch for changes> (default: .)
|
||||||
|
default_make: <a list of `make` targets to run> (default: empty list, so the first target in the Makefile)
|
||||||
|
extensions: <a list of file extensions that should trigger a ``make`` run> (default: '.js')
|
||||||
|
"""
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.conf import settings
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from watchdog.observers import Observer
|
||||||
|
from watchdog.events import FileSystemEventHandler
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
class MakeHandler(FileSystemEventHandler):
|
||||||
|
|
||||||
|
last_time = None
|
||||||
|
gap = 2
|
||||||
|
|
||||||
|
extensions = None
|
||||||
|
|
||||||
|
default_config = {
|
||||||
|
'path': '.',
|
||||||
|
'default_make': [],
|
||||||
|
'extensions': ['.js'],
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.configs = {}
|
||||||
|
|
||||||
|
self.root = root.resolve()
|
||||||
|
|
||||||
|
def get_think_root(self, d):
|
||||||
|
while d.parent != self.root:
|
||||||
|
d = d.parent
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def get_config(self, p):
|
||||||
|
d = self.get_think_root(p)
|
||||||
|
|
||||||
|
if d not in self.configs:
|
||||||
|
self.load_config(d)
|
||||||
|
|
||||||
|
return self.configs[d]
|
||||||
|
|
||||||
|
def load_config(self, d):
|
||||||
|
print(f"Fetch config for {d}")
|
||||||
|
|
||||||
|
config_path = d / '.watchmakerc'
|
||||||
|
|
||||||
|
config = deepcopy(self.default_config)
|
||||||
|
if config_path.exists():
|
||||||
|
with open(config_path) as f:
|
||||||
|
config.update(yaml.load(f.read(),Loader=yaml.SafeLoader))
|
||||||
|
|
||||||
|
self.configs[d] = config
|
||||||
|
|
||||||
|
def on_modified(self, event):
|
||||||
|
root = self.root
|
||||||
|
|
||||||
|
if event.is_directory:
|
||||||
|
return
|
||||||
|
|
||||||
|
t = datetime.now()
|
||||||
|
|
||||||
|
src_path = Path(event.src_path).resolve()
|
||||||
|
|
||||||
|
if self.extensions is None or src_path.suffix in self.extensions:
|
||||||
|
if not src_path.is_relative_to(root.resolve()):
|
||||||
|
return
|
||||||
|
|
||||||
|
print('{} modified at {}'.format(root / src_path.relative_to(root.resolve()),t))
|
||||||
|
|
||||||
|
if src_path.name == '.watchmakerc' or (self.last_time is None or (t-self.last_time).seconds > self.gap):
|
||||||
|
self.run(src_path)
|
||||||
|
|
||||||
|
self.last_time = t
|
||||||
|
|
||||||
|
def run(self, p):
|
||||||
|
d = self.get_think_root(p)
|
||||||
|
print(f"Run for {d}")
|
||||||
|
print(p.name)
|
||||||
|
if p.name == '.watchmakerc':
|
||||||
|
self.load_config(d)
|
||||||
|
config = self.get_config(p)
|
||||||
|
print(config)
|
||||||
|
|
||||||
|
if (d / 'Makefile').exists():
|
||||||
|
print("Making")
|
||||||
|
command = ['make'] + config.get('default_make', [])
|
||||||
|
res = subprocess.run(command, cwd=d, capture_output=True, encoding='utf-8')
|
||||||
|
with open(d / '.make.log', 'w') as f:
|
||||||
|
f.write(res.stdout)
|
||||||
|
f.write(res.stderr)
|
||||||
|
else:
|
||||||
|
print("No make")
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
self.rootpath = settings.THINKS_DIR
|
||||||
|
|
||||||
|
self.run()
|
||||||
|
|
||||||
|
def run(self,targets=None):
|
||||||
|
print(f"Watching {self.rootpath}")
|
||||||
|
|
||||||
|
event_handler = MakeHandler(self.rootpath)
|
||||||
|
|
||||||
|
observer = Observer()
|
||||||
|
observer.schedule(event_handler,str(self.rootpath),recursive=True)
|
||||||
|
observer.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
observer.stop()
|
||||||
|
|
||||||
|
observer.join()
|
||||||
|
|
21
thinks/migrations/0001_initial.py
Normal file
21
thinks/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 5.0.2 on 2024-03-04 14:43
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Think',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('slug', models.SlugField()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
0
thinks/migrations/__init__.py
Normal file
0
thinks/migrations/__init__.py
Normal file
45
thinks/models.py
Normal file
45
thinks/models.py
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import models
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
THINKS_DIR = settings.THINKS_DIR
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
class Think(models.Model):
|
||||||
|
|
||||||
|
slug = models.SlugField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('slug',)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root(self):
|
||||||
|
return (THINKS_DIR / self.slug).resolve()
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('think', kwargs={'slug': self.slug})
|
||||||
|
|
||||||
|
def get_static_url(self):
|
||||||
|
return settings.THINKS_STATIC_URL.format(slug=self.slug)
|
||||||
|
|
||||||
|
def files(self):
|
||||||
|
for f in self.root.iterdir():
|
||||||
|
yield f.relative_to(self.root)
|
||||||
|
|
||||||
|
def file_path(self, relpath):
|
||||||
|
if relpath is None:
|
||||||
|
return None
|
||||||
|
path = (self.root / relpath).resolve()
|
||||||
|
if not path.is_relative_to(self.root):
|
||||||
|
raise Exception(f"Bad path {path}")
|
||||||
|
return path
|
||||||
|
|
||||||
|
def get_readme(self):
|
||||||
|
readme_path = self.file_path('README')
|
||||||
|
for suffix in ['', '.md', '.rst', '.txt']:
|
||||||
|
p = readme_path.with_suffix(suffix)
|
||||||
|
if p.exists():
|
||||||
|
with open(p) as f:
|
||||||
|
return f.read()
|
||||||
|
|
||||||
|
return None
|
6
thinks/random_slug.py
Normal file
6
thinks/random_slug.py
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
from random import choice
|
||||||
|
|
||||||
|
words = 'avocado biscuit chocolate doughnut eclaire fudge goulash haddock icing juice koala lemon melon nut'.split(' ')
|
||||||
|
|
||||||
|
def random_slug():
|
||||||
|
return '-'.join(choice(words) for i in range(3))
|
32273
thinks/static/thinks/code-editor.mjs
Normal file
32273
thinks/static/thinks/code-editor.mjs
Normal file
File diff suppressed because it is too large
Load diff
119
thinks/static/thinks/thinks.css
Normal file
119
thinks/static/thinks/thinks.css
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
:root {
|
||||||
|
--spacing: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body > header {
|
||||||
|
padding: var(--spacing);
|
||||||
|
& h1 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.index {
|
||||||
|
& #thinks-list {
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing);
|
||||||
|
|
||||||
|
& > .think {
|
||||||
|
& .readme {
|
||||||
|
max-width: 80ch;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.thing-editor {
|
||||||
|
margin: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
"header" auto
|
||||||
|
"main" 1fr
|
||||||
|
;
|
||||||
|
gap: var(--spacing);
|
||||||
|
/*! overflow: hidden; */
|
||||||
|
|
||||||
|
& main {
|
||||||
|
display: grid;
|
||||||
|
gap: var(--spacing);
|
||||||
|
grid-template: "nav editor preview" / auto 3fr 2fr;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& > nav {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing);
|
||||||
|
|
||||||
|
& #file-tree {
|
||||||
|
margin: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
& form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 6em;
|
||||||
|
align-content: start;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #editor {
|
||||||
|
& #editor-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing);
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
& #code-editor {
|
||||||
|
display: block;
|
||||||
|
max-width: 50vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& #preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
& > iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#file-form {
|
||||||
|
overflow: auto;
|
||||||
|
/*! max-height: 100%; */
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
@media (max-width: 100ch) {
|
||||||
|
html {
|
||||||
|
font-size: min(3vw, 16px);
|
||||||
|
}
|
||||||
|
body.thing-editor {
|
||||||
|
& main {
|
||||||
|
grid-template:
|
||||||
|
"nav" min-content "editor" 40vh "preview";
|
||||||
|
|
||||||
|
& nav {
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
& form {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
28
thinks/templates/thinks/base.html
Normal file
28
thinks/templates/thinks/base.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% load static %}
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<title>{% block title %}Thinks{% endblock %}</title>
|
||||||
|
|
||||||
|
{% block stylesheets %}
|
||||||
|
<link rel="stylesheet" href="{% static "thinks/thinks.css" %}">
|
||||||
|
{% endblock stylesheets %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="module" src="{% static "thinks/code-editor.mjs" %}"></script>
|
||||||
|
{% endblock scripts %}
|
||||||
|
</head>
|
||||||
|
<body class="{% block body_class %}{% endblock %}">
|
||||||
|
{% block body %}
|
||||||
|
<header>
|
||||||
|
{% block header %}{% endblock %}
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{% block main %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
{% endblock body %}
|
||||||
|
</body>
|
||||||
|
</html>
|
15
thinks/templates/thinks/delete.html
Normal file
15
thinks/templates/thinks/delete.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Delete {{think.slug}}</h1>
|
||||||
|
{% endblock header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<form method="post" >
|
||||||
|
{{form}}
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock main %}
|
24
thinks/templates/thinks/index.html
Normal file
24
thinks/templates/thinks/index.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block body_class %}index{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Thinks</h1>
|
||||||
|
{% endblock header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<ul id="thinks-list">
|
||||||
|
{% for think in object_list %}
|
||||||
|
<li class="think">
|
||||||
|
<a href="{% url 'think' think.slug %}">{{think.slug}}</a>
|
||||||
|
{% with readme=think.get_readme %}
|
||||||
|
{% if readme %}
|
||||||
|
<pre class="readme">{{readme}}
|
||||||
|
</pre>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<a href="{% url 'new_think' %}">New think</a>
|
||||||
|
{% endblock main %}
|
15
thinks/templates/thinks/new.html
Normal file
15
thinks/templates/thinks/new.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>New think</h1>
|
||||||
|
{% endblock header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<form method="post" >
|
||||||
|
{{form}}
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock main %}
|
15
thinks/templates/thinks/remix.html
Normal file
15
thinks/templates/thinks/remix.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>Remix {{think.slug}}</h1>
|
||||||
|
{% endblock header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<form method="post" >
|
||||||
|
{{form}}
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Remix</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock main %}
|
15
thinks/templates/thinks/rename.html
Normal file
15
thinks/templates/thinks/rename.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<h1>{{think.slug}}</h1>
|
||||||
|
{% endblock header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
<form method="post" >
|
||||||
|
{{form}}
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit">Rename</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock main %}
|
106
thinks/templates/thinks/think.html
Normal file
106
thinks/templates/thinks/think.html
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
{% extends "thinks/base.html" %}
|
||||||
|
|
||||||
|
{% block body_class %}thing-editor {{block.super}}{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}
|
||||||
|
<a href="/">thinks</a>
|
||||||
|
<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 %}
|
||||||
|
|
||||||
|
{% 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 %}
|
3
thinks/tests.py
Normal file
3
thinks/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
18
thinks/urls.py
Normal file
18
thinks/urls.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path
|
||||||
|
from .views import *
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', IndexView.as_view(), name='index'),
|
||||||
|
path('think/<slug:slug>', ThinkView.as_view(), name='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>/file/<path:path>', ReadFileView.as_view(), name='read_file'),
|
||||||
|
path('think/<slug:slug>/save-file', SaveFileView.as_view(), name='save_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>/run-command', RunCommandView.as_view(), name='run_command'),
|
||||||
|
path('new', CreateThinkView.as_view(), name='new_think'),
|
||||||
|
path('new/<slug:slug>', RemixThinkView.as_view(), name='remix_think'),
|
||||||
|
]
|
||||||
|
|
159
thinks/views.py
Normal file
159
thinks/views.py
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import HttpResponse, JsonResponse
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.views import generic
|
||||||
|
from django.urls import reverse
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from . import forms
|
||||||
|
from .models import Think
|
||||||
|
from .random_slug import random_slug
|
||||||
|
|
||||||
|
class ThinkMixin:
|
||||||
|
model = Think
|
||||||
|
context_object_name = 'think'
|
||||||
|
|
||||||
|
class IndexView(ThinkMixin, generic.ListView):
|
||||||
|
template_name = 'thinks/index.html'
|
||||||
|
|
||||||
|
class CreateThinkView(ThinkMixin, generic.CreateView):
|
||||||
|
template_name = 'thinks/new.html'
|
||||||
|
form_class = forms.CreateThinkForm
|
||||||
|
|
||||||
|
class RemixThinkView(ThinkMixin, generic.UpdateView):
|
||||||
|
template_name = 'thinks/remix.html'
|
||||||
|
form_class = forms.RemixThinkForm
|
||||||
|
|
||||||
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
if 'referer' in request.headers:
|
||||||
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return self.do_remix()
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
return self.do_remix()
|
||||||
|
|
||||||
|
def do_remix(self):
|
||||||
|
slug = self.kwargs['slug']
|
||||||
|
nslug = random_slug()
|
||||||
|
while (settings.THINKS_DIR / nslug).exists():
|
||||||
|
nslug = random_slug()
|
||||||
|
source = Think.objects.get(slug=slug)
|
||||||
|
think = Think.objects.create(slug=nslug)
|
||||||
|
shutil.copytree(source.root, think.root)
|
||||||
|
|
||||||
|
return redirect(think.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
|
class ThinkView(ThinkMixin, generic.DetailView):
|
||||||
|
template_name = 'thinks/think.html'
|
||||||
|
|
||||||
|
def get_context_data(self, *args, **kwargs):
|
||||||
|
context = super().get_context_data(*args, **kwargs)
|
||||||
|
|
||||||
|
think = self.object
|
||||||
|
|
||||||
|
strpath = self.request.GET.get('path')
|
||||||
|
|
||||||
|
path = think.file_path(strpath)
|
||||||
|
|
||||||
|
relpath = path.relative_to(think.root) if path is not None else None
|
||||||
|
|
||||||
|
if path is None:
|
||||||
|
directory = think.root
|
||||||
|
elif path.is_dir():
|
||||||
|
directory = path
|
||||||
|
else:
|
||||||
|
directory = path.parent
|
||||||
|
|
||||||
|
files = [(p.name, p.relative_to(think.root)) for p in directory.iterdir()]
|
||||||
|
if directory != think.root:
|
||||||
|
files.insert(0, ('..', directory.parent.relative_to(think.root)))
|
||||||
|
context['files'] = files
|
||||||
|
|
||||||
|
context['directory'] = directory
|
||||||
|
|
||||||
|
context['path'] = relpath
|
||||||
|
|
||||||
|
if path is not None and path.is_file():
|
||||||
|
with open(path) as f:
|
||||||
|
content = f.read()
|
||||||
|
else:
|
||||||
|
content = ''
|
||||||
|
|
||||||
|
context['content'] = content
|
||||||
|
|
||||||
|
context['file_form'] = forms.SaveFileForm(instance=think, initial={'content': content, 'path': relpath})
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
class RenameThinkView(ThinkMixin, generic.UpdateView):
|
||||||
|
form_class = forms.RenameThinkForm
|
||||||
|
template_name = 'thinks/rename.html'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.object.get_absolute_url()
|
||||||
|
|
||||||
|
class DeleteThinkView(ThinkMixin, generic.DeleteView):
|
||||||
|
template_name = 'thinks/delete.html'
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return reverse('index')
|
||||||
|
|
||||||
|
class ReadFileView(ThinkMixin, generic.DetailView):
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
think = self.get_object()
|
||||||
|
relpath = self.kwargs['path']
|
||||||
|
path = think.root / relpath
|
||||||
|
print(path)
|
||||||
|
|
||||||
|
|
||||||
|
class SaveFileView(ThinkMixin, generic.UpdateView):
|
||||||
|
form_class = forms.SaveFileForm
|
||||||
|
template_name = 'thinks/save_file.html'
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.path = form.cleaned_data['path'].relative_to(self.object.root)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.object.get_absolute_url()+'?path='+str(self.path)
|
||||||
|
|
||||||
|
class RenameFileView(ThinkMixin, generic.UpdateView):
|
||||||
|
form_class = forms.RenameFileForm
|
||||||
|
template_name = 'thinks/rename_file.html'
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.path = form.cleaned_data['newpath'].relative_to(self.object.root)
|
||||||
|
print(form)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.object.get_absolute_url()+'?path='+str(self.path)
|
||||||
|
|
||||||
|
class DeleteFileView(ThinkMixin, generic.UpdateView):
|
||||||
|
form_class = forms.DeleteFileForm
|
||||||
|
template_name = 'thinks/delete_file.html'
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
self.path = form.cleaned_data['path'].relative_to(self.object.root)
|
||||||
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
return self.object.get_absolute_url()+'?path='+str(self.path.parent)
|
||||||
|
|
||||||
|
class RunCommandView(ThinkMixin, generic.UpdateView):
|
||||||
|
form_class = forms.RunCommandForm
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
think = self.object
|
||||||
|
command = form.cleaned_data['command']
|
||||||
|
res = subprocess.run(
|
||||||
|
command.split(' '),
|
||||||
|
cwd=think.root,
|
||||||
|
encoding='utf8',
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr})
|
0
thinkserver/__init__.py
Normal file
0
thinkserver/__init__.py
Normal file
16
thinkserver/asgi.py
Normal file
16
thinkserver/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
ASGI config for thinkserver project.
|
||||||
|
|
||||||
|
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'thinkserver.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
129
thinkserver/settings.py
Normal file
129
thinkserver/settings.py
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
"""
|
||||||
|
Django settings for thinkserver project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 5.0.2.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# 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#@'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = ['*']
|
||||||
|
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'thinks',
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'thinkserver.urls'
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||||
|
'DIRS': [],
|
||||||
|
'APP_DIRS': True,
|
||||||
|
'OPTIONS': {
|
||||||
|
'context_processors': [
|
||||||
|
'django.template.context_processors.debug',
|
||||||
|
'django.template.context_processors.request',
|
||||||
|
'django.contrib.auth.context_processors.auth',
|
||||||
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
WSGI_APPLICATION = 'thinkserver.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
USE_I18N = True
|
||||||
|
|
||||||
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = 'static/'
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
THINKS_DIR = Path('think_data')
|
||||||
|
THINKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
THINKS_STATIC_URL = 'http://{slug}.thinks.localhost'
|
23
thinkserver/urls.py
Normal file
23
thinkserver/urls.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
"""
|
||||||
|
URL configuration for thinkserver project.
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/5.0/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path("", include('thinks.urls')),
|
||||||
|
]
|
16
thinkserver/wsgi.py
Normal file
16
thinkserver/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
"""
|
||||||
|
WSGI config for thinkserver project.
|
||||||
|
|
||||||
|
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'thinkserver.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
Loading…
Reference in a new issue