Browse Source

2022.14.08.0 snapshot.

stable
Craig Oates 2 years ago
parent
commit
c66684baa0
  1. 324
      .gitignore
  2. 16
      README.markdown
  3. 41
      app.lisp
  4. 0
      db/schema.sql
  5. 0
      hot-line-python/README.md
  6. 251
      hot-line-python/app/hot-line-python.py
  7. 18
      hot-line-python/requirements.txt
  8. 67
      hot-line.asd
  9. 45
      src/config.lisp
  10. 26
      src/db.lisp
  11. 56
      src/main.lisp
  12. 256
      src/models/app-constants.lisp
  13. 63
      src/models/file.lisp
  14. 59
      src/models/user.lisp
  15. 85
      src/services/authentication.lisp
  16. 52
      src/services/convert.lisp
  17. 60
      src/services/db-management.lisp
  18. 76
      src/services/pagination.lisp
  19. 562
      src/services/routing.lisp
  20. 127
      src/services/storage-management.lisp
  21. 167
      src/services/storage.lisp
  22. 111
      src/services/user-management.lisp
  23. 45
      src/services/validation.lisp
  24. 73
      src/view.lisp
  25. 256
      src/web.lisp
  26. BIN
      static/css/avenir-book.otf
  27. BIN
      static/css/avenir.otf
  28. BIN
      static/css/firacode-retina.ttf
  29. 418
      static/css/full-search.css
  30. BIN
      static/css/iAWriterQuattroV.ttf
  31. 1087
      static/css/main.css
  32. 121
      static/css/search.css
  33. BIN
      static/images/audio-file.png
  34. BIN
      static/images/bar-line.png
  35. BIN
      static/images/bar.png
  36. BIN
      static/images/bokeh-logo.png
  37. BIN
      static/images/bokeh-simple.png
  38. BIN
      static/images/email.png
  39. BIN
      static/images/favicon.png
  40. BIN
      static/images/file.png
  41. BIN
      static/images/git.png
  42. BIN
      static/images/gravatar.png
  43. BIN
      static/images/hero.png
  44. BIN
      static/images/instagram.png
  45. BIN
      static/images/line-chart-overview.png
  46. BIN
      static/images/line.png
  47. BIN
      static/images/logo
  48. BIN
      static/images/network-effect.png
  49. BIN
      static/images/nuget.png
  50. BIN
      static/images/screen-video-1.mp4
  51. BIN
      static/images/screenshot-1.png
  52. BIN
      static/images/screenshot-2.1.png
  53. BIN
      static/images/screenshot-2.png
  54. BIN
      static/images/site-logo.png
  55. BIN
      static/images/tiktok.png
  56. 47
      templates/_errors/404.html
  57. 83
      templates/about.html
  58. 62
      templates/chart/add.html
  59. 83
      templates/index.html
  60. 46
      templates/layouts/base.html
  61. 18
      templates/nav-menu.html
  62. 58
      templates/privacy.html
  63. 7
      templates/shared/footer.html
  64. 19
      templates/shared/header.html
  65. 646
      templates/shared/mime-select-alt.html
  66. 646
      templates/shared/mime-select.html
  67. 57
      templates/shared/pagination.html
  68. 28
      templates/sign-up.html
  69. 44
      templates/user/add.html
  70. 80
      templates/user/dashboard.html
  71. 66
      templates/user/edit.html
  72. 87
      templates/user/index.html
  73. 20
      templates/user/log-in.html
  74. 21
      tests/tests.lisp

324
.gitignore vendored

