class FindingAid(XmlModel, eadmap.EncodedArchivalDescription):
    """
    Customized version of :class:`eulxml.EncodedArchivalDescription` EAD object.

    Additional fields and methods are used for search, browse, and display.
    """
    ROOT_NAMESPACES = {
        'e': eadmap.EAD_NAMESPACE,
        'xlink': eadmap.XLINK_NAMESPACE,
        'exist': 'http://exist.sourceforge.net/NS/exist',
        'util': 'http://exist-db.org/xquery/util',
    }
    # redeclaring namespace from eulxml to ensure prefix is correct for xpaths

    # NOTE: overridding these fields from EncodedArchivalDescription to allow
    # for efficiently retrieving unittitle and abstract in the full document OR
    # in the constructed return object returned from eXist for search/browse
    unittitle = xmlmap.NodeField('.//e:unittitle[not(ancestor::e:dsc)]', eadmap.UnitTitle)
    abstract = xmlmap.NodeField('.//e:abstract[not(ancestor::e:dsc)]', xmlmap.XmlObject)
    physical_descriptions = xmlmap.NodeListField('.//e:physdesc[not(ancestor::e:dsc)]',
        PhysicalDescription)

    list_title_xpaths = [
        "e:archdesc/e:did/e:origination/e:corpname",
        "e:archdesc/e:did/e:origination/e:famname",
        "e:archdesc/e:did/e:origination/e:persname",
        "e:archdesc/e:did[count(e:origination/e:corpname|e:origination/e:famname|e:origination/e:persname) = 0]/e:unittitle"
    ]
    list_title_xpath = "|".join("./%s" % xp for xp in list_title_xpaths)
    # ./archdesc/did/origination/node()|./archdesc/did[not(origination/node())]/unittitle"

    # field to use for alpha-browse - any origination name, fall back to unit title if no origination
    list_title = xmlmap.NodeField(list_title_xpath, xmlmap.XmlObject)
    "list title used for alphabetical browse - any origination name, or unittitle if there is none"

    list_title_normalized = xmlmap.StringField('normalize-space(%s)' %
                                               list_title_xpath)
    'list title variant with normalized whitespace, for searching'

    # first letter of title field
    first_letter = xmlmap.StringField("substring(normalize-space(%s),1,1)" % list_title_xpath)
    "First letter of list title"

    dc_subjects = xmlmap.StringListField('e:archdesc//e:controlaccess/e:subject[@encodinganalog = "650"] | \
            e:archdesc//e:controlaccess/e:persname[@encodinganalog = "600"] | \
            e:archdesc//e:controlaccess/e:corpname[@encodinganalog = "610"] | \
            e:archdesc//e:controlaccess/e:corpname[@encodinganalog = "611"] | \
            e:archdesc//e:controlaccess/e:geogname[@encodinganalog = "651"]', normalize=True)
    "control access fields that should be mapped to Dublin Core subject, based on encodinganalog attribute"

    dc_contributors = xmlmap.StringListField('e:archdesc//e:controlaccess/e:persname[@encodinganalog = "700"] | \
        e:archdesc//e:controlaccess/e:corpname[@encodinganalog = "710"]', normalize=True)
    "control access fields that should be mapped to Dublin Core contributor, based on encodinganalog attribute"

    # convenience mapping for searching on subject fields
    subject = xmlmap.StringField('.//e:controlaccess')

    # local repository *subarea* - e.g., MARBL, University Archives
    repository = xmlmap.StringListField('.//e:subarea', normalize=True)

    # boosted fields in the index: must be searched to get proper relevance score
    boostfields = xmlmap.StringField('''.//e:titleproper | .//e:origination |
        .//e:abstract | .//e:bioghist | .//e:scopecontent | .//e:controlaccess''')

    # temporary manual mapping for processinfo, will be incorporated into a release of eulxml
    process_info = xmlmap.NodeField("e:archdesc/e:processinfo", eadmap.Section)

    # is mapped as single in eulxml.eadmap but could be multiple
    separatedmaterial_list = xmlmap.NodeListField("e:archdesc/e:separatedmaterial", eadmap.Section)
    relatedmaterial_list = xmlmap.NodeListField("e:archdesc/e:relatedmaterial", eadmap.Section)

    # match-count on special groups of data for table of contents listing
    # - administrative info fields
    _admin_info = ['userestrict', 'altformavail', 'relatedmaterial', 'separatedmaterial',
                   'acqinfo', 'custodhist', 'prefercite', 'appraisal']


    # -- map as regular xmlmap field, for use when entire object is returned
    admin_info_matches = xmlmap.IntegerField(
        'count(./e:archdesc/*[' +
        '|'.join(['self::e:%s' % field for field in _admin_info]) + ']//exist:match)')
    # -- eXist-specific xpath for returning count without entire document
    admin_info_matches_xpath = 'count(util:expand(%(xq_var)s/e:archdesc/(' + \
        '|'.join(['e:%s' % field for field in _admin_info]) + '))//exist:match)'
    # - collection description fields
    _coll_desc = ['bioghist', 'bibliography', 'scopecontent', 'arrangement', 'otherfindaid']
    # -- map as regular xmlmap field, for use when entire object is returned
    coll_desc_matches = xmlmap.IntegerField(
        'count(' + '|'.join('./e:archdesc/e:%s//exist:match' % field for field in _coll_desc) + ')')
    # -- eXist-specific xpath for returning count without entire document
    coll_desc_matches_xpath = 'count(util:expand(%(xq_var)s/e:archdesc/(' +  \
        '|'.join('e:%s' % field for field in _coll_desc) + '))//exist:match)'
    # - controlaccess match-count
    controlaccess_matches_xpath = 'count(util:expand(%(xq_var)s/e:archdesc/e:controlaccess)//exist:match)'

    origination_name = xmlmap.NodeField('e:archdesc/e:did/e:origination/e:*', Name)
    'origination name, as an instance of :class:`Name`'

    # dao anywhere in the ead, to allow filtering on finding aids with daos
    daos = xmlmap.NodeListField('.//e:dao', DigitalArchivalObject)
    #: count of public dao elements in a record, to support search filtering
    #: "public" is defined as audience external or not set, xlink:href present,
    #: and show not set to none.
    public_dao_count = xmlmap.IntegerField('count(.//e:dao[@xlink:href][not(@xlink:show="none")][not(@audience) or @audience="external"])')

    objects = Manager('/e:ead')
    """:class:`eulexistdb.manager.Manager` - similar to an object manager
        for django db objects, used for finding and retrieving FindingAid objects
        in eXist.

        Configured to use */ead* as base search path.
    """

    @property
    def has_digital_content(self):
        '''boolean to indicate whether or not this EAD includes public
        digital content.

        .. Note::
            If using partial xml return, requires that `public_dao_count`
            is included.

        '''
        return self.public_dao_count >= 1

    @property
    def stored_offsite(self):
        '''boolean to indicate if collection materials are stored offsite,
        based on text in the collection access restriction note.

        .. Note::

            If using partial xml return, requires that
            `archdesc__access_restriction` is included.
        '''
        return 'collection stored off-site' in \
            unicode(self.archdesc.access_restriction).lower()

    def admin_info(self):
        """
        Generate a list of administrative information fields from the archive description.
        Only includes MARBL-designated fields to be displayed as administrative information,
        in a specified order.  Any fields not present in the finding aid will
        not be included in the list returned.

        These fields are included, in this order:
        access restrictions, use restrictions, alternate form, related material,
        separated material, acquisition info, custodial history, preferred citation

        :rtype: list of :class:`eulxml.xmlmap.eadmap.Section`
        """
        info = []
        if self.archdesc.access_restriction:
            info.append(self.archdesc.access_restriction)
        if self.archdesc.use_restriction:
            info.append(self.archdesc.use_restriction)
        if self.archdesc.alternate_form:
            info.append(self.archdesc.alternate_form)
        for rel_m in self.relatedmaterial_list:
            info.append(rel_m)
        for sep_m in self.separatedmaterial_list:
            info.append(sep_m)
        if self.archdesc.acquisition_info:
            info.append(self.archdesc.acquisition_info)
        if self.archdesc.custodial_history:
            info.append(self.archdesc.custodial_history)
        if self.archdesc.preferred_citation:
            info.append(self.archdesc.preferred_citation)
        if self.archdesc.appraisal:
            info.append(self.archdesc.appraisal)
        if self.process_info:
            info.append(self.process_info)
        return info

    def collection_description(self):
        """
        Generate a list of collection description fields from the archive description.
        Only includes MARBL-designated fields to be displayed as collection description,
        in a specified order.  Any fields not present in the finding aid will
        not be included in the list returned.

        These fields are included, in this order:
        biography/history, bibliography, scope & content, arrangement, other finding aid

        :rtype: list of :class:`eulxml.xmlmap.eadmap.Section`
        """
        fields = []
        if self.archdesc.biography_history:
            fields.append(self.archdesc.biography_history)
        if self.archdesc.bibliography:
            fields.append(self.archdesc.bibliography)
        if self.archdesc.scope_content:
            fields.append(self.archdesc.scope_content)
        if self.archdesc.arrangement:
            fields.append(self.archdesc.arrangement)
        if self.archdesc.other:
            fields.append(self.archdesc.other)

        return fields

    def dc_fields(self):
        """
        Generate a dictionary of Dublin Core fields from an EAD.
        Dictionary key: base name of a Dublin Core field (e.g., creator, title)
        Dictionary value: a list of values corresponding to the DC field.
        Note that some keys may have empty lists as their values.

        :rtype: dict
        """
        fields = dict()
        fields["title"] = set([self.title, self.unittitle])
        fields["creator"] = set([name for name in [self.archdesc.origination] if name])
        fields["publisher"] = set([self.file_desc.publication.publisher])
        fields["date"] = set([date.normalized for date in [self.profiledesc.date] if date])
        fields["language"] = set(self.profiledesc.language_codes)
        fields["subject"] = set(self.dc_subjects)
        fields["contributor"] = set(self.dc_contributors)
        fields["identifier"] = set([self.eadid.url])

        return fields


    collection_id = xmlmap.StringField('e:archdesc/e:did/e:unitid/@identifier')

    def collection_uri(self):
        # URI to use in RDF for the archival collection, as distinguished
        # from the findingaid document that describes the collection and
        # the materials that it includes.

        if self.collection_id is not None and \
           self.collection_id.startswith('http'):
            # if collection id is a URL, use that
            return self.collection_id

        else:
            # otherwise use findingaid ARK as base for collection URI
            return '%s#collection' % self.eadid.url

    def absolute_eadxml_url(self):
        ''' Generate an absolute url to the xml view for this ead
            for use with external services such as Aeon'''

        current_site = Site.objects.get_current()
        return ''.join([
            'http://',
            current_site.domain.rstrip('/'),
            reverse('fa:eadxml', kwargs={"id":self.eadid.value})
            ])

    def request_materials_url(self):
        ''' Construct the absolute url for use with external services such as Aeon'''

        if not hasattr(settings, 'REQUEST_MATERIALS_URL') or not settings.REQUEST_MATERIALS_URL:
            logger.warn("Request materials url is not configured.")
            return

        base = settings.REQUEST_MATERIALS_URL
        return ''.join([base, self.absolute_eadxml_url()])

    def requestable(self):
        ''' Determines if the EAD is applicable for the electronic request service.'''

        # If the request url is not configured, then the request can't be generated.
        if not hasattr(settings, 'REQUEST_MATERIALS_URL') or not settings.REQUEST_MATERIALS_URL:
            return False

        if not hasattr(settings, 'REQUEST_MATERIALS_REPOS') or not settings.REQUEST_MATERIALS_REPOS:
            return False

        # If the item is in on of the libraries defined, then it should be displayed.
        return any([normalize_whitespace(repo) in settings.REQUEST_MATERIALS_REPOS
                    for repo in self.repository]) class Deleted(models.Model):
    """
    Information about a previously published finding aid that has been deleted.
    """
    eadid = models.CharField('EAD Identifier', max_length=50, unique=True)
    title = models.CharField(max_length=200)
    date = models.DateTimeField('Date removed', auto_now_add=True)
    note = models.CharField(
        max_length=400, blank=True,
        help_text="Optional: Enter the reason this document is being deleted. " +
                  "These comments will be displayed to anyone who had the finding " +
                  "aid bookmarked and returns after it is gone.")

    class Meta:
        verbose_name = 'Deleted Record'

    def __unicode__(self):
        return self.eadid class Archive(models.Model):
    '''Model to define Archives associated with EAD documents, for use with
    admin user permissions and to identify subversion repositories where
    content will be published from.'''
    label = models.CharField(max_length=10,
        help_text='Short label to identify an archive')
    name = models.CharField(max_length=255,
        help_text='repository name (subarea) in EAD to identify finding aids associated with this archive')
    svn = models.URLField('Subversion Repository',
        help_text='URL to subversion repository containing EAD for this archive')
    slug = models.SlugField(help_text='''shorthand id
        (auto-generated from label; do not modify after initial archive definition)''')
    contacts = models.ManyToManyField(settings.AUTH_USER_MODEL,
        help_text='Contact person for display on the Request Materials page (email required)',
        blank=True)


    def __unicode__(self):
        return self.label

    @property
    def svn_local_path(self):
        return os.path.join(settings.SVN_WORKING_DIR, self.slug)

    def contact_names(self):
        '''List of contact names method for display in django admin list
        display.  Shows email if user has no full name.'''
        return ', '.join([contact.get_full_name() or contact.email
                          for contact in self.contacts.all()])
    contact_names.short_description = "Contacts" def pages_to_show(paginator, page, page_labels={}):
    """Generate a dictionary of pages to show around the current page. Show
    3 numbers on either side of the specified page, or more if close to end or
    beginning of available pages.

    :param paginator: paginator object, populated with objects
    :param page: number of the current page
    :param page_labels: optional dictionary of page labels, keyed on page number
    :rtype: dictionary
    """
    show_pages = {}
    if page != 1:
        before = 3      # default number of pages to show before the current page
        if page >= (paginator.num_pages - 3):   # current page is within 3 of end
            # increase number to show before current page based on distance to end
            before += (3 - (paginator.num_pages - page))
        for i in range(before, 0, -1):    # add pages from before away up to current page
            if (page - i) >= 1:
                # if there is a page label available, use that as dictionary value
                show_pages[page - i] = page_labels[page - i ] if (page - 1) in page_labels else None
    # show up to 3 to 7 numbers after the current number, depending on how many we already have
    for i in range(7 - len(show_pages)):
        if (page + i) <= paginator.num_pages:
            # if there is a page label available, use that as dictionary value
            show_pages[page + i] = page_labels[page + i] if (page + i) in page_labels else None

    return show_pages def get_findingaid(eadid=None, preview=False, only=None, also=None, order_by=None,
        filter=None):
    """Retrieve a  :class:`~findingaids.fa.models.FindingAid` (or
    :class:`~findingaids.fa.models.FindingAid`  :class:`eulexistdb.manager.Manager`)
    from eXist by eadid, with any query options specified.  Raises a
    :class:`django.http.Http404` if the requested document is not found in eXist.

    Handles switching to preview collection and then switching back when
    preview is set to True.

    :param eadid: eadid of the :class:`~findingaids.fa.models.FindingAid` to
            retrieve; if not specified, a :class:`eulexistdb.manager.Manager`
            will be returned instead
    :param preview: optional; set to True to load the finding aid from the
            preview collection; defaults to False
    :param only: optional list of fields to return (**only** return the specified fields)
    :param also: optional list of additional fields to return
    :param order_by: optional field to use for sorting
    :param filter: optional queryset filter to apply, in dictionary form
    :returns: :class:`~findingaids.fa.models.FindingAid` (when eadid is specified)
            or a :class:`~findingaids.fa.models.FindingAid` :class:`eulexistdb.manager.Manager`
            (if no eadid is specified)
    """
    try:
        fa = FindingAid.objects.all()   # make sure we always have a queryset to start with
        if only is not None:
            fa = fa.only(*only)
        if also is not None:
            fa = fa.also(*also)
        if order_by is not None:
            fa = fa.order_by(order_by)
        if filter is not None:
            fa = fa.filter(**filter)
        if preview:
            fa = fa.using(settings.EXISTDB_PREVIEW_COLLECTION)
        if eadid is not None:
            fa = fa.get(eadid=eadid)
    except DoesNotExist:
        raise http.Http404
    return fa def paginate_queryset(request, qs, per_page=10, orphans=0):    # 0 is django default
    # FIXME: should num-per-page be configurable via local settings?
    paginator = Paginator(qs, per_page, orphans=orphans)
    # Make sure page request is an int. If not, deliver first page.
    try:
        page = int(request.GET.get('page', '1'))
    except ValueError:
        page = 1
    # If page request (9999) is out of range, deliver last page of results.
    try:
        paginated_qs = paginator.page(page)
    except InvalidPage:
        raise http.Http404
    except EmptyPage:       # ??
        paginated_qs = paginator.page(paginator.num_pages)

    return paginated_qs, paginator def archive_access(user, archive=None, *args, **kwargs):
    '''Check if a user has permission to take action (e.g., prep, preview, publish)
    on a specific archive.  Superusers are have access to all archives.

    :param user: logged in user
    :param archive: slug identifier for an :class:`~findingaids.fa.models.Archive`.
    :param request: http request; optionally used to look for an 'archive'
        request parameter, if archive param is None

    :returns: true if access is allowed, false if not
    :rtype: bool
    '''
    # always false if user is not logged in
    if not user.is_authenticated():
        return False

    # always allowed if user is superuser
    # NOTE: even if archive is not specified or does not exist-
    # allow, and let the view handle that logic
    if user.is_superuser:
        return True

    # check for request in kwargs in case we need it for archive info
    request = kwargs.get('request', None)

    # if archive parameter is not set, check for an archive request parameter
    if archive is None and request is not None:
        # NOTE: not using request.REQUEST because it is deprecated in Django 1.7
        archive = request.GET.get('archive', None)
        if archive is None:
            archive = request.POST.get('archive', None)

    # error if we don't have an archive slug
    if archive is None:
        raise Exception('Archive not specified')

    try:
        arc = user.archivist
    except ObjectDoesNotExist:
        return False

    if arc.archives.filter(slug=archive).count():
        return True

    return False class DeleteForm(ModelForm):
    class Meta:
        model = Deleted
        # django 1.8 requires explicit field list or exclude list;
        # include all fields for now
        exclude = []
        # NOTE: not excluding date from edit form because on an update it MAY
        # need to be changed, but only a person can determine that
        widgets = {
            'note': Textarea(attrs={'cols': 80, 'rows': 10}),
            # display eadid and title, but don't allow them to be edited
            'eadid': TextInput(attrs={'readonly': 'readonly'}),
            'title': TextInput(attrs={'size': '80', 'readonly': 'readonly'})
        } class Archivist(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE)  # if user is deleted, delete archivist too
    archives = models.ManyToManyField(Archive, blank=True)
    order = models.CommaSeparatedIntegerField(max_length=255, blank=True,
        null=True)

    def __repr__(self):
        return '<Archivist %s: %s>' % (self.user.username,
            ', '.join(arch.label for arch in self.archives.all()))

    def __unicode__(self):
        return u'Archivist %s (%s)' % (unicode(self.user),
            ', '.join(arch.label for arch in self.archives.all()))

    def sorted_archives(self):
        '''List of archives this user is associated with, in order if
        they have specified any order preference.'''
        archives = self.archives.all()
        # if no archives are explicitly defined and this is a superuser,
        # give them access to all
        if not archives and self.user.is_superuser:
            archives = Archive.objects.all()
        if self.order:
            id_order = [int(id) for id in self.order.split(',')]
            # if id is not present (e.g., new archive), sort to the end
            return sorted(archives,
                key=lambda a: id_order.index(a.id) if a.id in id_order else 50)

        return archives @task
