Skip to content

Commit a59f0bb

Browse files
Rossi-Lucianoclaude
andcommitted
[MarkAPI-122] feat: add PubMed/PMC XML generation for XML documents
Add models, tasks, admin views and forms to upload an XML document and convert it to PubMed/PMC XML formats independently of other workflows, plus a "Last error" column in the XML Documents list to surface conversion errors. Also fixes a double-serialization bug where generate_pubmed_for_xml_document/generate_pmc_for_xml_document wrote xml_utils.tostring(...) over strings already returned by packtools' pipeline_pubmed/pipeline_pmc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 23af48d commit a59f0bb

11 files changed

Lines changed: 623 additions & 4 deletions

File tree

markup_doc/wagtail_hooks.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
SPSPackageValidationSnippetViewSet,
3535
XMLDocumentHTMLSnippetViewSet,
3636
XMLDocumentPDFSnippetViewSet,
37+
XMLDocumentPMCSnippetViewSet,
38+
XMLDocumentPubMedSnippetViewSet,
39+
XMLDocumentSnippetViewSet,
3740
)
3841

3942

@@ -226,8 +229,11 @@ class XMLSPSSnippetViewSetGroup(SnippetViewSetGroup):
226229
MarkupXMLViewSet,
227230
SPSPackageValidationSnippetViewSet,
228231
ProcessedDocxViewSet,
232+
XMLDocumentSnippetViewSet,
229233
XMLDocumentPDFSnippetViewSet,
230234
XMLDocumentHTMLSnippetViewSet,
235+
XMLDocumentPubMedSnippetViewSet,
236+
XMLDocumentPMCSnippetViewSet,
231237
)
232238

233239

tracker/choices.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
XML_DOCUMENT_CONVERSION_TO_HTML_ERROR = "CHE"
3636
XML_DOCUMENT_CONVERSION_TO_PDF_ERROR = "CPE"
3737
XML_DOCUMENT_CONVERSION_TO_TEX_ERROR = "CTE"
38+
XML_DOCUMENT_CONVERSION_TO_PUBMED_ERROR = "PME"
39+
XML_DOCUMENT_CONVERSION_TO_PMC_ERROR = "PCE"
3840
XML_DOCUMENT_UNKNOWN_ERROR = "UNE"
3941

4042
XML_DOCUMENT_EVENT = [
@@ -44,5 +46,7 @@
4446
(XML_DOCUMENT_CONVERSION_TO_HTML_ERROR, _("XML Conversion to HTML Error")),
4547
(XML_DOCUMENT_CONVERSION_TO_PDF_ERROR, _("XML Conversion to PDF Error")),
4648
(XML_DOCUMENT_CONVERSION_TO_TEX_ERROR, _("XML Conversion to TEX Error")),
49+
(XML_DOCUMENT_CONVERSION_TO_PUBMED_ERROR, _("XML Conversion to PubMed Error")),
50+
(XML_DOCUMENT_CONVERSION_TO_PMC_ERROR, _("XML Conversion to PMC Error")),
4751
(XML_DOCUMENT_UNKNOWN_ERROR, _("Unknown Error")),
4852
]

xml_manager/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,11 @@ class XML_File_PDF_Generation_Error(Exception):
1313
class XML_File_HTML_Generation_Error(Exception):
1414
pass
1515

16+
class XML_File_PubMed_Generation_Error(Exception):
17+
pass
18+
19+
class XML_File_PMC_Generation_Error(Exception):
20+
pass
21+
1622
class SPS_Package_Validation_Error(Exception):
1723
pass

xml_manager/forms.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from django.utils.translation import gettext_lazy as _
66
from wagtail.admin.forms import WagtailAdminModelForm
77

8-
from xml_manager.models import SPSPackageValidation
8+
from xml_manager.models import SPSPackageValidation, XMLDocument
99

1010

1111
class SPSPackageValidationForm(WagtailAdminModelForm):
@@ -57,3 +57,22 @@ def save_wagtail_document_from_path(file_path, title=None):
5757
document = Document(title=document_title)
5858
document.file.save(basename, File(fp), save=True)
5959
return document
60+
61+
62+
class XMLConvertUploadForm(WagtailAdminModelForm):
63+
xml_upload = forms.FileField(
64+
label=_("XML file"),
65+
help_text=_("Upload an XML file (SciELO Publishing Schema) to convert."),
66+
)
67+
68+
class Meta:
69+
model = XMLDocument
70+
fields = []
71+
72+
def clean_xml_upload(self):
73+
xml_upload = self.cleaned_data["xml_upload"]
74+
if not xml_upload.name.lower().endswith(".xml"):
75+
raise ValidationError(_("Only .xml files are allowed."))
76+
if xml_upload.size == 0:
77+
raise ValidationError(_("The file is empty."))
78+
return xml_upload
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Generated by Django 6.0.5 on 2026-06-11 14:25
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('xml_manager', '0004_spspackagevalidation'),
11+
]
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name='XMLDocumentPMC',
16+
fields=[
17+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+
('pmc_file', models.FileField(upload_to='xml_manager/pmc/', verbose_name='PMC XML File')),
19+
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='Uploaded At')),
20+
('xml_document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pmcs', to='xml_manager.xmldocument', verbose_name='XML Document')),
21+
],
22+
options={
23+
'verbose_name': 'XML Document PMC',
24+
'verbose_name_plural': 'XML Document PMCs',
25+
},
26+
),
27+
migrations.CreateModel(
28+
name='XMLDocumentPubMed',
29+
fields=[
30+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
31+
('pubmed_file', models.FileField(upload_to='xml_manager/pubmed/', verbose_name='PubMed XML File')),
32+
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='Uploaded At')),
33+
('xml_document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pubmeds', to='xml_manager.xmldocument', verbose_name='XML Document')),
34+
],
35+
options={
36+
'verbose_name': 'XML Document PubMed',
37+
'verbose_name_plural': 'XML Document PubMeds',
38+
},
39+
),
40+
]

