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.
244 lines
7.8 KiB
Python
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})
|