Change readings table to list of cards, also update edit reading page to include link to delete reading
This commit is contained in:
@@ -2,6 +2,7 @@ import csv
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
import io
|
import io
|
||||||
from flask import Blueprint, Response, make_response, render_template, redirect, request, send_file, url_for, flash
|
from flask import Blueprint, Response, make_response, render_template, redirect, request, send_file, url_for, flash
|
||||||
|
import humanize
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from werkzeug.http import http_date
|
from werkzeug.http import http_date
|
||||||
@@ -104,6 +105,12 @@ def dashboard():
|
|||||||
# Fetch and order readings
|
# Fetch and order readings
|
||||||
readings = readings_query.order_by(Reading.timestamp.desc()).all()
|
readings = readings_query.order_by(Reading.timestamp.desc()).all()
|
||||||
|
|
||||||
|
# Add human-readable relative timestamps to readings
|
||||||
|
now = datetime.utcnow()
|
||||||
|
for reading in readings:
|
||||||
|
reading.relative_timestamp = humanize.naturaltime(now - reading.timestamp)
|
||||||
|
|
||||||
|
|
||||||
# Weekly summary (last 7 days)
|
# Weekly summary (last 7 days)
|
||||||
one_week_ago = datetime.now() - timedelta(days=7)
|
one_week_ago = datetime.now() - timedelta(days=7)
|
||||||
weekly_readings = [r for r in readings if r.timestamp >= one_week_ago]
|
weekly_readings = [r for r in readings if r.timestamp >= one_week_ago]
|
||||||
|
|||||||
@@ -600,14 +600,54 @@ video {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inset-0 {
|
||||||
|
inset: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.top-0 {
|
.top-0 {
|
||||||
top: 0px;
|
top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.right-2 {
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-2 {
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-2 {
|
||||||
|
bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-4 {
|
||||||
|
left: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-4 {
|
||||||
|
right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-4 {
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.z-10 {
|
.z-10 {
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col-span-full {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
.mx-auto {
|
.mx-auto {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
@@ -810,6 +850,10 @@ video {
|
|||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flex-col {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-wrap {
|
.flex-wrap {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
@@ -842,6 +886,10 @@ video {
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gap-6 {
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-space-x-reverse: 0;
|
--tw-space-x-reverse: 0;
|
||||||
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
|
||||||
@@ -896,6 +944,10 @@ video {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-md {
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
}
|
}
|
||||||
@@ -1066,6 +1118,11 @@ video {
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.px-3 {
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pl-2 {
|
.pl-2 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
}
|
}
|
||||||
@@ -1117,6 +1174,11 @@ video {
|
|||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.font-bold {
|
.font-bold {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -1173,6 +1235,21 @@ video {
|
|||||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-gray-400 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(156 163 175 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-green-600 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(22 163 74 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-500 {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(239 68 68 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.no-underline {
|
.no-underline {
|
||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
}
|
}
|
||||||
@@ -1209,6 +1286,18 @@ video {
|
|||||||
transition-duration: 150ms;
|
transition-duration: 150ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.transition-all {
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transition-shadow {
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
transition-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:bg-blue-700:hover {
|
.hover\:bg-blue-700:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
|
background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1));
|
||||||
@@ -1269,6 +1358,21 @@ video {
|
|||||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:text-gray-600:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(75 85 99 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:text-gray-800:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:text-red-700:hover {
|
||||||
|
--tw-text-opacity: 1;
|
||||||
|
color: rgb(185 28 28 / var(--tw-text-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.hover\:underline:hover {
|
.hover\:underline:hover {
|
||||||
text-decoration-line: underline;
|
text-decoration-line: underline;
|
||||||
}
|
}
|
||||||
@@ -1277,6 +1381,12 @@ video {
|
|||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover\:shadow-lg:hover {
|
||||||
|
--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||||
|
--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.focus\:border-blue-500:focus {
|
.focus\:border-blue-500:focus {
|
||||||
--tw-border-opacity: 1;
|
--tw-border-opacity: 1;
|
||||||
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
|
||||||
@@ -1287,6 +1397,11 @@ video {
|
|||||||
border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
|
border-color: rgb(156 163 175 / var(--tw-border-opacity, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.focus\:border-red-500:focus {
|
||||||
|
--tw-border-opacity: 1;
|
||||||
|
border-color: rgb(239 68 68 / var(--tw-border-opacity, 1));
|
||||||
|
}
|
||||||
|
|
||||||
.focus\:text-white:focus {
|
.focus\:text-white:focus {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||||
@@ -1360,6 +1475,10 @@ video {
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lg\:grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
.lg\:items-center {
|
.lg\:items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,67 +168,58 @@
|
|||||||
<!-- Readings Table -->
|
<!-- Readings Table -->
|
||||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="w-full text-left border-collapse">
|
|
||||||
<!-- Table Header -->
|
|
||||||
<thead class="bg-gray-50 border-b">
|
|
||||||
<tr>
|
|
||||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 uppercase">Timestamp</th>
|
|
||||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 uppercase">Blood Pressure (mmHg)</th>
|
|
||||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 uppercase">Heart Rate</th>
|
|
||||||
<th class="px-6 py-3 text-sm font-semibold text-gray-700 uppercase text-center">Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<!-- Table Body -->
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
<tbody>
|
{% for reading in readings %}
|
||||||
{% for reading in readings %}
|
<a href="{{ url_for('main.edit_reading', reading_id=reading.id) }}"
|
||||||
<tr class="border-b hover:bg-gray-50">
|
class="bg-white shadow-md rounded-lg p-4 flex flex-col justify-between relative hover:shadow-lg transition-shadow">
|
||||||
<!-- Timestamp -->
|
<!-- Timestamp -->
|
||||||
<td class="px-6 py-4 text-sm text-gray-600 whitespace-nowrap">
|
<div class="absolute top-2 right-2 flex items-center text-gray-400 text-xs">
|
||||||
{{ reading.timestamp.strftime('%d %b %Y, %I:%M %p') }}
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||||
</td>
|
stroke="currentColor" stroke-width="1.5">
|
||||||
<!-- Blood Pressure -->
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
<td class="px-6 py-4 text-sm text-gray-600 whitespace-nowrap">
|
d="M12 8v4l3 3m9-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z" />
|
||||||
{{ reading.systolic }}/{{ reading.diastolic }}
|
</svg>
|
||||||
</td>
|
<span title="{{ reading.timestamp.strftime('%d %b %Y, %I:%M %p') }}">
|
||||||
<!-- Heart Rate -->
|
{{ reading.relative_timestamp }}
|
||||||
<td class="px-6 py-4 text-sm text-gray-600 whitespace-nowrap">
|
</span>
|
||||||
{{ reading.heart_rate }} bpm
|
</div>
|
||||||
</td>
|
|
||||||
<!-- Actions -->
|
<!-- Blood Pressure -->
|
||||||
<td class="px-6 py-4 text-center">
|
<div class="text-sm text-gray-600 mb-2">
|
||||||
<!-- Edit Button -->
|
<span class="block text-lg font-semibold text-gray-800">Blood Pressure</span>
|
||||||
<a rel="prefetch" href="{{ url_for('main.edit_reading', reading_id=reading.id) }}"
|
<span class="text-2xl font-bold text-blue-600">{{ reading.systolic }}</span>
|
||||||
class="inline-flex items-center px-2 py-1 text-sm font-medium text-blue-600 hover:text-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
<span class="text-lg text-gray-500">/</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
<span class="text-xl font-bold text-red-600">{{ reading.diastolic }}</span>
|
||||||
stroke-width="1.5" stroke="currentColor" class="w-3 h-3 mr-1">
|
<span class="text-sm text-gray-500">mmHg</span>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round"
|
</div>
|
||||||
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
|
|
||||||
</svg>
|
<!-- Heart Rate and Arrow -->
|
||||||
|
<div class="flex justify-between items-center mt-4">
|
||||||
|
<div class="text-sm text-gray-600">
|
||||||
|
<span class="block text-lg font-semibold text-gray-800">Heart Rate</span>
|
||||||
|
<span class="text-2xl font-bold text-green-600">{{ reading.heart_rate }}</span>
|
||||||
|
<span class="text-sm text-gray-500">bpm</span>
|
||||||
|
</div>
|
||||||
|
<!-- Arrow Icon -->
|
||||||
|
<div class="text-gray-400 hover:text-gray-600">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||||
|
stroke="currentColor" class="h-5 w-5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<div class="col-span-full text-center text-sm text-gray-500">
|
||||||
|
No readings found.
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<span>Edit</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Delete Button -->
|
|
||||||
<a href="{{ url_for('main.confirm_delete', reading_id=reading.id) }}"
|
|
||||||
class="flex items-center px-2 py-1 text-sm font-medium text-red-600 hover:text-red-800 focus:outline-none focus:ring-2 focus:ring-red-500">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none"
|
|
||||||
viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
||||||
</svg>
|
|
||||||
<span>Delete</span>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="px-6 py-4 text-center text-sm text-gray-500">
|
|
||||||
No readings found.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
{% extends "_layout.html" %}
|
{% extends "_layout.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md">
|
<div class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-md relative">
|
||||||
|
<!-- Cancel Button (Top-Left) -->
|
||||||
|
<a href="{{ url_for('main.dashboard') }}"
|
||||||
|
class="absolute top-4 left-4 flex items-center text-gray-600 hover:text-gray-800">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
<span>Back</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Delete Button (Top-Right) -->
|
||||||
|
<a href="{{ url_for('main.confirm_delete', reading_id=reading.id) }}"
|
||||||
|
class="absolute top-4 right-4 text-red-500 hover:text-red-700">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"
|
||||||
|
class="size-6">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round"
|
||||||
|
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Edit Reading</h1>
|
<h1 class="text-3xl font-bold text-center text-gray-800 mb-6">Edit Reading</h1>
|
||||||
<form method="POST" action="{{ url_for('main.edit_reading', reading_id=reading.id) }}" novalidate>
|
<form method="POST" action="{{ url_for('main.edit_reading', reading_id=reading.id) }}" novalidate>
|
||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
@@ -45,17 +66,10 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Save Button -->
|
||||||
<div class="flex justify-between items-center mt-6">
|
<div class="mt-6">
|
||||||
<!-- Cancel Button -->
|
|
||||||
<a href="{{ url_for('main.dashboard') }}" class="px-6 py-3 bg-gray-300 text-gray-700 rounded-lg shadow-sm
|
|
||||||
hover:bg-gray-400 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-gray-400">
|
|
||||||
Cancel
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Submit Button -->
|
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="px-6 py-3 bg-blue-600 text-white rounded-lg shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
class="w-full bg-blue-600 text-white py-3 rounded-lg font-semibold shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ flask-sqlalchemy==3.1.1
|
|||||||
flask-wtf==1.2.2
|
flask-wtf==1.2.2
|
||||||
greenlet==3.1.1
|
greenlet==3.1.1
|
||||||
gunicorn==23.0.0
|
gunicorn==23.0.0
|
||||||
|
humanize==4.11.0
|
||||||
idna==3.10
|
idna==3.10
|
||||||
importlib-metadata==8.5.0
|
importlib-metadata==8.5.0
|
||||||
itsdangerous==2.2.0
|
itsdangerous==2.2.0
|
||||||
|
|||||||
Reference in New Issue
Block a user