|
8 | 8 | from leaflet.admin import LeafletGeoAdmin |
9 | 9 | from publications.models import Publication, Source, HarvestingEvent, BlockedEmail, BlockedDomain, GlobalRegion |
10 | 10 | from import_export.admin import ImportExportModelAdmin |
11 | | -from publications.models import EmailLog, Subscription, UserProfile |
| 11 | +from publications.models import EmailLog, Subscription, UserProfile, WikidataExportLog |
12 | 12 | from publications.tasks import harvest_oai_endpoint, schedule_subscription_email_task, send_monthly_email, schedule_monthly_email_task |
13 | 13 | from django_q.models import Schedule |
14 | 14 | from django.utils.timezone import now |
|
17 | 17 | from publications.tasks import regenerate_geopackage_cache |
18 | 18 | from django.test import Client |
19 | 19 | from django.http import HttpResponse |
20 | | -from publications.wikidata import export_publications_to_wikidata |
| 20 | +from publications.wikidata import export_publications_to_wikidata, export_publications_to_wikidata_dryrun |
21 | 21 |
|
22 | | -@admin.action(description="Create new Wikidata items for selected publications") |
| 22 | +@admin.action(description="Export selected publications to Wikidata/Wikibase") |
23 | 23 | def export_to_wikidata(modeladmin, request, queryset): |
24 | | - created_count, updated_count, error_records = export_publications_to_wikidata(queryset) |
| 24 | + stats = export_publications_to_wikidata(queryset) |
25 | 25 |
|
26 | 26 | # Success messages |
27 | | - if created_count: |
28 | | - messages.success(request, f"{created_count} new Wikidata item(s) created.") |
29 | | - if updated_count: |
30 | | - messages.success(request, f"{updated_count} existing Wikidata item(s) updated.") |
31 | | - |
32 | | - # Warnings and errors |
33 | | - for publication, error_message in error_records: |
34 | | - if error_message == "no publicationDate": |
35 | | - messages.warning(request, f"Skipping “{publication.title}”: no publication date") |
36 | | - else: |
37 | | - messages.error(request, f"Failed to export “{publication.title}”: {error_message}") |
| 27 | + if stats['created']: |
| 28 | + messages.success(request, f"{stats['created']} new Wikidata item(s) created.") |
| 29 | + if stats['updated']: |
| 30 | + messages.success(request, f"{stats['updated']} existing Wikidata item(s) updated.") |
| 31 | + if stats['skipped']: |
| 32 | + messages.info(request, f"{stats['skipped']} publication(s) skipped (already exist or duplicate labels).") |
| 33 | + |
| 34 | + # Error messages |
| 35 | + if stats['errors']: |
| 36 | + messages.error(request, f"{stats['errors']} publication(s) failed to export. Check the Wikidata export logs for details.") |
| 37 | + |
| 38 | + # Summary message |
| 39 | + messages.info(request, f"Total: {stats['total']} publication(s) processed.") |
| 40 | + |
| 41 | +@admin.action(description="[DRY-RUN] Export selected publications to Wikidata/Wikibase") |
| 42 | +def export_to_wikidata_dryrun(modeladmin, request, queryset): |
| 43 | + stats = export_publications_to_wikidata_dryrun(queryset) |
| 44 | + |
| 45 | + # Dry-run summary messages |
| 46 | + messages.info(request, f"[DRY-RUN] Export simulation complete:") |
| 47 | + |
| 48 | + if stats['created']: |
| 49 | + messages.info(request, f" • Would create {stats['created']} new Wikidata item(s)") |
| 50 | + if stats['updated']: |
| 51 | + messages.info(request, f" • Would update {stats['updated']} existing Wikidata item(s)") |
| 52 | + if stats['skipped']: |
| 53 | + messages.info(request, f" • Would skip {stats['skipped']} publication(s)") |
| 54 | + if stats['errors']: |
| 55 | + messages.warning(request, f" • {stats['errors']} publication(s) have validation errors") |
| 56 | + |
| 57 | + # Summary message |
| 58 | + messages.success(request, f"[DRY-RUN] Total: {stats['total']} publication(s) analyzed. No changes were written to Wikibase.") |
38 | 59 |
|
39 | 60 | @admin.action(description="Mark selected publications as published") |
40 | 61 | def make_public(modeladmin, request, queryset): |
@@ -172,8 +193,9 @@ class PublicationAdmin(LeafletGeoAdmin, ImportExportModelAdmin): |
172 | 193 | "openalex_fulltext_origin", "openalex_is_retracted", |
173 | 194 | "openalex_ids", "openalex_open_access_status") |
174 | 195 | readonly_fields = ("created_by", "updated_by", "openalex_link") |
175 | | - actions = ["make_public", "make_draft", "regenerate_all_exports", |
176 | | - "export_permalinks_csv", "email_permalinks_preview", "export_to_wikidata"] |
| 196 | + actions = [make_public, make_draft, regenerate_all_exports, |
| 197 | + "export_permalinks_csv", "email_permalinks_preview", |
| 198 | + export_to_wikidata, export_to_wikidata_dryrun] |
177 | 199 |
|
178 | 200 | @admin.display(boolean=True, description="Has DOI") |
179 | 201 | def has_permalink(self, obj): |
@@ -243,12 +265,95 @@ class EmailLogAdmin(admin.ModelAdmin): |
243 | 265 | "sent_at", |
244 | 266 | "sent_by", |
245 | 267 | "trigger_source", |
246 | | - "status", |
247 | | - "error_message", |
| 268 | + "status", |
| 269 | + "error_message", |
248 | 270 | ) |
249 | | - list_filter = ("status", "trigger_source", "sent_at") |
250 | | - search_fields = ("recipient_email", "subject", "sent_by__username") |
251 | | - actions = [trigger_monthly_email, trigger_monthly_email_task] |
| 271 | + list_filter = ("status", "trigger_source", "sent_at") |
| 272 | + search_fields = ("recipient_email", "subject", "sent_by__username") |
| 273 | + actions = [trigger_monthly_email, trigger_monthly_email_task] |
| 274 | + |
| 275 | +@admin.register(WikidataExportLog) |
| 276 | +class WikidataExportLogAdmin(admin.ModelAdmin): |
| 277 | + """Admin interface for Wikidata export logs.""" |
| 278 | + list_display = ( |
| 279 | + "id", |
| 280 | + "publication_title", |
| 281 | + "action", |
| 282 | + "wikidata_link", |
| 283 | + "export_date", |
| 284 | + "fields_count", |
| 285 | + ) |
| 286 | + list_filter = ("action", "export_date") |
| 287 | + search_fields = ( |
| 288 | + "publication__title", |
| 289 | + "publication__doi", |
| 290 | + "wikidata_qid", |
| 291 | + "export_summary", |
| 292 | + ) |
| 293 | + readonly_fields = ( |
| 294 | + "publication", |
| 295 | + "export_date", |
| 296 | + "action", |
| 297 | + "wikidata_qid", |
| 298 | + "wikidata_url", |
| 299 | + "wikidata_link_display", |
| 300 | + "wikibase_endpoint", |
| 301 | + "exported_fields", |
| 302 | + "error_message_display", |
| 303 | + "export_summary", |
| 304 | + ) |
| 305 | + fields = ( |
| 306 | + "publication", |
| 307 | + "export_date", |
| 308 | + "action", |
| 309 | + "wikibase_endpoint", |
| 310 | + "wikidata_qid", |
| 311 | + "wikidata_link_display", |
| 312 | + "export_summary", |
| 313 | + "exported_fields", |
| 314 | + "error_message_display", |
| 315 | + ) |
| 316 | + ordering = ("-export_date",) |
| 317 | + date_hierarchy = "export_date" |
| 318 | + |
| 319 | + @admin.display(description="Publication") |
| 320 | + def publication_title(self, obj): |
| 321 | + return obj.publication.title[:60] if obj.publication else "—" |
| 322 | + |
| 323 | + @admin.display(description="Wikidata") |
| 324 | + def wikidata_link(self, obj): |
| 325 | + if obj.wikidata_qid and obj.wikidata_url: |
| 326 | + return format_html( |
| 327 | + '<a href="{}" target="_blank" rel="noopener"><i class="fas fa-external-link-alt"></i> {}</a>', |
| 328 | + obj.wikidata_url, |
| 329 | + obj.wikidata_qid |
| 330 | + ) |
| 331 | + return "—" |
| 332 | + |
| 333 | + @admin.display(description="Wikidata Link") |
| 334 | + def wikidata_link_display(self, obj): |
| 335 | + if obj.wikidata_qid and obj.wikidata_url: |
| 336 | + return format_html( |
| 337 | + '<a href="{}" target="_blank" rel="noopener">{}</a>', |
| 338 | + obj.wikidata_url, |
| 339 | + obj.wikidata_url |
| 340 | + ) |
| 341 | + return "—" |
| 342 | + |
| 343 | + @admin.display(description="Fields") |
| 344 | + def fields_count(self, obj): |
| 345 | + if obj.exported_fields: |
| 346 | + return len(obj.exported_fields) |
| 347 | + return 0 |
| 348 | + |
| 349 | + @admin.display(description="Error Message (Full Traceback)") |
| 350 | + def error_message_display(self, obj): |
| 351 | + if obj.error_message: |
| 352 | + return format_html( |
| 353 | + '<pre style="white-space: pre-wrap; font-family: monospace; font-size: 12px; background: #f5f5f5; padding: 10px; border: 1px solid #ddd; border-radius: 4px; max-height: 400px; overflow-y: auto;">{}</pre>', |
| 354 | + obj.error_message |
| 355 | + ) |
| 356 | + return "—" |
252 | 357 |
|
253 | 358 | @admin.register(Subscription) |
254 | 359 | class SubscriptionAdmin(admin.ModelAdmin): |
|
0 commit comments