Skip to content
This repository has been archived by the owner on Apr 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #1208 from vbessonov/feature/proquest-integration
Browse files Browse the repository at this point in the history
Add ProQuest integration
  • Loading branch information
vbessonov committed Nov 13, 2020
2 parents b95f340 + 524bc2f commit 5fce4b3
Show file tree
Hide file tree
Showing 12 changed files with 413 additions and 127 deletions.
26 changes: 23 additions & 3 deletions model/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class ExternalIntegration(Base, HasFullTableCache):
FEEDBOOKS = DataSourceConstants.FEEDBOOKS
LCP = DataSourceConstants.LCP
MANUAL = DataSourceConstants.MANUAL
PROQUEST = DataSourceConstants.PROQUEST

# These protocols were used on the Content Server when mirroring
# content from a given directory or directly from Project
Expand Down Expand Up @@ -524,6 +525,7 @@ def key(setting):
lines.append(explanation)
return lines


class ConfigurationSetting(Base, HasFullTableCache):
"""An extra piece of site configuration.
A ConfigurationSetting may be associated with an
Expand Down Expand Up @@ -882,6 +884,8 @@ class ConfigurationAttributeType(Enum):
TEXTAREA = 'textarea'
SELECT = 'select'
NUMBER = 'number'
LIST = 'list'
MENU = 'menu'

def to_control_type(self):
"""Converts the value to a attribute type understandable by circulation-web
Expand Down Expand Up @@ -909,6 +913,7 @@ class ConfigurationAttribute(Enum):
DEFAULT = 'default'
OPTIONS = 'options'
CATEGORY = 'category'
FORMAT = 'format'


class ConfigurationOption(object):
Expand Down Expand Up @@ -1035,7 +1040,9 @@ def __init__(
default=None,
options=None,
category=None,
index=None):
format=None,
index=None
):
"""Initializes a new instance of ConfigurationMetadata class
:param key: Setting's key
Expand Down Expand Up @@ -1070,6 +1077,7 @@ def __init__(
self._default = default
self._options = options
self._category = category
self._format = format

if index is not None:
self._index = index
Expand Down Expand Up @@ -1187,6 +1195,15 @@ def category(self):
"""
return self._category

@property
def format(self):
"""Returns the setting's format
:return: Setting's format
:rtype: string
"""
return self._format

@property
def index(self):
return self._index
Expand Down Expand Up @@ -1224,7 +1241,8 @@ def to_settings(self):
[option.to_settings() for option in self.options]
if self.options
else None,
ConfigurationAttribute.CATEGORY.value: self.category
ConfigurationAttribute.CATEGORY.value: self.category,
ConfigurationAttribute.FORMAT.value: self.format
}


Expand Down Expand Up @@ -1288,6 +1306,7 @@ def to_settings(cls):
default_attribute = getattr(member, ConfigurationAttribute.DEFAULT.value, None)
options_attribute = getattr(member, ConfigurationAttribute.OPTIONS.value, None)
category_attribute = getattr(member, ConfigurationAttribute.CATEGORY.value, None)
format_attribute = getattr(member, ConfigurationAttribute.FORMAT.value, None)

settings.append({
ConfigurationAttribute.KEY.value: key_attribute,
Expand All @@ -1300,7 +1319,8 @@ def to_settings(cls):
[option.to_settings() for option in options_attribute]
if options_attribute
else None,
ConfigurationAttribute.CATEGORY.value: category_attribute
ConfigurationAttribute.CATEGORY.value: category_attribute,
ConfigurationAttribute.FORMAT.value: format_attribute
})

return settings
Expand Down
3 changes: 3 additions & 0 deletions model/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class DataSourceConstants(object):
BIBBLIO = u"Bibblio"
ENKI = u"Enki"
LCP = u"LCP"
PROQUEST = u"ProQuest"

