Beispiel #1
0
    def get_notification_context(cls, notifications=None):
        """Get the context to render the notification template."""
        context = {}
        notifications = notifications or []
        if not notifications:
            return context

        context['loans'] = []
        doc_dumper = DocumentGenericDumper()
        item_dumper = ItemNotificationDumper()
        patron_dumper = PatronNotificationDumper()
        for notification in notifications:
            loan = notification.loan
            creation_date = format_date_filter(
                notification.get('creation_date'),
                date_format='medium',
                locale=language_iso639_2to1(
                    notification.get_language_to_use()))
            request_expire_date = format_date_filter(
                loan.get('request_expire_date'),
                date_format='medium',
                locale=language_iso639_2to1(
                    notification.get_language_to_use()))
            # merge doc and item metadata preserving document key
            item_data = notification.item.dumps(dumper=item_dumper)
            doc_data = notification.document.dumps(dumper=doc_dumper)
            doc_data = {**item_data, **doc_data}
            # pickup location name --> !! pickup is on notif.request_loan, not
            # on notif.loan
            request_loan = notification.request_loan
            pickup_location = Location.get_record_by_pid(
                request_loan.get('pickup_location_pid'))
            if not pickup_location:
                pickup_location = Location.get_record_by_pid(
                    request_loan.get('transaction_location_pid'))
            # request_patron
            request_patron = Patron.get_record_by_pid(
                request_loan.get('patron_pid'))

            loan_context = {
                'creation_date':
                creation_date,
                'document':
                doc_data,
                'pickup_name':
                pickup_location.get('pickup_name',
                                    pickup_location.get('name')),
                'request_expire_date':
                request_expire_date,
                'patron':
                request_patron.dumps(dumper=patron_dumper)
            }
            context['loans'].append(loan_context)

        return context
Beispiel #2
0
    def post_process_serialize_search(self, results, pid_fetcher):
        """Post process the search results.

        :param results: Elasticsearch search result.
        :param pid_fetcher: Persistent identifier fetcher.
        """
        records = results.get('hits', {}).get('hits', {})

        for record in records:
            metadata = record.get('metadata', {})
            document = search_document_by_pid(
                metadata.get('document').get('pid'))
            metadata['ui_title_text'] = title_format_text_head(
                document['title'], with_subtitle=True)

        # Add library name
        for lib_term in results.get('aggregations',
                                    {}).get('library', {}).get('buckets', []):
            lib = Library.get_record_by_pid(lib_term.get('key'))
            lib_term['name'] = lib.get('name')
        # Add location name
        for loc_term in results.get('aggregations',
                                    {}).get('location', {}).get('buckets', []):
            loc = Location.get_record_by_pid(loc_term.get('key'))
            loc_term['name'] = loc.get('name')

        # Add library name
        for item_type_term in results.get('aggregations',
                                          {}).get('item_type',
                                                  {}).get('buckets', []):
            item_type = ItemType.get_record_by_pid(item_type_term.get('key'))
            item_type_term['name'] = item_type.get('name')

        return super(ItemsJSONSerializer,
                     self).post_process_serialize_search(results, pid_fetcher)
Beispiel #3
0
def get_pickup_location_options():
    """Get all pickup location for all patron accounts."""
    for ptrn_pid in [ptrn.pid for ptrn in current_patrons]:
        for pid in Location.get_pickup_location_pids(ptrn_pid):
            location = Location.get_record_by_pid(pid)
            location_name = location.get('pickup_name', location.get('name'))
            yield (location.pid, location_name)
def test_location_cannot_delete(item_lib_martigny):
    """Test cannot delete."""
    location_pid = item_lib_martigny.location_pid
    location = Location.get_record_by_pid(location_pid)
    can, reasons = location.can_delete
    assert not can
    assert reasons['links']['holdings'] == 1
    assert reasons['links']['items'] == 1
Beispiel #5
0
def get_locations():
    """Get one pickup_location for each organisation.

    :return: A dict of locations pids by organisation
    """
    location_data = {}
    for pid in Location.get_pickup_location_pids():
        record = Location.get_record_by_pid(pid)
        if record.organisation_pid not in location_data:
            location_data[record.organisation_pid] = pid
    return location_data
