class DOILoaderError(BaseLoaderError):
    pass class PMIDLoaderError(BaseLoaderError):
    pass class AuthorDuplicatesForm(forms.Form):
    def get_authors_choices():
        return Author.objects.values_list("id", "last_name")

    authors = forms.MultipleChoiceField(
        label=_("Authors pool"),
        help_text=_("Authors to merge"),
        choices=get_authors_choices,
    )

    alias = forms.ChoiceField(
        label=_("Target author"),
        help_text=_("Reference author for which we will define aliases"),
        choices=get_authors_choices,
    )

    def clean_authors(self):
        authors = self.cleaned_data["authors"]
        return Author.objects.filter(id__in=authors)

    def clean_alias(self):
        alias = self.cleaned_data["alias"]
        return Author.objects.get(id=alias)

    def clean(self):
        super(AuthorDuplicatesForm, self).clean()

        authors = self.cleaned_data.get("authors", [])
        alias = self.cleaned_data.get("alias", None)

        if alias in authors:
            raise forms.ValidationError(
                _("Target author cannot be part of the selection")
            ) class EntryBatchImportForm(forms.Form):

    pmids = forms.CharField(
        label=_("PMID"),
        widget=forms.Textarea(attrs={"placeholder": "ex: 26588162, 19569182"}),
        help_text=_(
            "Paste a list of PubMed Identifiers " "(comma separated or one per line)"
        ),
        required=False,
    )

    dois = forms.CharField(
        label=_("DOI"),
        widget=forms.Textarea(
            attrs={"placeholder": "ex: 10.1093/nar/gks419, 10.1093/nar/gkp323"}
        ),
        help_text=_(
            "Paste a list of Digital Object Identifiers "
            "(comma separated or one per line)"
        ),
        required=False,
    )

    def clean_pmids(self):
        """Transform raw data in a PMID list"""
        pmids = text_to_list(self.cleaned_data["pmids"])
        for pmid in pmids:
            pmid_validator(pmid)
        return pmids

    def clean_dois(self):
        """Transform raw data in a DOI list"""
        dois = text_to_list(self.cleaned_data["dois"])
        for doi in dois:
            doi_validator(doi)
        return dois

    def clean(self):
        super(EntryBatchImportForm, self).clean()

        dois = self.cleaned_data.get("dois", [])
        pmids = self.cleaned_data.get("pmids", [])

        if not len(dois) and not len(pmids):
            raise forms.ValidationError(
                _("You need to submit at least one valid DOI or PMID")
            ) class Author(AbstractHuman):
    """Entry author"""

    class Meta:
        ordering = ("last_name", "first_name")
        verbose_name = _("Author")
        verbose_name_plural = _("Authors") class Collection(models.Model):
    """Define a collection of entries"""

    name = models.CharField(_("Name"), max_length=100)
    short_description = models.TextField(_("Short description"), blank=True, null=True)
    entries = models.ManyToManyField("Entry", related_name="collections")

    class Meta:
        verbose_name = _("Collection")
        verbose_name_plural = _("Collections")

    def __str__(self):
        return self.name class Entry(models.Model):
    """The core model for references

    Largely guided by the BibTeX file format (see
    http://en.wikipedia.org/wiki/BibTeX).

    Unsupported fields (for now):

    * eprint: A specification of an electronic publication, often a preprint
      or a technical report
    * howpublished: How it was published, if the publishing method is
      nonstandard
    * institution: The institution that was involved in the publishing, but not
      necessarily the publisher
    * key: A hidden field used for specifying or overriding the alphabetical
      order of entries (when the "author" and "editor" fields are missing).
      Note that this is very different from the key (mentioned just after this
      list) that is used to cite or cross-reference the entry.
    * series: The series of books the book was published in (e.g. "The Hardy
      Boys" or "Lecture Notes in Computer Science")
    * type: The field overriding the default type of publication (e.g.
      "Research Note" for techreport, "{PhD} dissertation" for phdthesis,
      "Section" for inbook/incollection)
    """

    ARTICLE = "article"
    BOOK = "book"
    BOOKLET = "booklet"
    CONFERENCE = "conference"
    INBOOK = "inbook"
    INCOLLECTION = "incollection"
    INPROCEEDINGS = "inproceedings"
    MANUAL = "manual"
    MASTERSTHESIS = "mastersthesis"
    MISC = "misc"
    PHDTHESIS = "phdthesis"
    PROCEEDINGS = "proceedings"
    TECHREPORT = "techreport"
    UNPUBLISHED = "unpublished"

    ENTRY_TYPES_CHOICES = (
        (ARTICLE, _("Article")),
        (BOOK, _("Book")),
        (BOOKLET, _("Book (no publisher)")),
        (CONFERENCE, _("Conference")),
        (INBOOK, _("Book chapter")),
        (INCOLLECTION, _("Book from a collection")),
        (INPROCEEDINGS, _("Conference proceedings article")),
        (MANUAL, _("Technical documentation")),
        (MASTERSTHESIS, _("Master's Thesis")),
        (MISC, _("Miscellaneous")),
        (PHDTHESIS, _("PhD Thesis")),
        (PROCEEDINGS, _("Conference proceedings")),
        (TECHREPORT, _("Technical report")),
        (UNPUBLISHED, _("Unpublished work")),
    )

    type = models.CharField(
        _("Entry type"), max_length=50, choices=ENTRY_TYPES_CHOICES, default=ARTICLE
    )

    # Base fields
    title = models.CharField(_("Title"), max_length=255)
    authors = models.ManyToManyField(
        "Author", related_name="entries", through="AuthorEntryRank"
    )
    journal = models.ForeignKey(
        "Journal", related_name="entries", blank=True, on_delete=models.CASCADE
    )
    publication_date = models.DateField(_("Publication date"), null=True)
    is_partial_publication_date = models.BooleanField(
        _("Partial publication date?"),
        default=True,
        help_text=_(
            "Check this if the publication date is incomplete (for example "
            "if only the year is valid)"
        ),
    )
    volume = models.CharField(
        _("Volume"),
        max_length=50,
        blank=True,
        help_text=_("The volume of a journal or multi-volume book"),
    )
    number = models.CharField(
        _("Number"),
        max_length=50,
        blank=True,
        help_text=_(
            "The '(issue) number' of a journal, magazine, or tech-report, if "
            "applicable. (Most publications have a 'volume', but no 'number' "
            "field.)"
        ),
    )
    pages = models.CharField(
        _("Pages"),
        max_length=50,
        blank=True,
        help_text=_("Page numbers, separated either by commas or " "double-hyphens"),
    )
    url = models.URLField(
        _("URL"), blank=True, help_text=_("The WWW address where to find this resource")
    )

    # Identifiers
    doi = models.CharField(
        _("DOI"),
        max_length=100,
        blank=True,
        help_text=_("Digital Object Identifier for this resource"),
    )
    issn = models.CharField(
        _("ISSN"),
        max_length=20,
        blank=True,
        help_text=_("International Standard Serial Number"),
    )
    isbn = models.CharField(
        _("ISBN"),
        max_length=20,
        blank=True,
        help_text=_("International Standard Book Number"),
    )
    pmid = models.CharField(
        _("PMID"), blank=True, max_length=20, help_text=_("Pubmed ID")
    )

    # Book
    booktitle = models.CharField(
        _("Book title"),
        max_length=50,
        blank=True,
        help_text=_("The title of the book, if only part of it is being cited"),
    )
    edition = models.CharField(
        _("Edition"),
        max_length=100,
        blank=True,
        help_text=_(
            "The edition of a book, long form (such as 'First' or " "'Second')"
        ),
    )
    chapter = models.CharField(_("Chapter number"), max_length=50, blank=True)

    # PhD Thesis
    school = models.CharField(
        _("School"),
        max_length=50,
        blank=True,
        help_text=_("The school where the thesis was written"),
    )

    # Proceedings
    organization = models.CharField(
        _("Organization"),
        max_length=50,
        blank=True,
        help_text=_("The conference sponsor"),
    )

    # Misc
    editors = models.ManyToManyField("Editor", related_name="entries", blank=True)
    publisher = models.ForeignKey(
        "Publisher",
        related_name="entries",
        null=True,
        blank=True,
        on_delete=models.CASCADE,
    )
    address = models.CharField(
        _("Address"),
        max_length=250,
        blank=True,
        help_text=_(
            "Publisher's address (usually just the city, but can be the full "
            "address for lesser-known publishers)"
        ),
    )
    annote = models.CharField(
        _("Annote"),
        max_length=250,
        blank=True,
        help_text=_("An annotation for annotated bibliography styles (not typical)"),
    )
    note = models.TextField(
        _("Note"), blank=True, help_text=_("Miscellaneous extra information")
    )

    # Related publications
    crossref = models.ManyToManyField("self", blank=True)

    class Meta:
        verbose_name = _("Entry")
        verbose_name_plural = _("Entries")
        ordering = ("-publication_date",)

    def __str__(self):
        """Format entry with a default bibliography style"""
        # Authors
        author_str = "%(last_name)s %(first_initial)s"
        s = ", ".join([author_str % a.__dict__ for a in self.get_authors()])
        s = ", and ".join(s.rsplit(", ", 1))  # last author case
        s += ", "

        # Title
        s += '"%(title)s", ' % self.__dict__

        # Journal
        if self.journal.abbreviation:
            s += "in %(abbreviation)s, " % self.journal.__dict__
        else:
            # fall back to the real name
            s += "in %(name)s, " % self.journal.__dict__

        # Misc
        if self.volume and self.pages:
            s += "vol. %(volume)s, pp. %(pages)s, " % self.__dict__
        if self.publication_date:
            s += "%s." % self.publication_date.strftime("%B %Y")

        return s

    def _get_first_author(self):
        """
        Get this entry first author
        """
        if not len(self.get_authors()):
            return ""
        return self.get_authors()[0]

    first_author = property(_get_first_author)

    def _get_last_author(self):
        """
        Get this entry last author
        """
        if not len(self.get_authors()):
            return ""
        return self.get_authors()[-1]

    last_author = property(_get_last_author)

    def get_authors(self):
        """
        Get ordered authors list

        Note that authorentryrank_set is ordered as expected while the authors
        queryset is not (M2M with a through case).
        """
        return [aer.author for aer in self.authorentryrank_set.all()] class Journal(AbstractEntity):
    """Peer reviewed journal"""

    class Meta:
        verbose_name = _("Journal")
        verbose_name_plural = _("Journals") class DOILoader(BaseLoader):
    """DOILoader

    This loader is designed to fetch & import a list of DOIs

    Usage:

    >>> from td_biblio.utils.loaders import DOILoader
    >>> loader = DOILoader()
    >>> loader.load_records(DOIs='10.1021/ct500592m')
    >>> loader.save_records()
    """

    def to_record(self, input):
        """Convert crossref item to a valid record"""

        date_parts = input["issued"]["date-parts"][0]
        is_partial_publication_date = len(date_parts) != 3

        # Fill date parts if only a year is given
        if is_partial_publication_date:
            date_parts = date_parts + ((3 - len(date_parts)) * [1])

        # Journal name defaults to 'short-container-title'
        # It falls back to 'container-title'
        if input.get("short-container-title"):
            journal = input["short-container-title"][0]
        else:
            journal = input.get("container-title", "")

        record = {
            "title": input.get("title", ""),
            "authors": [
                {"first_name": a.get("given", ""), "last_name": a.get("family", "")}
                for a in input.get("author")
            ],
            "journal": journal,
            "volume": input.get("volume", ""),
            "number": input.get("issue", ""),
            "pages": input.get("page", ""),
            "year": date_parts[0],
            "publication_date": datetime.date(*date_parts),
            "is_partial_publication_date": is_partial_publication_date,
        }

        return record

    def load_records(self, DOIs=None):
        """Load all crossref items as valid records"""

        records = cn.content_negotiation(ids=DOIs, format="citeproc-json")
        # Records might be a str or unicode (python 2)
        if not isinstance(records, list):
            records = [records]
        self.records = []
        for r in records:
            data = json.loads(r)
            try:
                record = self.to_record(data)
            except Exception:
                e, v, tb = sys.exc_info()
                msg = _(
                    "An error occured while loading the following DOI: {}. "
                    "Check logs for details."
                ).format(data.get("DOI"))
                logger.error("{}, error: {} [{}], data: {}".format(msg, e, v, data))
                raise DOILoaderError(msg)
            self.records.append(record) class PubmedLoader(BaseLoader):
    """PubmedLoader

    This loader is designed to fetch & import a list of Pubmed IDs

    Usage:

    >>> from td_biblio.utils.loaders import PubmedLoader
    >>> loader = PubmedLoader()
    >>> loader.load_records(PMIDs=26588162)
    >>> loader.save_records()
    """

    def __init__(self, *args, **kwargs):
        super(PubmedLoader, self).__init__(*args, **kwargs)
        try:
            self.client = eutils.client.Client()
        except AttributeError:
            self.client = eutils.Client()

    def to_record(self, input):
        """Convert eutils PubmedArticle xml facade to a valid record"""

        record = {
            "title": input.title,
            "authors": [],
            "journal": input.jrnl,
            "volume": input.volume,
            "number": input.issue if input.issue is not None else "",
            "pages": input.pages,
            "year": input.year,
            "publication_date": datetime.date(int(input.year), 1, 1),
            "is_partial_publication_date": True,
        }

        for author in input.authors:
            splited = author.split()
            record["authors"].append(
                {"first_name": " ".join(splited[1:]), "last_name": splited[0]}
            )

        return record

    def load_records(self, PMIDs=None):
        """Load all PMIDs as valid records"""

        entries = self.client.efetch(db="pubmed", id=PMIDs)
        self.records = []

        for entry in entries:
            try:
                record = self.to_record(entry)
            except Exception:
                e, v, tb = sys.exc_info()
                msg = _(
                    "An error occured while loading the following PMID: {}. "
                    "Check logs for details."
                ).format(entry.pmid)
                logger.error("{}, error: {} [{}], data: {}".format(msg, e, v, entry))
                raise PMIDLoaderError(msg)
            self.records.append(record)
