Compare commits

..
16 changed files with 629 additions and 56 deletions

No files matched your search

+9
View File
@@ -64,6 +64,7 @@ class PublicationStageFilter(admin.SimpleListFilter):
return ( return (
('primary', _("primary")), ('primary', _("primary")),
('secondary', _("secondary")), ('secondary', _("secondary")),
('2-secondary', _("2-secondary")),
('tertiary', _("tertiary")), ('tertiary', _("tertiary")),
('excluded', _("excluded")), ('excluded', _("excluded")),
('-', _("-")), ('-', _("-")),
@@ -95,6 +96,14 @@ class PublicationStageFilter(admin.SimpleListFilter):
referenced_by__sources__isnull=False, referenced_by__sources__isnull=False,
) )
if self.value() == '2-secondary':
ids: Set[int] = {
publication.id
for publication in queryset
if publication.stage == '2-secondary'
}
return queryset.filter(id__in=ids)
if self.value() == '-': if self.value() == '-':
ids: Set[int] = { ids: Set[int] = {
publication.id publication.id
+9 -5
View File
@@ -96,7 +96,8 @@ class PublicationResult:
year: int year: int
pages: Optional[Tuple[int, int]] pages: Optional[Tuple[int, int]]
dblp_doi: Optional[str] = None dblp_doi: Optional[str] = None
authors: List[str] = field(default_factory=list) access: Optional[str] = None
authors: List[Tuple[str,str]] = field(default_factory=list)
urls: List[str] = field(default_factory=list) urls: List[str] = field(default_factory=list)
@property @property
@@ -166,7 +167,7 @@ class PublicationResult:
url = f"https://dblp.uni-trier.de/rec/{key}.xml" url = f"https://dblp.uni-trier.de/rec/{key}.xml"
response = requests.get(url) response = requests.get(url)
response.raise_for_status response.raise_for_status()
parser = xml.sax.make_parser() parser = xml.sax.make_parser()
handler = DBLPHandler({key}) handler = DBLPHandler({key})
@@ -195,6 +196,7 @@ class PublicationResult:
# TODO Parse URLs ('ee') # TODO Parse URLs ('ee')
doi = None if not 'doi' in info else info['doi'] doi = None if not 'doi' in info else info['doi']
access = None if not 'access' in info else info['access']
return cls( return cls(
key=info['key'], key=info['key'],
@@ -202,7 +204,8 @@ class PublicationResult:
year=int(info['year']), year=int(info['year']),
pages=pages, pages=pages,
dblp_doi=doi, dblp_doi=doi,
authors=[html.unescape(author['text']) for author in authors], access = access,
authors=[(author['@pid'], html.unescape(author['text'])) for author in authors],
) )
@classmethod @classmethod
@@ -223,10 +226,10 @@ class PublicationResult:
'format': 'json', 'format': 'json',
}, },
) )
response.raise_for_status response.raise_for_status()
search_result = response.json()['result'] search_result = response.json()['result']
hits = search_result['hits'] hits = search_result['hits']
if hits['@total'] == '0': return (None, None, 0) if hits['@total'] == '0' or hits['@sent'] == '0': return (None, None, 0)
results = [cls.from_search_hit(hit) for hit in hits['hit']] results = [cls.from_search_hit(hit) for hit in hits['hit']]
total = hits['@total'] total = hits['@total']
@@ -453,6 +456,7 @@ class Command(BaseCommand):
# Add authors to database # Add authors to database
authors: List[Author] = [] authors: List[Author] = []
for name in result.authors: for name in result.authors:
# TODO? find author id's -> not in xml!
author, created = Author.objects.get_or_create(name=name) author, created = Author.objects.get_or_create(name=name)
if created: if created:
self.log_success(f"Added author: {author}") self.log_success(f"Added author: {author}")
+10 -2
View File
@@ -34,7 +34,7 @@ class Command(BaseCommand):
self.stdout.write("") self.stdout.write("")
self.log_info(result.cite_key) self.log_info(result.cite_key)
if 0 < len(result.authors): if 0 < len(result.authors):
self.stdout.write(" " + ", ".join([name for name in result.authors])) self.stdout.write(" " + ", ".join([author.name for author in result.authors]))
self.log_info(" " + result.title, nl=False) self.log_info(" " + result.title, nl=False)
self.stdout.write(f" ({result.year})") self.stdout.write(f" ({result.year})")
@@ -66,8 +66,16 @@ class Command(BaseCommand):
# Store Authors # Store Authors
authors: List[Author] = [] authors: List[Author] = []
for name in result.authors: for (pid, name) in result.authors:
author, created = Author.objects.get_or_create(name=name) author, created = Author.objects.get_or_create(name=name)
if not author.dblp_id and pid != None:
try:
author.dblp_id = pid
author.full_clean()
author.save()
except ValidationError as e:
raise CommandError(f"{author}: {e}")
if created: if created:
self.log_success(f"Added author: {author}") self.log_success(f"Added author: {author}")
else: else:
+211 -13
View File
@@ -2,14 +2,19 @@ from pprint import pprint
from time import sleep from time import sleep
from tqdm import tqdm from tqdm import tqdm
import re, requests, html
from django.db import transaction from django.db import transaction
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError, CommandParser
from django.db.models import Count
from typing import List, Optional, Tuple
import sok.management.commands.dblpimport as dblp import sok.management.commands.dblpimport as dblp
from sok.management.commands.snowball import semanticscholar from sok.management.commands.snowball import semanticscholar
from sok.models import Publication, PublicationReference, SemanticScholar from sok.models import Publication, PublicationReference, SemanticScholar, Author, PublicationAuthor
class Command(BaseCommand): class Command(BaseCommand):
@@ -17,6 +22,9 @@ class Command(BaseCommand):
def log_success(self, msg: str): def log_success(self, msg: str):
self.stdout.write(self.style.SUCCESS(msg)) self.stdout.write(self.style.SUCCESS(msg))
def log_warn(self, msg: str):
self.stdout.write(self.style.WARNING(msg))
def log_info(self, msg: str, nl: bool = True): def log_info(self, msg: str, nl: bool = True):
self.stdout.write(self.style.HTTP_INFO(msg), ending='\n' if nl else '') self.stdout.write(self.style.HTTP_INFO(msg), ending='\n' if nl else '')
self.stdout.flush() self.stdout.flush()
@@ -71,6 +79,7 @@ class Command(BaseCommand):
data = semanticscholar(semantic.paper_id) data = semanticscholar(semantic.paper_id)
if abstract := data['abstract']: if abstract := data['abstract']:
publication.abstract = abstract publication.abstract = abstract
if publication.peer_reviewed == None: publication.peer_reviewed = False
try: try:
publication.full_clean() publication.full_clean()
publication.save() publication.save()
@@ -83,9 +92,22 @@ class Command(BaseCommand):
def search_on_dblp(self, publication: Publication): def search_on_dblp(self, publication: Publication):
query, results, total = dblp.PublicationResult.from_search(publication.title, 100) query, results, total = dblp.PublicationResult.from_search(publication.title, 100)
if total == 0: return if total == 0: return
plausible = []
for result in results: for result in results:
if publication.doi and result.doi: if publication.doi and result.doi:
if publication.doi.lower() == result.doi.lower(): if publication.doi.lower() == result.doi.lower():
plausible = [result]
break
else: continue # quite definitely not the same publication
elif publication.title == result.title and publication.year == result.year:
plausible.append(result)
self.log_warn(f"Not quite certain about {result.cite_key} for publication {publication.title}")
else: continue # I'd rather look at that in detail for now
if len(plausible) == 1:
result = plausible[0]
if result.access.lower() == 'withdrawn':
self.log_warn(f"Publication {publication.title} was WITHDRAWN!")
return
publication.cite_key = result.cite_key publication.cite_key = result.cite_key
publication.year = result.year publication.year = result.year
if not result.is_peer_reviewed == None: if not result.is_peer_reviewed == None:
@@ -93,20 +115,56 @@ class Command(BaseCommand):
publication.first_page = result.first_page publication.first_page = result.first_page
publication.last_page = result.last_page publication.last_page = result.last_page
print(publication.peer_reviewed, result) print(publication.peer_reviewed, result)
# keep authors from semantic scholar for now, even though they might be a little broken?
try: try:
publication.full_clean() publication.full_clean()
publication.save() publication.save()
self.log_success(f"Added DBLP info for: {publication}") self.log_success(f"Added DBLP info for: {publication}")
except ValidationError as e: except ValidationError as e:
raise CommandError(f"{publication}: {e}") raise CommandError(f"{publication}: {e}")
else: continue # quite definitely not the same publication
elif publication.title == result.title and publication.year == result.year: # Store Authors
print(f"Not quite certain about {result.cite_key} for publication {publication.title}") authors: List[Author] = []
else: continue # I'd rather look at that in detail for now for (pid, name) in result.authors:
author, created = Author.objects.get_or_create(name=name)
if created:
self.log_success(f"Added author: {author}")
else:
self.log_info(f"Author '{author}' already known")
if not author.dblp_id and pid != None:
try:
author.dblp_id = pid
author.full_clean()
author.save()
self.log_success(f"Added pid to author: {author}")
except ValidationError as e:
raise CommandError(f"{author}: {e}")
authors.append(author)
# Replace authors for publication
if len(authors) != len(PublicationAuthor.objects.filter(publication=publication)): print(authors, [pa.author for pa in PublicationAuthor.objects.filter(publication=publication)])
assert len(authors) >= len(PublicationAuthor.objects.filter(publication=publication))
for publication_author in PublicationAuthor.objects.filter(publication=publication):
self.log_warn(f"Will now try to delete {publication_author.author} from {publication}")
try:
publication_author.delete()
except e:
raise CommandError(f"{publication} - {author}: {e}")
for position, author in enumerate(authors):
publication_author, created = PublicationAuthor.objects.get_or_create(
author=author,
publication=publication,
position=position,
)
if created:
self.log_success(f"Assigned author '{author}' to publication '{publication}' at position {position}")
else:
self.log_info(f"Author '{author}' already assigned to publication '{publication}' at position '{position}'")
def find_secondary_on_dblp(self): def find_secondary_on_dblp(self):
self.log_info("--- Searching for secondary and tertiary sources in the default DBLP dump ---") self.log_info("--- Searching for snowballed sources on DBLP ---")
for publication in tqdm(Publication.objects.exclude(cite_key__startswith=dblp.CITE_KEY_PREFIX), unit="publication"): for publication in tqdm(Publication.objects.exclude(cite_key__startswith=dblp.CITE_KEY_PREFIX), unit="publication"):
if publication.stage == 'excluded': continue if publication.stage == 'excluded': continue
self.search_on_dblp(publication) self.search_on_dblp(publication)
@@ -117,7 +175,7 @@ class Command(BaseCommand):
).values_list('cite_key', flat=True).distinct() ).values_list('cite_key', flat=True).distinct()
) )
n = len(keys) n = len(keys)
self.log_info(f"Found {n} publications that still need to be checked") self.log_info(f"Found {n} publications that still need to be verified")
def fix_dblp(self): def fix_dblp(self):
self.log_info("--- Searching for entries not in the default DBLP dump ---") self.log_info("--- Searching for entries not in the default DBLP dump ---")
@@ -165,16 +223,156 @@ class Command(BaseCommand):
obj.full_clean() obj.full_clean()
obj.save() obj.save()
self.log_success(f"Set semanticscholar ID for publication '{publication}': {paper_id}") self.log_success(f"Set semanticscholar ID for publication '{publication}': {paper_id}")
else: print(publication) else:
self.log_warn(f"Could not find semanticscholar ID for publication '{publication.title}' with DOI '{publication.doi}'")
sleep(2) # Throttle to avoid rate-limiting sleep(1) # Throttle to avoid rate-limiting (1/s with API Key)
def find_author_on_dblp(
self,
name: str,
limit: int = 100,
) -> List[Tuple[str, str]]:
url = 'http://dblp.org/search/author/api'
response = requests.get(
url,
params={
'q': name,
'f': 0,
'h': limit,
'c': 0,
'format': 'json',
},
)
response.raise_for_status()
search_result = response.json()['result']
hits = search_result['hits']
if hits['@total'] == '0': return None
results = [hit['info'] for hit in hits['hit']]
return results
def verify_author(self, pid, keys):
url = f"https://dblp.org/pid/{pid}.xml"
response = requests.get(url)
response.raise_for_status()
pubs = re.findall('key="([^"]*)"', str(response.content))
sleep(1) # Throttle to avoid rate-limiting
return keys.intersection(pubs) == keys
def find_pid_for_authors(self):
self.log_info("--- Adding ID's to authors ---")
authors = Author.objects.filter(dblp_id__exact="")
for author in tqdm(authors, unit='author'):
pid = ""
keys = set({
dblp.strip_cite_key_prefix(cite_key)
for cite_key in PublicationAuthor.objects.filter(
author__exact=author,
publication__cite_key__startswith=dblp.CITE_KEY_PREFIX
).values_list('publication__cite_key', flat=True).distinct()}
)
if len(keys) == 0: continue
options = self.find_author_on_dblp(author.name)
if not options:
self.log_warn(f"Could not find ID for author {author}")
elif len(options) == 1:
pid = re.match('https://dblp.org/pid/(.*)', options[0]['url']).groups()[0]
if self.verify_author(pid, keys):
self.log_success(f"Found {pid} for author {author}")
author.dblp_id = pid
author.full_clean()
author.save()
self.log_success(f"Added pid for author {author}")
else:
for opt in options:
if not 'url' in opt: continue
pid = re.match('https://dblp.org/pid/(.*)', opt['url']).groups()[0]
if self.verify_author(pid, keys):
self.log_success(f"Found {pid} for author {author}")
author.dblp_id = pid
author.full_clean()
author.save()
self.log_success(f"Added pid for author {author}")
break
@transaction.atomic
def merge(self, author, variant):
for pub_author in PublicationAuthor.objects.filter(author=variant):
publication = pub_author.publication
if PublicationAuthor.objects.filter(author=author, publication=publication).exists():
self.log_warn(f"{author} is already author of {publication}")
break
fixed = PublicationAuthor(
author=author,
publication=publication,
position=pub_author.position
)
try:
pub_author.delete()
fixed.full_clean()
fixed.save()
self.log_success(f"Changed author: {publication}: {variant} -> {fixed.author}")
except ValidationError as e:
raise CommandError(f"{publication}: {variant} -> {fixed.author}: {e}")
assert not PublicationAuthor.objects.filter(author=variant).exists()
variant.delete()
def merge_authors(self) -> None:
self.log_info("--- Searching for potential duplicate authors and merging their publications ---")
dup_ids = Author.objects.exclude(dblp_id__exact="").values_list('dblp_id').annotate(count=Count('id')).filter(count__gt=1)
try:
for (pid, counter) in tqdm(dup_ids, unit="duplicate"):
response = requests.get(f"https://dblp.org/pid/{pid}.xml")
response.raise_for_status()
dblp_person = re.search('dblpperson name="([^"]*)"', response.content.decode('ascii')).groups()
assert len(dblp_person) == 1
dblp_name = html.unescape(dblp_person[0])
variants = Author.objects.filter(dblp_id__exact=pid).exclude(name__exact=dblp_name)
# for v in variants:
# print(v.name, v.dblp_id)
# break
orig = Author.objects.filter(dblp_id__exact=pid, name__exact=dblp_name)
print(variants, dblp_name, orig)
assert len(orig) == 1
self.log_info(f"Suggestion to merge {', '.join([v.name for v in variants])} into {orig[0]}")
while True:
self.log_warn("Merge? [Y/n]")
choice = input().lower()
if choice in {'', 'y', 'yes'}:
for variant in variants: self.merge(orig[0], variant)
break
elif choice in {'', 'n', 'no'}:
break
except KeyboardInterrupt:
raise CommandError("Aborted.")
# BaseCommand # BaseCommand
def add_arguments(self, parser: CommandParser):
parser.add_argument('-b', '--abstract', action='store_true')
parser.add_argument('-d', '--dblp', action='store_true')
parser.add_argument('-a', '--authors', action='store_true')
parser.add_argument('-s', '--secondary', action='store_true')
parser.add_argument('-i', '--scholarid', action='store_true')
def handle(self, *args, **options): def handle(self, *args, **options):
self.fix_references() self.fix_references()
self.fix_abstracts() if options['secondary']: self.find_secondary_on_dblp()
self.find_secondary_on_dblp() if options['dblp']:
self.fix_dblp() self.fix_dblp()
self.find_missing_dois() self.find_missing_dois()
if options['scholarid'] or options['dblp']:
self.find_semanticscholar_ids() self.find_semanticscholar_ids()
if options['authors']:
self.find_pid_for_authors()
self.merge_authors()
if options['abstract']: self.fix_abstracts()
+69 -22
View File
@@ -6,6 +6,8 @@ from pathlib import Path
from time import sleep from time import sleep
from typing import Any, Dict, List, Set from typing import Any, Dict, List, Set
from django.conf import settings
import requests import requests
from django.core.management.base import BaseCommand, CommandParser, CommandError from django.core.management.base import BaseCommand, CommandParser, CommandError
@@ -24,7 +26,7 @@ from sok.models import (
) )
def semanticscholar(identifier: str, include_unknown_references: bool = False) -> Dict[str, Any]: def semanticscholar(identifier: str, fields: str = None, type: str = None, offset: int = 0, include_unknown_references: bool = False) -> Dict[str, Any]:
""" """
Retrieve information from the Semantic Scholar API. Retrieve information from the Semantic Scholar API.
@@ -33,11 +35,24 @@ def semanticscholar(identifier: str, include_unknown_references: bool = False) -
See: https://api.semanticscholar.org See: https://api.semanticscholar.org
""" """
url = f'https://api.semanticscholar.org/v1/paper/{identifier}' url = f'https://api.semanticscholar.org/graph/v1/paper/{identifier}'
params: Dict[str, Any] = dict() params: Dict[str, Any] = dict()
if type in ['citations', 'references']:
url += ('/' + type)
params['limit'] = '200'
params['offset'] = str(offset)
if include_unknown_references: if include_unknown_references:
params['include_unknown_references'] = 'true' params['include_unknown_references'] = 'true'
response = requests.get(url, params=params) if fields:
params['fields'] = fields
headers = {
'x-api-key': settings.SCHOLAR_API_KEY
}
response = requests.get(url, params=params, headers=headers)
response.raise_for_status response.raise_for_status
return response.json() return response.json()
@@ -48,7 +63,6 @@ class Command(BaseCommand):
if bold: if bold:
msg = self.style.HTTP_INFO(msg) msg = self.style.HTTP_INFO(msg)
tqdm.write(msg, end='\n' if nl else '') tqdm.write(msg, end='\n' if nl else '')
#self.stdout.write(msg, ending='\n' if nl else '')
def warn(self, msg: str): def warn(self, msg: str):
self.echo(self.style.WARNING(msg)) self.echo(self.style.WARNING(msg))
@@ -113,8 +127,9 @@ class Command(BaseCommand):
base: Publication, base: Publication,
is_reference: bool, is_reference: bool,
) -> Publication: ) -> Publication:
data = semanticscholar(paper_id) data = semanticscholar(paper_id, fields='title,authors,year,abstract,externalIds')
# Add authors to database # Add authors to database
## TODO: semantic scholar author ids nutzen?
authors: List[Author] = [] authors: List[Author] = []
first = True first = True
cite_key = '' cite_key = ''
@@ -124,7 +139,7 @@ class Command(BaseCommand):
if created: if created:
self.echo(f"Added author: {author}") self.echo(f"Added author: {author}")
else: else:
self.echo(f"Author '{author}' alreay known") self.echo(f"Author '{author}' already known")
authors.append(author) authors.append(author)
cite_key = '' cite_key = ''
if authors: if authors:
@@ -145,22 +160,22 @@ class Command(BaseCommand):
publication = None publication = None
# Add publication to database # Add publication to database
doi = data.get('doi', None) doi = data.get('externalIds', None).get('DOI', None)
if not publication: if not publication:
self.echo(f"Will create now with cite key {cite_key}")
publication = Publication.objects.create( publication = Publication.objects.create(
cite_key=cite_key, cite_key=cite_key,
title=title, title=title,
year=data.get('year', 0), year=data.get('year', 0),
peer_reviewed=None, peer_reviewed=None,
doi=doi, doi=doi,
abstract=data.get('abstract', None),
) )
self.echo(f"Added publication: {publication}") self.echo(f"Added publication: {publication}")
else: else:
self.echo(f"Publication '{publication}' already known") self.echo(f"Publication '{publication}' already known")
# Assign authors # Assign authors
for position, author in enumerate(authors): for position, author in enumerate(list(set(authors))):
publication_author, created = PublicationAuthor.objects.get_or_create( publication_author, created = PublicationAuthor.objects.get_or_create(
author=author, author=author,
publication=publication, publication=publication,
@@ -192,9 +207,11 @@ class Command(BaseCommand):
title = "Reference" if is_reference else "Citation" title = "Reference" if is_reference else "Citation"
if 0 < len(objs): if 0 < len(objs):
self.echo(f"--- {title}s ---") self.echo(f"--- {title}s ---")
for obj in tqdm(objs, unit=title.lower()): progress_iterator = tqdm(objs, unit=title.lower(), position=2, leave=False, desc=title + 's')
for obj in progress_iterator:
if paper_id := obj.get('paperId', None): if paper_id := obj.get('paperId', None):
try: try:
# This publication already exists and has a semantic scholar entry
existing = SemanticScholar.objects.get(paper_id=paper_id) existing = SemanticScholar.objects.get(paper_id=paper_id)
if is_reference: if is_reference:
self.add_reference(base, existing.publication) self.add_reference(base, existing.publication)
@@ -202,6 +219,7 @@ class Command(BaseCommand):
self.add_reference(existing.publication, base, is_reference) self.add_reference(existing.publication, base, is_reference)
continue continue
except SemanticScholar.DoesNotExist: except SemanticScholar.DoesNotExist:
# This publication already exists but does not have a semantic scholar entry
if doi := obj.get('doi', None): if doi := obj.get('doi', None):
try: try:
publication = Publication.objects.get(doi=doi) publication = Publication.objects.get(doi=doi)
@@ -215,6 +233,7 @@ class Command(BaseCommand):
self.add_reference(new.publication, base, is_reference) self.add_reference(new.publication, base, is_reference)
continue continue
except Publication.DoesNotExist: except Publication.DoesNotExist:
# This publication does not exist so we need to create it
pass pass
identifier = self.get_identifier(obj) identifier = self.get_identifier(obj)
@@ -225,11 +244,12 @@ class Command(BaseCommand):
paper_id = obj.get('paperId', None) paper_id = obj.get('paperId', None)
while True: while True:
self.echo("Ignore? [Y/n]", nl=True) self.echo("Ignore? [Y/n]", nl=False)
if paper_id is not None: if paper_id:
self.echo(", Show abstract [a]", nl=False) self.echo(", Show abstract [a]", nl=False)
self.echo(": ") self.echo(": ")
choice = input().lower() choice = input().lower()
if choice in {'', 'y', 'yes'}: if choice in {'', 'y', 'yes'}:
# Store choice # Store choice
self.cache.add(identifier) self.cache.add(identifier)
@@ -238,15 +258,16 @@ class Command(BaseCommand):
break break
elif choice in {'a'}: elif choice in {'a'}:
assert paper_id is not None assert paper_id is not None
data = semanticscholar(paper_id) if abstract := obj.get('abstract', None):
if abstract := data.get('abstract', None):
self.echo(abstract) self.echo(abstract)
else:
self.echo('Sorry, there is no abstract for this publication on Semantic Scholar')
elif choice in {'', 'n', 'no'}: elif choice in {'', 'n', 'no'}:
# TODO Import? copied and adapted from PR # DONE Import? copied and adapted from PR
if paper_id is not None: if paper_id is not None:
self.add_publ(paper_id, base, is_reference) self.add_publ(paper_id, base, is_reference)
else: else:
self.echo("Could not add this paper, please do it manually!") self.warn("Could not add this paper, please do it manually!")
break break
# BaseCommand # BaseCommand
@@ -255,11 +276,13 @@ class Command(BaseCommand):
parser.add_argument('--reset-choices', action='store_true') parser.add_argument('--reset-choices', action='store_true')
parser.add_argument('--no-references', action='store_true') parser.add_argument('--no-references', action='store_true')
parser.add_argument('--no-citations', action='store_true') parser.add_argument('--no-citations', action='store_true')
parser.add_argument('-s', '--stage', type=int, default=10000)
def handle(self, *args, **options): def handle(self, *args, **options):
reset_choices: bool = options['reset_choices'] reset_choices: bool = options['reset_choices']
no_citations: bool = options['no_citations'] no_citations: bool = options['no_citations']
no_references: bool = options['no_references'] no_references: bool = options['no_references']
stage: int = options['stage']
self.cache_path = Path('.choices.semanticscholar.pickle') self.cache_path = Path('.choices.semanticscholar.pickle')
self.cache: Set[str] = set() self.cache: Set[str] = set()
@@ -275,20 +298,44 @@ class Command(BaseCommand):
semanticscholar__isnull=False, semanticscholar__isnull=False,
exclusion_criteria__isnull=True, exclusion_criteria__isnull=True,
) )
if stage < 10000:
publications = [p for p in publications if p.stage_added() == stage]
self.echo(f"==== {len(publications)} publications from stage {stage} will be shown ====")
try: try:
for publication in tqdm(publications, unit="publication"): progress_iterator = tqdm(publications, unit="publication", position=1, desc='Publications')
self.echo(f"=== Publication {publication} ===") for publication in progress_iterator:
self.echo(f"=== Publication {publication}: {publication.title} ===")
for semantic in publication.semanticscholar_set.all(): for semantic in publication.semanticscholar_set.all():
data = semanticscholar(semantic.paper_id)
if not no_references: if not no_references:
references: List[Dict[str, Any]] = data['references'] offset = 0
while True:
data = semanticscholar(semantic.paper_id, type='references', fields='title,abstract', offset=offset)
if not data.get('data', None):
self.echo(self.style.WARNING("API did not return any references, verify manually!"))
break
references: List[Dict[str, Any]] = [d['citedPaper'] for d in data['data']]
self.handle_objs(publication, references, is_reference=True) self.handle_objs(publication, references, is_reference=True)
# Handle limitation if there are more than 200 paper references
if 'next' in data: offset = data['next']
else: break
if not no_citations: if not no_citations:
citations: List[Dict[str, Any]] = data['citations'] offset = 0
while True:
data = semanticscholar(semantic.paper_id, type='citations', fields='title,abstract', offset=offset)
if not data.get('data', None):
self.echo(self.style.WARNING("API did not return any citations, verify manually!"))
break
citations: List[Dict[str, Any]] = [d['citingPaper'] for d in data['data']]
self.handle_objs(publication, citations, is_reference=False) self.handle_objs(publication, citations, is_reference=False)
# Handle limitation if there are more than 200 paper references
if 'next' in data: offset = data['next']
else: break
sleep(1) # Throttle
sleep(2) # Throttle
except KeyboardInterrupt: except KeyboardInterrupt:
raise CommandError("Aborted.") raise CommandError("Aborted.")
+19
View File
@@ -1,4 +1,8 @@
from typing import Set from typing import Set
import pickle
from django.db.models import Count
from pathlib import Path
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import Count, Q from django.db.models import Count, Q
@@ -43,7 +47,22 @@ class Command(BaseCommand):
).distinct(): ).distinct():
publications_relevant.add(publication.cite_key) publications_relevant.add(publication.cite_key)
self.cache_path = Path('.choices.semanticscholar.pickle')
if self.cache_path.exists():
self.echo("Loading choices from snowballing...")
with self.cache_path.open('rb') as f:
self.cache = pickle.load(f)
self.echo(f"{len(self.cache)} publications excluded during snowballing")
self.echo(f"{len(Publication.objects.filter(variant_of__isnull=True))} publications included in total so far")
# Output # Output
self.echo(f"Total publications: {len(publications_found):4d}", bold=True) self.echo(f"Total publications: {len(publications_found):4d}", bold=True)
self.echo(f"- peer reviewed: {len(publications_peer_reviewed):4d}", bold=True) self.echo(f"- peer reviewed: {len(publications_peer_reviewed):4d}", bold=True)
self.echo(f"- relevant: {len(publications_relevant):4d}", bold=True) self.echo(f"- relevant: {len(publications_relevant):4d}", bold=True)
pubs = Publication.objects.values('title').annotate(count=Count('referenced_by')).values('title', 'year', 'count').order_by('count').reverse()
for p in pubs[:50]: print(f"{p['title']} ({p['year']}): {p['count']} citations")
years = Publication.objects.values('year').annotate(count=Count('year')).values('year', 'count')
for y in years: print(f"{y['year']}, {y['count']}")
+154
View File
@@ -0,0 +1,154 @@
import html
import io
import string
import csv
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
import requests
from django.db import transaction
from django.core.management.base import BaseCommand, CommandParser, CommandError
from sok.models import (
Author,
Publication,
PublicationAuthor,
PublicationSource,
PublicationTag,
SearchTerm,
Source,
Tag,
)
# TODO: anpassen für WoS?
PUBLICATIONS = {
'article',
'inproceedings',
'proceedings',
'book',
'incollection',
'phdthesis',
'mastersthesis',
'www',
'person',
'data',
}
CITE_KEY_PREFIX = 'Z:'
class Command(BaseCommand):
def log_success(self, msg: str):
self.stdout.write(self.style.SUCCESS(msg))
def log_info(self, msg: str, nl: bool = True):
self.stdout.write(self.style.HTTP_INFO(msg), ending='\n' if nl else '')
self.stdout.flush()
# BaseCommand
def add_arguments(self, parser: CommandParser):
parser.add_argument('--search-term', default='Not specified')
parser.add_argument('--source', default='Zotero')
parser.add_argument('zfile')
@transaction.atomic
def handle(self, *args, **options):
source, created = Source.objects.get_or_create(name=options['source'])
search_term: Optional[SearchTerm] = None
if name := options['search_term']:
search_term, created = SearchTerm.objects.get_or_create(name=name)
if created:
self.log_success(f"Created search term: {search_term}")
publications: List[Publication] = []
zotero_file = options['zfile']
with open(zotero_file, 'r') as csvfile:
reader = csv.DictReader(csvfile)
for publ in reader:
authors: List[Author] = []
for name in set(publ['Author'].split('; ')):
author, created = Author.objects.get_or_create(name=name)
if created:
self.log_success(f"Added author: {author}")
else:
self.log_info(f"Author '{author}' already known")
authors.append(author)
tags: List[Tag] = []
for t in set(publ['Manual Tags'].split('; ')).union(publ['Automatic Tags'].split('; ')):
if t == '': continue
tag, created = Tag.objects.get_or_create(name=t)
if created:
self.log_success(f"Added tag: {tag}")
else:
self.log_info(f"Tag '{tag}' already exists")
tags.append(tag)
pages = (None, None)
if '-' in publ['Pages']:
pages = publ['Pages'].split('-')
# Add publication to database
publication, created = Publication.objects.get_or_create(
cite_key=publ['Key'],
title=publ['Title'],
year=publ['Publication Year'],
# TODO: peer_reviewed=result.is_peer_reviewed,
first_page=pages[0],
last_page=pages[1],
doi=publ['DOI'] or None,
abstract=publ['Abstract Note'] or None,
)
if created:
self.log_success(f"Added publication: {publication}")
else:
self.log_info(f"Publication '{publication}' already known")
publications.append(publication)
# Assign authors
for position, author in enumerate(authors):
publication_author, created = PublicationAuthor.objects.get_or_create(
author=author,
publication=publication,
position=position,
)
if created:
self.log_success(f"Assigned author '{author}' to publication '{publication}' at position {position}")
else:
self.log_info(f"Author '{author}' already assigned to publication '{publication}' at position '{position}'")
# Assign tags
for position, tag in enumerate(tags):
publication_tag, created = PublicationTag.objects.get_or_create(
tag=tag,
publication=publication,
)
if created:
self.log_success(f"Assigned tag '{tag}' to publication '{publication}'")
else:
self.log_info(f"Tag '{tag}' already assigned to publication '{publication}'")
# Assign sources
if search_term is not None:
for publication in publications:
print(publication, search_term, source)
publication_source, created = PublicationSource.objects.get_or_create(
source=source,
publication=publication,
search_term=search_term,
)
if created:
self.log_success(f"Assigned source '{source}' to publication '{publication}' with search term '{search_term}'")
else:
self.log_info(f"Source '{source}' already assigned to publication '{publication}' with search term '{search_term}'")
+18
View File
@@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-05-16 08:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('sok', '0006_alter_publication_abstract'),
]
operations = [
migrations.AddField(
model_name='author',
name='dblp_id',
field=models.CharField(blank=True, max_length=127),
),
]
+32 -10
View File
@@ -7,6 +7,7 @@ from django.db.models.query import QuerySet
class Author(models.Model): class Author(models.Model):
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
dblp_id = models.CharField(max_length=127, blank=True)
def __str__(self) -> str: def __str__(self) -> str:
return self.name return self.name
@@ -103,6 +104,24 @@ class Publication(models.Model):
def relevant_referenced_by(self) -> QuerySet: def relevant_referenced_by(self) -> QuerySet:
return self.referenced_by.filter(exclusion_criteria__isnull=True) return self.referenced_by.filter(exclusion_criteria__isnull=True)
def stage_added(self) -> int:
if not self.is_relevant:
return 10000
if self.sources.exists():
return 0
if self.referenced_by.filter(exclusion_criteria__isnull=True, sources__isnull=False) or self.references.filter(exclusion_criteria__isnull=True, sources__isnull=False):
return 1
if ref_by := self.referenced_by.filter(exclusion_criteria__isnull=True):
stages = set([ref.stage_added() for ref in ref_by])
return 1 + sorted(stages)[0]
if refs := self.references.filter(exclusion_criteria__isnull=True):
stages = set([ref.stage_added() for ref in refs])
return 1 + sorted(stages)[0]
@property @property
def stage(self) -> Optional[str]: def stage(self) -> Optional[str]:
if not self.is_relevant: if not self.is_relevant:
@@ -113,24 +132,27 @@ class Publication(models.Model):
return 'primary' return 'primary'
# Referenced by primary (backward snowballing) # Referenced by primary (backward snowballing)
# TODO make transitive # DONE make transitive
if self.referenced_by.filter(exclusion_criteria__isnull=True, sources__isnull=False): if self.referenced_by.filter(exclusion_criteria__isnull=True, sources__isnull=False):
return 'secondary' return 'secondary'
# References a primary (forward snowballing) # References a primary (forward snowballing)
# TODO make transitive
if self.references.filter(exclusion_criteria__isnull=True, sources__isnull=False): if self.references.filter(exclusion_criteria__isnull=True, sources__isnull=False):
return 'tertiary' return 'tertiary'
if ref_by := self.referenced_by.filter(exclusion_criteria__isnull=True): # prefer secondary over tertiary regardless of stage for now
stages = set([ref.stage for ref in ref_by]) exclude = False
if 'secondary' in stages: return 'sec-secondary' if self.referenced_by.filter(exclusion_criteria__isnull=True):
elif 'tertiary' in stages: return 'sec-tertiary' stage = self.stage_added()
if stage > 10000: exclude = True
else: return str(stage) + '-secondary'
if refs := self.references.filter(exclusion_criteria__isnull=True): if self.references.filter(exclusion_criteria__isnull=True):
stages = set([ref.stage for ref in ref_by]) stage = self.stage_added()
if 'secondary' in stages: return 'tert-secondary' if stage > 10000: return 'excluded-by-ref'
elif 'tertiary' in stages: return 'tert-tertiary' else: return str(stage) + '-tertiary'
if exclude: return 'excluded-by-ref'
return None return None
+3
View File
@@ -0,0 +1,3 @@
li a {
color: green;
}
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>Tagging publications</title>
</head>
<body>
{% if publication %}
{% for author in publication.authors.all %}
<a>{{ author.name }}, </a>
{% endfor %}
<h2>{{ publication.title }} ({{ publication.year }})</h2>
{% if publication.doi %}
<!-- atm this is a link under /tag as well! -->
<a href="https://doi.org/{{ publication.doi }}">{{ publication.doi }}</a>
{% endif %}
{% else %}
<p>No publications are available.</p>
{% endif %}
</body>
</html>
+21
View File
@@ -0,0 +1,21 @@
<!doctype html>
{% load static %}
<link rel="stylesheet" href="{% static 'tag/style.css' %}">
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>List for tagging publications</title>
</head>
<body>
{% if newest_publications %}
<ul>
{% for publication in newest_publications %}
<li><a href="{% url 'detail' publication.cite_key %}/">{{ publication.title }}, {{publication.year}}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No publications are available.</p>
{% endif %}
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path("", views.IndexView.as_view(), name="index"),
path("<path:cite_key>", views.detail, name="detail"),
]
+30 -2
View File
@@ -1,3 +1,31 @@
from django.shortcuts import render from django.db.models import F
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic
from sok.models import Publication, PublicationReference
class IndexView(generic.ListView):
template_name = "tag/index.html"
context_object_name = "newest_publications"
def get_queryset(self):
return Publication.objects.order_by("-year")[:10]
def detail(request, cite_key):
try:
publication = get_object_or_404(Publication, cite_key=cite_key.strip('/'))
except Publication.DoesNotExist:
raise Http404("Publication does not exist")
return render(request, "tag/detail.html", {"publication": publication})
# def overview(request):
# try:
# ps = Publication.objects.get(...)
# except Publication.DoesNotExist:
# raise Http404("Publication does not exist")
# return render(request, "publications/overview.html", {"to_tag": ps})
# Create your views here.
+13 -1
View File
@@ -32,9 +32,17 @@ def get_or_generate_key() -> str:
assert path.exists() assert path.exists()
return path.read_text() return path.read_text()
def get_api_key() -> str:
path = Path(Path(__file__).parent,'api.secret')
assert path.exists()
return path.read_text()
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_or_generate_key() SECRET_KEY = get_or_generate_key()
SCHOLAR_API_KEY = get_api_key().strip('\n')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
@@ -89,9 +97,13 @@ WSGI_APPLICATION = 'sokman.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'jens.sqlite3',
},
'uc_sok': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3',
} },
} }
+2 -1
View File
@@ -14,8 +14,9 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('tag/', include('sok.urls')),
] ]