@ -1,154 +1,170 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Common Lisp Stuff
*.fasl
*.dx32fsl
*.dx64fsl
*.lx32fsl
*.lx64fsl
*.x86f
*~
.#*
# Python Stuff
-# Byte-compiled / optimized / DLL files
-__pycache__/
-*.py[cod]
-*$py.class
-
-# C extensions
-*.so
-
-# Distribution / packaging
-.Python
-build/
-develop-eggs/
-dist/
-downloads/
-eggs/
-.eggs/
-lib/
-lib64/
-parts/
-sdist/
-var/
-wheels/
-share/python-wheels/
-*.egg-info/
-.installed.cfg
-*.egg
-MANIFEST
-
-# PyInstaller
-# Usually these files are written by a python script from a template
-# before PyInstaller builds the exe, so as to inject date/other infos into it.
-*.manifest
-*.spec
-
-# Installer logs
-pip-log.txt
-pip-delete-this-directory.txt
-
-# Unit test / coverage reports
-htmlcov/
-.tox/
-.nox/
-.coverage
-.coverage.*
-.cache
-nosetests.xml
-coverage.xml
-*.cover
-*.py,cover
-.hypothesis/
-.pytest_cache/
-cover/
-
-# Translations
-*.mo
-*.pot
-
-# Django stuff:
-*.log
-local_settings.py
-db.sqlite3
-db.sqlite3-journal
-
-# Flask stuff:
-instance/
-.webassets-cache
-
-# Scrapy stuff:
-.scrapy
-
-# Sphinx documentation
-docs/_build/
-
-# PyBuilder
-.pybuilder/
-target/
-
-# Jupyter Notebook
-.ipynb_checkpoints
-
-# IPython
-profile_default/
-ipython_config.py
-
-# pyenv
-# For a library or package, you might want to ignore these files since the code is
-# intended to run in multiple environments; otherwise, check them in:
-# .python-version
-
-# pipenv
-# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
-# However, in case of collaboration, if having platform-specific dependencies or dependencies
-# having no cross-platform support, pipenv may install dependencies that don't work, or not
-# install all needed dependencies.
-#Pipfile.lock
-
-# poetry
-# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
-# This is especially recommended for binary packages to ensure reproducibility, and is more
-# commonly ignored for libraries.
-# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
-#poetry.lock
-
-# PEP 582; used by e.g. github.com/David-OConnor/pyflow
-__pypackages__/
-
-# Celery stuff
-celerybeat-schedule
-celerybeat.pid
-
-# SageMath parsed files
-*.sage.py
-
-# Environments
-.env
-.venv
-env/
-venv/
-ENV/
-env.bak/
-venv.bak/
hot-line-python/venv/
-
-# Spyder project settings
-.spyderproject
-.spyproject
-
-# Rope project settings
-.ropeproject
-
-# mkdocs documentation
-/site
-
-# mypy
-.mypy_cache/
-.dmypy.json
-dmypy.json
-
-# Pyre type checker
-.pyre/
-
-# pytype static type analyzer
-.pytype/
-
-# Cython debug symbols
-cython_debug/
-
-# PyCharm
-# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
-# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
-# and can be added to the global gitignore or merged into this file. For a more nuclear
-# option (not recommended) you can uncomment the following to ignore the entire idea folder.
-#.idea/
# Craig's Custom Stuff
# hot-line is a sym-link to the non-dev project folder.
/hot-line
/storage/*
/db/*.db

16
README.markdown

@ -0,0 +1,16 @@
# hot-line
A proof-of-concept website which generates interactive charts whilst integrating with Python and its Bokeh library.
## Usage
## Installation
## Author
* Craig Oates (craig@craigoates.net)
## Copyright
Copyright (c) 2022 Craig Oates (craig@craigoates.net)

41
app.lisp

@ -0,0 +1,41 @@
(ql:quickload :hot-line)
(defpackage hot-line.app
(:use :cl)
(:import-from :lack.builder
:builder)
(:import-from :ppcre
:scan
:regex-replace)
(:import-from :hot-line.web
:*web*)
(:import-from :hot-line.config
:config
:productionp
:*static-directory*))
(in-package :hot-line.app)
(funcall clack-errors:*clack-error-middleware*
(builder
(:static
:path (lambda (path)
(if (ppcre:scan "^(?:/images/|/css/|/js/|/robot\\.txt$|/favicon\\.ico$)" path)
path
nil))
:root *static-directory*)
(if (productionp)
nil
:accesslog)
(if (getf (config) :error-log)
`(:backtrace
:output ,(getf (config) :error-log))
nil)
:session
(if (productionp)
nil
(lambda (app)
(lambda (env)
(let ((datafly:*trace-sql* t))
(funcall app env)))))
*web*)
:debug t)

0
db/schema.sql

0
README.md → hot-line-python/README.md

251
hot-line-python/app/hot-line-python.py

@ -0,0 +1,251 @@
#!/usr/bin/env python
from bokeh.plotting import figure, output_file, save, show
from bokeh.models import Legend, FactorRange
from bokeh.palettes import Spectral4
import pandas as pd
import numpy as np
import os.path
from sys import stdin
import argparse
import logging
from pathlib import Path
from rich import print
from rich.console import Console
from rich.markdown import Markdown
from rich.logging import RichHandler
from rich.traceback import install
import random
# Global Variables (Making things easy for myself)
# ====================================================================
__version__ = "1.0.0"
logging.basicConfig(level="NOTSET", format="%(message)s",
datefmt="[%X] ", handlers=[RichHandler()])
log = logging.getLogger("rich")
console = Console()
# ====================================================================
def get_extension(path):
return os.path.splitext(path)[1]
def parse_arguments():
parser = argparse.ArgumentParser("hot-line-python")
parser.add_argument("-v", "--version", action="version",
version='%(prog)s: {version}'.format(version=__version__))
parser.add_argument("-V", "--verbose",
help="Provide info. on the current state of the chart creation.",
action="store_true")
parser.add_argument("input",
help="The path of the data file which hot-line will use to make the chart..")
parser.add_argument("output",
help="The location hot-line will write the newly created chart to.")
parser.add_argument("-t", "--title",
help="The title displayed on the chart.")
parser.add_argument("-x", "--xaxis",
help="The label used on the chart's x-axis.")
parser.add_argument("-y", "--yaxis",
help="The label used on the chart's y-axis.")
return parser.parse_args()
def get_y_values(data):
r = (data.loc[0])
return r[1::1]
def random_colour():
r = random.randint(0,255)
g = random.randint(0,255)
b = random.randint(0,255)
rgb = [r,g,b]
return rgb
# I could reduce the duplicated code by extracting the 'verbose' code out but I
# don't plan on touching this much after it's able to generate charts. So, I'm
# at a point where I don't care. If this script needs to expand, then I will
# worry about refactoring and making the code pretty then.
def build_chart(arguments):
column_data = []
column_headers = []
x_values = []
legend_items = []
if arguments.verbose is True:
tasks = ["Loading file: Complete",
"Parsing data: Complete",
"Building chart outline: Complete",
"Populating chart: Complete",
"All Done"]
tasks_total = len(tasks)
with console.status("[bold green]Performing magic...") as status:
try:
# 1. Load file...
extension = get_extension(arguments.input)
if extension == ".tsv":
data = pd.read_csv(arguments.input, sep="\t", header=None,
index_col=False, dtype='unicode')
elif extension == ".csv":
data = pd.read_csv(arguments.input, sep=",", header=None,
index_col=False, dtype='unicode')
else:
log.critical("File is neither a .csv or .tsv file. Unable to process.")
return
y_labels = get_y_values(data)
data = data[1:]
task = tasks.pop(0)
console.log(f"1/{tasks_total}. {task}")
# 2. Parse data...
for x in data.columns:
values = data.iloc[:, x]
column_data.append(values)
x_values = column_data[0]
column_data.pop(0)
task = tasks.pop(0)
console.log(f"2/{tasks_total}. {task}")
# If x_values contains duplicated values, they will
# need an 'ID' appended to each value. This part of
# the code checks for them so the code below will know
# to append it or not. If there are no duplicated
# entries, the appendaged is not needed.
dupes = False
for item in x_values.duplicated():
if item is False:
dupes = True
break
# Add an 'ID' tag to the x-axis values. This is done
# to stop duplicated values which causes errors in
# Bokeh.
x_range = []
if dupes == True:
counter = 0
for row_value in x_values:
x_range.append(f"{counter}: {row_value}")
counter = counter + 1
else:
x_range = x_values
# 3. Build chart outline...
p = figure(title=arguments.title,
x_range=FactorRange(* x_range),
x_axis_label=arguments.xaxis,
y_axis_label=arguments.yaxis,
sizing_mode="stretch_both")
p.axis.major_label_text_font_size = "12px"
p.axis.major_label_standoff = -10
task = tasks.pop(0)
console.log(f"3/{tasks_total}. {task}")
# 4. Populate chart...
# The reason for the '+ 1' and 'y - 1' when specifying
# an index value is to avoid O.B.O.B's. Program throws an
# error when y_label[0] is used, can only access from
# '1' or above. column_data starts at 0. So, the index
# (y) needs to drop down 1 from the current 'y' value
# -- which indicates where about in the loop the
# program is regarding the y_labels list.
for y in range (1, (len(y_labels) + 1)):
col = random_colour()
p.line(x_range,
column_data[y - 1],
legend_label=f"{y_labels[y]}",
line_color=random_colour(),
line_width=2)
task = tasks.pop(0)
console.log(f"4/{tasks_total}. {task}")
# 5. Write chart to disk...
output_file(arguments.output)
save(p)
task = tasks.pop(0)
console.log(f"5/{tasks_total}. {task}")
except IOError:
log.critical("File cannot be found.")
except Exception as e:
log.critical("Unrecoverable system error.")
console.print_exception()
else:
try:
# 1. Load file...
extension = get_extension(arguments.input)
if extension == ".tsv":
data = pd.read_csv(arguments.input, sep="\t", header=None,
index_col=False, dtype='unicode')
elif extension == ".csv":
data = pd.read_csv(arguments.input, sep=",", header=None,
index_col=False, dtype='unicode')
else:
log.critical("File is neither a .csv or .tsv file. Unable to process.")
return
y_labels = get_y_values(data)
data = data[1:]
# 2. Parse data...
for x in data.columns:
values = data.iloc[:, x]
column_data.append(values)
x_values = column_data[0]
column_data.pop(0)
# If x_values contains duplicated values, they will
# need an 'ID' appended to each value. This part of
# the code checks for them so the code below will know
# to append it or not. If there are no duplicated
# entries, the appendaged is not needed.
dupes = False
for item in x_values.duplicated():
if item is False:
dupes = True
break
# Add an 'ID' tag to the x-axis values. This is done
# to stop duplicated values which causes errors in
# Bokeh.
x_range = []
if dupes == True:
counter = 0
for row_value in x_values:
x_range.append(f"{counter}: {row_value}")
counter = counter + 1
else:
x_range = x_values
# 3. Build chart outline...
p = figure(title=arguments.title,
x_range=FactorRange(* x_range),
x_axis_label=arguments.xaxis,
y_axis_label=arguments.yaxis,
sizing_mode="stretch_both")
p.axis.major_label_text_font_size = "12px"
p.axis.major_label_standoff = 10
# 4. Populate chart...
# The reason for the '+ 1' and 'y - 1' when specifying
# an index value is to avoid O.B.O.B's. Program throws an
# error when y_label[0] is used, can only access from
# '1' or above. column_data starts at 0. So, the index
# (y) needs to drop down 1 from the current 'y' value
# -- which indicates where about in the loop the
# program is regarding the y_labels list.
for y in range (1, (len(y_labels) + 1)):
col = random_colour()
p.line(x_range,
column_data[y - 1],
legend_label=f"{y_labels[y]}",
line_color=random_colour(),
line_width=2)
# 5. Write chart to disk...
output_file(arguments.output)
save(p)
except IOError:
log.critical("File cannot be found.")
except Exception as e:
log.critical("Unrecoverable system error.")
console.print_exception()
def main():
args = parse_arguments()
build_chart(args)
if __name__ == "__main__":
main()

18
hot-line-python/requirements.txt

@ -0,0 +1,18 @@
bokeh==2.4.3
commonmark==0.9.1
Jinja2==3.1.2
MarkupSafe==2.1.1
numpy==1.23.1
packaging==21.3
pandas==1.4.3
pathlib==1.0.1
Pillow==9.2.0
Pygments==2.12.0
pyparsing==3.0.9
python-dateutil==2.8.2
pytz==2022.1
PyYAML==6.0
rich==12.5.1
six==1.16.0
tornado==6.2
typing-extensions==4.3.0

67
hot-line.asd

@ -0,0 +1,67 @@
(defsystem "hot-line"
:version "0.1.0"
:author "Craig Oates"
:license "MIT"
:depends-on ("clack"
"lack"
"caveman2"
"envy"
"cl-ppcre"
"uiop"
;; for @route annotation
"cl-syntax-annot"
;; HTML Template
"djula"
;; for DB
"datafly"
"sxql"
;; Additional Packages
#:clack-errors ; Error report (HTML/template views)
#:mito ; Database ORM
#:osicat ; Environment variables (dev/prod.)
#:ratify ; Utilites
#:sqlite ; Sqlite database ORM
#:hermetic ; Authentication
#:cl-fad ; Files and directories
#:serapeum ; Pagination
)
:pathname "src/"
:components (;; Caveman Files
(:file "config")
(:file "main")
(:file "db")
(:file "view")
;; hot-line Specific Files
(:file "models/app-constants")
(:file "models/user")
(:file "models/file")
(:file "services/storage")
(:file "services/authentication")
(:file "services/validation")
(:file "services/convert")
(:file "services/user-management")
(:file "services/db-management")
(:file "services/storage-management")
(:file "services/pagination")
(:file "services/routing")
;; Caveman Files
(:file "web"))
:description "A proof-of-concept website testing Python and its Bokeh library."
:build-operation "program-op" ;; leave as is
:build-pathname "hot-line-nellis"
:entry-point "hot-line:main"
:in-order-to ((test-op (test-op "hot-line/test"))))
(defsystem #:hot-line/tests
:author "Craig Oates"
:license "MIT"
:depends-on (#:hot-line
#:parachute)
:pathname "tests/"
:components ((:file "tests"))
:description "Test system for hot-line"
:perform (test-op (op s) (symbol-call :parachute :test :tests)))

45
src/config.lisp

@ -0,0 +1,45 @@
;; (in-package :cl-user)
(defpackage hot-line.config
(:use :cl)
(:import-from :envy
:config-env-var
:defconfig)
(:export :config
:*application-root*
:*static-directory*
:*template-directory*
:appenv
:developmentp
:productionp))
(in-package :hot-line.config)
(setf (config-env-var) "APP_ENV")
(defparameter *application-root* (asdf:system-source-directory :hot-line))
(defparameter *static-directory* (merge-pathnames #P"static/" *application-root*))
(defparameter *template-directory* (merge-pathnames #P"templates/" *application-root*))
(defconfig :common
`(:application-root ,(asdf:component-pathname (asdf:find-system :hot-line))
:databases ((:maindb :sqlite3 :database-name ,(merge-pathnames #P"db/hot-line.db")))))
(defconfig |development|
'())
(defconfig |production|
'())
(defconfig |test|
'())
(defun config (&optional key)
(envy:config #.(package-name *package*) key))
(defun appenv ()
(uiop:getenv (config-env-var #.(package-name *package*))))
(defun developmentp ()
(string= (appenv) "development"))
(defun productionp ()
(string= (appenv) "production"))

26
src/db.lisp

@ -0,0 +1,26 @@
;; (in-package :cl-user)
(defpackage hot-line.db
(:use :cl)
(:import-from :hot-line.config
:config)
(:import-from :datafly
:*connection*)
(:import-from :cl-dbi
:connect-cached)
(:export :connection-settings
:db
:with-connection))
(in-package :hot-line.db)
(defun connection-settings (&optional (db :maindb))
(cdr (assoc db (config :databases))))
(defun db (&optional (db :maindb))
(apply #'connect-cached (connection-settings db)))
(defmacro with-connection (conn &body body)
;; Added 'mito.connection:' to 'let' binding. To return to default,
;; just remove it so it just says '*connection*'. mito replaces
;; datafly.
`(let ((mito.connection:*connection* ,conn))
,@body))

56
src/main.lisp

@ -0,0 +1,56 @@
;; (in-package :cl-user)
(defpackage hot-line
(:use :cl)
(:import-from :hot-line.config
:config)
(:import-from :clack
:clackup)
(:export :start
:stop
#:main))
(in-package :hot-line)
(defvar *appfile-path*
(asdf:system-relative-pathname :hot-line #P"app.lisp"))
(defvar *handler* nil)
(defun start (&rest args &key server port debug &allow-other-keys)
(declare (ignore server port debug))
(when *handler*
(restart-case (error "Server is already running.")
(restart-server ()
:report "Restart the server"
(stop))))
(setf *handler*
(apply #'clackup *appfile-path* args)))
(defun stop ()
(prog1
(clack:stop *handler*)
(setf *handler* nil)))
#| 'main' Function Used For Starting Server From Script (I.E. Live Deployment)
================================================================================
https://lisp-journey.gitlab.io/web-dev/#building
The code below was taken from the URL above (with slight modifications). It's
main use is to make it easier to start the server via a script.
|#
(defun main ()
(start :server :hunchentoot :port 3002 :debug nil)
;; with bordeaux-threads
(handler-case (bt:join-thread
(find-if (lambda (th)
(search "hunchentoot" (bt:thread-name th)))
(bt:all-threads)))
(#+sbcl sb-sys:interactive-interrupt
#+ccl ccl:interrupt-signal-condition
#+clisp system::simple-interrupt-condition
#+ecl ext:interactive-interrupt
#+allegro excl:interrupt-signal
() (progn
(format *error-output* "Aborting.~&")
(clack:stop *handler*)
(uiop:quit 1)) ;; portable exit, included in ASDF, already loaded.
;; for others, unhandled errors (we might want to do the same).
(error (c) (format t "Woops, an unknown error occured:~&~a~&" c)))))

256
src/models/app-constants.lisp

@ -0,0 +1,256 @@
(defpackage #:app-constants
(:use #:cl)
(:export #:+false+
#:+true+
;; OTHER-ITEMS LIST SIZES
#:+default-other-items-size+
#:+single-other-items-size+
#:+small-other-items-size+
#:+large-other-items-size+
#:+extra-large-other-items-size+
;; PAGINATION SIZES
#:+default-page-size+
#:+extra-small-page-size+
#:+small-page-size+
#:+single-page-size+
#:+extra-large-page-size+
#:+large-page-size+
;; DIRECTORIES
#:+media-directory+
#:+uploads-directory+
;; GENERAL/GENERIC MESSAGES
#:+generic-fail+
#:+generic-success+
#:+nil-or-empty-string-used+
#:+undetermined-file-type+
;; SESSION MANAGEMENT
#:+incorrect-login-details+
#:+user-not-found+
;; STORAGE SECTION
#:+storage-directory-not-found+
#:+storage-directory-deleted+
#:+storage-file-already-exists+
#:+storage-file-deleted+
#:+storage-file-not-found+
#:+storage-file-successful-upload+
#:+storage-file-successfully-updated+
;; USER MANAGEMENT
#:+username-already-taken+
#:+new-user-added+
#:+user-deleted+
#:+user-not-authorised+
#:+user-role-updated+
#:+display-name-updated+
#:+password-updated+
#:+old-password-incorrect+))
(in-package #:app-constants)
#| Switched to `DEFINE-CONSTANT' from `DEFCONSTANT'.
================================================================================
Because this website uses Steel Bank Common Lisp (SBCL), I need to go through a
cycle of confirming changes to the constant values even though they have not
changed. This behaviour is explained in the SBCL Manual 2.1.3 2021-03 (Section
2.3.4 Defining Constants, page 5 (printed) page 13 (PDF)). The key part of the
section is,
'ANSI says that doing `DEFCONSTANT' of the same symbol more than once is
undefined unless the new value is eql to the old value.'
http://www.sbcl.org/manual/#Defining-Constants (this URL should provide the
latest information of the subject).
A workaround, provided by the SBCL Manual is to use the `DEFINE-CONSTANT' macro
instead of `DEFCONST'. By doing this, I can use Quickload to reload the code
(after a big change for example) and not have to repeat the cycle of 'updating'
the constants when they have not changed.
|#
(defmacro define-constant (name value &optional doc)
`(defconstant ,name (if (boundp ',name) (symbol-value ',name) ,value)
,@(when doc (list doc))))
#| SQLite does not have Boolean value types.
================================================================================
At the time of writing (February 2022), the website uses SQLite as its
database. So, I have made these constants to reduce hard-coded `1'
and/or `0' values when `TRUE' and `NIL'/`FALSE' values are want is
meant (in the code-base).
|#
(define-constant +false+ 0
"An integer representing 'false' (for SQLite mostly).")
(define-constant +true+ 1
"An integer representing 'true' (for SQLite mostly.")
;; These refer to the size of the 'Other X' lists displayed in the
;; 'Other X' sections of the content.html templates. Examples of the
;; 'Other' lists are 'Other Software Projects' and 'Other Artworks'.
(define-constant +default-other-items-size+ 10
"The default number of items to show in the 'other X' section of
content.html templates.")
(define-constant +single-other-items-size+ 1
"Display a single item in the 'other X' section of content.html
templates -- use for testing.")
(define-constant +small-other-items-size+ 5
"The number of items to show in the 'other X' section of
content.html templates.")
(define-constant +large-other-items-size+ 20
"The number of items to show in the 'other X' section of
content.html templates.")
(define-constant +extra-large-other-items-size+ 30
"The number of items to show in the 'other X' section of
content.html templates.")
;; The pagination code has a default value of 200 built-in. That is
;; why the amounts seems odd.
(define-constant +default-page-size+ 100
"The default value for the amount of items to display on a paginated
page.")
(define-constant +single-page-size+ 1
"Display only one item per paginated page -- use for testing.")
(define-constant +extra-small-page-size+ 25
"The value for a smaller than default amount of items on a paginated
page.")
(define-constant +small-page-size+ 50
"The value for a smaller than default amount of items on a paginated
page.")
(define-constant +large-page-size+ 150
"The value for a larger than default amount of items on a paginated
page.")
(define-constant +extra-large-page-size+ 250
"The value for a larger than default amount of items on a paginated
page.")
#| Alert Messages (add '| safe' to djula filter in HTML templates)
================================================================================
How the alerts are normally rendered in the HTML templates:
{% if alert %}{{alert | safe}}{% endif %}
The constants below are to be used with the `:ALERT' values when rendering an
HTML template as part of a 'defroute' in 'web.lisp'. The reason for these
string values being stored here instead of in-lining them when the :alert is
formatted is two-fold:
1. These messages can be multiple lines long which can make the code the
defroute a little messy.
2. Reduce repetitive code. The amount of repetitive code is not that high. But,
this will help.
The '| safe' filter (djula) is needed because the HTML tags are removed without
it. Refer to the djula manual for more information:
https://mmontone.github.io/djula/djula/Filters.html#format
|#
(define-constant +generic-success+
"<p class=\"ui-message-success\">Task completed. Great success!</p>"
"Alert message. Intended as a basic or placeholder message.")
(define-constant +media-directory+ "media"
"The /media directory holds the user's generic file uploads.")
(define-constant +uploads-directory+ "uploads"
"The directory which holds the user's uploaded .csv and .tsv files.")
(define-constant +generic-fail+
"<p class=\"ui-message-error\">Task failed.</p>"
"Alert message. Intended as a basic or placeholder message.")
(define-constant +nil-or-empty-string-used+
"<p class=\"ui-message-error\">An 'empty-string' was provided by the user (most likely), or the
website's code came across a 'nil' value instead of a string.</p>"
"Alert message. Intended as an error message for all user-input sections.")
(define-constant +undetermined-file-type+
"<p class=\"ui-message-error\">Cannot determine if file is a .csv or .tsv file.</p>"
"Alert message. Intended as an error message for all user-input sections.")
(define-constant +incorrect-login-details+
"<p class=\"ui-message-error\">Name and password do not match.</p>"
"Use as an alert message, intented for the login routes.")
(define-constant +user-not-found+
"<p class=\"ui-message-error\">No account found with those details.</p>"
"Use as an alert message, intented for the loging routes.")
(define-constant +storage-directory-deleted+
"<p class=\"ui-message-success\">Directory deleted from /storage.</p>"
"Use as an alert message, intented for the /storage routes.
This is used for relaying a directory within the /storage directory is
deleted. It does not mean the /storage directory is deleted.")
(define-constant +storage-directory-not-found+
"<p class=\"ui-message-error\">Directory not found in /storage.</p>"
"Use as an alert message, intented for the /storage routes.
Used to indicated a directory within /storage is not found and not the
/storage directory itself.")
(define-constant +storage-file-already-exists+
"<p class=\"ui-message-error\">A file with that name already exists.</p>"
"Use as an alert message, intended for the /storage routes.")
(define-constant +storage-file-deleted+
"<p class=\"ui-message-success\">File deleted.</p>"
"User as an alert message. Intended for the /storage routes.")
(define-constant +storage-file-not-found+
"<p class=\"ui-message-error\">File(s) could not be found.</p>"
"Use as an alert message. Intended for the /storage routes.")
(define-constant +storage-file-successful-upload+
"<p class=\"ui-message-success\">File uploaded. Great success!</p>"
"Use as an alert message. Intended for the /storage routes.")
(define-constant +storage-file-successfully-updated+
"<p class=\"ui-message-success\">File updated!</p>"
"Use as an alert message. Intended for the /storage routes.")
(define-constant +username-already-taken+
"<p class=\"ui-message-error\">Username already taken.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +new-user-added+
"<p class=\"ui-message-success\">New user added.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +user-deleted+
"<p class=\"ui-message-success\">User deleted.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +user-not-authorised+
"<p class=\"ui-message-error\">User is not authorised to make this change.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +user-role-updated+
"<p class=\"ui-message-success\">Role updated.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +display-name-updated+
"<p class=\"ui-message-success\">Display name changed.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +password-updated+
"<p class=\"ui-message-success\">Password updated.</p>"
"Use as an alert message, intented for the /user routes.")
(define-constant +old-password-incorrect+
"<p class=\"ui-message-error\">Old password is incorrect.</p>"
"Use as an alert message, intented for the /user routes.")

63
src/models/file.lisp

@ -0,0 +1,63 @@
(defpackage #:file
(:use #:cl
#:user)
(:export #:file))
(in-package #:file)
(defclass file ()
(
;; ((id
;; :documentation "The row Id. of the file stored in the database."
;; :col-type :integer
;; :initarg :id
;; :accessor id-of)
(filename
:documentation "The name of the file."
:col-type (:varchar 128)
:initarg :filename
:accessor filename-of)
(username :col-type user)
(slug
:documentation "The dev. friendly way to refer to the file. It
mostly is about giving options to shorten the URL needed to get a
file if the `NAME' is long and makes it cumbersome to refer to it
in the back-end/admin. controls or URL."
:col-type (:varchar 128)
:initarg :slug
:accessor slug-of)
(content-type
:documentation "The type of the file -- not the file
extension. `CONTENT-TYPE' refers to the file being more of a 'text'
file or 'image' file than it being a '.md' file or '.png' file. The
main reason for this property is so I can represent the text files
with a graphic when managing the uploaded files to /storage."
:col-type (:varchar 128)
:initarg :content-type
:accessor content-type-of)
;; (created_at
;; :documentation "The date this meta-data was added to the
;; database. This does not automatically mean it is the same time the
;; data-file was uploaded to the /storage directory (within the
;; website)."
;; :col-type (:varchar 64)
;; :initform (local-time:now)
;; :initarg :created_at
;; :accessor created_at)
;; (updated_at
;; :documentation "The last time the file was modified in the
;; database. This does not automatically align with the actual
;; data-file associated with this meta-data file. So, be careful when
;; making assumption about this property."
;; :col-type (:varchar 64)
;; :initarg :updated_at
;; :accessor updated_at))
)
(:documentation "The model used to describe `FILE' table in the
database. Essentially, this is the meta-data for the files uploaded
to the website and stored in the /storage directory. This class is
for keeping track of the files in /storage via the website's
database. It does not represent the actual file it is paired
with. The intended usage for this class is to provide ways to manage
the files in /storage from the website's (admin.) back-end.")
(:metaclass mito:dao-table-class))

59
src/models/user.lisp

@ -0,0 +1,59 @@
(defpackage #:user
(:use #:cl
#:hot-line.db
#:mito
#:app-constants)
;;(:import-from)
(:export #:user
#:seeds))
(in-package #:user)
(defclass user ()
(
;; ((id :col-type (:varchar 36)
;; :primary-key t)
(username
:documentation "The name the user uses to log into the website."
:col-type (:varchar 64)
:initarg :username
:accessor username-of)
(display-name
:documentation "The name used in the website GUI (the pretty name)."
:col-type (or (:varchar 128) :null)
:initarg :display-name
:accessor display-name-of)
(administrator
:documentation "States if user has admin. priveledges. At the time
of writing (21/01/2022), SQLite is the current database and it
does not have a Boolean datatype so '0' represents 'false' and '1'
represents 'true'. You will not come across '0' or '1' in the code
because of how mito maps the code to the database. But, you will
see it in the database if you view it directly."
:col-type :integer
:initarg :administrator
:initform app-constants:+true+ ; SQLite: 0 -> false 1 -> true.
:accessor is-administrator-p)
(password
:documentation "The user's password. It is hashed using
cl-pass. The password is 'deflated' into the database. This means
the password is storted as a hash and returned as the value the
user provided."
:col-type :text
:initarg :password
:accessor password-of
:inflate #'cl-pass:hash
;; :deflate #'cl-pass:hash
)
;; (created_at
;; :documentation "The date the user's account was created."
;; :col-type (:varchar 64)
;; :initarg :created_at
;; :accessor created_at)
;; (updated_at
;; :documentation "The last time the user logged into the website."
;; :col-type (:varchar 64)
;; :initarg :last-log-in
;; :accessor updated_at)
)
(:documentation "The model used to describe the `USER' table in the database")
(:metaclass mito:dao-table-class))

85
src/services/authentication.lisp

@ -0,0 +1,85 @@
(defpackage #:authentication
(:use #:cl
#:hermetic
#:sxql
;; #:datafly
#:ningle
#:mito
#:user)
(:import-from #:hot-line.db
#:connection-settings
#:db
#:with-connection)
(:export #:csrf-token
#:get-user-roles
#:request-params
#:get-current-user
#:get-user-id
#:flash-gethash))
(in-package #:authentication)
(defun csrf-token ()
"Cross-Site Request Forgery (CSRF) token."
(cdr (assoc "lack.session"
(lack.request:request-cookies ningle:*request*)
:test #'string=)))
(hermetic:setup
;; #' is needed. (hermetic:roles) generates infinite-loop when called
;; otherwise -- 'roles' called in other parts of code-base.
;; #' is shorthand for the 'function' operator (returns the function
;; object associated with the name of the function which is supplied
;; as an argument. Keep forgetting that.
:user-p #'(lambda (username)
(with-connection (db)
(mito:find-dao 'user::user :username username)))
:user-pass #'(lambda (username)
(user::password-of
(with-connection (db)
(mito:find-dao 'user::user :username username))))
:user-roles #'(lambda (username)
(cons :logged-in
(let ((user (with-connection (db)
(mito:find-dao
'user::user :username username))))
(and user
(= (user::is-administrator-p user) app-constants:+true+)
'(:administrator)))))
:Session ningle:*session*
:denied (constantly '(400 (:content-type "text/plain") ("Authentication denied"))))
(defun get-current-user()
"Returns the currently logged in user from the browser session."
(with-connection (db)
(mito:find-dao 'user::user
:id (gethash :id ningle:*session*))))
(defun get-user-id (username)
"Returns the Id. number of the specified `USERNAME' in the database."
(with-connection (db)
(mito:object-id
(mito:find-dao 'user::user :username username))))
(defun request-params (request)
"Loops through the HTTP `REQUEST' and creates a key-value pairing."
(loop :for (key . value) :in request
:collect (let ((*package* (find-package :keyword)))
(read-from-string key))
:collect value))
(defun get-user-roles()
"Returns a list of roles the current user has assigned to them.
This is mostly to check if the user is logged-in or has administration
privileges. You can then create if-blocks in the HTML templates and
control what the user can and cannot see or do."
(loop :for role :in (hermetic:roles)
:collect role
:collect t))
;; Copied over from rails-to-caveman project. I don't know what it
;; does exactly.
(defun flash-gethash (key table)
(let ((value (gethash key table)))
(remhash key table)
value))

52
src/services/convert.lisp

@ -0,0 +1,52 @@
(defpackage #:convert
(:use #:cl
#:app-constants)
(:export #:bool-to-checkbox
#:checkbox-to-bool
#:dimension-to-clean-string
#:universal-time-to-prefix))
(in-package #:convert)
(defun bool-to-checkbox (value)
"Converts `VALUE' so it can populate an HTML checkbox.
It is assumed you are converting a SQLite version of a Boolean so
either 1 (true) or 0 (false). If you need a traditional Boolean value,
DO NOT USE THIS FUNCTION."
(cond ((= value 0) "off")
((null value) "off")
(t "on")))
(defun checkbox-to-bool (value)
"Converts a HTML Checkbox `VALUE' to a Boolean.
The `VALUE' will either be 'on' or 'off'. 'Boolean' in this instance
is assuming you are using SQLite and need to convert `VALUE' to an
integer/number. If you are needing a traditional Boolean value, DO NOT USE
THIS FUNCTION."
(cond ((string= "on" value) +true+)
((string= "off" value) +false+)
((null value) +false+)))
(defun dimension-to-clean-string (dimension)
"Takes a string like '148.0d0' and changes to '148.00'.
This funciton is needed because of how the dimensions in the /art
section are rendered in the /art/edit.html template. The djula
template renders the dimensions as strings but the database stores
them as numbers. This means they are capable of possessing letters in
them -- like '148.0d0' -- which breaks when these values come into
contact with mito."
(if (not (null dimension))
(format nil "~,2F" dimension)
(format nil "")))
(defun universal-time-to-prefix ()
"Converts an encoded universal time to something a human can make sense of."
(multiple-value-bind
(second minute hour day month year)
(get-decoded-time)
(format nil "~d~2,'0d~d_~2,'0d~2,'0d~2,'0d"
year
month
day
hour
minute
second)))

60
src/services/db-management.lisp

@ -0,0 +1,60 @@
(defpackage #:db-management
(:use #:cl
#:mito
#:local-time
#:storage
#:user
#:file)
(:import-from #:hot-line.db
#:connection-settings
#:db
#:with-connection)
(:export #:get-distinct-column-totals
#:get-random-selection
#:seed-database))
(in-package #:db-management)
(defun seed-database()
"A quick way to reset the database."
(hot-line.db:with-connection (db)
(mito:ensure-table-exists 'user)
(mito:create-dao 'user:user
:username "admin"
:display-name "Dave from Accounting"
:administrator 1
:password "password")
(mito:ensure-table-exists 'file)
(mito:create-dao 'file:file
:filename "Testing.png"
:username (mito:find-dao 'user:user :username "admin")
:slug "testing"
:content-type "image/png")))
(defun get-distinct-column-totals (table column)
"Creates a list of distinct values and their totals in `COLUMN' in DB `TABLE'."
(with-connection (db)
(mito:retrieve-by-sql
(format nil
"SELECT ~S, COUNT(~S) AS col_totals FROM ~S GROUP BY ~S;"
column column table column))))
(defun get-random-selection (table amount exclude-id)
"Returns a random selection of rows from the `TABLE' in the database.
`AMOUNT' specifies how many rows to return and the `EXCLUDE-ID' is the row which
should NOT be returned. The intented use for this function is to generate a list
of 'other articles/artworks' Etc. when viewing a 'content.html' page. The need
for 'raw SQL' is because I couldn't find a way to apply the 'RANDOM()' function
using mito and sxql.
Please note, this function does not map the results to their classes like mito
usually does, especially when used with sxql. Because this functions main reason
to be is to provide a list for populating the 'Other X' columns in the
content.html templates, you will need to use the 'raw-date' djula filter (custom
made and in view.lisp) to render the date-time stamp."
(with-connection (db)
(mito:retrieve-by-sql
(format nil
"SELECT * FROM ~a WHERE ID != ~a ORDER BY RANDOM() LIMIT ~a"
table
exclude-id
amount))))

76
src/services/pagination.lisp

@ -0,0 +1,76 @@
(defpackage #:pagination
(:use #:cl)
(:export #:make-pagination))
(in-package #:pagination)
#| Found This Code on The Internet
================================================================================
Original Blog Post:
https://lisp-journey.gitlab.io/blog/lisp-for-the-web-pagination-and-cleaning-up-html/
Pagination Source Code:
https://gitlab.com/vindarel/abstock/-/blob/master/src/pagination.lisp
Djula HTML Template Code:
https://gitlab.com/vindarel/abstock/-/blob/master/src/templates/includes/pagination.html
I've modified the code a little bit to fit my needs but it's, for the most part,
as I found it (`BUTTON-RANGE' is the main thing I've added).
|#
(defun make-pagination (&key (page 1) (nb-elements 0) (page-size 200)
(max-nb-buttons 5))
"From a current page number, a total number of elements, a page size,
return a dict with all of that, and the total number of pages.
Example:
(get-pagination :nb-elements 1001)
;; =>
(dict
:PAGE 1
:NB-ELEMENTS 1001
:PAGE-SIZE 200
:NB-PAGES 6
:TEXT-LABEL \"Page 1 / 6\"
)
"
(let* ((nb-pages (get-nb-pages nb-elements page-size))
(max-nb-buttons (min nb-pages max-nb-buttons))
(button-list (build-pagination-list page max-nb-buttons)))
(serapeum:dict :page page
:nb-elements nb-elements
:page-size page-size
:nb-pages nb-pages
:button-range button-list
:max-nb-buttons max-nb-buttons
:text-label (format nil
"Page ~a / ~a" page nb-pages))))
(defun get-nb-pages (length page-size)
"Given a total number of elements and a page size, compute how many pages fit in there.
(if there's a remainder, add 1 page)"
(multiple-value-bind (nb-pages remainder)
(floor length page-size)
(if (plusp remainder)
(1+ nb-pages)
nb-pages)))
(defun build-pagination-list (current-page max-nb-buttons)
"Creates a list of numbers with values before and after `CURRENT-PAGE'.
This is a bit hacky because the list needs to have checks in the HTML
template (djula) to make sure it doesn't render negative numbers or
display page number beyond the maximun number of pages available,
`MAX-NB-BUTTONS'. With that said, it allows you to create a 'floating
list' of page numbers which updates itself based on what page the
viewer is on."
(let* ((start (- current-page max-nb-buttons))
(end (+ current-page max-nb-buttons)))
(range end :min start :step 1)))
(defun range (max &key (min 0) (step 1))
"Creates a list of number starting from `MIN' upto `MAX'.
Use `STEP' to specify the increment value for each entry. This is
basically the range function from Python."
(loop for n from min below max by step
collect n))

562
src/services/routing.lisp

@ -0,0 +1,562 @@
(defpackage #:routing
(:use #:cl
#:caveman2
#:hot-line.config
#:hot-line.view
#:hot-line.db
#:app-constants
#:datafly
#:sxql
#:local-time
#:sqlite
#:cl-pass
#:storage
#:validation
#:authentication
#:user-management
#:storage-management
#:convert
#:hermetic
#:pagination)
(:export
;; CHARTING SECTION
#:create-chart
#:create-chart1
;; STORAGE SECTION
#:add-storage-file
#:delete-storage-file
#:update-storage-file
;; SESSION MANAGEMENT
#:attempt-login
#:log-out
;; USER MANAGEMENT
#:add-user
#:delete-user
#:sign-up-user
#:update-role
#:update-display-name
#:update-password))
(in-package #:routing)
#| Routing.lisp (The overflow file for web.lisp)
================================================================================
In here is essentially the overflow of code from the web.lisp file. When the
code in one of 'defroute' macros needs breaking out into its own
function (defined with a 'defun'), I add that function into this file. The
reason why is so I can keep the website routes free from clutter (as much as I
can help it). By using something like the origami package in Emacs, I can reduce
the routes to their first line so I an quickly scan through the file and
navigate to the function I need to work next.
You will find, the functions in here tend to deal the HTTP POST requests almost
exclusively. At the time of writing (August 2022), I have not had any need to
include a function which deals with a HTTP GET request. But, I intend to add
them to this file unless this file becomes too unwieldy.
|#
;; CHARTING SECTION
(defun create-chart (request)
"Builds the chart by calling out to Python and returns the file created by it."
(destructuring-bind
(&key title content-file x-axis y-axis &allow-other-keys)
(authentication:request-params request)
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user))
(alert-message nil)
(checks-failed? nil)
(status-code nil)
(python-output nil)
(file-type (car (last content-file)))
(data-bag `(:token ,(authentication:csrf-token)
:user ,current-user
:roles ,(authentication:get-user-roles))))
(cond ((null content-file)
(setf alert-message +storage-file-not-found+)
(setf checks-failed? t))
((and (not (string-equal "text/csv" file-type))
(not (string-equal "text/tsv" file-type))
(not (string-equal "text/tab-separated-values" file-type)))
(setf alert-message +undetermined-file-type+)
(setf checks-failed? t)
(setf status-code 301))
((or (null (directory-exists-p username +uploads-directory+))
(null (directory-exists-p username "")))
(setf alert-message +storage-directory-not-found+)
(setf checks-failed? t)
(setf status-code 301))
(t (setf checks-failed? nil)
(setf status-code 201)))
(if (not checks-failed?)
(progn
;; File is stored on server so it available to hot-line-python.
;; hot-line-python is called from this website but is run in
;; seperate process to this website.
(let* ((sanitised-filename (clean-filename (second content-file)))
(sanitised-filename-root (pathname-name sanitised-filename))
(prefixed-filename (format nil "line_~a--~a.html"
(convert:universal-time-to-prefix)
sanitised-filename-root))
(temp-file-path ; For input .csv/.tsv file uploaded to server.
(make-path username
+uploads-directory+
sanitised-filename))
(output-path
(make-path username "" prefixed-filename)))
(storage:store-file username
+uploads-directory+
(format nil sanitised-filename)
content-file)
(setf python-output (uiop:run-program
(list (merge-pathnames
"hot-line-python/venv/bin/python"
hot-line.config::*application-root*)
(format nil "~a"
(merge-pathnames
"hot-line-python/app/hot-line-python.py"
hot-line.config::*application-root*))
;; "-V" ; For verbose output.
(format nil "-t ~a" title)
(format nil "-x ~a" x-axis)
(format nil "-y ~a" y-axis)
(format nil "~a" temp-file-path)
(format nil "~a" output-path))
:output :string
:error-output :string))
(storage:remove-file username +uploads-directory+
sanitised-filename))))
(if (and (not (search "CRITICAL" python-output))
(null alert-message))
(setf alert-message +generic-success+)
(setf alert-message +generic-fail+))
(format t "~@s" python-output)
`(,status-code () (,(hot-line.view:render
"user/dashboard.html"
(append data-bag
`(:alert ,alert-message
:storage-files
,(reverse
(storage:get-file-names
(storage:get-files-in-directory
username "")))
:python-output ,python-output))))))))
;; Storage Section
(defun add-storage-file (request)
"Stores the meta-data and data-file of an uploaded file (to the website).
The CSRF-Token and Logged-in checks should already be completed before calling
this function."
(destructuring-bind
(&key filename slug content-type content-file &allow-other-keys)
(authentication:request-params request)
(cond ((or (storage-management:get-file-from-db :slug slug)
(storage-management:get-file-from-db :filename filename))
`(303 () (,(hot-line.view:render
"storage/add.html"
`(:alert
,app-constants:+storage-file-already-exists+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:roles ,(authentication:get-user-roles))))))
(t (let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user))
(file-count (storage-management:get-file-count username)))
(storage-management:add-file-to-db filename username ; content-type
(caddr content-file)
slug)
(storage:store-file username app-constants:+media-directory+
filename content-file)
`(201 () (,(hot-line.view:render
"storage/manage.html"
`(:alert
,app-constants:+storage-file-successful-upload+
:token ,(authentication:csrf-token)
:root-url ,(format nil "storage/manage/~a" username)
:user ,current-user
:roles ,(authentication:get-user-roles)
:storage-count
,(storage-management:get-file-count username)
:categories
,(db-management:get-distinct-column-totals
"file" "content_type")
:storage-files
,(storage-management:get-paginated-files
username 1 +default-page-size+ :filename)
:pagination ,(pagination:make-pagination
:page 1
:page-size +default-page-size+
:nb-elements file-count))))))))))
(defun delete-storage-file (request)
"Deletes the data-file in the logged-in user's /storage directory."
(destructuring-bind
(&key filename &allow-other-keys)
(authentication:request-params request)
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user))
(token (authentication:csrf-token))
(roles (authentication:get-user-roles))
(alert-message nil))
(if (null (storage:file-exists-p username "" filename))
(setf alert-message +storage-file-not-found+)
(progn
(setf alert-message +storage-file-deleted+)
(storage:remove-file username "" filename)))
`(303 () (,(hot-line.view:render
"user/dashboard.html"
`(:alert ,alert-message
:token ,token
:user ,current-user
:roles ,roles
:storage-files ,(reverse
(storage:get-file-names
(storage:get-files-in-directory
username ""))))))))))
(defun update-storage-file (request)
"Updates the meta-data of a data-file stored in the /storage directory.
The function does not alter the actual data-file. The thinking is if you want to
edit the actual data-file, you should just delete the file stored on the
server (in /storage) and upload a new one. This function is for changing the
`FILENAME', `CONTENT-TYPE' and `SLUG' (I.E. the things you use to reference the
files in /storage when logged into the site)."
(destructuring-bind
(&key id filename content-type slug &allow-other-keys)
(authentication:request-params request)
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user))
(original-db-file-name (file::filename-of
(storage-management:get-file-from-db :id id))))
(cond ((null (storage-management:get-file-from-db :id id))
`(303 () (,(hot-line.view:render "storage/manage.html"
`(:alert ,app-constants:+storage-file-not-found+
:token ,(authentication:csrf-token)
:user ,current-user
:root-url ,(format nil "storage/manage/~a" username)
:roles ,(authentication:get-user-roles)
:storage-count
,(storage-management:get-file-count username)
:categories
,(db-management:get-distinct-column-totals
"file" "content_type")
:storage-files
,(storage-management:get-all-files-from-db
username :filename)
:pagination ,(make-pagination
:page 1
:page-size +default-page-size+
:nb-elements
(storage-management:get-file-count
username)))))))
(t ;; Read as 'ignore inner code-block of UNLESS the file exists'.
(unless (storage:file-exists-p username app-constants:+media-directory+ filename)
(storage:rename-content-file username app-constants:+media-directory+
original-db-file-name filename))
(storage-management:edit-file-in-db id filename content-type slug)
`(201 () (,(hot-line.view:render "storage/manage.html"
`(:alert
,app-constants:+storage-file-successfully-updated+
:token ,(authentication:csrf-token)
:user ,current-user
:root-url ,(format nil "storage/manage/~a" username)
:roles ,(authentication:get-user-roles)
:storage-count
,(storage-management:get-file-count username)
:categories
,(db-management:get-distinct-column-totals
"file" "content_type")
:storage-files
,(storage-management:get-all-files-from-db
username :filename)
:pagination ,(make-pagination
:page 1
:page-size +default-page-size+
:nb-elements
(storage-management:get-file-count
username)))))))))))
;; Session Management
(defun attempt-login (request)
"Attempts to log the user into the website.
Redirects the user depending on how successful the log-in attempt
is. Updates the session id and password if log-in is
successful. `REQUEST' consists of the body parameters of the log-in
attempt Derived from the log-in form comprised in /session and
/login."
(destructuring-bind
(&key username password authenticity-token &allow-other-keys)
(authentication:request-params request)
(if (not (string= authenticity-token (authentication:csrf-token)))
`(403 (:content-type "text/plain") ("Denied"))
(let ((params (list :|username| username :|password| password)))
(hermetic:login params
;; Successful log-in attempt.
(progn
(setf
;; Set session Id. to the logged in user.
(gethash :id ningle:*session*)
(authentication:get-user-id username)
;; Set the users password (for session)
(gethash :password ningle:*session*) password)
`(303 (:location "/dashboard")))
;; Failed log-in attempt.
(hot-line.view:render "user/log-in.html"
`(:alert ,+incorrect-login-details+
:token ,(authentication:csrf-token)))
;; No user found.
(hot-line.view:render "user/log-in.html"
`(:alert ,+user-not-found+
:token ,(authentication:csrf-token))))))))
(defun log-out (request)
"Logs the current user out of the browsing session."
(destructuring-bind
(&key username authenticity-token &allow-other-keys)
(authentication:request-params request)
(if (not (string= authenticity-token (authentication:csrf-token)))
`(403 (:content-type "text/plain") ("Denied"))
(hermetic::logout
;; Successful log-out.
(progn (authentication::flash-gethash :id ningle:*session*)
'(303 (:location "/")))
;; Failed log-out
'(303 (:location "/"))))))
;; User Management
(defun sign-up-user (request)
"Creates a new user via the sign-up section of site."
(destructuring-bind
(&key username display-name password &allow-other-keys)
(authentication:request-params request)
(let* ((alert-message nil)
(can-create-user? nil))
(cond ((user-management:user-in-db-p :username username)
(setf alert-message +username-already-taken+))
((find t (mapcar #'validation:string-is-nil-or-empty-p
`(,username ,display-name ,password)))
(setf alert-message +nil-or-empty-string-used+))
(t (setf alert-message +new-user-added+)
(setf can-create-user? t)))
(if (equal can-create-user? t)
(progn
(user-management:add-user-to-db
username display-name +false+
password)
(storage:ensure-directory-exists username "uploads")
(attempt-login request))
`(303 () (, (hot-line.view:render
"sign-up.html"
`(:alert ,+username-already-taken+
:username ,username
:display-name ,display-name
:token ,(authentication:csrf-token)))))))))
(defun add-user (request)
"Adds a new user to the database by a currently logged in (I.E. admin.) user.
`REQUEST' contains the data provided by the HTML form used to specify
the new user's data."
(destructuring-bind
(&key authenticity-token username display-name administrator
password &allow-other-keys)
(authentication:request-params request)
(cond ((if (user-management:user-in-db-p :username username)
`(303 () (,(hot-line.view:render
"user/add.html"
`(:alert ,+username-already-taken+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:roles ,(authentication:get-user-roles)))))))
;; TODO: Add validation for add-user arguments.
(t (progn
(user-management:add-user-to-db
username display-name
(convert:checkbox-to-bool administrator) password)
(storage:ensure-directory-exists username "uploads")
`(201 () (,(hot-line.view:render
"user/index.html"
`(:alert ,+new-user-added+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))))))
(defun delete-user (request)
"Deletes a user from the database.
`REQUEST' contains the data which specifies which user should be
deleted. It typically is provided by a HTML form."
(destructuring-bind
(&key username &allow-other-keys)
(authentication:request-params request)
(cond ((not (user-management:user-in-db-p :username username))
`(303 () (,(hot-line.view:render
"user/index.html"
`(:alert ,+user-not-found+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))
((or (and (equal +true+ (user::is-administrator-p (authentication:get-current-user)))
(user-management:user-in-db-p :username username))
(string= username (user::username-of (authentication:get-current-user))))
(let ((username-being-deleted (user::username-of (authentication:get-current-user))))
(user-management:delete-user-from-db :username username)
(storage:remove-directory username "")
(if (string= username username-being-deleted)
(log-out request)
`(201 () (,(hot-line.view:render
"user/index.html"
`(:alert ,app-constants:+user-deleted+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))))
(t (format nil "Well then, it looks like you managed to the
website into a right old pickle. For you to have got here,
you must have been snooping around. Fair play, you broke
the website.")))))
(defun update-role (request)
"Give or remove admin. privileges to user specified in `REQUEST'."
(destructuring-bind (&key username administrator &allow-other-keys)
(authentication:request-params request)
(cond ((null (user-management:user-in-db-p :username username))
`(303 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,+user-not-found+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles))))))
((not (= (user::is-administrator-p (authentication:get-current-user))
+true+))
`(303 () ,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,+user-not-authorised+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator")))))
(t (user-management:update-user-administration-role
username (convert:checkbox-to-bool administrator))
`(201 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,+user-role-updated+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator")))))))))
(defun update-display-name (request)
"Change the username of the user specified in `REQUEST'."
(destructuring-bind (&key username display-name &allow-other-keys)
(authentication:request-params request)
(format t "[INFO] SESSION: ~A" (gethash :password ningle:*session*))
(cond ((equal nil (user-management:user-in-db-p :username username))
`(303 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,app-constants:+user-not-found+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))
(t (user-management:update-user-display-name username display-name)
`(201 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,+display-name-updated+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator")))))))))
(defun update-password (request)
"Change the password of the user specified in `REQUEST'."
(destructuring-bind (&key username old-password new-password &allow-other-keys)
(authentication:request-params request)
(cond ((equal nil (user-management:user-in-db-p :username username))
`(303 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,app-constants:+user-not-found+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))
((if (and (not old-password) (hermetic:role-p :administrator))
(progn
(user-management:update-user-password username new-password)
`(201 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,app-constants:+password-updated+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))))
((equal nil
(cl-pass:check-password old-password
(user::password-of (user-management:user-in-db-p
:username username))))
`(303 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,app-constants:+old-password-incorrect+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator"))))))
(t (user-management:update-user-password username new-password)
`(201 () (,(hot-line.view:render (user-management:get-crud-redirect-url)
`(:alert ,app-constants:+password-updated+
:token ,(authentication:csrf-token)
:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:roles ,(authentication:get-user-roles)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator")))))))))

127
src/services/storage-management.lisp

@ -0,0 +1,127 @@
(defpackage #:storage-management
(:use #:cl
#:file
#:mito
#:local-time
#:storage)
(:import-from #:hot-line.db
#:connection-settings
#:db
#:with-connection)
(:export #:add-file-to-db
#:delete-file-from-db
#:edit-file-in-db
#:get-all-files-from-db
#:get-file-count
#:get-files-from-db
#:get-file-from-db
#:get-paginated-files))
(in-package #:storage-management)
(defun add-file-to-db (filename owner content-type slug)
"Adds an entry to the 'file' table in the database.
`CONTENT-TYPE' is more like a file extension (png, jpg, pdf, md Etc.) than
anything else. I've added to so I can differentiate the different types of files
in the website's GUI -- when managing the files in the Admin./backend parts.
NOTE: This only stores the meta-data of the uploaded file. You must use the
'storage' package to deal with the data-file associated with the database
entry."
(with-connection (db)
(mito:create-dao 'file
:filename filename
:owner owner
:content-type content-type
:slug slug)))
(defun delete-file-from-db (&key (id nil) (filename nil) (slug nil))
"Deletes the specified entry in the 'file' database.
It does not delete the actual data-file. You will need to use the 'storage'
package to do that. The database entry is the meta-data used by the
Admin./backend of the website to manage the functionality for the /storage
section."
(with-connection (db)
(cond ((and (not filename) (not slug))
(mito:delete-dao
(mito:find-dao 'file:file :id id)))
((and (not id) (not slug))
(mito:delete-dao
(mito:find-dao 'file:file :filename filename)))
((and (not id) (not filename))
(mito:delete-dao
(mito:find-dao 'file:file :slug slug))))))
(defun edit-file-in-db (id filename content-type slug)
"Updates the specified entry in the 'file' table in the database.
The entry is identified via the `ID'. That is the only argument which will not
change."
(with-connection (db)
(let ((file-to-update
(mito:find-dao 'file:file :id id)))
(setf (file::filename-of file-to-update) filename
(file::content-type-of file-to-update) content-type
(file::slug-of file-to-update) slug)
(mito:save-dao file-to-update))))
(defun get-file-count (&optional (owner nil))
"Returns the total number of entries in the 'storage' database table."
(with-connection (db)
(if (null owner)
(mito:count-dao 'file:file)
(mito:count-dao 'file:file :owner owner))))
(defun get-all-files-from-db (username order)
"Retrieves all the entries in the 'file' database for `USERNAME'.
It does not retrieve the data-files associated with the entries in the
database. You will need to use the 'storage' package alongside this to do
that. This function/package only deals with the meta-data for the files stored
in the /storage directory.
NOTE: `USERNAME' is used here (column name in the DB) but it refers to 'owner'
in the actual DB table I have used `USERNAME' here because it easier to think of
it that way from a programming point-of-view From the database's point-of-view,
'owner' can refer to more than just `USERNAME' I've just settled on that from a
programming perspective."
(with-connection (db)
(mito:select-dao 'file:file
(sxql:where (:= :owner username))
(sxql:order-by order))))
(defun get-files-from-db (username order amount &key reverse)
"Returns all the entries in the 'file' table in the database.
These files are the meta-data of the files stored in the /storage
directory. They are not the actual file. You would use this when you want to
manage the files in the /storage directory (admin./backend of website)."
(with-connection (db)
(mito:select-dao 'file:file
(sxql:where (:= :owner username))
(if reverse
(sxql:order-by (:desc order))
(sxql:order-by order))
(sxql:limit amount))))
(defun get-file-from-db (&key (id nil) (filename nil) (slug nil))
"Retrieves the specified entry from the 'file' table in the database.
It only returns the meta-data for the file stored in the /storage directory. Use
the 'storage' package alongside this one to work with the data-file and the
meta-data of the data-file."
(with-connection (db)
(cond ((and (not filename) (not slug))
(mito:find-dao 'file:file :id id))
((and (not id) (not slug))
(mito:find-dao 'file:file :filename filename))
((and (not id) (not filename))
(mito:find-dao 'file:file :slug slug))
(t nil))))
(defun get-paginated-files (username page page-amount order)
"Retrieves a list of rows from the 'file' database with an offset.
The offset is determined by watch page the viewer is on and `PAGE' and
`PAGE-AMOUNT'."
(with-connection (db)
(mito:select-dao 'file:file
(sxql:order-by order)
(sxql:where (:= :owner username))
(sxql:offset (* (- page 1) page-amount))
(sxql:limit page-amount))))

167
src/services/storage.lisp

@ -0,0 +1,167 @@
(defpackage #:storage
(:use #:cl)
(:export #:directory-exists-p
#:ensure-directory-exists
#:file-exists-p
#:get-files-in-directory
#:get-file-names
#:get-latest-file-type
#:make-path
#:open-file
#:open-binary-file
#:open-text-file
#:remove-directory
#:remove-file
#:rename-content-file
#:rename-directory
#:store-file))
(in-package #:storage)
(defun directory-exists-p (username directory)
"Checks to see if the specified diretory exists.
The directories path is returned if it does exist and `NIL' is
returned if the directory cannot be found."
(cl:probe-file (make-path username directory "")))
(defun ensure-directory-exists (username directory)
"The project's standardised way to call `ENSURE-DIRECTORY-EXISTS'.
If the directory exists, the full (absolute) path is
returned (equating to `T', otherwiser `NIL' it returned."
;; The empty string for `SLUG' (3rd arg.) is used because
;; `MAKE-PATH' can form paths for files. In this instance, only the
;; directory needs to be formed. The empty string kinda acts like
;; `NIL' but it is a bit of a hack, I will admit.
(ensure-directories-exist (make-path username directory "")))
(defun file-exists-p (username subdirectory slug)
"This project's standardised way to call `CL:PROBE-FILE'.
If the file exists, the full (absolute) path is returned (equates to
`T', otherwise `NIL' is returned."
(cl:probe-file (make-path username subdirectory slug)))
(defun get-files-in-directory (username directory)
""
(uiop:directory-files (make-path username directory "")))
(defun get-file-names (filenames)
""
(mapcar #'(lambda (x) (file-namestring x)) filenames))
(defun get-latest-file-type (old-file new-file)
"Gets the file type of the specified `FILE', return `NIL' if no file found"
(if (string= "" (cadr new-file))
(file::content-type-of old-file)
(caddr new-file)))
(defun make-path (username subdirectory slug)
"Forms the path used to save a file.
Storage path:
`*APPLICATION-ROOT*'/storage/`USERNAME'/`SUBDIRECTORY'/`SLUG'
Each user has their own directory in /storage. This is so I can build a media
manager at a later date -- I had not got around to writing it at the time I
implemented this function/feature. I decided to go with `USERNAME' and
not (user-)`ID' is because I wanted to easily identify the directories in
/storage."
(merge-pathnames (format nil "storage/~A/~A/~A"
username subdirectory slug)
hot-line.config::*application-root*))
(defun open-binary-file (username subdirectory slug)
"Reads the file stored in the /storage directory."
(with-open-file (stream
(make-path username subdirectory slug)
:element-type '(unsigned-byte 8))
(let* ((length (file-length stream))
(buffer (make-array length
:element-type '(unsigned-byte 8))))
(read-sequence buffer stream)
(values buffer length))))
(defun open-text-file (username subdirectory slug)
"Reads the text (.md) file stored in the /storage directory."
(with-open-file (stream (make-path username subdirectory slug))
(let ((data (make-string (file-length stream))))
(read-sequence data stream)
data)))
(defun remove-directory (username subdirectory)
"Deletes an entire sketchbook directory in /storage (not database).
Path template: `*APPLICATION-ROOT*'/storage/`USERNAME'/`SUBDIRECTORY'/'
- https://edicl.github.io/cl-fad/#delete-directory-and-files
- https://stackoverflow.com/questions/24350183/how-do-i-delete-a-directory-in-common-lisp
'cl-fad' (files and directories) is a wrapper package over the various
Common Lisp implementions to aid in keeping your Common Lisp code
portable. At the time of writing (February 2022), the website is using
Steel Bank Common Lisp (SBCL) but this allow you to use something else
if you want or need to switch."
(cl-fad:delete-directory-and-files (make-path username subdirectory "")))
(defun rename-directory (username original-directory new-directory)
"Renames a sub-directory in the /storage directory.
`USERNAME' is the directory holding the one which is to be
changed. `ORIGINAL-DIRECTORY' is the 'source' (in the usual Linux/Bash
CLI sense). `NEW-DIRECTORY' is the name the `ORIGINAL-DIRECTORY' will
be changed to. There are various examples of the path
structure/template in other comments in this file. Have a look
around (don't want to repeat myself)."
(rename-file (make-path username original-directory "")
(make-path username new-directory "")))
(defun remove-file (username subdirectory slug)
"Deletes the specified file, stored in the /storage directory.
Before calling this function, make sure the file exists. You should have
'file-exists-p' available to you -- within this (storage) package."
(delete-file (make-path username subdirectory slug)))
(defun rename-content-file (username subdirectory old-slug new-slug)
"This project's standardised way to call `RENAME-FILE'."
(rename-file (make-path username subdirectory old-slug)
(make-path username subdirectory new-slug)))
(defun store-file (username subdirectory filename data)
"Stores the uploaded file to the /storage directory.
Storage path: `*APPLICATION-ROOT*'/storage/`USERNAME'/`SUBDIRECTORY'/`FILENAME'
`DATA' is the actual contents which will be written to the said path."
(let ((path (ensure-directories-exist
(make-path username subdirectory filename))))
(cond ((or (string= (caddr data) "application/gzip")
(string= (caddr data) "application/zip")
(string= (caddr data) "application/epub+zip"))
(uiop:copy-file (slot-value (car data) 'pathname) path))
(t
(with-open-file (stream
path
:direction :output
:if-does-not-exist :create
:element-type '(unsigned-byte 8)
:if-exists :supersede)
(write-sequence (slot-value (car data) 'vector) stream))))))
;;; PORTED FROM RAILS-TO-CAVEMAN PROJECT (expect it to be deleted)
;;; =============================================================================
;; (defun prin1-to-base64-string (object)
;; (cl-base64:string-to-base64-string (prin1-to-string object)))
;; (defun read-from-base64-string(string)
;; (values (read-from-string
;; (cl-base64:base64-string-to-string string))))
;;; This function requires ImageMagick so you will need to install it
;;; with 'sudo apt install imagemagick' (assuming you are on a
;;; Debian-based system).
(defun convert (id subdirectory original-file converted-file)
(let ((command (format nil "convert -geometry ~A ~A ~A"
(file-size converted-file)
(make-storage-pathname id subdirectory original-file)
(make-storage-pathname id subdirectory converted-file))))
(let ((message (nth-value 1
(uiop:run-program command
:ignore-error-status t
:error-output :string))))
(when message (error message)))))

111
src/services/user-management.lisp

@ -0,0 +1,111 @@
(defpackage #:user-management
(:use #:cl
#:user
#:mito
#:local-time)
(:import-from #:hot-line.db
#:connection-settings
#:db
#:with-connection)
(:export #:add-user-to-db
#:user-in-db-p
#:validate-user
#:get-all-users
#:get-crud-redirect-url
#:update-user-display-name
#:update-user-password
#:delete-user-from-db
#:get-total-user-count
#:update-user-administration-role))
(in-package #:user-management)
(defun user-in-db-p (&key (id nil) username)
"Looks for the user with either the specified `ID' or `USERNAME'.
Make sure you specify which argument you want to use with either :id or
:username. The `USER' information will be returned if found. `NIL' will be
returned if no entry with either the specified `ID' or `USERNAME' is found."
(if (not id)
(with-connection (db)
(mito:find-dao 'user:user :username username))
(with-connection (db)
(mito:find-dao 'user:user :id id))))
(defun add-user-to-db (username display-name administrator password)
"Add a new user to the database.
Make sure you have validated the data before calling this function."
(with-connection (db)
(mito:create-dao 'user
:username username
:display-name display-name
:administrator administrator
:password password
;; :created_at (local-time:now)
;; :updated_at (local-time:now)
)))
(defun get-all-users ()
"Returns a list of all registered users in the database."
(with-connection (db)
(mito:select-dao 'user:user
(sxql:order-by (:asc :display-name)))))
(defun update-user-display-name (username display-name)
"Updates the specified user's display name.
`USERNAME' is used to specify which user to update in the
database. `DISPLAY-NAME' is used to replace the old one. The old 'display-name'
is not needed here as no validation check is done. This function simply
overwrites the old 'display-name' with the new `DISPLAY-NAME'."
(with-connection (db)
(mito:execute-sql
(format nil "UPDATE user SET display_name = ~S WHERE username = ~S;"
display-name username))))
(defun update-user-administration-role (username is-administrator)
"Adds or removes the specified user's administrator privileges.
You can specify the user via `USERNAME'. At the time of writing, the
website uses SQLite as it database so the `IS-ADMINISTRATOR' value is
determined via a '0' or '1'. You should use the 'APP-CONSTANTS'
package (E.G. 'app-constants:+true+') to specify (or help convert)
when specifying this 'make-shift' Boolean value."
(with-connection (db)
(mito:execute-sql
(format nil "UPDATE user SET administrator = ~D WHERE username = ~S;"
is-administrator username))))
(defun update-user-password (username new-password)
"Updates the password of the specified user.
Use `USERNAME' to specify which user your want from the database. `NEW-PASSWORD'
will overwrite the old password. Make sure you have validated the user and they
have entered the old password as a validation check before you call this
function."
(with-connection (db)
(let ((user-to-update
(mito:find-dao 'user:user :username username)))
(setf (user::password-of user-to-update) new-password)
(mito:save-dao user-to-update))))
(defun delete-user-from-db (&key (id nil) username)
"Deletes the user from the database with the specified `ID' or `USERNAME'.
Make sure your specify which argument you want to user when calling the function
with either :id or :username."
(if (not id)
(with-connection (db)
(mito:delete-by-values 'user:user :username username))
(with-connection (db)
(mito:delete-by-values 'user:user :id id))))
(defun get-crud-redirect-url ()
"Returns the URL the user is redirected to after a CRUD-based HTTP POST.
The `USERNAME' is the username of the currently logged in user. The URL is
formed based on the type of roles the user has asigned to him/her. If the user
is an administrator, the URL returns 'user/index.html' and 'user/dashboard.html'
otherwise."
(let ((user (authentication:get-current-user)))
(if (= (user::is-administrator-p user) app-constants:+true+)
"user/index.html" ; Admin.
"user/dashboard.html"))) ; Regular User
(defun get-total-user-count ()
"Returns the total number of registered users in the database."
(with-connection (db)
(mito:count-dao 'user:user)))

45
src/services/validation.lisp

@ -0,0 +1,45 @@
(defpackage #:validation
(:use #:cl)
(:export
#:clean-filename
#:string-is-nil-or-empty-p
#:separate-files-in-web-request))
(in-package #:validation)
(defun validation-test ()
(format t "~&[INFO] Validation package reached."))
(defun request-params (request)
(loop :for (key . value) :in request
:collect (let ((*package* (find-package :keyword)))
(read-from-string key))
:collect value))
(defun string-is-nil-or-empty-p (string-to-test)
"Tests to see if `STRING-TO-TEST' is empty of just whitespace.
This is essentially the 'IsNullOrWhiteSpace' function I use in C#. It
expands the 'empty string' check to include a check to see if there is
string with just a '(white) space' in it."
(if (or (string= string-to-test " ")
(zerop (length string-to-test))
(null string-to-test))
t
nil))
(defun separate-files-in-web-request (request &optional request-value)
"Creates a new list of 'upload' files from a web `REQUEST'.
You will mostly use this for processing a multi-file upload (HTML)
form. The standard value for the 'name' attribute in (file) input tag
in the HTML form is `CONTENT-FILES' but you can use a different
name. Just specify it in this function's `REQUEST-VALUE' argument."
(loop :for item :in request
if (or (string= "CONTENT-FILES" (car item))
(string= request-value (car item)))
collect item))
(defun clean-filename (filename)
"Basically, slugifies it, removes whitespace, trims it Etc."
(let* ((lowercased (string-downcase filename))
(trimmed (string-trim '(#\Space #\Tab) lowercased))
(whitespaced (substitute #\- #\Space trimmed)))
whitespaced))

73
src/view.lisp

@ -0,0 +1,73 @@
;; (in-package :cl-user)
(defpackage hot-line.view
(:use :cl)
(:import-from :hot-line.config
:*template-directory*)
(:import-from :caveman2
:*response*
:response-headers)
(:import-from :djula
:add-template-directory
:compile-template*
:render-template*
:*djula-execute-package*)
(:import-from :datafly
:encode-json)
(:export :render
:render-json))
(in-package :hot-line.view)
(djula:add-template-directory *template-directory*)
(defparameter *template-registry* (make-hash-table :test 'equal))
(defun render (template-path &optional env)
(let ((template (gethash template-path *template-registry*)))
(unless template
(setf template (djula:compile-template* (princ-to-string template-path)))
(setf (gethash template-path *template-registry*) template))
(apply #'djula:render-template*
template nil
env)))
(defun render-json (object)
(setf (getf (response-headers *response*) :content-type) "application/json")
(encode-json object))
;;
;; Execute package definition
(defpackage hot-line.djula
(:use :cl)
(:import-from :hot-line.config
:config
:appenv
:developmentp
:productionp)
(:import-from :caveman2
:url-for))
;;; '(in-package' line added after default Caveman set-up. Needed for
;;; custom filters and functions. Not part of default Caveman set-up.
(in-package #:hot-line.djula)
(setf djula:*djula-execute-package* (find-package :hot-line.djula))
(defun insert-umami-script ()
"Outputs the script for my Umami instance (stats.abbether.net).
It provides either the dev. or prod. tracker depending on the
environment this website is running in."
(cond ((equal t (hot-line.config:productionp))
(format
nil
"<script async defer data-website-id=\"ea4c9748-aa78-445e-a0af-f4943f3c16cb\" src=\"https://stats.abbether.net/umami.js\"></script>"))
(t (format nil "<!-- Umami has not been set-up -->"))))
(djula::def-filter :chart-icon (filename)
(if (null filename)
(format nil "/images/file.png")
(let ((chart-type
(subseq filename 0 (search "_" filename))))
(format nil "/images/~a.png" chart-type))))

256
src/web.lisp

@ -0,0 +1,256 @@
;; (in-package :cl-user)
(defpackage hot-line.web
(:use #:cl
#:caveman2
#:hot-line.config
#:hot-line.view
#:hot-line.db
#:datafly
#:sxql
#:app-constants
#:local-time
#:sqlite
#:cl-pass
;; #:validation
#:authentication
#:user-management
#:hermetic
#:storage-management
#:convert
#:pagination
#:routing
#:storage)
(:export :*web*))
(in-package :hot-line.web)
;; for @route annotation
(syntax:use-syntax :annot)
;;
;; Application
(defclass <web> (<app>) ())
(defvar *web* (make-instance '<web>))
(clear-routing-rules *web*)
;;
;; Routing rules
(defroute "/" ()
(cond ((not (hermetic:logged-in-p))
(render "/index.html"
`(:token ,(authentication:csrf-token))))
(t (render "/index.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute "/about" ()
(cond ((not (hermetic:logged-in-p))
(render "about.html"
`(:token ,(authentication:csrf-token))))
(t (render "about.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute "/privacy" ()
(cond ((not (hermetic:logged-in-p))
(render "privacy.html"
`(:token ,(authentication:csrf-token))))
(t (render "privacy.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute "/navigation" ()
(cond ((not (hermetic:logged-in-p))
(render "/nav-menu.html"
`(:token ,(authentication:csrf-token))))
(t (render "/nav-menu.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))))
(defroute ("/sign-up" :method :get) ()
(if (hermetic:logged-in-p)
`(301 (:location "/dashboard"))
(render "sign-up.html" `(:token ,(authentication:csrf-token)))))
(defroute ("/sign-up" :method :post) (&key method)
(destructuring-bind (&key authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((hermetic:logged-in-p)
'(301 (:location "/dashboard")))
((string= "sign-up-user" method)
(routing:sign-up-user
(lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method)))))))
;; Admin/User Section
(defroute "/login" ()
(if (hermetic:logged-in-p)
`(301 (:location "/dashboard"))
(render "user/log-in.html"
`(:token ,(authentication:csrf-token)))))
(defroute ("/login" :method :post) (&key method)
(cond ((string= "login" method)
(routing:attempt-login (lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method))))))
(defroute ("/logout" :method :post) (&key method)
(cond ((string= "logout" method)
(routing:log-out (lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method))))))
(defroute ("/users") ()
(cond ((not (hermetic:logged-in-p))
(on-exception *web* 404))
((equal +true+ (user::is-administrator-p
(authentication:get-current-user)))
(render "user/index.html"
`(:user ,(authentication:get-current-user)
:users ,(user-management:get-all-users)
:user-count
,(user-management:get-total-user-count)
:categories
,(db-management:get-distinct-column-totals
"user" "administrator")
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))
(t (on-exception *web* 404))))
(defroute ("/user/add") ()
(cond ((not (hermetic:logged-in-p))
(on-exception *web* 404))
((equal +true+ (user::is-administrator-p
(authentication:get-current-user)))
(render "user/add.html"
`(:user ,(authentication:get-current-user)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles))))
(t (on-exception *web* 404))))
(defroute ("/user/edit/:username") (&key username)
(cond ((or (not (hermetic:logged-in-p))
(null (user-management:user-in-db-p :username username)))
(on-exception *web* 404))
((or (and (not (null (user-management:user-in-db-p
:username username)))
(equal +true+ (user::is-administrator-p
(authentication:get-current-user))))
(string= username (user::username-of
(authentication:get-current-user))))
`(200 () (, (render "user/edit.html"
`(:user-to-edit ,(user-management:user-in-db-p
:username username)
:user ,(authentication:get-current-user)
:roles ,(authentication:get-user-roles)
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles)
:session ,ningle:*session*)))))
(t (on-exception *web* 404))))
(defroute ("/user" :method :post) (&key method)
(destructuring-bind
(&key authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((not (hermetic:logged-in-p))
'(303 (:location "/login")))
((string= "add" method)
(routing:add-user (lack.request:request-body-parameters ningle:*request*)))
((string= "update-role" method)
(routing:update-role (lack.request:request-body-parameters ningle:*request*)))
((string= "update-display-name" method)
(routing:update-display-name (lack.request:request-body-parameters ningle:*request*)))
((string= "update-password" method)
(routing:update-password (lack.request:request-body-parameters ningle:*request*)))
((string= "delete-user" method)
(routing:delete-user (lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method)))))))
(defroute "/dashboard" ()
(if (not (hermetic:logged-in-p))
'(303 (:location "/login"))
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user)))
(render "user/dashboard.html"
`(:user ,current-user
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles)
:storage-files ,(reverse
(storage:get-file-names
(storage:get-files-in-directory
username ""))))))))
(defroute "/storage/download/:username/:filename" (&key username filename)
(if (and (hermetic:logged-in-p)
(string= username (user::username-of (authentication:get-current-user))))
(if (storage:file-exists-p username "" filename)
`(200 (:content-type "octet/stream")
,(storage:open-binary-file username "" filename)))
(on-exception *web* 404)))
(defroute ("/storage" :method :POST) (&key method)
(destructuring-bind
(&key filename authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((validation:string-is-nil-or-empty-p filename)
(hot-line.web::on-exception hot-line.web:*web* 404))
((not (hermetic:logged-in-p))
'(303 (:location "/login")))
((string= "delete-storage-file" method)
(routing:delete-storage-file
(lack.request:request-body-parameters ningle:*request*)))
(t (on-exception *web* 404)))))
;; This is where the chart stuff starts...
(defroute "/chart/add" ()
(if (not (hermetic:logged-in-p))
'(303 (:location "/login"))
(progn
(let* ((current-user (authentication:get-current-user))
(username (user::username-of current-user)))
(render "chart/add.html"
`(:user ,current-user
:token ,(authentication:csrf-token)
:roles ,(authentication:get-user-roles)))))))
(defroute ("/chartify" :method :post) (&key method)
(destructuring-bind
(&key authenticity-token &allow-other-keys)
(authentication:request-params
(lack.request:request-body-parameters ningle:*request*))
(cond ((not (string= authenticity-token (authentication:csrf-token)))
'(403 (:content-type "text/plain") ("Denied")))
((not (hermetic:logged-in-p))
'(303 (:location "/login")))
((string= "create-chart" method)
(routing:create-chart
(lack.request:request-body-parameters ningle:*request*)))
(t `(400 (:content-type "text/plain")
(,(format nil "Unknown method ~S" method)))))))
;;
;; Error pages
(defmethod on-exception ((app <web>) (code (eql 404)))
(declare (ignore app))
(merge-pathnames #P"_errors/404.html"
*template-directory*))

BIN
static/css/avenir-book.otf

Binary file not shown.

BIN
static/css/avenir.otf

Binary file not shown.

BIN
static/css/firacode-retina.ttf

Binary file not shown.

418
static/css/full-search.css

@ -0,0 +1,418 @@
.search-dashboard {
margin: 0px;
padding: 0px;
display: flex;
flex-direction: column;
}
.refinements-panel h2,
.refinements-panel p {
padding: 0px;
margin: 0px;
}
.refinements-panel p {
color : silver;
font-size: 12px;
}
#clear-refinements {
margin-bottom: 20px;
}
.ais-ClearRefinements-button {
font-size: 16px;
height: 40px;
width: 100%;
margin: 0px;
font-family: 'main', sans-serif;
color: white;
background-color: #0094ff;
box-shadow: 2px 2px 1px black;
border-radius: 4px;
padding: 10px;
cursor: pointer;
text-transform: uppercase;
border: none;
}
.ais-ClearRefinements-button:hover {
background: lightblue;
color: #0094ff;
text-decoration: none;
box-shadow: none;
}
.ais-ClearRefinements-button--disabled {
text-align: center;
padding: 10px;
border: 2px solid silver;
border-radius: 4px;
text-transform: uppercase;
color: silver;
background: transparent;
box-shadow: none;
}
.ais-ClearRefinements-button--disabled:hover {
background: transparent;
color: silver;
}
.search-refinement-list {
margin: 20px 0px;
border-bottom: 2px solid black;
}
#searchbox {
width: 100%;
margin-top: 20px;
margin-bottom: 20px;
margin-left: auto;
margin-right: auto;
}
.ais-SearchBox,
.ais-ClearRefinements-button {
max-width: 600px;
}
.search-results-container {
display: flex;
flex-direction: column;
}
.ais-SearchBox-form {
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
.ais-SearchBox-input {
width: 100%;
height: 40px;
padding: 10px;
border: 2px solid #0094ff;
border-radius: 4px;
font-family: 'main', sans-serif;
font-size: 16px;
}
.ais-SearchBox-input::placeholder {
font-family: 'main', sans-serif;
font-size: 16px;
}
.ais-SearchBox-form input:focus-visible {
outline: none;
}
.ais-SearchBox-submitIcon,
.ais-SearchBox-submit,
.ais-SearchBox-reset {
display: none;
}
.ais-RefinementList-list {
list-style: none;
padding: 0px;
margin: 0px 0px 40px 0px;
display: flex;
flex-direction: row;
width: 100%;
overflow: scroll;
}
.ais-RefinementList-label {
display: flex;
align-items: center ;
width: max-content;
margin-right: 6px;
}
.ais-RefinementList-label > span {
padding: 0px 2px;
}
.ais-RefinementList-checkbox {
width: 30px;
height: 30px;
margin: 0px;
}
.search-hits-panel {
width: -webkit-fill-available;
}
#hits {
width: 100%;
min-width: 350px;
display: flex;
flex-direction: column;
}
.ais-Hits-list {
padding: 0px;
margin: 0px;
display: flex;
flex-direction: column;
list-style: none;
}
.ais-Hits-item {
margin: 12px 0px 12px 0px;
max-height: 400px;
border: 2px #0094ff solid;
border-radius: 4px;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 3px 3px 3px black;
}
.ais-Hits-item:hover {
border-color: lightblue;
box-shadow: none;
}
#pagination {
display: block;
margin: 40px 0px 40px 0px;
overflow: auto;
width: 100%;
}
.ais-Pagination-list {
list-style: none;
padding: 0px;
margin: 0px;
display: flex;
justify-content: center;
align-items: center;
}
.ais-Pagination-item {
margin: 0px;
display: flex;
justify-content: center;
align-items: center;
max-width: 64px;
height: 50px;
}
/* Hack to change content of pagination links
================================================================================
https://stackoverflow.com/questions/48907242/how-can-i-remove-and-replace-content-in-a-html-tag-using-css
Because Meilsearch adds the pagination stuff via JavaScript, I can't set the
contents of the various HTML elements via the djula templates. This means I have
to update the content after it's loaded. I didn't want to do this via JavaScript
because I want to keep the JavaScript to a minimum. So, to fix the problem, I
came across a hack (see Stack Overflow URL above). You need to change the
visibility (to hidden) of the element(s) you want to change and then use the
'content' property to set the new text value. When that is done, you adjust the
element's visibility back to 'visible'.
I ACKNOWLEDGE THIS IS JACKY BUT IT WORKS.
*/
.ais-Pagination-item--firstPage a,
.ais-Pagination-item--previousPage a,
.ais-Pagination-item--nextPage a {
visibility: hidden;
}
/* Meilisearch not designed for extended pagination use (disabled 'Last' link)
================================================================================
https://github.com/meilisearch/documentation/issues/561
Meilisearch adds a 'Last Page' link with its built-in pagination features. With
that said, the people developing the project acknowledge Meilisearch's
pagination features are limited. They recommend on not using or relying on it
too much. See the URL above for more information.
Whilst developing this part of the site, I found the 'Last Page' link was
behaving inconsistently. You would click the link and it would jump ahead
(I.E. more than one page) but it took several clicks to get to the 'final/last
page'. Because of that, I've just turned it off by hiding it with the
style-rule below. Again, see URL above for more info. on this behaviour.
*/
.ais-Pagination-item.ais-Pagination-item--lastPage {
display: none;
}
.ais-Pagination-item--firstPage.ais-Pagination-item--disabled span,
.ais-Pagination-item--previousPage.ais-Pagination-item--disabled span,
.ais-Pagination-item--nextPage.ais-Pagination-item--disabled span {
visibility: hidden;
}
/* The new text is set here: See 'Hack to change content of pagination links'
note above for more information. */
.ais-Pagination-item--firstPage.ais-Pagination-item--disabled span:before {
content: "First";
visibility: visible;
}
.ais-Pagination-item--previousPage.ais-Pagination-item--disabled span:before {
content: "Prev.";
visibility: visible;
}
.ais-Pagination-item--nextPage.ais-Pagination-item--disabled span:before {
content: "Next";
visibility: visible;
margin-left: 8px;
}
.ais-Pagination-item--firstPage.ais-Pagination-item--disabled span:before,
.ais-Pagination-item--previousPage.ais-Pagination-item--disabled span:before,
.ais-Pagination-item--nextPage.ais-Pagination-item--disabled span:before {
width: 70px;
height: 39px;
text-transform: uppercase;
border: 2px solid silver;
border-radius: 4px;
text-align: center;
padding: 8px;
color: silver;
}
.ais-Pagination-item--firstPage a:before {
content: "First";
visibility: visible;
}
.ais-Pagination-item--previousPage a:before {
content: "Prev.";
visibility: visible;
}
.ais-Pagination-item--nextPage a:before {
content: "Next";
visibility: visible;
margin-left: 8px;
}
.ais-Pagination-item--firstPage a:link::before,
.ais-Pagination-item--previousPage a:link::before,
.ais-Pagination-item--nextPage a:link::before {
width: 100%;
height: 39px;
text-transform: uppercase;
background: #0094ff;
color: white;
border: 2px solid #0094ff;
border-radius: 4px;
text-align: center;
padding: 8px;
text-decoration: none;
box-shadow: 2px 2px 3px black;
}
.ais-Pagination-item--page a:link {
background: #0094ff;
color: white;
padding: 10px;
border-radius: 4px;
text-transform: uppercase;
text-decoration: none;
margin: 0px 2px;
box-shadow: 2px 2px 3px black;
}
.ais-Pagination-item--selected a:link,
.ais-Pagination-item--selected a:hover {
background: silver !important;
color: white;
box-shadow: none;
}
.ais-Pagination-item--page a:hover {
background: lightblue;
text-decoration: none;
box-shadow: none;
}
.ais-Pagination-item--firstPage a:hover::before,
.ais-Pagination-item--previousPage a:hover::before,
.ais-Pagination-item--nextPage a:hover::before {
background: lightblue;
border: 2px solid lightblue;
border-radius: 4px;
text-align: center;
color: #0094ff;
box-shadow: none;
}
.ais-Pagination-item--firstPage a:hover,
.ais-Pagination-item--previousPage a:hover,
.ais-Pagination-item--nextPage a:hover {
text-decoration: none !important;
}
@media (min-width:600px) {
.ais-ClearRefinements-button {
max-width: 200px;
}
}
@media (min-width:961px) {
.search-results-container {
flex-direction: row;
}
.refinements-panel {
max-width: 300px;
width: 100%;
margin-right: 20px;
}
.ais-RefinementList-list {
font-size: 12px;
flex-direction: column;
overflow: auto;
}
.ais-RefinementList-item {
margin: 2px 0px;
}
.ais-RefinementList-checkbox {
width: 20px;
margin-right: 4px;
}
#hits {
flex-direction: row;
flex-wrap: wrap;
}
.ais-Hits-list {
padding: 0px;
margin: 0px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
list-style: none;
}
.ais-Hits-item {
width: 200px;
height: 280px;
margin: 12px;
height: 100%;
display: flex;
flex-direction: column;
}
.ais-Hits-item .ui-link-card .ui-card-text {
height: 75px;
/* white-space: nowrap; */
text-overflow: ellipsis;
}
.ais-Hits-item .ui-link-card .ui-card-text .ui-card-secondary {
text-overflow: ellipsis;
}
}

