Compare commits

...

15 Commits

56 changed files with 3336 additions and 126 deletions

159
.gitignore vendored Normal file
View 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
View 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

View File

@@ -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",
}

View File

@@ -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 = ''

View File

@@ -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)

View File

@@ -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
View File

205
core/clients/base.py Normal file
View 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
View 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

View 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
View 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
View 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

View File

@@ -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",
}

View File

@@ -0,0 +1 @@
from core.lib.schemas import psychwiki_s # noqa

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel, Extra
class MyModel(BaseModel):
class Config:
extra = Extra.forbid
# class GetDrugsList(MyModel):
# ...

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View File

@@ -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),
),
]

View 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')),
],
),
]

View 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',
),
]

View File

@@ -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:

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

View 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);
});

File diff suppressed because one or more lines are too long

2
core/static/js/jquery.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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

View File

@@ -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 %}

View 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 %}

View File

@@ -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>

View 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 %}

View 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 %}

View 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">&laquo;</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">&raquo;</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">&hellip;</span>
{% else %}
{{ p }}
{% endif %}
</a>
</li>
{% endfor %}
</ul>
{% endblock pagination.range %}
{% endif %}
</nav>
{% endif %}
{% endblock pagination %}
</div>
{% endblock table-wrapper %}
{# endcache #}

View File

@@ -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

View 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' #}

View 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>

View 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")

View File

@@ -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):

View File

@@ -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
View 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
View 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
View 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
View 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
View 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: {}

View File

@@ -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: {}

View File

@@ -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
View File

View 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