xml_manager/models.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,58 @@ def create(cls, xml_document, html_file, language="pt"):
122122
return html_instance
123123

124124

125+
class XMLDocumentPubMed(models.Model):
126+
xml_document = models.ForeignKey(
127+
XMLDocument,
128+
on_delete=models.CASCADE,
129+
related_name="pubmeds",
130+
verbose_name=_("XML Document"),
131+
)
132+
pubmed_file = models.FileField(
133+
upload_to="xml_manager/pubmed/", verbose_name=_("PubMed XML File")
134+
)
135+
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Uploaded At"))
136+
137+
def __str__(self):
138+
return f"PubMed XML for {self.xml_document.xml_file.name}"
139+
140+
class Meta:
141+
verbose_name = _("XML Document PubMed")
142+
verbose_name_plural = _("XML Document PubMeds")
143+
144+
@classmethod
145+
def create(cls, xml_document, pubmed_file):
146+
instance = cls(xml_document=xml_document, pubmed_file=pubmed_file)
147+
instance.save()
148+
return instance
149+
150+
151+
class XMLDocumentPMC(models.Model):
152+
xml_document = models.ForeignKey(
153+
XMLDocument,
154+
on_delete=models.CASCADE,
155+
related_name="pmcs",
156+
verbose_name=_("XML Document"),
157+
)
158+
pmc_file = models.FileField(
159+
upload_to="xml_manager/pmc/", verbose_name=_("PMC XML File")
160+
)
161+
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Uploaded At"))
162+
163+
def __str__(self):
164+
return f"PMC XML for {self.xml_document.xml_file.name}"
165+
166+
class Meta:
167+
verbose_name = _("XML Document PMC")
168+
verbose_name_plural = _("XML Document PMCs")
169+
170+
@classmethod
171+
def create(cls, xml_document, pmc_file):
172+
instance = cls(xml_document=xml_document, pmc_file=pmc_file)
173+
instance.save()
174+
return instance
175+
176+
125177
class SPSPackageValidation(models.Model):
126178
package_document = models.OneToOneField(
127179
"wagtaildocs.Document",

xml_manager/tasks.py

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
XML_DOCUMENT_CONVERSION_TO_DOCX_ERROR,
1313
XML_DOCUMENT_CONVERSION_TO_HTML_ERROR,
1414
XML_DOCUMENT_CONVERSION_TO_PDF_ERROR,
15+
XML_DOCUMENT_CONVERSION_TO_PMC_ERROR,
16+
XML_DOCUMENT_CONVERSION_TO_PUBMED_ERROR,
1517
XML_DOCUMENT_PARSING_ERROR,
1618
XML_DOCUMENT_UNKNOWN_ERROR,
1719
XML_DOCUMENT_VALIDATION_ERROR,
@@ -25,6 +27,8 @@
2527
XMLDocument,
2628
XMLDocumentHTML,
2729
XMLDocumentPDF,
30+
XMLDocumentPMC,
31+
XMLDocumentPubMed,
2832
)
2933

3034
User = get_user_model()
@@ -33,7 +37,7 @@
3337
def _get_user(request, username=None, user_id=None):
3438
try:
3539
return User.objects.get(pk=request.user_id)
36-
except AttributeError:
40+
except (AttributeError, User.DoesNotExist):
3741
if user_id:
3842
return User.objects.get(pk=user_id)
3943
if username:
@@ -52,6 +56,8 @@ def task_process_xml_document(self, xml_id, user_id=None, username=None):
5256
task_validate_xml_file.delay(xml_id, user_id=user_id, username=username)
5357
task_generate_pdf_file.delay(xml_id, user_id=user_id, username=username)
5458
task_generate_html_file.delay(xml_id, user_id=user_id, username=username)
59+
task_generate_pubmed_file.delay(xml_id, user_id=user_id, username=username)
60+
task_generate_pmc_file.delay(xml_id, user_id=user_id, username=username)
5561

