+ re_id = re.compile(u".*([^<]+).*",
+ re.MULTILINE | re.DOTALL | re.UNICODE)
+ re_sub_id = re.compile(u"().*()", re.UNICODE)
+
+ # Metashare glitch
+ def _id_from_meta( raw_xml_record ):
+ m = re_id.search(raw_xml_record)
+ return m.group(1) if m else None
+
+ from metashare.storage.models import StorageObject
+
+ storage_objects = StorageObject.objects.all()
+ so_map = {}
+ for so in storage_objects:
+ so_map[_id_from_meta(so.metadata)] = so
+
+ def _find_id_in_current_db( raw_xml_record ):
+ id_str = _id_from_meta(raw_xml_record)
+ return so_map[id_str] if id_str in so_map else None
+
+ pos = 0
+ client = _client(url_str, _reader(metadata_str))
+ for header, metadata, _2 in client.listRecords(metadataPrefix=metadata_str):
+ pos += 1
+ try:
+ if header.isDeleted():
+ ret_d[header.identifier()] = html_mark_warning(u"This record is deleted - delete it manually!!!"), \
+ header.identifier(), \
+ None
+ continue
+
+ # try to publish it on this node
+ raw_xml_record = metadata.getField("raw_xml")
+ for re_sub in re_subs:
+ raw_xml_record = re_sub.sub(u"", raw_xml_record)
+
+ if not itemid is None and not itemid in header.identifier():
+ _logger.info(u"Skipping [%s]", header.identifier())
+ continue
+
+ # e.g., for debugging
+ if not save_to_dir is None:
+ with codecs.open(os.path.join(save_to_dir, "UFAL-metashare-%s.xml" % pos),
+ mode="w+", encoding="utf-8") as fout:
+ fout.write(raw_xml_record)
+
+ # 3. do the import
+ #
+ # 2.5 find the id and check if we already have it
+ obj = _find_id_in_current_db(raw_xml_record)
+ if obj:
+ raw_xml_record = re_sub_id.sub(u"%s" % obj.identifier,
+ raw_xml_record, count=1)
+ _logger.info(u"Item [%s]->[%s] found in database",
+ _id_from_meta(raw_xml_record), obj.identifier)
+
+ resource = import_from_string(raw_xml_record, PUBLISHED, MASTER)
+ ret_d[header.identifier()] = OK_MSG, \
+ repr(resource), \
+ resource.storage_object._storage_folder()
+ except Exception, e:
+ ret_d[header.identifier()] = html_mark_error(u"Failed!"), \
+ html_mark_error(repr(e)), \
+ None
+
+ # 4. get ORE
+ #
+ only_in_ore = u"Found item in ORE but not in metadata format"
+ ret_d[only_in_ore] = []
+
+ metadata_str = "ore"
+ client = _client(url_str, _reader(metadata_str))
+ if import_from_dir:
+ for header, metadata, _2 in client.listRecords(metadataPrefix=metadata_str):
+
+ if not header.identifier() in ret_d or metadata is None:
+ ret_d[only_in_ore].append(header.identifier())
+ continue
+
+ # noinspection PyBroadException,PyUnusedLocal
+ try:
+ raw_xml_record = metadata.getField("raw_xml")
+ info, resource, import_to_path = ret_d[header.identifier()]
+
+ if OK_MSG != info:
+ continue
+
+ # find the path
+ imported_storage_files = re.compile(u"([^<]+)") \
+ .findall(raw_xml_record)
+ imported_storage_files = [os.path.join(import_from_dir, x) for x in imported_storage_files]
+ for imported_storage_file in imported_storage_files:
+ if not os.path.exists(imported_storage_file):
+ warn_msg = u" | Could not find imported file [%s]" % imported_storage_file
+ ret_d[header.identifier()] = info + warn_msg, \
+ resource + raw_xml_record, \
+ import_to_path
+ imported_storage_files = [x for x in imported_storage_files if os.path.exists(x)]
+
+ # hardcoded in storage/models.py
+ new_zip_path = os.path.join(import_to_path, 'archive.zip')
+ # we know where, we know from where
+ if os.path.exists(new_zip_path):
+ warn_msg = u" | Removed existing zip file"
+ ret_d[header.identifier()] = info + warn_msg, \
+ resource + raw_xml_record, \
+ import_to_path
+ safe_remove(new_zip_path)
+
+ # 5. copy and update
+ #
+ with ZipFile(new_zip_path, 'w') as new_zip:
+ for f in imported_storage_files:
+ new_zip.write(f, arcname=os.path.basename(f))
+
+ # all is where it should be and now update
+ object_id = os.path.basename(os.path.dirname(new_zip_path))
+ # Metashare glitch
+ #noinspection PyUnresolvedReferences
+ metashare.storage_admin.StorageObject = StorageObject
+ #noinspection PyUnresolvedReferences
+ metashare.storage_admin.checksum(object_id)
+ except Exception, e:
+ _logger.exception( u"Got exception while importing" )
+
+ # make html friendly
+ for k, v in ret_d.iteritems():
+ ret_d[k] = prehtmlify(v)
+
+ return ret_d
+
+
+class MetashareOaiServer(object):
+ """
+ The metashare implementation of the OAI verbs.
+
+ This is the first shot on a real implementation which is left for
+ the original authors; however, the basics and the api usage
+ is shown below.
+ """
+
+ def __init__( self, env ):
+ """
+ Settings, required arguments are `name`, `url`, `adminEmails`.
+ """
+ self.env = env
+
+ def identify(self):
+ """
+ OAI-PMH ?verb=identify.
+ See http://www.openarchives.org/OAI/openarchivesprotocol.html#Identify
+ """
+ return common.Identify(
+ repositoryName=self.env["name"],
+ baseURL=self.env["url"],
+ protocolVersion="2.0",
+ adminEmails=self.env["adminEmails"],
+ earliestDatestamp=datetime(2004, 1, 1),
+ deletedRecord='no',
+ granularity='YYYY-MM-DDThh:mm:ssZ',
+ compression=['identity'])
+
+ def getRecord(self, metadataPrefix, identifier):
+ """
+ TODO! one item is composed of
+ (common.Header(str(i), datestamp, '', False),
+ common.Metadata({'title': ['Title %s' % i]}),
+ None) # about
+
+ """
+ try:
+ return []
+ except IndexError:
+ raise oaipmh.error.IdDoesNotExistError, "Id does not exist: %s" % identifier
+
+ def listIdentifiers(self, metadataPrefix=None, from_=None, until=None, set=None):
+ """
+ TODO! check metadataPrefix
+ """
+ from metashare.storage.models import StorageObject
+
+ # get all objects according to id
+ re_id = re.compile(u".*([^<]+).*",
+ re.MULTILINE | re.DOTALL | re.UNICODE)
+
+ def _id_from_meta( raw_xml_record ):
+ m = re_id.search(raw_xml_record)
+ return m.group(1) if m else None
+
+ storage_objects = StorageObject.objects.all()
+ so_map = {}
+ result = []
+ for so in storage_objects:
+ i = _id_from_meta(so.metadata)
+ so_map[i] = so
+ result.append(
+ common.Header(str(i), so.created, '', False) )
+ return result
+
+ def listRecords(self, metadataPrefix=None, from_=None, until=None, set=None):
+ """
+ TODO!
+ resource = get_object_or_404(resourceInfoType_model,
+ storage_object__identifier=object_id,
+ storage_object__publication_status=PUBLISHED)
+ """
+ result = []
+ return result
+
+
+#==========================
+# Supported commands
+#==========================
+
+key_list_ids_for_import = u"list identifiers (use for import)"
+key_import = u"Import the resource(s)"
+key_list_metadata_format = u"list formats"
+
+supported_commands = OrderedDict()
+supported_commands[key_import] = import_id
+supported_commands[key_list_ids_for_import] = list_identifiers
+
+# available as separate buttons
+supported_commands.update({
+ u"list records": list_records,
+ u"identify server": identify,
+ u"list sets": list_sets,
+ key_list_metadata_format: list_formats,
+})
+
+
+#
+#
+if __name__ == "__main__":
+ pass
diff --git a/metashare/oai_pmh/_metadatareader.py b/metashare/oai_pmh/_metadatareader.py
new file mode 100644
index 000000000..9fd8028dd
--- /dev/null
+++ b/metashare/oai_pmh/_metadatareader.py
@@ -0,0 +1,25 @@
+# coding=utf-8
+# pylint:
+
+"""
+ OAI-PMH metashare scheme (specific) reader.
+
+ @author: jm (UFAL, Charles University in Prague)
+ @www: http://ufal-point.mff.cuni.cz
+ @version: 0.1
+"""
+
+from oaipmh import common
+from lxml import etree
+
+
+class metashare_schema_reader( object ):
+ """
+ An implementation of a reader.
+ """
+ def __init__(self):
+ pass
+
+ def __call__( self, lxml_elem ):
+ xml = etree.tostring( lxml_elem, pretty_print=True )
+ return common.Metadata( { "raw_xml": xml } )
diff --git a/metashare/oai_pmh/_utils.py b/metashare/oai_pmh/_utils.py
new file mode 100644
index 000000000..d25097904
--- /dev/null
+++ b/metashare/oai_pmh/_utils.py
@@ -0,0 +1,79 @@
+# coding=utf-8
+# pylint:
+
+"""
+ Helpers.
+
+ @author: jm (UFAL, Charles University in Prague)
+ @www: http://ufal-point.mff.cuni.cz
+ @version: 0.1
+"""
+
+import os
+import json
+import time
+
+
+def params( d, param_list ):
+ """
+ Check parameters and return the values or raise
+ AttributeError if missing.
+ """
+ for param in d:
+ if not param in d:
+ raise AttributeError("Missing [%s] parameter", param)
+ if 1 == len(param_list):
+ return d[param_list[0]]
+ else:
+ return [d[param] for param in param_list]
+
+
+def prehtmlify( obj, add_pre=True ):
+ """
+ Make object nicely displayable in .
+ """
+ if not obj is None:
+ str_ = u"%s" % json.dumps(obj, indent=2)
+ return u"%s
" % str_ \
+ if add_pre else str_
+ return u"Invalid"
+
+
+def smart_extend( dict_inst, *dict_arr ):
+ """
+ Recursively add items at every level to
+ dict_inst rather than simply update.
+ """
+ for a in dict_arr:
+ dict_inst.update(a)
+ return dict_inst
+
+
+def time_it( func, *args, **kwargs ):
+ """
+ Time a function.
+ Alternative implement as a decorator.
+ """
+ t1 = time.time()
+ ret = func(*args, **kwargs)
+ t2 = time.time()
+ return t2 - t1, ret
+
+
+# noinspection PyBroadException
+def safe_remove( file_str ):
+ """ Do not raise if not successful. """
+ try:
+ os.remove(file_str)
+ except:
+ pass
+
+
+def html_mark_error( s ):
+ """ Simple mark error using html and css. """
+ return u"%s
" % s
+
+
+def html_mark_warning( s ):
+ """ Simple mark warning using html and css. """
+ return u"%s
" % s
diff --git a/metashare/oai_pmh/oaipmh/__init__.py b/metashare/oai_pmh/oaipmh/__init__.py
new file mode 100644
index 000000000..1e0ca6a5f
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/__init__.py
@@ -0,0 +1 @@
+#
diff --git a/metashare/oai_pmh/oaipmh/client.py b/metashare/oai_pmh/oaipmh/client.py
new file mode 100644
index 000000000..72ce5e28b
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/client.py
@@ -0,0 +1,371 @@
+# Copyright 2003, 2004, 2005 Infrae
+# Released under the BSD license (see LICENSE.txt)
+from __future__ import nested_scopes
+import urllib2
+import base64
+from urllib import urlencode
+from StringIO import StringIO
+from types import SliceType
+from lxml import etree
+import time
+
+from oaipmh import common, metadata, validation, error
+from oaipmh.datestamp import datestamp_to_datetime, datetime_to_datestamp
+
+WAIT_DEFAULT = 120 # two minutes
+WAIT_MAX = 5
+
+class Error(Exception):
+ pass
+
+class BaseClient(common.OAIPMH):
+
+ def __init__(self, metadata_registry=None):
+ self._metadata_registry = (
+ metadata_registry or metadata.global_metadata_registry)
+ self._ignore_bad_character_hack = 0
+ self._day_granularity = False
+
+ def updateGranularity(self):
+ """Update the granularity setting dependent on that the server says.
+ """
+ identify = self.identify()
+ granularity = identify.granularity()
+ if granularity == 'YYYY-MM-DD':
+ self._day_granularity = True
+ elif granularity == 'YYYY-MM-DDThh:mm:ssZ':
+ self._day_granularity= False
+ else:
+ raise Error, "Non-standard granularity on server: %s" % granularity
+
+ def handleVerb(self, verb, kw):
+ # validate kw first
+ validation.validateArguments(verb, kw)
+ # encode datetimes as datestamps
+ from_ = kw.get('from_')
+ if from_ is not None:
+ # turn it into 'from', not 'from_' before doing actual request
+ kw['from'] = datetime_to_datestamp(from_,
+ self._day_granularity)
+ if 'from_' in kw:
+ # always remove it from the kw, no matter whether it be None or not
+ del kw['from_']
+
+ until = kw.get('until')
+ if until is not None:
+ kw['until'] = datetime_to_datestamp(until,
+ self._day_granularity)
+ elif 'until' in kw:
+ # until is None but is explicitly in kw, remove it
+ del kw['until']
+
+ # now call underlying implementation
+ method_name = verb + '_impl'
+ return getattr(self, method_name)(
+ kw, self.makeRequestErrorHandling(verb=verb, **kw))
+
+ def getNamespaces(self):
+ """Get OAI namespaces.
+ """
+ return {'oai': 'http://www.openarchives.org/OAI/2.0/'}
+
+ def getMetadataRegistry(self):
+ """Return the metadata registry in use.
+
+ Do we want to allow the returning of the global registry?
+ """
+ return self._metadata_registry
+
+ def ignoreBadCharacters(self, true_or_false):
+ """Set to ignore bad characters in UTF-8 input.
+ This is a hack to get around well-formedness errors of
+ input sources which *should* be in UTF-8 but for some reason
+ aren't completely.
+ """
+ self._ignore_bad_character_hack = true_or_false
+
+ def parse(self, xml):
+ """Parse the XML to a lxml tree.
+ """
+ # XXX this is only safe for UTF-8 encoded content,
+ # and we're basically hacking around non-wellformedness anyway,
+ # but oh well
+ if self._ignore_bad_character_hack:
+ xml = unicode(xml, 'UTF-8', 'replace')
+ # also get rid of character code 12
+ xml = xml.replace(chr(12), '?')
+ xml = xml.encode('UTF-8')
+ return etree.XML(xml)
+
+ def GetRecord_impl(self, args, tree):
+ records, token = self.buildRecords(
+ args['metadataPrefix'],
+ self.getNamespaces(),
+ self._metadata_registry,
+ tree
+ )
+ assert token is None
+ return records[0]
+
+ # implementation of the various methods, delegated here by
+ # handleVerb method
+
+ def Identify_impl(self, args, tree):
+ namespaces = self.getNamespaces()
+ evaluator = etree.XPathEvaluator(tree, namespaces=namespaces)
+ identify_node = evaluator.evaluate(
+ '/oai:OAI-PMH/oai:Identify')[0]
+ identify_evaluator = etree.XPathEvaluator(identify_node,
+ namespaces=namespaces)
+ e = identify_evaluator.evaluate
+
+ repositoryName = e('string(oai:repositoryName/text())')
+ baseURL = e('string(oai:baseURL/text())')
+ protocolVersion = e('string(oai:protocolVersion/text())')
+ adminEmails = e('oai:adminEmail/text()')
+ earliestDatestamp = datestamp_to_datetime(
+ e('string(oai:earliestDatestamp/text())'))
+ deletedRecord = e('string(oai:deletedRecord/text())')
+ granularity = e('string(oai:granularity/text())')
+ compression = e('oai:compression/text()')
+ # XXX description
+ identify = common.Identify(
+ repositoryName, baseURL, protocolVersion,
+ adminEmails, earliestDatestamp,
+ deletedRecord, granularity, compression)
+ return identify
+
+ def ListIdentifiers_impl(self, args, tree):
+ namespaces = self.getNamespaces()
+ def firstBatch():
+ return self.buildIdentifiers(namespaces, tree)
+ def nextBatch(token):
+ tree = self.makeRequestErrorHandling(verb='ListIdentifiers',
+ resumptionToken=token)
+ return self.buildIdentifiers(namespaces, tree)
+ return ResumptionListGenerator(firstBatch, nextBatch)
+
+ def ListMetadataFormats_impl(self, args, tree):
+ namespaces = self.getNamespaces()
+ evaluator = etree.XPathEvaluator(tree,
+ namespaces=namespaces)
+
+ metadataFormat_nodes = evaluator.evaluate(
+ '/oai:OAI-PMH/oai:ListMetadataFormats/oai:metadataFormat')
+ metadataFormats = []
+ for metadataFormat_node in metadataFormat_nodes:
+ e = etree.XPathEvaluator(metadataFormat_node,
+ namespaces=namespaces).evaluate
+ metadataPrefix = e('string(oai:metadataPrefix/text())')
+ schema = e('string(oai:schema/text())')
+ metadataNamespace = e('string(oai:metadataNamespace/text())')
+ metadataFormat = (metadataPrefix, schema, metadataNamespace)
+ metadataFormats.append(metadataFormat)
+
+ return metadataFormats
+
+ def ListRecords_impl(self, args, tree):
+ namespaces = self.getNamespaces()
+ metadata_prefix = args['metadataPrefix']
+ metadata_registry = self._metadata_registry
+ def firstBatch():
+ return self.buildRecords(
+ metadata_prefix, namespaces,
+ metadata_registry, tree)
+ def nextBatch(token):
+ tree = self.makeRequestErrorHandling(
+ verb='ListRecords',
+ resumptionToken=token)
+ return self.buildRecords(
+ metadata_prefix, namespaces,
+ metadata_registry, tree)
+ return ResumptionListGenerator(firstBatch, nextBatch)
+
+ def ListSets_impl(self, args, tree):
+ namespaces = self.getNamespaces()
+ def firstBatch():
+ return self.buildSets(namespaces, tree)
+ def nextBatch(token):
+ tree = self.makeRequestErrorHandling(
+ verb='ListSets',
+ resumptionToken=token)
+ return self.buildSets(namespaces, tree)
+ return ResumptionListGenerator(firstBatch, nextBatch)
+
+ # various helper methods
+
+ def buildRecords(self,
+ metadata_prefix, namespaces, metadata_registry, tree):
+ # first find resumption token if available
+ evaluator = etree.XPathEvaluator(tree,
+ namespaces=namespaces)
+ token = evaluator.evaluate(
+ 'string(/oai:OAI-PMH/*/oai:resumptionToken/text())')
+ if token.strip() == '':
+ token = None
+ record_nodes = evaluator.evaluate(
+ '/oai:OAI-PMH/*/oai:record')
+ result = []
+ for record_node in record_nodes:
+ record_evaluator = etree.XPathEvaluator(record_node,
+ namespaces=namespaces)
+ e = record_evaluator.evaluate
+ # find header node
+ header_node = e('oai:header')[0]
+ # create header
+ header = buildHeader(header_node, namespaces)
+ # find metadata node
+ metadata_list = e('oai:metadata')
+ if metadata_list:
+ metadata_node = metadata_list[0]
+ # create metadata
+ metadata = metadata_registry.readMetadata(metadata_prefix,
+ metadata_node)
+ else:
+ metadata = None
+ # XXX TODO: about, should be third element of tuple
+ result.append((header, metadata, None))
+ return result, token
+
+ def buildIdentifiers(self, namespaces, tree):
+ evaluator = etree.XPathEvaluator(tree,
+ namespaces=namespaces)
+ # first find resumption token is available
+ token = evaluator.evaluate(
+ 'string(/oai:OAI-PMH/oai:ListIdentifiers/oai:resumptionToken/text())')
+ if token.strip() == '':
+ token = None
+ header_nodes = evaluator.evaluate(
+ '/oai:OAI-PMH/oai:ListIdentifiers/oai:header')
+ result = []
+ for header_node in header_nodes:
+ header = buildHeader(header_node, namespaces)
+ result.append(header)
+ return result, token
+
+ def buildSets(self, namespaces, tree):
+ evaluator = etree.XPathEvaluator(tree,
+ namespaces=namespaces)
+ # first find resumption token if available
+ token = evaluator.evaluate(
+ 'string(/oai:OAI-PMH/oai:ListSets/oai:resumptionToken/text())')
+ if token.strip() == '':
+ token = None
+ set_nodes = evaluator.evaluate(
+ '/oai:OAI-PMH/oai:ListSets/oai:set')
+ sets = []
+ for set_node in set_nodes:
+ e = etree.XPathEvaluator(set_node,
+ namespaces=namespaces).evaluate
+ # make sure we get back unicode strings instead
+ # of lxml.etree._ElementUnicodeResult objects.
+ setSpec = unicode(e('string(oai:setSpec/text())'))
+ setName = unicode(e('string(oai:setName/text())'))
+ # XXX setDescription nodes
+ sets.append((setSpec, setName, None))
+ return sets, token
+
+ def makeRequestErrorHandling(self, **kw):
+ xml = self.makeRequest(**kw)
+ try:
+ tree = self.parse(xml)
+ except SyntaxError:
+ raise error.XMLSyntaxError(kw)
+ # check whether there are errors first
+ e_errors = tree.xpath('/oai:OAI-PMH/oai:error',
+ namespaces=self.getNamespaces())
+ if e_errors:
+ # XXX right now only raise first error found, does not
+ # collect error info
+ for e_error in e_errors:
+ code = e_error.get('code')
+ msg = e_error.text
+ if code not in ['badArgument', 'badResumptionToken',
+ 'badVerb', 'cannotDisseminateFormat',
+ 'idDoesNotExist', 'noRecordsMatch',
+ 'noMetadataFormats', 'noSetHierarchy']:
+ raise error.UnknownError,\
+ "Unknown error code from server: %s, message: %s" % (
+ code, msg)
+ # find exception in error module and raise with msg
+ raise getattr(error, code[0].upper() + code[1:] + 'Error'), msg
+ return tree
+
+ def makeRequest(self, **kw):
+ raise NotImplementedError
+
+class Client(BaseClient):
+ def __init__(
+ self, base_url, metadata_registry=None, credentials=None):
+ BaseClient.__init__(self, metadata_registry)
+ self._base_url = base_url
+ if credentials is not None:
+ self._credentials = base64.encodestring('%s:%s' % credentials)
+ else:
+ self._credentials = None
+
+ def makeRequest(self, **kw):
+ """Actually retrieve XML from the server.
+ """
+ # XXX include From header?
+ headers = {'User-Agent': 'pyoai'}
+ if self._credentials is not None:
+ headers['Authorization'] = 'Basic ' + self._credentials.strip()
+ request = urllib2.Request(
+ self._base_url, data=urlencode(kw), headers=headers)
+ return retrieveFromUrlWaiting(request)
+
+def buildHeader(header_node, namespaces):
+ e = etree.XPathEvaluator(header_node,
+ namespaces=namespaces).evaluate
+ identifier = e('string(oai:identifier/text())')
+ datestamp = datestamp_to_datetime(
+ str(e('string(oai:datestamp/text())')))
+ setspec = [str(s) for s in e('oai:setSpec/text()')]
+ deleted = e("@status = 'deleted'")
+ return common.Header(identifier, datestamp, setspec, deleted)
+
+def ResumptionListGenerator(firstBatch, nextBatch):
+ result, token = firstBatch()
+ while 1:
+ for item in result:
+ yield item
+ if token is None:
+ break
+ result, token = nextBatch(token)
+
+def retrieveFromUrlWaiting(request,
+ wait_max=WAIT_MAX, wait_default=WAIT_DEFAULT):
+ """Get text from URL, handling 503 Retry-After.
+ """
+ for i in range(wait_max):
+ try:
+ f = urllib2.urlopen(request)
+ text = f.read()
+ f.close()
+ # we successfully opened without having to wait
+ break
+ except urllib2.HTTPError, e:
+ if e.code == 503:
+ try:
+ retryAfter = int(e.hdrs.get('Retry-After'))
+ except TypeError:
+ retryAfter = None
+ if retryAfter is None:
+ time.sleep(wait_default)
+ else:
+ time.sleep(retryAfter)
+ else:
+ # reraise any other HTTP error
+ raise
+ else:
+ raise Error, "Waited too often (more than %s times)" % wait_max
+ return text
+
+class ServerClient(BaseClient):
+ def __init__(self, server, metadata_registry=None):
+ BaseClient.__init__(self, metadata_registry)
+ self._server = server
+
+ def makeRequest(self, **kw):
+ return self._server.handleRequest(kw)
diff --git a/metashare/oai_pmh/oaipmh/common.py b/metashare/oai_pmh/oaipmh/common.py
new file mode 100644
index 000000000..7335b0b2e
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/common.py
@@ -0,0 +1,192 @@
+import pkg_resources
+
+
+class Header(object):
+ def __init__(self, identifier, datestamp, setspec, deleted):
+ # force identifier to be a string, it might be
+ # an lxml.etree._ElementStringResult...
+ self._identifier = str(identifier)
+ self._datestamp = datestamp
+ self._setspec = setspec
+ self._deleted = deleted
+
+ def identifier(self):
+ return self._identifier
+
+ def datestamp(self):
+ return self._datestamp
+
+ def setSpec(self):
+ return self._setspec
+
+ def isDeleted(self):
+ return self._deleted
+
+class Metadata(object):
+ def __init__(self, map):
+ self._map = map
+
+ def getMap(self):
+ return self._map
+
+ def getField(self, name):
+ return self._map[name]
+
+ __getitem__ = getField
+
+class Identify(object):
+ def __init__(self, repositoryName, baseURL, protocolVersion, adminEmails,
+ earliestDatestamp, deletedRecord, granularity, compression,
+ toolkit_description=True):
+ self._repositoryName = repositoryName
+ self._baseURL = baseURL
+ self._protocolVersion = protocolVersion
+ self._adminEmails = adminEmails
+ self._earliestDatestamp = earliestDatestamp
+ self._deletedRecord = deletedRecord
+ self._granularity = granularity
+ self._compression = compression
+ self._descriptions = []
+
+ if toolkit_description:
+ req = pkg_resources.Requirement.parse('pyoai')
+ egg = pkg_resources.working_set.find(req)
+ if egg:
+ version = '%s' % egg.version
+ else:
+ version = ''
+ self.add_description(
+ ''
+ 'pyoai'
+ '%s'
+ 'http://infrae.com/products/oaipack'
+ '' % version)
+
+ def repositoryName(self):
+ return self._repositoryName
+
+ def baseURL(self):
+ return self._baseURL
+
+ def protocolVersion(self):
+ return self._protocolVersion
+
+ def adminEmails(self):
+ return self._adminEmails
+
+ def earliestDatestamp(self):
+ return self._earliestDatestamp
+
+ def deletedRecord(self):
+ return self._deletedRecord
+
+ def granularity(self):
+ return self._granularity
+
+ def compression(self):
+ return self._compression
+
+ def add_description(self, xml_string):
+ self._descriptions.append(xml_string)
+
+ def descriptions(self):
+ return self._descriptions
+
+def ResumptionTokenSpec(dict):
+ dict = dict.copy()
+ dict['resumptionToken'] = 'exclusive'
+ return dict
+
+class OAIMethodImpl(object):
+ def __init__(self, verb):
+ self._verb = verb
+
+ def __call__(self, bound_self, **kw):
+ return bound_self.handleVerb(self._verb, kw)
+
+def OAIMethod(verb):
+ obj = OAIMethodImpl(verb)
+ def method(self, **kw):
+ return obj(self, **kw)
+ return method
+
+class OAIPMH(object):
+ """Mixin that implements the Python-level OAI-PMH interface.
+
+ It does not include resumptionToken handling.
+
+ It passes the calls on to the 'handleVerb' method, which should be
+ overridden in a subclass.
+ """
+ def handleVerb(self, verb, kw):
+ raise NotImplementedError
+
+ getRecord = OAIMethod(
+ 'GetRecord',
+ )
+
+ identify = OAIMethod(
+ 'Identify',
+ )
+
+ listIdentifiers = OAIMethod(
+ 'ListIdentifiers',
+ )
+
+ listMetadataFormats = OAIMethod(
+ 'ListMetadataFormats',
+ )
+
+ listRecords = OAIMethod(
+ 'ListRecords',
+ )
+
+ listSets = OAIMethod(
+ 'ListSets',
+ )
+
+class ResumptionOAIPMH(object):
+ """Mixin that implements the Resumption-capable OAI-PMH interface.
+
+ It passes the arguments on to the 'handleVerb' method, which
+ should be overridden in a subclass.
+
+ The listIdentifiers, listSets and listRecords methods return
+ tuples of a list and resumptionToken. If the resumptionToken
+ returned is None, this indicates the end of the list is reached.
+ """
+
+ def handleVerb(self, verb, kw):
+ raise NotImplementedError
+
+ getRecord = OAIMethod(
+ 'GetRecord',
+ )
+
+ identify = OAIMethod(
+ 'Identify',
+ )
+
+ listIdentifiers = OAIMethod(
+ 'ListIdentifiers',
+ )
+
+ listMetadataFormats = OAIMethod(
+ 'ListMetadataFormats',
+ )
+
+ listRecords = OAIMethod(
+ 'ListRecords',
+ )
+
+ listSets = OAIMethod(
+ 'ListSets',
+ )
+
+def getMethodForVerb(server, verb):
+ return getattr(server, verb[0].lower() + verb[1:])
+
diff --git a/metashare/oai_pmh/oaipmh/datestamp.py b/metashare/oai_pmh/oaipmh/datestamp.py
new file mode 100644
index 000000000..f109cccfd
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/datestamp.py
@@ -0,0 +1,81 @@
+import datetime
+from oaipmh.error import DatestampError
+
+def datetime_to_datestamp(dt, day_granularity=False):
+ assert dt.tzinfo is None # only accept timezone naive datetimes
+ # ignore microseconds
+ dt = dt.replace(microsecond=0)
+ result = dt.isoformat() + 'Z'
+ if day_granularity:
+ result = result[:-10]
+ return result
+
+# handy utility function not used by pyoai itself yet
+def date_to_datestamp(d, day_granularity=False):
+ return datetime_to_datestamp(
+ datetime.datetime.combine(d, datetime.time(0)), day_granularity)
+
+def datestamp_to_datetime(datestamp, inclusive=False):
+ try:
+ return _datestamp_to_datetime(datestamp, inclusive)
+ except ValueError:
+ raise DatestampError(datestamp)
+
+def _datestamp_to_datetime(datestamp, inclusive=False):
+ splitted = datestamp.split('T')
+ if len(splitted) == 2:
+ d, t = splitted
+ if not t or t[-1] != 'Z':
+ raise DatestampError(datestamp)
+ # strip off 'Z'
+ t = t[:-1]
+ else:
+ d = splitted[0]
+ if inclusive:
+ # used when a date was specified as ?until parameter
+ t = '23:59:59'
+ else:
+ t = '00:00:00'
+ YYYY, MM, DD = d.split('-')
+ hh, mm, ss = t.split(':') # this assumes there's no timezone info
+ return datetime.datetime(
+ int(YYYY), int(MM), int(DD), int(hh), int(mm), int(ss))
+
+def tolerant_datestamp_to_datetime(datestamp):
+ """A datestamp to datetime that's more tolerant of diverse inputs.
+
+ Not used inside pyoai itself right now, but can be used when defining
+ your own metadata schema if that has a broader variety of datetimes
+ in there.
+ """
+ splitted = datestamp.split('T')
+ if len(splitted) == 2:
+ d, t = splitted
+ # if no Z is present, raise error
+ if t[-1] != 'Z':
+ raise DatestampError(datestamp)
+ # split off Z at the end
+ t = t[:-1]
+ else:
+ d = splitted[0]
+ t = '00:00:00'
+ d_splitted = d.split('-')
+ if len(d_splitted) == 3:
+ YYYY, MM, DD = d_splitted
+ elif len(d_splitted) == 2:
+ YYYY, MM = d_splitted
+ DD = '01'
+ elif len(d_splitted) == 1:
+ YYYY = d_splitted[0]
+ MM = '01'
+ DD = '01'
+ else:
+ raise DatestampError(datestamp)
+
+ t_splitted = t.split(':')
+ if len(t_splitted) == 3:
+ hh, mm, ss = t_splitted
+ else:
+ raise DatestampError(datestamp)
+ return datetime.datetime(
+ int(YYYY), int(MM), int(DD), int(hh), int(mm), int(ss))
diff --git a/metashare/oai_pmh/oaipmh/error.py b/metashare/oai_pmh/oaipmh/error.py
new file mode 100644
index 000000000..ddf1a0cff
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/error.py
@@ -0,0 +1,61 @@
+
+class ErrorBase(Exception):
+ def oainame(self):
+ name = self.__class__.__name__
+ # strip off 'Error' part
+ name = name[:-5]
+ # lowercase error name
+ name = name[0].lower() + name[1:]
+ return name
+
+class BadArgumentError(ErrorBase):
+ pass
+
+class BadVerbError(ErrorBase):
+ pass
+
+class BadResumptionTokenError(ErrorBase):
+ pass
+
+class CannotDisseminateFormatError(ErrorBase):
+ pass
+
+class IdDoesNotExistError(ErrorBase):
+ pass
+
+class NoRecordsMatchError(ErrorBase):
+ pass
+
+class NoMetadataFormatsError(ErrorBase):
+ pass
+
+class NoSetHierarchyError(ErrorBase):
+ pass
+
+class UnknownError(ErrorBase):
+ pass
+
+# errors not defined by OAI-PMH but which can occur in a client when
+# the server is somehow misbehaving
+class ClientError(Exception):
+ def details(self):
+ """Error details in human readable text.
+ """
+ raise NotImplementedError
+
+class XMLSyntaxError(ClientError):
+ """The OAI-PMH XML can not be parsed as it is not well-formed.
+ """
+ def details(self):
+ return ("The data delivered by the server could not be parsed, as it "
+ "is not well-formed XML.")
+
+class DatestampError(ClientError):
+ """The OAI-PMH datestamps were not proper UTC datestamps as by spec.
+ """
+ def __init__(self, datestamp):
+ self.datestamp = datestamp
+
+ def details(self):
+ return ("An illegal datestamp was encountered: %s" % self.datestamp)
+
diff --git a/metashare/oai_pmh/oaipmh/interfaces.py b/metashare/oai_pmh/oaipmh/interfaces.py
new file mode 100644
index 000000000..aec38bf48
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/interfaces.py
@@ -0,0 +1,170 @@
+class IOAI:
+ def getRecord(metadataPrefix, identifier):
+ """Get a record for a metadataPrefix and identifier.
+
+ metadataPrefix - identifies metadata set to retrieve
+ identifier - repository-unique identifier of record
+
+ Should raise error.CannotDisseminateFormatError if
+ metadataPrefix is unknown or not supported by identifier.
+
+ Should raise error.IdDoesNotExistError if identifier is
+ unknown or illegal.
+
+ Returns a header, metadata, about tuple describing the record.
+ """
+
+ def identify():
+ """Retrieve information about the repository.
+
+ Returns an Identify object describing the repository.
+ """
+
+ def listIdentifiers(metadataPrefix, set=None, from_=None, until=None):
+ """Get a list of header information on records.
+
+ metadataPrefix - identifies metadata set to retrieve
+ set - set identifier; only return headers in set (optional)
+ from_ - only retrieve headers from from_ date forward (optional)
+ until - only retrieve headers with dates up to and including
+ until date (optional)
+
+ Should raise error.CannotDisseminateFormatError if metadataPrefix
+ is not supported by the repository.
+
+ Should raise error.NoSetHierarchyError if the repository does not
+ support sets.
+
+ Returns an iterable of headers.
+ """
+
+ def listMetadataFormats(identifier=None):
+ """List metadata formats supported by repository or record.
+
+ identifier - identify record for which we want to know all
+ supported metadata formats. if absent, list all metadata
+ formats supported by repository. (optional)
+
+
+ Should raise error.IdDoesNotExistError if record with
+ identifier does not exist.
+
+ Should raise error.NoMetadataFormatsError if no formats are
+ available for the indicated record.
+
+ Returns an iterable of metadataPrefix, schema, metadataNamespace
+ tuples (each entry in the tuple is a string).
+ """
+
+ def listRecords(metadataPrefix, set=None, from_=None, until=None):
+ """Get a list of header, metadata and about information on records.
+
+ metadataPrefix - identifies metadata set to retrieve
+ set - set identifier; only return records in set (optional)
+ from_ - only retrieve records from from_ date forward (optional)
+ until - only retrieve records with dates up to and including
+ until date (optional)
+
+ Should raise error.CannotDisseminateFormatError if metadataPrefix
+ is not supported by the repository.
+
+ Should raise error.NoSetHierarchyError if the repository does not
+ support sets.
+
+ Returns an iterable of header, metadata, about tuples.
+ """
+
+ def listSets():
+ """Get a list of sets in the repository.
+
+ Should raise error.NoSetHierarchyError if the repository does not
+ support sets.
+
+ Returns an iterable of setSpec, setName tuples (strings).
+ """
+
+class IBatchingOAI:
+ """Very similar to IOAI, but the implementation can be batch-aware.
+
+ Methods that support resumption will get two extra arguments,
+ cursor and batch_size, which indicate the batch currently being
+ requested.
+ """
+
+ def getRecord(metadataPrefix, identifier):
+ pass
+
+ def identify():
+ pass
+
+ def listIdentifiers(metadataPrefix, set=None, from_=None, until=None,
+ cursor=0, batch_size=10):
+ pass
+
+ def listMetadataFormats(identifier=None):
+ pass
+
+ def listRecords(metadataPrefix, set=None, from_=None, until=None,
+ cursor=0, batch_size=10):
+ pass
+
+ def listSets():
+ pass
+
+class IIdentify:
+ def repositoryName():
+ """Name of repository.
+ """
+
+ def baseURL():
+ """Base URL for OAI-PMH requests.
+ """
+
+ def protocolVersion():
+ """OAI-PMH protocol version (should always be '2.0')
+ """
+
+ def adminEmails():
+ """List of email addresses of repository administrators.
+ """
+
+ def earliestDateStamp():
+ """The datetime (datestamp) of the earliest record in repository.
+ """
+
+ def deletedRecord():
+ """Way the repository handles deleted records.
+
+ Either 'no', 'transient' or 'persistent'.
+ """
+
+ def granularity():
+ """Datetime granularity of datestamps in repository.
+
+ Either YYYY-MM-DD or YYYY-MM-DDThh:mm:ssZ
+ """
+
+ def compression():
+ """List of types of compression schemes supported by repository.
+
+ 'identity' is the 'do-nothing' scheme.
+ """
+
+class IHeader:
+ def identifier():
+ """Repository-unique identifier of this record.
+ """
+
+ def datestamp():
+ """Datetime of creation, last modification or deletion of the record.
+
+ This can be used for selective harvesting.
+ """
+
+ def setSpec():
+ """A list of sets this record is a member of.
+ """
+
+ def isDeleted():
+ """If true, record has been deleted.
+ """
diff --git a/metashare/oai_pmh/oaipmh/metadata.py b/metashare/oai_pmh/oaipmh/metadata.py
new file mode 100644
index 000000000..3dd2347a7
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/metadata.py
@@ -0,0 +1,109 @@
+from lxml import etree
+from lxml.etree import SubElement
+from oaipmh import common
+
+class MetadataRegistry(object):
+ """A registry that contains readers and writers of metadata.
+
+ a reader is a function that takes a chunk of (parsed) XML and
+ returns a metadata object.
+
+ a writer is a function that takes a takes a metadata object and
+ produces a chunk of XML in the right format for this metadata.
+ """
+ def __init__(self):
+ self._readers = {}
+ self._writers = {}
+
+ def registerReader(self, metadata_prefix, reader):
+ self._readers[metadata_prefix] = reader
+
+ def registerWriter(self, metadata_prefix, writer):
+ self._writers[metadata_prefix] = writer
+
+ def hasReader(self, metadata_prefix):
+ return metadata_prefix in self._readers
+
+ def hasWriter(self, metadata_prefix):
+ return metadata_prefix in self._writers
+
+ def readMetadata(self, metadata_prefix, element):
+ """Turn XML into metadata object.
+
+ element - element to read in
+
+ returns - metadata object
+ """
+ return self._readers[metadata_prefix](element)
+
+ def writeMetadata(self, metadata_prefix, element, metadata):
+ """Write metadata as XML.
+
+ element - ElementTree element to write under
+ metadata - metadata object to write
+ """
+ self._writers[metadata_prefix](element, metadata)
+
+global_metadata_registry = MetadataRegistry()
+
+class Error(Exception):
+ pass
+
+class MetadataReader(object):
+ """A default implementation of a reader based on fields.
+ """
+ def __init__(self, fields, namespaces=None):
+ self._fields = fields
+ self._namespaces = namespaces or {}
+
+ def __call__(self, element):
+ map = {}
+ # create XPathEvaluator for this element
+ xpath_evaluator = etree.XPathEvaluator(element,
+ namespaces=self._namespaces)
+
+ e = xpath_evaluator.evaluate
+ # now extra field info according to xpath expr
+ for field_name, (field_type, expr) in self._fields.items():
+ if field_type == 'bytes':
+ value = str(e(expr))
+ elif field_type == 'bytesList':
+ value = [str(item) for item in e(expr)]
+ elif field_type == 'text':
+ # make sure we get back unicode strings instead
+ # of lxml.etree._ElementUnicodeResult objects.
+ value = unicode(e(expr))
+ elif field_type == 'textList':
+ # make sure we get back unicode strings instead
+ # of lxml.etree._ElementUnicodeResult objects.
+ value = [unicode(v) for v in e(expr)]
+ else:
+ raise Error, "Unknown field type: %s" % field_type
+ map[field_name] = value
+ return common.Metadata(map)
+
+oai_dc_reader = MetadataReader(
+ fields={
+ 'title': ('textList', 'oai_dc:dc/dc:title/text()'),
+ 'creator': ('textList', 'oai_dc:dc/dc:creator/text()'),
+ 'subject': ('textList', 'oai_dc:dc/dc:subject/text()'),
+ 'description': ('textList', 'oai_dc:dc/dc:description/text()'),
+ 'publisher': ('textList', 'oai_dc:dc/dc:publisher/text()'),
+ 'contributor': ('textList', 'oai_dc:dc/dc:contributor/text()'),
+ 'date': ('textList', 'oai_dc:dc/dc:date/text()'),
+ 'type': ('textList', 'oai_dc:dc/dc:type/text()'),
+ 'format': ('textList', 'oai_dc:dc/dc:format/text()'),
+ 'identifier': ('textList', 'oai_dc:dc/dc:identifier/text()'),
+ 'source': ('textList', 'oai_dc:dc/dc:source/text()'),
+ 'language': ('textList', 'oai_dc:dc/dc:language/text()'),
+ 'relation': ('textList', 'oai_dc:dc/dc:relation/text()'),
+ 'coverage': ('textList', 'oai_dc:dc/dc:coverage/text()'),
+ 'rights': ('textList', 'oai_dc:dc/dc:rights/text()')
+ },
+ namespaces={
+ 'oai_dc': 'http://www.openarchives.org/OAI/2.0/oai_dc/',
+ 'dc' : 'http://purl.org/dc/elements/1.1/'}
+ )
+
+
+
diff --git a/metashare/oai_pmh/oaipmh/server.py b/metashare/oai_pmh/oaipmh/server.py
new file mode 100644
index 000000000..8ffe19e2b
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/server.py
@@ -0,0 +1,485 @@
+from lxml.etree import ElementTree, Element, SubElement
+from lxml import etree
+from datetime import datetime
+from urllib import quote, unquote, urlencode
+import sys, cgi
+
+from oaipmh import common, metadata, validation, error
+from oaipmh.datestamp import datestamp_to_datetime, datetime_to_datestamp, DatestampError
+
+NS_OAIPMH = 'http://www.openarchives.org/OAI/2.0/'
+NS_XSI = 'http://www.w3.org/2001/XMLSchema-instance'
+NS_OAIDC = 'http://www.openarchives.org/OAI/2.0/oai_dc/'
+NS_DC = "http://purl.org/dc/elements/1.1/"
+
+NSMAP = {
+ None: NS_OAIPMH,
+ }
+
+class XMLTreeServer(object):
+ """A server that responds to messages by returning XML trees.
+
+ This is an implementation class that normally would not be exposed
+ to the outside world.
+
+ Takes a server object conforming to the ResumptionOAIPMH interface.
+ """
+ def __init__(self, server, metadata_registry, nsmap=None):
+ if nsmap is None:
+ nsmap = {}
+ self._nsmap = NSMAP.copy()
+ self._nsmap.update(nsmap)
+ self._server = server
+ self._metadata_registry = (
+ metadata_registry or metadata.global_metadata_registry)
+
+ def getRecord(self, **kw):
+ envelope, e_getRecord = self._outputEnvelope(
+ verb='GetRecord', **kw)
+ header, metadata, about = self._server.getRecord(**kw)
+ e_record = SubElement(e_getRecord, nsoai('record'))
+ self._outputHeader(e_record, header)
+ if not header.isDeleted():
+ self._outputMetadata(e_record, kw['metadataPrefix'], metadata)
+ return envelope
+
+ def identify(self):
+ envelope, e_identify = self._outputEnvelope(verb='Identify')
+ identify = self._server.identify()
+ e_repositoryName = SubElement(e_identify, nsoai('repositoryName'))
+ e_repositoryName.text = identify.repositoryName()
+ e_baseURL = SubElement(e_identify, nsoai('baseURL'))
+ e_baseURL.text = identify.baseURL()
+ e_protocolVersion = SubElement(e_identify, nsoai('protocolVersion'))
+ e_protocolVersion.text = identify.protocolVersion()
+ for adminEmail in identify.adminEmails():
+ e = SubElement(e_identify, nsoai('adminEmail'))
+ e.text = adminEmail
+ e_earliestDatestamp = SubElement(e_identify,
+ nsoai('earliestDatestamp'))
+ e_earliestDatestamp.text = datetime_to_datestamp(
+ identify.earliestDatestamp())
+ e_deletedRecord = SubElement(e_identify,
+ nsoai('deletedRecord'))
+ e_deletedRecord.text = identify.deletedRecord()
+ e_granularity = SubElement(e_identify, nsoai('granularity'))
+ e_granularity.text = identify.granularity()
+ compressions = identify.compression()
+ if compressions != ['identity']:
+ for compression in compressions:
+ e_compression = SubElement(e_identify, nsoai('compression'))
+ e_compression.text = compression
+
+ for description in identify.descriptions():
+ e_description = SubElement(e_identify, nsoai('description'))
+ e_description.append(etree.fromstring(description))
+ return envelope
+
+ def listMetadataFormats(self, **kw):
+ envelope, e_listMetadataFormats = self._outputEnvelope(
+ verb="ListMetadataFormats", **kw)
+ for (metadataPrefix, schema,
+ metadataNamespace) in self._server.listMetadataFormats(**kw):
+ e_metadataFormat = SubElement(e_listMetadataFormats,
+ nsoai('metadataFormat'))
+ e_metadataPrefix = SubElement(e_metadataFormat,
+ nsoai('metadataPrefix'))
+ e_metadataPrefix.text = metadataPrefix
+ e_schema = SubElement(e_metadataFormat,
+ nsoai('schema'))
+ e_schema.text = schema
+ e_metadataNamespace = SubElement(e_metadataFormat,
+ nsoai('metadataNamespace'))
+ e_metadataNamespace.text = metadataNamespace
+ return envelope
+
+ def listIdentifiers(self, **kw):
+ envelope, e_listIdentifiers = self._outputEnvelope(
+ verb='ListIdentifiers', **kw)
+ def outputFunc(element, headers, token_kw):
+ for header in headers:
+ self._outputHeader(element, header)
+ self._outputResuming(
+ e_listIdentifiers,
+ self._server.listIdentifiers,
+ outputFunc,
+ kw)
+ return envelope
+
+ def listRecords(self, **kw):
+ envelope, e_listRecords = self._outputEnvelope(
+ verb="ListRecords", **kw)
+ def outputFunc(element, records, token_kw):
+ metadataPrefix = token_kw['metadataPrefix']
+ for header, metadata, about in records:
+ e_record = SubElement(e_listRecords, nsoai('record'))
+ self._outputHeader(e_record, header)
+ if not header.isDeleted():
+ self._outputMetadata(e_record, metadataPrefix, metadata)
+ # XXX about
+ self._outputResuming(
+ e_listRecords,
+ self._server.listRecords,
+ outputFunc,
+ kw)
+ return envelope
+
+ def listSets(self, **kw):
+ envelope, e_listSets = self._outputEnvelope(
+ verb='ListSets', **kw)
+ def outputFunc(element, sets, token_kw):
+ for setSpec, setName, setDescription in sets:
+ e_set = SubElement(e_listSets, nsoai('set'))
+ e_setSpec = SubElement(e_set, nsoai('setSpec'))
+ e_setSpec.text = setSpec
+ e_setName = SubElement(e_set, nsoai('setName'))
+ e_setName.text = setName
+ # XXX ignore setDescription
+ self._outputResuming(
+ e_listSets,
+ self._server.listSets,
+ outputFunc,
+ kw)
+ return envelope
+
+ def handleException(self, exception):
+ if isinstance(exception, error.ErrorBase):
+ envelope = self._outputErrors(
+ [(exception.oainame(), str(exception))])
+ return envelope
+ # unhandled exception, so raise again
+ raise
+
+ def _outputBasicEnvelope(self, **kw):
+ e_oaipmh = Element(nsoai('OAI-PMH'), nsmap=self._nsmap)
+ e_oaipmh.set('{%s}schemaLocation' % NS_XSI,
+ ('http://www.openarchives.org/OAI/2.0/ '
+ 'http://www.openarchives.org/OAI/2.0/OAI-PMH.xsd'))
+ e_tree = ElementTree(element=e_oaipmh)
+ e_responseDate = SubElement(e_oaipmh, nsoai('responseDate'))
+ # date should be first possible moment
+ e_responseDate.text = datetime_to_datestamp(
+ datetime.utcnow().replace(microsecond=0))
+ e_request = SubElement(e_oaipmh, nsoai('request'))
+ for key, value in kw.items():
+ if key == 'from_':
+ key = 'from'
+ if key == 'from' or key == 'until':
+ value = datetime_to_datestamp(value)
+ e_request.set(key, value)
+ # XXX this is potentially slow..
+ e_request.text = self._server.identify().baseURL()
+ return e_tree, e_oaipmh
+
+ def _outputEnvelope(self, **kw):
+ e_tree, e_oaipmh = self._outputBasicEnvelope(**kw)
+ e_element = SubElement(e_oaipmh, nsoai(kw['verb']))
+ return e_tree, e_element
+
+ def _outputErrors(self, errors, **kw):
+ # only pass functional arguments
+ e_tree, e_oaipmh = self._outputBasicEnvelope(**kw)
+ for error_code, error_msg in errors:
+ e_error = SubElement(e_oaipmh, nsoai('error'))
+ e_error.set('code', error_code)
+ e_error.text = error_msg
+ return e_tree
+
+ def _outputResuming(self, element, input_func, output_func, kw):
+ if 'resumptionToken' in kw:
+ resumptionToken = kw['resumptionToken']
+ result, token = input_func(resumptionToken=resumptionToken)
+ # unpack keywords from resumption token
+ token_kw, dummy = decodeResumptionToken(resumptionToken)
+ else:
+ result, token = input_func(**kw)
+ # if we don't get results for the first request,
+ # then no records match
+ # XXX this will also be triggered if there are no sets,
+ # but input_func (listSets) should have already raised
+ # NoSetHierarchyError in that case
+ if not result:
+ raise error.NoRecordsMatchError,\
+ "No records match for request."
+ # without resumption token keys are fine
+ token_kw = kw
+ output_func(element, result, token_kw)
+ if token is not None:
+ e_resumptionToken = SubElement(element, nsoai('resumptionToken'))
+ e_resumptionToken.text = token
+
+ def _outputHeader(self, element, header):
+ e_header = SubElement(element, nsoai('header'))
+ if header.isDeleted():
+ e_header.set('status', 'deleted')
+ e_identifier = SubElement(e_header, nsoai('identifier'))
+ e_identifier.text = header.identifier()
+ e_datestamp = SubElement(e_header, nsoai('datestamp'))
+ e_datestamp.text = datetime_to_datestamp(header.datestamp())
+ for set in header.setSpec():
+ e = SubElement(e_header, nsoai('setSpec'))
+ e.text = set
+
+ def _outputMetadata(self, element, metadata_prefix, metadata):
+ e_metadata = SubElement(element, nsoai('metadata'))
+ if not self._metadata_registry.hasWriter(metadata_prefix):
+ raise error.CannotDisseminateFormatError,\
+ "Unknown metadata format: %s" % metadata_prefix
+ self._metadata_registry.writeMetadata(
+ metadata_prefix, e_metadata, metadata)
+
+class ServerBase(common.ResumptionOAIPMH):
+ """A server that responds to messages by returning OAI-PMH compliant XML.
+
+ Takes a server object complying with the ResumptionOAIPMH interface.
+ """
+ def __init__(self, server, metadata_registry=None, nsmap=None):
+ self._tree_server = XMLTreeServer(server, metadata_registry, nsmap)
+
+ def handleRequest(self, request_kw):
+ """Handles incoming OAI-PMH request.
+
+ request_kw is a dictionary containing request parameters, including
+ verb.
+ """
+ # try to get verb, if not, we have an argument handling error
+ try:
+ new_kw = {}
+ try:
+ for key, value in request_kw.items():
+ new_kw[str(key)] = value
+ except UnicodeError:
+ raise error.BadVerbError,\
+ "Non-ascii keys in request."
+ request_kw = new_kw
+ try:
+ verb = request_kw.pop('verb')
+ except KeyError:
+ verb = 'unknown'
+ raise error.BadVerbError,\
+ "Required verb argument not found."
+ if verb not in ['GetRecord', 'Identify', 'ListIdentifiers',
+ 'ListMetadataFormats', 'ListRecords', 'ListSets']:
+ raise error.BadVerbError, "Illegal verb: %s" % verb
+ # replace from and until arguments if necessary
+ from_ = request_kw.get('from')
+ if from_ is not None:
+ # rename to from_ for internal use
+ try:
+ request_kw['from_'] = datestamp_to_datetime(from_)
+ except DatestampError, err:
+ raise error.BadArgumentError(
+ "The value '%s' of the argument "
+ "'%s' is not valid." %(from_, 'from'))
+ del request_kw['from']
+ until = request_kw.get('until')
+ if until is not None:
+ try:
+ request_kw['until'] = datestamp_to_datetime(until,
+ inclusive=True)
+ except DatestampError, err:
+ raise error.BadArgumentError(
+ "The value '%s' of the argument "
+ "'%s' is not valid." %(until, 'until'))
+
+ if from_ is not None and until is not None:
+ if (('T' in from_ and not 'T' in until) or
+ ('T' in until and not 'T' in from_)):
+ raise error.BadArgumentError(
+ "The request has different granularities for"
+ " the from and until parameters")
+
+ # now validate parameters
+ try:
+ validation.validateResumptionArguments(verb, request_kw)
+ except validation.BadArgumentError, e:
+ # have to raise this as a error.BadArgumentError
+ raise error.BadArgumentError, str(e)
+ # now handle verb
+ return self.handleVerb(verb, request_kw)
+ except:
+ # in case of exception, call exception handler
+ return self.handleException(request_kw, sys.exc_info())
+
+ def handleVerb(self, verb, kw):
+ method = common.getMethodForVerb(self._tree_server, verb)
+ return etree.tostring(method(**kw).getroot(),
+ encoding='UTF-8',
+ xml_declaration=True,
+ pretty_print=True)
+
+ def handleException(self, kw, exc_info):
+ type, value, traceback = exc_info
+ return etree.tostring(
+ self._tree_server.handleException(value).getroot(),
+ encoding='UTF-8',
+ xml_declaration=True,
+ pretty_print=True)
+
+class Server(ServerBase):
+ """Expects to be initialized with a IOAI server implementation.
+ """
+ def __init__(self, server, metadata_registry=None, nsmap=None,
+ resumption_batch_size=10):
+ super(Server, self).__init__(
+ Resumption(server, resumption_batch_size),
+ metadata_registry,
+ nsmap)
+
+class BatchingServer(ServerBase):
+ """Expects to be initialized with a IBatchingOAI server implementation.
+ """
+ def __init__(self, server, metadata_registry=None, nsmap=None,
+ resumption_batch_size=10):
+ super(BatchingServer, self).__init__(
+ BatchingResumption(server, resumption_batch_size),
+ metadata_registry,
+ nsmap)
+
+class Resumption(common.ResumptionOAIPMH):
+ """
+ The Resumption class can turn a plain IOAIPMH interface into
+ a ResumptionOAIPMH interface
+
+ This implementation is not particularly efficient for large
+ result sets, as the complete result set needs to be reconstructed each
+ time.
+ """
+ def __init__(self, server, batch_size=10):
+ self._server = server
+ self._batch_size = batch_size
+
+ def handleVerb(self, verb, kw):
+ # do original query
+ method = common.getMethodForVerb(self._server, verb)
+ # if we're handling a resumption token
+ if 'resumptionToken' in kw:
+ kw, cursor = decodeResumptionToken(
+ kw['resumptionToken'])
+ end_batch = cursor + self._batch_size
+ # do query again with original parameters
+ result = method(**kw)
+ # XXX defeat laziness of any generators..
+ result = list(result)
+ if end_batch < len(result):
+ resumptionToken = encodeResumptionToken(
+ kw, end_batch)
+ else:
+ resumptionToken = None
+ return result[cursor:end_batch], resumptionToken
+
+ # we're not handling resumption token, so do request
+ result = method(**kw)
+
+ # now handle resumption system
+ if verb in ['ListSets', 'ListIdentifiers', 'ListRecords']:
+ # XXX defeat the laziness effect of any generators..
+ result = list(result)
+ end_batch = self._batch_size
+ if end_batch < len(result):
+ resumptionToken = encodeResumptionToken(
+ kw, end_batch)
+ else:
+ resumptionToken = None
+ return result[0:end_batch], resumptionToken
+ return result
+
+class BatchingResumption(common.ResumptionOAIPMH):
+ """
+ The BatchingResumption class can turn a IBatchingOAIPMH interface into
+ a ResumptionOAIPMH interface.
+ """
+
+ def __init__(self, server, batch_size=10):
+ self._server = server
+ self._batch_size = batch_size
+
+ def handleVerb(self, verb, kw):
+ if 'resumptionToken' in kw:
+ kw, cursor = decodeResumptionToken(
+ kw['resumptionToken'])
+ kw['cursor'] = cursor
+
+ method = common.getMethodForVerb(self._server, verb)
+
+ # now handle resumption system
+ if verb in ['ListSets', 'ListIdentifiers', 'ListRecords']:
+ kw = kw.copy()
+ cursor = kw.get('cursor', None)
+ if cursor is None:
+ kw['cursor'] = cursor = 0
+ # we request 1 beyond the batch size, so that
+ # if we retrieve <= batch_size items, we know we
+ # don't need to output another resumption token
+ kw['batch_size'] = self._batch_size + 1
+ result = method(**kw)
+ result = list(result)
+ if len(result) > self._batch_size:
+ # more results are expected, so encode resumption token
+ resumptionToken = encodeResumptionToken(
+ kw, cursor + self._batch_size)
+ # we also want to result only the batch_size, so pop the
+ # last one
+ result.pop()
+ else:
+ # no more results are expected
+ resumptionToken = None
+ return result, resumptionToken
+ return method(**kw)
+
+def encodeResumptionToken(kw, cursor):
+ kw = kw.copy()
+ kw['cursor'] = str(cursor)
+ from_ = kw.get('from_')
+ if from_ is not None:
+ kw['from_'] = datetime_to_datestamp(from_)
+ until = kw.get('until')
+ if until is not None:
+ kw['until'] = datetime_to_datestamp(until)
+ return quote(urlencode(kw))
+
+def decodeResumptionToken(token):
+ token = str(unquote(token))
+
+ try:
+ kw = cgi.parse_qs(token, True, True)
+ except ValueError:
+ raise error.BadResumptionTokenError,\
+ "Unable to decode resumption token: %s" % token
+ result = {}
+ for key, value in kw.items():
+ value = value[0]
+ if key == 'from_' or key == 'until':
+ value = datestamp_to_datetime(value)
+ result[key] = value
+ try:
+ cursor = int(result.pop('cursor'))
+ except (KeyError, ValueError):
+ raise error.BadResumptionTokenError,\
+ "Unable to decode resumption token (bad cursor): %s" % token
+ # XXX should also validate result contents. Need verb information
+ # for this, and somewhat more flexible verb validation support
+ return result, cursor
+
+def oai_dc_writer(element, metadata):
+ e_dc = SubElement(element, nsoaidc('dc'),
+ nsmap={'oai_dc': NS_OAIDC, 'dc': NS_DC, 'xsi': NS_XSI})
+ e_dc.set('{%s}schemaLocation' % NS_XSI,
+ '%s http://www.openarchives.org/OAI/2.0/oai_dc.xsd' % NS_DC)
+ map = metadata.getMap()
+ for name in [
+ 'title', 'creator', 'subject', 'description', 'publisher',
+ 'contributor', 'date', 'type', 'format', 'identifier',
+ 'source', 'language', 'relation', 'coverage', 'rights']:
+ for value in map.get(name, []):
+ e = SubElement(e_dc, nsdc(name))
+ e.text = value
+
+def nsoai(name):
+ return '{%s}%s' % (NS_OAIPMH, name)
+
+def nsoaidc(name):
+ return '{%s}%s' % (NS_OAIDC, name)
+
+def nsdc(name):
+ return '{%s}%s' % (NS_DC, name)
diff --git a/metashare/oai_pmh/oaipmh/validation.py b/metashare/oai_pmh/oaipmh/validation.py
new file mode 100644
index 000000000..5b395d1b0
--- /dev/null
+++ b/metashare/oai_pmh/oaipmh/validation.py
@@ -0,0 +1,88 @@
+
+#
+class BadArgumentError(Exception):
+ pass
+
+def validate(argspec, dict):
+ exclusive = None
+ for arg_name, arg_type in argspec.items():
+ if arg_type == 'exclusive':
+ exclusive = arg_name
+ # check if we have unknown arguments
+ for key, value in dict.items():
+ if not argspec.has_key(key):
+ msg = "Unknown argument: %s" % key
+ raise BadArgumentError, msg
+ # first investigate if we have exclusive argument
+ if dict.has_key(exclusive):
+ if len(dict) > 1:
+ msg = ("Exclusive argument %s is used but other "
+ "arguments found." % exclusive)
+ raise BadArgumentError, msg
+ return
+ # if not exclusive, check for required
+ for arg_name, arg_type in argspec.items():
+ if arg_type == 'required':
+ msg = "Argument required but not found: %s" % arg_name
+ if not dict.has_key(arg_name):
+ raise BadArgumentError, msg
+ return
+
+class ValidationSpec(object):
+ GetRecord = {
+ 'identifier':'required',
+ 'metadataPrefix':'required'
+ }
+
+ Identify = {
+ }
+
+ ListIdentifiers = {
+ 'from_':'optional',
+ 'until':'optional',
+ 'metadataPrefix':'required',
+ 'set':'optional',
+ }
+
+ ListMetadataFormats = {
+ 'identifier':'optional'
+ }
+
+ ListRecords = {
+ 'from_':'optional',
+ 'until':'optional',
+ 'set':'optional',
+ 'metadataPrefix':'required',
+ }
+
+ ListSets = {
+ }
+
+class ResumptionValidationSpec(ValidationSpec):
+
+ ListIdentifiers = {
+ 'from_':'optional',
+ 'until':'optional',
+ 'metadataPrefix':'required',
+ 'set':'optional',
+ 'resumptionToken':'exclusive',
+ }
+
+ ListRecords = {
+ 'from_':'optional',
+ 'until':'optional',
+ 'set':'optional',
+ 'metadataPrefix':'required',
+ 'resumptionToken':'exclusive',
+ }
+
+ ListSets = {
+ 'resumptionToken':'exclusive',
+ }
+
+def validateArguments(verb, kw):
+ validate(getattr(ValidationSpec, verb), kw)
+
+def validateResumptionArguments(verb, kw):
+ validate(getattr(ResumptionValidationSpec, verb), kw)
+
diff --git a/metashare/repository/forms.py b/metashare/repository/forms.py
index 4452f46a3..2d145f039 100644
--- a/metashare/repository/forms.py
+++ b/metashare/repository/forms.py
@@ -16,6 +16,14 @@
from haystack.forms import FacetedSearchForm
from haystack.query import SQ
+#noinspection PyBroadException
+try:
+ OAI_PMH_IMPORT_FROM_DIR = None
+ from metashare.settings import OAI_PMH_IMPORT_FROM_DIR
+except:
+ pass
+
+
# Setup logging support.
LOGGER = logging.getLogger(__name__)
@@ -221,3 +229,43 @@ class DownloadContactForm(forms.Form):
"""
userEmail = forms.EmailField(label=_("Your e-mail"))
message = forms.CharField(label=_("Your message"), widget=forms.Textarea())
+
+
+from oai_pmh import supported_commands
+
+class OaiPmhForm(forms.Form):
+ """
+ Settings form for OAI-PMH import.
+ """
+ url = forms.URLField(
+ label=u"Harvested URL supporting OAI-PMH",
+ initial=u"http://ufal-point.mff.cuni.cz/oai/request",
+ help_text=u"URL with OAI-PMH support (for importing file contents you need a little updated ORE support on the remote server)",
+ )
+
+ verb = forms.ChoiceField(
+ label=u"What to do",
+ choices=[ (name, name) for name in supported_commands.keys() ],
+ help_text=u"Select OAI-PMH action (fill out item id if applicable)",
+ )
+
+ metadata_str = forms.CharField(
+ label=u"OAI metadata format",
+ help_text=u"Use list metadata for choices",
+ initial=u"oai_metasharev3",
+ required=False,
+ )
+
+ itemid = forms.CharField(
+ label=u"OAI item ID",
+ help_text=u"Leave empty for the whole collection or if not applicable",
+ required=False,
+ )
+
+ import_from_dir = forms.CharField(
+ label=u"Imported directory",
+ help_text=u"Because of licensing, we use ORE only for information about directories and use local " + \
+ u"(or remote) directories for importing. Should be provided in local_settings.py",
+ initial=OAI_PMH_IMPORT_FROM_DIR,
+ required=False,
+ )
diff --git a/metashare/repository/urls.py b/metashare/repository/urls.py
index edaa98dd5..6af22bcfc 100644
--- a/metashare/repository/urls.py
+++ b/metashare/repository/urls.py
@@ -71,6 +71,12 @@
'download'),
(r'^download_contact/(?P\w+)/$',
'download_contact'),
+ (r'^oai-pmh-import/$',
+ 'oai_pmh_import'),
+ (r'^oai-pmh-export/$',
+ 'oai_pmh_export'),
+ (r'^oai.*$',
+ 'oai_server'),
url(r'^search/$',
search_view_factory(view_class=MetashareFacetedSearchView,
form_class=FacetedBrowseForm,
diff --git a/metashare/repository/views.py b/metashare/repository/views.py
index 198d41273..d268c96ae 100644
--- a/metashare/repository/views.py
+++ b/metashare/repository/views.py
@@ -14,6 +14,7 @@
from django.template.loader import render_to_string
from django.core.mail import send_mail
from django.utils.translation import ugettext as _
+from django.utils.safestring import mark_safe
from haystack.views import FacetedSearchView
@@ -34,6 +35,10 @@
get_download_recommendations, get_view_recommendations, \
get_more_from_same_creators_qs, get_more_from_same_projects_qs
+from metashare.repository.forms import OaiPmhForm
+from metashare import oai_pmh
+from oai_pmh import time_it, smart_extend
+
MAXIMUM_READ_BLOCK_SIZE = 4096
@@ -959,3 +964,172 @@ def show_subfilter(self, facet, sel_facets, facet_fields, results):
'addable': addable})
return results
+
+
+
+# PAI-PMH import
+#
+@login_required
+def oai_pmh_import(request):
+ """
+ Handle the OAI-PMH importing.
+
+ There are 2 features *not* out of the box ready when you use an arbitrary
+ digital library supporting OAI-PMH:
+ 1) the MetaShare metadata format (according to the v3.0 schema)
+ 2) ORE must give a hint to local paths (because of our specific and strict
+ licensing on the other point)
+
+ Please, consult UFAL, Charles University for more detail. We use it
+ for DSpace <-> Metashare synchronisation.
+ """
+
+ def _mark_obj_as_html_ok( data_dict ):
+ """ We return html strings, ouch """
+ if isinstance(data_dict, dict):
+ for k, v in data_dict.iteritems():
+ if isinstance(v, basestring):
+ data_dict[k] = mark_safe(v)
+
+ def _get_what_to_do_from_fast_buttons( form ):
+ """
+ Simplification for the user if another button was clicked than
+ the Process button.
+ """
+ verb = form.cleaned_data['verb']
+ for key in form.data.keys():
+ if key.startswith("verb_"):
+ verb = key.replace("verb_", "")
+ break
+ return verb
+
+ def _remove_all_items():
+ from metashare.storage.models import StorageObject
+ from metashare.sync.sync_utils import remove_resource
+
+ d = {}
+ all_sos = StorageObject.objects.all()
+ # somehow it is not always working (could be known windows delete problem)
+ for so in all_sos:
+ d[so.id] = "Removed"
+ try:
+ remove_resource(so)
+ except:
+ pass
+ try:
+ so.delete()
+ except:
+ pass
+ return d
+
+
+ form = OaiPmhForm()
+ # the user posted something so do what he wanted
+ if "POST" == request.method:
+
+ data_info = {}
+ form = OaiPmhForm(request.POST)
+ if form.is_valid():
+ # find out what to do and then do it
+ what_to_do = _get_what_to_do_from_fast_buttons(form)
+
+ try:
+
+ if what_to_do in oai_pmh.supported_commands:
+
+ # call the function associated with the task
+ #
+ ftor = oai_pmh.supported_commands[what_to_do]
+ data_dict_title = what_to_do
+ tm, data_dict = time_it(ftor, smart_extend(locals(), form.cleaned_data))
+ data_info = {
+ "Item size": len(data_dict),
+ "Time taken": tm,
+ }
+ _mark_obj_as_html_ok(data_dict)
+
+ # ok so use javascript if needed
+ #
+ if oai_pmh.key_list_ids_for_import == what_to_do:
+ javascript_for_id = 1
+ import_verb = oai_pmh.key_import
+ elif oai_pmh.key_list_metadata_format == what_to_do:
+ javascript_for_id = 2
+
+ # special case
+ elif "removeall" == what_to_do:
+ tm, data_dict = time_it(_remove_all_items)
+ data_info = {
+ "Item size": len(data_dict),
+ "Time taken": tm,
+ }
+
+ else:
+ error_str = u"Invalid command [%s]" % what_to_do
+
+ except Exception, e:
+ error_str = repr(e)
+
+
+ # give it all params together with form params
+ params = smart_extend(
+ {
+ 'showform': "1",
+ 'form_input': form,
+ },
+ locals(),
+ getattr(form, 'cleaned_data', {}),
+ )
+
+ # and show it
+ return render_to_response(
+ 'repository/oai_pmh_import.html',
+ params,
+ context_instance=RequestContext(request))
+
+
+# PAI-PMH export
+#
+oai_url = DJANGO_URL.rstrip("/") + "/repository/oai"
+
+from oai_pmh import MetashareOaiServer
+from oai_pmh.oaipmh.server import Server
+from metashare.settings import ADMINS, METASHARE_VERSION
+
+__env_dict = {
+ "name": u"Metashare " + METASHARE_VERSION,
+ "url": oai_url,
+ "adminEmails": [x[1] for x in ADMINS],
+}
+oai_server_inst = Server(server=MetashareOaiServer(__env_dict))
+
+
+@login_required
+def oai_pmh_export(request):
+ """
+ Simple OAI-PMH page with testing and settings information.
+ """
+ params = {
+ "oai_url": oai_url,
+ }
+ return render_to_response(
+ 'repository/oai_pmh_export.html',
+ params,
+ context_instance=RequestContext(request))
+
+
+def oai_server(request):
+ """
+ Handle the OAI-PMH exporting.
+ """
+ if not "verb" in request.GET:
+ invalid_html = oai_server_inst.handleRequest({})
+ return HttpResponse(content=invalid_html,
+ status=400,
+ content_type="text/xml")
+
+ verb = request.GET.get("verb")
+ content = oai_server_inst.handleVerb(verb, {})
+ return HttpResponse(content=content,
+ status=200,
+ content_type="text/xml")
diff --git a/metashare/templates/header.html b/metashare/templates/header.html
index 70fd2040d..b7ffd2e5e 100644
--- a/metashare/templates/header.html
+++ b/metashare/templates/header.html
@@ -24,6 +24,10 @@
{# drop-down menu created with http://javascript-array.com/scripts/jquery_simple_drop_down_menu/ #}
- Browse Resources
+ {% if user.is_staff and user.get_profile.has_editor_permission %}
+ - Import OAI-PMH
+ - Export OAI-PMH
+ {% endif %}
{% if user.get_profile.has_editor_permission %}
- Manage Resources
diff --git a/metashare/templates/repository/oai_pmh_export.html b/metashare/templates/repository/oai_pmh_export.html
new file mode 100644
index 000000000..1791f8b85
--- /dev/null
+++ b/metashare/templates/repository/oai_pmh_export.html
@@ -0,0 +1,32 @@
+{% extends "base.html" %}
+
+{% block title %}
+ – Export OAI-PMH –
+{% endblock %}
+
+{% block content %}
+OAI-PMH export
+
+{% if error_str %}
+ {{ error_str }}
+{% endif %}
+
+
+
OAI-PMH Settings
+
+
+
+ | OAI-PMH Identify | test |
+
+
+ | OAI-PMH ListIdentifiers | test |
+
+
+ | OAI-PMH error | test |
+
+
+
+
+
+
+{% endblock %}
diff --git a/metashare/templates/repository/oai_pmh_import.html b/metashare/templates/repository/oai_pmh_import.html
new file mode 100644
index 000000000..e63bcbce0
--- /dev/null
+++ b/metashare/templates/repository/oai_pmh_import.html
@@ -0,0 +1,105 @@
+{% extends "base.html" %}
+
+{% block title %}
+ – Import OAI-PMH –
+{% endblock %}
+
+{% block content %}
+OAI-PMH (UFAL, Charles University specific implementation)
+
+{% if error_str %}
+ {{ error_str }}
+{% endif %}
+
+
+ {% if showform == "1" %}
+
OAI-PMH Settings
+
+ {% endif %}
+
+
+
+ {% if data_dict %}
+
+
+ Result: {{ data_dict_title }}
+
+
+ Info detail:{{ data_info }}
+
+
+
+
+ {% for k, v in data_dict.iteritems %}
+
+ | {{ k }} |
+ {{ v }} |
+
+ {% endfor %}
+
+
+ {% endif %}
+
+ {% if data_info %}
+
+
Info detail
{{ data_info }}
+
+ {% endif %}
+
+ {% if javascript_for_id %}
+
+
+
+ {% endif %}
+
+
+
+
+{% endblock %}