-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathservices.py
More file actions
186 lines (167 loc) · 6.56 KB
/
Copy pathservices.py
File metadata and controls
186 lines (167 loc) · 6.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
"""
Database logic for WG21 Paper Tracker.
"""
from __future__ import annotations
from datetime import date
from typing import TYPE_CHECKING, Optional
from django.db import IntegrityError, transaction
from cppa_user_tracker.services import get_or_create_wg21_paper_author_profile
from wg21_paper_tracker.models import WG21Mailing, WG21Paper, WG21PaperAuthor
if TYPE_CHECKING:
from cppa_user_tracker.models import WG21PaperAuthorProfile
def _normalize_year(year: int | str | None) -> int:
"""Return a 4-digit year as int, or 0 if missing/invalid."""
if year is None:
return 0
if isinstance(year, int):
return year if 0 < year <= 9999 else 0
s = str(year).strip()[:4]
return int(s) if s.isdigit() else 0
@transaction.atomic
def get_or_create_mailing(mailing_date: str, title: str) -> tuple[WG21Mailing, bool]:
mailing, created = WG21Mailing.objects.get_or_create(
mailing_date=mailing_date, defaults={"title": title}
)
if not created and mailing.title != title:
mailing.title = title
mailing.save(update_fields=["title", "updated_at"])
return mailing, created
def get_or_create_paper(
paper_id: str,
url: str,
title: str,
document_date: date | None,
mailing: WG21Mailing,
subgroup: str = "",
author_names: Optional[list[str]] = None,
author_emails: Optional[list[str]] = None,
year: int | None = None,
) -> tuple[WG21Paper, bool]:
paper_id = (paper_id or "").strip().lower()
if not paper_id:
raise ValueError("paper_id is required")
year_val = _normalize_year(year)
def _update_paper(paper: WG21Paper) -> bool:
updated = False
if paper.url != url:
paper.url = url
updated = True
if paper.title != title:
paper.title = title
updated = True
if paper.document_date != document_date:
paper.document_date = document_date
updated = True
if paper.mailing_id != mailing.id:
paper.mailing = mailing
updated = True
if paper.subgroup != subgroup:
paper.subgroup = subgroup
updated = True
if paper.year != year_val:
paper.year = year_val
updated = True
if updated:
paper.save()
return updated
try:
with transaction.atomic():
if year_val > 0:
# Prefer exact (paper_id, year); else promote placeholder (paper_id, 0) to real year
paper = WG21Paper.objects.filter(
paper_id=paper_id, year=year_val
).first()
if paper:
_update_paper(paper)
created = False
else:
placeholder = WG21Paper.objects.filter(
paper_id=paper_id, year=0
).first()
if placeholder:
try:
placeholder.url = url
placeholder.title = title
placeholder.document_date = document_date
placeholder.mailing = mailing
placeholder.subgroup = subgroup
placeholder.year = year_val
placeholder.save()
paper = placeholder
created = False
except IntegrityError:
raise # Roll back this transaction; recovery runs below
else:
paper, created = WG21Paper.objects.get_or_create(
paper_id=paper_id,
year=year_val,
defaults={
"url": url,
"title": title,
"document_date": document_date,
"mailing": mailing,
"subgroup": subgroup,
},
)
else:
paper, created = WG21Paper.objects.get_or_create(
paper_id=paper_id,
year=0,
defaults={
"url": url,
"title": title,
"document_date": document_date,
"mailing": mailing,
"subgroup": subgroup,
},
)
if not created:
_update_paper(paper)
except IntegrityError:
# Placeholder promotion hit (paper_id, year_val) unique constraint; fetch and update canonical row
with transaction.atomic():
paper = WG21Paper.objects.filter(paper_id=paper_id, year=year_val).first()
if not paper:
raise
_update_paper(paper)
created = False
if author_names:
if not created:
for author in paper.authors.all():
author.delete()
emails = author_emails or []
for i, name in enumerate(author_names):
email = emails[i] if i < len(emails) else None
profile, _ = get_or_create_wg21_paper_author_profile(name, email=email)
get_or_create_paper_author(paper, profile, i + 1)
return paper, created
def get_or_create_paper_author(
paper: WG21Paper,
profile: WG21PaperAuthorProfile,
author_order: int,
) -> tuple[WG21PaperAuthor, bool]:
"""Get or create a WG21PaperAuthor link for (paper, profile), with author_order (1-based).
Updates author_order on existing link if it differs.
"""
if not isinstance(author_order, int) or author_order <= 0:
raise ValueError("author_order must be a positive integer")
link, link_created = WG21PaperAuthor.objects.get_or_create(
paper=paper,
profile=profile,
defaults={"author_order": author_order},
)
if not link_created and link.author_order != author_order:
link.author_order = author_order
link.save(update_fields=["author_order"])
return link, link_created
def mark_paper_downloaded(paper_id: str, year: int | None = None):
paper_id = (paper_id or "").strip().lower()
if not paper_id:
raise ValueError("paper_id is required")
if year is None:
raise ValueError("year is required; pass 0 explicitly for placeholder papers")
year_val = _normalize_year(year)
WG21Paper.objects.filter(
paper_id=paper_id,
year=year_val,
).update(is_downloaded=True)