@ -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 |
@ -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) |
||||
|
@ -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,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() |
@ -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 |
@ -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))) |
@ -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")) |
@ -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)) |
@ -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))))) |
@ -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.") |
@ -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)) |
||||
|
@ -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)) |
@ -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)) |
@ -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))) |
@ -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)))) |
@ -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)) |
@ -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"))))))))) |
@ -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)))) |
@ -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))))) |
@ -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))) |
@ -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)) |
@ -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)))) |
@ -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*)) |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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; |
||||
} |
||||
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 8.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 8.9 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 1.0 KiB |
After Width: | Height: | Size: 9.3 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 44 KiB |
After Width: | Height: | Size: 486 KiB |
After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 74 KiB |
After Width: | Height: | Size: 7.5 KiB |
After Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 68 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 17 KiB |
@ -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> |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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> |
@ -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 %} |
@ -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 %} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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 %} |
@ -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"))) |