Beispiel #6
0
def test_location_create(db, es_clear, loc_public_martigny_data):
    """Test location creation."""
    loc = Location.create(loc_public_martigny_data, delete_pid=True)
    assert loc == loc_public_martigny_data
    assert loc.get('pid') == '1'

    loc = Location.get_record_by_pid('1')
    assert loc == loc_public_martigny_data

    fetched_pid = fetcher(loc.id, loc)
    assert fetched_pid.pid_value == '1'
    assert fetched_pid.pid_type == 'loc'
Beispiel #7
0
 def _process_loan_pending_at_desk_in_transit_for_pickup(
         self, metadata, item_pid):
     """Process for PENDING, ITEM_AT_DESK, ITEM_IN_TRANSIT_FOR_PICKUP."""
     pickup_loc = Location.get_record_by_pid(
         metadata['pickup_location_pid'])
     metadata['pickup_name'] = \
         pickup_loc.get('pickup_name', pickup_loc.get('name'))
     if metadata['state'] == LoanState.ITEM_AT_DESK:
         metadata['rank'] = 0
     if metadata['state'] in [
             LoanState.PENDING, LoanState.ITEM_IN_TRANSIT_FOR_PICKUP
     ]:
         patron = Patron.get_record_by_pid(metadata['patron_pid'])
         item = Item.get_record_by_pid(item_pid)
         metadata['rank'] = item.patron_request_rank(patron)
Beispiel #8
0
def _build_notification_email_context(loan, item, location):
    """Build the context used by the send_notification_to_location function.

    :param loan : the loan for which build context
    :param item : the item for which build context
    :param location : the item location
    """
    document_pid = Item.get_document_pid_by_item_pid(loan.item_pid)
    document = Document.get_record_by_pid(document_pid)
    pickup_location = Location.get_record_by_pid(
        loan.get('pickup_location_pid'))
    patron = Patron.get_record_by_pid(loan.patron_pid)

    # inherit holdings call number when possible
    issue_call_number = item.issue_inherited_first_call_number
    if issue_call_number:
        item['call_number'] = issue_call_number

    ctx = {
        'loan': loan.replace_refs().dumps(),
        'item': item.replace_refs().dumps(),
        'document': document.replace_refs().dumps(),
        'pickup_location': pickup_location,
        'item_location': location.dumps(),
        'patron': patron
    }
    library = pickup_location.get_library()
    ctx['pickup_location']['library'] = library
    ctx['item']['item_type'] = \
        ItemType.get_record_by_pid(item.item_type_circulation_category_pid)
    titles = [
        title for title in ctx['document'].get('title', [])
        if title['type'] == 'bf:Title'
    ]
    ctx['document']['title_text'] = \
        next(iter(titles or []), {}).get('_text')
    responsibility_statement = create_title_responsibilites(
        document.get('responsibilityStatement', []))
    ctx['document']['responsibility_statement'] = \
        next(iter(responsibility_statement or []), '')
    trans_date = ciso8601.parse_datetime(loan.get('transaction_date'))
    trans_date = trans_date\
        .replace(tzinfo=timezone.utc)\
        .astimezone(tz=library.get_timezone())
    ctx['loan']['transaction_date'] = \
        trans_date.strftime("%d.%m.%Y - %H:%M:%S")
    return ctx
def test_location_create(db, es_clear, loc_public_martigny_data, lib_martigny,
                         loc_online_martigny):
    """Test location creation."""
    loc_public_martigny_data['is_online'] = True
    with pytest.raises(RecordValidationError):
        loc = Location.create(loc_public_martigny_data, delete_pid=True)

    del loc_public_martigny_data['is_online']
    loc = Location.create(loc_public_martigny_data, delete_pid=True)
    assert loc == loc_public_martigny_data
    assert loc.get('pid') == '2'

    loc = Location.get_record_by_pid('2')
    assert loc == loc_public_martigny_data

    fetched_pid = fetcher(loc.id, loc)
    assert fetched_pid.pid_value == '2'
    assert fetched_pid.pid_type == 'loc'
