Add CRUD for Drug

This commit is contained in:
Mark Veidemanis 2024-01-01 18:21:27 +00:00
parent 0249b66be7
commit 37534b31bf
Signed by: m
GPG Key ID: 5ACFCEED46C0904F
16 changed files with 1355 additions and 54 deletions

View File

@ -20,7 +20,7 @@ from django.contrib.auth.views import LogoutView
from django.urls import include, path from django.urls import include, path
from two_factor.urls import urlpatterns as tf_urls from two_factor.urls import urlpatterns as tf_urls
from core.views import base, demo, notifications from core.views import base, demo, drugs, notifications
urlpatterns = [ urlpatterns = [
path("__debug__/", include("debug_toolbar.urls")), path("__debug__/", include("debug_toolbar.urls")),
@ -40,4 +40,25 @@ urlpatterns = [
notifications.NotificationsUpdate.as_view(), notifications.NotificationsUpdate.as_view(),
name="notifications_update", name="notifications_update",
), ),
path("drugs/<str:type>/", drugs.DrugList.as_view(), name="drugs"),
path(
"drugs/<str:type>/create/",
drugs.DrugCreate.as_view(),
name="drug_create",
),
path(
"drugs/<str:type>/update/<str:pk>/",
drugs.DrugUpdate.as_view(),
name="drug_update",
),
path(
"drugs/<str:type>/delete/<str:pk>/",
drugs.DrugDelete.as_view(),
name="drug_delete",
),
path(
"drugs/clear/all/",
drugs.DrugClear.as_view(),
name="drug_clear",
),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

View File

@ -1,14 +1,14 @@
import os import os
import stripe # import stripe
from django.conf import settings # from django.conf import settings
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true" os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
# from redis import StrictRedis # from redis import StrictRedis
# r = StrictRedis(unix_socket_path="/var/run/redis/redis.sock", db=0) # r = StrictRedis(unix_socket_path="/var/run/redis/redis.sock", db=0)
if settings.STRIPE_TEST: # if settings.STRIPE_TEST:
stripe.api_key = settings.STRIPE_API_KEY_TEST # stripe.api_key = settings.STRIPE_API_KEY_TEST
else: # else:
stripe.api_key = settings.STRIPE_API_KEY_PROD # stripe.api_key = settings.STRIPE_API_KEY_PROD

View File

@ -2,7 +2,20 @@ from django.contrib import admin
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from .forms import CustomUserCreationForm from .forms import CustomUserCreationForm
from .models import NotificationSettings, User from .models import (
SEI,
Action,
Dosage,
Drug,
Effect,
Entry,
Experience,
ExperienceDose,
NotificationSettings,
Source,
Timing,
User,
)
# Register your models here. # Register your models here.
@ -34,3 +47,13 @@ class NotificationSettingsAdmin(admin.ModelAdmin):
admin.site.register(User, CustomUserAdmin) admin.site.register(User, CustomUserAdmin)
admin.site.register(NotificationSettings, NotificationSettingsAdmin) admin.site.register(NotificationSettings, NotificationSettingsAdmin)
admin.site.register(Drug)
admin.site.register(Entry)
admin.site.register(Dosage)
admin.site.register(Timing)
admin.site.register(Effect)
admin.site.register(Action)
admin.site.register(Experience)
admin.site.register(Source)
admin.site.register(SEI)
admin.site.register(ExperienceDose)

View File

@ -3,7 +3,9 @@ from django.contrib.auth.forms import UserCreationForm
from django.forms import ModelForm from django.forms import ModelForm
from mixins.restrictions import RestrictedFormMixin from mixins.restrictions import RestrictedFormMixin
from .models import NotificationSettings, User from mxs.restrictions import RestrictedFormMixinStaff
from .models import Drug, NotificationSettings, User
# Create your forms here. # Create your forms here.
@ -47,3 +49,32 @@ class CustomUserCreationForm(UserCreationForm):
class Meta: class Meta:
model = User model = User
fields = "__all__" fields = "__all__"
class DrugForm(RestrictedFormMixinStaff, ModelForm):
class Meta:
model = Drug
fields = (
"name",
"drug_class",
"common_name",
"links",
"dosages",
"timings",
"effects",
"actions",
"experiences",
)
help_texts = {
"name": "Lysergic acid diethylamide, Phenibut",
"drug_class": "Psychedelic, Sedative, Stimulant",
"common_name": "LSD",
"links": "Factsheets, posts",
"dosages": "Dosages, how much to take to get a certain effect",
"timings": (
"Timings, how long to wait to reach maximum intensity (and " "others)"
),
"effects": "Effects, what does it do on a subjective level?",
"actions": "Actions, what does it do on an objective level?",
"experiences": "Experiences, what do people experience?",
}

View File

@ -0,0 +1,162 @@
# Generated by Django 4.2.8 on 2024-01-01 17:37
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Action',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('site', models.CharField(choices=[('5HT2C', '5-HT2C'), ('5HT2A', '5-HT2A'), ('GABAB', 'GABAB'), ('NMDA', 'NMDA')], max_length=255)),
('mechanism', models.CharField(choices=[('AGONISM', 'Agonism'), ('ANTAGONISM', 'Antagomism'), ('MODULATION', 'Modulation')], max_length=255)),
('affinity', models.IntegerField(blank=True)),
('reversible', models.BooleanField(blank=True, null=True)),
],
),
migrations.CreateModel(
name='Dosage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('unit', models.CharField(choices=[('mg', 'mg'), ('g', 'g'), ('ug', 'ug')], max_length=255)),
('threshold', models.IntegerField()),
('light', models.IntegerField()),
('common', models.IntegerField()),
('strong', models.IntegerField()),
('heavy', models.IntegerField()),
],
),
migrations.CreateModel(
name='Drug',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('drug_class', models.CharField(max_length=255)),
('common_name', models.CharField(max_length=1024, unique=True)),
('actions', models.ManyToManyField(to='core.action')),
('dosages', models.ManyToManyField(to='core.dosage')),
],
),
migrations.CreateModel(
name='Entry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.CharField(blank=True, max_length=1024, null=True)),
('author', models.CharField(blank=True, max_length=255, null=True)),
('description', models.CharField(blank=True, max_length=1024, null=True)),
],
),
migrations.CreateModel(
name='SEI',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('PHYSICAL', 'Physical'), ('COGNITIVE', 'Cognitive'), ('VISUAL', 'Visual'), ('AUDITORY', 'Auditory'), ('MULTISENSORY', 'Multi-sensory'), ('TRANSPERSONAL', 'Transpersonal')], default='PHYSICAL', max_length=255)),
('subtype', models.CharField(choices=[('ENTACTOGENIC', 'Entactogenic (touch-enhancing)'), ('ENTHEOGENIC', 'Entheogenic (spirituality-enhancing)'), ('PSYCHEDELIC', 'Psychedelic (mind-manifesting)'), ('DISSOCIATIVE', 'Dissociative'), ('HALLUCINOGENIC', 'Hallucinogenic (hallucination-inducing)'), ('DELIRIANT', 'Deliriant'), ('PSYCHOTOMIMETIC', 'Psychotomimetic (psychosis-inducing)'), ('STIMULATING', 'Stimulating'), ('SEDATING', 'Sedating'), ('DEPRESSANT', 'Depressant'), ('EUPHORIC', 'Euphoric'), ('ANXIOLYTIC', 'Anxiolytic'), ('ANTIPSYCHOTIC', 'Antipsychotic'), ('PSYCHOSTIMULANT', 'Psychostimulant'), ('EUGEROIC', 'Eugeroic (wakefulness-promoting)'), ('NOOTROPIC', 'Nootropic')], max_length=255)),
('description', models.CharField(blank=True, max_length=4096, null=True)),
],
),
migrations.CreateModel(
name='Source',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, unique=True)),
('type', models.CharField(choices=[('PSITE', 'Professional pharmaceutical data repository'), ('DWIKI', 'Dedicated peer-reviewed community wiki'), ('CWIKI', 'Peer-reviewed community wiki'), ('WIKI', 'Private wiki'), ('DFORUM', 'Dedicated community forum'), ('FORUM', 'Community forum')], max_length=255)),
('endpoint', models.CharField(blank=True, max_length=1024, null=True)),
('score', models.IntegerField(blank=True)),
],
),
migrations.CreateModel(
name='Timing',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('unit', models.CharField(choices=[('SECONDS', 'Seconds'), ('MINUTES', 'Minutes'), ('HOURS', 'Hours'), ('DAYS', 'Days :D'), ('WEEKS', 'Weeks :O'), ('MONTHS', 'Months :-|'), ('YEARS', 'Years x_X')], default='HOURS', max_length=255)),
('onset', models.IntegerField()),
('comeup', models.IntegerField()),
('peak', models.IntegerField()),
('offset', models.IntegerField(blank=True, null=True)),
('total', models.IntegerField()),
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.entry')),
],
),
migrations.CreateModel(
name='ExperienceDose',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('seconds_since_start', models.IntegerField()),
('dose', models.IntegerField()),
('dose_per_kg', models.IntegerField()),
('unit', models.CharField(choices=[('mg', 'mg'), ('g', 'g'), ('ug', 'ug')], max_length=255)),
('roa', models.CharField(max_length=255)),
('form', models.CharField(max_length=255)),
('drug', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.drug')),
],
),
migrations.CreateModel(
name='Experience',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('body_weight_kg', models.IntegerField(blank=True, null=True)),
('year_of_experience', models.IntegerField(blank=True, null=True)),
('gender', models.CharField(blank=True, max_length=255, null=True)),
('age_at_experience', models.IntegerField(blank=True, null=True)),
('date_published', models.DateTimeField(blank=True, null=True)),
('views', models.IntegerField(blank=True, null=True)),
('date_crawled', models.DateTimeField(blank=True, null=True)),
('tags', models.CharField(blank=True, max_length=255, null=True)),
('experience_id', models.IntegerField(blank=True, null=True)),
('text', models.TextField()),
('doses', models.ManyToManyField(to='core.experiencedose')),
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.entry')),
],
),
migrations.AddField(
model_name='entry',
name='source',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.source'),
),
migrations.CreateModel(
name='Effect',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.entry')),
('subjective_effects', models.ManyToManyField(to='core.sei')),
],
),
migrations.AddField(
model_name='drug',
name='effects',
field=models.ManyToManyField(to='core.effect'),
),
migrations.AddField(
model_name='drug',
name='experiences',
field=models.ManyToManyField(to='core.experience'),
),
migrations.AddField(
model_name='drug',
name='links',
field=models.ManyToManyField(to='core.entry'),
),
migrations.AddField(
model_name='drug',
name='timings',
field=models.ManyToManyField(to='core.timing'),
),
migrations.AddField(
model_name='dosage',
name='entry',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.entry'),
),
migrations.AddField(
model_name='action',
name='entry',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.entry'),
),
]

