summaryrefslogtreecommitdiffstats
path: root/core/views/uploads.py
blob: 655738f19e56d06f536681dd5a93b330a6f7ad2f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
import subprocess
from datetime import datetime
from pathlib import Path

from django import forms
from django.core.files.storage import FileSystemStorage
from django.shortcuts import render, redirect

import settings

from troggle.core.models.caves import GetCaveLookup
from troggle.core.models.logbooks import LogbookEntry, writelogbook, PersonLogEntry
from troggle.core.models.survex import DrawingFile
from troggle.core.models.troggle import DataIssue, Expedition, PersonExpedition
from troggle.core.utils import alphabet_suffix, current_expo, sanitize_name
from troggle.parsers.people import GetPersonExpeditionNameLookup, known_foreigner

# from databaseReset import reinit_db # don't do this. databaseRest runs code *at import time*

from .auth import login_required_if_public

"""File upload 'views' 
Note that there are other file upload forms in views/wallet_edit.py
and that core/forms.py contains Django class-based forms for caves and entrances.
"""

todo = """
- Ideally we should validate uploaded file as being a valid file type, not a dubious script or hack
  Validate image files using a magic recogniser in walletedit()
  https://pypi.org/project/reportlab/   or 
  https://stackoverflow.com/questions/889333/how-to-check-if-a-file-is-a-valid-image-file

- Write equivalent GPX upload form system, similar to walletedit() but in expofiles/gpslogs/
  Need to validate it as being a valid GPX file using an XML parser, not a dubious script or hack
  
- Validate Tunnel & Therion files using an XML parser in dwgupload(). Though Julian says 
  tunnel is  only mostly correct XML, and it does fail at least one XML parser.
  
- parse the uploaded drawing file for links to wallets and scan files as done 
  in parsers/drawings.py

- Enable folder creation in dwguploads or as a separate form

- Enable file rename on expofiles, not just for /surveyscans/ (aka wallets)

- Make file rename utility less ugly.
"""

def create_new_lbe_slug(date):
    onthisdate = LogbookEntry.objects.filter(date=date)
    n = len(onthisdate)
    # print(f" Already entries on this date: {n}\n {onthisdate}")
    
    suffix = alphabet_suffix(n)
        
    tid = f"{date}{suffix}"
    print(tid)
    return tid

def store_edited_entry_into_database(date, place, title, text, others, author, tu, slug):
    """saves a single logbook entry and related personlogentry items
    
    need to  select out *guest and foreign friends from others
    
    Rather similar to similarly named function in parsers/logbooks but circular reference prevents us using it directly,
    and they need refactoring anyway.
    """
    
    year = slug[0:4]
    expedition = Expedition.objects.get(year=year)
    cave = GetCaveLookup().get(place.lower())
    # print(f"{place} {cave=}")
    
    if LogbookEntry.objects.filter(slug=slug).exists():
        # oops.
        message = " ! - DUPLICATE SLUG for logbook entry " + tripdate + " - " + slug
        DataIssue.objects.create(parser="logbooks", message=message)
        slug = slug + "_" + unique_slug(text,2)
    
    nonLookupAttribs = {
        "place": place,
        "text": text,
        "expedition": expedition,
        "time_underground": tu,
        "cave_slug": str(cave),
        "title": f"{place} - {title}",
        # "other_people": others
    }
    lookupAttribs = {"slug": slug, "date": date }

    lbo = LogbookEntry.objects.create(**nonLookupAttribs, **lookupAttribs)
    
    pt_list = []
    # These entities have to be PersonExpedition objects
    team = others.split(",")
    team.append(author)
    
    odds = []
    for name in team:
        name = name.strip()
        if len(name) > 0: 
            if name[0] == "*": # a name prefix of "*" is special, just a string.
                odds.append(name)
                print(f" - adding * special name '{name}'")
            else:
                try:
                    personyear = GetPersonExpeditionNameLookup(expedition).get(name.lower())
                    if not personyear:
                        odds.append(name)
                        print(f" - adding unrecognised expoer '{name}'")
                        if known_foreigner(name):
                            message = f" ! - Known foreigner: '{name}' in entry {slug=}"
                            print(message)
                        else:
                            message = f" ! - No name match for: '{name}' in entry {slug=}"
                            print(message)
                            DataIssue.objects.create(parser="logbooks", message=message)
                    else:                    
                        lookupAttribs = {"personexpedition": personyear, "nickname_used": name, "logbook_entry": lbo} # lbo is primary key
                        nonLookupAttribs = {"time_underground": tu, "is_logbook_entry_author": (name==author)}
                        pt_list.append(PersonLogEntry(**nonLookupAttribs, **lookupAttribs))

                except:
                    # This should not happen. We do not raise exceptions in that function
                    message = f" ! - EXCEPTION: '{name}' in entry {slug=}"
                    print(message)
                    DataIssue.objects.create(parser="logbooks", message=message)
                    raise
 
    PersonLogEntry.objects.bulk_create(pt_list)
    
    lbo.other_people = ", ".join(odds)
    print(f" - Saving other_people '{lbo.other_people}'")
    lbo.save()

