thinkserver/thinks/views.py
Christian Lawson-Perfect 1b6a9956c5 Use a default path in the editor
When opening a think with no path specified, try some of the most common
paths and show the first one that exists.
2025-02-20 13:04:31 +00:00

256 lines
8 KiB
Python

from django.conf import settings
from django.contrib.auth.mixins import AccessMixin
from django.http import HttpResponse, JsonResponse
from django.shortcuts import render, redirect
from django.views import generic
from django.urls import reverse
from itertools import groupby
import json
from pathlib import Path
import shutil
import shlex
import subprocess
from . import forms
from .make import ThingMaker
from .models import Think
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):
model = Think
context_object_name = 'think'
class IndexView(ThinkMixin, generic.ListView):
template_name = 'thinks/index.html'
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['templates'] = Think.objects.filter(is_template=True)
context['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()))
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):
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"
default_paths = [
'src/App.elm',
'script.js',
'index.html',
]
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
think = self.object
root = think.root
strpath = self.request.GET.get('path')
path = think.file_path(strpath)
if path is None:
for f in self.default_paths:
if (root / f).exists():
path = root / f
break
relpath = path.relative_to(root) if path is not None else None
if path is None:
directory = root
elif path.is_dir():
directory = path
else:
directory = path.parent
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:
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():
with open(path) as f:
content = f.read()
else:
content = ''
data = context['think_editor_data'] = {
'preview_url': think.get_static_url(),
'slug': think.slug,
'files': files,
'file_path': str(relpath),
'file_content': content,
'is_dir': path is None or path.is_dir(),
'no_preview': self.request.GET.get('no-preview') is not None,
}
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
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()
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
context['categories'] = sorted(Think.objects.exclude(category=None).order_by('category').values_list('category',flat=True).distinct())
return context
class DeleteThinkView(ThinkMixin, generic.DeleteView):
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)
thing = form.save()
maker = ThingMaker(thing)
result = maker.make(self.path)
return JsonResponse(result or {"error": "not built"})
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(
['bash','-c',command],
cwd=think.root,
encoding='utf8',
capture_output=True
)
return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr})
class LogView(ThinkMixin, generic.DetailView):
template_name = 'thinks/think.html'
def get(self, *args, **kwargs):
think = self.get_object()
return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8')
class JJStatusView(ThinkMixin, generic.detail.DetailView):
def get(self, request, *args, **kwargs):
status = self.get_object().jj_controller.status()
return JsonResponse({'status': status})
class JJCommitView(ThinkMixin, generic.UpdateView):
form_class = forms.GitCommitForm
def form_valid(self, form):
message = form.cleaned_data['message']
think = form.instance
jj = think.jj_controller
res = jj.commit(message)
return JsonResponse({'ok': res.returncode == 0, 'think': think.pk, 'stdout': res.stdout, 'stderr': res.stderr})