View File

@ -0,0 +1,146 @@
# Generated by Django 4.2.8 on 2024-01-01 17:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0002_action_dosage_drug_entry_sei_source_timing_and_more'),
]
operations = [
migrations.RemoveField(
model_name='timing',
name='comeup',
),
migrations.RemoveField(
model_name='timing',
name='offset',
),
migrations.RemoveField(
model_name='timing',
name='onset',
),
migrations.RemoveField(
model_name='timing',
name='peak',
),
migrations.RemoveField(
model_name='timing',
name='total',
),
migrations.AddField(
model_name='timing',
name='comeup_lower',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='comeup_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='offset_lower',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='timing',
name='offset_upper',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='timing',
name='onset_lower',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='onset_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='peak_lower',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='peak_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='total_lower',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='total_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AlterField(
model_name='dosage',
name='common',
field=models.FloatField(),
),
migrations.AlterField(
model_name='dosage',
name='heavy',
field=models.FloatField(),
),
migrations.AlterField(
model_name='dosage',
name='light',
field=models.FloatField(),
),
migrations.AlterField(
model_name='dosage',
name='strong',
field=models.FloatField(),
),
migrations.AlterField(
model_name='dosage',
name='threshold',
field=models.FloatField(),
),
migrations.AlterField(
model_name='drug',
name='actions',
field=models.ManyToManyField(blank=True, to='core.action'),
),
migrations.AlterField(
model_name='drug',
name='dosages',
field=models.ManyToManyField(blank=True, to='core.dosage'),
),
migrations.AlterField(
model_name='drug',
name='effects',
field=models.ManyToManyField(blank=True, to='core.effect'),
),
migrations.AlterField(
model_name='drug',
name='experiences',
field=models.ManyToManyField(blank=True, to='core.experience'),
),
migrations.AlterField(
model_name='drug',
name='links',
field=models.ManyToManyField(blank=True, to='core.entry'),
),
migrations.AlterField(
model_name='drug',
name='timings',
field=models.ManyToManyField(blank=True, to='core.timing'),
),
]