class FilesForm(forms.Form):  # not a model-form, just a form-form
    uploadfiles = forms.FileField()

class FilesRenameForm(forms.Form):  # not a model-form, just a form-form
    uploadfiles = forms.FileField()
    renameto = forms.CharField(strip=True, required=False)

class TextForm(forms.Form):  # not a model-form, just a form-form
    photographer = forms.CharField(strip=True)
    
class ExpofileRenameForm(forms.Form):  # not a model-form, just a form-form
    renameto = forms.CharField(strip=True, required=False)
    
class LogbookEditForm(forms.Form):  # not a model-form, just a form-form
    author = forms.CharField(strip=True, required=False)
    
@login_required_if_public
def logbookedit(request, year=None, slug=None):
    """Edit a logbook entry
    
    This 'validates' the author as being on expo in the current year, but only indicates this by
    putting the text of the form prompt in red (same as for an invalid date, which is arguably more important).
    No check is done on the other people on the trip as this is picked up anyway by parsing on import
    and we don't really care at this point.
    """
    def validate_year(year):
        try:
            expo = Expedition.objects.get(year=year)
        except:
            year = current_expo()
        return year
        
    def new_entry_form():
        return render(
            request,
            "logbookform.html",
            {
                "form": form,
                "year": year,
            },
        )
    def clean_tu(tu):
        if tu =="":
            return 0
        try:
            tu = float(tu)/1 # check numeric
        except:
            return 0
        return tu
    
    if not year:
        if not slug:
            year = current_expo()
        else:
            year = slug[0:4]
            try:
                year = str(int(year))
            except:
                year = current_expo()
            
    author = ""
    
    if request.method == "POST":
        form = LogbookEditForm(request.POST)
        if not form.is_valid():
            message = f'Invalid form response for logbook entry creating "{request.POST}"'
            print(message)
            return render(request, "errors/generic.html", {"message": message})
        else:
            # if there is no slug then this is a completely new lbe and we need to enter it into the db
            # otherwise it is an update
            # validation all to be done yet..
            date = request.POST["date"].strip()  
            author = request.POST["author"].strip()   # TODO check against personexpedition  on submit
            others = request.POST["others"].strip()    # TODO check each against personexpedition on submit
            place = request.POST["place"].strip().replace(' - ',' = ')  # no hyphens !
            title = request.POST["title"].strip()   
            entry = request.POST["text"].strip() 
            entry = entry.replace('\r','') # remove HTML-standard CR inserted
            entry = entry.replace('\n\n','\n<br />\n<br />\n') # replace 2 \n  with <br><br>
            entry = entry.replace('<p','<br />\n<br') # replace <p> tag, even if it has attributes,  with <br><br>
            entry = entry.replace('<br>','<br />') # clean up previous hack
            tu = request.POST["tu"].strip()
            tu = clean_tu(tu)
            
            try:
                odate = datetime.strptime(date.replace(".", "-"), "%Y-%m-%d").date()
                dateflag = False
            except:
                odate = datetime.strptime(f"{year}-01-01", "%Y-%m-%d").date()
                print(f"! Invalid date string {date}, setting to {odate}")
                dateflag = True
            date = odate.isoformat()
            
            year = validate_year(year)
            expo = Expedition.objects.get(year=year)
            personyear = GetPersonExpeditionNameLookup(expo).get(author.lower())
            if personyear:
                authorflag = False
            else:
                authorflag = True
                print(f"! Unrecognised author: {author}")
            
            
            if not slug:
                # Creating a new logbook entry with all the gubbins
                slug = create_new_lbe_slug(date)
            else:
                # OK we could patch the object in place, but if the people on the trip have changed this 
                # would get very messy. So we delete it and recreate it and all its links
                print(f"- Deleting the LogBookEntry {slug}")
                LogbookEntry.objects.filter(slug=slug).delete()

            print(f"- Creating the LogBookEntry {slug}")
            store_edited_entry_into_database(date, place, title, entry, others, author, tu, slug)
            print(f"- Rewriting the entire {year} logbook to disc ")
            filename= "logbook.html"
            try:
                writelogbook(year, filename) # uses a template, not the code fragment below which is just a visible hint to logged on user
            except:
                message = f'! - Logbook saving failed - \n!! Permissions failure ?! on attempting to save file "logbook.html"'
                print(message)
                return render(request, "errors/generic.html", {"message": message})
            
            # Code fragment illustration - not actually what gets saved to database
            output = f'''

<div class="tripdate" id="{slug}">{date}</div>
<div class="trippeople"><u>{author}</u>, {others}</div>
<div class="triptitle">{place} - {title}</div>

{entry}

<div class="timeug">T/U {tu} hrs</div>
<hr />

'''
            # Successful POST
            # So save to database and then write out whole new logbook.html file
            
            # We do author validation on the form as displayed by GET, not at the moment of POST.
            # If we had JS validation then we could be more timely.
            git = settings.GIT
            dirpath = Path(settings.EXPOWEB) / "years" / str(year)
            lbe_add = subprocess.run(
                [git, "add", filename], cwd=dirpath, capture_output=True, text=True
            )
            msgdata = (
                lbe_add.stderr
                + "\n"
                + lbe_add.stdout
                + "\nreturn code: "
                + str(lbe_add.returncode)
            )
            message = f'! - FORM Logbook Edit {slug} - Success: git ADD on server for this file {filename}.' + msgdata
            print(message)
            if lbe_add.returncode != 0:
                msgdata = (
                    "Ask a nerd to fix this.\n\n"
                    + lbe_add.stderr
                    + "\n\n"
                    + lbe_add.stdout
                    + "\n\nreturn code: "
                    + str(lbe_add.returncode)
                )
                message = (
                    f"! - FORM Logbook Edit  - CANNOT git ADD on server for this file {filename}. {slug} edits saved but not added to git.\n"
                    + msgdata
                )
                print(message)
                return render(request, "errors/generic.html", {"message": message})
                
            lbe_commit = subprocess.run(
                [git, "commit", "-m", f"Logbook edited {slug}"],
                cwd=dirpath,
                capture_output=True,
                text=True,
            )
            message = f'! - FORM Logbook Edit - {filename}. {slug} edits saved, added to git, and COMMITTED.\n' + msgdata
            print(message)
            #This produces return code = 1 if it commits OK
            if lbe_commit.returncode != 0:
                msgdata = (
                    "Ask a nerd to fix this.\n\n"
                    + lbe_commit.stderr
                    + "\n"
                    + lbe_commit.stdout
                    + "\nreturn code: "
                    + str(lbe_commit.returncode)
                )
                message = (
                    f"! - FORM Logbook Edit -Error code with git on server for {filename}. {slug} edits saved, added to git, but NOT committed.\n"
                    + msgdata
                )
                print(message)
                return render(request, "errors/generic.html", {"message": message})
            
            return render(
                request,
                "logbookform.html",
                {
                    "form": form,
                    "year": year,
                    "date": date, "dateflag": dateflag,
                    "author": author, "authorflag": authorflag,
                    "others": others,
                    "place": place,
                    "title": title,
                    "tu": tu,
                    "entry": entry,
                    "output": output,
                },
            )
    # GET here
    else:
        form = LogbookEditForm()
        year = validate_year(year)

        if not slug: # no slug or bad slug for an lbe which does not exist
            return new_entry_form()
        else:
            lbes = LogbookEntry.objects.filter(slug=slug)
            if not lbes:
                    return new_entry_form()
            else:
                if len(lbes) > 1:
                    return render(request, "object_list.html", {"object_list": lbes}) # ie a bug
                else:
                    lbe = lbes[0]
                print(f"{lbe}")
                tu = clean_tu(lbe.time_underground)
                
                people = []
                for p in lbe.personlogentry_set.filter(logbook_entry=lbe): # p is a PersonLogEntry object
                    if p.is_logbook_entry_author:
                        # author = p.personexpedition.person.fullname
                        author = p.nickname_used
                    else:
                        # people.append(p.personexpedition.person.fullname)
                        people.append(p.nickname_used)
                others =', '.join(people)
                print(f"{others=}")
                if lbe.other_people:
                    others = others + ", " + lbe.other_people
                lenothers = min(70,max(20, len(others)))
                print(f"{others=}")
 
                text = lbe.text
                rows = max(5,len(text)/50)
                return render(
                    request,
                    "logbookform.html",
                    {
                        "form": form,
                        "year": year,
                        "date": lbe.date.isoformat(),
                        "author": author,
                        "others": others,
                        "lenothers": lenothers,
                        "place": lbe.place,
                        "title": lbe.title.replace(f"{lbe.place} - ",""),
                        "tu": tu,
                        "entry": text,
                        "textrows": rows,
                     },
                )
   