BIN
static/css/iAWriterQuattroV.ttf

Binary file not shown.

1087
static/css/main.css

File diff suppressed because it is too large Load Diff

121
static/css/search.css

@ -0,0 +1,121 @@
.ui-search-container {
margin: 0px;
display: flex;
position: abosolute;
flex-direction: column;
align-items: center;
height: 120px;
border-bottom: 2px solid black;
}
#searchbox {
position: relative;
top: 40px;
width: 100%;
max-width: 600px;
}
.ais-SearchBox-form {
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
}
.ais-SearchBox-input {
width: 100%;
height: 40px;
padding: 10px;
border: 2px solid #0094ff;
border-radius: 4px 4px 0px 0px;
font-family: 'main', sans-serif;
font-size: 16px;
}
.ais-SearchBox-input::placeholder {
font-family: 'main', sans-serif;
font-size: 16px;
}
.ais-SearchBox-form input:focus-visible {
outline: none;
}
.ais-SearchBox-submit {
font-size: 16px;
height: 40px;
margin: 0px;
font-family: 'main', sans-serif;
color: white;
background-color: #0094ff;
border-radius: 4px;
padding: 10px;
cursor: pointer;
text-transform: uppercase;
border: none;
}
.ais-SearchBox-submitIcon,
.ais-SearchBox-submit,
.ais-SearchBox-reset,
#hits {
display: none;
}
#hits {
position: relative;
top:40px;
background: white;
width: calc(100% - 4px);
max-width: calc(600px - 4px);
border-right: 2px solid #0094ff;
border-bottom: 2px solid #0094ff;
border-left: 2px solid #0094ff;
border-radius: 0px 0px 4px 4px;
}
.ais-Hits-list {
list-style: none;
padding: 0px 2px;
margin: 0px;
}
.ais-Hits-list:first-child {
display: flex;
flex-wrap: nowrap;
flex-direction: column;
justify-content: flex-start;
}
.ais-Hits-item {
margin: 4px 0px 0px 0px;
}
.ui-link-search-card {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
}
.ui-link-search-card:link {
text-decoration: none;
float: left;
border-radius: 0px;
padding: 4px 0px;
}
.ui-link-search-card img {
width: 30px;
height: 30px;
display: inline;
float: left;
padding: 0px 6px;
}
.ui-link-search-card span {
vertical-align: middle;
}

