adds log simple viewer

This commit is contained in:
Michael Beck 2025-04-11 15:38:01 +02:00
parent f5b3716810
commit a2c7176385
6 changed files with 279 additions and 24 deletions

View File

@ -5,6 +5,7 @@ from .main import bp as main_bp
from .papers import bp as papers_bp
from .upload import bp as upload_bp
from .schedule import bp as schedule_bp
from .logger import bp as logger_bp
def register_blueprints(app: Flask):
@ -13,3 +14,4 @@ def register_blueprints(app: Flask):
app.register_blueprint(papers_bp, url_prefix='/papers')
app.register_blueprint(upload_bp, url_prefix='/upload')
app.register_blueprint(schedule_bp, url_prefix='/schedule')
app.register_blueprint(logger_bp, url_prefix='/logs')

View File

@ -0,0 +1,108 @@
"""Logger view."""
import csv
import io
import datetime
from flask import Blueprint, render_template, request, send_file
from ..db import db
from ..models import ActivityLog, ActivityCategory
bp = Blueprint("logger", __name__, url_prefix="/logs")
@bp.route("/")
def list_logs():
page = request.args.get("page", 1, type=int)
per_page = 50
# Filters
category = request.args.get("category")
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
search_term = request.args.get("search_term")
query = ActivityLog.query
if category:
query = query.filter(ActivityLog.category == category)
if start_date:
start_date_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(ActivityLog.timestamp >= start_date_dt)
if end_date:
end_date_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
query = query.filter(ActivityLog.timestamp <= end_date_dt)
if search_term:
query = query.filter(db.or_(
ActivityLog.action.contains(search_term),
ActivityLog.description.contains(search_term)
))
pagination = query.order_by(ActivityLog.timestamp.desc()).paginate(page=page, per_page=per_page, error_out=False)
categories = [e.value for e in ActivityCategory]
return render_template(
"logger.html.jinja",
logs=pagination.items,
pagination=pagination,
categories=categories,
category=category,
start_date=start_date,
end_date=end_date,
search_term=search_term,
app_title="PaperScraper",
)
@bp.route("/download")
def download_logs():
# Filters - reuse logic from list_logs
category = request.args.get("category")
start_date = request.args.get("start_date")
end_date = request.args.get("end_date")
search_term = request.args.get("search_term")
query = ActivityLog.query
if category:
query = query.filter(ActivityLog.category == category)
if start_date:
start_date_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d")
query = query.filter(ActivityLog.timestamp >= start_date_dt)
if end_date:
end_date_dt = datetime.datetime.strptime(end_date, "%Y-%m-%d") + datetime.timedelta(days=1)
query = query.filter(ActivityLog.timestamp <= end_date_dt)
if search_term:
query = query.filter(db.or_(
ActivityLog.action.contains(search_term),
ActivityLog.description.contains(search_term)
))
logs = query.order_by(ActivityLog.timestamp.desc()).all()
# Prepare CSV data
csv_data = io.StringIO()
csv_writer = csv.writer(csv_data)
csv_writer.writerow(["Timestamp", "Category", "Action", "Description", "Extra Data"]) # Header
for log in logs:
csv_writer.writerow([
log.timestamp,
log.category,
log.action,
log.description,
log.extra_data # Consider formatting this better
])
# Create response
filename = f"logs_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return send_file(
io.StringIO(csv_data.getvalue()),
mimetype="text/csv",
as_attachment=True,
download_name=filename
)
@bp.route("/<int:log_id>/detail")
def log_detail(log_id):
log = ActivityLog.query.get_or_404(log_id)
return render_template("partials/log_detail_modal.html.jinja", log=log)

View File