View File

@ -0,0 +1,68 @@
# Generated by Django 4.2.8 on 2024-01-01 17:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0003_remove_timing_comeup_remove_timing_offset_and_more'),
]
operations = [
migrations.RenameField(
model_name='dosage',
old_name='common',
new_name='common_lower',
),
migrations.RenameField(
model_name='dosage',
old_name='heavy',
new_name='heavy_lower',
),
migrations.RenameField(
model_name='dosage',
old_name='light',
new_name='light_lower',
),
migrations.RenameField(
model_name='dosage',
old_name='strong',
new_name='strong_lower',
),
migrations.RenameField(
model_name='dosage',
old_name='threshold',
new_name='threshold_lower',
),
migrations.AddField(
model_name='dosage',
name='common_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='dosage',
name='heavy_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='dosage',
name='light_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='dosage',
name='strong_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
migrations.AddField(
model_name='dosage',
name='threshold_upper',
field=models.FloatField(default=0),
preserve_default=False,
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 4.2.8 on 2024-01-01 18:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0004_rename_common_dosage_common_lower_and_more'),
]
operations = [
migrations.AddField(
model_name='dosage',
name='roa',
field=models.CharField(choices=[('ORAL', 'Oral'), ('SMOKED', 'Smoked/vaped'), ('INSUFFLATED', 'Insufflated'), ('INJECTED', 'Injected'), ('SUBLINGUAL', 'Sublingual'), ('TRANSDERMAL', 'Transdermal'), ('RECTAL', 'Rectal')], default='ORAL', max_length=255),
preserve_default=False,
),
migrations.AddField(
model_name='timing',
name='roa',
field=models.CharField(choices=[('ORAL', 'Oral'), ('SMOKED', 'Smoked/vaped'), ('INSUFFLATED', 'Insufflated'), ('INJECTED', 'Injected'), ('SUBLINGUAL', 'Sublingual'), ('TRANSDERMAL', 'Transdermal'), ('RECTAL', 'Rectal')], default='ORAL', max_length=255),
preserve_default=False,
),
migrations.AlterField(
model_name='experiencedose',
name='roa',
field=models.CharField(choices=[('ORAL', 'Oral'), ('SMOKED', 'Smoked/vaped'), ('INSUFFLATED', 'Insufflated'), ('INJECTED', 'Injected'), ('SUBLINGUAL', 'Sublingual'), ('TRANSDERMAL', 'Transdermal'), ('RECTAL', 'Rectal')], max_length=255),
),
]

View File

@ -19,6 +19,16 @@ MECHANISM_CHOICES = (
("MODULATION", "Modulation"), ("MODULATION", "Modulation"),
) )
ROA_CHOICES = (
("ORAL", "Oral"),
("SMOKED", "Smoked/vaped"),
("INSUFFLATED", "Insufflated"),
("INJECTED", "Injected"),
("SUBLINGUAL", "Sublingual"),
("TRANSDERMAL", "Transdermal"),
("RECTAL", "Rectal"),
)
SOURCE_TYPE_CHOICES = ( SOURCE_TYPE_CHOICES = (
("PSITE", "Professional pharmaceutical data repository"), ("PSITE", "Professional pharmaceutical data repository"),
("DWIKI", "Dedicated peer-reviewed community wiki"), ("DWIKI", "Dedicated peer-reviewed community wiki"),
@ -53,6 +63,30 @@ SEI_TYPE_CHOICES = (
("TRANSPERSONAL", "Transpersonal"), ("TRANSPERSONAL", "Transpersonal"),
) )
SEI_SUBTYPE_CHOICES = (
# Common psychedelic subtypes
("ENTACTOGENIC", "Entactogenic (touch-enhancing)"),
("ENTHEOGENIC", "Entheogenic (spirituality-enhancing)"),
("PSYCHEDELIC", "Psychedelic (mind-manifesting)"),
("DISSOCIATIVE", "Dissociative"),
("HALLUCINOGENIC", "Hallucinogenic (hallucination-inducing)"),
# Unpleasant psychedelic-esque subtypes
("DELIRIANT", "Deliriant"),
("PSYCHOTOMIMETIC", "Psychotomimetic (psychosis-inducing)"),
# Common recreational drug subtypes
("STIMULATING", "Stimulating"),
("SEDATING", "Sedating"),
("DEPRESSANT", "Depressant"),
("EUPHORIC", "Euphoric"),
# Common pharmaceutical subtypes
("ANXIOLYTIC", "Anxiolytic"),
("ANTIPSYCHOTIC", "Antipsychotic"),
# Common nootropic subtypes
("PSYCHOSTIMULANT", "Psychostimulant"),
("EUGEROIC", "Eugeroic (wakefulness-promoting)"),
("NOOTROPIC", "Nootropic"),
)
class User(AbstractUser): class User(AbstractUser):
# Stripe customer ID # Stripe customer ID
@ -99,11 +133,14 @@ class Entry(models.Model):
Used to gather conflicting information and store it coherently. Used to gather conflicting information and store it coherently.
""" """
source = models.ForeignKey(Source) 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) slug = models.CharField(max_length=1024, null=True, blank=True)
# Authorship information, if present
author = models.CharField(max_length=255, null=True, blank=True)
# 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)
@ -114,26 +151,33 @@ class Dosage(models.Model):
Linked to Entry to analyse conflicting records. Linked to Entry to analyse conflicting records.
""" """
entry = models.ForeignKey("core.Entry") entry = models.ForeignKey("core.Entry", on_delete=models.CASCADE)
roa = models.CharField(max_length=255, choices=ROA_CHOICES)
# Unit of mass as drugs are diverse # Unit of mass as drugs are diverse
# Dosages varying between micrograms and grams # Dosages varying between micrograms and grams
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 = models.IntegerField() threshold_lower = models.FloatField()
threshold_upper = models.FloatField()
# Light # Light
light = models.IntegerField() light_lower = models.FloatField()
light_upper = models.FloatField()
# Average dose for a user # Average dose for a user
common = models.IntegerField() common_lower = models.FloatField()
common_upper = models.FloatField()
# Strong intensity, many sober activities may become impossible # Strong intensity, many sober activities may become impossible
strong = models.IntegerField() strong_lower = models.FloatField()
strong_upper = models.FloatField()
# Highest intensity # Highest intensity
heavy = models.IntegerField() heavy_lower = models.FloatField()
heavy_upper = models.FloatField()
class Timing(models.Model): class Timing(models.Model):
@ -142,7 +186,9 @@ class Timing(models.Model):
Linked to Entry to analyse conflicting records. Linked to Entry to analyse conflicting records.
""" """
entry = models.ForeignKey("core.Entry") entry = models.ForeignKey("core.Entry", on_delete=models.CASCADE)
roa = models.CharField(max_length=255, choices=ROA_CHOICES)
# Unit of time as drugs are diverse # Unit of time as drugs are diverse
# Half-lives varying between seconds and months # Half-lives varying between seconds and months
@ -151,19 +197,24 @@ 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 = models.IntegerField() onset_lower = models.FloatField()
onset_upper = models.FloatField()
# The intensity is accelerating # The intensity is accelerating
comeup = models.IntegerField() comeup_lower = models.FloatField()
comeup_upper = models.FloatField()
# The maximum intensity has been reached # The maximum intensity has been reached
# How long this state occurs # How long this state occurs
peak = models.IntegerField() peak_lower = models.FloatField()
peak_upper = models.FloatField()
# How long it takes to get back to baseline # How long it takes to get back to baseline
offset = models.IntegerField(null=True, blank=True) offset_lower = models.FloatField(null=True, blank=True)
offset_upper = models.FloatField(null=True, blank=True)
total = models.IntegerField() total_lower = models.FloatField()
total_upper = models.FloatField()
class SEI(models.Model): class SEI(models.Model):
@ -178,6 +229,11 @@ class SEI(models.Model):
max_length=255, choices=SEI_TYPE_CHOICES, default="PHYSICAL" max_length=255, choices=SEI_TYPE_CHOICES, default="PHYSICAL"
) )
subtype = models.CharField(
max_length=255,
choices=SEI_SUBTYPE_CHOICES,
)
# WIP: consider euphoric, depressant, relaxant # WIP: consider euphoric, depressant, relaxant
# Specify how # Specify how
@ -191,7 +247,7 @@ class Effect(models.Model):
Linked to Entry to analyse conflicting records. Linked to Entry to analyse conflicting records.
""" """
entry = models.ForeignKey("core.Entry") entry = models.ForeignKey("core.Entry", on_delete=models.CASCADE)
# 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)
@ -205,7 +261,7 @@ class Action(models.Model):
Linked to Entry to analyse conflicting records. Linked to Entry to analyse conflicting records.
""" """
entry = models.ForeignKey("core.Entry") entry = models.ForeignKey("core.Entry", on_delete=models.CASCADE)
# Site - like 5HT2A for LSD # Site - like 5HT2A for LSD
site = models.CharField(max_length=255, choices=SITE_CHOICES) site = models.CharField(max_length=255, choices=SITE_CHOICES)
@ -216,6 +272,73 @@ class Action(models.Model):
# Free integer for binding affinity # Free integer for binding affinity
affinity = models.IntegerField(blank=True) affinity = models.IntegerField(blank=True)
# For MAOI, SSRI
reversible = models.BooleanField(blank=True, null=True)
# MAOI A (I)
# Site: Monoamine Oxidase A
# Mechanism: Inhibition
# Affinity: Permanent (999999)
# Moclobemide
# Site: Monoamine Oxidase A
# Mechanism: Inhibition
# Affinity: Reversible (1)
class ExperienceDose(models.Model):
# Seconds since the start of the experiment
# To be converted to T+00:22 format
seconds_since_start = models.IntegerField()
# Dosage, integer
dose = models.IntegerField()
# Dose per kg of body weight
dose_per_kg = models.IntegerField()
# Unit of mass as drugs are diverse
unit = models.CharField(max_length=255, choices=DOSAGE_UNIT_CHOICES)
# Route of administration, oral, smoked, injected, etc
roa = models.CharField(max_length=255, choices=ROA_CHOICES)
# The drug that was taken, linked to our database
drug = models.ForeignKey("core.Drug", on_delete=models.CASCADE)
# The form of the drug: pills, powder, crystals, tabs, etc
form = models.CharField(max_length=255)
# TODO: Subjective effects (use AI/ML)
# TODO: Sentiment analysis (use AI/ML)
# TODO: Time-based effects (use AI/ML)
class Experience(models.Model):
"""
Registers a subjective experience from a user.
"""
# Where did the data come from?
entry = models.ForeignKey("core.Entry", on_delete=models.CASCADE)
# List of the doses and drugs taken
doses = models.ManyToManyField(ExperienceDose)
# Erowid-specific fields
body_weight_kg = models.IntegerField(blank=True, null=True)
year_of_experience = models.IntegerField(blank=True, null=True)
gender = models.CharField(max_length=255, blank=True, null=True)
age_at_experience = models.IntegerField(blank=True, null=True)
date_published = models.DateTimeField(blank=True, null=True)
views = models.IntegerField(blank=True, null=True)
date_crawled = models.DateTimeField(blank=True, null=True)
tags = models.CharField(max_length=255, blank=True, null=True)
experience_id = models.IntegerField(blank=True, null=True)
# Description of the experience
text = models.TextField()
class Drug(models.Model): class Drug(models.Model):
""" """
@ -225,42 +348,34 @@ class Drug(models.Model):
# Lysergic acid diethylamide, Phenibut # Lysergic acid diethylamide, Phenibut
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
# Psychedelic, Sedative # Psychedelic, Sedative, Stimulant
drug_class = models.CharField(max_length=255) drug_class = models.CharField(max_length=255)
# LSD # LSD
common_name = models.CharField(max_length=1024, unique=True) common_name = models.CharField(max_length=1024, unique=True)
# Factsheets, posts # Factsheets, posts
links = models.ManyToManyField(Entry) links = models.ManyToManyField(Entry, blank=True)
# Dosages, how much to take to get a certain effect # Dosages, how much to take to get a certain effect
dosages = models.ManyToManyField(Dosage) dosages = models.ManyToManyField(Dosage, blank=True)
# Timings, how long to wait to reach maximum intensity (and others) # Timings, how long to wait to reach maximum intensity (and others)
timings = models.ManyToManyField(Timing) timings = models.ManyToManyField(Timing, blank=True)
# Effects, what does it do on a subjective level? # Effects, what does it do on a subjective level?
effects = models.ManyToManyField(Effect) effects = models.ManyToManyField(Effect, blank=True)
# Actions, what does it do on an objective level? # Actions, what does it do on an objective level?
actions = models.ManyToManyField(Action) actions = models.ManyToManyField(Action, blank=True)
# Experiences, what do people experience?
experiences = models.ManyToManyField(Experience, blank=True)
# class Perms(models.Model): # class Perms(models.Model):
# class Meta: # class Meta:
# permissions = ( # permissions = (
# ("bypass_hashing", "Can bypass field hashing"), # # ("permission_name", "Permission description"),
# ("bypass_blacklist", "Can bypass the blacklist"), #
# ("bypass_encryption", "Can bypass field encryption"), #
# ("bypass_obfuscation", "Can bypass field obfuscation"), #
# ("bypass_delay", "Can bypass data delay"), #
# ("bypass_randomisation", "Can bypass data randomisation"), #
# ("post_irc", "Can post to IRC"),
# ("post_discord", "Can post to Discord"),
# ("query_search", "Can search with query strings"), #
# ("use_insights", "Can use the Insights page"),
# ("index_int", "Can use the internal index"),
# ("index_meta", "Can use the meta index"),
# ("restricted_sources", "Can access restricted sources"),
# ) # )