@login_required_if_public
def expofilerename(request, filepath):
    """Rename any single file in /expofiles/ - eventually.
    Currently this just does files within wallets i.e. in /surveyscans/
    and it returns control to the original wallet edit page
    """
    def rotate_image():
        print("ROTATE")
        return simple_get()
        
    def simple_get():
        form = ExpofileRenameForm()
        return render(
            request,
            "renameform.html",
            {
                "form": form,
                "filepath": filepath,
                "filename": filename,
                "filesize": filesize,
                "files": files,
                "walletpath": walletpath,
                "notpics": notpics,
            },
        )
        
    if filepath:
        actualpath = Path(settings.EXPOFILES) / Path(filepath)
    else:
        message = f'\n File to rename not specified "{filepath}"'
        print(message)
        return render(request, "errors/generic.html", {"message": message})
    
    if not actualpath.is_file():
        message = f'\n File not found when attempting rename "{filepath}"'
        print(message)
        return render(request, "errors/generic.html", {"message": message})
    else:
        filename = Path(filepath).name
        walletpath = Path(filepath).parent
        folder = actualpath.parent
        filesize = f"{actualpath.stat().st_size:,}"

    if not actualpath.is_relative_to(Path(settings.SCANS_ROOT)):
        message = f'\n Can only do rename within wallets (expofiles/surveyscans/) currently, sorry. "{actualpath}" '
        print(message)
        return render(request, "errors/generic.html", {"message": message})
        
    files = []
    dirs = []
    notpics =[]
    dirpath = actualpath.parent
    print(f'! - FORM rename expofile - start \n{filepath=} \n{dirpath=} \n{walletpath=}')
    if dirpath.is_dir():
        try:
            for f in dirpath.iterdir():
                if f.is_dir():
                    for d in f.iterdir():
                        dirs.append(f"{f.name}/{d.name}")
                if f.is_file():
                    if Path(f.name).suffix.lower() in [".jpg", ".jpeg", ".png"]:
                        files.append(f.name)
                    else:
                         notpics.append(f.name)     
        except FileNotFoundError:
            files.append(
                "(Error. There should be at least one filename visible here. Try refresh.)"
            )
    if request.method == "GET":
        return simple_get()
        
    elif request.method == "POST":
        form = ExpofileRenameForm(request.POST)
        if not form.is_valid():
            message = f'Invalid form response for file renaming "{request.POST}"'
            print(message)
            return render(request, "errors/generic.html", {"message": message})

        if "rotate" in request.POST:
            return rotate_image()
            
        if "rename" in request.POST:
            if "renametoname" not in request.POST:
                print("renametoname not in request.POST")
                # blank filename passed it, so just treat as another GET
                return simple_get()  


            renameto = sanitize_name(request.POST["renametoname"])  
            if (folder / renameto).is_file() or (folder / renameto).is_dir():
                rename_bad = renameto
                message = f'\n Cannot rename to an existing file or folder. "{filename}" -> "{(folder / renameto)}"'
                print(message)
                return render(
                    request,
                    "renameform.html",
                    {
                        "form": form,
                        "filepath": filepath,
                        "filename": filename,
                        "filesize": filesize,
                        "files": files,
                        "walletpath": walletpath,
                        "notpics": notpics,
                        "rename_bad": rename_bad,
                    },
                )

            actualpath.rename((folder / renameto))
            message = f'\n RENAMED "{filename}" -> "{(folder / renameto)}"'
            print(message)
            walletid  = actualpath.relative_to(Path(settings.SCANS_ROOT)).parent.stem.replace("#",":")
            print(walletid)
            return redirect(f'/survey_scans/{walletid}/')
            
    else: # not GET or POST
        print("UNRECOGNIZED action")
        return simple_get()
 
