add a URL to upload files

This commit is contained in:
Christian Lawson-Perfect 2025-03-18 15:52:59 +00:00
parent d061110d88
commit dcdb3d87be
3 changed files with 91 additions and 8 deletions

View file

@ -3,6 +3,7 @@ from django.conf import settings
from .models import Think from .models import Think
class CreateThinkForm(forms.ModelForm): class CreateThinkForm(forms.ModelForm):
class Meta: class Meta:
model = Think model = Think
@ -15,11 +16,13 @@ class CreateThinkForm(forms.ModelForm):
instance.root.mkdir(exist_ok=True,parents=True) instance.root.mkdir(exist_ok=True,parents=True)
return instance return instance
class RemixThinkForm(forms.ModelForm): class RemixThinkForm(forms.ModelForm):
class Meta: class Meta:
model = Think model = Think
fields = [] fields = []
class RenameThinkForm(forms.ModelForm): class RenameThinkForm(forms.ModelForm):
class Meta: class Meta:
model = Think model = Think
@ -49,6 +52,7 @@ class RenameThinkForm(forms.ModelForm):
instance = super().save(commit) instance = super().save(commit)
return instance return instance
class SaveFileForm(forms.ModelForm): class SaveFileForm(forms.ModelForm):
path = forms.CharField() path = forms.CharField()
content = forms.CharField(required=False, widget=forms.Textarea) content = forms.CharField(required=False, widget=forms.Textarea)
@ -72,6 +76,7 @@ class SaveFileForm(forms.ModelForm):
return super().save(commit) return super().save(commit)
class RenameFileForm(forms.ModelForm): class RenameFileForm(forms.ModelForm):
path = forms.CharField() path = forms.CharField()
newpath = forms.CharField() newpath = forms.CharField()
@ -100,6 +105,7 @@ class RenameFileForm(forms.ModelForm):
return super().save(commit) return super().save(commit)
class DeleteFileForm(forms.ModelForm): class DeleteFileForm(forms.ModelForm):
path = forms.CharField() path = forms.CharField()
@ -121,6 +127,7 @@ class DeleteFileForm(forms.ModelForm):
return super().save(commit) return super().save(commit)
class RunCommandForm(forms.ModelForm): class RunCommandForm(forms.ModelForm):
command = forms.CharField() command = forms.CharField()
@ -128,9 +135,51 @@ class RunCommandForm(forms.ModelForm):
model = Think model = Think
fields = [] fields = []
class GitCommitForm(forms.ModelForm): class GitCommitForm(forms.ModelForm):
message = forms.CharField() message = forms.CharField()
class Meta: class Meta:
model = Think model = Think
fields = [] fields = []
class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True
class MultipleFileField(forms.FileField):
def __init__(self, *args, **kwargs):
kwargs.setdefault("widget", MultipleFileInput())
super().__init__(*args, **kwargs)
def clean(self, data, initial=None):
single_file_clean = super().clean
if isinstance(data, (list, tuple)):
result = [single_file_clean(d, initial) for d in data]
else:
result = [single_file_clean(data, initial)]
return result
class UploadFileForm(forms.ModelForm):
files = MultipleFileField()
class Meta:
model = Think
fields = []
def save(self, commit=False):
think = self.instance
files = self.cleaned_data['files']
for uf in files:
path = think.file_path(uf.name)
print(uf, path)
with open(path, 'wb') as df:
for chunk in uf.chunks():
df.write(chunk)
return super().save(commit)

View file

