def full_findingaid(request, id, mode, preview=False): """View the full contents of a single finding aid as PDF or plain html. :param id: eadid for the document to be displayed :param mode: one of 'html' or 'pdf' - note that the html mode is not publicly linked anywhere, and is intended mostly for development and testing of the PDF display :param preview: boolean indicating preview mode, defaults to False """ fa = get_findingaid(id, preview=preview) series = _subseries_links(fa.dsc, url_ids=[fa.eadid], url_callback=_series_anchor, preview=preview) template = 'fa/full.html' template_args = {'ead': fa, 'series': series, 'mode': mode, 'preview': preview, 'request': request, # normally supplied by context processor 'DEFAULT_DAO_LINK_TEXT': getattr(settings, 'DEFAULT_DAO_LINK_TEXT', '[Resource available online]') } if mode == 'html': return render(request, template, template_args) elif mode == 'pdf': return render_to_pdf(template, template_args, filename='%s.pdf' % fa.eadid.value) elif mode == 'xsl-fo': xslfo = html_to_xslfo(template, template_args) return HttpResponse(etree.tostring(xslfo), content_type='application/xml')
def eadxml(request, id, preview=False): """Display the full EAD XML content of a finding aid. :param id: eadid for the document to be displayed :param preview: boolean indicating preview mode, defaults to False """ fa = get_findingaid(id, preview=preview) xml_ead = fa.serialize(pretty=True) return HttpResponse(xml_ead, content_type='application/xml')
def preview(request, archive): if request.method == 'POST': archive = get_object_or_404(Archive, slug=archive) filename = request.POST['filename'] errors = [] err = None try: # only load to exist if document passes publication check ok, response, dbpath, fullpath = _prepublication_check(request, filename, archive, mode='preview') if ok is not True: return response db = ExistDB() # load the document to the *preview* collection in eXist with the same fileneame preview_dbpath = settings.EXISTDB_PREVIEW_COLLECTION + "/" + filename # make sure the preview collection exists, but don't complain if it's already there success = db.load(open(fullpath, 'r'), preview_dbpath) except ExistDBException as err: success = False errors.append(err.message()) if success: # load the file as a FindingAid object so we can generate the preview url ead = load_xmlobject_from_file(fullpath, FindingAid) messages.success(request, 'Successfully loaded <b>%s</b> for preview.' % filename) # redirect to document preview page with code 303 (See Other) return HttpResponseSeeOtherRedirect(reverse('fa-admin:preview:findingaid', kwargs={'id': ead.eadid})) else: # no exception but no success means the load failed; # *probably* due to insufficient permissions if errors == [] and success == False: errors.append('Failed to load the document to the preview collection') return render(request, 'fa_admin/publish-errors.html', {'errors': errors, 'filename': filename, 'mode': 'preview', 'exception': err}) # NOTE: preview list is not used anymore; functionality is handled # by main admin view; if we revisit preview list, to be more usable it # should be filterable by archive else: fa = get_findingaid(preview=True, only=['eadid', 'list_title', 'last_modified'], order_by='last_modified') return render(request, 'fa_admin/preview_list.html', {'findingaids': fa, #'querytime': [fa.queryTime()] })
def get_previewed(self): """Date object was loaded to eXist preview collection, if currently available in preview.""" if self._previewed is None: try: fa = get_findingaid(filter={'document_name': self.filename}, only=['last_modified'], preview=True) if fa.count(): self._previewed = fa[0].last_modified except Exception: pass # not found or error - store so we don't look up again if self._previewed is None: self._previewed = False return self._previewed
def findingaid(request, id, preview=False): """View a single finding aid. In preview mode, pulls the document from the configured eXist-db preview collection instead of the default public one. :param id: eadid for the document to view :param preview: boolean indicating preview mode, defaults to False """ if 'keywords' in request.GET: search_terms = request.GET['keywords'] url_params = '?' + urlencode({'keywords': search_terms.encode('utf-8')}) filter = {'highlight': search_terms} else: url_params = '' filter = {} fa = get_findingaid(id, preview=preview, filter=filter) last_modified = ead_lastmodified(request, id, preview) series = _subseries_links(fa.dsc, url_ids=[fa.eadid], preview=preview, url_params=url_params) extra_ns = RDFA_NAMESPACES.copy() # add any non-default namespaces from the EAD document extra_ns.update(dict((prefix, ns) for prefix, ns in fa.node.nsmap.iteritems() if prefix is not None)) context = { 'ead': fa, 'series': series, 'all_indexes': fa.archdesc.index, 'preview': preview, 'url_params': url_params, 'docsearch_form': KeywordSearchForm(), 'last_search': request.session.get('last_search', None), 'feedback_opts': _get_feedback_options(request, id), 'extra_ns': extra_ns, 'last_modified': last_modified, } # provide series list without keyword params to use in RDFa uris if url_params and not preview: context['series_noparam'] = _subseries_links(fa.dsc, url_ids=[fa.eadid]) response = render(request, 'fa/findingaid.html', context, current_app='preview') # Set Cache-Control to private when there is a last_search if "last_search" in request.session: response['Cache-Control'] = 'private' return response
def get_published(self): "Date object was modified in eXist, if published" # TODO: previewed & published logic substantially the same; consolidate if self._published is None: try: fa = get_findingaid(filter={'document_name': self.filename}, only=['last_modified']) if fa.count(): self._published = fa[0].last_modified except Exception: # FIXME: distinguish between not found and eXist error? pass # not found or error - store so we don't look it up again if self._published is None: self._published = False return self._published
def feedback(request): '''Feedback form. On GET, displays the form; on POST, processes the submitted form and sends an email (if all required fields are present).''' ead = None if request.method == 'POST': data = request.POST.copy() data['remote_ip'] = request.META['REMOTE_ADDR'] form = FeedbackForm(data) if form.is_valid(): err = None try: email_ok = form.send_email() # if it processed without exception, email should be sent ok except Exception as ex: err = ex email_ok = False # display a success/thank you page response = render_to_response('content/feedback.html', { 'email_sent': email_ok, 'err': err, }, context_instance=RequestContext(request)) # if the email didn't go through, don't return a 200 ok status if not email_ok: response.status_code = 500 return response else: ead = None if 'eadid' in request.GET: # retrieve minimal ead info to display ead title to user on the form try: ead = get_findingaid(eadid=request.GET['eadid'], only=['title']) except: # if retrieval fails, ignore it - not required for the form to work pass # GET may include eadid & url; use as initial data to populate those fields form = FeedbackForm(initial=request.GET) captcha_theme = getattr(settings, 'RECAPTCHA_THEME', None) return render_to_response('content/feedback.html', { 'form': form, 'findingaid': ead, 'captcha_theme': captcha_theme, }, context_instance=RequestContext(request))
def send_email(self): '''Send an email based on the posted form data. This method should not be called unless the form has validated. Sends a "Site Feedback" email to feedback email address configured in Django settings with the message submitted on the form. If email and name are specified, they will be used as the From: address in the email that is generated; otherwise, the email will be sent from the SERVER_EMAIL setting configured in Django settings. Returns true when the email send. Could raise an :class:`smtplib.SMTPException` if something goes wrong at the SMTP level. ''' # construct a simple text message with the data from the form msg_parts = [] # name & email are optional - repeat in email message if set if self.cleaned_data['email'] or self.cleaned_data['name']: msg_parts.append('Feedback from %(name)s %(email)s' % self.cleaned_data) # eadid & url are optional, hidden - will be set together for single-finding aid feedback if self.cleaned_data['eadid'] and self.cleaned_data['url']: eadid = self.cleaned_data['eadid'] ead = get_findingaid(eadid=eadid, only=['title']) msg_parts.append("Feedback about %s (%s)\nFrom url: %s" % \ (unicode(ead.title), eadid, self.cleaned_data['url'])) # message is required, so should always be set msg_parts.append(self.cleaned_data['message']) # join together whatever parts of the message should be present message = '\n\n'.join(msg_parts) # send an email with settings comparable to mail_admins or mail_managers return send_mail( settings.EMAIL_SUBJECT_PREFIX + self.email_subject, # subject message, # email text generate_from_email(self.cleaned_data), # from address settings.FEEDBACK_EMAIL # list of recipient emails )
def publish(request): """ Admin publication form. Allows publishing an EAD file by updating or adding it to the configured eXist database so it will be immediately visible on the public site. Files can only be published if they pass an EAD sanity check, implemented in :meth:`~findingaids.fa_admin.utils.check_ead`. On POST, sanity-check the EAD file specified in request from the configured and (if it passes all checks), publish it to make it immediately visible on the site. If publish is successful, redirects the user to main admin page with a success message that links to the published document on the site. If the sanity-check fails, displays a page with any problems found. """ # formerly supported publish from filename, but now only supports # publish from preview if 'preview_id' not in request.POST: messages.error(request, "No preview document specified for publication") return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) id = request.POST['preview_id'] # retrieve info about the document from preview collection try: # because of the way existdb.query.queryset constructs returns with 'also' fields, # it is simpler and better to retrieve document name separately ead = get_findingaid(id, preview=True) ead_docname = get_findingaid(id, preview=True, only=['document_name']) filename = ead_docname.document_name except (ExistDBException, Http404): # not found in exist OR permission denied messages.error(request, '''Publish failed. Could not retrieve <b>%s</b> from preview collection. Please reload and try again.''' % id) # if ead could not be retrieved from preview mode, skip processing return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) # determine archive this ead is associated with archive = None if not ead.repository: messages.error(request, '''Publish failed. Could not determine which archive <b>%s</b> belongs to. Please update subarea, reload, and try again.''' % id) else: archive_name = ead.repository[0] # NOTE: EAD supports multiple subarea tags, but in practice we only # use one, so it should be safe to assume the first should be used for permissions try: archive = Archive.objects.get(name=archive_name) except ObjectDoesNotExist: messages.error(request, '''Publish failed. Could not find archive <b>%s</b>.''' % archive_name) # bail out if archive could not be identified if archive is None: return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) # check that user is allowed to publish this document if not archive_access(request.user, archive.slug): messages.error(request, '''You do not have permission to publish <b>%s</b> materials.''' \ % archive.label) return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) errors = [] try: # NOTE: *not* using serialized xml here, because it may introduce # whitespace errors not present in the original file. ok, response, dbpath, fullpath = _prepublication_check(request, filename, archive) if ok is not True: # publication check failed - do not publish return response # only load to exist if there are no errors found db = ExistDB() # get information to determine if an existing file is being replaced replaced = db.describeDocument(dbpath) try: # move the document from preview collection to configured public collection success = db.moveDocument(settings.EXISTDB_PREVIEW_COLLECTION, settings.EXISTDB_ROOT_COLLECTION, filename) # FindingAid instance ead already set above except ExistDBException, e: # special-case error message errors.append("Failed to move document %s from preview collection to main collection." \ % filename) # re-raise and let outer exception handling take care of it raise e except ExistDBException as err: errors.append(err.message()) success = False if success: # request the cache to reload the PDF - queue asynchronous task result = reload_cached_pdf.delay(ead.eadid.value) task = TaskResult(label='PDF reload', object_id=ead.eadid.value, url=reverse('fa:findingaid', kwargs={'id': ead.eadid.value}), task_id=result.task_id) task.save() ead_url = reverse('fa:findingaid', kwargs={'id': ead.eadid.value}) change = "updated" if replaced else "added" messages.success(request, 'Successfully %s <b>%s</b>. View <a href="%s">%s</a>.' % (change, filename, ead_url, unicode(ead.unittitle))) # redirect to main admin page and display messages return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) else: return render(request, 'fa_admin/publish-errors.html', {'errors': errors, 'filename': filename, 'mode': 'publish', 'exception': err})
def document_search(request, id): "Keyword search on file-level items in a single Finding Aid." form = KeywordSearchForm(request.GET) query_error = False # get the findingaid - will 404 if not found # document/collection name required to generate document path ead = get_findingaid(id, only=['eadid', 'title', 'repository', 'document_name', 'collection_name']) if form.is_valid(): search_terms = form.cleaned_data['keywords'] try: # do a full-text search at the file level # include parent series information and enough ancestor series ids # in order to generate link to containing series at any level (c01-c03) # use path to restrict query to a single document (much faster) path = '%s/%s' % (ead.collection_name, ead.document_name) files = FileComponent.objects.filter(document_path=path) # at least one of search terms or dao may be present, # but both are optional # filter by keyword if present if search_terms: files = files.filter(fulltext_terms=search_terms) # restrict to publicly-accessible dao items, if set if form.cleaned_data['dao']: files = files.filter(did__dao_list__exists=True) # if user can view internal daos, no additional filter is needed # otherwise, restrict to publicly-accessible dao content if not request.user.has_perm('fa_admin.can_view_internal_dao'): files = files.filter(public_dao_count__gte=1) files = files.also('parent__id', 'parent__did', 'series1__id', 'series1__did', 'series2__id', 'series2__did') # if there is a keyword search term, pass on for highlighting url_params = '' if search_terms: url_params = '?' + urlencode({'keywords': search_terms.encode('utf-8')}) return render(request, 'fa/document_search.html', { 'files': files, 'ead': ead, # 'querytime': [files.queryTime(), ead.queryTime()], 'keywords': search_terms, 'dao': form.cleaned_data['dao'], 'url_params': url_params, 'docsearch_form': KeywordSearchForm(), }) except ExistDBTimeout: # error for exist db timeout messages.error(request, "Your search has resulted in too many hits, \ please make your terms more specific by using a direct phrase \ search (e.g. \"Martin Luther King\").") except ExistDBException as err: # for an invalid full-text query (e.g., missing close quote), eXist # error reports 'Cannot parse' and 'Lexical error' # NOTE: some duplicate logic from error handling in main keyword search query_error = True if 'Cannot parse' in err.message(): messages.error(request, 'Your search query could not be parsed. ' + 'Please revise your search and try again.') else: # generic error message for any other exception messages.error(request, 'There was an error processing your search.') else: # invalid form messages.error(request, 'Please enter a search term.') # display empty search results response = render(request, 'fa/document_search.html', { 'files': [], 'ead': ead, 'docsearch_form': KeywordSearchForm(), }) # if query could not be parsed, set a 'Bad Request' status code on the response if query_error: response.status_code = 400 return response
def publish(request): """ Admin publication form. Allows publishing an EAD file by updating or adding it to the configured eXist database so it will be immediately visible on the public site. Files can only be published if they pass an EAD sanity check, implemented in :meth:`~findingaids.fa_admin.utils.check_ead`. On POST, sanity-check the EAD file specified in request from the configured and (if it passes all checks), publish it to make it immediately visible on the site. If publish is successful, redirects the user to main admin page with a success message that links to the published document on the site. If the sanity-check fails, displays a page with any problems found. """ # formerly supported publish from filename, but now only supports # publish from preview if 'preview_id' not in request.POST: messages.error(request, "No preview document specified for publication") return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) id = request.POST['preview_id'] # retrieve info about the document from preview collection try: # because of the way eulcore.existdb.queryset constructs returns with 'also' fields, # it is simpler and better to retrieve document name separately ead = get_findingaid(id, preview=True) ead_docname = get_findingaid(id, preview=True, only=['document_name']) filename = ead_docname.document_name except Http404: # not found in exist messages.error(request, '''Publish failed. Could not retrieve <b>%s</b> from preview collection. Please reload and try again.''' % id) # if ead could not be retrieved from preview mode, skip processing return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) # determine archive this ead is associated with xml = ead.serialize() archive = None if not ead.repository: messages.error(request, '''Publish failed. Could not determine which archive <b>%s</b> belongs to. Please update subarea, reload, and try again.''' % id) else: archive_name = ead.repository[0] # NOTE: EAD supports multiple subarea tags, but in practice we only # use one, so it should be safe to assume the first should be used for permissions try: archive = Archive.objects.get(name=archive_name) except ObjectDoesNotExist: messages.error(request, '''Publish failed. Could not find archive <b>%s</b>.''' % archive_name) # bail out if archive could not be identified if archive is None: return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) # check that user is allowed to publish this document if not archive_access(request.user, archive.slug): messages.error(request, '''You do not have permission to publish <b>%s</b> materials.''' \ % archive.label) return HttpResponseSeeOtherRedirect(reverse('fa-admin:index')) errors = [] try: ok, response, dbpath, fullpath = _prepublication_check(request, filename, archive, xml=xml) if ok is not True: # publication check failed - do not publish return response # only load to exist if there are no errors found db = ExistDB() # get information to determine if an existing file is being replaced replaced = db.describeDocument(dbpath) try: # move the document from preview collection to configured public collection success = db.moveDocument(settings.EXISTDB_PREVIEW_COLLECTION, settings.EXISTDB_ROOT_COLLECTION, filename) # FindingAid instance ead already set above except ExistDBException, e: # special-case error message errors.append("Failed to move document %s from preview collection to main collection." \ % filename) # re-raise and let outer exception handling take care of it raise e except ExistDBException, e: errors.append(e.message()) success = False
errors.append(e.message()) if success: # load the file as a FindingAid object so we can generate the preview url ead = load_xmlobject_from_file(fullpath, FindingAid) messages.success(request, 'Successfully loaded <b>%s</b> for preview.' % filename) # redirect to document preview page with code 303 (See Other) return HttpResponseSeeOtherRedirect(reverse('fa-admin:preview:findingaid', kwargs={'id': ead.eadid})) else: return render(request, 'fa_admin/publish-errors.html', {'errors': errors, 'filename': filename, 'mode': 'preview', 'exception': e}) # TODO: preview list needs to either go away (not currently used) # or be filtered by archive else: fa = get_findingaid(preview=True, only=['eadid', 'list_title', 'last_modified'], order_by='last_modified') return render(request, 'fa_admin/preview_list.html', {'findingaids': fa, 'querytime': [fa.queryTime()]}) @permission_required_with_403('fa_admin.can_prepare') @cache_page(1) # cache this view and use it as source for prep diff/summary views @user_passes_test_with_ajax(archive_access) def prepared_eadxml(request, archive, filename): """On GET, serves out a prepared version of the EAD file in the specified archive subversion directory. Response header is set so the user should be prompted to download the xml, with a filename matching that of the original document. On POST, commits the prepared version of the EAD file to the subversion directory of the specified archive, with a log message indicating the user