Compare commits
15 Commits
37534b31bf
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
6546f73b98
|
|||
|
b6d9916267
|
|||
|
0bb64fd144
|
|||
|
4baffbe014
|
|||
|
46b1858897
|
|||
|
4c8411b863
|
|||
|
7526ebbd03
|
|||
|
6131c58857
|
|||
|
b924c0556c
|
|||
|
1295e4f76d
|
|||
|
c929744f0e
|
|||
|
635350f5ac
|
|||
|
029afeb389
|
|||
|
b3df0bf249
|
|||
|
0488a3e0b2
|
159
.gitignore
vendored
Normal file
159
.gitignore
vendored
Normal file
@@ -0,0 +1,159 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
# lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
stack.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
env-glibc/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
.bash_history
|
||||
.vscode/
|
||||
core/static/admin
|
||||
core/static/debug_toolbar
|
||||
30
.pre-commit-config.yaml
Normal file
30
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,30 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: ^core/migrations
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.11.5
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args: [--max-line-length=88]
|
||||
exclude: ^core/migrations
|
||||
- repo: https://github.com/rtts/djhtml
|
||||
rev: v2.0.0
|
||||
hooks:
|
||||
- id: djhtml
|
||||
args: [-t 2]
|
||||
- id: djcss
|
||||
exclude : ^core/static/css # slow
|
||||
- id: djjs
|
||||
exclude: ^core/static/js # slow
|
||||
- repo: https://github.com/sirwart/ripsecrets.git
|
||||
rev: v0.1.5
|
||||
hooks:
|
||||
- id: ripsecrets
|
||||
@@ -49,3 +49,17 @@ if DEBUG:
|
||||
]
|
||||
|
||||
SETTINGS_EXPORT = ["BILLING_ENABLED"]
|
||||
|
||||
MAIN_SIZES = ["1", "5", "15", "30", "50", "100", "250", "500", "1000"]
|
||||
MAIN_SIZES_ANON = ["1", "5", "15", "30", "50", "100"]
|
||||
MAIN_SOURCES = ["substances", "experiences", "all"]
|
||||
# CACHE = False
|
||||
# CACHE_TIMEOUT = 2
|
||||
|
||||
DRUGS_RESULTS_PER_PAGE = 15
|
||||
DRUGS_DEFAULT_PARAMS = {
|
||||
"size": "15",
|
||||
"sorting": "desc",
|
||||
"source": "substances",
|
||||
"index": "main",
|
||||
}
|
||||
|
||||
@@ -41,8 +41,8 @@ INSTALLED_APPS = [
|
||||
"django_htmx",
|
||||
"crispy_forms",
|
||||
"crispy_bulma",
|
||||
# "django_tables2",
|
||||
# "django_tables2_bulma_template",
|
||||
"django_tables2",
|
||||
"django_tables2_bulma_template",
|
||||
"django_otp",
|
||||
"django_otp.plugins.otp_totp",
|
||||
# "django_otp.plugins.otp_email",
|
||||
@@ -58,17 +58,17 @@ INSTALLED_APPS = [
|
||||
]
|
||||
|
||||
# Performance optimisations
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "unix:///var/run/socks/redis.sock",
|
||||
"OPTIONS": {
|
||||
"db": "10",
|
||||
# "parser_class": "django_redis.cache.RedisCache",
|
||||
"pool_class": "redis.BlockingConnectionPool",
|
||||
},
|
||||
}
|
||||
}
|
||||
# CACHES = {
|
||||
# "default": {
|
||||
# "BACKEND": "django_redis.cache.RedisCache",
|
||||
# "LOCATION": "unix:///var/run/socks/redis.sock",
|
||||
# "OPTIONS": {
|
||||
# "db": "10",
|
||||
# # "parser_class": "django_redis.cache.RedisCache",
|
||||
# "pool_class": "redis.BlockingConnectionPool",
|
||||
# },
|
||||
# }
|
||||
# }
|
||||
# CACHE_MIDDLEWARE_ALIAS = 'default'
|
||||
# CACHE_MIDDLEWARE_SECONDS = '600'
|
||||
# CACHE_MIDDLEWARE_KEY_PREFIX = ''
|
||||
|
||||
42
app/urls.py
42
app/urls.py
@@ -20,11 +20,12 @@ from django.contrib.auth.views import LogoutView
|
||||
from django.urls import include, path
|
||||
from two_factor.urls import urlpatterns as tf_urls
|
||||
|
||||
from core.views import base, demo, drugs, notifications
|
||||
from core.views import base, demo, drugs, favourites, notifications, search
|
||||
|
||||
urlpatterns = [
|
||||
path("__debug__/", include("debug_toolbar.urls")),
|
||||
path("", base.Home.as_view(), name="home"),
|
||||
path("", search.DrugsTableView.as_view(), name="home"),
|
||||
# path("", base.Home.as_view(), name="home"),
|
||||
path("admin/", admin.site.urls),
|
||||
# 2FA login urls
|
||||
path("", include(tf_urls)),
|
||||
@@ -56,9 +57,46 @@ urlpatterns = [
|
||||
drugs.DrugDelete.as_view(),
|
||||
name="drug_delete",
|
||||
),
|
||||
path(
|
||||
"drugs/<str:type>/detail/<str:pk>/",
|
||||
drugs.DrugDetail.as_view(),
|
||||
name="drug_detail",
|
||||
),
|
||||
path(
|
||||
"drugs/clear/all/",
|
||||
drugs.DrugClear.as_view(),
|
||||
name="drug_clear",
|
||||
),
|
||||
path(
|
||||
"drugs/refresh/all/",
|
||||
drugs.DrugPullMerge.as_view(),
|
||||
name="drug_pull_merge",
|
||||
),
|
||||
# Drug search
|
||||
path("search/", search.DrugsTableView.as_view(), name="search"),
|
||||
path("search/partial/", search.DrugsTableView.as_view(), name="search_partial"),
|
||||
# Favourites
|
||||
path(
|
||||
"favourites/<str:type>/", favourites.FavouriteList.as_view(), name="favourites"
|
||||
),
|
||||
path(
|
||||
"favourites/<str:type>/create/",
|
||||
favourites.FavouriteCreate.as_view(),
|
||||
name="favourite_create",
|
||||
),
|
||||
path(
|
||||
"favourites/<str:type>/update/<str:pk>/",
|
||||
favourites.FavouriteUpdate.as_view(),
|
||||
name="favourite_update",
|
||||
),
|
||||
path(
|
||||
"favourites/<str:type>/delete/<str:pk>/",
|
||||
favourites.FavouriteDelete.as_view(),
|
||||
name="favourite_delete",
|
||||
),
|
||||
path(
|
||||
"favourites/<str:type>/detail/<str:pk>/",
|
||||
favourites.FavouriteDetail.as_view(),
|
||||
name="favourite_detail",
|
||||
),
|
||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||
|
||||
@@ -11,6 +11,7 @@ from .models import (
|
||||
Entry,
|
||||
Experience,
|
||||
ExperienceDose,
|
||||
Favourite,
|
||||
NotificationSettings,
|
||||
Source,
|
||||
Timing,
|
||||
@@ -57,3 +58,4 @@ admin.site.register(Experience)
|
||||
admin.site.register(Source)
|
||||
admin.site.register(SEI)
|
||||
admin.site.register(ExperienceDose)
|
||||
admin.site.register(Favourite)
|
||||
|
||||
0
core/clients/__init__.py
Normal file
0
core/clients/__init__.py
Normal file
205
core/clients/base.py
Normal file
205
core/clients/base.py
Normal file
@@ -0,0 +1,205 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import aiohttp
|
||||
import orjson
|
||||
from glom import glom
|
||||
from pydantic.error_wrappers import ValidationError
|
||||
|
||||
from core.lib import schemas
|
||||
from core.util import logs
|
||||
|
||||
# Return error if the schema for the message type is not found
|
||||
STRICT_VALIDATION = False
|
||||
|
||||
# Raise exception if the conversion schema is not found
|
||||
STRICT_CONVERSION = False
|
||||
|
||||
# TODO: Set them to True when all message types are implemented
|
||||
|
||||
log = logs.get_logger("clients")
|
||||
|
||||
|
||||
class NoSchema(Exception):
|
||||
"""
|
||||
Raised when:
|
||||
- The schema for the message type is not found
|
||||
- The conversion schema is not found
|
||||
- There is no schema library for the client
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NoSuchMethod(Exception):
|
||||
"""
|
||||
Client library has no such method.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GenericAPIError(Exception):
|
||||
"""
|
||||
Generic API error.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def is_camel_case(s):
|
||||
return s != s.lower() and s != s.upper() and "_" not in s
|
||||
|
||||
|
||||
def snake_to_camel(word):
|
||||
if is_camel_case(word):
|
||||
return word
|
||||
return "".join(x.capitalize() or "_" for x in word.split("_"))
|
||||
|
||||
|
||||
DEFAULT_HEADERS = {
|
||||
"accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
class BaseClient(ABC):
|
||||
token = None
|
||||
|
||||
async def __new__(cls, *a, **kw):
|
||||
instance = super().__new__(cls)
|
||||
await instance.__init__(*a, **kw)
|
||||
return instance
|
||||
|
||||
async def __init__(self):
|
||||
"""
|
||||
Initialise the client.
|
||||
:param instance: the database object, e.g. Aggregator
|
||||
"""
|
||||
name = self.__class__.__name__
|
||||
self.name = name.replace("Client", "").lower()
|
||||
# self.instance = instance
|
||||
self.client = None
|
||||
|
||||
await self.connect()
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def schema(self):
|
||||
"""
|
||||
Get the schema library for the client.
|
||||
"""
|
||||
# Does the schemas library have a library for this client name?
|
||||
if hasattr(schemas, f"{self.name}_s"):
|
||||
schema_instance = getattr(schemas, f"{self.name}_s")
|
||||
else:
|
||||
log.error(f"No schema library for {self.name}")
|
||||
raise Exception(f"No schema library for client {self.name}")
|
||||
|
||||
return schema_instance
|
||||
|
||||
def get_schema(self, method, convert=False):
|
||||
if isinstance(method, str):
|
||||
to_camel = snake_to_camel(method)
|
||||
else:
|
||||
to_camel = snake_to_camel(method.__class__.__name__)
|
||||
if convert:
|
||||
to_camel = f"{to_camel}Schema"
|
||||
|
||||
# if hasattr(self.schema, method):
|
||||
# schema = getattr(self.schema, method)
|
||||
if hasattr(self.schema, to_camel):
|
||||
schema = getattr(self.schema, to_camel)
|
||||
else:
|
||||
raise NoSchema(f"Could not get schema: {to_camel}")
|
||||
return schema
|
||||
|
||||
async def call_method(self, method, *args, **kwargs):
|
||||
"""
|
||||
Call a method with aiohttp.
|
||||
"""
|
||||
if kwargs.get("append_slash", True):
|
||||
path = f"{self.url}/{method}/"
|
||||
else:
|
||||
path = f"{self.url}/{method}"
|
||||
|
||||
http_method = kwargs.get("http_method", "get")
|
||||
|
||||
cast = {
|
||||
"headers": DEFAULT_HEADERS,
|
||||
}
|
||||
|
||||
# Use the token if it's set
|
||||
if self.token is not None:
|
||||
cast["headers"]["Authorization"] = f"Bearer {self.token}"
|
||||
|
||||
if "data" in kwargs:
|
||||
cast["data"] = orjson.dumps(kwargs["data"])
|
||||
|
||||
# Use the method to send a HTTP request
|
||||
async with aiohttp.ClientSession() as session:
|
||||
session_method = getattr(session, http_method)
|
||||
async with session_method(path, **cast) as response:
|
||||
response_json = await response.json()
|
||||
return response_json
|
||||
|
||||
def convert_spec(self, response, method):
|
||||
"""
|
||||
Convert an API response to the requested spec.
|
||||
:raises NoSchema: If the conversion schema is not found
|
||||
"""
|
||||
schema = self.get_schema(method, convert=True)
|
||||
|
||||
# Use glom to convert the response to the schema
|
||||
converted = glom(response, schema)
|
||||
return converted
|
||||
|
||||
def validate_response(self, response, method):
|
||||
schema = self.get_schema(method)
|
||||
# Return a dict of the validated response
|
||||
try:
|
||||
response_valid = schema(**response).dict()
|
||||
except ValidationError as e:
|
||||
log.error(f"Error validating {method} response: {response}")
|
||||
log.error(f"Errors: {e}")
|
||||
raise GenericAPIError("Error validating response")
|
||||
return response_valid
|
||||
|
||||
def method_filter(self, method):
|
||||
"""
|
||||
Return a new method.
|
||||
"""
|
||||
return method
|
||||
|
||||
async def call(self, method, *args, **kwargs):
|
||||
"""
|
||||
Call the exchange API and validate the response
|
||||
:raises NoSchema: If the method is not in the schema mapping
|
||||
:raises ValidationError: If the response cannot be validated
|
||||
"""
|
||||
# try:
|
||||
response = await self.call_method(method, *args, **kwargs)
|
||||
# except (APIError, V20Error) as e:
|
||||
# log.error(f"Error calling method {method}: {e}")
|
||||
# raise GenericAPIError(e)
|
||||
|
||||
if "schema" in kwargs:
|
||||
method = kwargs["schema"]
|
||||
else:
|
||||
method = self.method_filter(method)
|
||||
try:
|
||||
response_valid = self.validate_response(response, method)
|
||||
except NoSchema as e:
|
||||
log.error(f"{e} - {response}")
|
||||
response_valid = response
|
||||
# Convert the response to a format that we can use
|
||||
try:
|
||||
response_converted = self.convert_spec(response_valid, method)
|
||||
except NoSchema as e:
|
||||
log.error(f"{e} - {response}")
|
||||
response_converted = response_valid
|
||||
|
||||
# return (True, response_converted)
|
||||
return response_converted
|
||||
26
core/clients/graphql.py
Normal file
26
core/clients/graphql.py
Normal file
@@ -0,0 +1,26 @@
|
||||
from abc import ABC
|
||||
|
||||
from core.models import Source
|
||||
from core.util import logs
|
||||
|
||||
log = logs.get_logger("graphql")
|
||||
|
||||
|
||||
class GraphQLClient(ABC):
|
||||
"""
|
||||
GraphQL API handler.
|
||||
"""
|
||||
|
||||
async def connect(self):
|
||||
try:
|
||||
source = Source.objects.get(name=self.source_name)
|
||||
except Source.DoesNotExist:
|
||||
source = Source(
|
||||
name=self.source_name,
|
||||
type=self.source_type,
|
||||
endpoint=self.source_endpoint,
|
||||
score=self.source_score,
|
||||
)
|
||||
source.save()
|
||||
self.url = source.endpoint
|
||||
self.source = source
|
||||
299
core/clients/sources/psychwiki.py
Normal file
299
core/clients/sources/psychwiki.py
Normal file
@@ -0,0 +1,299 @@
|
||||
from core.clients.base import BaseClient
|
||||
from core.clients.graphql import GraphQLClient
|
||||
from core.models import SEI, Dosage, Drug, Effect, Entry, Timing
|
||||
|
||||
|
||||
class PsychWikiClient(GraphQLClient, BaseClient):
|
||||
# url = "https://api.psychonautwiki.org"
|
||||
search_limit = 5000
|
||||
source_name = "Psychonaut Wiki GraphQL API"
|
||||
source_type = "DWIKI"
|
||||
source_endpoint = "https://api.psychonautwiki.org"
|
||||
source_score = 75
|
||||
|
||||
async def update_drugs(self):
|
||||
data = await self.get_all_data()
|
||||
self.store_data(data)
|
||||
return len(data)
|
||||
|
||||
def store_data(self, data):
|
||||
"""
|
||||
Store the data in the database.
|
||||
"""
|
||||
for drug in data["substances"]:
|
||||
print("DRUG ITER", drug)
|
||||
try:
|
||||
drug_obj = Drug.objects.get(name=drug["name"])
|
||||
except Drug.DoesNotExist:
|
||||
drug_obj = Drug(name=drug["name"])
|
||||
|
||||
if "commonNames" in drug:
|
||||
if drug["commonNames"]:
|
||||
print("common names", drug["commonNames"])
|
||||
drug_obj.common_names = ",".join(drug["commonNames"])
|
||||
if "class" in drug:
|
||||
if drug["class"]:
|
||||
if "psychoactive" in drug["class"]:
|
||||
if drug["class"]["psychoactive"]:
|
||||
drug_obj.drug_class = ",".join(
|
||||
drug["class"]["psychoactive"]
|
||||
)
|
||||
try:
|
||||
entry = Entry.objects.get(source=self.source, url=drug["url"])
|
||||
except Entry.DoesNotExist:
|
||||
entry = Entry.objects.create(source=self.source, url=drug["url"])
|
||||
if not drug_obj.pk:
|
||||
drug_obj.save()
|
||||
if entry not in drug_obj.links.all():
|
||||
drug_obj.links.add(entry)
|
||||
if "roas" in drug:
|
||||
for roa in drug["roas"]:
|
||||
if "name" in roa:
|
||||
roa_name = roa["name"]
|
||||
|
||||
# Parsing dosage information
|
||||
dose = roa["dose"]
|
||||
|
||||
if dose:
|
||||
dosage_data = {
|
||||
"entry": entry,
|
||||
"roa": roa_name,
|
||||
"unit": dose["units"],
|
||||
}
|
||||
# Check and assign dosage values
|
||||
for dose_type in [
|
||||
"threshold",
|
||||
"light",
|
||||
"common",
|
||||
"strong",
|
||||
"heavy",
|
||||
]:
|
||||
if dose_type in dose:
|
||||
if isinstance(dose[dose_type], dict):
|
||||
dosage_data[f"{dose_type}_lower"] = dose[
|
||||
dose_type
|
||||
]["min"]
|
||||
dosage_data[f"{dose_type}_upper"] = dose[
|
||||
dose_type
|
||||
]["max"]
|
||||
else:
|
||||
dosage_data[f"{dose_type}_lower"] = dose[
|
||||
dose_type
|
||||
]
|
||||
dosage_data[f"{dose_type}_upper"] = dose[
|
||||
dose_type
|
||||
]
|
||||
|
||||
# Check if Dosage already exists and is linked to the Drug
|
||||
dosage, created = Dosage.objects.get_or_create(
|
||||
**dosage_data
|
||||
)
|
||||
if created or dosage not in drug_obj.dosages.all():
|
||||
drug_obj.dosages.add(dosage)
|
||||
print("YES DOSAGE", drug_obj.dosages)
|
||||
|
||||
# Parsing timing information
|
||||
timing = roa["duration"]
|
||||
|
||||
print("TIMING", timing)
|
||||
# Check and assign timing values
|
||||
if timing:
|
||||
timing_data = {"entry": entry, "roa": roa_name}
|
||||
for time_type in [
|
||||
"onset",
|
||||
"comeup",
|
||||
"peak",
|
||||
"offset",
|
||||
"total",
|
||||
]:
|
||||
if (
|
||||
time_type in timing
|
||||
and timing[time_type] is not None
|
||||
):
|
||||
unit = timing[time_type].get("units", "hours")
|
||||
|
||||
# Handle case where timing value is a single integer
|
||||
if isinstance(timing[time_type], int):
|
||||
lower = timing[time_type]
|
||||
upper = None
|
||||
else:
|
||||
lower = timing[time_type].get("min")
|
||||
upper = timing[time_type].get("max")
|
||||
|
||||
if unit == "minutes" and lower is not None:
|
||||
lower = lower / 60.0
|
||||
if upper is not None:
|
||||
upper = upper / 60.0
|
||||
|
||||
timing_data[f"{time_type}_lower"] = lower
|
||||
timing_data[f"{time_type}_upper"] = upper
|
||||
timing_data[
|
||||
"unit"
|
||||
] = "HOURS" # Store all times in hours
|
||||
|
||||
# Check if Timing already exists and is linked to the Drug
|
||||
timing_obj, created = Timing.objects.get_or_create(
|
||||
**timing_data
|
||||
)
|
||||
if created or timing_obj not in drug_obj.timings.all():
|
||||
drug_obj.timings.add(timing_obj)
|
||||
if "effects" in drug:
|
||||
# Create or retrieve Effect object linked to the Entry
|
||||
effect_obj, effect_created = Effect.objects.get_or_create(entry=entry)
|
||||
|
||||
for effect in drug["effects"]:
|
||||
# Create or retrieve SEI object
|
||||
sei_obj, sei_created = SEI.objects.get_or_create(
|
||||
name=effect["name"],
|
||||
url=effect.get(
|
||||
"url", ""
|
||||
), # Using .get() to handle missing urls
|
||||
)
|
||||
|
||||
# Link SEI object to Effect if not already linked
|
||||
if (
|
||||
sei_created
|
||||
or sei_obj not in effect_obj.subjective_effects.all()
|
||||
):
|
||||
effect_obj.subjective_effects.add(sei_obj)
|
||||
# Link Effect object to Drug if not already linked
|
||||
if effect_created or effect_obj not in drug_obj.effects.all():
|
||||
drug_obj.effects.add(effect_obj)
|
||||
|
||||
print("SAVING DRUG", drug_obj)
|
||||
drug_obj.save()
|
||||
|
||||
async def get_drugs_list(self):
|
||||
"""
|
||||
Get all drug names from PsychWiki
|
||||
"""
|
||||
body = {"query": "{substances(limit: %i) {name}}" % self.search_limit}
|
||||
result = await self.call(
|
||||
"?",
|
||||
http_method="post",
|
||||
data=body,
|
||||
schema="GetDrugsList",
|
||||
append_slash=False,
|
||||
)
|
||||
print("RESULT", result)
|
||||
|
||||
return result["data"]
|
||||
|
||||
async def get_all_data(self):
|
||||
"""
|
||||
Get all the drug data from PsychWiki (warning - intensive)
|
||||
"""
|
||||
body = {
|
||||
"query": """
|
||||
{
|
||||
substances(limit: %i) {
|
||||
name
|
||||
url
|
||||
featured
|
||||
summary
|
||||
addictionPotential
|
||||
toxicity
|
||||
crossTolerances
|
||||
commonNames
|
||||
class {
|
||||
chemical
|
||||
psychoactive
|
||||
}
|
||||
tolerance {
|
||||
full
|
||||
half
|
||||
zero
|
||||
}
|
||||
roas {
|
||||
name
|
||||
dose {
|
||||
units
|
||||
threshold
|
||||
heavy
|
||||
common {
|
||||
min
|
||||
max
|
||||
}
|
||||
light {
|
||||
min
|
||||
max
|
||||
}
|
||||
strong {
|
||||
min
|
||||
max
|
||||
}
|
||||
}
|
||||
duration {
|
||||
afterglow {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
comeup {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
duration {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
offset {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
onset {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
peak {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
total {
|
||||
min
|
||||
max
|
||||
units
|
||||
}
|
||||
}
|
||||
bioavailability {
|
||||
min
|
||||
max
|
||||
}
|
||||
}
|
||||
effects {
|
||||
name
|
||||
url
|
||||
}
|
||||
images {
|
||||
thumb
|
||||
image
|
||||
}
|
||||
uncertainInteractions {
|
||||
name
|
||||
}
|
||||
unsafeInteractions {
|
||||
name
|
||||
}
|
||||
dangerousInteractions {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"""
|
||||
% self.search_limit
|
||||
}
|
||||
result = await self.call(
|
||||
"?",
|
||||
http_method="post",
|
||||
data=body,
|
||||
schema="GetDrugsList",
|
||||
append_slash=False,
|
||||
)
|
||||
|
||||
return result["data"]
|
||||
16
core/db/__init__.py
Normal file
16
core/db/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.conf import settings
|
||||
|
||||
from core.db import orm
|
||||
|
||||
|
||||
def remove_defaults(query_params):
|
||||
for field, value in list(query_params.items()):
|
||||
if field in settings.DRUGS_DEFAULT_PARAMS:
|
||||
if value == settings.DRUGS_DEFAULT_PARAMS[field]:
|
||||
del query_params[field]
|
||||
|
||||
|
||||
def add_defaults(query_params):
|
||||
for field, value in settings.DRUGS_DEFAULT_PARAMS.items():
|
||||
if field not in query_params:
|
||||
query_params[field] = value
|
||||
97
core/db/orm.py
Normal file
97
core/db/orm.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from django.conf import settings
|
||||
|
||||
from core import db
|
||||
from core.models import Drug
|
||||
|
||||
|
||||
class QueryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def parse_size(query_params, sizes):
|
||||
if "size" in query_params:
|
||||
size = query_params["size"]
|
||||
if size not in sizes:
|
||||
message = "Size is not permitted"
|
||||
message_class = "danger"
|
||||
return {"message": message, "class": message_class}
|
||||
size = int(size)
|
||||
else:
|
||||
size = 15
|
||||
|
||||
return size
|
||||
|
||||
|
||||
def parse_source(user, query_params):
|
||||
source = None
|
||||
if "source" in query_params:
|
||||
source = query_params["source"]
|
||||
|
||||
# Check validity of source
|
||||
if source not in settings.MAIN_SOURCES:
|
||||
message = f"Invalid source: {source}"
|
||||
message_class = "danger"
|
||||
return {"message": message, "class": message_class}
|
||||
|
||||
if source == "all":
|
||||
source = None # the next block will populate it
|
||||
|
||||
if source:
|
||||
sources = [source]
|
||||
else:
|
||||
# Here we need to populate what "all" means for the user.
|
||||
# They may only have access to a subset of the sources.
|
||||
# We build a custom source list with ones they have access
|
||||
# to, and then remove "all" from the list.
|
||||
sources = list(settings.MAIN_SOURCES)
|
||||
|
||||
# Get rid of "all", it's just a meta-source
|
||||
if "all" in sources:
|
||||
sources.remove("all")
|
||||
|
||||
return sources
|
||||
|
||||
|
||||
def run_query(query_params, tags, size, sources, ranges, sort):
|
||||
if "query" in query_params:
|
||||
query = query_params["query"]
|
||||
results = Drug.objects.filter(name__icontains=query)[:size]
|
||||
return results
|
||||
|
||||
|
||||
def drug_query(request, query_params, size=None, tags=None):
|
||||
db.add_defaults(query_params)
|
||||
|
||||
print("PARAMS11", query_params)
|
||||
print("SIZE", size)
|
||||
print("TAGS", tags)
|
||||
|
||||
# S - Size
|
||||
if request.user.is_anonymous:
|
||||
sizes = settings.MAIN_SIZES_ANON
|
||||
else:
|
||||
sizes = settings.MAIN_SIZES
|
||||
if not size:
|
||||
size = parse_size(query_params, sizes)
|
||||
if isinstance(size, dict):
|
||||
return size
|
||||
|
||||
# S - Source
|
||||
sources = parse_source(request.user, query_params)
|
||||
if isinstance(sources, dict):
|
||||
return sources
|
||||
|
||||
# R - Ranges
|
||||
ranges = None
|
||||
|
||||
# S - Sort
|
||||
sort = None
|
||||
|
||||
# Q/T - Query/Tags
|
||||
result = run_query(query_params, tags, size, sources, ranges, sort)
|
||||
for x in result:
|
||||
print(x.dosages)
|
||||
rtrn = {
|
||||
"object_list": result,
|
||||
}
|
||||
return rtrn
|
||||
@@ -5,7 +5,7 @@ from mixins.restrictions import RestrictedFormMixin
|
||||
|
||||
from mxs.restrictions import RestrictedFormMixinStaff
|
||||
|
||||
from .models import Drug, NotificationSettings, User
|
||||
from .models import Drug, Favourite, NotificationSettings, User
|
||||
|
||||
# Create your forms here.
|
||||
|
||||
@@ -78,3 +78,20 @@ class DrugForm(RestrictedFormMixinStaff, ModelForm):
|
||||
"actions": "Actions, what does it do on an objective level?",
|
||||
"experiences": "Experiences, what do people experience?",
|
||||
}
|
||||
|
||||
|
||||
class FavouriteForm(RestrictedFormMixin, ModelForm):
|
||||
class Meta:
|
||||
model = Favourite
|
||||
fields = (
|
||||
"nickname",
|
||||
"name",
|
||||
"drug_class",
|
||||
"common_name",
|
||||
)
|
||||
help_texts = {
|
||||
"nickname": "Call it whatever you like.",
|
||||
"name": "Lysergic acid diethylamide, Phenibut",
|
||||
"drug_class": "Psychedelic, Sedative, Stimulant",
|
||||
"common_name": "LSD",
|
||||
}
|
||||
|
||||
1
core/lib/schemas/__init__.py
Normal file
1
core/lib/schemas/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from core.lib.schemas import psychwiki_s # noqa
|
||||
10
core/lib/schemas/psychwiki_s.py
Normal file
10
core/lib/schemas/psychwiki_s.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from pydantic import BaseModel, Extra
|
||||
|
||||
|
||||
class MyModel(BaseModel):
|
||||
class Config:
|
||||
extra = Extra.forbid
|
||||
|
||||
|
||||
# class GetDrugsList(MyModel):
|
||||
# ...
|
||||
@@ -0,0 +1,41 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-07 14:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0005_dosage_roa_timing_roa_alter_experiencedose_roa'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='entry',
|
||||
old_name='slug',
|
||||
new_name='url',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sei',
|
||||
name='description',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sei',
|
||||
name='subtype',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='sei',
|
||||
name='type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sei',
|
||||
name='name',
|
||||
field=models.CharField(default='DEFAULT', max_length=255),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sei',
|
||||
name='url',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-07 14:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0006_rename_slug_entry_url_remove_sei_description_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='comeup_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='comeup_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='onset_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='onset_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='peak_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='peak_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='total_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='timing',
|
||||
name='total_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-07 14:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_alter_timing_comeup_lower_alter_timing_comeup_upper_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='common_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='common_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='heavy_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='heavy_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='light_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='light_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='strong_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='strong_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='threshold_lower',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='dosage',
|
||||
name='threshold_upper',
|
||||
field=models.FloatField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 4.2.9 on 2024-01-07 14:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0008_alter_dosage_common_lower_alter_dosage_common_upper_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='drug',
|
||||
name='common_name',
|
||||
field=models.CharField(blank=True, max_length=1024, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='drug',
|
||||
name='drug_class',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
44
core/migrations/0010_price_favouritedrug.py
Normal file
44
core/migrations/0010_price_favouritedrug.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.0.3 on 2024-05-17 19:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0009_alter_drug_common_name_alter_drug_drug_class'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Price',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount_mg', models.IntegerField()),
|
||||
('price_gbp', models.FloatField()),
|
||||
('note', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FavouriteDrug',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('nickname', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('drug_class', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('common_name', models.CharField(blank=True, max_length=1024, null=True)),
|
||||
('actions', models.ManyToManyField(blank=True, to='core.action')),
|
||||
('dosages', models.ManyToManyField(blank=True, to='core.dosage')),
|
||||
('effects', models.ManyToManyField(blank=True, to='core.effect')),
|
||||
('experiences', models.ManyToManyField(blank=True, to='core.experience')),
|
||||
('links', models.ManyToManyField(blank=True, to='core.entry')),
|
||||
('original', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.drug')),
|
||||
('timings', models.ManyToManyField(blank=True, to='core.timing')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('prices', models.ManyToManyField(blank=True, null=True, to='core.price')),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
core/migrations/0011_rename_favouritedrug_favourite.py
Normal file
17
core/migrations/0011_rename_favouritedrug_favourite.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.0.3 on 2024-05-17 19:02
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0010_price_favouritedrug'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='FavouriteDrug',
|
||||
new_name='Favourite',
|
||||
),
|
||||
]
|
||||
157
core/models.py
157
core/models.py
@@ -126,6 +126,9 @@ class Source(models.Model):
|
||||
# Score, affects ordering
|
||||
score = models.IntegerField(blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.type})"
|
||||
|
||||
|
||||
class Entry(models.Model):
|
||||
"""
|
||||
@@ -136,7 +139,7 @@ class Entry(models.Model):
|
||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||
|
||||
# Slug of the article on the Source
|
||||
slug = models.CharField(max_length=1024, null=True, blank=True)
|
||||
url = models.CharField(max_length=1024, null=True, blank=True)
|
||||
|
||||
# Authorship information, if present
|
||||
author = models.CharField(max_length=255, null=True, blank=True)
|
||||
@@ -144,6 +147,9 @@ class Entry(models.Model):
|
||||
# Extra information can be added
|
||||
description = models.CharField(max_length=1024, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.source.name} - {self.url}"
|
||||
|
||||
|
||||
class Dosage(models.Model):
|
||||
"""
|
||||
@@ -160,24 +166,31 @@ class Dosage(models.Model):
|
||||
unit = models.CharField(max_length=255, choices=DOSAGE_UNIT_CHOICES)
|
||||
|
||||
# I can no longer say I am sober, but it is slight
|
||||
threshold_lower = models.FloatField()
|
||||
threshold_upper = models.FloatField()
|
||||
threshold_lower = models.FloatField(null=True, blank=True)
|
||||
threshold_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# Light
|
||||
light_lower = models.FloatField()
|
||||
light_upper = models.FloatField()
|
||||
light_lower = models.FloatField(null=True, blank=True)
|
||||
light_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# Average dose for a user
|
||||
common_lower = models.FloatField()
|
||||
common_upper = models.FloatField()
|
||||
common_lower = models.FloatField(null=True, blank=True)
|
||||
common_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# Strong intensity, many sober activities may become impossible
|
||||
strong_lower = models.FloatField()
|
||||
strong_upper = models.FloatField()
|
||||
strong_lower = models.FloatField(null=True, blank=True)
|
||||
strong_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# Highest intensity
|
||||
heavy_lower = models.FloatField()
|
||||
heavy_upper = models.FloatField()
|
||||
heavy_lower = models.FloatField(null=True, blank=True)
|
||||
heavy_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
text = (
|
||||
f"{self.threshold_lower} {self.light_lower} {self.common_lower} "
|
||||
f"{self.strong_lower} {self.heavy_lower}"
|
||||
)
|
||||
return f"{self.roa} {text} ({self.unit})"
|
||||
|
||||
|
||||
class Timing(models.Model):
|
||||
@@ -197,24 +210,27 @@ class Timing(models.Model):
|
||||
)
|
||||
|
||||
# It has just now begin, I can no longer say I am sober
|
||||
onset_lower = models.FloatField()
|
||||
onset_upper = models.FloatField()
|
||||
onset_lower = models.FloatField(null=True, blank=True)
|
||||
onset_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# The intensity is accelerating
|
||||
comeup_lower = models.FloatField()
|
||||
comeup_upper = models.FloatField()
|
||||
comeup_lower = models.FloatField(null=True, blank=True)
|
||||
comeup_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# The maximum intensity has been reached
|
||||
# How long this state occurs
|
||||
peak_lower = models.FloatField()
|
||||
peak_upper = models.FloatField()
|
||||
peak_lower = models.FloatField(null=True, blank=True)
|
||||
peak_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
# How long it takes to get back to baseline
|
||||
offset_lower = models.FloatField(null=True, blank=True)
|
||||
offset_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
total_lower = models.FloatField()
|
||||
total_upper = models.FloatField()
|
||||
total_lower = models.FloatField(null=True, blank=True)
|
||||
total_upper = models.FloatField(null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.roa} {self.unit} {self.total_lower}-{self.total_upper}"
|
||||
|
||||
|
||||
class SEI(models.Model):
|
||||
@@ -225,19 +241,24 @@ class SEI(models.Model):
|
||||
"""
|
||||
|
||||
# PHYSICAL, COGNITIVE, etc
|
||||
type = models.CharField(
|
||||
max_length=255, choices=SEI_TYPE_CHOICES, default="PHYSICAL"
|
||||
)
|
||||
# type = models.CharField(
|
||||
# max_length=255, choices=SEI_TYPE_CHOICES, default="PHYSICAL"
|
||||
# )
|
||||
|
||||
subtype = models.CharField(
|
||||
max_length=255,
|
||||
choices=SEI_SUBTYPE_CHOICES,
|
||||
)
|
||||
# subtype = models.CharField(
|
||||
# max_length=255,
|
||||
# choices=SEI_SUBTYPE_CHOICES,
|
||||
# )
|
||||
|
||||
# WIP: consider euphoric, depressant, relaxant
|
||||
name = models.CharField(max_length=255)
|
||||
url = models.CharField(max_length=1024, blank=True, null=True)
|
||||
|
||||
# Specify how
|
||||
description = models.CharField(max_length=4096, blank=True, null=True)
|
||||
# description = models.CharField(max_length=4096, blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name}"
|
||||
|
||||
|
||||
class Effect(models.Model):
|
||||
@@ -252,6 +273,9 @@ class Effect(models.Model):
|
||||
# List of subjective effects, since they would likely be from the same entry
|
||||
subjective_effects = models.ManyToManyField(SEI)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.entry} {self.subjective_effects}"
|
||||
|
||||
|
||||
class Action(models.Model):
|
||||
"""
|
||||
@@ -285,6 +309,9 @@ class Action(models.Model):
|
||||
# Mechanism: Inhibition
|
||||
# Affinity: Reversible (1)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.site} {self.mechanism} {self.affinity}"
|
||||
|
||||
|
||||
class ExperienceDose(models.Model):
|
||||
# Seconds since the start of the experiment
|
||||
@@ -313,6 +340,9 @@ class ExperienceDose(models.Model):
|
||||
# TODO: Sentiment analysis (use AI/ML)
|
||||
# TODO: Time-based effects (use AI/ML)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.form} {self.dose} {self.unit} {self.roa}"
|
||||
|
||||
|
||||
class Experience(models.Model):
|
||||
"""
|
||||
@@ -339,6 +369,9 @@ class Experience(models.Model):
|
||||
# Description of the experience
|
||||
text = models.TextField()
|
||||
|
||||
def __str__(self):
|
||||
return f"Experience ({len(self.text)} chars)"
|
||||
|
||||
|
||||
class Drug(models.Model):
|
||||
"""
|
||||
@@ -349,10 +382,10 @@ class Drug(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
||||
# Psychedelic, Sedative, Stimulant
|
||||
drug_class = models.CharField(max_length=255)
|
||||
drug_class = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# LSD
|
||||
common_name = models.CharField(max_length=1024, unique=True)
|
||||
common_name = models.CharField(max_length=1024, blank=True, null=True)
|
||||
|
||||
# Factsheets, posts
|
||||
links = models.ManyToManyField(Entry, blank=True)
|
||||
@@ -372,6 +405,72 @@ class Drug(models.Model):
|
||||
# Experiences, what do people experience?
|
||||
experiences = models.ManyToManyField(Experience, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.common_name})"
|
||||
|
||||
|
||||
class Price(models.Model):
|
||||
"""
|
||||
Price of a drug.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
amount_mg = models.IntegerField()
|
||||
price_gbp = models.FloatField()
|
||||
note = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
|
||||
# class Stack: references, times
|
||||
# class StackUnit: reference, times, dose_mg
|
||||
|
||||
|
||||
class Favourite(models.Model):
|
||||
"""
|
||||
Model of a drug. Owned by a user and customisable.
|
||||
"""
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
# Nickname
|
||||
nickname = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# Prices, how much certain mass of this substance costs
|
||||
prices = models.ManyToManyField(Price, blank=True, null=True)
|
||||
|
||||
# Internals
|
||||
original = models.ForeignKey(Drug, on_delete=models.SET_NULL, blank=True, null=True)
|
||||
|
||||
# Below duplicates Drug
|
||||
# Lysergic acid diethylamide, Phenibut
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
# Psychedelic, Sedative, Stimulant
|
||||
drug_class = models.CharField(max_length=255, blank=True, null=True)
|
||||
|
||||
# LSD
|
||||
common_name = models.CharField(max_length=1024, blank=True, null=True)
|
||||
|
||||
# Factsheets, posts
|
||||
links = models.ManyToManyField(Entry, blank=True)
|
||||
|
||||
# Dosages, how much to take to get a certain effect
|
||||
dosages = models.ManyToManyField(Dosage, blank=True)
|
||||
|
||||
# Timings, how long to wait to reach maximum intensity (and others)
|
||||
timings = models.ManyToManyField(Timing, blank=True)
|
||||
|
||||
# Effects, what does it do on a subjective level?
|
||||
effects = models.ManyToManyField(Effect, blank=True)
|
||||
|
||||
# Actions, what does it do on an objective level?
|
||||
actions = models.ManyToManyField(Action, blank=True)
|
||||
|
||||
# Experiences, what do people experience?
|
||||
experiences = models.ManyToManyField(Experience, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} [{self.nickname}] ({self.common_name})"
|
||||
|
||||
|
||||
# class Perms(models.Model):
|
||||
# class Meta:
|
||||
|
||||
1
core/static/css/bulma-calendar.min.css
vendored
Normal file
1
core/static/css/bulma-calendar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
core/static/css/bulma-slider.min.css
vendored
Normal file
1
core/static/css/bulma-slider.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
core/static/css/bulma-switch.min.css
vendored
Normal file
1
core/static/css/bulma-switch.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
core/static/css/bulma-tagsinput.min.css
vendored
Normal file
2
core/static/css/bulma-tagsinput.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
core/static/css/bulma.min.css
vendored
4
core/static/css/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
1
core/static/js/bulma-calendar.min.js
vendored
Normal file
1
core/static/js/bulma-calendar.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
core/static/js/bulma-slider.min.js
vendored
Normal file
1
core/static/js/bulma-slider.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
core/static/js/bulma-tagsinput.min.js
vendored
Normal file
2
core/static/js/bulma-tagsinput.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
13
core/static/js/chart.js
Normal file
13
core/static/js/chart.js
Normal file
File diff suppressed because one or more lines are too long
259
core/static/js/column-shifter.js
Normal file
259
core/static/js/column-shifter.js
Normal file
@@ -0,0 +1,259 @@
|
||||
// Author: Grzegorz Tężycki
|
||||
|
||||
$(document).ready(function(){
|
||||
|
||||
// In web storage is saved structure like that:
|
||||
// localStorage['django_tables2_column_shifter'] = {
|
||||
// 'table_class_container1' : {
|
||||
// 'id' : 'on',
|
||||
// 'col1' : 'off',
|
||||
// 'col2' : 'on',
|
||||
// 'col3' : 'on',
|
||||
// },
|
||||
// 'table_class_container2' : {
|
||||
// 'id' : 'on',
|
||||
// 'col1' : 'on'
|
||||
// },
|
||||
// }
|
||||
|
||||
// main name for key in web storage
|
||||
var COLUMN_SHIFTER_STORAGE_ACCESOR = "django_tables2_column_shifter";
|
||||
|
||||
// Return storage structure for shifter
|
||||
// If structure does'n exist in web storage
|
||||
// will be return empty object
|
||||
var get_column_shifter_storage = function(){
|
||||
var storage = localStorage.getItem(COLUMN_SHIFTER_STORAGE_ACCESOR);
|
||||
if (storage === null) {
|
||||
storage = {
|
||||
"drilldown-table": {
|
||||
"date": "off",
|
||||
"time": "off",
|
||||
"id": "off",
|
||||
"host": "off",
|
||||
"ident": "off",
|
||||
"channel": "off",
|
||||
"net": "off",
|
||||
"num": "off",
|
||||
"channel_nsfw": "off",
|
||||
"channel_category": "off",
|
||||
"channel_category_id": "off",
|
||||
"channel_category_nsfw": "off",
|
||||
"channel_id": "off",
|
||||
"guild_member_count": "off",
|
||||
"bot": "off",
|
||||
"msg_id": "off",
|
||||
"user": "off",
|
||||
"net_id": "off",
|
||||
"user_id": "off",
|
||||
"nick_id": "off",
|
||||
"status": "off",
|
||||
"num_users": "off",
|
||||
"num_chans": "off",
|
||||
"exemption": "off",
|
||||
// "version_sentiment": "off",
|
||||
"sentiment": "off",
|
||||
"num": "off",
|
||||
"online": "off",
|
||||
"mtype": "off",
|
||||
"realname": "off",
|
||||
"server": "off",
|
||||
"mtype": "off",
|
||||
"hidden": "off",
|
||||
"filename": "off",
|
||||
"file_md5": "off",
|
||||
"file_ext": "off",
|
||||
"file_size": "off",
|
||||
"lang_code": "off",
|
||||
"tokens": "off",
|
||||
"rule_id": "off",
|
||||
"index": "off",
|
||||
"meta": "off",
|
||||
"match_ts": "off",
|
||||
"batch_id": "off"
|
||||
//"lang_name": "off",
|
||||
// "words_noun": "off",
|
||||
// "words_adj": "off",
|
||||
// "words_verb": "off",
|
||||
// "words_adv": "off"
|
||||
},
|
||||
};
|
||||
} else {
|
||||
storage = JSON.parse(storage);
|
||||
}
|
||||
return storage;
|
||||
};
|
||||
|
||||
// Save structure in web storage
|
||||
var set_column_shifter_storage = function(storage){
|
||||
var json_storage = JSON.stringify(storage)
|
||||
localStorage.setItem(COLUMN_SHIFTER_STORAGE_ACCESOR, json_storage);
|
||||
};
|
||||
|
||||
// Remember state for single button
|
||||
var save_btn_state = function($btn){
|
||||
|
||||
// Take css class for container with table
|
||||
var table_class_container = $btn.data("table-class-container");
|
||||
// Take html object with table
|
||||
var $table_class_container = $("#" + table_class_container);
|
||||
// Take single button statne ("on" / "off")
|
||||
var state = $btn.data("state");
|
||||
// td-class is a real column name in table
|
||||
var td_class = $btn.data("td-class");
|
||||
var storage = get_column_shifter_storage();
|
||||
// Table id
|
||||
var id = $table_class_container.attr("id");
|
||||
|
||||
// Checking if the ID is already in storage
|
||||
if (id in storage) {
|
||||
data = storage[id]
|
||||
} else {
|
||||
data = {}
|
||||
storage[id] = data;
|
||||
}
|
||||
|
||||
// Save state for table column in storage
|
||||
data[td_class] = state;
|
||||
set_column_shifter_storage(storage);
|
||||
};
|
||||
|
||||
// Load states for buttons from storage for single tabel
|
||||
var load_states = function($table_class_container) {
|
||||
var storage = get_column_shifter_storage();
|
||||
// Table id
|
||||
var id = $table_class_container.attr("id");
|
||||
var data = {};
|
||||
|
||||
// Checking if the ID is already in storage
|
||||
if (id in storage) {
|
||||
data = storage[id]
|
||||
|
||||
// For each shifter button set state
|
||||
$table_class_container.find(".btn-shift-column").each(function(){
|
||||
var $btn = $(this);
|
||||
var td_class = $btn.data("td-class");
|
||||
|
||||
// If name of column is in store then get state
|
||||
// and set state
|
||||
if (td_class in data) {
|
||||
var state = data[td_class]
|
||||
set_btn_state($btn, state);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Show table content and hide spiner
|
||||
var show_table_content = function($table_class_container){
|
||||
$table_class_container.find("#loader").hide();
|
||||
$table_class_container.find("#table-container").show();
|
||||
};
|
||||
|
||||
// Load buttons states for all button in page
|
||||
var load_state_for_all_containters = function(){
|
||||
$(".column-shifter-container").each(function(){
|
||||
$table_class_container = $(this);
|
||||
|
||||
// Load states for all buttons in single container
|
||||
load_states($table_class_container);
|
||||
|
||||
// When states was loaded then table must be show and
|
||||
// loader (spiner) must be hide
|
||||
show_table_content($table_class_container);
|
||||
});
|
||||
};
|
||||
|
||||
// change visibility column for single button
|
||||
// if button has state "on" then show column
|
||||
// else then column will be hide
|
||||
shift_column = function( $btn ){
|
||||
// button state
|
||||
var state = $btn.data("state");
|
||||
|
||||
// td-class is a real column name in table
|
||||
var td_class = $btn.data("td-class");
|
||||
var table_class_container = $btn.data("table-class-container");
|
||||
var $table_class_container = $("#" + table_class_container);
|
||||
var $table = $table_class_container.find("table");
|
||||
var $cels = $table.find("." + td_class);
|
||||
|
||||
if ( state === "on" ) {
|
||||
$cels.show();
|
||||
} else {
|
||||
$cels.hide();
|
||||
}
|
||||
};
|
||||
|
||||
// Shift visibility for all columns
|
||||
shift_columns = function(){
|
||||
var cols = $(".btn-shift-column");
|
||||
var i, len = cols.length;
|
||||
for (i=0; i < len; i++) {
|
||||
shift_column($(cols[i]));
|
||||
}
|
||||
};
|
||||
|
||||
// Set icon imgae visibility for button state
|
||||
var set_icon_for_state = function( $btn, state ) {
|
||||
if (state === "on") {
|
||||
$btn.find("span.uncheck").hide();
|
||||
$btn.find("span.check").show();
|
||||
} else {
|
||||
$btn.find("span.check").hide();
|
||||
$btn.find("span.uncheck").show();
|
||||
}
|
||||
};
|
||||
|
||||
// Set state for single button
|
||||
var set_btn_state = function($btn, state){
|
||||
$btn.data('state', state);
|
||||
set_icon_for_state($btn, state);
|
||||
}
|
||||
|
||||
// Change state for single button
|
||||
var change_btn_state = function($btn){
|
||||
var state = $btn.data("state");
|
||||
|
||||
if (state === "on") {
|
||||
state = "off"
|
||||
} else {
|
||||
state = "on"
|
||||
}
|
||||
set_btn_state($btn, state);
|
||||
};
|
||||
|
||||
// Run show/hide when click on button
|
||||
$(".btn-shift-column").on("click", function(event){
|
||||
var $btn = $(this);
|
||||
event.stopPropagation();
|
||||
change_btn_state($btn);
|
||||
shift_column($btn);
|
||||
save_btn_state($btn);
|
||||
});
|
||||
|
||||
// Load saved states for all tables
|
||||
load_state_for_all_containters();
|
||||
|
||||
// show or hide columns based on data from web storage
|
||||
shift_columns();
|
||||
|
||||
// Add API method for retrieving non-visible cols for table
|
||||
// Pass the 0-based index of the table or leave the parameter
|
||||
// empty to return the hidden cols for the 1st table found
|
||||
$.django_tables2_column_shifter_hidden = function(idx) {
|
||||
if(idx==undefined) {
|
||||
idx = 0;
|
||||
}
|
||||
return $('#table-container').eq(idx).find('.btn-shift-column').filter(function(z) {
|
||||
return $(this).data('state')=='off'
|
||||
}).map(function(z) {
|
||||
return $(this).data('td-class')
|
||||
}).toArray();
|
||||
}
|
||||
const event = new Event('restore-scroll');
|
||||
document.dispatchEvent(event);
|
||||
const event2 = new Event('load-widget-results');
|
||||
document.dispatchEvent(event2);
|
||||
|
||||
});
|
||||
1
core/static/js/htmx.min copy.js
Normal file
1
core/static/js/htmx.min copy.js
Normal file
File diff suppressed because one or more lines are too long
2
core/static/js/jquery.min.js
vendored
Normal file
2
core/static/js/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -6,16 +6,24 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>XF - {{ request.path_info }}</title>
|
||||
<title>DIMAR - {{ request.path_info }}</title>
|
||||
<link rel="shortcut icon" href="{% static 'favicon.ico' %}">
|
||||
<link rel="manifest" href="{% static 'manifest.webmanifest' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tooltip.min.css' %}">
|
||||
<link rel="stylesheet" href="https://site-assets.fontawesome.com/releases/v6.1.1/css/all.css">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-slider.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-calendar.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-tagsinput.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bulma-switch.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/gridstack.min.css' %}">
|
||||
<script src="{% static 'js/bulma-calendar.min.js' %}" integrity="sha384-DThNif0xGXbopX7+PE+UabkuClfI/zELNhaVqoGLutaWB76dyMw0vIQBGmUxSfVQ" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/bulma-slider.min.js' %}" integrity="sha384-wbyps8iLG8QzJE02viYc/27BtT5HSa11+b5V7QPR1/huVuA8f4LRTNGc82qAIeIZ" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/htmx.min.js' %}" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/remove-me.js' %}" integrity="sha384-6fHcFNoQ8QEI3ZDgw9Z/A6Brk64gF7AnFbLgdrumo8/kBbsKQ/wo7wPegj5WkzuG" crossorigin="anonymous"></script>
|
||||
<script defer src="{% static 'js/hyperscript.min.js' %}" integrity="sha384-6GYN8BDHOJkkru6zcpGOUa//1mn+5iZ/MyT6mq34WFIpuOeLF52kSi721q0SsYF9" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'js/bulma-tagsinput.min.js' %}"></script>
|
||||
<script src="{% static 'js/jquery.min.js' %}"></script>
|
||||
<script src="{% static 'js/gridstack-all.js' %}"></script>
|
||||
<script defer src="{% static 'js/magnet.min.js' %}"></script>
|
||||
<script>
|
||||
@@ -112,12 +120,37 @@
|
||||
cursor:pointer;
|
||||
background-color:rgba(221, 224, 255, 0.3) !important;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: rgba(84, 84, 84, 0.9) !important;
|
||||
--modal-color: rgba(81, 81, 81, 0.9) !important;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background-color-light: rgba(210, 210, 210, 0.9) !important;
|
||||
--background-color-dark: rgba(84, 84, 84, 0.9) !important;
|
||||
--background-color-modal-light: rgba(250, 250, 250, 0.5) !important;
|
||||
--background-color-modal-dark: rgba(210, 210, 210, 0.9) !important;
|
||||
}
|
||||
|
||||
[data-theme="light"] {
|
||||
--background-color: var(--background-color-light);
|
||||
--modal-color: var(--background-color-modal-light);
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--background-color: var(--background-color-dark);
|
||||
--modal-color: var(--background-color-modal-dark);
|
||||
}
|
||||
|
||||
.panel, .box, .modal {
|
||||
background-color:rgba(250, 250, 250, 0.5) !important;
|
||||
/* background-color:rgba(250, 250, 250, 0.5) !important; */
|
||||
background-color: var(--modal-color) !important;
|
||||
}
|
||||
.modal, .modal.box{
|
||||
background-color:rgba(210, 210, 210, 0.9) !important;
|
||||
/* background-color:rgba(210, 210, 210, 0.9) !important; */
|
||||
background-color: var(--background-color) !important;
|
||||
}
|
||||
.modal-background{
|
||||
background-color:rgba(255, 255, 255, 0.3) !important;
|
||||
@@ -201,6 +234,12 @@
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
Home
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'home' %}">
|
||||
Search
|
||||
</a>
|
||||
<a class="navbar-item" href="{% url 'favourites' type='page' %}">
|
||||
Favourites
|
||||
</a>
|
||||
{% if user.is_superuser %}
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
@@ -217,7 +256,9 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.is_authenticated %}
|
||||
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link">
|
||||
Account
|
||||
|
||||
@@ -1,7 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load joinsep %}
|
||||
{% block outer_content %}
|
||||
{% if params.modal == 'context' %}
|
||||
<div
|
||||
style="display: none;"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-post="{% url 'modal_context' %}"
|
||||
hx-vals='{"net": "{{ params.net|escapejs }}",
|
||||
"num": "{{ params.num|escapejs }}",
|
||||
"source": "{{ params.source|escapejs }}",
|
||||
"channel": "{{ params.channel|escapejs }}",
|
||||
"time": "{{ params.time|escapejs }}",
|
||||
"date": "{{ params.date|escapejs }}",
|
||||
"index": "{{ params.index }}",
|
||||
"type": "{{ params.type|escapejs }}",
|
||||
"mtype": "{{ params.mtype|escapejs }}",
|
||||
"nick": "{{ params.nick|escapejs }}"}'
|
||||
hx-target="#modals-here"
|
||||
hx-trigger="load">
|
||||
</div>
|
||||
{% endif %}
|
||||
<script src="{% static 'js/chart.js' %}"></script>
|
||||
<script src="{% static 'tabs.js' %}"></script>
|
||||
<script>
|
||||
function setupTags() {
|
||||
var inputTags = document.getElementById('tags');
|
||||
new BulmaTagsInput(inputTags);
|
||||
|
||||
inputTags.BulmaTagsInput().on('before.add', function(item) {
|
||||
if (item.includes(": ")) {
|
||||
var spl = item.split(": ");
|
||||
} else {
|
||||
var spl = item.split(":");
|
||||
}
|
||||
var field = spl[0];
|
||||
try {
|
||||
var value = JSON.parse(spl[1]);
|
||||
} catch {
|
||||
var value = spl[1];
|
||||
}
|
||||
return `${field}: ${value}`;
|
||||
});
|
||||
inputTags.BulmaTagsInput().on('after.remove', function(item) {
|
||||
var spl = item.split(": ");
|
||||
var field = spl[0];
|
||||
var value = spl[1].trim();
|
||||
});
|
||||
}
|
||||
function populateSearch(field, value) {
|
||||
var inputTags = document.getElementById('tags');
|
||||
inputTags.BulmaTagsInput().add(field+": "+value);
|
||||
//htmx.trigger("#search", "click");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid-stack" id="grid-stack-main">
|
||||
<div class="grid-stack-item" gs-w="7" gs-h="10" gs-y="0" gs-x="1">
|
||||
@@ -9,10 +61,10 @@
|
||||
<nav class="panel">
|
||||
<p class="panel-heading" style="padding: .2em; line-height: .5em;">
|
||||
<i class="fa-solid fa-arrows-up-down-left-right has-text-grey-light"></i>
|
||||
Home
|
||||
Search
|
||||
</p>
|
||||
<article class="panel-block is-active">
|
||||
{% include 'window-content/main.html' %}
|
||||
{% include 'window-content/search.html' %}
|
||||
</article>
|
||||
</nav>
|
||||
</div>
|
||||
@@ -31,6 +83,7 @@
|
||||
animate: true,
|
||||
});
|
||||
// GridStack.init();
|
||||
setupTags();
|
||||
|
||||
// a widget is ready to be loaded
|
||||
document.addEventListener('load-widget', function(event) {
|
||||
@@ -53,43 +106,47 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clear the queue element
|
||||
container.outerHTML = "";
|
||||
grid.addWidget(widgetelement);
|
||||
|
||||
// temporary workaround, other widgets can be duplicated, but not results
|
||||
if (widgetelement.id == 'widget-results') {
|
||||
grid.removeWidget("widget-results");
|
||||
}
|
||||
|
||||
grid.addWidget(widgetelement);
|
||||
// re-create the HTMX JS listeners, otherwise HTMX won't work inside the grid
|
||||
htmx.process(widgetelement);
|
||||
|
||||
// update the size of the widget according to its content
|
||||
var added_widget = htmx.find(grid_element, "#"+new_id);
|
||||
var itemContent = htmx.find(added_widget, ".control");
|
||||
var scrollheight = itemContent.scrollHeight+80;
|
||||
var verticalmargin = 0;
|
||||
var cellheight = grid.opts.cellHeight;
|
||||
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
|
||||
var opts = {
|
||||
h: height,
|
||||
}
|
||||
grid.update(
|
||||
added_widget,
|
||||
opts
|
||||
);
|
||||
// update size when the widget is loaded
|
||||
document.addEventListener('load-widget-results', function(evt) {
|
||||
var added_widget = htmx.find(grid_element, '#widget-results');
|
||||
var itemContent = htmx.find(added_widget, ".control");
|
||||
var scrollheight = itemContent.scrollHeight+80;
|
||||
var verticalmargin = 0;
|
||||
var cellheight = grid.opts.cellHeight;
|
||||
var height = Math.ceil((scrollheight + verticalmargin) / (cellheight + verticalmargin));
|
||||
var opts = {
|
||||
h: height,
|
||||
}
|
||||
grid.update(
|
||||
added_widget,
|
||||
opts
|
||||
);
|
||||
});
|
||||
|
||||
// run the JS scripts inside the added element again
|
||||
// for instance, this will fix the dropdown
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
eval(scripts[i].innerHTML);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="modals-here">
|
||||
</div>
|
||||
<div id="items-here">
|
||||
</div>
|
||||
<div id="widgets-here" style="display: none;">
|
||||
</div>
|
||||
<script>
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% block widgets %}
|
||||
{% if table or message is not None %}
|
||||
{% include 'partials/results_load.html' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
246
core/templates/partials/drug-detail.html
Normal file
246
core/templates/partials/drug-detail.html
Normal file
@@ -0,0 +1,246 @@
|
||||
{% load pretty %}
|
||||
|
||||
{% load cache %}
|
||||
|
||||
{% cache 600 favourite_detail request.user.id object %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
|
||||
{% if object is not None %}
|
||||
<h1 class="title">{{ object }}</h1>
|
||||
<p class="subtitle"><strong>{{ object.drug_class }}</strong></p>
|
||||
<div class="block">
|
||||
<a class="button is-info" href="#">Prices</a>
|
||||
<a class="button is-info" href="#">More info</a>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="cell">
|
||||
<div class="box">
|
||||
<h2 class="subtitle">Dosage</h2>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="#">
|
||||
<tbody>
|
||||
{% for item in object.dosages.all %}
|
||||
|
||||
<tr>
|
||||
<th>roa</th>
|
||||
<td>
|
||||
{{ item.roa }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>unit</th>
|
||||
<td>
|
||||
{{ item.unit }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>threshold</th>
|
||||
<td>
|
||||
{{ item.threshold_lower }} - {{ item.threshold_upper }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>light</th>
|
||||
<td>
|
||||
{{ item.light_lower }} - {{ item.light_upper }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>common</th>
|
||||
<td>
|
||||
{{ item.common_lower }} - {{ item.common_upper }}
|
||||
</td>
|
||||
<tr>
|
||||
|
||||
<tr>
|
||||
<th>strong</th>
|
||||
<td>
|
||||
{{ item.strong_lower }} - {{ item.strong_upper }}
|
||||
</td>
|
||||
<tr>
|
||||
|
||||
<tr>
|
||||
<th>heavy</th>
|
||||
<td>
|
||||
{{ item.heavy_lower }} - {{ item.heavy_upper }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="subtitle">Timing</h2>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="#">
|
||||
<tbody>
|
||||
{% for item in object.timings.all %}
|
||||
|
||||
<tr>
|
||||
<th>roa</th>
|
||||
<td>
|
||||
{{ item.roa }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>unit</th>
|
||||
<td>
|
||||
{{ item.unit }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>onset</th>
|
||||
<td>
|
||||
{{ item.onset_lower }} - {{ item.onset_upper }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>comeup</th>
|
||||
<td>
|
||||
{{ item.comeup_lower }} - {{ item.comeup_upper }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>peak</th>
|
||||
<td>
|
||||
{{ item.peak_lower }} - {{ item.peak_upper }}
|
||||
</td>
|
||||
<tr>
|
||||
|
||||
<tr>
|
||||
<th>offset</th>
|
||||
<td>
|
||||
{{ item.offset_lower }} - {{ item.offset_upper }}
|
||||
</td>
|
||||
<tr>
|
||||
|
||||
<tr>
|
||||
<th>total</th>
|
||||
<td>
|
||||
{{ item.total_lower }} - {{ item.total_upper }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="subtitle">Links</h2>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="#">
|
||||
<tbody>
|
||||
{% for item in object.links.all %}
|
||||
|
||||
<tr>
|
||||
<th>source</th>
|
||||
<td>
|
||||
{{ item.source }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>url</th>
|
||||
<td>
|
||||
<a href="{{ item.url }}">{{ item.url }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>author</th>
|
||||
<td>
|
||||
{{ item.author }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="box">
|
||||
<h2 class="subtitle">Actions</h2>
|
||||
<ul>
|
||||
{% for action in object.actions.all %}
|
||||
<li>{{ action }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h2 class="subtitle">Effects</h2>
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="#">
|
||||
<tbody>
|
||||
{% for item in object.effects.all %}
|
||||
|
||||
<tr>
|
||||
<th>entry</th>
|
||||
<td>
|
||||
<a href="{{ item.entry.url }}">{{ item.entry.url }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<th>SEI</th>
|
||||
<td>
|
||||
<div class="grid">
|
||||
{% for effect in item.subjective_effects.all %}
|
||||
<div class="cell box">
|
||||
{{ effect }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<h2 class="subtitle">Experiences</h2>
|
||||
<ul>
|
||||
{% for exp in object.experiences.all %}
|
||||
<li>{{ exp }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
@@ -28,7 +28,7 @@
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.drug_class }}</td>
|
||||
<td>{{ item.common_name }}s</td>
|
||||
<td>{{ item.common_name }}</td>
|
||||
<td>{{ item.links.count }}</td>
|
||||
<td>{{ item.dosages.count }}</td>
|
||||
<td>{{ item.timings.count }}</td>
|
||||
@@ -64,6 +64,15 @@
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<a href="{% url 'drug_detail' type='page' pk=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
86
core/templates/partials/favourite-list.html
Normal file
86
core/templates/partials/favourite-list.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% load cache %}
|
||||
{% load cachalot cache %}
|
||||
{% get_last_invalidation 'core.Favourite' as last %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% cache 600 objects_favourittes request.user.id object_list last %}
|
||||
<table
|
||||
class="table is-fullwidth is-hoverable"
|
||||
hx-target="#{{ context_object_name }}-table"
|
||||
id="{{ context_object_name }}-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||
hx-get="{{ list_url }}">
|
||||
<thead>
|
||||
<th>id</th>
|
||||
<th>nickname</th>
|
||||
<th>prices</th>
|
||||
<th>name</th>
|
||||
<th>drug class</th>
|
||||
<th>common name</th>
|
||||
<th>links</th>
|
||||
<th>dosages</th>
|
||||
<th>timings</th>
|
||||
<th>effects</th>
|
||||
<th>actions</th>
|
||||
<th>experiences</th>
|
||||
<th>actions</th>
|
||||
</thead>
|
||||
{% for item in object_list %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.nickname }}</td>
|
||||
<td>{{ item.prices.count }}</td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.drug_class }}</td>
|
||||
<td>{{ item.common_name }}</td>
|
||||
<td>{{ item.links.count }}</td>
|
||||
<td>{{ item.dosages.count }}</td>
|
||||
<td>{{ item.timings.count }}</td>
|
||||
<td>{{ item.effects.count }}</td>
|
||||
<td>{{ item.actions.count }}</td>
|
||||
<td>{{ item.experiences.count }}</td>
|
||||
<td>
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'favourite_update' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#{{ type }}s-here"
|
||||
hx-swap="innerHTML"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-delete="{% url 'favourite_delete' type=type pk=item.id %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<a href="{% url 'favourite_detail' type='page' pk=item.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</table>
|
||||
{% endcache %}
|
||||
21
core/templates/partials/results_load.html
Normal file
21
core/templates/partials/results_load.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends 'mixins/wm/widget.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block heading %}
|
||||
Results
|
||||
{% endblock %}
|
||||
|
||||
{% block panel_content %}
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% if cache is not None %}
|
||||
<span class="icon has-tooltip-bottom" data-tooltip="Cached">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
fetched {{ table.data|length }} hits
|
||||
|
||||
|
||||
{% include 'partials/results_table.html' %}
|
||||
{# include 'partials/sentiment_chart.html' #}
|
||||
{% endblock %}
|
||||
293
core/templates/partials/results_table.html
Normal file
293
core/templates/partials/results_table.html
Normal file
@@ -0,0 +1,293 @@
|
||||
{% load django_tables2 %}
|
||||
{% load django_tables2_bulma_template %}
|
||||
{% load static %}
|
||||
{% load joinsep %}
|
||||
{% load urlsafe %}
|
||||
{% load cache %}
|
||||
|
||||
{# cache 3600 results_table_full request.user.id table #}
|
||||
{% block table-wrapper %}
|
||||
<script src="{% static 'js/column-shifter.js' %}"></script>
|
||||
<div id="drilldown-table" class="column-shifter-container" style="position:relative; z-index:1;">
|
||||
{% block table %}
|
||||
<div class="nowrap-parent">
|
||||
<div class="nowrap-child">
|
||||
<div class="dropdown" id="dropdown">
|
||||
<div class="dropdown-trigger">
|
||||
<button id="dropdown-trigger" class="button dropdown-toggle" aria-haspopup="true" aria-controls="dropdown-menu">
|
||||
<span>Show/hide fields</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-angle-down" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="dropdown-menu" role="menu">
|
||||
<div class="dropdown-content" style="position:absolute; z-index:2;">
|
||||
{% for column in table.columns %}
|
||||
{% if column.name in show %}
|
||||
<a class="btn-shift-column dropdown-item"
|
||||
data-td-class="{{ column.name }}"
|
||||
data-state="on"
|
||||
{% if not forloop.last %} style="border-bottom:1px solid #ccc;" {%endif %}
|
||||
data-table-class-container="drilldown-table">
|
||||
<span class="check icon" data-tooltip="Visible" style="display:none;">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
<span class="uncheck icon" data-tooltip="Hidden" style="display:none;">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{{ column.header }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nowrap-child">
|
||||
<span id="loader" class="button is-light has-text-link is-loading">Static</span>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
var dropdown_button = document.getElementById("dropdown-trigger");
|
||||
var dropdown = document.getElementById("dropdown");
|
||||
dropdown_button.addEventListener('click', function(e) {
|
||||
// elements[i].preventDefault();
|
||||
dropdown.classList.toggle('is-active');
|
||||
});
|
||||
|
||||
</script>
|
||||
<div id="table-container" style="display:none;">
|
||||
<table {% render_attrs table.attrs class="table drilldown-results-table is-fullwidth" %}>
|
||||
{% block table.thead %}
|
||||
{% if table.show_header %}
|
||||
<thead {% render_attrs table.attrs.thead class="" %}>
|
||||
{% block table.thead.row %}
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
{% if column.name in show %}
|
||||
{% block table.thead.th %}
|
||||
<th class="orderable {{ column.name }}">
|
||||
<div class="nowrap-parent">
|
||||
{% if column.orderable %}
|
||||
<div class="nowrap-child">
|
||||
{% if column.is_ordered %}
|
||||
{% is_descending column.order_by as descending %}
|
||||
{% if descending %}
|
||||
<span class="icon" aria-hidden="true">{% block table.desc_icon %}<i class="fa-solid fa-sort-down"></i>{% endblock table.desc_icon %}</span>
|
||||
{% else %}
|
||||
<span class="icon" aria-hidden="true">{% block table.asc_icon %}<i class="fa-solid fa-sort-up"></i>{% endblock table.asc_icon %}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="icon" aria-hidden="true">{% block table.orderable_icon %}<i class="fa-solid fa-sort"></i>{% endblock table.orderable_icon %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="nowrap-child">
|
||||
<a
|
||||
hx-get="search/partial/{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}&{{ uri }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-trigger="click"
|
||||
hx-target="#drilldown-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#spinner"
|
||||
style="cursor: pointer;">
|
||||
{{ column.header }}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="nowrap-child">
|
||||
{{ column.header }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
{% endblock table.thead.th %}
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endblock table.thead.row %}
|
||||
</thead>
|
||||
{% endif %}
|
||||
{% endblock table.thead %}
|
||||
{% block table.tbody %}
|
||||
<tbody {{ table.attrs.tbody.as_html }}>
|
||||
{% for row in table.paginated_rows %}
|
||||
{% block table.tbody.row %}
|
||||
{% if row.cells.type == 'control' %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<span class="icon has-text-grey" data-tooltip="Hidden">
|
||||
<i class="fa-solid fa-file-slash"></i>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<p class="has-text-grey">Hidden {{ row.cells.hidden }} similar result{% if row.cells.hidden > 1%}s{% endif %}</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
{% for column, cell in row.items %}
|
||||
{% if column.name in show %}
|
||||
{% block table.tbody.td %}
|
||||
{% if cell == '—' %}
|
||||
<td class="{{ column.name }}">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-file-slash"></i>
|
||||
</span>
|
||||
</td>
|
||||
{% elif cell is True or cell is False %}
|
||||
<td class="{{ column.name }}">
|
||||
{% if cell is True %}
|
||||
<span class="icon has-text-success">
|
||||
<i class="fa-solid fa-check"></i>
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% elif column.name == "dosages" %}
|
||||
<td class="{{ column.name }}">
|
||||
{{ cell.entry }}
|
||||
</td>
|
||||
{% elif column.name == "name" %}
|
||||
<td class="{{ column.name }}">
|
||||
<a href="{% url 'drug_detail' type='page' pk=row.cells.id %}"><button
|
||||
class="button">
|
||||
<span class="icon-text">
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
{{ cell }}
|
||||
</td>
|
||||
|
||||
{% else %}
|
||||
<td class="{{ column.name }}">
|
||||
<a
|
||||
class="has-text-grey"
|
||||
onclick="populateSearch('{{ column.name }}', '{{ cell|escapejs }}')">
|
||||
{{ cell }}
|
||||
</a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endblock table.tbody.td %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endblock table.tbody.row %}
|
||||
{% empty %}
|
||||
{% if table.empty_text %}
|
||||
{% block table.tbody.empty_text %}
|
||||
<tr><td class="{{ column.name }}" colspan="{{ table.columns|length }}">{{ table.empty_text }}</td></tr>
|
||||
{% endblock table.tbody.empty_text %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endblock table.tbody %}
|
||||
{% block table.tfoot %}
|
||||
{% if table.has_footer %}
|
||||
<tfoot {{ table.attrs.tfoot.as_html }}>
|
||||
{% block table.tfoot.row %}
|
||||
<tr>
|
||||
{% for column in table.columns %}
|
||||
{% block table.tfoot.td %}
|
||||
<td class="{{ column.name }}" {{ column.attrs.tf.as_html }}>{{ column.footer }}</td>
|
||||
{% endblock table.tfoot.td %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endblock table.tfoot.row %}
|
||||
</tfoot>
|
||||
{% endif %}
|
||||
{% endblock table.tfoot %}
|
||||
</table>
|
||||
</div>
|
||||
{% endblock table %}
|
||||
{% block pagination %}
|
||||
{% if table.page and table.paginator.num_pages > 1 %}
|
||||
<nav class="pagination is-justify-content-flex-end" role="navigation" aria-label="pagination">
|
||||
{% block pagination.previous %}
|
||||
<a
|
||||
class="pagination-previous is-flex-grow-0 {% if not table.page.has_previous %}is-hidden-mobile{% endif %}"
|
||||
{% if table.page.has_previous %}
|
||||
hx-get="search/partial/{% querystring table.prefixed_page_field=table.page.previous_page_number %}&{{ uri }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-trigger="click"
|
||||
hx-target="#drilldown-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#spinner"
|
||||
{% else %}
|
||||
href="#"
|
||||
disabled
|
||||
{% endif %}
|
||||
style="order:1;">
|
||||
{% block pagination.previous.text %}
|
||||
<span aria-hidden="true">«</span>
|
||||
{% endblock pagination.previous.text %}
|
||||
</a>
|
||||
{% endblock pagination.previous %}
|
||||
{% block pagination.next %}
|
||||
<a
|
||||
class="pagination-next is-flex-grow-0 {% if not table.page.has_next %}is-hidden-mobile{% endif %}"
|
||||
{% if table.page.has_next %}
|
||||
hx-get="search/partial/{% querystring table.prefixed_page_field=table.page.next_page_number %}&{{ uri }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-trigger="click"
|
||||
hx-target="#drilldown-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#spinner"
|
||||
{% else %}
|
||||
href="#"
|
||||
disabled
|
||||
{% endif %}
|
||||
style="order:3;"
|
||||
>
|
||||
{% block pagination.next.text %}
|
||||
<span aria-hidden="true">»</span>
|
||||
{% endblock pagination.next.text %}
|
||||
</a>
|
||||
{% endblock pagination.next %}
|
||||
{% if table.page.has_previous or table.page.has_next %}
|
||||
{% block pagination.range %}
|
||||
<ul class="pagination-list is-flex-grow-0" style="order:2;">
|
||||
{% for p in table.page|table_page_range:table.paginator %}
|
||||
<li>
|
||||
<a
|
||||
class="pagination-link {% if p == table.page.number %}is-current{% endif %}"
|
||||
aria-label="Page {{ p }}" block
|
||||
{% if p == table.page.number %}aria-current="page"{% endif %}
|
||||
{% if p == table.page.number %}
|
||||
href="#"
|
||||
{% else %}
|
||||
hx-get="search/partial/{% querystring table.prefixed_page_field=p %}&{{ uri }}"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-trigger="click"
|
||||
hx-target="#drilldown-table"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#spinner"
|
||||
{% endif %}
|
||||
>
|
||||
{% if p == '...' %}
|
||||
<span class="pagination-ellipsis">…</span>
|
||||
{% else %}
|
||||
{{ p }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock pagination.range %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock pagination %}
|
||||
</div>
|
||||
{% endblock table-wrapper %}
|
||||
{# endcache #}
|
||||
@@ -1,9 +1,9 @@
|
||||
<p class="title">This is a demo panel</p>
|
||||
<p class="title">Common controls</p>
|
||||
|
||||
<div class="buttons">
|
||||
<button
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||
hx-get="{% url 'modal' %}"
|
||||
hx-get="{% url 'home' %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#modals-here"
|
||||
class="button is-info">
|
||||
@@ -11,7 +11,7 @@
|
||||
<span class="icon">
|
||||
<i class="fa-solid fa-list"></i>
|
||||
</span>
|
||||
<span>Open modal</span>
|
||||
<span>Open search widget</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
||||
14
core/templates/window-content/results.html
Normal file
14
core/templates/window-content/results.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% load static %}
|
||||
|
||||
{% include 'mixins/partials/notify.html' %}
|
||||
{% if cache is not None %}
|
||||
<span class="icon has-tooltip-bottom" data-tooltip="Cached">
|
||||
<i class="fa-solid fa-database"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
fetched {{ table.data|length }} hits
|
||||
|
||||
|
||||
{% include 'partials/results_table.html' %}
|
||||
{# include 'partials/sentiment_chart.html' #}
|
||||
269
core/templates/window-content/search.html
Normal file
269
core/templates/window-content/search.html
Normal file
@@ -0,0 +1,269 @@
|
||||
<form class="skipEmptyFields" method="POST" hx-post="{% url 'search' %}"
|
||||
hx-trigger="change"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="innerHTML"
|
||||
hx-indicator="#spinner">
|
||||
{% csrf_token %}
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div class="field has-addons">
|
||||
<div id="query" class="control is-expanded has-icons-left">
|
||||
<input
|
||||
hx-post="{% url 'search' %}"
|
||||
hx-trigger="keyup changed delay:200ms"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="innerHTML"
|
||||
name="query"
|
||||
value="{{ params.query }}"
|
||||
class="input"
|
||||
type="text"
|
||||
placeholder="Search something">
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<div class="field">
|
||||
<button
|
||||
id="search"
|
||||
class="button is-fullwidth"
|
||||
hx-post="{% url 'search' %}"
|
||||
hx-trigger="click"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="innerHTML">
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-3">
|
||||
<div class="nowrap-parent">
|
||||
<div
|
||||
data-script="on click toggle .is-hidden on #options"
|
||||
class="button is-right nowrap-child">
|
||||
Options
|
||||
</div>
|
||||
<div class="nowrap-child">
|
||||
<span id="spinner" class="button is-light has-text-link is-loading htmx-indicator">Static</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="options" class="block is-hidden">
|
||||
<div class="columns is-multiline">
|
||||
<div class="column is-narrow">
|
||||
<div class="field has-addons">
|
||||
<div class="control has-icons-left">
|
||||
<span class="select">
|
||||
<select name="size">
|
||||
{% for size in sizes %}
|
||||
{% if size == params.size %}
|
||||
<option selected value="{{ size }}">{{ size }}</option>
|
||||
{% else %}
|
||||
<option value="{{ size }}">{{ size }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="control">
|
||||
<a class="button is-static">
|
||||
results
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div class="field has-addons block">
|
||||
<div class="control has-icons-left">
|
||||
<span class="select">
|
||||
<select id="source" name="source">
|
||||
{% if params.source == 'experiences' %}
|
||||
<option selected value="experiences">Experiences</option>
|
||||
{% else %}
|
||||
<option value="experiences">Experiences</option>
|
||||
{% endif %}
|
||||
|
||||
{% if params.source == 'Other' %}
|
||||
<option selected value="other">Other</option>
|
||||
{% else %}
|
||||
<option value="other">Other</option>
|
||||
{% endif %}
|
||||
|
||||
{% if params.source == None %}
|
||||
<option selected value="substances">Substances</option>
|
||||
{% elif params.source == 'substances' %}
|
||||
<option selected value="substances">Substances</option>
|
||||
{% else %}
|
||||
<option value="substances">Substances</option>
|
||||
{% endif %}
|
||||
|
||||
{% if params.source == 'all' %}
|
||||
<option selected value="all">All</option>
|
||||
{% else %}
|
||||
<option value="all">All</option>
|
||||
{% endif %}
|
||||
|
||||
</select>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="control">
|
||||
<a class="button is-static">
|
||||
source
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-narrow">
|
||||
<div id="date">
|
||||
<div class="field">
|
||||
<div class="control">
|
||||
<input type="date" name="dates" value="{{ params.date }}">
|
||||
<script>
|
||||
var options = {
|
||||
"type": "datetime",
|
||||
"isRange": true,
|
||||
"color": "info",
|
||||
"validateLabel": "Save",
|
||||
"dateFormat": "yyyy-MM-dd",
|
||||
"startDate": "{{ params.from_date|escapejs }}",
|
||||
"startTime": "{{ params.from_time|escapejs }}",
|
||||
"endDate": "{{ params.to_date|escapejs }}",
|
||||
"endTime": "{{ params.to_time|escapejs }}",
|
||||
"displayMode": "dialog"
|
||||
};
|
||||
// Initialize all input of type date
|
||||
var calendars = bulmaCalendar.attach('[type="date"]', options);
|
||||
|
||||
// Loop on each calendar initialized
|
||||
for(var i = 0; i < calendars.length; i++) {
|
||||
// Add listener to select event
|
||||
calendars[i].on('save', date => {
|
||||
htmx.trigger("#search", "click");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
<div class="control">
|
||||
<label class="radio button has-text-link">
|
||||
<input
|
||||
type="radio"
|
||||
value="desc"
|
||||
name="sorting"
|
||||
{% if params.sorting == None %}
|
||||
checked
|
||||
{% elif params.sorting == 'desc' %}
|
||||
checked
|
||||
{% endif %}>
|
||||
<span class="icon" data-tooltip="Sort descending">
|
||||
<i class="fa-solid fa-sort-down"></i>
|
||||
</span>
|
||||
</label>
|
||||
<label class="radio button">
|
||||
<input
|
||||
type="radio"
|
||||
value="asc"
|
||||
name="sorting"
|
||||
{% if params.sorting == 'asc' %}
|
||||
checked
|
||||
{% endif %}>
|
||||
<span class="icon" data-tooltip="Sort ascending">
|
||||
<i class="fa-solid fa-sort-up"></i>
|
||||
</span>
|
||||
</label>
|
||||
<label class="radio button">
|
||||
<input
|
||||
type="radio"
|
||||
value="none"
|
||||
name="sorting"
|
||||
{% if params.sorting == 'none' %}
|
||||
checked
|
||||
{% endif %}>
|
||||
<span class="icon" data-tooltip="No sort">
|
||||
<i class="fa-solid fa-sort"></i>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if params.rule is None %}
|
||||
<div class="column is-narrow rounded-tooltip">
|
||||
<div class="field has-addons">
|
||||
<div class="control has-icons-left">
|
||||
<span class="select is-warning">
|
||||
<select {% if not user.is_superuser %}disabled{% endif %} id="index" name="index">
|
||||
{% if params.index == 'main' %}
|
||||
<option selected value="main">Main</option>
|
||||
{% elif params.index == None %}
|
||||
<option selected value="main">Main</option>
|
||||
{% else %}
|
||||
<option value="main">Main</option>
|
||||
{% endif %}
|
||||
|
||||
{% if params.index == 'internal' %}
|
||||
<option selected value="internal">Internal</option>
|
||||
{% else %}
|
||||
<option value="internal">Internal</option>
|
||||
{% endif %}
|
||||
|
||||
{% if params.index == 'meta' %}
|
||||
<option selected value="meta">Meta</option>
|
||||
{% else %}
|
||||
<option value="meta">Meta</option>
|
||||
{% endif %}
|
||||
|
||||
{% if params.index == 'restricted' %}
|
||||
<option selected value="restricted">Restricted</option>
|
||||
{% else %}
|
||||
<option value="restricted">Restricted</option>
|
||||
{% endif %}
|
||||
|
||||
</select>
|
||||
<span class="icon is-small is-left">
|
||||
<i class="fas fa-magnifying-glass"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p class="control">
|
||||
<a class="button is-static">
|
||||
index
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% if not user.is_superuser %}
|
||||
<span class="tooltiptext tag is-danger is-light">No access</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<input
|
||||
hx-trigger="change"
|
||||
hx-post="{% url 'search' %}"
|
||||
hx-target="#widgets-here"
|
||||
hx-swap="innerHTML"
|
||||
id="tags"
|
||||
class="input"
|
||||
type="tags"
|
||||
name="tags"
|
||||
placeholder="Tag search: nick: john"
|
||||
value="{{ params.tags }}">
|
||||
</div>
|
||||
<div class="is-hidden"></div>
|
||||
{% if params.rule is not None %}
|
||||
<div style="display:none;">
|
||||
<input name="rule" value="{{ params.rule }}">
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
9
core/templatetags/pretty.py
Normal file
9
core/templatetags/pretty.py
Normal file
@@ -0,0 +1,9 @@
|
||||
import orjson
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter
|
||||
def pretty(data):
|
||||
return orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8")
|
||||
@@ -14,11 +14,11 @@ logger = logging.getLogger(__name__)
|
||||
# Create your views here
|
||||
|
||||
|
||||
class Home(View):
|
||||
template_name = "index.html"
|
||||
# class Home(View):
|
||||
# template_name = "index.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name)
|
||||
# def get(self, request):
|
||||
# return render(request, self.template_name)
|
||||
|
||||
|
||||
class Signup(CreateView):
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from core.clients.sources.psychwiki import PsychWikiClient
|
||||
from core.forms import DrugForm
|
||||
from core.models import Drug
|
||||
from core.views.helpers import synchronize_async_helper
|
||||
from mxs.restrictions import StaffMemberRequiredMixin
|
||||
from mxs.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||
from mxs.views import ObjectCreate, ObjectDelete, ObjectList, ObjectRead, ObjectUpdate
|
||||
|
||||
# from mixins.views import ObjectRead
|
||||
|
||||
|
||||
class DrugList(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectList):
|
||||
@@ -19,6 +24,19 @@ class DrugList(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectList):
|
||||
submit_url_name = "drug_create"
|
||||
delete_all_url_name = "drug_clear"
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
self.extra_buttons = [
|
||||
{
|
||||
"url": reverse("drug_pull_merge"),
|
||||
"action": "refresh",
|
||||
"method": "post",
|
||||
"label": "Update database from sources",
|
||||
"icon": "fa-solid fa-refresh",
|
||||
},
|
||||
]
|
||||
return context
|
||||
|
||||
|
||||
class DrugCreate(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectCreate):
|
||||
model = Drug
|
||||
@@ -38,6 +56,20 @@ class DrugDelete(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectDelete):
|
||||
model = Drug
|
||||
|
||||
|
||||
class DrugDetail(ObjectRead):
|
||||
model = Drug
|
||||
form_class = DrugForm
|
||||
detail_template = "partials/drug-detail.html"
|
||||
|
||||
detail_url_name = "drug_detail"
|
||||
detail_url_args = ["type", "pk"]
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
pk = kwargs.get("pk")
|
||||
info = Drug.objects.get(pk=pk)
|
||||
return info
|
||||
|
||||
|
||||
class DrugClear(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
|
||||
def delete(self, request):
|
||||
template_name = "mixins/partials/notify.html"
|
||||
@@ -50,3 +82,18 @@ class DrugClear(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
|
||||
response = render(request, template_name, context)
|
||||
response["HX-Trigger"] = "drugEvent"
|
||||
return response
|
||||
|
||||
|
||||
class DrugPullMerge(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
|
||||
def post(self, request):
|
||||
template_name = "mixins/partials/notify.html"
|
||||
# Do something
|
||||
run = synchronize_async_helper(PsychWikiClient())
|
||||
result = synchronize_async_helper(run.update_drugs())
|
||||
context = {
|
||||
"message": f"Drugs fetched: {result}",
|
||||
"class": "success",
|
||||
}
|
||||
response = render(request, template_name, context)
|
||||
response["HX-Trigger"] = "drugEvent"
|
||||
return response
|
||||
|
||||
85
core/views/favourites.py
Normal file
85
core/views/favourites.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
|
||||
# from mixins.restrictions import StaffMemberRequiredMixin
|
||||
from mixins.views import (
|
||||
ObjectCreate,
|
||||
ObjectDelete,
|
||||
ObjectList,
|
||||
ObjectRead,
|
||||
ObjectUpdate,
|
||||
)
|
||||
|
||||
from core.forms import FavouriteForm
|
||||
from core.models import Favourite
|
||||
|
||||
|
||||
class FavouriteList(LoginRequiredMixin, ObjectList):
|
||||
list_template = "partials/favourite-list.html"
|
||||
model = Favourite
|
||||
page_title = "Global list of favourites"
|
||||
|
||||
list_url_name = "favourites"
|
||||
list_url_args = ["type"]
|
||||
|
||||
submit_url_name = "favourite_create"
|
||||
|
||||
|
||||
class FavouriteCreate(LoginRequiredMixin, ObjectCreate):
|
||||
model = Favourite
|
||||
form_class = FavouriteForm
|
||||
|
||||
submit_url_name = "favourite_create"
|
||||
|
||||
|
||||
class FavouriteUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||
model = Favourite
|
||||
form_class = FavouriteForm
|
||||
|
||||
submit_url_name = "favourite_update"
|
||||
|
||||
|
||||
class FavouriteDelete(LoginRequiredMixin, ObjectDelete):
|
||||
model = Favourite
|
||||
|
||||
|
||||
class FavouriteDetail(LoginRequiredMixin, ObjectRead):
|
||||
model = Favourite
|
||||
form_class = FavouriteForm
|
||||
detail_template = "partials/drug-detail.html"
|
||||
|
||||
detail_url_name = "favourite_detail"
|
||||
detail_url_args = ["type", "pk"]
|
||||
|
||||
def get_object(self, **kwargs):
|
||||
pk = kwargs.get("pk")
|
||||
info = Favourite.objects.get(pk=pk, user=self.request.user)
|
||||
return info
|
||||
|
||||
|
||||
# class FavouriteClear(LoginRequiredMixin, APIView):
|
||||
# def delete(self, request):
|
||||
# template_name = "mixins/partials/notify.html"
|
||||
# favourites_all = Favourite.objects.all()
|
||||
# favourites_all.delete()
|
||||
# context = {
|
||||
# "message": "Deleted all favourites",
|
||||
# "class": "success",
|
||||
# }
|
||||
# response = render(request, template_name, context)
|
||||
# response["HX-Trigger"] = "drugEvent"
|
||||
# return response
|
||||
|
||||
|
||||
# class FavouritePullMerge(LoginRequiredMixin, APIView):
|
||||
# def post(self, request):
|
||||
# template_name = "mixins/partials/notify.html"
|
||||
# # Do something
|
||||
# run = synchronize_async_helper(PsychWikiClient())
|
||||
# result = synchronize_async_helper(run.update_favourites())
|
||||
# context = {
|
||||
# "message": f"Favourites fetched: {result}",
|
||||
# "class": "success",
|
||||
# }
|
||||
# response = render(request, template_name, context)
|
||||
# response["HX-Trigger"] = "drugEvent"
|
||||
# return response
|
||||
18
core/views/helpers.py
Normal file
18
core/views/helpers.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
def synchronize_async_helper(to_await):
|
||||
async_response = []
|
||||
|
||||
async def run_and_capture_result():
|
||||
r = await to_await
|
||||
async_response.append(r)
|
||||
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
except RuntimeError:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
coroutine = run_and_capture_result()
|
||||
loop.run_until_complete(coroutine)
|
||||
return async_response[0]
|
||||
254
core/views/search.py
Normal file
254
core/views/search.py
Normal file
@@ -0,0 +1,254 @@
|
||||
import urllib
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
from django_tables2 import SingleTableView
|
||||
from mixins.views import (
|
||||
ObjectCreate,
|
||||
ObjectDelete,
|
||||
ObjectList,
|
||||
ObjectRead,
|
||||
ObjectUpdate,
|
||||
)
|
||||
|
||||
from core import db
|
||||
from core.models import Drug
|
||||
from core.views.ui.tables import DrugsTable
|
||||
|
||||
|
||||
def make_table(context):
|
||||
object_list = [x.__dict__ for x in context["object_list"]]
|
||||
table = DrugsTable(object_list)
|
||||
context["table"] = table
|
||||
# del context["results"]
|
||||
return context
|
||||
|
||||
|
||||
def parse_dates(dates):
|
||||
spl = dates.split(" - ")
|
||||
if all(spl):
|
||||
spl = [f"{x.replace(' ', 'T')}" for x in spl]
|
||||
if not len(spl) == 2:
|
||||
message = "Invalid dates"
|
||||
message_class = "danger"
|
||||
return {"message": message, "class": message_class}
|
||||
from_ts, to_ts = spl
|
||||
from_date, from_time = from_ts.split("T")
|
||||
to_date, to_time = to_ts.split("T")
|
||||
|
||||
return {
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"from_time": from_time,
|
||||
"to_time": to_time,
|
||||
}
|
||||
|
||||
|
||||
def create_tags(query):
|
||||
"""
|
||||
Grab the tags out of the query and make a list
|
||||
we can add to the Bulma tags element when the page loads.
|
||||
"""
|
||||
spl = query.split("AND")
|
||||
spl = [x.strip() for x in spl if ":" in x]
|
||||
spl = [x.replace('"', "") for x in spl]
|
||||
tags = [f"{tag}: {elem}" for tag, elem in [x.split(":")[:2] for x in spl]]
|
||||
return tags
|
||||
|
||||
|
||||
def parse_tags(tags_pre):
|
||||
"""
|
||||
Parse the tags from the variable tags_pre.
|
||||
"""
|
||||
tags = []
|
||||
tags_spl = tags_pre.split(",")
|
||||
if tags_spl:
|
||||
for tag in tags_spl:
|
||||
tag = tag.split(": ")
|
||||
if len(tag) == 2:
|
||||
key, val = tag
|
||||
tags.append({key: val})
|
||||
return tags
|
||||
|
||||
|
||||
class DrugsTableView(SingleTableView):
|
||||
table_class = DrugsTable
|
||||
template_name = "mixins/wm/widget.html"
|
||||
window_content = "window-content/results.html"
|
||||
# htmx_partial = "partials/"
|
||||
paginate_by = settings.DRUGS_RESULTS_PER_PAGE
|
||||
widget_options = 'gs-w="10" gs-h="1" gs-y="10" gs-x="1"'
|
||||
|
||||
def common_request(self, request, **kwargs):
|
||||
extra_params = {}
|
||||
|
||||
if request.user.is_anonymous:
|
||||
sizes = settings.MAIN_SIZES_ANON
|
||||
else:
|
||||
sizes = settings.MAIN_SIZES
|
||||
|
||||
if request.GET:
|
||||
self.template_name = "index.html"
|
||||
# GET arguments in URL like ?query=xyz
|
||||
query_params = request.GET.dict()
|
||||
if request.htmx:
|
||||
if request.resolver_match.url_name == "search_partial":
|
||||
self.template_name = "partials/results_table.html"
|
||||
elif request.POST:
|
||||
query_params = request.POST.dict()
|
||||
else:
|
||||
self.template_name = "index.html"
|
||||
# No query, this is a fresh page load
|
||||
# Don't try to search, since there's clearly nothing to do
|
||||
params_with_defaults = {}
|
||||
db.add_defaults(params_with_defaults)
|
||||
context = {
|
||||
"sizes": sizes,
|
||||
"params": params_with_defaults,
|
||||
"unique": "results",
|
||||
"widget_options": self.widget_options,
|
||||
"window_content": self.window_content,
|
||||
"title": "Results",
|
||||
}
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
# Merge everything together just in case
|
||||
tmp_post = request.POST.dict()
|
||||
tmp_get = request.GET.dict()
|
||||
tmp_post = {k: v for k, v in tmp_post.items() if v and not v == "None"}
|
||||
tmp_get = {k: v for k, v in tmp_get.items() if v and not v == "None"}
|
||||
query_params.update(tmp_post)
|
||||
query_params.update(tmp_get)
|
||||
|
||||
# URI we're passing to the template for linking
|
||||
if "csrfmiddlewaretoken" in query_params:
|
||||
del query_params["csrfmiddlewaretoken"]
|
||||
|
||||
# Parse the dates
|
||||
if "dates" in query_params:
|
||||
dates = parse_dates(query_params["dates"])
|
||||
del query_params["dates"]
|
||||
if dates:
|
||||
if "message" in dates:
|
||||
return render(request, self.template_name, dates)
|
||||
query_params["from_date"] = dates["from_date"]
|
||||
query_params["to_date"] = dates["to_date"]
|
||||
query_params["from_time"] = dates["from_time"]
|
||||
query_params["to_time"] = dates["to_time"]
|
||||
|
||||
# Remove null values
|
||||
if "query" in query_params:
|
||||
if query_params["query"] == "":
|
||||
del query_params["query"]
|
||||
|
||||
# Remove null tags values
|
||||
# TODO: TAGS
|
||||
if "tags" in query_params:
|
||||
if query_params["tags"] == "":
|
||||
del query_params["tags"]
|
||||
else:
|
||||
# Parse the tags and populate cast to pass to search function
|
||||
tags = parse_tags(query_params["tags"])
|
||||
extra_params["tags"] = tags
|
||||
|
||||
context = db.orm.drug_query(request, query_params, **extra_params)
|
||||
|
||||
print("CONTEXT", context)
|
||||
|
||||
# Unique is for identifying the widgets.
|
||||
# We don't want a random one since we only want one results pane.
|
||||
context["unique"] = "results"
|
||||
context["window_content"] = self.window_content
|
||||
context["widget_options"] = self.widget_options
|
||||
context["title"] = "Results"
|
||||
|
||||
# Valid sizes
|
||||
context["sizes"] = sizes
|
||||
|
||||
# Add any default parameters to the context
|
||||
params_with_defaults = dict(query_params)
|
||||
db.add_defaults(params_with_defaults)
|
||||
context["params"] = params_with_defaults
|
||||
|
||||
# Remove anything that we or the user set to a default for
|
||||
# pretty URLs
|
||||
db.remove_defaults(query_params)
|
||||
url_params = urllib.parse.urlencode(query_params)
|
||||
context["client_uri"] = url_params
|
||||
|
||||
# There's an error
|
||||
if "message" in context:
|
||||
response = render(request, self.template_name, context)
|
||||
# Still push the URL so they can share it to get assistance
|
||||
if request.GET:
|
||||
if request.htmx:
|
||||
response["HX-Replace-Url"] = reverse("home") + "?" + url_params
|
||||
elif request.POST:
|
||||
response["HX-Replace-Url"] = reverse("home") + "?" + url_params
|
||||
return response
|
||||
|
||||
# Create data for chart.js sentiment graph
|
||||
# graph = make_graph(context["object_list"])
|
||||
# context["data"] = graph
|
||||
|
||||
# Create the table
|
||||
context = make_table(context)
|
||||
print("CONTEXT", context)
|
||||
|
||||
# URI we're passing to the template for linking, table fields removed
|
||||
table_fields = ["page", "sort"]
|
||||
clean_params = {k: v for k, v in query_params.items() if k not in table_fields}
|
||||
clean_url_params = urllib.parse.urlencode(clean_params)
|
||||
context["uri"] = clean_url_params
|
||||
|
||||
# unique = str(uuid.uuid4())[:8]
|
||||
# self.context = context
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
print("GET")
|
||||
self.context = self.common_request(request)
|
||||
if isinstance(self.context, HttpResponse):
|
||||
return self.context
|
||||
self.object_list = self.context["object_list"]
|
||||
show = []
|
||||
show = set().union(
|
||||
*([x.name for x in d._meta.get_fields()] for d in self.object_list)
|
||||
)
|
||||
allow_empty = self.get_allow_empty()
|
||||
|
||||
if not allow_empty:
|
||||
# When pagination is enabled and object_list is a queryset,
|
||||
# it's better to do a cheap query than to load the unpaginated
|
||||
# queryset in memory.
|
||||
if self.get_paginate_by(self.object_list) is not None and hasattr(
|
||||
self.object_list, "exists"
|
||||
):
|
||||
is_empty = not self.object_list.exists() # noqa
|
||||
else:
|
||||
is_empty = not self.object_list # noqa
|
||||
context = self.get_context_data()
|
||||
|
||||
for k, v in self.context.items():
|
||||
if k not in context:
|
||||
context[k] = v
|
||||
context["show"] = show
|
||||
|
||||
# if request.htmx:
|
||||
# self.template_name = self.window_content
|
||||
# if request.method == "GET":
|
||||
# if not request.htmx:
|
||||
# self.template_name = "ui/drilldown/drilldown.html"
|
||||
response = self.render_to_response(context)
|
||||
# if not request.method == "GET":
|
||||
if "client_uri" in context:
|
||||
response["HX-Replace-Url"] = reverse("home") + "?" + context["client_uri"]
|
||||
return response
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.get(request, *args, **kwargs)
|
||||
43
core/views/ui/tables.py
Normal file
43
core/views/ui/tables.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from django.conf import settings
|
||||
from django_tables2 import Column, Table
|
||||
|
||||
# from django_tables2.columns.base import BoundColumn
|
||||
|
||||
# Make the table column headings lowercase
|
||||
# orig_Column_header = BoundColumn.header
|
||||
|
||||
|
||||
# @property
|
||||
# def format_header(self):
|
||||
# header = orig_Column_header.__get__(self)
|
||||
# header = header.lower()
|
||||
# header = header.title()
|
||||
# if header != "Ident":
|
||||
# header = header.replace("Id", "ID")
|
||||
# header = header.replace("id", "ID")
|
||||
# if header == "Ts":
|
||||
# header = "TS"
|
||||
# if header == "Match Ts":
|
||||
# header = "Match TS"
|
||||
# header = header.replace("Nsfw", "NSFW")
|
||||
|
||||
# return header
|
||||
|
||||
|
||||
# BoundColumn.header = format_header
|
||||
|
||||
|
||||
class DrugsTable(Table):
|
||||
id = Column()
|
||||
name = Column()
|
||||
drug_class = Column()
|
||||
common_name = Column()
|
||||
links = Column()
|
||||
dosages = Column()
|
||||
timings = Column()
|
||||
effects = Column()
|
||||
actions = Column()
|
||||
experiences = Column()
|
||||
|
||||
template_name = "ui/search/table_results.html"
|
||||
paginate_by = settings.DRUGS_RESULTS_PER_PAGE
|
||||
174
docker-compose.prod.yml
Normal file
174
docker-compose.prod.yml
Normal file
@@ -0,0 +1,174 @@
|
||||
version: "2.2"
|
||||
|
||||
services:
|
||||
app:
|
||||
image: xf/drugs:prod
|
||||
container_name: drugs
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
OPERATION: ${OPERATION}
|
||||
volumes:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
#ports:
|
||||
# - "8000:8000" # uwsgi socket
|
||||
# I don't like this any more than you do
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
STACK_FILE: "${STACK_FILE}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
PROFILER: "${PROFILER}"
|
||||
BILLING_ENABLED: "${BILLING_ENABLED}"
|
||||
DRUGBANK_USERNAME: "${DRUGBANK_USERNAME}"
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
depends_on:
|
||||
# redis:
|
||||
# condition: service_healthy
|
||||
migration:
|
||||
condition: service_started
|
||||
collectstatic:
|
||||
condition: service_started
|
||||
networks:
|
||||
- default
|
||||
- xf
|
||||
|
||||
migration:
|
||||
image: xf/drugs:prod
|
||||
container_name: migration_drugs
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
OPERATION: ${OPERATION}
|
||||
command: sh -c '. /venv/bin/activate && python manage.py migrate --noinput'
|
||||
volumes:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
STACK_FILE: "${STACK_FILE}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
PROFILER: "${PROFILER}"
|
||||
BILLING_ENABLED: "${BILLING_ENABLED}"
|
||||
DRUGBANK_USERNAME: "${DRUGBANK_USERNAME}"
|
||||
|
||||
collectstatic:
|
||||
image: xf/drugs:prod
|
||||
container_name: collectstatic_drugs
|
||||
build:
|
||||
context: .
|
||||
args:
|
||||
OPERATION: ${OPERATION}
|
||||
command: sh -c '. /venv/bin/activate && python manage.py collectstatic --noinput'
|
||||
volumes:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
environment:
|
||||
APP_PORT: "${APP_PORT}"
|
||||
REPO_DIR: "${REPO_DIR}"
|
||||
STACK_FILE: "${STACK_FILE}"
|
||||
APP_DATABASE_FILE: "${APP_DATABASE_FILE}"
|
||||
DOMAIN: "${DOMAIN}"
|
||||
URL: "${URL}"
|
||||
ALLOWED_HOSTS: "${ALLOWED_HOSTS}"
|
||||
NOTIFY_TOPIC: "${NOTIFY_TOPIC}"
|
||||
CSRF_TRUSTED_ORIGINS: "${CSRF_TRUSTED_ORIGINS}"
|
||||
DEBUG: "${DEBUG}"
|
||||
SECRET_KEY: "${SECRET_KEY}"
|
||||
STATIC_ROOT: "${STATIC_ROOT}"
|
||||
REGISTRATION_OPEN: "${REGISTRATION_OPEN}"
|
||||
OPERATION: "${OPERATION}"
|
||||
PROFILER: "${PROFILER}"
|
||||
BILLING_ENABLED: "${BILLING_ENABLED}"
|
||||
DRUGBANK_USERNAME: "${DRUGBANK_USERNAME}"
|
||||
|
||||
nginx:
|
||||
image: nginx:latest
|
||||
container_name: nginx_drugs
|
||||
ports:
|
||||
- ${APP_PORT}:9999
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
volumes:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/nginx/conf.d/${OPERATION}.conf:/etc/nginx/conf.d/default.conf
|
||||
- app_static:${STATIC_ROOT}
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
networks:
|
||||
- default
|
||||
- xf
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_started
|
||||
|
||||
# tmp:
|
||||
# image: busybox
|
||||
# container_name: tmp_drugs
|
||||
# command: chmod -R 777 /var/run/socks
|
||||
# volumes:
|
||||
# - /var/run/socks
|
||||
|
||||
# redis:
|
||||
# image: redis
|
||||
# command: redis-server /etc/redis.conf
|
||||
# ulimits:
|
||||
# nproc: 65535
|
||||
# nofile:
|
||||
# soft: 65535
|
||||
# hard: 65535
|
||||
# volumes:
|
||||
# - ${REPO_DIR}/docker/redis.conf:/etc/redis.conf
|
||||
# - redis_data:/data
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
# healthcheck:
|
||||
# test: "redis-cli -s /var/run/socks/redis.sock ping"
|
||||
# interval: 2s
|
||||
# timeout: 2s
|
||||
# retries: 15
|
||||
|
||||
networks:
|
||||
default:
|
||||
driver: bridge
|
||||
xf:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
app_static: {}
|
||||
# redis_data: {}
|
||||
@@ -17,11 +17,11 @@ services:
|
||||
# - "8000:8000" # uwsgi socket
|
||||
env_file:
|
||||
- stack.env
|
||||
volumes_from:
|
||||
- tmp
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
# redis:
|
||||
# condition: service_healthy
|
||||
migration:
|
||||
condition: service_started
|
||||
collectstatic:
|
||||
@@ -43,8 +43,8 @@ services:
|
||||
- ${REPO_DIR}/docker/uwsgi.ini:/conf/uwsgi.ini
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
volumes_from:
|
||||
- tmp
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
env_file:
|
||||
- stack.env
|
||||
|
||||
@@ -60,8 +60,8 @@ services:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${APP_DATABASE_FILE}:/conf/db.sqlite3
|
||||
- app_static:${STATIC_ROOT}
|
||||
volumes_from:
|
||||
- tmp
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
env_file:
|
||||
- stack.env
|
||||
|
||||
@@ -79,8 +79,8 @@ services:
|
||||
- ${REPO_DIR}:/code
|
||||
- ${REPO_DIR}/docker/nginx/conf.d/${OPERATION}.conf:/etc/nginx/conf.d/default.conf
|
||||
- app_static:${STATIC_ROOT}
|
||||
volumes_from:
|
||||
- tmp
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
networks:
|
||||
- default
|
||||
- xf
|
||||
@@ -88,31 +88,31 @@ services:
|
||||
app:
|
||||
condition: service_started
|
||||
|
||||
tmp:
|
||||
image: busybox
|
||||
container_name: tmp_drugs
|
||||
command: chmod -R 777 /var/run/socks
|
||||
volumes:
|
||||
- /var/run/socks
|
||||
# tmp:
|
||||
# image: busybox
|
||||
# container_name: tmp_drugs
|
||||
# command: chmod -R 777 /var/run/socks
|
||||
# volumes:
|
||||
# - /var/run/socks
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
command: redis-server /etc/redis.conf
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
volumes:
|
||||
- ${REPO_DIR}/docker/redis.conf:/etc/redis.conf
|
||||
- redis_data:/data
|
||||
volumes_from:
|
||||
- tmp
|
||||
healthcheck:
|
||||
test: "redis-cli -s /var/run/socks/redis.sock ping"
|
||||
interval: 2s
|
||||
timeout: 2s
|
||||
retries: 15
|
||||
# redis:
|
||||
# image: redis
|
||||
# command: redis-server /etc/redis.conf
|
||||
# ulimits:
|
||||
# nproc: 65535
|
||||
# nofile:
|
||||
# soft: 65535
|
||||
# hard: 65535
|
||||
# volumes:
|
||||
# - ${REPO_DIR}/docker/redis.conf:/etc/redis.conf
|
||||
# - redis_data:/data
|
||||
# volumes_from:
|
||||
# - tmp
|
||||
# healthcheck:
|
||||
# test: "redis-cli -s /var/run/socks/redis.sock ping"
|
||||
# interval: 2s
|
||||
# timeout: 2s
|
||||
# retries: 15
|
||||
|
||||
networks:
|
||||
default:
|
||||
@@ -122,4 +122,4 @@ networks:
|
||||
|
||||
volumes:
|
||||
app_static: {}
|
||||
redis_data: {}
|
||||
# redis_data: {}
|
||||
|
||||
@@ -5,7 +5,7 @@ env=DJANGO_SETTINGS_MODULE=app.settings
|
||||
master=1
|
||||
pidfile=/tmp/project-master.pid
|
||||
socket=0.0.0.0:8000
|
||||
harakiri=20
|
||||
harakiri=600
|
||||
max-requests=100000
|
||||
vacuum=1
|
||||
home=/venv
|
||||
|
||||
0
mxs/common.py
Normal file
0
mxs/common.py
Normal file
@@ -2,7 +2,7 @@ wheel
|
||||
uwsgi
|
||||
django
|
||||
pre-commit
|
||||
django-crispy-forms==1.14.0
|
||||
django-crispy-forms
|
||||
crispy-bulma
|
||||
# stripe
|
||||
django-rest-framework
|
||||
@@ -21,8 +21,10 @@ django-otp-yubikey
|
||||
phonenumbers
|
||||
qrcode
|
||||
pydantic
|
||||
# glom
|
||||
glom
|
||||
git+https://git.zm.is/XF/django-crud-mixins
|
||||
aiohttp[speedups]
|
||||
|
||||
# pyroscope-io
|
||||
# For caching
|
||||
redis
|
||||
@@ -30,4 +32,6 @@ hiredis
|
||||
django-cachalot
|
||||
django_redis
|
||||
drugbank_downloader
|
||||
bioversions
|
||||
bioversions
|
||||
django-tables2
|
||||
django-tables2-bulma-template
|
||||
Reference in New Issue
Block a user