BIN
static/images/audio-file.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
static/images/bar-line.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
static/images/bar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/images/bokeh-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
static/images/bokeh-simple.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
static/images/email.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/images/favicon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
static/images/file.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
static/images/git.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
static/images/gravatar.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
static/images/hero.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
static/images/instagram.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
static/images/line-chart-overview.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

BIN
static/images/line.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
static/images/logo

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/images/network-effect.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
static/images/nuget.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
static/images/screen-video-1.mp4

Binary file not shown.

BIN
static/images/screenshot-1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
static/images/screenshot-2.1.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
static/images/screenshot-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
static/images/site-logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
static/images/tiktok.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

47
templates/_errors/404.html

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>404 NOT FOUND</title>
<style type="text/css">
html {
height: 100%;
}
body {
height: 100%;
font-family: 'Myriad Pro', Calibri, Helvetica, Arial, sans-serif;
background-color: #DFDFDF;
}
#main {
display: table;
width: 100%;
height: 100%;
}
.error {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.error .code {
font-size: 1600%;
font-weight: bold;
}
.error .message {
font-size: 400%;
}
</style>
</head>
<body>
<div id="main">
<div class="error">
<div class="code">404</div>
<div class="message">NOT FOUND</div>
</div>
</div>
</body>
</html>

83
templates/about.html

@ -0,0 +1,83 @@
{% extends "layouts/base.html" %}
{% block title %}About Craig Oates{% endblock %}
{% block content %}
<div class="content-header">
<h1>About</h1>
</div>
<div class="flex-center">
<article>
<p><strong>
This website's aim is to help people take their spreadsheets and generate
interactive charts with them.
</strong>
The charts are created using a plotting/visualisation library written in
Python called Bokeh.
</p>
<h2 style="display:flex; align-items:center;">
<img style="margin-right: 12px;"
width="72px" src="images/bokeh-simple.png">
About Bokeh
</h2>
<p>
The following it taken from Bokeh's website,
</p>
<blockquote>
Bokeh is a Python library for creating interactive visualizations for modern
web browsers. It helps you build beautiful graphics, ranging from simple
plots to complex dashboards with streaming datasets. With Bokeh, you can
create JavaScript-powered visualizations without writing any JavaScript
yourself.
</blockquote>
<p>
If you would like to know more about Bokeh, please use the following links:
</p>
<ul>
<li><a href="https://bokeh.org">Bokeh's Main Website</a></li>
<li><a href="https://docs.bokeh.org">Bokeh's Documentation Website</a></li>
<li><a href="https://demo.bokeh.org/">Examples of charts made with Bokeh</a></li>
</ul>
<h2>The Site's Intended Users</h2>
<p>
The main group of people I had in mind when I developed this website were
people with little to no programming experience but worked with
spreadsheets. Because of this, the type of charts a user can create
utilises only a sub-set of Bokeh's functionality. This trade-off, though,
allows non-programmers to make interactive charts quickly and easily. From
there, they can explore and share said charts like it was another file on
your computer.
</p>
<h2>Accepted File Types</h2>
<p>
So far, this website only accepts the following file types:
</p>
<ul>
<li>Comma-Seperate values (.csv)</li>
<li>Tab-Seperated values (.tsv)</li>
</ul>
<p>
These types of files quite common and can be viewed in standard
spreadsheet software such as:
</p>
<ul>
<li><a href="https://www.libreoffice.org/download/download/">Libre Office: Calc</a></li>
<li><a href="https://www.microsoft.com/en-gb/microsoft-365/excel">Microsoft Excel</a></li>
</ul>
<p>
<strong>Note: </strong>
Because .csv and .tsv are 'plain text' files, you should be able to open them
using Notepad (Windows) or TextEdit (Mac OS).
</p>
<h2>About Craig</h2>
<p>
The short version is on my personal website you will find artworks I've
made, blog posts I've written, 3D models I've concocted, graphic design
projects I've put together and software I've developed. For the longer
version,
visit <a href="https://www.craigoates.net/about">craigoates.net/about</a>.
</p>
</article>
</div>
{% endblock %}

62
templates/chart/add.html

@ -0,0 +1,62 @@
{% extends "layouts/base.html" %}
{% block title %}{{user.display-name}}: Create a new chart{% endblock %}
{% block content %}
<div class="dashboard-header">
<h1>Create Chart</h1>
</div>
<div class="flex-center">
<h1>Preparing Your Data</h1>
</div>
<div class="flex-center">
<article style="max-width: 800px;">
<p>
For this website to build interactive charts for you, without you
needing writing any code, it needs to make some assumptions about
how the data is organised. Below is an example of how your
spreadsheet should look.
</p>
<div class="flex-center">
<img style="max-width: 1200px; width: 100%;" src="/images/line-chart-overview.png">
</div>
<p>
The 'Title', 'X-Axis' and 'Y-Axis' sections in the form are what
the chart uses to label the chart. As the form says, the are
optional. The only thing you need to submit is the actual
spreadsheet.
</p>
<p>
If your files are stores in Microsoft Excel's native format (.xlsx
or .xls), you will need to export them to either a Comma-Separated
Value (.csv) or Tab-Separated Value (.tsv) file. This website is
unable to process 'Excel files' at this moment in time.
</p>
</article>
</div>
<div class="ui-form">
<form class="ui-form-add" action="/chartify" method="post" enctype="multipart/form-data">
<input required type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input required type="hidden" name="METHOD" value="create-chart">
<fieldset>
<legend>Chart Details</legend>
<label>Title <em>(Optional)</em></label>
<input type="text" name="TITLE">
<label>X-Axis <em>(Optional)</em></label>
<input type="text" name="X-AXIS">
<label>Y-AXIS <em >(Optional)</em></label>
<input type="text" name="Y-AXIS">
<label>Spreadsheet <em>(.csv or .tsv file)</em></label>
<input type="file" name="CONTENT-FILE">
</fieldset>
<input class="ui-button-add" type="submit" value="Create Chart"/>
</form>
</div>
{% endblock %}

