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
2 changes: 1 addition & 1 deletion app/email_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def render_change_block(change: SimplifiedNotification, user_name: str) -> tuple
def render_email(task: EmailTask, sub: SubEmailData, ids_to_contact: dict[int, UserContact]) -> tuple[str, str]:
sub_text, sub_html = render_submission_block(sub)
change_texts, change_htmls = [], []
for change in sorted(task.notifications.changes, key=lambda c: c.time, reverse=True):
for change in sorted(task.notifications.changes, key=lambda c: c.time):
contact = ids_to_contact.get(change.user_id)
name = contact.display_name if contact else f"user {change.user_id}"
ct, ch = render_change_block(change, name)
Expand Down
2 changes: 1 addition & 1 deletion app/templates/email_body.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
MOD_HUB_TITLE = "Moderator Hub"

_FOOTER_LINE1 = "This email was generated by the moderator email system version 2.0"
_FOOTER_LINE2 = "Some of your arXiv moderation emails may look different. We are in the process of transitioning email systems."
_FOOTER_LINE2 = "Some of your arXiv moderation emails may look different. Throughout 2026, we will be transitioning email systems."


def render_body(
Expand Down
10 changes: 9 additions & 1 deletion app/templates/submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@

CHECK_SUBMISSION_URL = "https://check.arxiv.org/submit/{submission_id}"
_ET = ZoneInfo(arxiv_settings.ARXIV_BUSINESS_TZ)
MAX_AUTHORS = 15


def truncate_authors(authors_str: str) -> str:
parts = [a.strip() for a in authors_str.split(",")]
if len(parts) > MAX_AUTHORS:
return ", ".join(parts[:MAX_AUTHORS]) + ", ..."
return ", ".join(parts)


def render_submission_block(sub: SubEmailData) -> tuple[str, str]:
status_label = STATUS_NAMES.get(sub.status, str(sub.status))
cat_list = sub.submission_categories or "(none)"
check_url = CHECK_SUBMISSION_URL.format(submission_id=sub.submission_id)
title = sub.title or "(no title)"
authors = sub.authors or "(no authors)"
authors = truncate_authors(sub.authors) if sub.authors else "(no authors)"

submit_time_str = (
sub.submit_time.astimezone(_ET).strftime("%Y-%m-%d %H:%M %Z")
Expand Down
3 changes: 3 additions & 0 deletions cicd/cloudbuild.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# this can be used to update the cloud run job
# for the initial creation use the same template but switch update command with create

options:
logging: CLOUD_LOGGING_ONLY

Expand Down
35 changes: 31 additions & 4 deletions tests/test_email_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@

from app.schema import SimplifiedNotification, CommentData, PromoteData, NewPropData, PropRespData
from app.schema import SubEmailData
from app.email_content import get_submission_info, _build_category_string, render_change_block
from app.email_content import get_submission_info, _build_category_string, render_change_block, render_email
from app.schema import EmailTask, ConsolidatedNotifications
from app.templates.comment import render_comment_block
from app.templates.promote import render_promote_block
from app.templates.new_prop import render_new_prop_block
from app.templates.prop_resp import render_prop_resp_block
from app.templates.submission import render_submission_block
from app.templates.submission import render_submission_block, truncate_authors, MAX_AUTHORS
from app.templates.email_body import render_body, CHECK_GUIDE_URL, HOW_TO_MOD_URL, MOD_HUB_URL

_TIME = datetime(2024, 6, 15, 14, 30, tzinfo=timezone.utc)
Expand Down Expand Up @@ -110,6 +111,21 @@ def _mock_submission(submission_id=123, title="ML Paper", authors="Alice, Bob",
submit_time=submit_time,
)

def test_truncate_authors_under_limit():
authors = ", ".join(f"Author {i}" for i in range(MAX_AUTHORS - 1))
assert truncate_authors(authors) == authors

def test_truncate_authors_at_limit():
authors = ", ".join(f"Author {i}" for i in range(MAX_AUTHORS))
assert truncate_authors(authors) == authors
assert "..." not in truncate_authors(authors)

def test_truncate_authors_over_limit():
parts = [f"Author {i}" for i in range(MAX_AUTHORS + 5)]
result = truncate_authors(", ".join(parts))
assert result.endswith(", ...")
assert result.count(",") == MAX_AUTHORS # 14 between names + 1 before ellipsis

def test_render_submission_block():
sub = _mock_submission()
text, html_out = render_submission_block(sub)
Expand Down Expand Up @@ -161,6 +177,17 @@ def test_render_email_contains_all_sections_and_footer():
assert url in body_html, f"missing {url} in html"


def test_render_email_changes_oldest_first():
t_old = datetime(2024, 6, 15, 14, 30, tzinfo=timezone.utc)
t_new = datetime(2024, 6, 15, 14, 32, tzinfo=timezone.utc)
older = SimplifiedNotification(time=t_old, user_id=1, data=CommentData(comment="older comment"))
newer = SimplifiedNotification(time=t_new, user_id=1, data=CommentData(comment="newer comment"))
notifications = ConsolidatedNotifications(submission_id=123, changes=[newer, older])
task = EmailTask(submission_id=123, to_emails=[], notifications=notifications)
text, _ = render_email(task, _mock_submission(), {})
assert text.index("older comment") < text.index("newer comment")


# ── exact output tests ────────────────────────────────────────────────────────
#will need to be updated whenever format changes
#feel free to delete if too annoying, but its kind of nice to see the whole output
Expand Down Expand Up @@ -321,7 +348,7 @@ def test_full_email_exact_text():
f"Moderator Hub: {_HUB_URL} \n"
"\n"
"This email was generated by the moderator email system version 2.0\n"
"Some of your arXiv moderation emails may look different. We are in the process of transitioning email systems.\n"
"Some of your arXiv moderation emails may look different. Throughout 2026, we will be transitioning email systems.\n"
)


Expand Down Expand Up @@ -352,7 +379,7 @@ def test_full_email_exact_html():
f"<a href=\"{_MOD_URL}\">How to moderate</a> | "
f"<a href=\"{_HUB_URL}\">Moderator Hub</a></p>\n"
"<p>This email was generated by the moderator email system version 2.0<br>\n"
"Some of your arXiv moderation emails may look different. We are in the process of transitioning email systems.</p>\n"
"Some of your arXiv moderation emails may look different. Throughout 2026, we will be transitioning email systems.</p>\n"
)