@ -0,0 +1,117 @@
{% extends "base.html" %}
{% block content %}
<h1>Activity Logs</h1>
<form method="get" class="mb-3">
<div class="row g-2">
<div class="col-md-3">
<label for="category" class="form-label">Category:</label>
<select name="category" id="category" class="form-select">
<option value="">All</option>
{% for cat in categories %}
<option value="{{ cat }}" {% if category==cat %}selected{% endif %}>{{ cat }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label for="start_date" class="form-label">Start Date:</label>
<input type="date" name="start_date" id="start_date" value="{{ start_date }}" class="form-control">
</div>
<div class="col-md-3">
<label for="end_date" class="form-label">End Date:</label>
<input type="date" name="end_date" id="end_date" value="{{ end_date }}" class="form-control">
</div>
<div class="col-md-3">
<label for="search_term" class="form-label">Search:</label>
<input type="text" name="search_term" id="search_term" value="{{ search_term }}" class="form-control">
</div>
</div>
<div class="mt-3">
<button type="submit" class="btn btn-primary">Filter</button>
<a href="{{ url_for('logger.download_logs', category=category, start_date=start_date, end_date=end_date, search_term=search_term) }}"
class="btn btn-secondary">Download CSV</a>
</div>
</form>
<ul class="list-group">
{% for log in logs %}
<li class="list-group-item log-item" data-log-id="{{ log.id }}">
<div class="d-flex justify-content-between align-items-center">
<div class="ms-2 me-auto">
<div class="fw-bold">{{ log.timestamp }}</div>
{{ log.action }} - {{ log.description }}
</div>
<span class="badge bg-primary rounded-pill">{{ log.category }}</span>
</div>
</li>
{% endfor %}
</ul>
{% if pagination %}
<nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link"
href="{{ url_for('logger.list_logs', page=pagination.prev_num, category=category, start_date=start_date, end_date=end_date, search_term=search_term) }}">Previous</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Previous</span>
</li>
{% endif %}
<li class="page-item disabled">
<span class="page-link">Page {{ pagination.page }} of {{ pagination.pages }}</span>
</li>
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link"
href="{{ url_for('logger.list_logs', page=pagination.next_num, category=category, start_date=start_date, end_date=end_date, search_term=search_term) }}">Next</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link">Next</span>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
<!-- Modal for log details -->
<div class="modal fade" id="logDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content" id="log-detail-content">
<!-- Log details will be loaded here via AJAX -->
</div>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const modal = new bootstrap.Modal(document.getElementById('logDetailModal'));
const content = document.getElementById('log-detail-content');
document.querySelectorAll('.log-item').forEach(item => {
item.addEventListener('click', function () {
const logId = this.getAttribute('data-log-id');
fetch(`/logs/${logId}/detail`)
.then(response => response.text())
.then(html => {
content.innerHTML = html;
modal.show();
})
.catch(err => {
content.innerHTML = '<div class="modal-body text-danger">Error loading log details.</div>';
modal.show();
});
});
});
});
</script>
{% endblock content %}

View File

@ -38,7 +38,7 @@
</a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li>
<a class="dropdown-item" href="/logs">Logs</a>
<a class="dropdown-item" href="{{ url_for('logger.list_logs') }}">Logs</a>
</li>
<li>
<a class="dropdown-item" href="/about">About</a>

View File

@ -0,0 +1,18 @@
<div class="modal-header">
<h5 class="modal-title">Log Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p><strong>Timestamp:</strong> {{ log.timestamp }}</p>
<p><strong>Category:</strong> {{ log.category }}</p>
<p><strong>Action:</strong> {{ log.action }}</p>
<p><strong>Description:</strong> {{ log.description }}</p>
{% if log.extra_data %}
<p><strong>Extra Data:</strong>
<pre><code>{{ log.extra_data }}</code></pre>
</p>
{% endif %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>

View File

@ -3,25 +3,35 @@
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
{% for key, value in paper.__dict__.items() %}
{% if key == 'doi' %}
<p><strong>DOI:</strong> <a href="https://doi.org/{{ value }}" target="_blank">{{ value }}</a></p>
{% elif key == 'issn' %}
{% if ',' in value %}
<p><strong>ISSN:</strong>
{% for key, value in paper.__dict__.items() %} {% if key == 'doi' %}
<p>
<strong>DOI:</strong>
<a href="https://doi.org/{{ value }}" target="_blank">{{ value }}</a>
</p>
{% elif key == 'issn' %} {% if ',' in value %}
<p>
<strong>ISSN:</strong>
{% for issn in value.split(',') %}
<a href="https://www.worldcat.org/search?q=issn:{{ issn.strip() }}" target="_blank">{{ issn.strip() }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
<a
href="https://www.worldcat.org/search?q=issn:{{ issn.strip() }}"
target="_blank"
>{{ issn.strip() }}</a
>{% if not loop.last %}, {% endif %} {% endfor %}
</p>
{% else %}
<p><strong>ISSN:</strong> <a href="https://www.worldcat.org/search?q=issn:{{ value }}" target="_blank">{{ value }}</a></p>
{% endif %}
{% endif %}
{% if not key.startswith('_') and key != 'metadata' and key != 'doi' and key != 'issn' %}
<p>
<strong>ISSN:</strong>
<a href="https://www.worldcat.org/search?q=issn:{{ value }}" target="_blank"
>{{ value }}</a
>
</p>
{% endif %} {% endif %} {% if not key.startswith('_') and key != 'metadata'
and key != 'doi' and key != 'issn' %}
<p><strong>{{ key.replace('_', ' ').capitalize() }}:</strong> {{ value }}</p>
{% endif %}
{% endfor %}
{% endif %} {% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
Close
</button>
</div>