83
templates/index.html

@ -0,0 +1,83 @@
{% extends "layouts/base.html" %}
{% block title %}
Turn your spreadsheets into interactive charts, without writing a single line of code.
{% endblock %}
{% block content %}
<style>
h1, h2 {
text-align: center;
font-size: 50px;
}
img {
width: 100%;
}
hr {
margin: 60px 0px;
}
.ui-link-external {
background: white;
border: 2px solid #0094ff;
border-radius :4px;
text-transform: uppercase;
text-decoration: none;
color: white;
display: flex;
justify-content: center;
align-items: center;
margin-left: 20px;
box-shadow: 2px 2px 1px black;
}
.ui-link-image {
width: 20px;
padding: 0px 2px;
display: inline;
}
</style>
<div class="flex-center">
<article>
<div class="flex-center">
<img src="/images/hero.png">
</div>
<h2>Turn your spreadsheets into interactive charts, without writing a single line of code.</h1>
<div class="flex-center">
<a class="ui-link-add" href="/sign-up">Create Account</a>
</div>
<hr >
<div class="flex-center">
<img src="/images/screenshot-1.png">
</div>
<h2>Your newly created charts are accessible from anywhere on the web.</h2>
<div class="flex-center">
<img src="/images/screenshot-2.png">
</div>
<h2>Fill out a simple form and start exploring your data.</h2>
<div class="flex-center">
<video width=100% controls>
<source src="/images/screen-video-1.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
<h2>Use Bokeh's interactive controls to explore your data.</h2>
<div class="flex-center">
<a class="ui-link-add" href="/sign-up">Create Account</a>
<a class="ui-link-external" href="https://bokeh.org">
<img class="ui-link-image" src="/images/bokeh-simple.png">
Bokeh's Website
</a>
</div>
<div class="flex-center">
<img src="/images/network-effect.png">
</div>
<h2>Sharing your charts is as easy as attaching a file to an email.</h2>
<div class="flex-center" style="margin-bottom: 20px;">
<a class="ui-link-add" href="/sign-up">Create Account</a>
<a class="ui-link-external" href="https://bokeh.org/">
<img class="ui-link-image" src="/images/bokeh-simple.png">
Bokeh's Website
</a>
</div>
</article>
</div>
{% endblock %}

46
templates/layouts/base.html

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<meta name="description"
content="Use your spreadsheets to create interactive line-charts quickly and easily.">
<meta name="keywords"
content="craig, oates, charts, plotting, graphics, visualisations, interactive, csv, tsv, bokeh">
<meta name="author" content="Craig Oates">
<link rel="shortcut icon" href="/images/favicon.png">
<link rel="stylesheet" type="text/css" media="screen" href="/css/main.css">
{% lisp (insert-umami-script) %}
</head>
<body>
<header>
{% include "shared/header.html" %}
</header>
<div class="flex-column">
{% if roles.logged-in == True %}
<p class="logged-in-info"><span>Logged in as: </span> {{ user.username }}</p>
{% endif %}
{% if roles.logged-in == True and roles.administrator == True %}
<p class="logged-in-info"><span>Account Type: </span>Admininstrator</p>
{% elif roles.logged-in == True and roles.administrator == False %}
<p class="logged-in-info"><span>Account Type: </span>Basic</p>
{% endif %}
</div>
<main>
{% if alert %}<p>{{alert | safe}}</p>{% endif %}
{% block content %}{% endblock %}
</main>
<footer>
{% include "shared/footer.html" %}
</footer>
</body>
</html>

18
templates/nav-menu.html

@ -0,0 +1,18 @@
{% extends "layouts/base.html" %}
{% block title %}Craig Oates: Navigation{% endblock %}
{% block content %}
<nav class="mobile-nav">
<a class="ui-link" href="/search">Search</a>
<hr style="width: 100%;">
<a class="ui-link" href="/">Home</a>
<a class="ui-link" href="/3dmodels">3D Models</a>
<a class="ui-link" href="/blog">Articles</a>
<a class="ui-link" href="/art">Artwork</a>
<a class="ui-link" href="/graphics">Graphics</a>
<a class="ui-link" href="/sketchbooks">Sketchbooks</a>
<a class="ui-link" href="/software">Software</a>
</nav>
{% endblock %}

58
templates/privacy.html

@ -0,0 +1,58 @@
{% extends "layouts/base.html" %} {% block title %}Craig Oates: Privacy Policy{%
endblock %}
{% block content %}
<div class="content-header">
<h1>Privacy Information</h1>
</div>
<div class="flex-center">
<article>
<p>
Basically, this website aims to keep as little of your personal
information as possible. It does not contain any advertisements with
tracking cookies or any social media trackers either.
</p>
<h2>What Data this Website Stores</h2>
<p>
This website stores your Username, Display Name, Password and the HTML
files (I.E. the charts) generated by the files you upload (.csv and .tsv
files) to the server. You are not required to provide any personal
information as part of your Username or Display Name. For the sake
completeness, the same applies for the Password.
</p>
<h2>What is Not Stored on this Website</h2>
<p>
This website temporarily stores any files (I.E. spreadsheets) you upload
so it can generate the charts (stored as HTML files). When the server has
finished generating the chart and stored it as an HTML file, the uploaded
file is deleted. None of your personally uploaded files are stored
long-term.
</p>
<h2>What is Viewable to the Public</h2>
<p>
Nothing is viewable to the general public. The only thing logged in users
can access is their own Dashboard and the charts they have created.
</p>
<h2>What is Deleted when a User Deletes their Account</h2>
<p>
The website removes the account's details from the database (Username,
Display Name and Password) and all the files, attached to that account, on
the server at that moment in time.
When a user deletes a HTML file (via the Dashboard), that file is
permanently deleted. It is not reversible. The same applies when deleting
an account. When a file or account is deleted, that is it. There is no way
to retrieve/restore it.
</p>
<h2>What is Shared Between this Website and craigoates.net</h2>
<p>
In terms of data, nothing is shared. This website shares the
'craigoates.net' domain name but this website uses a seperate database and
stores all its files in a different location.
</p>
</article>
</div>
{% endblock %}

7
templates/shared/footer.html

@ -0,0 +1,7 @@
<p>
<a class="ui-link" href="/">Home</a>
<a class="ui-link" href="/about">About</a>
<a class="ui-link" href="/privacy">Privacy</a>
<a class="ui-link" href="https://www.craigoates.net">Main Site</a>
</p>
<p>© Craig Oates 2011-{% lisp (local-time:timestamp-year (local-time:now)) %}</p>

19
templates/shared/header.html

@ -0,0 +1,19 @@
<nav>
<div class="nav-header">
<img src="/images/site-logo.png" alt="Craig Oates Icon"/><span>Charts</span>
</div>
<div>
{% if roles.logged-in %}
<a class="ui-link-admin" href="/dashboard">Dashboard</a>
<form action="/logout" method="post">
<input type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input type="hidden" name="METHOD" value="logout">
<input type="submit" value="logout">
</form>
{% else %}
<a class="ui-link" href="/">Home</a>
<a class="ui-link-admin" href="/login">Log-in</a>
<a class="ui-link-add" href="/sign-up">Create Account</a>
{% endif %}
</div>
</nav>

646
templates/shared/mime-select-alt.html

