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