Expand Down
8 changes: 4 additions & 4 deletions tests/test_process_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ def test_all_successful_sends_all_acked():
assert "ack-3" in acked # parse failure — acked immediately

@pytest.mark.usefixtures("db_session")
def test_changes_ordered_newest_first():
def test_changes_ordered_oldest_first():
older = {
"time": "2024-01-01T10:00:00Z",
"submission_id": 123,
Expand All @@ -357,7 +357,7 @@ def test_changes_ordered_newest_first():
"action": "comment-added",
"data": {"comment": "second comment"}
}
# messages arrive in chronological order; rendered email should show newest first
# messages arrive in reverse order; rendered email should show oldest first (newest on bottom)
msg1 = _make_pubsub_message("ack-1", older)
msg2 = _make_pubsub_message("ack-2", newer)

Expand All @@ -367,14 +367,14 @@ def test_changes_ordered_newest_first():

assert mock_send.call_count == 1
body = mock_send.call_args.kwargs["body"]
assert body.index("01-02 05:00 EST") < body.index("01-01 05:00 EST")
assert body.index("01-01 05:00 EST") < body.index("01-02 05:00 EST")

with patch("app.process.send_email", mock_send):
process_messages([msg2, msg1], ack_fn=Mock())

assert mock_send.call_count == 2
body = mock_send.call_args.kwargs["body"]
assert body.index("01-02 05:00 EST") < body.index("01-01 05:00 EST")
assert body.index("01-01 05:00 EST") < body.index("01-02 05:00 EST")

@pytest.mark.usefixtures("db_session")
def test_subject_uses_paper_categories():
Expand Down
Loading