Beispiel #10
0
def test_location_create(db, es, loc_public_martigny_data, lib_martigny,
                         loc_online_martigny):
    """Test location creation."""
    loc_public_martigny_data['is_online'] = True
    with pytest.raises(ValidationError):
        Location.create(loc_public_martigny_data, delete_pid=True)
    db.session.rollback()

    next_pid = Location.provider.identifier.next()
    del loc_public_martigny_data['is_online']
    loc = Location.create(loc_public_martigny_data, delete_pid=True)
    next_pid += 1
    assert loc == loc_public_martigny_data
    assert loc.get('pid') == str(next_pid)

    loc = Location.get_record_by_pid(loc.pid)
    assert loc == loc_public_martigny_data

    fetched_pid = fetcher(loc.id, loc)
    assert fetched_pid.pid_value == str(next_pid)
    assert fetched_pid.pid_type == 'loc'
Beispiel #11
0
def calculate_notification_amount(notification):
    """Return amount due for a notification.

    :param notification: the notification for which to compute the amount. At
                         this time, this is not yet a `Notification`, only a
                         dict of structured data.
    :return the amount due for this notification. 0 if no amount could be
            compute.
    """
    # Find the reminder type to use based on the notification that we would
    # sent. If no reminder type is found, then no amount could be calculated
    # and we can't return '0'
    notif_type = notification.get('notification_type')
    reminder_type_mapping = {
        NotificationType.DUE_SOON: DUE_SOON_REMINDER_TYPE,
        NotificationType.OVERDUE: OVERDUE_REMINDER_TYPE
    }
    reminder_type = reminder_type_mapping.get(notif_type)
    if not notif_type or not reminder_type:
        return 0

    # to find the notification due amount, we firstly need to get the
    # circulation policy linked to the parent loan.
    location_pid = notification.transaction_location_pid
    location = Location.get_record_by_pid(location_pid)
    cipo = CircPolicy.provide_circ_policy(
        location.organisation_pid, location.library_pid,
        notification.patron.patron_type_pid,
        notification.item.holding_circulation_category_pid)

    # now we get the circulation policy, search the correct reminder depending
    # of the reminder_counter from the notification context.
    reminder = cipo.get_reminder(reminder_type=reminder_type,
                                 idx=notification.get('context', {}).get(
                                     'reminder_counter', 0))
    return reminder.get('fee_amount', 0) if reminder else 0
Beispiel #12
0
 def transaction_location(self):
     """Shortcut for transaction location of the notification."""
     return Location.get_record_by_pid(self.transaction_location_pid)
Beispiel #13
0
def get_pickup_location_options():
    """Get all pickup location."""
    for pid in Location.get_pickup_location_pids(current_patron.pid):
        location = Location.get_record_by_pid(pid)
        location_name = location.get('pickup_name', location.get('name'))
        yield tuple([location.pid, location_name])