def reload_cached_pdf(eadid):
    """Request the PDF of the finding aid (specified by eadid) from the configured
    proxy server, to trigger the proxy reloading and caching the latest version
    of that PDF (e.g., after updating or adding a new document in eXist)."""
    logger = reload_cached_pdf.get_logger()
    if hasattr(settings, 'PROXY_HOST') and hasattr(settings, 'SITE_BASE_URL'):
        sleep(3)    # may need to sleep for a few seconds so cache will recognized as modified (?)
        url = "%s%s" % (settings.SITE_BASE_URL.rstrip('/'),
            reverse('fa:printable', kwargs={'id': eadid}))
        logger.info("Requesting PDF for %s from configured cache at %s" % (eadid, url))
        # set headers to force the cache to get a fresh copy
        basic_headers = {
            'User-Agent': 'FindingAids PDF Reloader/%s' % SW_VERSION,
            'Accept': '*/*',
        }
        refresh_cache = {
            # tell the cache to grab a fresh copy (implied: cache the fresh copy)
            'Cache-Control': 'max-age=0',
            # squid doesn't seem to cache the new version when we specify no-cache,
            # even though that is not what the spec says no-cache means
            #'Pragma': 'no-cache',
        }
        refresh_cache.update(basic_headers)
        logger.debug('Request headers: \n%s' % \
                     '\n'.join(['\t%s: %s' % header for header in refresh_cache.iteritems()]))
        # enable the configured proxy
        urllib2.ProxyHandler({'http': settings.PROXY_HOST})
        request = urllib2.Request(url, None, refresh_cache)
        response = urllib2.urlopen(request)
        logger.debug('Response headers: \n%s' % response.info())

        if response.code != 200:
            raise Exception("Got unexpected HTTP status code from response: %s" \
                            % response.code)
        return True

    else:
        raise Exception("PROXY_HOST and/or SITE_BASE_URL settings not available.  Failed to reload cached PDF.") def check_ead(filename, dbpath, xml=None):
