Upgrade Bulma and begin drug info screen and favourites
This commit is contained in:
@@ -61,4 +61,5 @@ DRUGS_DEFAULT_PARAMS = {
|
|||||||
"size": "15",
|
"size": "15",
|
||||||
"sorting": "desc",
|
"sorting": "desc",
|
||||||
"source": "substances",
|
"source": "substances",
|
||||||
|
"index": "main",
|
||||||
}
|
}
|
||||||
|
|||||||
31
app/urls.py
31
app/urls.py
@@ -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, drugs, notifications, search
|
from core.views import base, demo, drugs, favourites, notifications, search
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("__debug__/", include("debug_toolbar.urls")),
|
path("__debug__/", include("debug_toolbar.urls")),
|
||||||
@@ -57,6 +57,11 @@ urlpatterns = [
|
|||||||
drugs.DrugDelete.as_view(),
|
drugs.DrugDelete.as_view(),
|
||||||
name="drug_delete",
|
name="drug_delete",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"drugs/<str:type>/detail/<str:pk>/",
|
||||||
|
drugs.DrugDetail.as_view(),
|
||||||
|
name="drug_detail",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"drugs/clear/all/",
|
"drugs/clear/all/",
|
||||||
drugs.DrugClear.as_view(),
|
drugs.DrugClear.as_view(),
|
||||||
@@ -70,4 +75,28 @@ urlpatterns = [
|
|||||||
# Drug search
|
# Drug search
|
||||||
path("search/", search.DrugsTableView.as_view(), name="search"),
|
path("search/", search.DrugsTableView.as_view(), name="search"),
|
||||||
path("search/partial/", search.DrugsTableView.as_view(), name="search_partial"),
|
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)
|
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from .models import (
|
|||||||
Entry,
|
Entry,
|
||||||
Experience,
|
Experience,
|
||||||
ExperienceDose,
|
ExperienceDose,
|
||||||
|
Favourite,
|
||||||
NotificationSettings,
|
NotificationSettings,
|
||||||
Source,
|
Source,
|
||||||
Timing,
|
Timing,
|
||||||
@@ -57,3 +58,4 @@ admin.site.register(Experience)
|
|||||||
admin.site.register(Source)
|
admin.site.register(Source)
|
||||||
admin.site.register(SEI)
|
admin.site.register(SEI)
|
||||||
admin.site.register(ExperienceDose)
|
admin.site.register(ExperienceDose)
|
||||||
|
admin.site.register(Favourite)
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class PsychWikiClient(GraphQLClient, BaseClient):
|
|||||||
Store the data in the database.
|
Store the data in the database.
|
||||||
"""
|
"""
|
||||||
for drug in data["substances"]:
|
for drug in data["substances"]:
|
||||||
|
print("DRUG ITER", drug)
|
||||||
try:
|
try:
|
||||||
drug_obj = Drug.objects.get(name=drug["name"])
|
drug_obj = Drug.objects.get(name=drug["name"])
|
||||||
except Drug.DoesNotExist:
|
except Drug.DoesNotExist:
|
||||||
@@ -89,6 +90,7 @@ class PsychWikiClient(GraphQLClient, BaseClient):
|
|||||||
)
|
)
|
||||||
if created or dosage not in drug_obj.dosages.all():
|
if created or dosage not in drug_obj.dosages.all():
|
||||||
drug_obj.dosages.add(dosage)
|
drug_obj.dosages.add(dosage)
|
||||||
|
print("YES DOSAGE", drug_obj.dosages)
|
||||||
|
|
||||||
# Parsing timing information
|
# Parsing timing information
|
||||||
timing = roa["duration"]
|
timing = roa["duration"]
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ def drug_query(request, query_params, size=None, tags=None):
|
|||||||
|
|
||||||
# Q/T - Query/Tags
|
# Q/T - Query/Tags
|
||||||
result = run_query(query_params, tags, size, sources, ranges, sort)
|
result = run_query(query_params, tags, size, sources, ranges, sort)
|
||||||
|
for x in result:
|
||||||
|
print(x.dosages)
|
||||||
rtrn = {
|
rtrn = {
|
||||||
"object_list": result,
|
"object_list": result,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from mixins.restrictions import RestrictedFormMixin
|
|||||||
|
|
||||||
from mxs.restrictions import RestrictedFormMixinStaff
|
from mxs.restrictions import RestrictedFormMixinStaff
|
||||||
|
|
||||||
from .models import Drug, NotificationSettings, User
|
from .models import Drug, Favourite, NotificationSettings, User
|
||||||
|
|
||||||
# Create your forms here.
|
# Create your forms here.
|
||||||
|
|
||||||
@@ -78,3 +78,20 @@ class DrugForm(RestrictedFormMixinStaff, ModelForm):
|
|||||||
"actions": "Actions, what does it do on an objective level?",
|
"actions": "Actions, what does it do on an objective level?",
|
||||||
"experiences": "Experiences, what do people experience?",
|
"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",
|
||||||
|
}
|
||||||
|
|||||||
44
core/migrations/0010_price_favouritedrug.py
Normal file
44
core/migrations/0010_price_favouritedrug.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-05-17 19:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0009_alter_drug_common_name_alter_drug_drug_class'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Price',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('amount_mg', models.IntegerField()),
|
||||||
|
('price_gbp', models.FloatField()),
|
||||||
|
('note', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='FavouriteDrug',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('nickname', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('drug_class', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('common_name', models.CharField(blank=True, max_length=1024, null=True)),
|
||||||
|
('actions', models.ManyToManyField(blank=True, to='core.action')),
|
||||||
|
('dosages', models.ManyToManyField(blank=True, to='core.dosage')),
|
||||||
|
('effects', models.ManyToManyField(blank=True, to='core.effect')),
|
||||||
|
('experiences', models.ManyToManyField(blank=True, to='core.experience')),
|
||||||
|
('links', models.ManyToManyField(blank=True, to='core.entry')),
|
||||||
|
('original', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.drug')),
|
||||||
|
('timings', models.ManyToManyField(blank=True, to='core.timing')),
|
||||||
|
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
('prices', models.ManyToManyField(blank=True, null=True, to='core.price')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
17
core/migrations/0011_rename_favouritedrug_favourite.py
Normal file
17
core/migrations/0011_rename_favouritedrug_favourite.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.0.3 on 2024-05-17 19:02
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0010_price_favouritedrug'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='FavouriteDrug',
|
||||||
|
new_name='Favourite',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -188,7 +188,7 @@ class Dosage(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
text = (
|
text = (
|
||||||
f"{self.threshold_lower} {self.light_lower} {self.common_lower} "
|
f"{self.threshold_lower} {self.light_lower} {self.common_lower} "
|
||||||
"{self.strong_lower} {self.heavy_lower}"
|
f"{self.strong_lower} {self.heavy_lower}"
|
||||||
)
|
)
|
||||||
return f"{self.roa} {text} ({self.unit})"
|
return f"{self.roa} {text} ({self.unit})"
|
||||||
|
|
||||||
@@ -409,6 +409,69 @@ class Drug(models.Model):
|
|||||||
return f"{self.name} ({self.common_name})"
|
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.common_name})"
|
||||||
|
|
||||||
|
|
||||||
# class Perms(models.Model):
|
# class Perms(models.Model):
|
||||||
# class Meta:
|
# class Meta:
|
||||||
# permissions = (
|
# permissions = (
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
4
core/static/css/bulma.min.css
vendored
4
core/static/css/bulma.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -209,6 +209,12 @@
|
|||||||
<a class="navbar-item" href="{% url 'home' %}">
|
<a class="navbar-item" href="{% url 'home' %}">
|
||||||
Home
|
Home
|
||||||
</a>
|
</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 %}
|
{% if user.is_superuser %}
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
<a class="navbar-link">
|
<a class="navbar-link">
|
||||||
@@ -225,9 +231,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a class="navbar-item" href="{% url 'home' %}">
|
|
||||||
Search
|
|
||||||
</a>
|
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
|
|
||||||
<div class="navbar-item has-dropdown is-hoverable">
|
<div class="navbar-item has-dropdown is-hoverable">
|
||||||
|
|||||||
76
core/templates/partials/drug-detail.html
Normal file
76
core/templates/partials/drug-detail.html
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{% 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.name }} - {{ object.nickname }} - {{ object.common_name }}</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>
|
||||||
|
<ul>
|
||||||
|
{% for dose in object.dosages.all %}
|
||||||
|
<li>{{ dose }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="subtitle">Timing</h2>
|
||||||
|
<ul>
|
||||||
|
{% for timing in object.timings.all %}
|
||||||
|
<li>{{ timing }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box">
|
||||||
|
<h2 class="subtitle">Links</h2>
|
||||||
|
<ul>
|
||||||
|
{% for link in object.links.all %}
|
||||||
|
<li>{{ link }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</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>
|
||||||
|
<ul>
|
||||||
|
{% for effect in object.effects.all %}
|
||||||
|
<li>{{ effect }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</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 %}
|
||||||
@@ -64,6 +64,15 @@
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
86
core/templates/partials/favourite-list.html
Normal file
86
core/templates/partials/favourite-list.html
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{% load cache %}
|
||||||
|
{% load cachalot cache %}
|
||||||
|
{% get_last_invalidation 'core.Favourite' as last %}
|
||||||
|
{% include 'mixins/partials/notify.html' %}
|
||||||
|
{% cache 600 objects_favourittes request.user.id object_list last %}
|
||||||
|
<table
|
||||||
|
class="table is-fullwidth is-hoverable"
|
||||||
|
hx-target="#{{ context_object_name }}-table"
|
||||||
|
id="{{ context_object_name }}-table"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
hx-trigger="{{ context_object_name_singular }}Event from:body"
|
||||||
|
hx-get="{{ list_url }}">
|
||||||
|
<thead>
|
||||||
|
<th>id</th>
|
||||||
|
<th>nickname</th>
|
||||||
|
<th>prices</th>
|
||||||
|
<th>name</th>
|
||||||
|
<th>drug class</th>
|
||||||
|
<th>common name</th>
|
||||||
|
<th>links</th>
|
||||||
|
<th>dosages</th>
|
||||||
|
<th>timings</th>
|
||||||
|
<th>effects</th>
|
||||||
|
<th>actions</th>
|
||||||
|
<th>experiences</th>
|
||||||
|
<th>actions</th>
|
||||||
|
</thead>
|
||||||
|
{% for item in object_list %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.id }}</td>
|
||||||
|
<td>{{ item.nickname }}</td>
|
||||||
|
<td>{{ item.prices.count }}</td>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.drug_class }}</td>
|
||||||
|
<td>{{ item.common_name }}</td>
|
||||||
|
<td>{{ item.links.count }}</td>
|
||||||
|
<td>{{ item.dosages.count }}</td>
|
||||||
|
<td>{{ item.timings.count }}</td>
|
||||||
|
<td>{{ item.effects.count }}</td>
|
||||||
|
<td>{{ item.actions.count }}</td>
|
||||||
|
<td>{{ item.experiences.count }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="buttons">
|
||||||
|
<button
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-get="{% url 'favourite_update' type=type pk=item.id %}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#{{ type }}s-here"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-pencil"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
||||||
|
hx-delete="{% url 'favourite_delete' type=type pk=item.id %}"
|
||||||
|
hx-trigger="click"
|
||||||
|
hx-target="#modals-here"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="Are you sure you wish to delete {{ item.name }}?"
|
||||||
|
class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'favourite_detail' type='page' pk=item.id %}"><button
|
||||||
|
class="button">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa-solid fa-eye"></i>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
{% endcache %}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
{% load urlsafe %}
|
{% load urlsafe %}
|
||||||
{% load cache %}
|
{% load cache %}
|
||||||
|
|
||||||
{% cache 3600 results_table_full request.user.id table %}
|
{# cache 3600 results_table_full request.user.id table #}
|
||||||
{% block table-wrapper %}
|
{% block table-wrapper %}
|
||||||
<script src="{% static 'js/column-shifter.js' %}"></script>
|
<script src="{% static 'js/column-shifter.js' %}"></script>
|
||||||
<div id="drilldown-table" class="column-shifter-container" style="position:relative; z-index:1;">
|
<div id="drilldown-table" class="column-shifter-container" style="position:relative; z-index:1;">
|
||||||
@@ -128,14 +128,7 @@
|
|||||||
|
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr class="
|
<tr>
|
||||||
{% if row.cells.exemption == True %}has-background-grey-lighter
|
|
||||||
{% elif cell == 'join' %}has-background-success-light
|
|
||||||
{% elif cell == 'quit' %}has-background-danger-light
|
|
||||||
{% elif cell == 'kick' %}has-background-danger-light
|
|
||||||
{% elif cell == 'part' %}has-background-warning-light
|
|
||||||
{% elif cell == 'mode' %}has-background-info-light
|
|
||||||
{% endif %}">
|
|
||||||
{% for column, cell in row.items %}
|
{% for column, cell in row.items %}
|
||||||
{% if column.name in show %}
|
{% if column.name in show %}
|
||||||
{% block table.tbody.td %}
|
{% block table.tbody.td %}
|
||||||
@@ -145,213 +138,6 @@
|
|||||||
<i class="fa-solid fa-file-slash"></i>
|
<i class="fa-solid fa-file-slash"></i>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
{% elif column.name == 'src' %}
|
|
||||||
<td class="{{ column.name }}">
|
|
||||||
<a
|
|
||||||
class="has-text-grey"
|
|
||||||
onclick="populateSearch('src', '{{ cell|escapejs }}')">
|
|
||||||
{% if row.cells.src == 'irc' %}
|
|
||||||
<span class="icon" data-tooltip="IRC">
|
|
||||||
<i class="fa-solid fa-hashtag" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
{% elif row.cells.src == 'dis' %}
|
|
||||||
<span class="icon" data-tooltip="Discord">
|
|
||||||
<i class="fa-brands fa-discord" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
{% elif row.cells.src == '4ch' %}
|
|
||||||
<span class="icon" data-tooltip="4chan">
|
|
||||||
<i class="fa-solid fa-leaf" aria-hidden="true"></i>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
{% elif column.name == 'ts' %}
|
|
||||||
<td class="{{ column.name }}">
|
|
||||||
<p>{{ row.cells.date }}</p>
|
|
||||||
<p>{{ row.cells.time }}</p>
|
|
||||||
</td>
|
|
||||||
{% elif column.name == 'type' or column.name == 'mtype' %}
|
|
||||||
<td class="{{ column.name }}">
|
|
||||||
<a
|
|
||||||
class="has-text-grey"
|
|
||||||
onclick="populateSearch('{{ column.name }}', '{{ cell|escapejs }}')">
|
|
||||||
{% if cell == 'msg' %}
|
|
||||||
<span class="icon" data-tooltip="Message">
|
|
||||||
<i class="fa-solid fa-message"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'join' %}
|
|
||||||
<span class="icon" data-tooltip="Join">
|
|
||||||
<i class="fa-solid fa-person-to-portal"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'part' %}
|
|
||||||
<span class="icon" data-tooltip="Part">
|
|
||||||
<i class="fa-solid fa-person-from-portal"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'quit' %}
|
|
||||||
<span class="icon" data-tooltip="Quit">
|
|
||||||
<i class="fa-solid fa-circle-xmark"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'kick' %}
|
|
||||||
<span class="icon" data-tooltip="Kick">
|
|
||||||
<i class="fa-solid fa-user-slash"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'nick' %}
|
|
||||||
<span class="icon" data-tooltip="Nick">
|
|
||||||
<i class="fa-solid fa-signature"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'mode' %}
|
|
||||||
<span class="icon" data-tooltip="Mode">
|
|
||||||
<i class="fa-solid fa-gear"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'action' %}
|
|
||||||
<span class="icon" data-tooltip="Action">
|
|
||||||
<i class="fa-solid fa-exclamation"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'notice' %}
|
|
||||||
<span class="icon" data-tooltip="Notice">
|
|
||||||
<i class="fa-solid fa-message-code"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'conn' %}
|
|
||||||
<span class="icon" data-tooltip="Connection">
|
|
||||||
<i class="fa-solid fa-cloud-exclamation"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'znc' %}
|
|
||||||
<span class="icon" data-tooltip="ZNC">
|
|
||||||
<i class="fa-brands fa-unity"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'query' %}
|
|
||||||
<span class="icon" data-tooltip="Query">
|
|
||||||
<i class="fa-solid fa-message"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'highlight' %}
|
|
||||||
<span class="icon" data-tooltip="Highlight">
|
|
||||||
<i class="fa-solid fa-exclamation"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'who' %}
|
|
||||||
<span class="icon" data-tooltip="Who">
|
|
||||||
<i class="fa-solid fa-passport"></i>
|
|
||||||
</span>
|
|
||||||
{% elif cell == 'topic' %}
|
|
||||||
<span class="icon" data-tooltip="Topic">
|
|
||||||
<i class="fa-solid fa-sign"></i>
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
{{ cell }}
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
{% elif column.name == 'msg' %}
|
|
||||||
<td class="{{ column.name }} wrap">
|
|
||||||
<a
|
|
||||||
class="has-text-grey is-underlined"
|
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
|
||||||
hx-post="{% url 'modal_context' %}"
|
|
||||||
hx-vals='{"net": "{{ row.cells.net|escapejs }}",
|
|
||||||
"num": "{{ row.cells.num|escapejs }}",
|
|
||||||
"source": "{{ row.cells.src|escapejs }}",
|
|
||||||
"channel": "{{ row.cells.channel|escapejs }}",
|
|
||||||
"time": "{{ row.cells.time|escapejs }}",
|
|
||||||
"date": "{{ row.cells.date|escapejs }}",
|
|
||||||
"index": "{% if row.cells.index != '—' %}{{row.cells.index}}{% else %}{{ params.index }}{% endif %}",
|
|
||||||
"type": "{{ row.cells.type }}",
|
|
||||||
"mtype": "{{ row.cells.mtype }}",
|
|
||||||
"nick": "{{ row.cells.nick|escapejs }}",
|
|
||||||
"dedup": "{{ params.dedup }}"}'
|
|
||||||
hx-target="#modals-here"
|
|
||||||
hx-trigger="click"
|
|
||||||
href="/?modal=context&net={{row.cells.net|escapejs}}&num={{row.cells.num|escapejs}}&source={{row.cells.src|escapejs}}&channel={{row.cells.channel|urlsafe}}&time={{row.cells.time|escapejs}}&date={{row.cells.date|escapejs}}&index={{params.index}}&type={{row.cells.type}}&mtype={{row.cells.mtype}}&nick={{row.cells.mtype|escapejs}}">
|
|
||||||
{{ row.cells.msg }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
{% elif column.name == 'nick' %}
|
|
||||||
<td class="{{ column.name }}">
|
|
||||||
<div class="nowrap-parent">
|
|
||||||
<div class="nowrap-child">
|
|
||||||
{% if row.cells.online is True %}
|
|
||||||
<span class="icon has-text-success has-tooltip-success" data-tooltip="Online">
|
|
||||||
<i class="fa-solid fa-circle"></i>
|
|
||||||
</span>
|
|
||||||
{% elif row.cells.online is False %}
|
|
||||||
<span class="icon has-text-danger has-tooltip-danger" data-tooltip="Offline">
|
|
||||||
<i class="fa-solid fa-circle"></i>
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="icon has-text-warning has-tooltip-warning" data-tooltip="Unknown">
|
|
||||||
<i class="fa-solid fa-circle"></i>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<a class="nowrap-child has-text-grey" onclick="populateSearch('nick', '{{ cell|escapejs }}')">
|
|
||||||
{{ cell }}
|
|
||||||
</a>
|
|
||||||
<div class="nowrap-child">
|
|
||||||
{% if row.cells.src == 'irc' %}
|
|
||||||
<a
|
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
|
||||||
hx-post="{% url 'modal_drilldown' %}"
|
|
||||||
hx-vals='{"net": "{{ row.cells.net }}", "nick": "{{ row.cells.nick }}", "channel": "{{ row.cells.channel }}"}'
|
|
||||||
hx-target="#modals-here"
|
|
||||||
hx-trigger="click"
|
|
||||||
class="has-text-black">
|
|
||||||
<span class="icon" data-tooltip="Open drilldown modal">
|
|
||||||
<i class="fa-solid fa-album"></i>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
|
||||||
hx-post="{% url 'modal_drilldown' type='window' %}"
|
|
||||||
hx-vals='{"net": "{{ row.cells.net }}", "nick": "{{ row.cells.nick }}", "channel": "{{ row.cells.channel }}"}'
|
|
||||||
hx-target="#windows-here"
|
|
||||||
hx-swap="afterend"
|
|
||||||
hx-trigger="click"
|
|
||||||
class="has-text-black">
|
|
||||||
<span class="icon" data-tooltip="Open drilldown window">
|
|
||||||
<i class="fa-solid fa-album"></i>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
|
|
||||||
hx-post="{% url 'modal_drilldown' type='widget' %}"
|
|
||||||
hx-vals='{"net": "{{ row.cells.net }}", "nick": "{{ row.cells.nick }}", "channel": "{{ row.cells.channel }}"}'
|
|
||||||
hx-target="#widgets-here"
|
|
||||||
hx-trigger="click"
|
|
||||||
class="has-text-black">
|
|
||||||
<span class="icon" data-tooltip="Open drilldown widget">
|
|
||||||
<i class="fa-solid fa-album"></i>
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if row.cells.num_chans != '—' %}
|
|
||||||
<div class="nowrap-child">
|
|
||||||
<span class="tag">
|
|
||||||
{{ row.cells.num_chans }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
{% elif column.name == 'channel' %}
|
|
||||||
<td class="{{ column.name }}">
|
|
||||||
{% if cell != '—' %}
|
|
||||||
<div class="nowrap-parent">
|
|
||||||
<a
|
|
||||||
class="nowrap-child has-text-grey"
|
|
||||||
onclick="populateSearch('channel', '{{ cell|escapejs }}')">
|
|
||||||
{{ cell }}
|
|
||||||
</a>
|
|
||||||
{% if row.cells.num_users != '—' %}
|
|
||||||
<div class="nowrap-child">
|
|
||||||
<span class="tag">
|
|
||||||
{{ row.cells.num_users }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{{ cell }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% elif cell is True or cell is False %}
|
{% elif cell is True or cell is False %}
|
||||||
<td class="{{ column.name }}">
|
<td class="{{ column.name }}">
|
||||||
{% if cell is True %}
|
{% if cell is True %}
|
||||||
@@ -364,44 +150,24 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% elif column.name == "tokens" %}
|
{% elif column.name == "dosages" %}
|
||||||
<td class="{{ column.name }}">
|
<td class="{{ column.name }}">
|
||||||
<div class="tags">
|
{{ cell.entry }}
|
||||||
{% for word in cell %}
|
|
||||||
<a
|
|
||||||
class="tag"
|
|
||||||
onclick="populateSearch('{{ column.name }}', '{{ word }}')">
|
|
||||||
{{ word }}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
{% elif column.name == "meta" %}
|
{% elif column.name == "name" %}
|
||||||
<td class="{{ column.name }}">
|
<td class="{{ column.name }}">
|
||||||
<pre class="small-field" style="cursor: pointer;">{{ cell }}</pre>
|
<a href="{% url 'drug_detail' type='page' pk=row.cells.id %}"><button
|
||||||
</td>
|
class="button">
|
||||||
{% elif 'id' in column.name and column.name != "ident" %}
|
<span class="icon-text">
|
||||||
<td class="{{ column.name }}">
|
<span class="icon">
|
||||||
<div class="buttons">
|
<i class="fa-solid fa-eye"></i>
|
||||||
<div class="nowrap-parent">
|
|
||||||
<!-- <input class="input" type="text" value="{{ cell }}" style="width: 50px;" readonly> -->
|
|
||||||
<a
|
|
||||||
class="has-text-grey button nowrap-child"
|
|
||||||
onclick="populateSearch('{{ column.name }}', '{{ cell|escapejs }}')">
|
|
||||||
<span class="icon" data-tooltip="Populate {{ cell }}">
|
|
||||||
<i class="fa-solid fa-arrow-left-long-to-line" aria-hidden="true"></i>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
class="has-text-grey button nowrap-child"
|
|
||||||
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ cell|escapejs }}');">
|
|
||||||
<span class="icon" data-tooltip="Copy to clipboard">
|
|
||||||
<i class="fa-solid fa-copy" aria-hidden="true"></i>
|
|
||||||
</span>
|
</span>
|
||||||
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
{{ cell }}
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<td class="{{ column.name }}">
|
<td class="{{ column.name }}">
|
||||||
<a
|
<a
|
||||||
@@ -524,4 +290,4 @@
|
|||||||
{% endblock pagination %}
|
{% endblock pagination %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock table-wrapper %}
|
{% endblock table-wrapper %}
|
||||||
{% endcache %}
|
{# endcache #}
|
||||||
9
core/templatetags/pretty.py
Normal file
9
core/templatetags/pretty.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import orjson
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def pretty(data):
|
||||||
|
return orjson.dumps(data, option=orjson.OPT_INDENT_2).decode("utf-8")
|
||||||
@@ -8,7 +8,9 @@ from core.forms import DrugForm
|
|||||||
from core.models import Drug
|
from core.models import Drug
|
||||||
from core.views.helpers import synchronize_async_helper
|
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, ObjectRead, ObjectUpdate
|
||||||
|
|
||||||
|
# from mixins.views import ObjectRead
|
||||||
|
|
||||||
|
|
||||||
class DrugList(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectList):
|
class DrugList(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectList):
|
||||||
@@ -54,6 +56,25 @@ class DrugDelete(LoginRequiredMixin, StaffMemberRequiredMixin, ObjectDelete):
|
|||||||
model = Drug
|
model = Drug
|
||||||
|
|
||||||
|
|
||||||
|
class DrugDetail(LoginRequiredMixin, StaffMemberRequiredMixin, 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):
|
||||||
|
print("GET")
|
||||||
|
pk = kwargs.get("pk")
|
||||||
|
info = Drug.objects.get(pk=pk)
|
||||||
|
|
||||||
|
# self.extra_context = {}
|
||||||
|
print("info", info)
|
||||||
|
# return dictionary
|
||||||
|
return info.__dict__
|
||||||
|
|
||||||
|
|
||||||
class DrugClear(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
|
class DrugClear(LoginRequiredMixin, StaffMemberRequiredMixin, APIView):
|
||||||
def delete(self, request):
|
def delete(self, request):
|
||||||
template_name = "mixins/partials/notify.html"
|
template_name = "mixins/partials/notify.html"
|
||||||
|
|||||||
85
core/views/favourites.py
Normal file
85
core/views/favourites.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
|
||||||
|
# from mixins.restrictions import StaffMemberRequiredMixin
|
||||||
|
from mixins.views import (
|
||||||
|
ObjectCreate,
|
||||||
|
ObjectDelete,
|
||||||
|
ObjectList,
|
||||||
|
ObjectRead,
|
||||||
|
ObjectUpdate,
|
||||||
|
)
|
||||||
|
|
||||||
|
from core.forms import FavouriteForm
|
||||||
|
from core.models import Favourite
|
||||||
|
|
||||||
|
|
||||||
|
class FavouriteList(LoginRequiredMixin, ObjectList):
|
||||||
|
list_template = "partials/favourite-list.html"
|
||||||
|
model = Favourite
|
||||||
|
page_title = "Global list of favourites"
|
||||||
|
|
||||||
|
list_url_name = "favourites"
|
||||||
|
list_url_args = ["type"]
|
||||||
|
|
||||||
|
submit_url_name = "favourite_create"
|
||||||
|
|
||||||
|
|
||||||
|
class FavouriteCreate(LoginRequiredMixin, ObjectCreate):
|
||||||
|
model = Favourite
|
||||||
|
form_class = FavouriteForm
|
||||||
|
|
||||||
|
submit_url_name = "favourite_create"
|
||||||
|
|
||||||
|
|
||||||
|
class FavouriteUpdate(LoginRequiredMixin, ObjectUpdate):
|
||||||
|
model = Favourite
|
||||||
|
form_class = FavouriteForm
|
||||||
|
|
||||||
|
submit_url_name = "favourite_update"
|
||||||
|
|
||||||
|
|
||||||
|
class FavouriteDelete(LoginRequiredMixin, ObjectDelete):
|
||||||
|
model = Favourite
|
||||||
|
|
||||||
|
|
||||||
|
class FavouriteDetail(LoginRequiredMixin, ObjectRead):
|
||||||
|
model = Favourite
|
||||||
|
form_class = FavouriteForm
|
||||||
|
detail_template = "partials/drug-detail.html"
|
||||||
|
|
||||||
|
detail_url_name = "favourite_detail"
|
||||||
|
detail_url_args = ["type", "pk"]
|
||||||
|
|
||||||
|
def get_object(self, **kwargs):
|
||||||
|
pk = kwargs.get("pk")
|
||||||
|
info = Favourite.objects.get(pk=pk, user=self.request.user)
|
||||||
|
return info.__dict__
|
||||||
|
|
||||||
|
|
||||||
|
# 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
|
||||||
Reference in New Issue
Block a user