Beispiel #14
0
    def post_process_serialize_search(self, results, pid_fetcher):
        """Post process the search results.

        :param results: Elasticsearch search result.
        :param pid_fetcher: Persistent identifier fetcher.
        """
        records = results.get('hits', {}).get('hits', {})
        orgs = {}
        libs = {}
        locs = {}
        for record in records:
            metadata = record.get('metadata', {})
            document = search_document_by_pid(
                metadata.get('document').get('pid')
            )
            metadata['ui_title_text'] = title_format_text_head(
                document['title'],
                with_subtitle=True
            )

            item = Item.get_record_by_pid(metadata.get('pid'))
            metadata['availability'] = {
                'available': item.available,
                'status': metadata['status'],
                'display_text': item.availability_text,
                'request': item.number_of_requests()
            }
            if not metadata['available']:
                if metadata['status'] == ItemStatus.ON_LOAN:
                    metadata['availability']['due_date'] =\
                        item.get_item_end_date(format='long', language='en')

            # Item in collection
            collection = item.in_collection()
            if collection:
                metadata['in_collection'] = collection
            # Organisation
            organisation = metadata['organisation']
            if organisation['pid'] not in orgs:
                orgs[organisation['pid']] = Organisation \
                    .get_record_by_pid(organisation['pid'])
            organisation['viewcode'] = orgs[organisation['pid']].get('code')
            # Library
            library = metadata['library']
            if library['pid'] not in libs:
                libs[library['pid']] = Library \
                    .get_record_by_pid(library['pid'])
            library['name'] = libs[library['pid']].get('name')
            # Location
            location = metadata['location']
            if location['pid'] not in locs:
                locs[location['pid']] = Location \
                    .get_record_by_pid(location['pid'])
            location['name'] = locs[location['pid']].get('name')

        # Add library name
        for lib_term in results.get('aggregations', {}).get(
                'library', {}).get('buckets', []):
            lib = Library.get_record_by_pid(lib_term.get('key'))
            lib_term['name'] = lib.get('name')
        # Add location name
        for loc_term in results.get('aggregations', {}).get(
                'location', {}).get('buckets', []):
            loc = Location.get_record_by_pid(loc_term.get('key'))
            loc_term['name'] = loc.get('name')

        # Add item type name
        for item_type_term in results.get('aggregations', {}).get(
                'item_type', {}).get('buckets', []):
            item_type = ItemType.get_record_by_pid(item_type_term.get('key'))
            item_type_term['name'] = item_type.get('name')

        # Add vendor name
        for vendor_term in results.get('aggregations', {}).get(
                'vendor', {}).get('buckets', []):
            vendor = Vendor.get_record_by_pid(vendor_term.get('key'))
            vendor_term['name'] = vendor.get('name')

        # Correct document type buckets
        buckets = results['aggregations']['document_type']['buckets']
        results['aggregations']['document_type']['buckets'] = \
            filter_document_type_buckets(buckets)

        return super().post_process_serialize_search(results, pid_fetcher)

        # Correct document type buckets
        buckets = results['aggregations']['document_type']['buckets']
        results['aggregations']['document_type']['buckets'] = \
            filter_document_type_buckets(buckets)
Beispiel #15
0
 def pickup_location(self):
     """Shortcut for pickup location of the notification."""
     return Location.get_record_by_pid(self.pickup_location_pid)
Beispiel #16
0
 def location(self):
     """Shortcut for item location of the notification."""
     return Location.get_record_by_pid(self.location_pid)
