was working, changes to displays
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
<!-- File: app/templates/core/base.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -119,11 +120,15 @@
|
||||
Orphaned Media
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('admin.list_invitations') }}">
|
||||
Invitations
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Plugins dropdown -->
|
||||
<div class="dropdown me-3">
|
||||
<a
|
||||
|
BIN
celerybeat-schedule
Normal file
BIN
celerybeat-schedule
Normal file
Binary file not shown.
@ -10,7 +10,7 @@ from sqlalchemy import func, desc
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
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.models import Plant
|
||||
from plugins.media.models import Media
|
||||
@ -26,9 +26,9 @@ def dashboard():
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
|
||||
now = datetime.utcnow()
|
||||
week_ago = now - timedelta(days=7)
|
||||
month_ago= now - timedelta(days=30)
|
||||
now = datetime.utcnow()
|
||||
week_ago = now - timedelta(days=7)
|
||||
month_ago = now - timedelta(days=30)
|
||||
|
||||
# ─── Overview metrics ────────────────────────────────────────────── #
|
||||
|
||||
@ -51,7 +51,7 @@ def dashboard():
|
||||
# Sign‐ups last 30 days
|
||||
signup_dates = [(month_ago + timedelta(days=i)).date() for i in range(31)]
|
||||
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
|
||||
]
|
||||
chart_dates = [d.strftime('%Y-%m-%d') for d in signup_dates]
|
||||
@ -64,7 +64,7 @@ def dashboard():
|
||||
|
||||
# ─── Analytics aggregates ─────────────────────────────────────────── #
|
||||
|
||||
ev_q = AnalyticsEvent.query.filter(AnalyticsEvent.timestamp >= week_ago)
|
||||
ev_q = AnalyticsEvent.query.filter(AnalyticsEvent.timestamp >= week_ago)
|
||||
total_ev = ev_q.count() or 1
|
||||
error_ev = ev_q.filter(AnalyticsEvent.status_code >= 500).count()
|
||||
error_pct = round(error_ev / total_ev * 100, 1)
|
||||
@ -96,10 +96,14 @@ def dashboard():
|
||||
)
|
||||
for ua, cnt in ua_counts:
|
||||
b = 'Other'
|
||||
if 'Chrome' in ua: b = 'Chrome'
|
||||
elif 'Firefox' in ua: b = 'Firefox'
|
||||
elif 'Safari' in ua and 'Chrome' not in ua: b = 'Safari'
|
||||
elif 'Edge' in ua: b = 'Edge'
|
||||
if 'Chrome' in ua:
|
||||
b = 'Chrome'
|
||||
elif 'Firefox' in ua:
|
||||
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
|
||||
|
||||
referrers = dict(
|
||||
@ -116,16 +120,14 @@ def dashboard():
|
||||
|
||||
# ─── Stats metrics ────────────────────────────────────────────────── #
|
||||
|
||||
# Plant totals
|
||||
total_plants = Plant.query.count()
|
||||
total_plants = Plant.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(
|
||||
Plant.plant_type,
|
||||
func.count(Plant.id).label('count')
|
||||
).filter(Plant.is_active==True) \
|
||||
).filter(Plant.is_active == True) \
|
||||
.group_by(Plant.plant_type) \
|
||||
.order_by(desc('count')) \
|
||||
.limit(5) \
|
||||
@ -340,7 +342,27 @@ def undelete_user(user_id):
|
||||
@bp.route('/orphaned-media')
|
||||
@login_required
|
||||
def orphaned_media_list():
|
||||
if not current_user.role == 'admin':
|
||||
abort(403)
|
||||
if current_user.role != 'admin':
|
||||
return "Access denied", 403
|
||||
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
|
||||
)
|
||||
|
40
plugins/admin/templates/admin/invitations/list.html
Normal file
40
plugins/admin/templates/admin/invitations/list.html
Normal 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 %}
|
@ -1,3 +1,4 @@
|
||||
{# File: plugins/admin/templates/admin/users/form.html #}
|
||||
{% extends 'core/base.html' %}
|
||||
{% block title %}{{ action }} User – Admin – Nature In Pots{% endblock %}
|
||||
|
||||
@ -37,6 +38,14 @@
|
||||
{{ form.excluded_from_analytics.label(class="form-check-label") }}
|
||||
</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>
|
||||
|
||||
<div class="mb-3">
|
||||
|
@ -1,3 +1,4 @@
|
||||
{# File: plugins/admin/templates/admin/users/list.html #}
|
||||
{% extends 'core/base.html' %}
|
||||
{% block title %}Users – Admin – Nature In Pots{% endblock %}
|
||||
{% block content %}
|
||||
@ -36,9 +37,15 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>Email</th><th>Role</th>
|
||||
<th>Verified</th><th>Excluded</th><th>Status</th>
|
||||
<th>Joined</th><th>Actions</th>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>Role</th>
|
||||
<th>Verified</th>
|
||||
<th>Excluded</th>
|
||||
<th>Status</th>
|
||||
<th>Joined</th>
|
||||
<th>Invites Remaining</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
@ -61,6 +68,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ u.created_at.strftime('%Y-%m-%d') }}</td>
|
||||
<td>{{ u.invites_remaining }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('admin.edit_user',
|
||||
user_id=u.id,
|
||||
@ -68,6 +76,13 @@
|
||||
show_deleted='1' if show_deleted else None,
|
||||
q=q) }}"
|
||||
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 %}
|
||||
<form action="{{ url_for('admin.undelete_user',
|
||||
user_id=u.id,
|
||||
@ -168,7 +183,6 @@
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
{# --- AJAX search script --- #}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
@ -218,15 +232,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<td>${u.excluded_from_analytics ? '✓' : ''}</td>
|
||||
<td>${statusHtml}</td>
|
||||
<td>${u.created_at}</td>
|
||||
<td>${u.invites_remaining}</td>
|
||||
<td>
|
||||
<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}
|
||||
</td>
|
||||
</tr>
|
||||
`);
|
||||
});
|
||||
|
||||
// hide pagination when searching
|
||||
paginationNav.style.display = q ? 'none' : '';
|
||||
});
|
||||
}
|
||||
|
Reference in New Issue
Block a user