@login_required_if_public
def photoupload(request, folder=None):
    """Upload photo image files into /expofiles/photos/<year>/<photographer>/
    This does NOT use a Django model linked to a Django form. Just a simple Django form.
    You will find the Django documentation on forms very confusing, This is simpler.


    When uploading from a phone, it is useful to be able to rename the file to something
    meaningful as this is difficult to do on a phone. Previously we had assumed files would
    be renamed to something useful before starting the upload.
    Unfortunately this only works when uploading one file at at time ,
    inevitable once you think about it.
    
    Pending generic file renaming capability more generally.
    """
    year = settings.PHOTOS_YEAR
    filesaved = False
    actual_saved = []

    context = {"year": year, "placeholder": "AnathemaDevice"}

    yearpath = Path(settings.PHOTOS_ROOT, year)

    if folder == str(year) or folder == str(year) + "/":
        folder = None

    if folder is None:
        folder = ""  # improve this later
        dirpath = Path(settings.PHOTOS_ROOT, year)
        urlfile = f"/expofiles/photos/{year}"
        urldir = f"/photoupload/{year}"
    else:  # it will contain the year as well as the photographer
        dirpath = Path(settings.PHOTOS_ROOT, folder)
        if dirpath.is_dir():
            urlfile = f"/expofiles/photos/{folder}"
            urldir = Path("/photoupload") / folder
        else:
            folder = ""  # improve this later
            dirpath = Path(settings.PHOTOS_ROOT, year)
            urlfile = f"/expofiles/photos/{year}"
            urldir = f"/photoupload/{year}"

    form = FilesRenameForm()
    formd = TextForm()

    if request.method == "POST":
        if "photographer" in request.POST:
            formd = TextForm(request.POST)
            if formd.is_valid():
                newphotographer = sanitize_name(request.POST["photographer"])
                try:
                    (yearpath / newphotographer).mkdir(exist_ok=True)
                except:
                    message = f'\n !! Permissions failure ?! 0 attempting to mkdir "{(yearpath / newphotographer)}"'
                    print(message)
                    return render(request, "errors/generic.html", {"message": message})

        else:
            form = FilesRenameForm(request.POST, request.FILES)
            if form.is_valid():
                f = request.FILES["uploadfiles"]
                multiple = request.FILES.getlist("uploadfiles")
                # NO CHECK that the files being uploaded are image files
                fs = FileSystemStorage(dirpath)

                renameto = sanitize_name(request.POST["renameto"])

                actual_saved = []
                if multiple:
                    if len(multiple) == 1:
                        if renameto != "":
                            try:  # crashes in Django os.chmod call if on WSL, but does save file!
                                saved_filename = fs.save(renameto, content=f)
                            except:
                                print(
                                    f'\n !! Permissions failure ?! 1 attempting to save "{f.name}" in "{dirpath}" {renameto=}'
                                )
                                if "saved_filename" in locals():
                                    if saved_filename.is_file():
                                        actual_saved.append(saved_filename)
                                        filesaved = True
                        else:  # multiple is the uploaded content
                            try:  # crashes in Django os.chmod call if on WSL, but does save file!
                                saved_filename = fs.save(f.name, content=f)
                            except:
                                print(
                                    f'\n !! Permissions failure ?! 2 attempting to save "{f.name}" in "{dirpath}" {renameto=}'
                                )
                                if "saved_filename" in locals():
                                    if saved_filename.is_file():
                                        actual_saved.append(saved_filename)
                                        filesaved = True
                    else:  # multiple is a list of content
                        for f in multiple:
                            try:  # crashes in Django os.chmod call if on WSL, but does save file!
                                saved_filename = fs.save(f.name, content=f)
                            except:
                                print(
                                    f'\n !! Permissions failure ?! 3 attempting to save "{f.name}" in "{dirpath}" {renameto=}'
                                )
                                if "saved_filename" in locals():
                                    if saved_filename.is_file():
                                        actual_saved.append(saved_filename)
                                        filesaved = True
    files = []
    dirs = []
    try:
        for f in dirpath.iterdir():
            if f.is_dir():
                dirs.append(f.name)
            if f.is_file():
                files.append(f.name)
    except FileNotFoundError:
        files.append("(no folder yet - would be created)")
    if len(files) > 0:
        files = sorted(files)

    if dirs:
        dirs = sorted(dirs)

    return render(
        request,
        "photouploadform.html",
        {
            "form": form,
            **context,
            "urlfile": urlfile,
            "urldir": urldir,
            "folder": folder,
            "files": files,
            "dirs": dirs,
            "filesaved": filesaved,
            "actual_saved": actual_saved,
        },
    )


