Implement drug refresh and view
This commit is contained in:
parent
37534b31bf
commit
0488a3e0b2
|
@ -61,4 +61,9 @@ urlpatterns = [
|
||||||
drugs.DrugClear.as_view(),
|
drugs.DrugClear.as_view(),
|
||||||
name="drug_clear",
|
name="drug_clear",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"drugs/refresh/all/",
|
||||||
|
drugs.DrugPullMerge.as_view(),
|
||||||
|
name="drug_pull_merge",
|
||||||
|
),
|
||||||
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,297 @@
|
||||||
|
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"]:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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"]
|
|
@ -0,0 +1 @@
|
||||||
|
from core.lib.schemas import psychwiki_s # noqa
|
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
|
@ -126,6 +126,9 @@ class Source(models.Model):
|
||||||
# Score, affects ordering
|
# Score, affects ordering
|
||||||
score = models.IntegerField(blank=True)
|
score = models.IntegerField(blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.type})"
|
||||||
|
|
||||||
|
|
||||||
class Entry(models.Model):
|
class Entry(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -136,7 +139,7 @@ class Entry(models.Model):
|
||||||
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
source = models.ForeignKey(Source, on_delete=models.CASCADE)
|
||||||
|
|
||||||
# Slug of the article on the Source
|
# 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
|
# Authorship information, if present
|
||||||
author = models.CharField(max_length=255, null=True, blank=True)
|
author = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
@ -144,6 +147,9 @@ class Entry(models.Model):
|
||||||
# Extra information can be added
|
# Extra information can be added
|
||||||
description = models.CharField(max_length=1024, null=True, blank=True)
|
description = models.CharField(max_length=1024, null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.source.name} - {self.url}"
|
||||||
|
|
||||||
|
|
||||||
class Dosage(models.Model):
|
class Dosage(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -160,24 +166,31 @@ class Dosage(models.Model):
|
||||||
unit = models.CharField(max_length=255, choices=DOSAGE_UNIT_CHOICES)
|
unit = models.CharField(max_length=255, choices=DOSAGE_UNIT_CHOICES)
|
||||||
|
|
||||||
# I can no longer say I am sober, but it is slight
|
# I can no longer say I am sober, but it is slight
|
||||||
threshold_lower = models.FloatField()
|
threshold_lower = models.FloatField(null=True, blank=True)
|
||||||
threshold_upper = models.FloatField()
|
threshold_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# Light
|
# Light
|
||||||
light_lower = models.FloatField()
|
light_lower = models.FloatField(null=True, blank=True)
|
||||||
light_upper = models.FloatField()
|
light_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# Average dose for a user
|
# Average dose for a user
|
||||||
common_lower = models.FloatField()
|
common_lower = models.FloatField(null=True, blank=True)
|
||||||
common_upper = models.FloatField()
|
common_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# Strong intensity, many sober activities may become impossible
|
# Strong intensity, many sober activities may become impossible
|
||||||
strong_lower = models.FloatField()
|
strong_lower = models.FloatField(null=True, blank=True)
|
||||||
strong_upper = models.FloatField()
|
strong_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# Highest intensity
|
# Highest intensity
|
||||||
heavy_lower = models.FloatField()
|
heavy_lower = models.FloatField(null=True, blank=True)
|
||||||
heavy_upper = models.FloatField()
|
heavy_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
text = (
|
||||||
|
f"{self.threshold_lower} {self.light_lower} {self.common_lower} "
|
||||||
|
"{self.strong_lower} {self.heavy_lower}"
|
||||||
|
)
|
||||||
|
return f"{self.roa} {text} ({self.unit})"
|
||||||
|
|
||||||
|
|
||||||
class Timing(models.Model):
|
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
|
# It has just now begin, I can no longer say I am sober
|
||||||
onset_lower = models.FloatField()
|
onset_lower = models.FloatField(null=True, blank=True)
|
||||||
onset_upper = models.FloatField()
|
onset_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# The intensity is accelerating
|
# The intensity is accelerating
|
||||||
comeup_lower = models.FloatField()
|
comeup_lower = models.FloatField(null=True, blank=True)
|
||||||
comeup_upper = models.FloatField()
|
comeup_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# The maximum intensity has been reached
|
# The maximum intensity has been reached
|
||||||
# How long this state occurs
|
# How long this state occurs
|
||||||
peak_lower = models.FloatField()
|
peak_lower = models.FloatField(null=True, blank=True)
|
||||||
peak_upper = models.FloatField()
|
peak_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
# How long it takes to get back to baseline
|
# How long it takes to get back to baseline
|
||||||
offset_lower = models.FloatField(null=True, blank=True)
|
offset_lower = models.FloatField(null=True, blank=True)
|
||||||
offset_upper = models.FloatField(null=True, blank=True)
|
offset_upper = models.FloatField(null=True, blank=True)
|
||||||
|
|
||||||
total_lower = models.FloatField()
|
total_lower = models.FloatField(null=True, blank=True)
|
||||||
total_upper = models.FloatField()
|
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):
|
class SEI(models.Model):
|
||||||
|
@ -225,19 +241,24 @@ class SEI(models.Model):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# PHYSICAL, COGNITIVE, etc
|
# PHYSICAL, COGNITIVE, etc
|
||||||
type = models.CharField(
|
# type = models.CharField(
|
||||||
max_length=255, choices=SEI_TYPE_CHOICES, default="PHYSICAL"
|
# max_length=255, choices=SEI_TYPE_CHOICES, default="PHYSICAL"
|
||||||
)
|
# )
|
||||||
|
|
||||||
subtype = models.CharField(
|
# subtype = models.CharField(
|
||||||
max_length=255,
|
# max_length=255,
|
||||||
choices=SEI_SUBTYPE_CHOICES,
|
# choices=SEI_SUBTYPE_CHOICES,
|
||||||
)
|
# )
|
||||||
|
|
||||||
# WIP: consider euphoric, depressant, relaxant
|
# WIP: consider euphoric, depressant, relaxant
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
url = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
|
||||||
# Specify how
|
# 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):
|
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
|
# List of subjective effects, since they would likely be from the same entry
|
||||||
subjective_effects = models.ManyToManyField(SEI)
|
subjective_effects = models.ManyToManyField(SEI)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.entry} {self.subjective_effects}"
|
||||||
|
|
||||||
|
|
||||||
class Action(models.Model):
|
class Action(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -285,6 +309,9 @@ class Action(models.Model):
|
||||||
# Mechanism: Inhibition
|
# Mechanism: Inhibition
|
||||||
# Affinity: Reversible (1)
|
# Affinity: Reversible (1)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.site} {self.mechanism} {self.affinity}"
|
||||||
|
|
||||||
|
|
||||||
class ExperienceDose(models.Model):
|
class ExperienceDose(models.Model):
|
||||||
# Seconds since the start of the experiment
|
# Seconds since the start of the experiment
|
||||||
|
@ -313,6 +340,9 @@ class ExperienceDose(models.Model):
|
||||||
# TODO: Sentiment analysis (use AI/ML)
|
# TODO: Sentiment analysis (use AI/ML)
|
||||||
# TODO: Time-based effects (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):
|
class Experience(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -339,6 +369,9 @@ class Experience(models.Model):
|
||||||
# Description of the experience
|
# Description of the experience
|
||||||
text = models.TextField()
|
text = models.TextField()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Experience ({len(self.text)} chars)"
|
||||||
|
|
||||||
|
|
||||||
class Drug(models.Model):
|
class Drug(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -349,10 +382,10 @@ class Drug(models.Model):
|
||||||
name = models.CharField(max_length=255, unique=True)
|
name = models.CharField(max_length=255, unique=True)
|
||||||
|
|
||||||
# Psychedelic, Sedative, Stimulant
|
# Psychedelic, Sedative, Stimulant
|
||||||
drug_class = models.CharField(max_length=255)
|
drug_class = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|
||||||
# LSD
|
# LSD
|
||||||
common_name = models.CharField(max_length=1024, unique=True)
|
common_name = models.CharField(max_length=1024, blank=True, null=True)
|
||||||
|
|
||||||
# Factsheets, posts
|
# Factsheets, posts
|
||||||
links = models.ManyToManyField(Entry, blank=True)
|
links = models.ManyToManyField(Entry, blank=True)
|
||||||
|
@ -372,6 +405,9 @@ class Drug(models.Model):
|
||||||
# Experiences, what do people experience?
|
# Experiences, what do people experience?
|
||||||
experiences = models.ManyToManyField(Experience, blank=True)
|
experiences = models.ManyToManyField(Experience, blank=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.name} ({self.common_name})"
|
||||||
|
|
||||||
|
|
||||||
# class Perms(models.Model):
|
# class Perms(models.Model):
|
||||||
# class Meta:
|
# class Meta:
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
<td>{{ item.id }}</td>
|
<td>{{ item.id }}</td>
|
||||||
<td>{{ item.name }}</td>
|
<td>{{ item.name }}</td>
|
||||||
<td>{{ item.drug_class }}</td>
|
<td>{{ item.drug_class }}</td>
|
||||||
<td>{{ item.common_name }}s</td>
|
<td>{{ item.common_name }}</td>
|
||||||
<td>{{ item.links.count }}</td>
|
<td>{{ item.links.count }}</td>
|
||||||
<td>{{ item.dosages.count }}</td>
|
<td>{{ item.dosages.count }}</td>
|
||||||
<td>{{ item.timings.count }}</td>
|
<td>{{ item.timings.count }}</td>
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from core.clients.sources.psychwiki import PsychWikiClient
|
||||||
from core.forms import DrugForm
|
from core.forms import DrugForm
|
||||||
from core.models import Drug
|
from core.models import Drug
|
||||||
|
from core.views.helpers import synchronize_async_helper
|
||||||
from mxs.restrictions import StaffMemberRequiredMixin
|
from mxs.restrictions import StaffMemberRequiredMixin
|
||||||
from mxs.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
from mxs.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
|
||||||
|
|
||||||
|
@ -19,6 +22,19 @@ class DrugList(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectList):
|
||||||
submit_url_name = "drug_create"
|
submit_url_name = "drug_create"
|
||||||
delete_all_url_name = "drug_clear"
|
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):
|
class DrugCreate(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectCreate):
|
||||||
model = Drug
|
model = Drug
|
||||||
|
@ -50,3 +66,18 @@ class DrugClear(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
|
||||||
response = render(request, template_name, context)
|
response = render(request, template_name, context)
|
||||||
response["HX-Trigger"] = "drugEvent"
|
response["HX-Trigger"] = "drugEvent"
|
||||||
return response
|
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
|
||||||
|
|
|
@ -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]
|
|
@ -21,8 +21,10 @@ django-otp-yubikey
|
||||||
phonenumbers
|
phonenumbers
|
||||||
qrcode
|
qrcode
|
||||||
pydantic
|
pydantic
|
||||||
# glom
|
glom
|
||||||
git+https://git.zm.is/XF/django-crud-mixins
|
git+https://git.zm.is/XF/django-crud-mixins
|
||||||
|
aiohttp[speedups]
|
||||||
|
|
||||||
# pyroscope-io
|
# pyroscope-io
|
||||||
# For caching
|
# For caching
|
||||||
redis
|
redis
|
||||||
|
|
Loading…
Reference in New Issue