adds papers view
This commit is contained in:
parent
59b6404b99
commit
7a41e531bd
Binary file not shown.
258
scipaperloader/templates/papers.html
Normal file
258
scipaperloader/templates/papers.html
Normal 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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">«</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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">»</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">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">«</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">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="page-item disabled">
|
||||
<span class="page-link" aria-hidden="true">»</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 %}
|
14
scipaperloader/templates/partials/paper_detail_modal.html
Normal file
14
scipaperloader/templates/partials/paper_detail_modal.html
Normal 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>
|
@ -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 .db import db
|
||||
import pandas as pd
|
||||
from io import StringIO
|
||||
import codecs
|
||||
import datetime
|
||||
import io
|
||||
import csv
|
||||
from sqlalchemy import asc, desc
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
@ -12,6 +16,7 @@ bp = Blueprint('main', __name__)
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
REQUIRED_COLUMNS = {"alternative_id", "journal", "doi", "issn", "title"}
|
||||
|
||||
|
||||
@ -53,7 +58,7 @@ def upload():
|
||||
type=row.get('type'),
|
||||
language=row.get('language'),
|
||||
published_online=parse_date(row.get('published_online')),
|
||||
status=None,
|
||||
status="New",
|
||||
file_path=None,
|
||||
error_msg=None
|
||||
)
|
||||
@ -70,10 +75,126 @@ def upload():
|
||||
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")
|
||||
def papers():
|
||||
return render_template("papers.html", app_title="PaperScraper")
|
||||
# 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):
|
||||
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"])
|
||||
|
Loading…
x
Reference in New Issue
Block a user