@ -0,0 +1,646 @@
<option value="x-world/x-3dmf">.3dm</option>
<option value="x-world/x-3dmf">.3dmf</option>
<option value="application/octet-stream">.a</option>
<option value="application/x-authorware-bin">.aab</option>
<option value="application/x-authorware-map">.aam</option>
<option value="application/x-authorware-seg">.aas</option>
<option value="text/vnd.abc">.abc</option>
<option value="text/html">.acgi</option>
<option value="video/animaflex">.afl</option>
<option value="application/postscript">.ai</option>
<option value="audio/aiff">.aif</option>
<option value="audio/x-aiff">.aif</option>
<option value="audio/aiff">.aifc</option>
<option value="audio/x-aiff">.aifc</option>
<option value="audio/aiff">.aiff</option>
<option value="audio/x-aiff">.aiff</option>
<option value="application/x-aim">.aim</option>
<option value="text/x-audiosoft-intra">.aip</option>
<option value="application/x-navi-animation">.ani</option>
<option value="application/x-nokia-9000-communicator-add-on-software">.aos</option>
<option value="application/mime">.aps</option>
<option value="application/octet-stream">.arc</option>
<option value="application/arj">.arj</option>
<option value="application/octet-stream">.arj</option>
<option value="image/x-jg">.art</option>
<option value="video/x-ms-asf">.asf</option>
<option value="text/x-asm">.asm</option>
<option value="text/asp">.asp</option>
<option value="application/x-mplayer2">.asx</option>
<option value="video/x-ms-asf">.asx</option>
<option value="video/x-ms-asf-plugin">.asx</option>
<option value="audio/basic">.au</option>
<option value="audio/x-au">.au</option>
<option value="application/x-troff-msvideo">.avi</option>
<option value="video/avi">.avi</option>
<option value="video/msvideo">.avi</option>
<option value="video/x-msvideo">.avi</option>
<option value="video/avs-video">.avs</option>
<option value="application/x-bcpio">.bcpio</option>
<option value="application/mac-binary">.bin</option>
<option value="application/macbinary">.bin</option>
<option value="application/octet-stream">.bin</option>
<option value="application/x-binary">.bin</option>
<option value="application/x-macbinary">.bin</option>
<option value="image/bmp">.bm</option>
<option value="image/bmp">.bmp</option>
<option value="image/x-windows-bmp">.bmp</option>
<option value="application/book">.boo</option>
<option value="application/book">.book</option>
<option value="application/x-bzip2">.boz</option>
<option value="application/x-bsh">.bsh</option>
<option value="application/x-bzip">.bz</option>
<option value="application/x-bzip2">.bz2</option>
<option value="text/plain">.c</option>
<option value="text/x-c">.c</option>
<option value="text/plain">.c++</option>
<option value="application/vnd.ms-pki.seccat">.cat</option>
<option value="text/plain">.cc</option>
<option value="text/x-c">.cc</option>
<option value="application/clariscad">.ccad</option>
<option value="application/x-cocoa">.cco</option>
<option value="application/cdf">.cdf</option>
<option value="application/x-cdf">.cdf</option>
<option value="application/x-netcdf">.cdf</option>
<option value="application/pkix-cert">.cer</option>
<option value="application/x-x509-ca-cert">.cer</option>
<option value="application/x-chat">.cha</option>
<option value="application/x-chat">.chat</option>
<option value="application/java">.class</option>
<option value="application/java-byte-code">.class</option>
<option value="application/x-java-class">.class</option>
<option value="application/octet-stream">.com</option>
<option value="text/plain">.com</option>
<option value="text/plain">.conf</option>
<option value="application/x-cpio">.cpio</option>
<option value="text/x-c">.cpp</option>
<option value="application/mac-compactpro">.cpt</option>
<option value="application/x-compactpro">.cpt</option>
<option value="application/x-cpt">.cpt</option>
<option value="application/pkcs-crl">.crl</option>
<option value="application/pkix-crl">.crl</option>
<option value="application/pkix-cert">.crt</option>
<option value="application/x-x509-ca-cert">.crt</option>
<option value="application/x-x509-user-cert">.crt</option>
<option value="application/x-csh">.csh</option>
<option value="text/x-script.csh">.csh</option>
<option value="application/x-pointplus">.css</option>
<option value="text/css">.css</option>
<option value="text/plain">.cxx</option>
<option value="application/x-director">.dcr</option>
<option value="application/x-deepv">.deepv</option>
<option value="text/plain">.def</option>
<option value="application/x-x509-ca-cert">.der</option>
<option value="video/x-dv">.dif</option>
<option value="application/x-director">.dir</option>
<option value="video/dl">.dl</option>
<option value="video/x-dl">.dl</option>
<option value="application/msword">.doc</option>
<option value="application/msword">.dot</option>
<option value="application/commonground">.dp</option>
<option value="application/drafting">.drw</option>
<option value="application/octet-stream">.dump</option>
<option value="video/x-dv">.dv</option>
<option value="application/x-dvi">.dvi</option>
<option value="drawing/x-dwf (old)">.dwf</option>
<option value="model/vnd.dwf">.dwf</option>
<option value="application/acad">.dwg</option>
<option value="image/vnd.dwg">.dwg</option>
<option value="image/x-dwg">.dwg</option>
<option value="application/dxf">.dxf</option>
<option value="image/vnd.dwg">.dxf</option>
<option value="image/x-dwg">.dxf</option>
<option value="application/x-director">.dxr</option>
<option value="text/x-script.elisp">.el</option>
<option value="application/x-bytecode.elisp (compiled elisp)">.elc</option>
<option value="application/x-elc">.elc</option>
<option value="application/x-envoy">.env</option>
<option value="application/postscript">.eps</option>
<option value="application/x-esrehber">.es</option>
<option value="text/x-setext">.etx</option>
<option value="application/envoy">.evy</option>
<option value="application/x-envoy">.evy</option>
<option value="application/octet-stream">.exe</option>
<option value="text/plain">.f</option>
<option value="text/x-fortran">.f</option>
<option value="text/x-fortran">.f77</option>
<option value="text/plain">.f90</option>
<option value="text/x-fortran">.f90</option>
<option value="application/vnd.fdf">.fdf</option>
<option value="application/fractals">.fif</option>
<option value="image/fif">.fif</option>
<option value="video/fli">.fli</option>
<option value="video/x-fli">.fli</option>
<option value="image/florian">.flo</option>
<option value="text/vnd.fmi.flexstor">.flx</option>
<option value="video/x-atomic3d-feature">.fmf</option>
<option value="text/plain">.for</option>
<option value="text/x-fortran">.for</option>
<option value="image/vnd.fpx">.fpx</option>
<option value="image/vnd.net-fpx">.fpx</option>
<option value="application/freeloader">.frl</option>
<option value="audio/make">.funk</option>
<option value="text/plain">.g</option>
<option value="image/g3fax">.g3</option>
<option value="image/gif">.gif</option>
<option value="video/gl">.gl</option>
<option value="video/x-gl">.gl</option>
<option value="audio/x-gsm">.gsd</option>
<option value="audio/x-gsm">.gsm</option>
<option value="application/x-gsp">.gsp</option>
<option value="application/x-gss">.gss</option>
<option value="application/x-gtar">.gtar</option>
<option value="application/x-compressed">.gz</option>
<option value="application/x-gzip">.gz</option>
<option value="application/x-gzip">.gzip</option>
<option value="multipart/x-gzip">.gzip</option>
<option value="text/plain">.h</option>
<option value="text/x-h">.h</option>
<option value="application/x-hdf">.hdf</option>
<option value="application/x-helpfile">.help</option>
<option value="application/vnd.hp-hpgl">.hgl</option>
<option value="text/plain">.hh</option>
<option value="text/x-h">.hh</option>
<option value="text/x-script">.hlb</option>
<option value="application/hlp">.hlp</option>
<option value="application/x-helpfile">.hlp</option>
<option value="application/x-winhelp">.hlp</option>
<option value="application/vnd.hp-hpgl">.hpg</option>
<option value="application/vnd.hp-hpgl">.hpgl</option>
<option value="application/binhex">.hqx</option>
<option value="application/binhex4">.hqx</option>
<option value="application/mac-binhex">.hqx</option>
<option value="application/mac-binhex40">.hqx</option>
<option value="application/x-binhex40">.hqx</option>
<option value="application/x-mac-binhex40">.hqx</option>
<option value="application/hta">.hta</option>
<option value="text/x-component">.htc</option>
<option value="text/html">.htm</option>
<option value="text/html">.html</option>
<option value="text/html">.htmls</option>
<option value="text/webviewhtml">.htt</option>
<option value="text/html">.htx</option>
<option value="x-conference/x-cooltalk">.ice</option>
<option value="image/x-icon">.ico</option>
<option value="text/plain">.idc</option>
<option value="image/ief">.ief</option>
<option value="image/ief">.iefs</option>
<option value="application/iges">.iges</option>
<option value="model/iges">.iges</option>
<option value="application/iges">.igs</option>
<option value="model/iges">.igs</option>
<option value="application/x-ima">.ima</option>
<option value="application/x-httpd-imap">.imap</option>
<option value="application/inf">.inf</option>
<option value="application/x-internett-signup">.ins</option>
<option value="application/x-ip2">.ip</option>
<option value="video/x-isvideo">.isu</option>
<option value="audio/it">.it</option>
<option value="application/x-inventor">.iv</option>
<option value="i-world/i-vrml">.ivr</option>
<option value="application/x-livescreen">.ivy</option>
<option value="audio/x-jam">.jam</option>
<option value="text/plain">.jav</option>
<option value="text/x-java-source">.jav</option>
<option value="text/plain">.java</option>
<option value="text/x-java-source">.java</option>
<option value="application/x-java-commerce">.jcm</option>
<option value="image/jpeg">.jfif</option>
<option value="image/pjpeg">.jfif</option>
<option value="image/jpeg">.jfif-tbnl</option
<option value="image/jpeg">.jpe</option>
<option value="image/pjpeg">.jpe</option>
<option value="image/jpeg">.jpeg</option>
<option value="image/pjpeg">.jpeg</option>
<option value="image/jpeg">.jpg</option>
<option value="image/pjpeg">.jpg</option>
<option value="image/x-jps">.jps</option>
<option value="application/x-javascript">.js</option>
<option value="application/javascript">.js</option>
<option value="application/ecmascript">.js</option>
<option value="text/javascript">.js</option>
<option value="text/ecmascript">.js</option>
<option value="image/jutvision">.jut</option>
<option value="audio/midi">.kar</option>
<option value="music/x-karaoke">.kar</option>
<option value="application/x-ksh">.ksh</option>
<option value="text/x-script.ksh">.ksh</option>
<option value="audio/nspaudio">.la</option>
<option value="audio/x-nspaudio">.la</option>
<option value="audio/x-liveaudio">.lam</option>
<option value="application/x-latex">.latex</option>
<option value="application/lha">.lha</option>
<option value="application/octet-stream">.lha</option>
<option value="application/x-lha">.lha</option>
<option value="application/octet-stream">.lhx</option>
<option value="text/plain">.list</option>
<option value="audio/nspaudio">.lma</option>
<option value="audio/x-nspaudio">.lma</option>
<option value="text/plain">.log</option>
<option value="application/x-lisp">.lsp</option>
<option value="text/x-script.lisp">.lsp</option>
<option value="text/plain">.lst</option>
<option value="text/x-la-asf">.lsx</option>
<option value="application/x-latex">.ltx</option>
<option value="application/octet-stream">.lzh</option>
<option value="application/x-lzh">.lzh</option>
<option value="application/lzx">.lzx</option>
<option value="application/octet-stream">.lzx</option>
<option value="application/x-lzx">.lzx</option>
<option value="text/plain">.m</option>
<option value="text/x-m">.m</option>
<option value="video/mpeg">.m1v</option>
<option value="audio/mpeg">.m2a</option>
<option value="video/mpeg">.m2v</option>
<option value="audio/x-mpequrl">.m3u</option>
<option value="application/x-troff-man">.man</option>
<option value="application/x-navimap">.map</option>
<option value="text/plain">.mar</option>
<option value="application/mbedlet">.mbd</option>
<option value="application/x-magic-cap-package-1.0">.mc$</option>
<option value="application/mcad">.mcd</option>
<option value="application/x-mathcad">.mcd</option>
<option value="image/vasa">.mcf</option>
<option value="text/mcf">.mcf</option>
<option value="application/netmc">.mcp</option>
<option value="application/x-troff-me">.me</option>
<option value="message/rfc822">.mht</option>
<option value="message/rfc822">.mhtml</option>
<option value="application/x-midi">.mid</option>
<option value="audio/midi">.mid</option>
<option value="audio/x-mid">.mid</option>
<option value="audio/x-midi">.mid</option>
<option value="music/crescendo">.mid</option>
<option value="x-music/x-midi">.mid</option>
<option value="application/x-midi">.midi</option>
<option value="audio/midi">.midi</option>
<option value="audio/x-mid">.midi</option>
<option value="audio/x-midi">.midi</option>
<option value="music/crescendo">.midi</option>
<option value="x-music/x-midi">.midi</option>
<option value="application/x-frame">.mif</option>
<option value="application/x-mif">.mif</option>
<option value="message/rfc822">.mime</option>
<option value="www/mime">.mime</option>
<option value="audio/x-vnd.audioexplosion.mjuicemediafile">.mjf</option>
<option value="video/x-motion-jpeg">.mjpg</option>
<option value="application/base64">.mm</option>
<option value="application/x-meme">.mm</option>
<option value="application/base64">.mme</option>
<option value="audio/mod">.mod</option>
<option value="audio/x-mod">.mod</option>
<option value="video/quicktime">.moov</option>
<option value="video/quicktime">.mov</option>
<option value="video/x-sgi-movie">.movie</option>
<option value="audio/mpeg">.mp2</option>
<option value="audio/x-mpeg">.mp2</option>
<option value="video/mpeg">.mp2</option>
<option value="video/x-mpeg">.mp2</option>
<option value="video/x-mpeq2a">.mp2</option>
<option value="audio/mpeg3">.mp3</option>
<option value="audio/x-mpeg-3">.mp3</option>
<option value="video/mpeg">.mp3</option>
<option value="video/x-mpeg">.mp3</option>
<option value="audio/mpeg">.mpa</option>
<option value="video/mpeg">.mpa</option>
<option value="application/x-project">.mpc</option>
<option value="video/mpeg">.mpe</option>
<option value="video/mpeg">.mpeg</option>
<option value="audio/mpeg">.mpg</option>
<option value="video/mpeg">.mpg</option>
<option value="audio/mpeg">.mpga</option>
<option value="application/vnd.ms-project">.mpp</option>
<option value="application/x-project">.mpt</option>
<option value="application/x-project">.mpv</option>
<option value="application/x-project">.mpx</option>
<option value="application/marc">.mrc</option>
<option value="application/x-troff-ms">.ms</option>
<option value="video/x-sgi-movie">.mv</option>
<option value="audio/make">.my</option>
<option value="application/x-vnd.audioexplosion.mzz">.mzz</option>
<option value="image/naplps">.nap</option>
<option value="image/naplps">.naplps</option>
<option value="application/x-netcdf">.nc</option>
<option value="application/vnd.nokia.configuration-message">.ncm</option>
<option value="image/x-niff">.nif</option>
<option value="image/x-niff">.niff</option>
<option value="application/x-mix-transfer">.nix</option>
<option value="application/x-conference">.nsc</option>
<option value="application/x-navidoc">.nvd</option>
<option value="application/octet-stream">.o</option>
<option value="application/oda">.oda</option>
<option value="application/x-omc">.omc</option>
<option value="application/x-omcdatamaker">.omcd</option>
<option value="application/x-omcregerator">.omcr</option>
<option value="text/x-pascal">.p</option>
<option value="application/pkcs10">.p10</option>
<option value="application/x-pkcs10">.p10</option>
<option value="application/pkcs-12">.p12</option>
<option value="application/x-pkcs12">.p12</option>
<option value="application/x-pkcs7-signature">.p7a</option>
<option value="application/pkcs7-mime">.p7c</option>
<option value="application/x-pkcs7-mime">.p7c</option>
<option value="application/pkcs7-mime">.p7m</option>
<option value="application/x-pkcs7-mime">.p7m</option>
<option value="application/x-pkcs7-certreqresp">.p7r</option>
<option value="application/pkcs7-signature">.p7s</option>
<option value="application/pro_eng">.part</option>
<option value="text/pascal">.pas</option>
<option value="image/x-portable-bitmap">.pbm</option>
<option value="application/vnd.hp-pcl">.pcl</option>
<option value="application/x-pcl">.pcl</option>
<option value="image/x-pict">.pct</option>
<option value="image/x-pcx">.pcx</option>
<option value="chemical/x-pdb">.pdb</option>
<option value="application/pdf">.pdf</option>
<option value="audio/make">.pfunk</option>
<option value="audio/make.my.funk">.pfunk</option>
<option value="image/x-portable-graymap">.pgm</option>
<option value="image/x-portable-greymap">.pgm</option>
<option value="image/pict">.pic</option>
<option value="image/pict">.pict</option>
<option value="application/x-newton-compatible-pkg">.pkg</option>
<option value="application/vnd.ms-pki.pko">.pko</option>
<option value="text/plain">.pl</option>
<option value="text/x-script.perl">.pl</option>
<option value="application/x-pixclscript">.plx</option>
<option value="image/x-xpixmap">.pm</option>
<option value="text/x-script.perl-module">.pm</option>
<option value="application/x-pagemaker">.pm4</option>
<option value="application/x-pagemaker">.pm5</option>
<option value="image/png">.png</option>
<option value="application/x-portable-anymap">.pnm</option>
<option value="image/x-portable-anymap">.pnm</option>
<option value="application/mspowerpoint">.pot</option>
<option value="application/vnd.ms-powerpoint">.pot</option>
<option value="model/x-pov">.pov</option>
<option value="application/vnd.ms-powerpoint">.ppa</option>
<option value="image/x-portable-pixmap">.ppm</option>
<option value="application/mspowerpoint">.pps</option>
<option value="application/vnd.ms-powerpoint">.pps</option>
<option value="application/mspowerpoint">.ppt</option>
<option value="application/powerpoint">.ppt</option>
<option value="application/vnd.ms-powerpoint">.ppt</option>
<option value="application/x-mspowerpoint">.ppt</option>
<option value="application/mspowerpoint">.ppz</option>
<option value="application/x-freelance">.pre</option>
<option value="application/pro_eng">.prt</option>
<option value="application/postscript">.ps</option>
<option value="application/octet-stream">.psd</option>
<option value="paleovu/x-pv">.pvu</option>
<option value="application/vnd.ms-powerpoint">.pwz</option>
<option value="text/x-script.phyton">.py</option>
<option value="application/x-bytecode.python">.pyc</option>
<option value="audio/vnd.qcelp">.qcp</option>
<option value="x-world/x-3dmf">.qd3</option>
<option value="x-world/x-3dmf">.qd3d</option>
<option value="image/x-quicktime">.qif</option>
<option value="video/quicktime">.qt</option>
<option value="video/x-qtc">.qtc</option>
<option value="image/x-quicktime">.qti</option>
<option value="image/x-quicktime">.qtif</option>
<option value="audio/x-pn-realaudio">.ra</option>
<option value="audio/x-pn-realaudio-plugin">.ra</option>
<option value="audio/x-realaudio">.ra</option>
<option value="audio/x-pn-realaudio">.ram</option>
<option value="application/x-cmu-raster">.ras</option>
<option value="image/cmu-raster">.ras</option>
<option value="image/x-cmu-raster">.ras</option>
<option value="image/cmu-raster">.rast</option>
<option value="text/x-script.rexx">.rexx</option>
<option value="image/vnd.rn-realflash">.rf</option>
<option value="image/x-rgb">.rgb</option>
<option value="application/vnd.rn-realmedia">.rm</option>
<option value="audio/x-pn-realaudio">.rm</option>
<option value="audio/mid">.rmi</option>
<option value="audio/x-pn-realaudio">.rmm</option>
<option value="audio/x-pn-realaudio">.rmp</option>
<option value="audio/x-pn-realaudio-plugin">.rmp</option>
<option value="application/ringing-tones">.rng</option>
<option value="application/vnd.nokia.ringing-tone">.rng</option>
<option value="application/vnd.rn-realplayer">.rnx</option>
<option value="application/x-troff">.roff</option>
<option value="image/vnd.rn-realpix">.rp</option>
<option value="audio/x-pn-realaudio-plugin">.rpm</option>
<option value="text/richtext">.rt</option>
<option value="text/vnd.rn-realtext">.rt</option>
<option value="application/rtf">.rtf</option>
<option value="application/x-rtf">.rtf</option>
<option value="text/richtext">.rtf</option>
<option value="application/rtf">.rtx</option>
<option value="text/richtext">.rtx</option>
<option value="video/vnd.rn-realvideo">.rv</option>
<option value="text/x-asm">.s</option>
<option value="audio/s3m">.s3m</option>
<option value="application/octet-stream">.saveme</option>
<option value="application/x-tbook">.sbk</option>
<option value="application/x-lotusscreencam">.scm</option>
<option value="text/x-script.guile">.scm</option>
<option value="text/x-script.scheme">.scm</option>
<option value="video/x-scm">.scm</option>
<option value="text/plain">.sdml</option>
<option value="application/sdp">.sdp</option>
<option value="application/x-sdp">.sdp</option>
<option value="application/sounder">.sdr</option>
<option value="application/sea">.sea</option>
<option value="application/x-sea">.sea</option>
<option value="application/set">.set</option>
<option value="text/sgml">.sgm</option>
<option value="text/x-sgml">.sgm</option>
<option value="text/sgml">.sgml</option>
<option value="text/x-sgml">.sgml</option>
<option value="application/x-bsh">.sh</option>
<option value="application/x-sh">.sh</option>
<option value="application/x-shar">.sh</option>
<option value="text/x-script.sh">.sh</option>
<option value="application/x-bsh">.shar</option>
<option value="application/x-shar">.shar</option>
<option value="text/html">.shtml</option>
<option value="text/x-server-parsed-html">.shtml</option>
<option value="audio/x-psid">.sid</option>
<option value="application/x-sit">.sit</option>
<option value="application/x-stuffit">.sit</option>
<option value="application/x-koan">.skd</option>
<option value="application/x-koan">.skm</option>
<option value="application/x-koan">.skp</option>
<option value="application/x-koan">.skt</option>
<option value="application/x-seelogo">.sl</option>
<option value="application/smil">.smi</option>
<option value="application/smil">.smil</option>
<option value="audio/basic">.snd</option>
<option value="audio/x-adpcm">.snd</option>
<option value="application/solids">.sol</option>
<option value="application/x-pkcs7-certificates">.spc</option>
<option value="text/x-speech">.spc</option>
<option value="application/futuresplash">.spl</option>
<option value="application/x-sprite">.spr</option>
<option value="application/x-sprite">.sprite</option>
<option value="application/x-wais-source">.src</option>
<option value="text/x-server-parsed-html">.ssi</option>
<option value="application/streamingmedia">.ssm</option>
<option value="application/vnd.ms-pki.certstore">.sst</option>
<option value="application/step">.step</option>
<option value="application/sla">.stl</option>
<option value="application/vnd.ms-pki.stl">.stl</option>
<option value="application/x-navistyle">.stl</option>
<option value="application/step">.stp</option>
<option value="application/x-sv4cpio">.sv4cpio</option>
<option value="application/x-sv4crc">.sv4crc</option>
<option value="image/vnd.dwg">.svf</option>
<option value="image/x-dwg">.svf</option>
<option value="application/x-world">.svr</option>
<option value="x-world/x-svr">.svr</option>
<option value="application/x-shockwave-flash">.swf</option>
<option value="application/x-troff">.t</option>
<option value="text/x-speech">.talk</option>
<option value="application/x-tar">.tar</option>
<option value="application/toolbook">.tbk</option>
<option value="application/x-tbook">.tbk</option>
<option value="application/x-tcl">.tcl</option>
<option value="text/x-script.tcl">.tcl</option>
<option value="text/x-script.tcsh">.tcsh</option>
<option value="application/x-tex">.tex</option>
<option value="application/x-texinfo">.texi</option>
<option value="application/x-texinfo">.texinfo</option>
<option value="application/plain">.text</option>
<option value="text/plain">.text</option>
<option value="application/gnutar">.tgz</option>
<option value="application/x-compressed">.tgz</option>
<option value="image/tiff">.tif</option>
<option value="image/x-tiff">.tif</option>
<option value="image/tiff">.tiff</option>
<option value="image/x-tiff">.tiff</option>
<option value="application/x-troff">.tr</option>
<option value="audio/tsp-audio">.tsi</option>
<option value="application/dsptype">.tsp</option>
<option value="audio/tsplayer">.tsp</option>
<option value="text/tab-separated-values">.tsv</option>
<option value="image/florian">.turbot</option>
<option value="text/plain">.txt</option>
<option value="text/x-uil">.uil</option>
<option value="text/uri-list">.uni</option>
<option value="text/uri-list">.unis</option>
<option value="application/i-deas">.unv</option>
<option value="text/uri-list">.uri</option>
<option value="text/uri-list">.uris</option>
<option value="application/x-ustar">.ustar</option>
<option value="multipart/x-ustar">.ustar</option>
<option value="application/octet-stream">.uu</option>
<option value="text/x-uuencode">.uu</option>
<option value="text/x-uuencode">.uue</option>
<option value="application/x-cdlink">.vcd</option>
<option value="text/x-vcalendar">.vcs</option>
<option value="application/vda">.vda</option>
<option value="video/vdo">.vdo</option>
<option value="application/groupwise">.vew</option>
<option value="video/vivo">.viv</option>
<option value="video/vnd.vivo">.viv</option>
<option value="video/vivo">.vivo</option>
<option value="video/vnd.vivo">.vivo</option>
<option value="application/vocaltec-media-desc">.vmd</option>
<option value="application/vocaltec-media-file">.vmf</option>
<option value="audio/voc">.voc</option>
<option value="audio/x-voc">.voc</option>
<option value="video/vosaic">.vos</option>
<option value="audio/voxware">.vox</option>
<option value="audio/x-twinvq-plugin">.vqe</option>
<option value="audio/x-twinvq">.vqf</option>
<option value="audio/x-twinvq-plugin">.vql</option>
<option value="application/x-vrml">.vrml</option>
<option value="model/vrml">.vrml</option>
<option value="x-world/x-vrml">.vrml</option>
<option value="x-world/x-vrt">.vrt</option>
<option value="application/x-visio">.vsd</option>
<option value="application/x-visio">.vst</option>
<option value="application/x-visio">.vsw</option>
<option value="application/wordperfect6.0">.w60</option>
<option value="application/wordperfect6.1">.w61</option>
<option value="application/msword">.w6w</option>
<option value="audio/wav">.wav</option>
<option value="audio/x-wav">.wav</option>
<option value="application/x-qpro">.wb1</option>
<option value="image/vnd.wap.wbmp">.wbmp</option>
<option value="application/vnd.xara">.web</option>
<option value="application/msword">.wiz</option>
<option value="application/x-123">.wk1</option>
<option value="windows/metafile">.wmf</option>
<option value="text/vnd.wap.wml">.wml</option>
<option value="application/vnd.wap.wmlc">.wmlc</option>
<option value="text/vnd.wap.wmlscript">.wmls</option>
<option value="application/vnd.wap.wmlscriptc">.wmlsc</option>
<option value="application/msword">.word</option>
<option value="application/wordperfect">.wp</option>
<option value="application/wordperfect">.wp5</option>
<option value="application/wordperfect6.0">.wp5</option>
<option value="application/wordperfect">.wp6</option>
<option value="application/wordperfect">.wpd</option>
<option value="application/x-wpwin">.wpd</option>
<option value="application/x-lotus">.wq1</option>
<option value="application/mswrite">.wri</option>
<option value="application/x-wri">.wri</option>
<option value="application/x-world">.wrl</option>
<option value="model/vrml">.wrl</option>
<option value="x-world/x-vrml">.wrl</option>
<option value="model/vrml">.wrz</option>
<option value="x-world/x-vrml">.wrz</option>
<option value="text/scriplet">.wsc</option>
<option value="application/x-wais-source">.wsrc</option>
<option value="application/x-wintalk">.wtk</option>
<option value="image/x-xbitmap">.xbm</option>
<option value="image/x-xbm">.xbm</option>
<option value="image/xbm">.xbm</option>
<option value="video/x-amt-demorun">.xdr</option>
<option value="xgl/drawing">.xgz</option>
<option value="image/vnd.xiff">.xif</option>
<option value="application/excel">.xl</option>
<option value="application/excel">.xla</option>
<option value="application/x-excel">.xla</option>
<option value="application/x-msexcel">.xla</option>
<option value="application/excel">.xlb</option>
<option value="application/vnd.ms-excel">.xlb</option>
<option value="application/x-excel">.xlb</option>
<option value="application/excel">.xlc</option>
<option value="application/vnd.ms-excel">.xlc</option>
<option value="application/x-excel">.xlc</option>
<option value="application/excel">.xld</option>
<option value="application/x-excel">.xld</option>
<option value="application/excel">.xlk</option>
<option value="application/x-excel">.xlk</option>
<option value="application/excel">.xll</option>
<option value="application/vnd.ms-excel">.xll</option>
<option value="application/x-excel">.xll</option>
<option value="application/excel">.xlm</option>
<option value="application/vnd.ms-excel">.xlm</option>
<option value="application/x-excel">.xlm</option>
<option value="application/excel">.xls</option>
<option value="application/vnd.ms-excel">.xls</option>
<option value="application/x-excel">.xls</option>
<option value="application/x-msexcel">.xls</option>
<option value="application/excel">.xlt</option>
<option value="application/x-excel">.xlt</option>
<option value="application/excel">.xlv</option>
<option value="application/x-excel">.xlv</option>
<option value="application/excel">.xlw</option>
<option value="application/vnd.ms-excel">.xlw</option>
<option value="application/x-excel">.xlw</option>
<option value="application/x-msexcel">.xlw</option>
<option value="audio/xm">.xm</option>
<option value="application/xml">.xml</option>
<option value="text/xml">.xml</option>
<option value="xgl/movie">.xmz</option>
<option value="application/x-vnd.ls-xpix">.xpix</option>
<option value="image/x-xpixmap">.xpm</option>
<option value="image/xpm">.xpm</option>
<option value="image/png">.x-png</option>
<option value="video/x-amt-showrun">.xsr</option>
<option value="image/x-xwd">.xwd</option>
<option value="image/x-xwindowdump">.xwd</option>
<option value="chemical/x-pdb">.xyz</option>
<option value="application/x-compress">.z</option>
<option value="application/x-compressed">.z</option>
<option value="application/x-compressed">.zip</option>
<option value="application/x-zip-compressed">.zip</option>
<option value="application/zip">.zip</option>
<option value="multipart/x-zip">.zip</option>
<option value="application/octet-stream">.zoo</option>
<option value="text/x-script.zsh">.zsh</option>

