was working, changes to displays

This commit is contained in:
2025-07-10 02:52:55 -05:00
parent 2b63f4c9c7
commit ab2060c711
6 changed files with 116 additions and 24 deletions

View File

@ -1,3 +1,4 @@
<!-- File: app/templates/core/base.html -->
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
@ -119,11 +120,15 @@
Orphaned Media Orphaned Media
</a> </a>
</li> </li>
<li>
<a class="dropdown-item" href="{{ url_for('admin.list_invitations') }}">
Invitations
</a>
</li>
</ul> </ul>
</div> </div>
{% endif %} {% endif %}
<!-- Plugins dropdown --> <!-- Plugins dropdown -->
<div class="dropdown me-3"> <div class="dropdown me-3">
<a <a

BIN
celerybeat-schedule Normal file

Binary file not shown.

View File

@ -10,7 +10,7 @@ from sqlalchemy import func, desc
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app import db from app import db
from plugins.auth.models import User from plugins.auth.models import User, Invitation
from plugins.plant.growlog.models import GrowLog from plugins.plant.growlog.models import GrowLog
from plugins.plant.models import Plant from plugins.plant.models import Plant
from plugins.media.models import Media from plugins.media.models import Media
@ -28,7 +28,7 @@ def dashboard():
now = datetime.utcnow() now = datetime.utcnow()
week_ago = now - timedelta(days=7) week_ago = now - timedelta(days=7)
month_ago= now - timedelta(days=30) month_ago = now - timedelta(days=30)
# ─── Overview metrics ────────────────────────────────────────────── # # ─── Overview metrics ────────────────────────────────────────────── #
@ -51,7 +51,7 @@ def dashboard():
# Signups last 30 days # Signups last 30 days
signup_dates = [(month_ago + timedelta(days=i)).date() for i in range(31)] signup_dates = [(month_ago + timedelta(days=i)).date() for i in range(31)]
signup_counts = [ signup_counts = [
User.query.filter(func.date(User.created_at)==d).count() User.query.filter(func.date(User.created_at) == d).count()
for d in signup_dates for d in signup_dates
] ]
chart_dates = [d.strftime('%Y-%m-%d') for d in signup_dates] chart_dates = [d.strftime('%Y-%m-%d') for d in signup_dates]
@ -96,10 +96,14 @@ def dashboard():
) )
for ua, cnt in ua_counts: for ua, cnt in ua_counts:
b = 'Other' b = 'Other'
if 'Chrome' in ua: b = 'Chrome' if 'Chrome' in ua:
elif 'Firefox' in ua: b = 'Firefox' b = 'Chrome'
elif 'Safari' in ua and 'Chrome' not in ua: b = 'Safari' elif 'Firefox' in ua:
elif 'Edge' in ua: b = 'Edge' b = 'Firefox'
elif 'Safari' in ua and 'Chrome' not in ua:
b = 'Safari'
elif 'Edge' in ua:
b = 'Edge'
browsers[b] = browsers.get(b, 0) + cnt browsers[b] = browsers.get(b, 0) + cnt
referrers = dict( referrers = dict(
@ -116,16 +120,14 @@ def dashboard():
# ─── Stats metrics ────────────────────────────────────────────────── # # ─── Stats metrics ────────────────────────────────────────────────── #
# Plant totals
total_plants = Plant.query.count() total_plants = Plant.query.count()
total_images = Media.query.count() total_images = Media.query.count()
active_plants = Plant.query.filter_by(is_active=True).count() active_plants = Plant.query.filter_by(is_active=True).count()
# Top 5 popular plant types
popular_plants = db.session.query( popular_plants = db.session.query(
Plant.plant_type, Plant.plant_type,
func.count(Plant.id).label('count') func.count(Plant.id).label('count')
).filter(Plant.is_active==True) \ ).filter(Plant.is_active == True) \
.group_by(Plant.plant_type) \ .group_by(Plant.plant_type) \
.order_by(desc('count')) \ .order_by(desc('count')) \
.limit(5) \ .limit(5) \
@ -340,7 +342,27 @@ def undelete_user(user_id):
@bp.route('/orphaned-media') @bp.route('/orphaned-media')
@login_required @login_required
def orphaned_media_list(): def orphaned_media_list():
if not current_user.role == 'admin': if current_user.role != 'admin':
abort(403) return "Access denied", 403
items = Media.query.filter_by(status='orphaned').order_by(Media.orphaned_at.desc()).all() items = Media.query.filter_by(status='orphaned').order_by(Media.orphaned_at.desc()).all()
return render_template('admin/orphaned_media_list.html', items=items) return render_template('admin/orphaned_media_list.html', items=items)
# ─── Invitation Management ──────────────────────────────────────────────── #
@bp.route('/invitations')
@login_required
def list_invitations():
if current_user.role != 'admin':
return "Access denied", 403
user_id = request.args.get('user_id', type=int)
query = Invitation.query
if user_id:
query = query.filter(Invitation.created_by == user_id)
invitations = query.order_by(Invitation.created_at.desc()).all()
return render_template(
'admin/invitations/list.html',
invitations=invitations
)

View File

@ -0,0 +1,40 @@
{% extends 'core/base.html' %}
{% block title %}Invitations Admin Nature In Pots{% endblock %}
{% block content %}
<h1>Invitations</h1>
<p>
<a href="{{ url_for('auth.send_invite') }}"
class="btn btn-primary mb-3">
Create Invitation
</a>
</p>
<table class="table table-striped">
<thead>
<tr>
<th>Code</th>
<th>Recipient Email</th>
<th>Created By</th>
<th>Created At</th>
<th>Used By</th>
<th>Used At</th>
<th>Active</th>
</tr>
</thead>
<tbody>
{% for inv in invitations %}
<tr>
<td>{{ inv.code }}</td>
<td>{{ inv.recipient_email }}</td>
<td>{{ inv.creator.email }}</td>
<td>{{ inv.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
<td>{{ inv.user.email if inv.user else '' }}</td>
<td>{{ inv.used_at.strftime('%Y-%m-%d %H:%M') if inv.used_at else '' }}</td>
<td>{{ 'Yes' if inv.is_active else 'No' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -1,3 +1,4 @@
{# File: plugins/admin/templates/admin/users/form.html #}
{% extends 'core/base.html' %} {% extends 'core/base.html' %}
{% block title %}{{ action }} User Admin Nature In Pots{% endblock %} {% block title %}{{ action }} User Admin Nature In Pots{% endblock %}
@ -37,6 +38,14 @@
{{ form.excluded_from_analytics.label(class="form-check-label") }} {{ form.excluded_from_analytics.label(class="form-check-label") }}
</div> </div>
<div class="mb-3">
{{ form.invites_remaining.label(class="form-label") }}
{{ form.invites_remaining(class="form-control") }}
<small class="form-text text-muted">
How many invitations this user may still send.
</small>
</div>
<hr> <hr>
<div class="mb-3"> <div class="mb-3">

View File

@ -1,3 +1,4 @@
{# File: plugins/admin/templates/admin/users/list.html #}
{% extends 'core/base.html' %} {% extends 'core/base.html' %}
{% block title %}Users Admin Nature In Pots{% endblock %} {% block title %}Users Admin Nature In Pots{% endblock %}
{% block content %} {% block content %}
@ -36,9 +37,15 @@
<table class="table table-striped"> <table class="table table-striped">
<thead> <thead>
<tr> <tr>
<th>ID</th><th>Email</th><th>Role</th> <th>ID</th>
<th>Verified</th><th>Excluded</th><th>Status</th> <th>Email</th>
<th>Joined</th><th>Actions</th> <th>Role</th>
<th>Verified</th>
<th>Excluded</th>
<th>Status</th>
<th>Joined</th>
<th>Invites Remaining</th>
<th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody id="userTableBody"> <tbody id="userTableBody">
@ -61,6 +68,7 @@
{% endif %} {% endif %}
</td> </td>
<td>{{ u.created_at.strftime('%Y-%m-%d') }}</td> <td>{{ u.created_at.strftime('%Y-%m-%d') }}</td>
<td>{{ u.invites_remaining }}</td>
<td> <td>
<a href="{{ url_for('admin.edit_user', <a href="{{ url_for('admin.edit_user',
user_id=u.id, user_id=u.id,
@ -68,6 +76,13 @@
show_deleted='1' if show_deleted else None, show_deleted='1' if show_deleted else None,
q=q) }}" q=q) }}"
class="btn btn-sm btn-primary">Edit</a> class="btn btn-sm btn-primary">Edit</a>
<a href="{{ url_for('auth.adjust_invites', user_id=u.id) }}"
class="btn btn-sm btn-info">Adjust Invites</a>
<a href="{{ url_for('admin.list_invitations', user_id=u.id) }}"
class="btn btn-sm btn-warning">Invitations</a>
{% if u.is_deleted %} {% if u.is_deleted %}
<form action="{{ url_for('admin.undelete_user', <form action="{{ url_for('admin.undelete_user',
user_id=u.id, user_id=u.id,
@ -168,7 +183,6 @@
</ul> </ul>
</nav> </nav>
{# --- AJAX search script --- #}
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('searchInput'); const searchInput = document.getElementById('searchInput');
@ -218,15 +232,17 @@ document.addEventListener('DOMContentLoaded', function() {
<td>${u.excluded_from_analytics ? '✓' : ''}</td> <td>${u.excluded_from_analytics ? '✓' : ''}</td>
<td>${statusHtml}</td> <td>${statusHtml}</td>
<td>${u.created_at}</td> <td>${u.created_at}</td>
<td>${u.invites_remaining}</td>
<td> <td>
<a href="/admin/users/${u.id}/edit" class="btn btn-sm btn-primary">Edit</a> <a href="/admin/users/${u.id}/edit" class="btn btn-sm btn-primary">Edit</a>
<a href="/admin/users/${u.id}/adjust-invites" class="btn btn-sm btn-info">Adjust Invites</a>
<a href="/admin/invitations?user_id=${u.id}" class="btn btn-sm btn-warning">Invitations</a>
${actionForms} ${actionForms}
</td> </td>
</tr> </tr>
`); `);
}); });
// hide pagination when searching
paginationNav.style.display = q ? 'none' : ''; paginationNav.style.display = q ? 'none' : '';
}); });
} }