import datetime
import logging
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
    from django.core.urlresolvers import reverse_lazy
    from django.urls import reverse_lazy
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, ListView
from django.views.generic.edit import FormMixin
from .exceptions import DOILoaderError, PMIDLoaderError
from .forms import AuthorDuplicatesForm, EntryBatchImportForm
from .models import Author, Collection, Entry, Journal
from .utils.loaders import DOILoader, PubmedLoader
    return actual_decorator


class LoginRequiredMixin(object):
    @classmethod
    def as_view(cls, **initkwargs):
        view = super(LoginRequiredMixin, cls).as_view(**initkwargs)
        return login_required(view)


class SuperuserRequiredMixin(object):
    @classmethod
    def as_view(cls, **initkwargs):
        view = super(SuperuserRequiredMixin, cls).as_view(**initkwargs)
        return superuser_required(view)


class EntryListView(ListView):
    """Entry list view"""

    model = Entry
    paginate_by = 20
    template = "td_biblio/entry_list.html"

    def get(self, request, *args, **kwargs):
        """Check GET request parameters validity and store them"""

        # -- Publication year
        year = self.request.GET.get("year", None)
        if year is not None:
            try:
                year = datetime.date(int(year), 1, 1)
            except ValueError:
                year = None
        self.current_publication_date = year

        # -- Publication author
        author = self.request.GET.get("author", None)
        if author is not None:
            try:
                author = int(author)
            except ValueError:
                author = None
        self.current_publication_author = author

        # -- Publication collection
        collection = self.request.GET.get("collection", None)
        if collection is not None:
            try:
                collection = int(collection)
            except ValueError:
                collection = None
        self.current_publication_collection = collection

        return super(EntryListView, self).get(request, *args, **kwargs)

    def get_queryset(self):
        """
        Add GET requests filters
        """
        filters = dict()

        # Publication date
        if self.current_publication_date:
            year = self.current_publication_date.year
            filters["publication_date__year"] = year

        # Publication authors
        if self.current_publication_author:
            author = Author.objects.get(id=self.current_publication_author)
            aliases = list(author.aliases.values_list("id", flat=True))
            filters["authors__id__in"] = [author.id] + aliases

        # Publication collection
        if self.current_publication_collection:
            filters["collections__id"] = self.current_publication_collection

        # Base queryset
        qs = super(EntryListView, self).get_queryset()

        # Return filtered queryset
        return qs.filter(**filters)

    def get_context_data(self, **kwargs):
        """
        Add filtering data to context
        """
        ctx = super(EntryListView, self).get_context_data(**kwargs)

        # -- Metrics
        # Publications (Entries)
        ctx["n_publications_total"] = Entry.objects.count()
        ctx["n_publications_filter"] = self.get_queryset().count()

        # Authors (from selected entries)
        ctx["n_authors_total"] = Author.objects.filter(alias=None).count()
        author_ids = self.get_queryset().values_list("authors__id", flat=True)
        author_ids = list(set(author_ids))
        filtered_authors = Author.objects.filter(id__in=author_ids, alias=None)
        ctx["n_authors_filter"] = filtered_authors.count()

        # Journals (Entries)
        ctx["n_journals_total"] = Journal.objects.count()
        journal_ids = self.get_queryset().values_list("journal__id", flat=True)
        journal_ids = list(set(journal_ids))
        ctx["n_journals_filter"] = len(journal_ids)

        # -- Filters
        # publication date
        ctx["publication_years"] = self.get_queryset().dates(
            "publication_date", "year", order="DESC"
        )
        ctx["current_publication_year"] = self.current_publication_date

        # Publication author
        authors_order = ("last_name", "first_name")
        ctx["publication_authors"] = filtered_authors.order_by(*authors_order)
        ctx["current_publication_author"] = self.current_publication_author

        # Publication collection

        ctx["publication_collections"] = Collection.objects.all()