|
|
|
@@ -0,0 +1,89 @@
|
|
|
|
|
"""Cross-source publisher identities must preserve attempts and decision history."""
|
|
|
|
|
import hashlib
|
|
|
|
|
import json
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import sqlite3
|
|
|
|
|
import tempfile
|
|
|
|
|
import unittest
|
|
|
|
|
|
|
|
|
|
from paper_atlas.db.submissions import identities_for_row
|
|
|
|
|
from paper_atlas.ingestion.records import ingest_file
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DOI = '10.1234/aamas.identity.example'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def row(source, external_id, *, venue='AAMAS', year=2012, track='demo', status='accepted', identifiers=None):
|
|
|
|
|
return {'source': source, 'external_id': external_id,
|
|
|
|
|
'url': 'https://example.org/'+source+'/'+external_id,
|
|
|
|
|
'title': 'A shared publication with independently recorded evidence',
|
|
|
|
|
'abstract': 'An independently recorded source abstract.',
|
|
|
|
|
'authors': ['Jane Doe', 'Bob Smith'],
|
|
|
|
|
'identifiers': identifiers if identifiers is not None else {'doi': DOI},
|
|
|
|
|
'venue': venue, 'year': year, 'track': track, 'status': status,
|
|
|
|
|
'observed_at': '2026-11-09T00:00:00Z',
|
|
|
|
|
'raw_decision': source+' observed '+status,
|
|
|
|
|
'metadata': {'original_source': source}, 'tags': []}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ingest(db, root, record):
|
|
|
|
|
path = root/(record['source']+'-'+record['external_id'].replace('/', '-')+'.jsonl')
|
|
|
|
|
path.write_text(json.dumps(record)+'\n')
|
|
|
|
|
path.with_suffix('.manifest.json').write_text(json.dumps({
|
|
|
|
|
'source': record['source'], 'parser_version': 1, 'venue': record['venue'],
|
|
|
|
|
'year': record['year'], 'track': record['track'], 'count': 1,
|
|
|
|
|
'list_url': 'https://example.org/'+record['source'], 'status': 'partial',
|
|
|
|
|
'file_sha256': hashlib.sha256(path.read_bytes()).hexdigest(),
|
|
|
|
|
'missing_abstract_count': 0}))
|
|
|
|
|
ingest_file(db, path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AamasPublicationIdentityTests(unittest.TestCase):
|
|
|
|
|
def test_only_accepted_known_publisher_rows_supply_publication_doi_identity(self):
|
|
|
|
|
record = row('aamas_proceedings', '2012/D1_1', identifiers={'doi': 'https://doi.org/'+DOI.upper()})
|
|
|
|
|
self.assertEqual(identities_for_row(record), [('publication_doi', DOI)])
|
|
|
|
|
for status in ['rejected', 'desk_rejected', 'withdrawn', 'under_review', 'unknown']:
|
|
|
|
|
with self.subTest(status=status):
|
|
|
|
|
self.assertEqual(identities_for_row({**record, 'status': status}), [])
|
|
|
|
|
self.assertEqual(identities_for_row({**record, 'source': 'unverified_source'}), [])
|
|
|
|
|
|
|
|
|
|
def test_same_edition_doi_joins_sources_in_both_orders_without_duplicate_attempts(self):
|
|
|
|
|
publisher = row('aamas_proceedings', '2012/D1_1')
|
|
|
|
|
bibliography = row('dblp', 'conf/atal/Example12')
|
|
|
|
|
for order in [(publisher, bibliography), (bibliography, publisher)]:
|
|
|
|
|
with self.subTest(first=order[0]['source']), tempfile.TemporaryDirectory() as temporary:
|
|
|
|
|
root = Path(temporary)
|
|
|
|
|
db = root/'sample.sqlite'
|
|
|
|
|
for record in order:
|
|
|
|
|
ingest(db, root, record)
|
|
|
|
|
ingest(db, root, publisher)
|
|
|
|
|
with sqlite3.connect(db) as connection:
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM papers').fetchone()[0], 1)
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM source_records').fetchone()[0], 2)
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM submissions').fetchone()[0], 1)
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM submission_sources').fetchone()[0], 2)
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM decision_sources').fetchone()[0], 2)
|
|
|
|
|
self.assertEqual(connection.execute('SELECT scheme,value FROM submission_identifiers').fetchall(), [('publication_doi', DOI)])
|
|
|
|
|
metadata = [json.loads(r[0]) for r in connection.execute('SELECT metadata_json FROM source_records')]
|
|
|
|
|
self.assertEqual({r['original_source'] for r in metadata}, {'aamas_proceedings', 'dblp'})
|
|
|
|
|
self.assertEqual(connection.execute('PRAGMA foreign_key_check').fetchall(), [])
|
|
|
|
|
|
|
|
|
|
def test_cross_venue_rejection_and_distinct_tracks_remain_separate_attempts(self):
|
|
|
|
|
with tempfile.TemporaryDirectory() as temporary:
|
|
|
|
|
root = Path(temporary)
|
|
|
|
|
db = root/'sample.sqlite'
|
|
|
|
|
rejected = row('openreview_snapshot', 'original-forum', venue='AAAI', year=2011,
|
|
|
|
|
track='main', status='rejected', identifiers={'arxiv': '1101.00001', 'openreview': 'original-forum'})
|
|
|
|
|
accepted = row('aamas_proceedings', '2012/D1_1', identifiers={'arxiv': '1101.00001', 'doi': DOI})
|
|
|
|
|
another_track = row('aamas_proceedings', '2012/F1_1', track='proceedings_unspecified', identifiers={'doi': DOI})
|
|
|
|
|
for record in [rejected, accepted, another_track]:
|
|
|
|
|
ingest(db, root, record)
|
|
|
|
|
with sqlite3.connect(db) as connection:
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM papers').fetchone()[0], 1)
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM submissions').fetchone()[0], 3)
|
|
|
|
|
history = connection.execute('''SELECT v.acronym,e.year,e.track,d.status FROM decisions d
|
|
|
|
|
JOIN submissions s ON s.id=d.submission_id JOIN editions e ON e.id=s.edition_id
|
|
|
|
|
JOIN venues v ON v.id=e.venue_id ORDER BY v.acronym,e.track''').fetchall()
|
|
|
|
|
self.assertEqual(set(history), {('AAAI',2011,'main','rejected'),('AAMAS',2012,'demo','accepted'),('AAMAS',2012,'proceedings_unspecified','accepted')})
|
|
|
|
|
self.assertEqual(connection.execute('SELECT count(*) FROM decision_sources').fetchone()[0], 3)
|
|
|
|
|
self.assertEqual(connection.execute('PRAGMA foreign_key_check').fetchall(), [])
|