first commit

This commit is contained in:
Christian Lawson-Perfect 2024-03-04 15:05:35 +00:00
commit 52ab7aa331
33 changed files with 33447 additions and 0 deletions

View 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 $@

View 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);

View 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
View 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
View file

@ -0,0 +1,3 @@
django
pyyaml
watchdog

0
thinks/__init__.py Normal file
View file

3
thinks/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
thinks/apps.py Normal file
View 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
View 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 = []

View file

View file

View 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()

View 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()),
],
),
]

View file

45
thinks/models.py Normal file
View 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
View 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))

File diff suppressed because it is too large Load diff

View 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;
}
}
}
}
}

View 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>

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

18
thinks/urls.py Normal file
View 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
View 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
View file

16
thinkserver/asgi.py Normal file
View 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
View 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
View 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
View 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()