thinkserver/thinks/views.py
Christian Lawson-Perfect d474a394f5 Jujutsu/git integration
The editor now has a button to commit changes to a Jujutsu repository,
which is automatically created if it's not present.

A git remote is automatically set up using the URL template in
settings.py. I use this with Forgejo's create-on-push feature to
automatically create repositories on my Forgejo instance.
2025-02-10 10:05:00 +00:00

244 lines
7.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"
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)
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})