@login_required_if_public
def dwgupload(request, folder=None, gitdisable="no"):
    """Upload DRAWING files (tunnel or therion) into the upload folder in :drawings
    AND registers it into the :drawings: git repo.

    This does NOT use a Django model linked to a Django form. Just a simple Django form.
    You will find the Django documentation on forms very confusing, This is simpler.

    We could validate the uploaded files as being a valid files using an XML parser, not a dubious script or hack,
    but this won't work on Tunnel files as Tunnel does not produce exactly valid xml

    We use get_or_create instead of simply creating a new object in case someone uploads the same file
    several times in one session, and expects them to be overwritten in the database. Although
    the actual file will be duplicated in the filesystem with different random name ending.
    """

    def dwgvalid(name):
        if name in [
            ".gitignore",
        ]:
            return False
        if Path(name).suffix.lower() in [".xml", ".th", ".th2", "", ".svg", ".txt"]:
            return True  # dangerous, we should check the actual file binary signature
        return False

    def dwgvaliddisp(name):
        """OK to display, even if we are not going to allow a new one to be uploaded"""
        if name in [
            ".gitignore",
        ]:
            return False
        if Path(name).suffix.lower() in [
            ".xml",
            ".th",
            ".th2",
            "",
            ".svg",
            ".txt",
            ".jpg",
            ".jpeg",
            ".png",
            ".pdf",
            ".top",
            ".topo",
        ]:
            return True  # dangerous, we should check the actual file binary signature
        return False

    filesaved = False
    actual_saved = []
    refused = []
    doesnotexist = ""
    # print(f'! - FORM dwgupload - start "{folder}" - gitdisable "{gitdisable}"')
    if folder is None:
        folder = ""  # improve this later
        dirpath = Path(settings.DRAWINGS_DATA)
        urlfile = "/dwgdataraw"
        urldir = "/dwgupload"
    else:
        dirpath = Path(settings.DRAWINGS_DATA, folder)
        urlfile = Path("/dwgdataraw/") / folder
        urldir = Path("/dwgupload/") / folder

    form = FilesForm()

    if request.method == "POST":
        form = FilesForm(request.POST, request.FILES)
        if form.is_valid():
            # print(f'! - FORM dwgupload - POST valid: "{request.FILES["uploadfiles"]}" ')
            f = request.FILES["uploadfiles"]
            multiple = request.FILES.getlist("uploadfiles")
            savepath = Path(settings.DRAWINGS_DATA, folder)
            fs = FileSystemStorage(savepath)

            actual_saved = []
            refused = []

            # GIT see also core/views/expo.py editexpopage()
            # GIT see also core/models/cave.py writetrogglefile()
            if gitdisable != "yes":  # set in url 'dwguploadnogit/'
                git = settings.GIT
            else:
                git = "echo"
                # print(f'git DISABLED {f.name}')

            if multiple:
                for f in multiple:
                    # print(f'! - FORM dwgupload - file {f} in {multiple=}')
                    if dwgvalid(f.name):
                        try:  # crashes in Django os.chmod call if on WSL without metadata drvfs, but does save file!
                            saved_filename = fs.save(f.name, content=f)
                        except:
                            print(
                                f'! - FORM dwgupload - \n!! Permissions failure ?! on attempting to save file "{f.name}" in "{savepath}". Attempting to continue..'
                            )
                        if "saved_filename" in locals():
                            if Path(dirpath, saved_filename).is_file():
                                actual_saved.append(saved_filename)
                                if gitdisable != "yes":
                                    dr_add = subprocess.run(
                                        [git, "add", saved_filename], cwd=dirpath, capture_output=True, text=True
                                    )
                                    msgdata = (
                                        dr_add.stderr
                                        + "\n"
                                        + dr_add.stdout
                                        + "\nreturn code: "
                                        + str(dr_add.returncode)
                                    )
                                    # message = f'! - FORM dwgupload - Success: git ADD on server for this file {saved_filename}.' + msgdata
                                    # print(message)
                                    if dr_add.returncode != 0:
                                        msgdata = (
                                            "Ask a nerd to fix this.\n\n"
                                            + dr_add.stderr
                                            + "\n\n"
                                            + dr_add.stdout
                                            + "\n\nreturn code: "
                                            + str(dr_add.returncode)
                                        )
                                        message = (
                                            f"! - FORM dwgupload - CANNOT git ADD on server for this file {saved_filename}. Edits saved but not added to git.\n"
                                            + msgdata
                                        )
                                        print(message)
                                        return render(request, "errors/generic.html", {"message": message})
                                dwgfile, created = DrawingFile.objects.get_or_create(
                                    dwgpath=saved_filename, dwgname=Path(f.name).stem, filesize=f.size
                                )
                                dwgfile.save()
                            else:
                                message = f"! - FORM dwgupload - NOT A FILE {Path(dirpath, saved_filename)=}. "
                                print(message)
                        else:
                            message = f"! - FORM dwgupload - Save failure for {f.name}. Changes NOT saved."
                            print(message)
                            return render(request, "errors/generic.html", {"message": message})

                        if saved_filename != f.name:
                            # message = f'! - FORM dwgupload - Save RENAME {f.name} renamed as {saved_filename}. This is OK.'
                            # print(message)
                            pass

                    else:
                        refused.append(f.name)
                        # print(f'REFUSED {f.name}')

            if actual_saved:
                filesaved = True
                if len(actual_saved) > 1:
                    dots = "..."
                else:
                    dots = ""
                if gitdisable != "yes":
                    dr_commit = subprocess.run(
                        [git, "commit", "-m", f"Drawings upload - {actual_saved[0]}{dots}"],
                        cwd=dirpath,
                        capture_output=True,
                        text=True,
                    )
                    # message = f'! - FORM dwgupload - For uploading {actual_saved[0]}{dots}. Edits saved, added to git, and COMMITTED.\n' + msgdata
                    # print(message)
                    # This produces return code = 1 if it commits OK
                    if dr_commit.returncode != 0:
                        msgdata = (
                            "Ask a nerd to fix this.\n\n"
                            + dr_commit.stderr
                            + "\n"
                            + dr_commit.stdout
                            + "\nreturn code: "
                            + str(dr_commit.returncode)
                        )
                        message = (
                            f"! - FORM dwgupload -Error code with git on server for this {actual_saved[0]}{dots}. Edits saved, added to git, but NOT committed.\n"
                            + msgdata
                        )
                        print(message)
                        return render(request, "errors/generic.html", {"message": message})
                else:
                    print(f' git disabled "{git=}"')
            else:  # maybe all were refused by the suffix test in dwgvalid()
                message = f"! - FORM dwgupload - Nothing actually saved. All were refused. {actual_saved=}"
                print(message)

    files = []
    dirs = []
    # print(f'! - FORM dwgupload - start {folder=} \n"{dirpath=}" \n"{dirpath.parent=}" \n"{dirpath.exists()=}"')
    try:
        for f in dirpath.iterdir():
            if f.is_dir():
                if f.name not in [".git"]:
                    dirs.append(f.name)
                    continue
            if f.is_file():
                if dwgvaliddisp(f.name):
                    files.append(f.name)
                    continue
    except FileNotFoundError:
        doesnotexist = True
    if files:
        files = sorted(files)

    if dirs:
        dirs = sorted(dirs)

    return render(
        request,
        "dwguploadform.html",
        {
            "form": form,
            "doesnotexist": doesnotexist,
            "urlfile": urlfile,
            "urldir": urldir,
            "folder": folder,
            "files": files,
            "dirs": dirs,
            "filesaved": filesaved,
            "actual_saved": actual_saved,
            "refused": refused,
        },
    )