Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies = [
"filelock>=3.20.0",
"jinja2>=3.1.6",
"easy-oauth",
"markdown>=3.10",
]

[project.urls]
Expand Down
1 change: 1 addition & 0 deletions src/paperoni/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Server:
max_results: int = 10000
process_pool_executor: dict = field(default_factory=dict)
auth: OAuthManager = None
assets: Path = None

def __post_init__(self):
if self.process_pool_executor.get("max_workers", 0) == 0:
Expand Down
26 changes: 26 additions & 0 deletions src/paperoni/web/assets/help.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

Welcome to Paperoni! Paperoni is a tool for paper management.

# Search {: #search}

The search interface allows searching through Paperoni's database of papers. It will search as you type. Results are sorted so that the most recent publications appear on top.

* **Title**: Search by paper title.
* **Author**: Search by author. It is currently not possible to search for multiple authors, nor by author affiliation. We will likely add this functionality in the future.
* **Venue**: Search by venue. Venue aliases are not properly taken into account yet, which means that you may need to search for "Neural Information Processing Systems" instead of "NeurIPS" or vice versa.
* **Start/end date**: Search for a paper that had any release between the start and end date. For example, an article with a preprint in 2022 and a publication in 2023 will appear both in 2022 and in 2023.
* **Validated/Invalidated/Not processed/All**: Only select papers that were manually validated/not yet validated or invalidated/all papers regardless of validation.

## Exporting search results

Search results can be downloaded as JSON or CSV files. Simply click on the "JSON" or "CSV" button. CSV files can be [imported in Google Sheets](https://blog.golayer.io/google-sheets/import-csv-to-google-sheets) or in Excel.

# Validation {: #validation}

The validation interface allows the validation or invalidation of papers, in order to make the database cleaner.

# Editing papers {: #edit}

The edition interface lets you change all data associated to papers.

Note that the validation interface sets/unsets the valid/invalid flags, so it is possible to do it within the validation interface, it is just less straightforward.
9 changes: 9 additions & 0 deletions src/paperoni/web/assets/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

Welcome to Paperoni.

- [🔍 Search papers](/search): Find and filter scientific publications by title, author, venue, year, excerpt, and more.
- [✅ Validate papers](/validate): Classify which papers are valid according to your criteria, using interactive validation tools.
- [❓ Read the help](/help): Consult full documentation and best practices.
- [📝 API documentation](/docs): For how to work with Paperoni programmatically.

These features are available to authorized users according to your account permissions. If you need help or want to learn more, visit the [help page](/help).
176 changes: 176 additions & 0 deletions src/paperoni/web/assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,182 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}

/* Main Header Styles */
.main-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 40px;
background: #f9f9f9;
border-bottom: 3px solid #2c5aa0;
}

.header-left {
flex: 0 0 auto;
display: flex;
align-items: center;
gap: 12px;
}

.page-title {
margin: 0;
color: #333;
font-weight: 600;
}

.help-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
background: #2c5aa0;
color: white;
text-decoration: none;
border-radius: 50%;
font-size: 24px;
font-weight: bold;
transition: background-color 0.2s, transform 0.2s;
}

.help-link:hover {
background: #1e3a6f;
transform: scale(1.1);
}

.header-right {
display: flex;
align-items: center;
gap: 20px;
}

.header-logo {
height: 50px;
width: auto;
}

.header-nav {
display: flex;
flex-direction: column;
}

.nav-link {
color: #2c5aa0;
text-decoration: none;
font-size: 18px;
font-weight: 500;
padding: 2px 8px;
border-radius: 3px;
transition: background-color 0.2s;
}

.nav-link:hover {
background-color: #e9ecef;
text-decoration: underline;
}

/* Markdown Content Container */
.markdown-container {
padding: 40px;
max-width: 900px;
margin: 0 auto;
line-height: 1.6;
}

.markdown-container h1 {
color: #333;
border-bottom: 2px solid #2c5aa0;
padding-bottom: 10px;
margin-top: 30px;
margin-bottom: 20px;
}