646
templates/shared/mime-select.html

@ -0,0 +1,646 @@
<option value=".3dm"> x-world/x-3dmf</option>
<option value=".3dmf"> x-world/x-3dmf</option>
<option value=".a"> application/octet-stream</option>
<option value=".aab"> application/x-authorware-bin</option>
<option value=".aam"> application/x-authorware-map</option>
<option value=".aas"> application/x-authorware-seg</option>
<option value=".abc"> text/vnd.abc</option>
<option value=".acgi"> text/html</option>
<option value=".afl"> video/animaflex</option>
<option value=".ai"> application/postscript</option>
<option value=".aif"> audio/aiff</option>
<option value=".aif"> audio/x-aiff</option>
<option value=".aifc"> audio/aiff</option>
<option value=".aifc"> audio/x-aiff</option>
<option value=".aiff"> audio/aiff</option>
<option value=".aiff"> audio/x-aiff</option>
<option value=".aim"> application/x-aim</option>
<option value=".aip"> text/x-audiosoft-intra</option>
<option value=".ani"> application/x-navi-animation</option>
<option value=".aos"> application/x-nokia-9000-communicator-add-on-software</option>
<option value=".aps"> application/mime</option>
<option value=".arc"> application/octet-stream</option>
<option value=".arj"> application/arj</option>
<option value=".arj"> application/octet-stream</option>
<option value=".art"> image/x-jg</option>
<option value=".asf"> video/x-ms-asf</option>
<option value=".asm"> text/x-asm</option>
<option value=".asp"> text/asp</option>
<option value=".asx"> application/x-mplayer2</option>
<option value=".asx"> video/x-ms-asf</option>
<option value=".asx"> video/x-ms-asf-plugin</option>
<option value=".au"> audio/basic</option>
<option value=".au"> audio/x-au</option>
<option value=".avi"> application/x-troff-msvideo</option>
<option value=".avi"> video/avi</option>
<option value=".avi"> video/msvideo</option>
<option value=".avi"> video/x-msvideo</option>
<option value=".avs"> video/avs-video</option>
<option value=".bcpio"> application/x-bcpio</option>
<option value=".bin"> application/mac-binary</option>
<option value=".bin"> application/macbinary</option>
<option value=".bin"> application/octet-stream</option>
<option value=".bin"> application/x-binary</option>
<option value=".bin"> application/x-macbinary</option>
<option value=".bm"> image/bmp</option>
<option value=".bmp"> image/bmp</option>
<option value=".bmp"> image/x-windows-bmp</option>
<option value=".boo"> application/book</option>
<option value=".book"> application/book</option>
<option value=".boz"> application/x-bzip2</option>
<option value=".bsh"> application/x-bsh</option>
<option value=".bz"> application/x-bzip</option>
<option value=".bz2"> application/x-bzip2</option>
<option value=".c"> text/plain</option>
<option value=".c"> text/x-c</option>
<option value=".c++"> text/plain</option>
<option value=".cat"> application/vnd.ms-pki.seccat</option>
<option value=".cc"> text/plain</option>
<option value=".cc"> text/x-c</option>
<option value=".ccad"> application/clariscad</option>
<option value=".cco"> application/x-cocoa</option>
<option value=".cdf"> application/cdf</option>
<option value=".cdf"> application/x-cdf</option>
<option value=".cdf"> application/x-netcdf</option>
<option value=".cer"> application/pkix-cert</option>
<option value=".cer"> application/x-x509-ca-cert</option>
<option value=".cha"> application/x-chat</option>
<option value=".chat"> application/x-chat</option>
<option value=".class"> application/java</option>
<option value=".class"> application/java-byte-code</option>
<option value=".class"> application/x-java-class</option>
<option value=".com"> application/octet-stream</option>
<option value=".com"> text/plain</option>
<option value=".conf"> text/plain</option>
<option value=".cpio"> application/x-cpio</option>
<option value=".cpp"> text/x-c</option>
<option value=".cpt"> application/mac-compactpro</option>
<option value=".cpt"> application/x-compactpro</option>
<option value=".cpt"> application/x-cpt</option>
<option value=".crl"> application/pkcs-crl</option>
<option value=".crl"> application/pkix-crl</option>
<option value=".crt"> application/pkix-cert</option>
<option value=".crt"> application/x-x509-ca-cert</option>
<option value=".crt"> application/x-x509-user-cert</option>
<option value=".csh"> application/x-csh</option>
<option value=".csh"> text/x-script.csh</option>
<option value=".css"> application/x-pointplus</option>
<option value=".css"> text/css</option>
<option value=".cxx"> text/plain</option>
<option value=".dcr"> application/x-director</option>
<option value=".deepv"> application/x-deepv</option>
<option value=".def"> text/plain</option>
<option value=".der"> application/x-x509-ca-cert</option>
<option value=".dif"> video/x-dv</option>
<option value=".dir"> application/x-director</option>
<option value=".dl"> video/dl</option>
<option value=".dl"> video/x-dl</option>
<option value=".doc"> application/msword</option>
<option value=".dot"> application/msword</option>
<option value=".dp"> application/commonground</option>
<option value=".drw"> application/drafting</option>
<option value=".dump"> application/octet-stream</option>
<option value=".dv"> video/x-dv</option>
<option value=".dvi"> application/x-dvi</option>
<option value=".dwf"> drawing/x-dwf (old)</option>
<option value=".dwf"> model/vnd.dwf</option>
<option value=".dwg"> application/acad</option>
<option value=".dwg"> image/vnd.dwg</option>
<option value=".dwg"> image/x-dwg</option>
<option value=".dxf"> application/dxf</option>
<option value=".dxf"> image/vnd.dwg</option>
<option value=".dxf"> image/x-dwg</option>
<option value=".dxr"> application/x-director</option>
<option value=".el"> text/x-script.elisp</option>
<option value=".elc"> application/x-bytecode.elisp (compiled elisp)</option>
<option value=".elc"> application/x-elc</option>
<option value=".env"> application/x-envoy</option>
<option value=".eps"> application/postscript</option>
<option value=".es"> application/x-esrehber</option>
<option value=".etx"> text/x-setext</option>
<option value=".evy"> application/envoy</option>
<option value=".evy"> application/x-envoy</option>
<option value=".exe"> application/octet-stream</option>
<option value=".f"> text/plain</option>
<option value=".f"> text/x-fortran</option>
<option value=".f77"> text/x-fortran</option>
<option value=".f90"> text/plain</option>
<option value=".f90"> text/x-fortran</option>
<option value=".fdf"> application/vnd.fdf</option>
<option value=".fif"> application/fractals</option>
<option value=".fif"> image/fif</option>
<option value=".fli"> video/fli</option>
<option value=".fli"> video/x-fli</option>
<option value=".flo"> image/florian</option>
<option value=".flx"> text/vnd.fmi.flexstor</option>
<option value=".fmf"> video/x-atomic3d-feature</option>
<option value=".for"> text/plain</option>
<option value=".for"> text/x-fortran</option>
<option value=".fpx"> image/vnd.fpx</option>
<option value=".fpx"> image/vnd.net-fpx</option>
<option value=".frl"> application/freeloader</option>
<option value=".funk"> audio/make</option>
<option value=".g"> text/plain</option>
<option value=".g3"> image/g3fax</option>
<option value=".gif"> image/gif</option>
<option value=".gl"> video/gl</option>
<option value=".gl"> video/x-gl</option>
<option value=".gsd"> audio/x-gsm</option>
<option value=".gsm"> audio/x-gsm</option>
<option value=".gsp"> application/x-gsp</option>
<option value=".gss"> application/x-gss</option>
<option value=".gtar"> application/x-gtar</option>
<option value=".gz"> application/x-compressed</option>
<option value=".gz"> application/x-gzip</option>
<option value=".gzip"> application/x-gzip</option>
<option value=".gzip"> multipart/x-gzip</option>
<option value=".h"> text/plain</option>
<option value=".h"> text/x-h</option>
<option value=".hdf"> application/x-hdf</option>
<option value=".help"> application/x-helpfile</option>
<option value=".hgl"> application/vnd.hp-hpgl</option>
<option value=".hh"> text/plain</option>
<option value=".hh"> text/x-h</option>
<option value=".hlb"> text/x-script</option>
<option value=".hlp"> application/hlp</option>
<option value=".hlp"> application/x-helpfile</option>
<option value=".hlp"> application/x-winhelp</option>
<option value=".hpg"> application/vnd.hp-hpgl</option>
<option value=".hpgl"> application/vnd.hp-hpgl</option>
<option value=".hqx"> application/binhex</option>
<option value=".hqx"> application/binhex4</option>
<option value=".hqx"> application/mac-binhex</option>
<option value=".hqx"> application/mac-binhex40</option>
<option value=".hqx"> application/x-binhex40</option>
<option value=".hqx"> application/x-mac-binhex40</option>
<option value=".hta"> application/hta</option>
<option value=".htc"> text/x-component</option>
<option value=".htm"> text/html</option>
<option value=".html"> text/html</option>
<option value=".htmls"> text/html</option>
<option value=".htt"> text/webviewhtml</option>
<option value=".htx"> text/html</option>
<option value=".ice"> x-conference/x-cooltalk</option>
<option value=".ico"> image/x-icon</option>
<option value=".idc"> text/plain</option>
<option value=".ief"> image/ief</option>
<option value=".iefs"> image/ief</option>
<option value=".iges"> application/iges</option>
<option value=".iges"> model/iges</option>
<option value=".igs"> application/iges</option>
<option value=".igs"> model/iges</option>
<option value=".ima"> application/x-ima</option>
<option value=".imap"> application/x-httpd-imap</option>
<option value=".inf"> application/inf</option>
<option value=".ins"> application/x-internett-signup</option>
<option value=".ip"> application/x-ip2</option>
<option value=".isu"> video/x-isvideo</option>
<option value=".it"> audio/it</option>
<option value=".iv"> application/x-inventor</option>
<option value=".ivr"> i-world/i-vrml</option>
<option value=".ivy"> application/x-livescreen</option>
<option value=".jam"> audio/x-jam</option>
<option value=".jav"> text/plain</option>
<option value=".jav"> text/x-java-source</option>
<option value=".java"> text/plain</option>
<option value=".java"> text/x-java-source</option>
<option value=".jcm"> application/x-java-commerce</option>
<option value=".jfif"> image/jpeg</option>
<option value=".jfif"> image/pjpeg</option>
<option value=".jfif-tbnl"> image/jpeg</option>
<option value=".jpe"> image/jpeg</option>
<option value=".jpe"> image/pjpeg</option>
<option value=".jpeg"> image/jpeg</option>
<option value=".jpeg"> image/pjpeg</option>
<option value=".jpg"> image/jpeg</option>
<option value=".jpg"> image/pjpeg</option>
<option value=".jps"> image/x-jps</option>
<option value=".js"> application/x-javascript</option>
<option value=".js"> application/javascript</option>
<option value=".js"> application/ecmascript</option>
<option value=".js"> text/javascript</option>
<option value=".js"> text/ecmascript</option>
<option value=".jut"> image/jutvision</option>
<option value=".kar"> audio/midi</option>
<option value=".kar"> music/x-karaoke</option>
<option value=".ksh"> application/x-ksh</option>
<option value=".ksh"> text/x-script.ksh</option>
<option value=".la"> audio/nspaudio</option>
<option value=".la"> audio/x-nspaudio</option>
<option value=".lam"> audio/x-liveaudio</option>
<option value=".latex"> application/x-latex</option>
<option value=".lha"> application/lha</option>
<option value=".lha"> application/octet-stream</option>
<option value=".lha"> application/x-lha</option>
<option value=".lhx"> application/octet-stream</option>
<option value=".list"> text/plain</option>
<option value=".lma"> audio/nspaudio</option>
<option value=".lma"> audio/x-nspaudio</option>
<option value=".log"> text/plain</option>
<option value=".lsp"> application/x-lisp</option>
<option value=".lsp"> text/x-script.lisp</option>
<option value=".lst"> text/plain</option>
<option value=".lsx"> text/x-la-asf</option>
<option value=".ltx"> application/x-latex</option>
<option value=".lzh"> application/octet-stream</option>
<option value=".lzh"> application/x-lzh</option>
<option value=".lzx"> application/lzx</option>
<option value=".lzx"> application/octet-stream</option>
<option value=".lzx"> application/x-lzx</option>
<option value=".m"> text/plain</option>
<option value=".m"> text/x-m</option>
<option value=".m1v"> video/mpeg</option>
<option value=".m2a"> audio/mpeg</option>
<option value=".m2v"> video/mpeg</option>
<option value=".m3u"> audio/x-mpequrl</option>
<option value=".man"> application/x-troff-man</option>
<option value=".map"> application/x-navimap</option>
<option value=".mar"> text/plain</option>
<option value=".mbd"> application/mbedlet</option>
<option value=".mc$"> application/x-magic-cap-package-1.0</option>
<option value=".mcd"> application/mcad</option>
<option value=".mcd"> application/x-mathcad</option>
<option value=".mcf"> image/vasa</option>
<option value=".mcf"> text/mcf</option>
<option value=".mcp"> application/netmc</option>
<option value=".me"> application/x-troff-me</option>
<option value=".mht"> message/rfc822</option>
<option value=".mhtml"> message/rfc822</option>
<option value=".mid"> application/x-midi</option>
<option value=".mid"> audio/midi</option>
<option value=".mid"> audio/x-mid</option>
<option value=".mid"> audio/x-midi</option>
<option value=".mid"> music/crescendo</option>
<option value=".mid"> x-music/x-midi</option>
<option value=".midi"> application/x-midi</option>
<option value=".midi"> audio/midi</option>
<option value=".midi"> audio/x-mid</option>
<option value=".midi"> audio/x-midi</option>
<option value=".midi"> music/crescendo</option>
<option value=".midi"> x-music/x-midi</option>
<option value=".mif"> application/x-frame</option>
<option value=".mif"> application/x-mif</option>
<option value=".mime"> message/rfc822</option>
<option value=".mime"> www/mime</option>
<option value=".mjf"> audio/x-vnd.audioexplosion.mjuicemediafile</option>
<option value=".mjpg"> video/x-motion-jpeg</option>
<option value=".mm"> application/base64</option>
<option value=".mm"> application/x-meme</option>
<option value=".mme"> application/base64</option>
<option value=".mod"> audio/mod</option>
<option value=".mod"> audio/x-mod</option>
<option value=".moov"> video/quicktime</option>
<option value=".mov"> video/quicktime</option>
<option value=".movie"> video/x-sgi-movie</option>
<option value=".mp2"> audio/mpeg</option>
<option value=".mp2"> audio/x-mpeg</option>
<option value=".mp2"> video/mpeg</option>
<option value=".mp2"> video/x-mpeg</option>
<option value=".mp2"> video/x-mpeq2a</option>
<option value=".mp3"> audio/mpeg3</option>
<option value=".mp3"> audio/x-mpeg-3</option>
<option value=".mp3"> video/mpeg</option>
<option value=".mp3"> video/x-mpeg</option>
<option value=".mpa"> audio/mpeg</option>
<option value=".mpa"> video/mpeg</option>
<option value=".mpc"> application/x-project</option>
<option value=".mpe"> video/mpeg</option>
<option value=".mpeg"> video/mpeg</option>
<option value=".mpg"> audio/mpeg</option>
<option value=".mpg"> video/mpeg</option>
<option value=".mpga"> audio/mpeg</option>
<option value=".mpp"> application/vnd.ms-project</option>
<option value=".mpt"> application/x-project</option>
<option value=".mpv"> application/x-project</option>
<option value=".mpx"> application/x-project</option>
<option value=".mrc"> application/marc</option>
<option value=".ms"> application/x-troff-ms</option>
<option value=".mv"> video/x-sgi-movie</option>
<option value=".my"> audio/make</option>
<option value=".mzz"> application/x-vnd.audioexplosion.mzz</option>
<option value=".nap"> image/naplps</option>
<option value=".naplps"> image/naplps</option>
<option value=".nc"> application/x-netcdf</option>
<option value=".ncm"> application/vnd.nokia.configuration-message</option>
<option value=".nif"> image/x-niff</option>
<option value=".niff"> image/x-niff</option>
<option value=".nix"> application/x-mix-transfer</option>
<option value=".nsc"> application/x-conference</option>
<option value=".nvd"> application/x-navidoc</option>
<option value=".o"> application/octet-stream</option>
<option value=".oda"> application/oda</option>
<option value=".omc"> application/x-omc</option>
<option value=".omcd"> application/x-omcdatamaker</option>
<option value=".omcr"> application/x-omcregerator</option>
<option value=".p"> text/x-pascal</option>
<option value=".p10"> application/pkcs10</option>
<option value=".p10"> application/x-pkcs10</option>
<option value=".p12"> application/pkcs-12</option>
<option value=".p12"> application/x-pkcs12</option>
<option value=".p7a"> application/x-pkcs7-signature</option>
<option value=".p7c"> application/pkcs7-mime</option>
<option value=".p7c"> application/x-pkcs7-mime</option>
<option value=".p7m"> application/pkcs7-mime</option>
<option value=".p7m"> application/x-pkcs7-mime</option>
<option value=".p7r"> application/x-pkcs7-certreqresp</option>
<option value=".p7s"> application/pkcs7-signature</option>
<option value=".part"> application/pro_eng</option>
<option value=".pas"> text/pascal</option>
<option value=".pbm"> image/x-portable-bitmap</option>
<option value=".pcl"> application/vnd.hp-pcl</option>
<option value=".pcl"> application/x-pcl</option>
<option value=".pct"> image/x-pict</option>
<option value=".pcx"> image/x-pcx</option>
<option value=".pdb"> chemical/x-pdb</option>
<option value=".pdf"> application/pdf</option>
<option value=".pfunk"> audio/make</option>
<option value=".pfunk"> audio/make.my.funk</option>
<option value=".pgm"> image/x-portable-graymap</option>
<option value=".pgm"> image/x-portable-greymap</option>
<option value=".pic"> image/pict</option>
<option value=".pict"> image/pict</option>
<option value=".pkg"> application/x-newton-compatible-pkg</option>
<option value=".pko"> application/vnd.ms-pki.pko</option>
<option value=".pl"> text/plain</option>
<option value=".pl"> text/x-script.perl</option>
<option value=".plx"> application/x-pixclscript</option>
<option value=".pm"> image/x-xpixmap</option>
<option value=".pm"> text/x-script.perl-module</option>
<option value=".pm4"> application/x-pagemaker</option>
<option value=".pm5"> application/x-pagemaker</option>
<option value=".png"> image/png</option>
<option value=".pnm"> application/x-portable-anymap</option>
<option value=".pnm"> image/x-portable-anymap</option>
<option value=".pot"> application/mspowerpoint</option>
<option value=".pot"> application/vnd.ms-powerpoint</option>
<option value=".pov"> model/x-pov</option>
<option value=".ppa"> application/vnd.ms-powerpoint</option>
<option value=".ppm"> image/x-portable-pixmap</option>
<option value=".pps"> application/mspowerpoint</option>
<option value=".pps"> application/vnd.ms-powerpoint</option>
<option value=".ppt"> application/mspowerpoint</option>
<option value=".ppt"> application/powerpoint</option>
<option value=".ppt"> application/vnd.ms-powerpoint</option>
<option value=".ppt"> application/x-mspowerpoint</option>
<option value=".ppz"> application/mspowerpoint</option>
<option value=".pre"> application/x-freelance</option>
<option value=".prt"> application/pro_eng</option>
<option value=".ps"> application/postscript</option>
<option value=".psd"> application/octet-stream</option>
<option value=".pvu"> paleovu/x-pv</option>
<option value=".pwz"> application/vnd.ms-powerpoint</option>
<option value=".py"> text/x-script.phyton</option>
<option value=".pyc"> application/x-bytecode.python</option>
<option value=".qcp"> audio/vnd.qcelp</option>
<option value=".qd3"> x-world/x-3dmf</option>
<option value=".qd3d"> x-world/x-3dmf</option>
<option value=".qif"> image/x-quicktime</option>
<option value=".qt"> video/quicktime</option>
<option value=".qtc"> video/x-qtc</option>
<option value=".qti"> image/x-quicktime</option>
<option value=".qtif"> image/x-quicktime</option>
<option value=".ra"> audio/x-pn-realaudio</option>
<option value=".ra"> audio/x-pn-realaudio-plugin</option>
<option value=".ra"> audio/x-realaudio</option>
<option value=".ram"> audio/x-pn-realaudio</option>
<option value=".ras"> application/x-cmu-raster</option>
<option value=".ras"> image/cmu-raster</option>
<option value=".ras"> image/x-cmu-raster</option>
<option value=".rast"> image/cmu-raster</option>
<option value=".rexx"> text/x-script.rexx</option>
<option value=".rf"> image/vnd.rn-realflash</option>
<option value=".rgb"> image/x-rgb</option>
<option value=".rm"> application/vnd.rn-realmedia</option>
<option value=".rm"> audio/x-pn-realaudio</option>
<option value=".rmi"> audio/mid</option>
<option value=".rmm"> audio/x-pn-realaudio</option>
<option value=".rmp"> audio/x-pn-realaudio</option>
<option value=".rmp"> audio/x-pn-realaudio-plugin</option>
<option value=".rng"> application/ringing-tones</option>
<option value=".rng"> application/vnd.nokia.ringing-tone</option>
<option value=".rnx"> application/vnd.rn-realplayer</option>
<option value=".roff"> application/x-troff</option>
<option value=".rp"> image/vnd.rn-realpix</option>
<option value=".rpm"> audio/x-pn-realaudio-plugin</option>
<option value=".rt"> text/richtext</option>
<option value=".rt"> text/vnd.rn-realtext</option>
<option value=".rtf"> application/rtf</option>
<option value=".rtf"> application/x-rtf</option>
<option value=".rtf"> text/richtext</option>
<option value=".rtx"> application/rtf</option>
<option value=".rtx"> text/richtext</option>
<option value=".rv"> video/vnd.rn-realvideo</option>
<option value=".s"> text/x-asm</option>
<option value=".s3m"> audio/s3m</option>
<option value=".saveme"> application/octet-stream</option>
<option value=".sbk"> application/x-tbook</option>
<option value=".scm"> application/x-lotusscreencam</option>
<option value=".scm"> text/x-script.guile</option>
<option value=".scm"> text/x-script.scheme</option>
<option value=".scm"> video/x-scm</option>
<option value=".sdml"> text/plain</option>
<option value=".sdp"> application/sdp</option>
<option value=".sdp"> application/x-sdp</option>
<option value=".sdr"> application/sounder</option>
<option value=".sea"> application/sea</option>
<option value=".sea"> application/x-sea</option>
<option value=".set"> application/set</option>
<option value=".sgm"> text/sgml</option>
<option value=".sgm"> text/x-sgml</option>
<option value=".sgml"> text/sgml</option>
<option value=".sgml"> text/x-sgml</option>
<option value=".sh"> application/x-bsh</option>
<option value=".sh"> application/x-sh</option>
<option value=".sh"> application/x-shar</option>
<option value=".sh"> text/x-script.sh</option>
<option value=".shar"> application/x-bsh</option>
<option value=".shar"> application/x-shar</option>
<option value=".shtml"> text/html</option>
<option value=".shtml"> text/x-server-parsed-html</option>
<option value=".sid"> audio/x-psid</option>
<option value=".sit"> application/x-sit</option>
<option value=".sit"> application/x-stuffit</option>
<option value=".skd"> application/x-koan</option>
<option value=".skm"> application/x-koan</option>
<option value=".skp"> application/x-koan</option>
<option value=".skt"> application/x-koan</option>
<option value=".sl"> application/x-seelogo</option>
<option value=".smi"> application/smil</option>
<option value=".smil"> application/smil</option>
<option value=".snd"> audio/basic</option>
<option value=".snd"> audio/x-adpcm</option>
<option value=".sol"> application/solids</option>
<option value=".spc"> application/x-pkcs7-certificates</option>
<option value=".spc"> text/x-speech</option>
<option value=".spl"> application/futuresplash</option>
<option value=".spr"> application/x-sprite</option>
<option value=".sprite"> application/x-sprite</option>
<option value=".src"> application/x-wais-source</option>
<option value=".ssi"> text/x-server-parsed-html</option>
<option value=".ssm"> application/streamingmedia</option>
<option value=".sst"> application/vnd.ms-pki.certstore</option>
<option value=".step"> application/step</option>
<option value=".stl"> application/sla</option>
<option value=".stl"> application/vnd.ms-pki.stl</option>
<option value=".stl"> application/x-navistyle</option>
<option value=".stp"> application/step</option>
<option value=".sv4cpio"> application/x-sv4cpio</option>
<option value=".sv4crc"> application/x-sv4crc</option>
<option value=".svf"> image/vnd.dwg</option>
<option value=".svf"> image/x-dwg</option>
<option value=".svr"> application/x-world</option>
<option value=".svr"> x-world/x-svr</option>
<option value=".swf"> application/x-shockwave-flash</option>
<option value=".t"> application/x-troff</option>
<option value=".talk"> text/x-speech</option>
<option value=".tar"> application/x-tar</option>
<option value=".tbk"> application/toolbook</option>
<option value=".tbk"> application/x-tbook</option>
<option value=".tcl"> application/x-tcl</option>
<option value=".tcl"> text/x-script.tcl</option>
<option value=".tcsh"> text/x-script.tcsh</option>
<option value=".tex"> application/x-tex</option>
<option value=".texi"> application/x-texinfo</option>
<option value=".texinfo"> application/x-texinfo</option>
<option value=".text"> application/plain</option>
<option value=".text"> text/plain</option>
<option value=".tgz"> application/gnutar</option>
<option value=".tgz"> application/x-compressed</option>
<option value=".tif"> image/tiff</option>
<option value=".tif"> image/x-tiff</option>
<option value=".tiff"> image/tiff</option>
<option value=".tiff"> image/x-tiff</option>
<option value=".tr"> application/x-troff</option>
<option value=".tsi"> audio/tsp-audio</option>
<option value=".tsp"> application/dsptype</option>
<option value=".tsp"> audio/tsplayer</option>
<option value=".tsv"> text/tab-separated-values</option>
<option value=".turbot"> image/florian</option>
<option value=".txt"> text/plain</option>
<option value=".uil"> text/x-uil</option>
<option value=".uni"> text/uri-list</option>
<option value=".unis"> text/uri-list</option>
<option value=".unv"> application/i-deas</option>
<option value=".uri"> text/uri-list</option>
<option value=".uris"> text/uri-list</option>
<option value=".ustar"> application/x-ustar</option>
<option value=".ustar"> multipart/x-ustar</option>
<option value=".uu"> application/octet-stream</option>
<option value=".uu"> text/x-uuencode</option>
<option value=".uue"> text/x-uuencode</option>
<option value=".vcd"> application/x-cdlink</option>
<option value=".vcs"> text/x-vcalendar</option>
<option value=".vda"> application/vda</option>
<option value=".vdo"> video/vdo</option>
<option value=".vew"> application/groupwise</option>
<option value=".viv"> video/vivo</option>
<option value=".viv"> video/vnd.vivo</option>
<option value=".vivo"> video/vivo</option>
<option value=".vivo"> video/vnd.vivo</option>
<option value=".vmd"> application/vocaltec-media-desc</option>
<option value=".vmf"> application/vocaltec-media-file</option>
<option value=".voc"> audio/voc</option>
<option value=".voc"> audio/x-voc</option>
<option value=".vos"> video/vosaic</option>
<option value=".vox"> audio/voxware</option>
<option value=".vqe"> audio/x-twinvq-plugin</option>
<option value=".vqf"> audio/x-twinvq</option>
<option value=".vql"> audio/x-twinvq-plugin</option>
<option value=".vrml"> application/x-vrml</option>
<option value=".vrml"> model/vrml</option>
<option value=".vrml"> x-world/x-vrml</option>
<option value=".vrt"> x-world/x-vrt</option>
<option value=".vsd"> application/x-visio</option>
<option value=".vst"> application/x-visio</option>
<option value=".vsw"> application/x-visio</option>
<option value=".w60"> application/wordperfect6.0</option>
<option value=".w61"> application/wordperfect6.1</option>
<option value=".w6w"> application/msword</option>
<option value=".wav"> audio/wav</option>
<option value=".wav"> audio/x-wav</option>
<option value=".wb1"> application/x-qpro</option>
<option value=".wbmp"> image/vnd.wap.wbmp</option>
<option value=".web"> application/vnd.xara</option>
<option value=".wiz"> application/msword</option>
<option value=".wk1"> application/x-123</option>
<option value=".wmf"> windows/metafile</option>
<option value=".wml"> text/vnd.wap.wml</option>
<option value=".wmlc"> application/vnd.wap.wmlc</option>
<option value=".wmls"> text/vnd.wap.wmlscript</option>
<option value=".wmlsc"> application/vnd.wap.wmlscriptc</option>
<option value=".word"> application/msword</option>
<option value=".wp"> application/wordperfect</option>
<option value=".wp5"> application/wordperfect</option>
<option value=".wp5"> application/wordperfect6.0</option>
<option value=".wp6"> application/wordperfect</option>
<option value=".wpd"> application/wordperfect</option>
<option value=".wpd"> application/x-wpwin</option>
<option value=".wq1"> application/x-lotus</option>
<option value=".wri"> application/mswrite</option>
<option value=".wri"> application/x-wri</option>
<option value=".wrl"> application/x-world</option>
<option value=".wrl"> model/vrml</option>
<option value=".wrl"> x-world/x-vrml</option>
<option value=".wrz"> model/vrml</option>
<option value=".wrz"> x-world/x-vrml</option>
<option value=".wsc"> text/scriplet</option>
<option value=".wsrc"> application/x-wais-source</option>
<option value=".wtk"> application/x-wintalk</option>
<option value=".xbm"> image/x-xbitmap</option>
<option value=".xbm"> image/x-xbm</option>
<option value=".xbm"> image/xbm</option>
<option value=".xdr"> video/x-amt-demorun</option>
<option value=".xgz"> xgl/drawing</option>
<option value=".xif"> image/vnd.xiff</option>
<option value=".xl"> application/excel</option>
<option value=".xla"> application/excel</option>
<option value=".xla"> application/x-excel</option>
<option value=".xla"> application/x-msexcel</option>
<option value=".xlb"> application/excel</option>
<option value=".xlb"> application/vnd.ms-excel</option>
<option value=".xlb"> application/x-excel</option>
<option value=".xlc"> application/excel</option>
<option value=".xlc"> application/vnd.ms-excel</option>
<option value=".xlc"> application/x-excel</option>
<option value=".xld"> application/excel</option>
<option value=".xld"> application/x-excel</option>
<option value=".xlk"> application/excel</option>
<option value=".xlk"> application/x-excel</option>
<option value=".xll"> application/excel</option>
<option value=".xll"> application/vnd.ms-excel</option>
<option value=".xll"> application/x-excel</option>
<option value=".xlm"> application/excel</option>
<option value=".xlm"> application/vnd.ms-excel</option>
<option value=".xlm"> application/x-excel</option>
<option value=".xls"> application/excel</option>
<option value=".xls"> application/vnd.ms-excel</option>
<option value=".xls"> application/x-excel</option>
<option value=".xls"> application/x-msexcel</option>
<option value=".xlt"> application/excel</option>
<option value=".xlt"> application/x-excel</option>
<option value=".xlv"> application/excel</option>
<option value=".xlv"> application/x-excel</option>
<option value=".xlw"> application/excel</option>
<option value=".xlw"> application/vnd.ms-excel</option>
<option value=".xlw"> application/x-excel</option>
<option value=".xlw"> application/x-msexcel</option>
<option value=".xm"> audio/xm</option>
<option value=".xml"> application/xml</option>
<option value=".xml"> text/xml</option>
<option value=".xmz"> xgl/movie</option>
<option value=".xpix"> application/x-vnd.ls-xpix</option>
<option value=".xpm"> image/x-xpixmap</option>
<option value=".xpm"> image/xpm</option>
<option value=".x-png"> image/png</option>
<option value=".xsr"> video/x-amt-showrun</option>
<option value=".xwd"> image/x-xwd</option>
<option value=".xwd"> image/x-xwindowdump</option>
<option value=".xyz"> chemical/x-pdb</option>
<option value=".z"> application/x-compress</option>
<option value=".z"> application/x-compressed</option>
<option value=".zip"> application/x-compressed</option>
<option value=".zip"> application/x-zip-compressed</option>
<option value=".zip"> application/zip</option>
<option value=".zip"> multipart/x-zip</option>
<option value=".zoo"> application/octet-stream</option>
<option value=".zsh"> text/x-script.zsh</option>