5662
return True
5763

@@ -235,6 +241,128 @@ def task_generate_html_file(self, xml_id, user_id=None, username=None):
235241
return False
236242

237243

244+
@celery_app.task(bind=True, timelimit=-1)
245+
def task_generate_pubmed_file(self, xml_id, user_id=None, username=None):
246+
try:
247+
xml_document = XMLDocument.objects.get(id=xml_id)
248+
except XMLDocument.DoesNotExist:
249+
logging.error(f"XML file with ID {xml_id} does not exist.")
250+
return False
251+
252+
user = _get_user(self.request, username=username, user_id=user_id)
253+
254+
logging.info(
255+
f"Starting PubMed XML generation for XML file {xml_document.xml_file.name}."
256+
)
257+
try:
258+
path_pubmed = utils.generate_pubmed_for_xml_document(
259+
xml_document.xml_file.path,
260+
output_root_dir=os.path.join(settings.MEDIA_ROOT, "xml_manager", "pubmed"),
261+
params={},
262+
)
263+
264+
pubmed_instance = XMLDocumentPubMed(xml_document=xml_document)
265+
pubmed_instance.pubmed_file.name = os.path.relpath(
266+
path_pubmed, settings.MEDIA_ROOT
267+
)
268+
pubmed_instance.save()
269+
270+
except exceptions.XML_File_Parsing_Error as e:
271+
logging.error(f"Error during XML parsing: {e}")
272+
XMLDocumentEvent.create(
273+
xml_document=xml_document,
274+
error_type=XML_DOCUMENT_PARSING_ERROR,
275+
data={},
276+
message=str(e),
277+
save=True,
278+
)
279+
return False
280+
281+
except exceptions.XML_File_PubMed_Generation_Error as e:
282+
logging.error(f"Error during PubMed XML generation: {e}")
283+
XMLDocumentEvent.create(
284+
xml_document=xml_document,
285+
error_type=XML_DOCUMENT_CONVERSION_TO_PUBMED_ERROR,
286+
data={},
287+
message=str(e),
288+
save=True,
289+
)
290+
return False
291+
292+
except Exception as e:
293+
logging.error(f"Unexpected error during PubMed XML generation: {e}")
294+
XMLDocumentEvent.create(
295+
xml_document=xml_document,
296+
error_type=XML_DOCUMENT_UNKNOWN_ERROR,
297+
data={},
298+
message=str(e),
299+
save=True,
300+
)
301+
return False
302+
303+
return True
304+
305+
306+
@celery_app.task(bind=True, timelimit=-1)
307+
def task_generate_pmc_file(self, xml_id, user_id=None, username=None):
308+
try:
309+
xml_document = XMLDocument.objects.get(id=xml_id)
310+
except XMLDocument.DoesNotExist:
311+
logging.error(f"XML file with ID {xml_id} does not exist.")
312+
return False
313+
314+
user = _get_user(self.request, username=username, user_id=user_id)
315+
316+
logging.info(
317+
f"Starting PMC XML generation for XML file {xml_document.xml_file.name}."
318+
)
319+
try:
320+
path_pmc = utils.generate_pmc_for_xml_document(
321+
xml_document.xml_file.path,
322+
output_root_dir=os.path.join(settings.MEDIA_ROOT, "xml_manager", "pmc"),
323+
params={},
324+
)
325+
326+
pmc_instance = XMLDocumentPMC(xml_document=xml_document)
327+
pmc_instance.pmc_file.name = os.path.relpath(path_pmc, settings.MEDIA_ROOT)
328+
pmc_instance.save()
329+
330+
except exceptions.XML_File_Parsing_Error as e:
331+
logging.error(f"Error during XML parsing: {e}")
332+
XMLDocumentEvent.create(
333+
xml_document=xml_document,
334+
error_type=XML_DOCUMENT_PARSING_ERROR,
335+
data={},
336+
message=str(e),
337+
save=True,
338+
)
339+
return False
340+
341+
except exceptions.XML_File_PMC_Generation_Error as e:
342+
logging.error(f"Error during PMC XML generation: {e}")
343+
XMLDocumentEvent.create(
344+
xml_document=xml_document,
345+
error_type=XML_DOCUMENT_CONVERSION_TO_PMC_ERROR,
346+
data={},
347+
message=str(e),
348+
save=True,
349+
)
350+
return False
351+
352+
except Exception as e:
353+
logging.error(f"Unexpected error during PMC XML generation: {e}")
354+
XMLDocumentEvent.create(
355+
xml_document=xml_document,
356+
error_type=XML_DOCUMENT_UNKNOWN_ERROR,
357+
data={},
358+
message=str(e),
359+
save=True,
360+
)
361+
return False
362+
363+
return True
364+
365+
238366
@celery_app.task(bind=True)
239367
def task_validate_sps_package(self, validation_pk):
240368
try:

0 commit comments

Comments
 (0)