Beispiel #17
0
def test_requesting_item_from_non_circulating_library(
        client, librarian_martigny, lib_martigny, lib_martigny_bourg,
        loc_restricted_martigny_bourg,
        patron_martigny, loc_public_martigny, loc_public_martigny_bourg,
        item_lib_martigny_bourg, circulation_policies,
        librarian_martigny_bourg):
    """Test requests on items of a non-circulating library."""
    login_user_via_session(client, librarian_martigny_bourg.user)
    # Test a checkout of an item at a library with open-hours and no pickup
    # locations defined is possible.
    opening_hours = [
      {
        "day": "monday",
        "is_open": True,
        "times": [
          {
            "start_time": "07:00",
            "end_time": "19:00"
          }
        ]
      }
    ]
    lib_martigny_bourg['opening_hours'] = opening_hours
    lib_martigny_bourg.update(lib_martigny_bourg, dbcommit=True, reindex=True)
    params = dict(
        item_pid=item_lib_martigny_bourg.pid,
        patron_pid=patron_martigny.pid,
        transaction_user_pid=librarian_martigny_bourg.pid,
        transaction_library_pid=lib_martigny_bourg.pid,
    )
    res, data = postdata(
        client,
        'api_item.checkout',
        params
    )
    assert res.status_code == 200
    loan_pid = data.get('action_applied').get('checkout').get('pid')
    loan = Loan.get_record_by_pid(loan_pid)
    transaction_loc = Location.get_record_by_pid(loan.transaction_location_pid)
    assert transaction_loc.library_pid == lib_martigny_bourg.pid
    pickup_lib_pid = Location.get_record_by_pid(
        loan.pickup_location_pid).library_pid
    assert pickup_lib_pid == lib_martigny_bourg.pid
    assert loan.get('state') == LoanState.ITEM_ON_LOAN

    res, data = postdata(
        client,
        'api_item.checkin',
        dict(
            item_pid=item_lib_martigny_bourg.pid,
            transaction_library_pid=lib_martigny_bourg.pid,
            transaction_user_pid=librarian_martigny_bourg.pid,
        )
    )
    assert res.status_code == 200
    item = Item.get_record_by_pid(item_lib_martigny_bourg.pid)
    assert item.status == ItemStatus.ON_SHELF

    # TEST: a librarian from an external library can request and item from a
    # non-circulating library to be picked-up at his own library.
    lib_martigny_bourg.pop('opening_hours', None)
    lib_martigny_bourg.update(lib_martigny_bourg, dbcommit=True, reindex=True)

    login_user_via_session(client, librarian_martigny.user)
    res, data = postdata(
        client,
        'api_item.librarian_request',
        dict(
            item_pid=item_lib_martigny_bourg.pid,
            patron_pid=patron_martigny.pid,
            pickup_location_pid=loc_public_martigny.pid,
            transaction_library_pid=lib_martigny_bourg.pid,
            transaction_user_pid=librarian_martigny.pid
        )
    )
    assert res.status_code == 200
    loan = Loan(data.get('metadata').get('pending_loans')[0])
    transaction_loc = Location.get_record_by_pid(
      loan.transaction_location_pid)
    assert transaction_loc.library_pid == lib_martigny_bourg.pid
    pickup_lib_pid = Location.get_record_by_pid(
        loan.pickup_location_pid).library_pid
    assert pickup_lib_pid == lib_martigny.pid

    # non-circulating library send items to requesting library
    login_user_via_session(client, librarian_martigny_bourg.user)
    res, data = postdata(
        client,
        'api_item.validate_request',
        dict(
            pid=loan.get('pid'),
            transaction_library_pid=lib_martigny_bourg.pid,
            transaction_user_pid=librarian_martigny_bourg.pid
        )
    )
    assert res.status_code == 200
    loan = Loan(data.get('metadata').get('pending_loans')[0])
    transaction_loc = Location.get_record_by_pid(
      loan.transaction_location_pid)
    assert transaction_loc.library_pid == lib_martigny_bourg.pid
    pickup_lib_pid = Location.get_record_by_pid(
        loan.pickup_location_pid).library_pid
    assert pickup_lib_pid == lib_martigny.pid

    assert loan.get('state') == LoanState.ITEM_IN_TRANSIT_FOR_PICKUP

    # requesting library receives an item from non-circulating library.
    login_user_via_session(client, librarian_martigny.user)
    res, data = postdata(
        client,
        'api_item.receive',
        dict(
            pid=loan.get('pid'),
            transaction_library_pid=lib_martigny.pid,
            transaction_user_pid=librarian_martigny.pid
        )
    )
    assert res.status_code == 200
    loan = Loan(data.get('metadata').get('pending_loans')[0])

    transaction_loc = Location.get_record_by_pid(
      loan.transaction_location_pid)
    assert transaction_loc.library_pid == lib_martigny.pid
    pickup_lib_pid = \
        Location.get_record_by_pid(loan.pickup_location_pid).library_pid
    assert pickup_lib_pid == lib_martigny.pid

    assert loan.get('state') == LoanState.ITEM_AT_DESK

    # checkout item to requested patron
    login_user_via_session(client, librarian_martigny.user)
    date_format = '%Y/%m/%dT%H:%M:%S.000Z'
    today = datetime.utcnow()
    eod = today.replace(hour=23, minute=59, second=0, microsecond=0,
                        tzinfo=lib_martigny.get_timezone())
    params = dict(
        item_pid=item_lib_martigny_bourg.pid,
        patron_pid=patron_martigny.pid,
        transaction_user_pid=librarian_martigny.pid,
        transaction_library_pid=lib_martigny.pid,
        end_date=eod.strftime(date_format)
    )
    res, data = postdata(
        client,
        'api_item.checkout',
        params
    )
    assert res.status_code == 200
    loan_pid = data.get('action_applied').get('checkout').get('pid')
    loan = Loan.get_record_by_pid(loan_pid)

    transaction_loc = Location.get_record_by_pid(
      loan.transaction_location_pid)
    assert transaction_loc.library_pid == lib_martigny.pid
    pickup_lib_pid = Location.get_record_by_pid(
        loan.pickup_location_pid).library_pid
    assert pickup_lib_pid == lib_martigny.pid

    assert loan.get('state') == LoanState.ITEM_ON_LOAN