diff --git a/thinks/forms.py b/thinks/forms.py index 1f015ca..15f1f6f 100644 --- a/thinks/forms.py +++ b/thinks/forms.py @@ -3,6 +3,7 @@ from django.conf import settings from .models import Think + class CreateThinkForm(forms.ModelForm): class Meta: model = Think @@ -15,11 +16,13 @@ class CreateThinkForm(forms.ModelForm): 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 @@ -49,6 +52,7 @@ class RenameThinkForm(forms.ModelForm): instance = super().save(commit) return instance + class SaveFileForm(forms.ModelForm): path = forms.CharField() content = forms.CharField(required=False, widget=forms.Textarea) @@ -72,6 +76,7 @@ class SaveFileForm(forms.ModelForm): return super().save(commit) + class RenameFileForm(forms.ModelForm): path = forms.CharField() newpath = forms.CharField() @@ -100,6 +105,7 @@ class RenameFileForm(forms.ModelForm): return super().save(commit) + class DeleteFileForm(forms.ModelForm): path = forms.CharField() @@ -121,6 +127,7 @@ class DeleteFileForm(forms.ModelForm): return super().save(commit) + class RunCommandForm(forms.ModelForm): command = forms.CharField() @@ -128,9 +135,51 @@ class RunCommandForm(forms.ModelForm): model = Think fields = [] + class GitCommitForm(forms.ModelForm): message = forms.CharField() class Meta: model = Think 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) diff --git a/thinks/urls.py b/thinks/urls.py index 2426c6d..930fb4d 100644 --- a/thinks/urls.py +++ b/thinks/urls.py @@ -12,6 +12,7 @@ urlpatterns = [ path('think//rename-file', RenameFileView.as_view(), name='rename_file'), path('think//delete-file', DeleteFileView.as_view(), name='delete_file'), path('think//run-command', RunCommandView.as_view(), name='run_command'), + path('think//upload-files', UploadFileView.as_view(), name='upload_files'), path('think//log', LogView.as_view(), name='log'), path('think//jj/status', JJStatusView.as_view(), name='jj_status'), path('think//jj/commit', JJCommitView.as_view(), name='jj_commit'), diff --git a/thinks/views.py b/thinks/views.py index e0eb83d..bbb1dc1 100644 --- a/thinks/views.py +++ b/thinks/views.py @@ -6,6 +6,7 @@ from django.views import generic from django.urls import reverse from itertools import groupby import json +import mimetypes from pathlib import Path import shutil import shlex @@ -16,6 +17,7 @@ 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: @@ -25,10 +27,12 @@ class LoginRequiredMixin(AccessMixin): 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' @@ -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 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 @@ -79,6 +85,7 @@ class RemixThinkView(ThinkMixin, generic.UpdateView): return redirect(think.get_absolute_url()) + class ThinkView(ThinkMixin, generic.DetailView): template_name = "thinks/think.html" @@ -124,28 +131,37 @@ class ThinkView(ThinkMixin, generic.DetailView): 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.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': 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' @@ -160,18 +176,22 @@ class RenameThinkView(ThinkMixin, generic.UpdateView): 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) + 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): @@ -191,6 +211,7 @@ class SaveFileView(ThinkMixin, generic.UpdateView): 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' @@ -203,6 +224,7 @@ class RenameFileView(ThinkMixin, generic.UpdateView): 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' @@ -214,6 +236,7 @@ class DeleteFileView(ThinkMixin, generic.UpdateView): 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 @@ -228,6 +251,14 @@ class RunCommandView(ThinkMixin, generic.UpdateView): ) 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): 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') + 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