def check_eadxml(ead):
def check_series_ids(series):
def prep_ead(ead, filename):
def generate_ark(ead):
def set_series_ids(series, eadid, position):
import os
import difflib
import logging
import time
import datetime
from lxml.etree import XMLSyntaxError
from django.http import HttpResponse, HttpResponseServerError, Http404, \
    HttpResponseBadRequest
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.views import logout_then_login
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import Paginator, EmptyPage, InvalidPage
from django.core.urlresolvers import reverse
from django.core.cache import cache
from django.shortcuts import render, get_object_or_404
from django.views.decorators.cache import cache_page
from django.views.decorators.http import require_POST
from django_auth_ldap.backend import LDAPBackend
from eulcommon.djangoextras.auth import permission_required_with_403, \
   login_required_with_ajax, user_passes_test_with_ajax
from eulexistdb.db import ExistDB, ExistDBException
from eulcommon.djangoextras.http import HttpResponseSeeOtherRedirect
from eulcommon.djangoextras.taskresult.models import TaskResult
from eullocal.django.log import message_logging
from eulxml.xmlmap.core import load_xmlobject_from_file, load_xmlobject_from_string
from eulexistdb.exceptions import DoesNotExist
from findingaids.fa.models import FindingAid, Deleted, Archive
from findingaids.fa.utils import pages_to_show, get_findingaid, paginate_queryset
from findingaids.fa_admin.auth import archive_access
from findingaids.fa_admin.forms import DeleteForm
from findingaids.fa_admin.models import Archivist
from findingaids.fa_admin.source import files_to_publish
from findingaids.fa_admin.svn import svn_client
from findingaids.fa_admin.tasks import reload_cached_pdf
from findingaids.fa_admin import utils
@user_passes_test_with_ajax(archive_access)   # could add last-modified but ajax doesn't cache
def list_files(request, archive):
    '''List files associated with an archive to be prepped and previewed
    for publication.  Expected to be retrieved via ajax and loaded in a
    jquery ui tab, so only returns a partial html page without site theme.
    '''
    archive = get_object_or_404(Archive, slug=archive)

    files = files_to_publish(archive)
    # sort by last modified time, most recent first
    files = sorted(files, key=lambda file: file.mtime, reverse=True)

    paginator = Paginator(files, 30, orphans=5)
    try:
        page = int(request.GET.get('page', '1'))
    except ValueError:
        page = 1
    show_pages = pages_to_show(paginator, page)
    # If page request (9999) is out of range, deliver last page of results.
    try:
        recent_files = paginator.page(page)
    except (EmptyPage, InvalidPage):
        recent_files = paginator.page(paginator.num_pages)

    # query for publish/preview modification time all at once
    # (more efficient than individual queries for each file)
    published = FindingAid.objects.only('document_name', 'last_modified') \
        .filter(document_name__in=[f.filename for f in recent_files.object_list])
    pubinfo = dict((r.document_name, r.last_modified) for r in published)
    # NOTE: if needed, we can also load preview info like this:
    # preview = published.using(settings.EXISTDB_PREVIEW_COLLECTION)

    for f in recent_files.object_list:
        f.published = pubinfo.get(f.filename, None)

    return render(request, 'fa_admin/snippets/list_files_tab.html', {
        'files': recent_files,
        'show_pages': show_pages})