57
templates/shared/pagination.html

@ -0,0 +1,57 @@
<nav class="pagination" role="navigation" aria-label="pagination">
<div class="pagination-list">
<div>
<div>
{% if pagination.page == 1 %}
<span class="ui-disabled pagination" aria-label="Goto previous page">First</span>
{% else %}
<a class="ui-link pagination" aria-label="Goto previous page"
href="/{{root-url}}?PAGE=1">First</a>
{% endif %}
{% if pagination.page > 1 %}
<a class="ui-link pagination"
aria-label="Goto previous page"
href="/{{root-url}}?PAGE={{pagination.page|add:-1}}">Prev.</a>
{% else %}
<span class="ui-disabled pagination" aria-label="Goto previous page">Prev.</span>
{% endif %}
</div>
<div>
{% for item in pagination.button-range %}
{% if item > 0
and item <= pagination.nb-pages %}
{% if item == pagination.page %}
<a class="ui-link ui-is-current pagination"
aria-label="Goto page {{item}}"
href="/{{root-url}}?PAGE={{item}}">{{item}}</a>
{% else %}
<a class="ui-link pagination" aria-label="Goto page {{item}}"
href="/{{root-url}}?PAGE={{item}}">{{item}}</a>
{% endif %}
{% endif %}
{% endfor %}
</div>
<div>
{% if pagination.page < pagination.nb-pages %}
<a class="ui-link pagination"
aria-label="Goto previous page"
href="/{{root-url}}?PAGE={{pagination.page|add:1}}">Next</a>
{% else %}
<span class="ui-disabled"
aria-label="Goto previous page">Next</span>
{% endif %}
{% ifequal pagination.page pagination.nb-pages %}
<span class="ui-disabled" aria-label="Goto next page">Last</span>
{% else %}
<a class="ui-link pagination" aria-label="Goto next page"
href="/{{root-url}}?PAGE={{pagination.nb-pages}}">Last</a>
{% endifequal %}
</div>
</div>
<div>{{pagination.text-label}}</div>
</div>
</nav>

28
templates/sign-up.html

@ -0,0 +1,28 @@
{% extends "layouts/base.html" %}
{% block title %}Create your account and start turning your spreadsheets into interactive charts.{% endblock %}
{% block content %}
<h1>Create Account</h1>
<div class="ui-form">
<form class="ui-form-add" action="/sign-up" method="post">
<input required type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input required type="hidden" name="METHOD" value="sign-up-user">
<fieldset>
<legend>Account Details</legend>
<label>Username <em>(Cannot change after account created.)</em></label>
<input required type="text" name="USERNAME">
<label>Display Name</label>
<input required type="text" name="DISPLAY-NAME">
<label>Password</label>
<input required type="password" name="PASSWORD">
</fieldset>
<input class="ui-button-add" type="submit" value="Create Account"/>
</form>
</div>
{% endblock %}

44
templates/user/add.html

@ -0,0 +1,44 @@
{% extends "layouts/base.html" %}
{% block title %}Craig Oates: Add New User{% endblock %}
{% block content %}
{% if roles.logged-in %}
<div class="logged-in-options">
<a class="ui-link-admin" href="/users">Manage</a>
</div>
{% endif %}
<h1>Add User</h1>
<p class="ui-message-warning">The username can not be changed after the account has been created.</p>
<div class="ui-form">
<form class="ui-form-add" action="/user" method="post">
<input required type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input required type="hidden" name="METHOD" value="add">
<fieldset>
<legend>User Details</legend>
<label>Username</label>
<input type="text" name="USERNAME">
<label>Display Name</label>
<input required type="text" name="DISPLAY-NAME">
<label>Password</label>
<input required type="password" name="PASSWORD">
</fieldset>
<fieldset class="ui-form-checkbox-fieldset">
<legend>User Privileges</legend>
<div>
<input type="checkbox" name="ADMINISTRATOR">
<label>Administrator</label>
</div>
</fieldset>
<input class="ui-button-add" type="submit" value="Add User"/>
</form>
</div>
{% endblock %}

80
templates/user/dashboard.html

@ -0,0 +1,80 @@
{% extends "layouts/base.html" %}
{% block title %}{{user.display-name}}: Dashboard{% endblock %}
{% block content %}
{% if roles.administrator == true %}
<span class="console-output">{{python-output}}</span>
{% endif %}
<main class="dashboard">
<div class="dashboard-header">
<h1>Dashboard</h1>
<h2>{{user.display-name}}</h2>
</div>
<section>
<div>
<h3><span>{{storage-files | length}}</span> Files</h3>
<div>
<a class="ui-link-add" href="/chart/add">New Chart</a>
</div>
</div>
<table class="storage-dashboard-table">
<thead>
<tr>
<th>Type</th>
<th>File Name</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{% for file in storage-files %}
<tr>
<td>
<img src="{{file | chart-icon}}">
</td>
<td>
{{file}}
</td>
<td class="flex-end">
<a class="ui-link" href="/storage/download/{{user.username}}/{{file}}">
Download
</a>
<form action="/storage" method="post">
<input type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input type="hidden" name="METHOD" value="delete-storage-file">
<input type="hidden" name="FILENAME" value="{{file}}">
<input class="ui-button-danger" type="submit" value="Delete">
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<section>
<h3>Account Options</h3>
<p class="ui-message-danger">
Warning: Deleting your account is permanent and cannot be undone.
</p>
<div>
{% if roles.administrator == true %}
<a class="ui-link-add" href="/user/add">Add User</a>
<a class="ui-link-admin" href="/users">Manage Users</a>
{% endif %}
<a class="ui-link-edit" href="/user/edit/{{user.username}}">Edit Account</a>
<form action="/user" method="post">
<input type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input type="hidden" name="METHOD" value="delete-user">
<input type="hidden" name="USERNAME" value="{{user.username}}">
<input class="ui-button-danger" type="submit" value="Delete Account">
</form>
</div>
</section>
</main>
{% endblock %}

66
templates/user/edit.html

@ -0,0 +1,66 @@
{% extends "layouts/base.html" %}
{% block title %}{{user.display-name}}: Edit Account{% endblock %}
{% block content %}
{% if roles.administrator == True %}
<div class="logged-in-options">
<a class="ui-link-add" href="/user/add">Add</a>
<a class="ui-link-admin" href="/users">Manage</a>
</div>
{% endif %}
<h1>Edit Account</h1>
{% if roles.logged-in and roles.administrator == true %}
<div class="ui-form">
<form class="ui-form-edit" action="/user" method="post">
<input required type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input required type="hidden" name="METHOD" value="update-role">
<input required type="hidden" name="USERNAME" value="{{user-to-edit.username}}">
<fieldset class="ui-form-checkbox-fieldset">
<legend>User Privileges</legend>
<div>
<input type="checkbox" name="ADMINISTRATOR"
{% ifequal user-to-edit.administrator 1 %}checked{% endifequal %}>
<label>Administrator</label>
</div>
</fieldset>
<input class="ui-button-edit" type="submit" value="Update Privileges">
</form>
</div>
{% endif %}
<div class="ui-form">
<form class="ui-form-edit" action="/user" method="post">
<input required type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input required type="hidden" name="METHOD" value="update-display-name">
<input required type="hidden" name="USERNAME" value="{{user-to-edit.username}}">
<input required type="hidden" name="PASSWORD" value="{{user-to-edit.password}}">
<label>Display Name:</label>
<input required type="text" name="DISPLAY-NAME" value="{{user-to-edit.display-name}}">
<input class="ui-button-edit" type="submit" value="Update Display Name">
</form>
</div>
<div class="ui-form">
<form class="ui-form-edit"action="/user" method="post">
<input required type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input required type="hidden" name="METHOD" value="update-password">
<input required type="hidden" name="USERNAME" value="{{user-to-edit.username}}">
{% ifequal user-to-edit.username user.username %}
<label>Old Password:</label>
<input required type="password" name="OLD-PASSWORD">
{% endifequal %}
<label>New Password:</label>
<input required type="password" name="NEW-PASSWORD">
<input class="ui-button-edit" type="submit" value="Update Password">
</form>
</div>
{% endblock %}

87
templates/user/index.html

@ -0,0 +1,87 @@
{% extends "layouts/base.html" %}
{% block title %}Craig Oates: Users{% endblock %}
{% block content %}
<div class="logged-in-options">
<a class="ui-link-add" href="/user/add">Add</a>
</div>
<div class="dashboard">
<section>
<h1><span>{{user-count}}</span> Users</h1>
<table class="summary-dashboard-table">
<thead>
<tr>
<th>Total</th>
<th>Category</th>
</tr>
</thead>
<tbody>
{% for item in categories %}
<tr>
<td>{{item.col-totals}}</td>
<td>
{% if item.administrator == 0 %}
Basic
{% else %}
Administrator
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<table class="user-manage-table">
<thead>
<tr>
<th>Display Name</th>
<th>Username</th>
<th>Account Type</th>
<th>Created</th>
<th>Updated</th>
<th colspan="2">Options</th>
<tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{user.display-name}}</td>
<td>{{user.username}}</td>
<td>
{% if user.administrator == 0 %}
Basic
{% else %}
Administrator
{% endif %}
</td>
<td>{{user.created-at
| date: ((:year 4)"/"(:month 2)"/"(:day 2)" "(:hour 2)":"(:min 2))}}
</td>
<td>
{% ifnotequal user.updated-at null %}
{{user.updated-at
| date: ((:year 4)"/"(:month 2)"/"(:day 2)" "(:hour 2)":"(:min 2))}}
{% else %}
<span class="ui-muted-text">N/A</span>
{% endifnotequal %}
</td>
<td><a class="ui-link-edit" href="/user/edit/{{user.username}}">Edit</a></td>
<td>
<form action="/user" method="post">
<input type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input type="hidden" name="METHOD" value="delete-user">
<input type="hidden" name="USERNAME" value="{{user.username}}">
<input class="ui-button-danger" type="submit" value="Delete">
</form>
<td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}

20
templates/user/log-in.html

@ -0,0 +1,20 @@
{% extends "layouts/base.html" %}
{% block title%}
Log-in to charts.craigoates.net and start turning your spreadsheets into interactive charts.
{%endblock%}
{% block content %}
<h2>Login</h2>
<div class="ui-form">
<form class="ui-form-admin" action="/login" method="post">
<input type="hidden" name="AUTHENTICITY-TOKEN" value="{{token}}">
<input type="hidden" name="METHOD" value="login">
<label>Username:</label>
<input required type="text" name="USERNAME">
<label>password:</label>
<input required type="password" name="PASSWORD">
<input class="ui-button-admin" type="submit" value="Log-in">
</form>
</div>
{% endblock %}

21
tests/tests.lisp

@ -0,0 +1,21 @@
(in-package :cl-user)
(defpackage #:tests
(:use #:cl
#:parachute))
(in-package #:tests)
#| parachute: https://shinmera.github.io/parachute/
================================================================================
Use the URL to access the documentation for parachute.
|#
;; This was an example taken from the doc's for parachute. I'm going to keep it
;; here as a reference until I get comfortable with parachute.
(define-test reference-tests
(of-type integer 5)
(true (numberp 2/3))
(false (numberp :keyword))
(is-values (values 0 "1")
(= 0)
(equal "1")))
Loading…
Cancel
Save