DEPRECATED_NAMES = {
u"3M" : BIBLIOTHECA,
Expand Down Expand Up @@ -140,6 +141,7 @@ class EditionConstants(object):
COURSEWARE_MEDIUM: "courseware"
}


class IdentifierConstants(object):
# Common types of identifiers.
OVERDRIVE_ID = u"Overdrive ID"
Expand All @@ -163,6 +165,7 @@ class IdentifierConstants(object):
BIBBLIO_CONTENT_ITEM_ID = u"Bibblio Content Item ID"
ENKI_ID = u"Enki ID"
SUDOC_CALL_NUMBER = u"SuDoc Call Number"
PROQUEST_ID = u"ProQuest Doc ID"

DEPRECATED_NAMES = {
u"3M ID" : BIBLIOTHECA_ID,
Expand Down
156 changes: 121 additions & 35 deletions model/credential.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
# encoding: utf-8
# Credential, DRMDeviceIdentifier, DelegatedPatronIdentifier
from nose.tools import set_trace

from . import (
Base,
get_one,
get_one_or_create,
)

import datetime
import uuid

import sqlalchemy
from nose.tools import set_trace
from sqlalchemy import (
Column,
DateTime,
Expand All @@ -18,13 +14,14 @@
String,
UniqueConstraint,
)
from sqlalchemy.orm import (
backref,
relationship,
)
from sqlalchemy.orm import backref, relationship
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.expression import and_
import uuid

from ..util import is_session
from ..util.string_helpers import is_string
from . import Base, get_one, get_one_or_create


class Credential(Base):
"""A place to store credentials for external services."""
Expand Down Expand Up @@ -89,14 +86,40 @@ class Credential(Base):
IDENTIFIER_FROM_REMOTE_SERVICE = "Identifier Received From Remote Service"

@classmethod
def lookup(self, _db, data_source, type, patron, refresher_method,
def _filter_invalid_credential(cls, credential, allow_persistent_token):
"""Filter out invalid credentials based on their expiration time and persistence.
:param credential: Credential object
:type credential: Credential
:param allow_persistent_token: Boolean value indicating whether persistent tokens are allowed
:type allow_persistent_token: bool
"""
if not credential:
# No matching token.
return None

if not credential.expires:
if allow_persistent_token:
return credential
else:
# It's an error that this token never expires. It's invalid.
return None
elif credential.expires > datetime.datetime.utcnow():
return credential
else:
# Token has expired.
return None

@classmethod
def lookup(cls, _db, data_source, token_type, patron, refresher_method,
allow_persistent_token=False, allow_empty_token=False,
collection=None, force_refresh=False):
from datasource import DataSource
if isinstance(data_source, basestring):
if is_string(data_source):
data_source = DataSource.lookup(_db, data_source)
credential, is_new = get_one_or_create(
_db, Credential, data_source=data_source, type=type, patron=patron, collection=collection)
_db, Credential, data_source=data_source, type=token_type, patron=patron, collection=collection)
if (is_new
or force_refresh
or (not credential.expires and not allow_persistent_token)
Expand All @@ -108,32 +131,88 @@ def lookup(self, _db, data_source, type, patron, refresher_method,
return credential

@classmethod
def lookup_by_token(self, _db, data_source, type, token,
allow_persistent_token=False):
def lookup_by_token(
cls,
_db,
data_source,
token_type,
token,
allow_persistent_token=False
):
"""Look up a unique token.
Lookup will fail on expired tokens. Unless persistent tokens
are specifically allowed, lookup will fail on persistent tokens.
"""

credential = get_one(
_db, Credential, data_source=data_source, type=type,
_db, Credential, data_source=data_source, type=token_type,
credential=token)

if not credential:
# No matching token.
return None
return cls._filter_invalid_credential(credential, allow_persistent_token)

if not credential.expires:
if allow_persistent_token:
return credential
else:
# It's an error that this token never expires. It's invalid.
return None
elif credential.expires > datetime.datetime.utcnow():
return credential
else:
# Token has expired.
return None
@classmethod
def lookup_by_patron(
cls,
_db,
data_source_name,
token_type,
patron,
allow_persistent_token=False,
auto_create_datasource=True
):
"""Look up a unique token.
Lookup will fail on expired tokens. Unless persistent tokens
are specifically allowed, lookup will fail on persistent tokens.
:param _db: Database session
:type _db: sqlalchemy.orm.session.Session
:param data_source_name: Name of the data source
:type data_source_name: str
:param token_type: Token type
:type token_type: str
:param patron: Patron object
:type patron: core.model.patron.Patron
:param allow_persistent_token: Boolean value indicating whether persistent tokens are allowed or not
:type allow_persistent_token: bool
:param auto_create_datasource: Boolean value indicating whether
a data source should be created in the case it doesn't
:type auto_create_datasource: bool
"""
from patron import Patron

if not is_session(_db):
raise ValueError('"_db" argument must be a valid SQLAlchemy session')
if not is_string(data_source_name) or not data_source_name:
raise ValueError('"data_source_name" argument must be a non-empty string')
if not is_string(token_type) or not token_type:
raise ValueError('"token_type" argument must be a non-empty string')
if not isinstance(patron, Patron):
raise ValueError('"patron" argument must be an instance of Patron class')
if not isinstance(allow_persistent_token, bool):
raise ValueError('"allow_persistent_token" argument must be boolean')
if not isinstance(auto_create_datasource, bool):
raise ValueError('"auto_create_datasource" argument must be boolean')

from datasource import DataSource
data_source = DataSource.lookup(
_db,
data_source_name,
autocreate=auto_create_datasource
)
credential = get_one(
_db,
Credential,
data_source=data_source,
type=token_type,
patron=patron
)

return cls._filter_invalid_credential(credential, allow_persistent_token)

@classmethod
def lookup_and_expire_temporary_token(cls, _db, data_source, type, token):
Expand All @@ -147,15 +226,21 @@ def lookup_and_expire_temporary_token(cls, _db, data_source, type, token):

@classmethod
def temporary_token_create(
self, _db, data_source, type, patron, duration, value=None
cls,
_db,
data_source,
token_type,
patron,
duration,
value=None
):
"""Create a temporary token for the given data_source/type/patron.
The token will be good for the specified `duration`.
"""
expires = datetime.datetime.utcnow() + duration
token_string = value or str(uuid.uuid1())
credential, is_new = get_one_or_create(
_db, Credential, data_source=data_source, type=type, patron=patron)
_db, Credential, data_source=data_source, type=token_type, patron=patron)
# If there was already a token of this type for this patron,
# the new one overwrites the old one.
credential.credential=token_string
Expand Down Expand Up @@ -223,6 +308,7 @@ class DRMDeviceIdentifier(Base):
credential_id = Column(Integer, ForeignKey('credentials.id'), index=True)
device_identifier = Column(String(255), index=True)


class DelegatedPatronIdentifier(Base):
"""This library is in charge of coming up with, and storing,
identifiers associated with the patrons of some other library.
Expand Down
3 changes: 2 additions & 1 deletion model/datasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ def well_known_sources(cls, _db):
(cls.INTERNAL_PROCESSING, False, False, None, None),
(cls.FEEDBOOKS, True, False, IdentifierConstants.URI, None),
(cls.BIBBLIO, False, True, IdentifierConstants.BIBBLIO_CONTENT_ITEM_ID, None),
(cls.ENKI, True, False, IdentifierConstants.ENKI_ID, None)
(cls.ENKI, True, False, IdentifierConstants.ENKI_ID, None),
(cls.PROQUEST, True, False, IdentifierConstants.PROQUEST_ID, None)
):

obj = DataSource.lookup(
Expand Down

0 comments on commit 5fce4b3

Please sign in to comment.