View File

@ -208,8 +208,8 @@
</a> </a>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="navbar-item" href="#"> <a class="navbar-item" href="{% url 'drugs' type='page' %}">
Admin1 Drugs
</a> </a>
<a class="navbar-item" href="#"> <a class="navbar-item" href="#">
Admin2 Admin2
@ -292,8 +292,16 @@
{% endblock %} {% endblock %}
<section class="section"> <section class="section">
<div class="container"> <div class="container">
{% block content_wrapper %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}
{% endblock %}
<div id="modals-here">
</div>
<div id="windows-here">
</div>
<div id="widgets-here" style="display: none;">
</div>
</div> </div>
</section> </section>
</body> </body>

View File

@ -0,0 +1,73 @@
{% load cache %}
{% load cachalot cache %}
{% get_last_invalidation 'core.Drug' as last %}
{% include 'mixins/partials/notify.html' %}
{% cache 600 objects_drugs 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>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.name }}</td>
<td>{{ item.drug_class }}</td>
<td>{{ item.common_name }}s</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 'drug_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 'drug_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>
</div>
</td>
</tr>
{% endfor %}
</table>
{% endcache %}

52
core/views/drugs.py Normal file
View File

@ -0,0 +1,52 @@
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import render
from rest_framework.views import APIView
from core.forms import DrugForm
from core.models import Drug
from mxs.restrictions import StaffMemberRequiredMixin
from mxs.views import ObjectCreate, ObjectDelete, ObjectList, ObjectUpdate
class DrugList(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectList):
list_template = "partials/drug-list.html"
model = Drug
page_title = "Global list of drugs"
list_url_name = "drugs"
list_url_args = ["type"]
submit_url_name = "drug_create"
delete_all_url_name = "drug_clear"
class DrugCreate(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectCreate):
model = Drug
form_class = DrugForm
submit_url_name = "drug_create"
class DrugUpdate(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectUpdate):
model = Drug
form_class = DrugForm
submit_url_name = "drug_update"
class DrugDelete(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectDelete):
model = Drug
class DrugClear(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
def delete(self, request):
template_name = "mixins/partials/notify.html"
drugs_all = Drug.objects.all()
drugs_all.delete()
context = {
"message": "Deleted all drugs",
"class": "success",
}
response = render(request, template_name, context)
response["HX-Trigger"] = "drugEvent"
return response

View File

@ -2,8 +2,8 @@ version: "2.2"
services: services:
app: app:
image: xf/envelope:prod image: xf/drugs:prod
container_name: envelope container_name: drugs
build: build:
context: . context: .
args: args:
@ -31,8 +31,8 @@ services:
- xf - xf
migration: migration:
image: xf/envelope:prod image: xf/drugs:prod
container_name: migration_envelope container_name: migration_drugs
build: build:
context: . context: .
args: args:
@ -49,8 +49,8 @@ services:
- stack.env - stack.env
collectstatic: collectstatic:
image: xf/envelope:prod image: xf/drugs:prod
container_name: collectstatic_envelope container_name: collectstatic_drugs
build: build:
context: . context: .
args: args:
@ -67,7 +67,7 @@ services:
nginx: nginx:
image: nginx:latest image: nginx:latest
container_name: nginx_envelope container_name: nginx_drugs
ports: ports:
- ${APP_PORT}:9999 - ${APP_PORT}:9999
ulimits: ulimits:
@ -90,7 +90,7 @@ services:
tmp: tmp:
image: busybox image: busybox
container_name: tmp_envelope container_name: tmp_drugs
command: chmod -R 777 /var/run/socks command: chmod -R 777 /var/run/socks
volumes: volumes:
- /var/run/socks - /var/run/socks

109
mxs/restrictions.py Normal file
View File

@ -0,0 +1,109 @@
from django.contrib.auth.mixins import UserPassesTestMixin
from django.core.exceptions import ImproperlyConfigured
from django.core.paginator import Paginator
from django.db.models import QuerySet
# Boilerplate: https://git.zm.is/XF/django-crud-mixins
# Edited to not undertake user= checks
class StaffMemberRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_staff
class RestrictedViewMixinStaff:
"""
This mixin overrides two helpers in order to pass the user object to the filters.
get_queryset alters the objects returned for list views.
get_form_kwargs passes the request object to the form class. Remaining permissions
checks are in forms.py
"""
allow_empty = True
queryset = None
model = None
paginate_by = None
paginate_orphans = 0
context_object_name = None
paginator_class = Paginator
page_kwarg = "page"
ordering = None
def set_extra_args(self, user):
"""
This function is overriden to filter the objects by the requesting user.
"""
self.extra_permission_args = {}
def get_queryset(self, **kwargs):
"""
This function is overriden to filter the objects by the requesting user.
"""
self.set_extra_args(self.request.user)
if self.queryset is not None:
queryset = self.queryset
if isinstance(queryset, QuerySet):
# queryset = queryset.all()
# queryset = queryset.filter(
# user=self.request.user, **self.extra_permission_args COMMENTED
# )
queryset = queryset.filter(**self.extra_permission_args)
elif self.model is not None:
# queryset = self.model._default_manager.filter(
# user=self.request.user, **self.extra_permission_args COMMENTED
# )
queryset = self.model._default_manager.filter(**self.extra_permission_args)
else:
raise ImproperlyConfigured(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
)
if hasattr(self, "get_ordering"):
ordering = self.get_ordering()
if ordering:
if isinstance(ordering, str):
ordering = (ordering,)
queryset = queryset.order_by(*ordering)
return queryset
def get_form_kwargs(self):
"""Passes the request object to the form class.
This is necessary to only display members that belong to a given user"""
kwargs = super().get_form_kwargs()
kwargs["request"] = self.request
return kwargs
class RestrictedFormMixinStaff:
"""
Just used to set the request object on the form class.
"""
fieldargs = {}
# TODO: implement set_extra_args to check more permissions here
# for completeness, however as views open forms, the permissions
# are already checked there, so it may not be necessary.
def __init__(self, *args, **kwargs):
# self.fieldargs = {}
self.request = kwargs.pop("request")
super().__init__(*args, **kwargs)
# for field in self.fields:
# # Check it's not something like a CharField which has no queryset
# if not hasattr(self.fields[field], "queryset"):
# continue
# model = self.fields[field].queryset.model
# Check if the model has a user field
# try:
# model._meta.get_field("user")
# # Add the user to the queryset filters
# self.fields[field].queryset = model.objects.filter(
# user=self.request.user, **self.fieldargs.get(field, {})
# )
# except FieldDoesNotExist:
# pass

461
mxs/views.py Normal file
View File

@ -0,0 +1,461 @@
import uuid
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.urls import reverse
from django.views.generic.detail import DetailView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.list import ListView
from rest_framework.parsers import FormParser
from mxs.restrictions import RestrictedViewMixinStaff
# Boilerplate: https://git.zm.is/XF/django-crud-mixins
# Edited to not undertake user= checks
class AbortSave(Exception):
pass
class ObjectNameMixin(object):
def __init__(self, *args, **kwargs):
if self.model is None:
self.title = self.context_object_name.title()
self.title_singular = self.context_object_name_singular.title()
else:
self.title_singular = self.model._meta.verbose_name.title() # Hook
self.context_object_name_singular = self.title_singular.lower() # hook
self.title = self.model._meta.verbose_name_plural.title() # Hooks
self.context_object_name = self.title.lower() # hooks
self.context_object_name = self.context_object_name.replace(" ", "")
self.context_object_name_singular = (
self.context_object_name_singular.replace(" ", "")
)
super().__init__(*args, **kwargs)
class ObjectList(RestrictedViewMixinStaff, ObjectNameMixin, ListView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "mixins/window-content/objects.html"
list_template = None
page_title = None
page_subtitle = None
list_url_name = None
# WARNING: TAKEN FROM locals()
list_url_args = ["type"]
submit_url_name = None
submit_url_args = ["type"]
delete_all_url_name = None
widget_options = None
extra_buttons = None
def queryset_mutate(self, queryset):
pass
# copied from BaseListView
def get(self, request, *args, **kwargs):
type = kwargs.get("type", None)
if not type:
return HttpResponseBadRequest("No type specified")
if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified")
self.request = request
self.object_list = self.get_queryset(**kwargs)
if isinstance(self.object_list, HttpResponse):
return self.object_list
if isinstance(self.object_list, HttpResponseBadRequest):
return self.object_list
self.queryset_mutate(self.object_list)
allow_empty = self.get_allow_empty()
self.template_name = f"mixins/wm/{type}.html"
unique = str(uuid.uuid4())[:8]
list_url_args = {}
for arg in self.list_url_args:
if arg in locals():
list_url_args[arg] = locals()[arg]
elif arg in kwargs:
list_url_args[arg] = kwargs[arg]
orig_type = type
if type == "page":
type = "modal"
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()
else:
is_empty = not self.object_list
if is_empty:
raise Http404("Empty list")
submit_url_args = {}
for arg in self.submit_url_args:
if arg in locals():
submit_url_args[arg] = locals()[arg]
elif arg in kwargs:
submit_url_args[arg] = kwargs[arg]
context = self.get_context_data()
context["title"] = self.title + f" ({type})"
context["title_singular"] = self.title_singular
context["unique"] = unique
context["window_content"] = self.window_content
context["list_template"] = self.list_template
context["page_title"] = self.page_title
context["page_subtitle"] = self.page_subtitle
context["type"] = type
context["context_object_name"] = self.context_object_name
context["context_object_name_singular"] = self.context_object_name_singular
if self.submit_url_name is not None:
context["submit_url"] = reverse(
self.submit_url_name, kwargs=submit_url_args
)
if self.list_url_name is not None:
context["list_url"] = reverse(self.list_url_name, kwargs=list_url_args)
if self.delete_all_url_name:
context["delete_all_url"] = reverse(self.delete_all_url_name)
if self.widget_options:
context["widget_options"] = self.widget_options
if self.extra_buttons is not None:
context["extra_buttons"] = self.extra_buttons
# Return partials for HTMX
if self.request.htmx:
if request.headers["HX-Target"] == self.context_object_name + "-table":
self.template_name = self.list_template
elif orig_type == "page":
self.template_name = self.list_template
else:
context["window_content"] = self.list_template
return self.render_to_response(context)
class ObjectCreate(RestrictedViewMixinStaff, ObjectNameMixin, CreateView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "mixins/window-content/object-form.html"
parser_classes = [FormParser]
page_title = None
page_subtitle = None
model = None
submit_url_name = None
submit_url_args = ["type"]
request = None
# Whether to hide the cancel button in the form
hide_cancel = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = "Create " + self.context_object_name_singular
def post_save(self, obj):
pass
def pre_save_mutate(self, user, obj):
pass
def form_valid(self, form):
obj = form.save(commit=False)
if self.request is None:
raise Exception("Request is None")
obj.user = self.request.user
try:
self.pre_save_mutate(self.request.user, obj)
except AbortSave as e:
context = {"message": f"Failed to save: {e}", "class": "danger"}
return self.render_to_response(context)
obj.save()
form.save_m2m()
self.post_save(obj)
context = {"message": "Object created", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
return self.get(self.request, **self.kwargs, form=form)
def get(self, request, *args, **kwargs):
type = kwargs.get("type", None)
if not type:
return HttpResponseBadRequest("No type specified")
if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified")
self.template_name = f"mixins/wm/{type}.html"
unique = str(uuid.uuid4())[:8]
self.request = request
self.kwargs = kwargs
if type == "widget":
self.hide_cancel = True
if type == "page":
type = "modal"
self.object = None
submit_url_args = {}
for arg in self.submit_url_args:
if arg in locals():
submit_url_args[arg] = locals()[arg]
elif arg in kwargs:
submit_url_args[arg] = kwargs[arg]
submit_url = reverse(self.submit_url_name, kwargs=submit_url_args)
context = self.get_context_data()
form = kwargs.get("form", None)
if form:
context["form"] = form
context["unique"] = unique
context["window_content"] = self.window_content
context["context_object_name"] = self.context_object_name
context["context_object_name_singular"] = self.context_object_name_singular
context["submit_url"] = submit_url
context["type"] = type
context["hide_cancel"] = self.hide_cancel
if self.page_title:
context["page_title"] = self.page_title
if self.page_subtitle:
context["page_subtitle"] = self.page_subtitle
response = self.render_to_response(context)
# response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def post(self, request, *args, **kwargs):
self.request = request
self.template_name = "mixins/partials/notify.html"
return super().post(request, *args, **kwargs)
class ObjectRead(RestrictedViewMixinStaff, ObjectNameMixin, DetailView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "mixins/window-content/object.html"
detail_template = "mixins/partials/generic-detail.html"
page_title = None
page_subtitle = None
model = None
# submit_url_name = None
detail_url_name = None
# WARNING: TAKEN FROM locals()
detail_url_args = ["type"]
request = None
extra_buttons = None
def get(self, request, *args, **kwargs):
type = kwargs.get("type", None)
if not type:
return HttpResponseBadRequest("No type specified")
if type not in self.allowed_types:
return HttpResponseBadRequest()
self.template_name = f"mixins/wm/{type}.html"
unique = str(uuid.uuid4())[:8]
detail_url_args = {}
for arg in self.detail_url_args:
if arg in locals():
detail_url_args[arg] = locals()[arg]
elif arg in kwargs:
detail_url_args[arg] = kwargs[arg]
self.request = request
self.kwargs = kwargs
self.object = self.get_object(**kwargs)
if isinstance(self.object, HttpResponse):
return self.object
orig_type = type
if type == "page":
type = "modal"
context = self.get_context_data()
context["title"] = self.title + f" ({type})"
context["title_singular"] = self.title_singular
context["unique"] = unique
context["window_content"] = self.window_content
context["detail_template"] = self.detail_template
if self.page_title:
context["page_title"] = self.page_title
if self.page_subtitle:
context["page_subtitle"] = self.page_subtitle
context["type"] = type
context["context_object_name"] = self.context_object_name
context["context_object_name_singular"] = self.context_object_name_singular
if self.detail_url_name is not None:
context["detail_url"] = reverse(
self.detail_url_name, kwargs=detail_url_args
)
if self.extra_buttons is not None:
context["extra_buttons"] = self.extra_buttons
# Return partials for HTMX
if self.request.htmx:
if request.headers["HX-Target"] == self.context_object_name + "-info":
self.template_name = self.detail_template
elif orig_type == "page":
self.template_name = self.detail_template
else:
context["window_content"] = self.detail_template
return self.render_to_response(context)
class ObjectUpdate(RestrictedViewMixinStaff, ObjectNameMixin, UpdateView):
allowed_types = ["modal", "widget", "window", "page"]
window_content = "mixins/window-content/object-form.html"
parser_classes = [FormParser]
page_title = None
page_subtitle = None
model = None
submit_url_name = None
submit_url_args = ["type", "pk"]
request = None
# Whether pk is required in the get request
pk_required = True
# Whether to hide the cancel button in the form
hide_cancel = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title = "Update " + self.context_object_name_singular
def post_save(self, obj):
pass
def form_valid(self, form):
obj = form.save(commit=False)
if self.request is None:
raise Exception("Request is None")
obj.save()
form.save_m2m()
self.post_save(obj)
context = {"message": "Object updated", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def form_invalid(self, form):
"""If the form is invalid, render the invalid form."""
return self.get(self.request, **self.kwargs, form=form)
def get(self, request, *args, **kwargs):
self.request = request
type = kwargs.get("type", None)
pk = kwargs.get("pk", None)
if not type:
return HttpResponseBadRequest("No type specified")
if not pk:
if self.pk_required:
return HttpResponseBadRequest("No pk specified")
if type not in self.allowed_types:
return HttpResponseBadRequest("Invalid type specified")
self.template_name = f"mixins/wm/{type}.html"
unique = str(uuid.uuid4())[:8]
if type == "widget":
self.hide_cancel = True
if type == "page":
type = "modal"
self.object = self.get_object()
submit_url_args = {}
for arg in self.submit_url_args:
if arg in locals():
submit_url_args[arg] = locals()[arg]
elif arg in kwargs:
submit_url_args[arg] = kwargs[arg]
submit_url = reverse(self.submit_url_name, kwargs=submit_url_args)
context = self.get_context_data()
form = kwargs.get("form", None)
if form:
context["form"] = form
context["title"] = self.title + f" ({type})"
context["title_singular"] = self.title_singular
context["unique"] = unique
context["window_content"] = self.window_content
context["context_object_name"] = self.context_object_name
context["context_object_name_singular"] = self.context_object_name_singular
context["submit_url"] = submit_url
context["type"] = type
context["hide_cancel"] = self.hide_cancel
if self.page_title:
context["page_title"] = self.page_title
if self.page_subtitle:
context["page_subtitle"] = self.page_subtitle
response = self.render_to_response(context)
# response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
def post(self, request, *args, **kwargs):
self.request = request
self.template_name = "mixins/partials/notify.html"
return super().post(request, *args, **kwargs)
class ObjectDelete(RestrictedViewMixinStaff, ObjectNameMixin, DeleteView):
model = None
template_name = "mixins/partials/notify.html"
# Overriden to prevent success URL from being used
def delete(self, request, *args, **kwargs):
"""
Call the delete() method on the fetched object and then redirect to the
success URL.
"""
self.object = self.get_object()
# success_url = self.get_success_url()
self.object.delete()
context = {"message": "Object deleted", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response
# This will be used in newer Django versions, until then we get a warning
def form_valid(self, form):
"""
Call the delete() method on the fetched object.
"""
self.object = self.get_object()
self.object.delete()
context = {"message": "Object deleted", "class": "success"}
response = self.render_to_response(context)
response["HX-Trigger"] = f"{self.context_object_name_singular}Event"
return response

View File

@ -29,3 +29,5 @@ redis
hiredis hiredis
django-cachalot django-cachalot
django_redis django_redis
drugbank_downloader
bioversions