adds log simple viewer
This commit is contained in:
parent
f5b3716810
commit
a2c7176385
@ -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):
|
||||
@ -12,4 +13,5 @@ def register_blueprints(app: Flask):
|
||||
app.register_blueprint(main_bp)
|
||||
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(schedule_bp, url_prefix='/schedule')
|
||||
app.register_blueprint(logger_bp, url_prefix='/logs')
|
108
scipaperloader/blueprints/logger.py
Normal file
108
scipaperloader/blueprints/logger.py
Normal 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)
|
117
scipaperloader/templates/logger.html.jinja
Normal file
117
scipaperloader/templates/logger.html.jinja
Normal 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 %}
|
@ -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>
|
||||
|
@ -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>
|
@ -1,27 +1,37 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ paper.title }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
<h5 class="modal-title">{{ paper.title }}</h5>
|
||||
<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 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 %}
|
||||
</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>{{ key.replace('_', ' ').capitalize() }}:</strong> {{ value }}</p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
</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>{{ key.replace('_', ' ').capitalize() }}:</strong> {{ value }}</p>
|
||||
{% endif %} {% endfor %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user