.markdown-container h2 {
color: #333;
margin-top: 25px;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
padding-bottom: 5px;
}

.markdown-container h3 {
color: #555;
margin-top: 20px;
margin-bottom: 10px;
}

.markdown-container p {
margin-bottom: 15px;
color: #444;
}

.markdown-container a {
color: #2c5aa0;
text-decoration: none;
}

.markdown-container a:hover {
text-decoration: underline;
}

.markdown-container ul,
.markdown-container ol {
margin-bottom: 15px;
padding-left: 30px;
}

.markdown-container li {
margin-bottom: 8px;
}

.markdown-container code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: "Courier New", monospace;
font-size: 0.9em;
}

.markdown-container pre {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
border: 1px solid #ddd;
}

.markdown-container pre code {
background: none;
padding: 0;
}

.markdown-container blockquote {
border-left: 4px solid #2c5aa0;
padding-left: 15px;
margin-left: 0;
color: #666;
font-style: italic;
}

.markdown-container table {
border-collapse: collapse;
width: 100%;
margin-bottom: 15px;
}

.markdown-container table th,
.markdown-container table td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}

.markdown-container table th {
background: #f5f5f5;
font-weight: 600;
}

/* Report List Container */
Expand Down
10 changes: 2 additions & 8 deletions src/paperoni/web/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,19 @@
FastAPI route for editing papers with a web interface.
"""

from pathlib import Path

from fastapi import Depends, FastAPI, Request
from fastapi.templating import Jinja2Templates

here = Path(__file__).parent
from .helpers import render_template


def install_edit(app: FastAPI) -> FastAPI:
"""Install the edit web interface route."""

hascap = app.auth.get_email_capability
templates = Jinja2Templates(directory=str((here / "templates").resolve()))

@app.get("/edit/{paper_id}", dependencies=[Depends(hascap("validate"))])
async def edit_page(request: Request, paper_id: int):
"""Render the paper edit page."""
return templates.TemplateResponse(
"edit.html", {"request": request, "paper_id": paper_id}
)
return render_template("edit.html", request, paper_id=paper_id)

return app
60 changes: 60 additions & 0 deletions src/paperoni/web/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Helper functions for web routes.
"""

from functools import cache
from pathlib import Path

from fastapi import Request
from fastapi.templating import Jinja2Templates

from ..config import config

here = Path(__file__).parent


@cache
def templates():
return Jinja2Templates(directory=str((here / "templates").resolve()))


def render_template(
template_name: str,
request: Request,
help_section: str | bool = True,
**kwargs,
):
"""
Render a template with standard context variables.

Args:
template_name: Name of the template file
request: FastAPI Request object
**kwargs: Additional context variables to pass to the template

Returns:
TemplateResponse
"""
# Check for logo and custom CSS
has_logo = False
has_custom_css = False

if config.server.assets:
custom_assets_path = Path(config.server.assets)
has_logo = (custom_assets_path / "logo.png").exists()
has_custom_css = (custom_assets_path / "style.css").exists()

logged_in = request.session.get("user", None) is not None
if help_section is True:
help_section = template_name.split(".")[0]

context = {
"request": request,
"has_logo": has_logo,
"has_custom_css": has_custom_css,
"logged_in": logged_in,
"help_section": help_section,
**kwargs,
}

return templates().TemplateResponse(template_name, context)
6 changes: 6 additions & 0 deletions src/paperoni/web/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from ..config import config
from .edit import install_edit
from .pages import install_pages
from .reports import install_reports
from .restapi import install_api
from .search import install_search
Expand Down Expand Up @@ -43,8 +44,13 @@ async def exception_handler(request, exc):

app.mount("/assets", StaticFiles(directory=(here / "assets")), name="assets")

# Mount custom assets if configured
if config.server.assets and Path(config.server.assets).exists():
app.mount("/custom", StaticFiles(directory=config.server.assets), name="custom")

install_api(app)
install_reports(app)
install_search(app)
install_edit(app)
install_pages(app)
return app
Loading
Loading