@ -12,6 +12,7 @@ urlpatterns = [
path('think/<slug:slug>/rename-file', RenameFileView.as_view(), name='rename_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>/delete-file', DeleteFileView.as_view(), name='delete_file'),
path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'), path('think/<slug:slug>/run-command', RunCommandView.as_view(), name='run_command'),
path('think/<slug:slug>/upload-files', UploadFileView.as_view(), name='upload_files'),
path('think/<slug:slug>/log', LogView.as_view(), name='log'), path('think/<slug:slug>/log', LogView.as_view(), name='log'),
path('think/<slug:slug>/jj/status', JJStatusView.as_view(), name='jj_status'), path('think/<slug:slug>/jj/status', JJStatusView.as_view(), name='jj_status'),
path('think/<slug:slug>/jj/commit', JJCommitView.as_view(), name='jj_commit'), path('think/<slug:slug>/jj/commit', JJCommitView.as_view(), name='jj_commit'),

View file

@ -6,6 +6,7 @@ from django.views import generic
from django.urls import reverse from django.urls import reverse
from itertools import groupby from itertools import groupby
import json import json
import mimetypes
from pathlib import Path from pathlib import Path
import shutil import shutil
import shlex import shlex
@ -16,6 +17,7 @@ from .make import ThingMaker
from .models import Think from .models import Think
from .random_slug import random_slug from .random_slug import random_slug
class LoginRequiredMixin(AccessMixin): class LoginRequiredMixin(AccessMixin):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated: if not request.user.is_authenticated:
@ -25,10 +27,12 @@ class LoginRequiredMixin(AccessMixin):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
class ThinkMixin(LoginRequiredMixin): class ThinkMixin(LoginRequiredMixin):
model = Think model = Think
context_object_name = 'think' context_object_name = 'think'
class IndexView(ThinkMixin, generic.ListView): class IndexView(ThinkMixin, generic.ListView):
template_name = 'thinks/index.html' template_name = 'thinks/index.html'
@ -50,10 +54,12 @@ class IndexView(ThinkMixin, generic.ListView):
return JsonResponse({'templates': [t.as_json() for t in context['templates']], 'thinks': [t.as_json() for t in context['thinks']]}) 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) return super().get(request, *args, **kwargs)
class CreateThinkView(ThinkMixin, generic.CreateView): class CreateThinkView(ThinkMixin, generic.CreateView):
template_name = 'thinks/new.html' template_name = 'thinks/new.html'
form_class = forms.CreateThinkForm form_class = forms.CreateThinkForm
class RemixThinkView(ThinkMixin, generic.UpdateView): class RemixThinkView(ThinkMixin, generic.UpdateView):
template_name = 'thinks/remix.html' template_name = 'thinks/remix.html'
form_class = forms.RemixThinkForm form_class = forms.RemixThinkForm
@ -79,6 +85,7 @@ class RemixThinkView(ThinkMixin, generic.UpdateView):
return redirect(think.get_absolute_url()) return redirect(think.get_absolute_url())
class ThinkView(ThinkMixin, generic.DetailView): class ThinkView(ThinkMixin, generic.DetailView):
template_name = "thinks/think.html" template_name = "thinks/think.html"
@ -124,28 +131,37 @@ class ThinkView(ThinkMixin, generic.DetailView):
files = sorted(files, key=lambda x: x['name'].lower()) 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'] = { data = context['think_editor_data'] = {
'preview_url': think.get_static_url(), 'preview_url': think.get_static_url(),
'slug': think.slug, 'slug': think.slug,
'files': files, 'files': files,
'file_path': str(relpath), 'file_path': str(relpath),
'file_content': content,
'is_dir': path is None or path.is_dir(), 'is_dir': path is None or path.is_dir(),
'no_preview': self.request.GET.get('no-preview') is not None, 'no_preview': self.request.GET.get('no-preview') is not None,
} }
if path is not None and path.is_file():
mime_types = {
'.elm': 'text/application+elm',
}
mime_type, encoding = mimetypes.guess_type(path)
if mime_type is None:
mime_type = mime_types.get(path.suffix, 'text/plain')
category, filetype = mime_type.split('/') if mime_type is not None else ('text', 'plain')
binary_categories = ['audio', 'video', 'image']
is_binary = category in binary_categories and mime_type != 'image/svg+xml'
data['mime_type'] = mime_type
if not is_binary:
with open(path) as f:
data['file_content'] = f.read()
if path is not None and path.suffix == '.elm': if path is not None and path.suffix == '.elm':
with open('public/elm-packages.json') as f: with open('public/elm-packages.json') as f:
data['elm_packages'] = json.load(f) data['elm_packages'] = json.load(f)
return context return context
class RenameThinkView(ThinkMixin, generic.UpdateView): class RenameThinkView(ThinkMixin, generic.UpdateView):
form_class = forms.RenameThinkForm form_class = forms.RenameThinkForm
template_name = 'thinks/rename.html' template_name = 'thinks/rename.html'
@ -160,18 +176,22 @@ class RenameThinkView(ThinkMixin, generic.UpdateView):
return context return context
class DeleteThinkView(ThinkMixin, generic.DeleteView): class DeleteThinkView(ThinkMixin, generic.DeleteView):
template_name = 'thinks/delete.html' template_name = 'thinks/delete.html'
def get_success_url(self): def get_success_url(self):
return reverse('index') return reverse('index')
class ReadFileView(ThinkMixin, generic.DetailView): class ReadFileView(ThinkMixin, generic.DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
think = self.get_object() think = self.get_object()
relpath = self.kwargs['path'] relpath = self.kwargs['path']
path = think.root / relpath path = think.root / relpath
print(path) if not path.is_relative_to(think.root):
raise Exception(f"Bad path: {relpath}")
return redirect(think.get_static_url() + '/' + relpath)
class SaveFileView(ThinkMixin, generic.UpdateView): class SaveFileView(ThinkMixin, generic.UpdateView):
@ -191,6 +211,7 @@ class SaveFileView(ThinkMixin, generic.UpdateView):
def get_success_url(self): def get_success_url(self):
return self.object.get_absolute_url()+'?path='+str(self.path) return self.object.get_absolute_url()+'?path='+str(self.path)
class RenameFileView(ThinkMixin, generic.UpdateView): class RenameFileView(ThinkMixin, generic.UpdateView):
form_class = forms.RenameFileForm form_class = forms.RenameFileForm
template_name = 'thinks/rename_file.html' template_name = 'thinks/rename_file.html'
@ -203,6 +224,7 @@ class RenameFileView(ThinkMixin, generic.UpdateView):
def get_success_url(self): def get_success_url(self):
return self.object.get_absolute_url()+'?path='+str(self.path) return self.object.get_absolute_url()+'?path='+str(self.path)
class DeleteFileView(ThinkMixin, generic.UpdateView): class DeleteFileView(ThinkMixin, generic.UpdateView):
form_class = forms.DeleteFileForm form_class = forms.DeleteFileForm
template_name = 'thinks/delete_file.html' template_name = 'thinks/delete_file.html'
@ -214,6 +236,7 @@ class DeleteFileView(ThinkMixin, generic.UpdateView):
def get_success_url(self): def get_success_url(self):
return self.object.get_absolute_url()+'?path='+str(self.path.parent) return self.object.get_absolute_url()+'?path='+str(self.path.parent)
class RunCommandView(ThinkMixin, generic.UpdateView): class RunCommandView(ThinkMixin, generic.UpdateView):
form_class = forms.RunCommandForm form_class = forms.RunCommandForm
@ -228,6 +251,14 @@ class RunCommandView(ThinkMixin, generic.UpdateView):
) )
return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr}) return JsonResponse({'stdout': res.stdout, 'stderr': res.stderr})
class UploadFileView(ThinkMixin, generic.UpdateView):
form_class = forms.UploadFileForm
def get_success_url(self):
return self.object.get_absolute_url()
class LogView(ThinkMixin, generic.DetailView): class LogView(ThinkMixin, generic.DetailView):
template_name = 'thinks/think.html' template_name = 'thinks/think.html'
@ -236,11 +267,13 @@ class LogView(ThinkMixin, generic.DetailView):
return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8') return HttpResponse(think.get_log(), content_type='text/plain; charset=utf-8')
class JJStatusView(ThinkMixin, generic.detail.DetailView): class JJStatusView(ThinkMixin, generic.detail.DetailView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
status = self.get_object().jj_controller.status() status = self.get_object().jj_controller.status()
return JsonResponse({'status': status}) return JsonResponse({'status': status})
class JJCommitView(ThinkMixin, generic.UpdateView): class JJCommitView(ThinkMixin, generic.UpdateView):
form_class = forms.GitCommitForm form_class = forms.GitCommitForm