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 .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"])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user