@require_POST
@login_required_with_ajax()
def archive_order(request):
    # expects a comma-separated list of archive slugs
    ids = request.POST.get('ids', None)
    if not ids:
        return HttpResponseBadRequest()

    slugs = ids.split(',')
    # find all archives matching any of the slugs passed in
    archives = Archive.objects.filter(slug__in=slugs)
    # re-sort according to the order in the request
    archives = sorted(archives, key=lambda arch: slugs.index(arch.slug))

    # save order to user account
    try:
        arc = request.user.archivist
    except ObjectDoesNotExist:
        # if for some reason user model does not have an archivist,
        # create one so we can store the order preference
        arc = Archivist()
        request.user.archivist = arc

    arc.order = ','.join([str(a.id) for a in archives])
    request.user.archivist.save()

    return HttpResponse('Updated order')

@require_POST
@login_required_with_ajax()
def current_archive(request):
    # Store the cerrently active archive tab in the main admin page,
    # so it can be automatically reloaded when returning there.
    # Expects a numeric id for the index of the tab to be active.
    tab_id = request.POST.get('id', None)
    if not tab_id:
        return HttpResponseBadRequest()
    request.session['active_admin_tab'] = tab_id
    return HttpResponse('Saved current tab')


@login_required
def logout(request):
    """Admin Logout view. Displays a message and then calls
    :meth:`django.contrib.auth.views.logout_then_login`.
    """
    # make sure we reset any admin tab selection
    if 'active_admin_tab' in request.session:
        del request.session['active_admin_tab']
    messages.success(request, 'You are now logged out.')
    return logout_then_login(request)


def _prepublication_check(request, filename, archive, mode='publish', xml=None):
    """
    Pre-publication check logic common to :meth:`publish` and :meth:`preview`.

    Generates a full path to the file in the configured EAD source directory,
    and the expected published location in eXist, and then runs
    :meth:`~findingaids.fa_admin.utils.check_ead` to check the xml for errors.

    If there are errors, will generate an error response that can be displayed.

    :param request: request object passed into the view (for generating error response)
    :param filename: base filename of the ead file to be checked
    :param archive: :class:`~findingaids.fa.models.Archive`, used to locate
        the file on disk
    :param mode: optional mode, for display on error page (defaults to publish)

    :rtype: list
    :returns: list of the following:
      - boolean ok (if True, all checks passed)
      - HttpResponse response error response to display, if there were errors
      - dbpath - full path to publication location in configured eXist db
      - fullpath - full path to the file in the configured source directory
    """

    # full path to the local file
    fullpath = os.path.join(archive.svn_local_path, filename)
    # full path in exist db collection
    dbpath = settings.EXISTDB_ROOT_COLLECTION + "/" + filename

    errors = utils.check_ead(fullpath, dbpath, xml)