adds papers view

This commit is contained in:
Michael Beck 2025-04-01 23:02:19 +02:00
parent 59b6404b99
commit 7a41e531bd
4 changed files with 398 additions and 5 deletions

Binary file not shown.

View File

@ -0,0 +1,258 @@
{% extends "base.html" %}
{% block title %}Papers{% endblock %}
{% block content %}
{# --- Sort direction logic for each column --- #}
{% set title_sort = 'asc' if sort_by != 'title' or sort_dir == 'desc' else 'desc' %}
{% set journal_sort = 'asc' if sort_by != 'journal' or sort_dir == 'desc' else 'desc' %}
{% set doi_sort = 'asc' if sort_by != 'doi' or sort_dir == 'desc' else 'desc' %}
{% set issn_sort = 'asc' if sort_by != 'issn' or sort_dir == 'desc' else 'desc' %}
{% set status_sort = 'asc' if sort_by != 'status' or sort_dir == 'desc' else 'desc' %}
{% set created_sort = 'asc' if sort_by != 'created_at' or sort_dir == 'desc' else 'desc' %}
{% set updated_sort = 'asc' if sort_by != 'updated_at' or sort_dir == 'desc' else 'desc' %}
<form method="get" class="mb-4 row g-3">
<div class="col-md-2">
<label>Status</label>
<select name="status" class="form-select">
<option value="">All</option>
{% if request.args.get('status') == 'Pending' %}
<option value="Pending" selected>Pending</option>
{% else %}
<option value="Pending">Pending</option>
{% endif %}
{% if request.args.get('status') == 'Done' %}
<option value="Done" selected>Done</option>
{% else %}
<option value="Done">Done</option>
{% endif %}
{% if request.args.get('status') == 'Failed' %}
<option value="Failed" selected>Failed</option>
{% else %}
<option value="Failed">Failed</option>
{% endif %}
</select>
</div>
<div class="col-md-2">
<label>Created from</label>
<input type="date" name="created_from" class="form-control" value="{{ request.args.get('created_from', '') }}">
</div>
<div class="col-md-2">
<label>Created to</label>
<input type="date" name="created_to" class="form-control" value="{{ request.args.get('created_to', '') }}">
</div>
<div class="col-md-2">
<label>Updated from</label>
<input type="date" name="updated_from" class="form-control" value="{{ request.args.get('updated_from', '') }}">
</div>
<div class="col-md-2">
<label>Updated to</label>
<input type="date" name="updated_to" class="form-control" value="{{ request.args.get('updated_to', '') }}">
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Filter</button>
</div>
</form>
<div class="modal fade" id="paperDetailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content" id="paper-detail-content">
<!-- AJAX-loaded content will go here -->
</div>
</div>
</div>
<div class="d-flex align-items-center mb-4">
<!-- Statistics Section -->
<div class="me-auto">
<div class="list-group list-group-horizontal">
<div class="list-group-item d-flex justify-content-between align-items-center">
<strong>Total Papers</strong>
<span class="badge bg-primary rounded-pill">{{ total_papers }}</span>
</div>
{% for status, count in status_counts.items() %}
<div class="list-group-item d-flex justify-content-between align-items-center">
<strong>{{ status }}:</strong>
<span class="badge bg-primary rounded-pill">{{ count }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Pagination Section -->
<nav aria-label="Page navigation" class="mx-auto">
<ul class="pagination justify-content-center mb-0">
{% if pagination.has_prev %}
<li class="page-item">
{% set params = request.args.to_dict() %}
{% set _ = params.pop('page', None) %}
<a class="page-link" href="{{ url_for('main.list_papers', page=pagination.prev_num, **params) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&laquo;</span>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
{% set params = request.args.to_dict() %}
{% set _ = params.pop('page', None) %}
<a class="page-link" href="{{ url_for('main.list_papers', page=page_num, **params) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
{% set params = request.args.to_dict() %}
{% set _ = params.pop('page', None) %}
<a class="page-link" href="{{ url_for('main.list_papers', page=pagination.next_num, **params) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
<!-- Buttons Section -->
<div class="ms-auto">
<a href="{{ url_for('main.export_papers') }}" class="btn btn-outline-secondary">Export CSV</a>
</div>
</div>
<table class="table table-striped table-bordered table-smaller">
<thead>
<tr>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'title', 'sort_dir': title_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">Title</a>
</th>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'journal', 'sort_dir': journal_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">Journal</a>
</th>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'doi', 'sort_dir': doi_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">DOI</a>
</th>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'issn', 'sort_dir': issn_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">ISSN</a>
</th>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'status', 'sort_dir': status_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">Status</a>
</th>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'created_at', 'sort_dir': created_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">Created</a>
</th>
<th>
{% set params = request.args.to_dict() %}
{% set params = params.update({'sort_by': 'updated_at', 'sort_dir': updated_sort}) or params %}
<a href="{{ url_for('main.list_papers', **params) }}">Updated</a>
</th>
</tr>
</thead>
<tbody>
{% for paper in papers %}
<tr>
<td><a href="#" class="paper-link" data-url="{{ url_for('main.paper_detail', paper_id=paper.id) }}">{{ paper.title }}</a></td>
<td>{{ paper.journal }}</td>
<td>{{ paper.doi }}</td>
<td>{{ paper.issn }}</td>
<td>{{ paper.status }}</td>
<td>{{ paper.created_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td>{{ paper.updated_at.strftime('%Y-%m-%d %H:%M:%S') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
{% set params = request.args.to_dict() %}
{% set _ = params.pop('page', None) %}
<a class="page-link" href="{{ url_for('main.list_papers', page=pagination.prev_num, **params) }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&laquo;</span>
</li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=2, right_edge=2, left_current=2, right_current=2) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}">
{% set params = request.args.to_dict() %}
{% set _ = params.pop('page', None) %}
<a class="page-link" href="{{ url_for('main.list_papers', page=page_num, **params) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
{% set params = request.args.to_dict() %}
{% set _ = params.pop('page', None) %}
<a class="page-link" href="{{ url_for('main.list_papers', page=pagination.next_num, **params) }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% else %}
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">&raquo;</span>
</li>
{% endif %}
</ul>
</nav>
<script>
document.addEventListener("DOMContentLoaded", function () {
const modal = new bootstrap.Modal(document.getElementById('paperDetailModal'));
const content = document.getElementById('paper-detail-content');
document.querySelectorAll('.paper-link').forEach(link => {
link.addEventListener('click', function (e) {
e.preventDefault();
const url = this.getAttribute('data-url');
fetch(url)
.then(response => response.text())
.then(html => {
content.innerHTML = html;
modal.show();
})
.catch(err => {
content.innerHTML = '<div class="modal-body text-danger">Error loading details.</div>';
modal.show();
});
});
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,14 @@
<div class="modal-header">
<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 not key.startswith('_') and key != 'metadata' %}
<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>

View File

@ -1,9 +1,13 @@
from flask import Blueprint, render_template, current_app, request, flash, redirect, url_for from flask import Blueprint, render_template, current_app, request, flash, redirect, url_for, send_file
from .models import ScheduleConfig, VolumeConfig, PaperMetadata from .models import ScheduleConfig, VolumeConfig, PaperMetadata
from .db import db from .db import db
import pandas as pd import pandas as pd
from io import StringIO from io import StringIO
import codecs import codecs
import datetime
import io
import csv
from sqlalchemy import asc, desc
bp = Blueprint('main', __name__) bp = Blueprint('main', __name__)
@ -12,6 +16,7 @@ bp = Blueprint('main', __name__)
def index(): def index():
return render_template("index.html") return render_template("index.html")
REQUIRED_COLUMNS = {"alternative_id", "journal", "doi", "issn", "title"} REQUIRED_COLUMNS = {"alternative_id", "journal", "doi", "issn", "title"}
@ -53,7 +58,7 @@ def upload():
type=row.get('type'), type=row.get('type'),
language=row.get('language'), language=row.get('language'),
published_online=parse_date(row.get('published_online')), published_online=parse_date(row.get('published_online')),
status=None, status="New",
file_path=None, file_path=None,
error_msg=None error_msg=None
) )
@ -70,10 +75,126 @@ def upload():
return render_template('upload.html') return render_template('upload.html')
@bp.route('/papers')
def list_papers():
page = request.args.get('page', 1, type=int)
per_page = 50
@bp.route("/papers") # Filters
def papers(): status = request.args.get('status')
return render_template("papers.html", app_title="PaperScraper") created_from = request.args.get('created_from')
created_to = request.args.get('created_to')
updated_from = request.args.get('updated_from')
updated_to = request.args.get('updated_to')
sort_by = request.args.get('sort_by', 'created_at')
sort_dir = request.args.get('sort_dir', 'desc')
query = PaperMetadata.query
# Apply filters
if status:
query = query.filter(PaperMetadata.status == status)
def parse_date(val):
from datetime import datetime
try:
return datetime.strptime(val, '%Y-%m-%d')
except (ValueError, TypeError):
return None
if created_from := parse_date(created_from):
query = query.filter(PaperMetadata.created_at >= created_from)
if created_to := parse_date(created_to):
query = query.filter(PaperMetadata.created_at <= created_to)
if updated_from := parse_date(updated_from):
query = query.filter(PaperMetadata.updated_at >= updated_from)
if updated_to := parse_date(updated_to):
query = query.filter(PaperMetadata.updated_at <= updated_to)
# Sorting
sort_col = getattr(PaperMetadata, sort_by, PaperMetadata.created_at)
sort_func = desc if sort_dir == 'desc' else asc
query = query.order_by(sort_func(sort_col))
# Pagination
pagination = query.paginate(page=page, per_page=per_page, error_out=False)
# Statistics
total_papers = PaperMetadata.query.count()
status_counts = (
db.session.query(PaperMetadata.status, db.func.count(PaperMetadata.status))
.group_by(PaperMetadata.status)
.all()
)
status_counts = {status: count for status, count in status_counts}
return render_template(
'papers.html',
papers=pagination.items,
pagination=pagination,
total_papers=total_papers,
status_counts=status_counts,
sort_by=sort_by,
sort_dir=sort_dir,
)
@bp.route('/papers/export')
def export_papers():
query = PaperMetadata.query
# Filters
status = request.args.get('status')
created_from = request.args.get('created_from')
created_to = request.args.get('created_to')
updated_from = request.args.get('updated_from')
updated_to = request.args.get('updated_to')
sort_by = request.args.get('sort_by', 'created_at')
sort_dir = request.args.get('sort_dir', 'desc')
query = PaperMetadata.query
# Apply filters
if status:
query = query.filter(PaperMetadata.status == status)
def parse_date(val):
try:
return datetime.datetime.strptime(val, "%Y-%m-%d")
except Exception:
return None
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['ID', 'Title', 'Journal', 'DOI', 'ISSN',
'Status', 'Created At', 'Updated At'])
for paper in query:
writer.writerow([
paper.id,
paper.title,
getattr(paper, 'journal', ''),
paper.doi,
paper.issn,
paper.status,
paper.created_at,
paper.updated_at
])
output.seek(0)
return send_file(io.BytesIO(output.read().encode('utf-8')),
mimetype='text/csv',
as_attachment=True,
download_name='papers.csv')
from flask import jsonify, render_template
@bp.route('/papers/<int:paper_id>/detail')
def paper_detail(paper_id):
paper = PaperMetadata.query.get_or_404(paper_id)
return render_template('partials/paper_detail_modal.html', paper=paper)
@bp.route("/schedule", methods=["GET", "POST"]) @bp.route("/schedule", methods=["GET", "POST"])