def confirm_reservation(reservation): ''' Check if reservation can be immediately rented and send appropriate email. ''' preceding_reservations = Reservation.objects.filter(Q_reservation_active|Q(rental__isnull=False, rental__end_date__isnull=True), book_copy=reservation.book_copy, id__lt=reservation.id) if not preceding_reservations and reservation.start_date <= utils.today(): mail.reservation_active(reservation) reservation.active_since = utils.today() reservation.save() else: mail.made_reservation(reservation)
def max_prolongation_period(rental): ''' Desc: Returnes number of days -- for how long book can be prolonged. Args: rental -- instance of Rental. ''' assert isinstance(rental, Rental) reservation = rental.reservation config = Config() days_left_to_enable_prolongation = config.get_int('min_days_left_to_enable_prolongation') days_left_to_rend = (reservation.end_date - utils.today()).days enabled_prolongation = days_left_to_rend <= days_left_to_enable_prolongation if not enabled_prolongation: return 0 max_period = config.get_int('max_prolongation_days') kopy = reservation.book_copy for rsv in kopy.reservation_set.exclude(for_whom=reservation.for_whom): if rsv.start_date <= reservation.end_date < rsv.end_date: # someone else reserved -> prolongation impossible return 0 if reservation.end_date == rsv.start_date: return 0 if rsv.end_date <= reservation.end_date: # other rsv finished -> no collision, continue processing continue if reservation.end_date < rsv.start_date: # other rsv starts in future -> try to shrink max_period max_period = min(max_period, (rsv.start_date - reservation.end_date).days - 1) return max_period
def mark_available(book_copy): ''' Desc: If there is an active reservation awaiting for this copy, let it know. ''' reservations = Reservation.objects.filter(Q_reservation_active, book_copy=book_copy).filter(start_date__lte=utils.today()) if reservations.count() > 0: # TODO: this probably can be done more effectively first_reservation = reservations[0] first_reservation.active_since = utils.today() first_reservation.save() mail.reservation_active(first_reservation)
def get_reservations_for_copies(copies_ids, only=[], related=[]): ''' Collects and returns list of Reservation objects for given book copies. Args: copies_ids -- ids of copies for which you want to get reservations. List of ints. only -- model fields one need to use. List of strings. As default 'book_copy__id' is used. ''' only_fields = only + ['book_copy__id'] reservations = Reservation.objects.only(*only_fields) \ .filter(book_copy__id__in=copies_ids) \ .filter(start_date__lte=utils.today()) \ .filter(Q_reservation_active) return reservations
def reservation_status(reservation): ''' Desc: returns reservation status: integer means for how many days book can be rented string is explaining why rental is not possible ''' all = Reservation.objects.filter(book_copy=reservation.book_copy)\ .filter(rental=None)\ .filter(when_cancelled=None)\ .filter(end_date__gte=utils.today()) if reservation not in all: return 'Incorrect reservation' # book rented: if Rental.objects.filter(reservation__book_copy=reservation.book_copy).filter(end_date=None).count() > 0: return 'Copy rented.' # book unavailable: if reservation.book_copy.state.is_available == False: return 'Copy not available (' + reservation.book_copy.state.name + ').' # too early if reservation.start_date > utils.today(): return 'Reservation is not yet active' # reservation cannot be purchased for there are some reservation before this one starts if reservation.start_date > utils.today() and all.filter(start_date__lt=reservation.start_date).count() > 0: return 'There are reservations before this one starts.' # some older reservation is active if all.filter(id__lt=reservation.id).filter(start_date__lte=utils.today()).filter(Q_reservation_active).count() > 0: # WARNING/NOTE: this might need modification if additional conditions are added to definition of active reservation return 'Reservation in queue' config = Config() max_allowed = config.get_int('rental_duration') try: max_possible = (min([r.start_date for r in all.filter(id__lt=reservation.id).filter(start_date__gt=utils.today())]) - utils.today()).days except ValueError: max_possible = max_allowed return min(max_allowed, max_possible)
def rent(reservation, librarian): ''' Desc: Librarian rents book indicated by reservation. Return value says when it is supposed to be returned ''' if not librarian.has_perm('baseapp.add_rental'): raise PermissionDenied if not librarian in reservation.book_copy.location.get_all_maintainers(): raise PermissionDenied('One can only rent from location one maintains') if not is_reservation_rentable(reservation): raise PermissionDenied('Reservation not rentable.') max_end_date = utils.today() + timedelta(reservation_status(reservation)-1) # return one day before it's reserved if max_end_date < reservation.end_date: reservation.end_date = max_end_date reservation.save() rental = Rental(reservation=reservation, who_handed_out=librarian, start_date=utils.now()) rental.save() mail.made_rental(rental) return reservation.end_date
def show_reservations(request, shipment_requested=False, only_rentable=True, all_locations=False): if request.method == 'POST': post = request.POST reservation = get_object_or_404(Reservation, id=post['reservation_id']) if 'rent' in post: user = reservation.for_whom until = rent(reservation, request.user) messages.info(request, 'Rented %s (%s) to %s until %s.' % \ (reservation.book_copy.book.title, reservation.book_copy.shelf_mark, user_full_name(reservation.for_whom), until.isoformat())) if 'cancel' in post: cancel_reservation(reservation, request.user) messages.info(request, 'Cancelled.') context = {} locations = request.user.location_set.all() # locations maintained by user reservations = Reservation.objects.filter(Q_reservation_active) if shipment_requested: reservations = reservations.filter(shipment_requested=True).select_related() if not all_locations: reservations = reservations.filter(book_copy__location__in=locations) if only_rentable: reservations = [r for r in reservations.filter(start_date__lte=utils.today()) if is_reservation_rentable(r)] reservation_list = [ { 'id' : r.id, 'user' : user_full_name(r.for_whom.id), 'start_date' : r.start_date, 'end_date' : r.end_date, 'location' : r.book_copy.location, 'shelf_mark' : r.book_copy.shelf_mark, 'title' : r.book_copy.book.title, 'authors' : [a.name for a in r.book_copy.book.author.all()], 'rental_possible' : is_reservation_rentable(r), } for r in reservations ] context.update({ 'rows' : reservation_list }) if shipment_requested: context.update({ 'header' : 'Shipment requests', 'shipments' : True, }) elif not only_rentable: context.update({ 'header' : 'All active reservations', 'show_all' : True, }) else: context.update({ 'header' : 'Current reservations'}) return render_response(request, 'current_reservations.html', context)
sys.path.insert(0, imaging_path) sys.path.insert(0, django_path) sys.path.insert(0, project_path) sys.path.insert(0, app_path) from baseapp.models import * from django.contrib.auth.models import User from baseapp.utils import today, after_days from baseapp.config import Config from datetime import datetime import baseapp.views_aux as aux import baseapp.emails as mail for r in Rental.objects.all(): if not r.end_date: if r.reservation.end_date < today( ): # reservation expired: notify reader every day mail.overdued(r) if r.reservation.end_date == after_days( Config().get_int('due_remind_time')): mail.returnal_date_coming( r) # reservation will expire soon: notify reader for r in Reservation.objects.filter(aux.Q_reservation_active): if not r.active_since and aux.is_reservation_rentable(r): # reservation has just become active - notify user r.active_since = today() r.save() mail.reservation_active(r) if r.active_since and r.active_since <= after_days( -1 * Config().get_int('reservation_rush')):
sys.path.insert(0, project_path) sys.path.insert(0, app_path) from baseapp.models import * from django.contrib.auth.models import User from baseapp.utils import today, after_days from baseapp.config import Config from datetime import datetime import baseapp.views_aux as aux import baseapp.emails as mail for r in Rental.objects.all(): if not r.end_date : if r.reservation.end_date < today(): # reservation expired: notify reader every day mail.overdued(r) if r.reservation.end_date == after_days(Config().get_int('due_remind_time')): mail.returnal_date_coming(r) # reservation will expire soon: notify reader for r in Reservation.objects.filter(aux.Q_reservation_active): if not r.active_since and aux.is_reservation_rentable(r): # reservation has just become active - notify user r.active_since = today() r.save() mail.reservation_active(r) if r.active_since and r.active_since <= after_days(-1*Config().get_int('reservation_rush')): # reservation has been active for 'reservation_rush' days, so it expires r.when_cancelled = datetime.now() r.save()
def get_time_bar_code_for_copy(config, book_copy, from_date, to_date): """ Returns time bar's html for given copy and date range. Args: book_copy -- instance of BookCopy config -- instance of Config from_date, to_date -- instances of datetime.date . Range from which records will be displayed. Returns: If from_date > to_date, then empty string is returned. """ assert isinstance(book_copy, BookCopy) assert isinstance(from_date, date) assert isinstance(to_date, date) date_range = (from_date, to_date) if from_date > to_date: return '' Q_start_inside_range = Q(start_date__gte=from_date) & Q( start_date__lte=to_date) # > |---<--------| Q_end_inside_range = Q(end_date__gte=from_date) & Q( end_date__lte=to_date) # |--------->--| < Q_covers_whole_range = Q(start_date__lte=from_date) & Q( end_date__gte=to_date) # |---->----<------| reservations = Reservation.objects.filter(book_copy=book_copy) \ .filter(when_cancelled=None) \ .filter(Q_start_inside_range | Q_end_inside_range | Q_covers_whole_range) \ .order_by('start_date') # .filter(Q(rental=None)|Q(rental__end_date=None)) \ segments = [] for r in reservations: seg = Segment(r.start_date, r.end_date) seg.who_reserved = r.for_whom rentals = list(r.rental_set.all()) seg.rental = rentals[0] if rentals else None seg.overdued = False if seg.rental: end = seg.rental.end_date.date( ) if seg.rental.end_date else seg.rental.reservation.end_date if r.end_date > today( ) else today() seg.overdued = r.end_date < end seg.overdue_time = end - r.end_date seg.end = end segments.append(seg) # NOTE: if one would like to add some extra informations to segments, it can be done above. # This is why segment is an object and not a tuple. # ok, we gathered segments, now let's create time bar on top of them tb = TimeBar(config, one=timedelta(1)) result_segments = tb.divide_colliding_segments( segments, shuffle=False) # detect collisions grouped_segs = tb.split_segments_by_depth( result_segments) # and collect them in groups # set number of colliding groups. Cut surplus. max_colliding_groups = 10 grouped_segs = grouped_segs[:max_colliding_groups] # "correct" each group, which means add "green segments" in between, cut/enlarge start and end dates properly result_segments = [] for group in grouped_segs: # each of operations below may drop some segments. It may happen that all segments will be dropped. filled_group = tb.add_segment_between_each_two(group) if filled_group: filled_group = tb.start_with_value(filled_group, date_range[0]) if filled_group: filled_group = tb.end_with_value(filled_group, date_range[1]) if filled_group: result_segments.append(filled_group) # if there is no reservations, then add available one-Segment group if not result_segments: group = [Segment(from_date, to_date, is_available=True)] result_segments.append(group) # create group which is a flattened version of time bar if len(result_segments) > 1: flattened_group = tb.get_flattened_group(result_segments) filled_group = tb.add_segment_between_each_two(flattened_group) if filled_group: filled_group = tb.start_with_value(filled_group, date_range[0]) if filled_group: filled_group = tb.end_with_value(filled_group, date_range[1]) flattened_group = filled_group for segment in flattened_group: segment.from_flattened_group = True result_segments.insert(0, flattened_group) # generate html html = tb.get_html_for_segments(result_segments) # add scale (or two scales if there is a lot of colliding segments) scale = tb.get_html_of_scale(result_segments) if result_segments else '' if len(result_segments) > 100000: html = scale + html + scale elif len(result_segments) > 0: html = scale + html return html
def get_report_data(report_type, from_date, to_date, order_by=[]): """ Desc: Returns data for creating selected report. Args: report_type - basestring. type of the report from_date, to_date - basestring. describe the time period for the report. order_by - iterable. Names of parameters by which result should be ordered by. Currently only one item is supported, bo using order_by will more then one element will take no effect. Return: dict { 'report' : report data 'template' : template to be rendered (only filename. Proper dir will be attached elsewhere) 'error' : error message if sth goes wrong 'ordering' : list of names used to sort report data } """ assert from_date assert to_date order_by = list(order_by) order_by = order_by[:1] if order_by else [] # only one name can be used to order data # NOTE: `ob` stands from order by ob_rename_dict = {'title' : order_asc_by_key('title'), '-title' : order_desc_by_key('title'), 'location' : order_asc_by_key('location_str'), '-location' : order_desc_by_key('location_str'), 'shelf_mark' : order_asc_by_key('shelf_mark'), '-shelf_mark' : order_desc_by_key('shelf_mark'), 'status' : order_asc_by_key('status_str'), '-status' : order_desc_by_key('status_str'), 'for_whom' : order_asc_by_key('for_whom'), '-for_whom' : order_desc_by_key('for_whom'), 'when' : order_asc_by_key('when'), '-when' : order_desc_by_key('when'), 'librarian' : order_asc_by_key('by_whom'), '-librarian' : order_desc_by_key('by_whom'), 'name' : order_asc_by_key('name'), '-name' : order_desc_by_key('name'), 'num_cancels' : order_asc_by_key('num_of_cancels'), '-num_cancels' : order_desc_by_key('num_of_cancels'), 'num_overdues' : order_asc_by_key('num_of_overdues'), '-num_overdues': order_desc_by_key('num_of_overdues'), 'len_overdues' : order_asc_by_key('duration_of_overdues'), '-len_overdues': order_desc_by_key('duration_of_overdues'), 'num_rentals' : order_asc_by_key('num_of_rentals'), '-num_rentals' : order_desc_by_key('num_of_rentals'), 'num_rsvs' : order_asc_by_key('num_of_reservations'), '-num_rsvs' : order_desc_by_key('num_of_reservations'), } ob_in_status = ['title', '-title', 'location', '-location', 'shelf_mark', '-shelf_mark', 'status', '-status', 'for_whom', '-for_whom', 'when', '-when', 'librarian', '-librarian'] ob_in_often_rented = ['title', '-title', 'num_rentals', '-num_rentals'] ob_in_often_reserved = ['title', '-title', 'num_rsvs', '-num_rsvs'] ob_in_black_list = ['name', '-name', 'num_cancels', '-num_cancels', 'num_overdues', '-num_overdues', 'len_overdues', '-len_overdues'] ob_in_lost_books = copy(ob_in_status) if report_type == u'status': book_infos = [] copies = BookCopy.objects.select_related('id', 'state', 'book__title', 'location', 'location__building', 'shelf_mark') last_rentals = {} from datetime import timedelta, datetime status_day = utils.str_to_date(from_date, utils.today()) status_day_start = datetime(status_day.year, status_day.month, status_day.day, 0, 0, 0) status_day_end = datetime(status_day.year, status_day.month, status_day.day, 23, 59, 59) # Q_start_date_filter = Q(start_date__contains='') # Q_all # if from_date: # Q_start_date_filter = Q(start_date__gte=from_date) # Q_end_date_filter = Q(end_date__isnull=True) # if to_date: # Q_end_date_filter = Q(end_date__lte=to_date) | Q(end_date__isnull=True) # rentals = Rental.objects.select_related('reservation', 'reservation__book_copy__id') \ # .filter(Q_start_date_filter) \ # .filter(Q_end_date_filter) Q_start_date_filter = Q(start_date__lte=status_day_end) Q_end_date_filter = (Q(end_date__isnull=True) | Q(end_date__gte=status_day_start)) rentals = Rental.objects.select_related('reservation', 'reservation__book_copy__id') \ .filter(Q_start_date_filter) \ .filter(Q_end_date_filter) for rental in rentals: book_copy_id = rental.reservation.book_copy.id start_date = rental.start_date if book_copy_id not in last_rentals: last_rentals.update({book_copy_id: {'when' : start_date, 'for_whom': rental.reservation.for_whom, 'by_whom' : rental.who_handed_out, }}) elif last_rentals[book_copy_id]['when'] < start_date: last_rentals.update({book_copy_id: {'when' : start_date, 'for_whom': rental.reservation.for_whom, 'by_whom' : rental.who_handed_out, }}) book_infos = [] statuses = book_copies_status(copies) for kopy in copies: copy_status = statuses[kopy.id] not_rented_yet = copy_status['copy'].id not in last_rentals if not_rented_yet: for_whom = when = by_whom = '- never -' else: last_rental = last_rentals[copy_status['copy'].id] (for_whom, when, by_whom) = (last_rental['for_whom'], last_rental['when'].date(), last_rental['by_whom']) book_infos.append({ 'title' : copy_status['copy'].book.title, 'shelf_mark' : copy_status['copy'].shelf_mark, 'copy' : kopy, 'location' : copy_status['copy'].location, 'location_str' : unicode(copy_status['copy'].location).lower(), # field for ordering purpose 'status' : copy_status['status'], 'status_str' : unicode(copy_status['status']).lower(), 'for_whom' : for_whom, 'when' : unicode(when), 'by_whom' : by_whom, 'not_rented_yet': not_rented_yet}) sort_by = list(set(order_by) & set(ob_in_status)) if sort_by: book_infos.sort(cmp=ob_rename_dict[sort_by[0]]) return {'report' : book_infos, 'template' : 'library_status.html', 'error' : False, 'ordering' : sort_by, } ### elif report_type == u'most_often_rented': book_infos = [] books = Book.objects.all() rentals = Rental.objects.all() date_empty = False nums_of_rentals = {} if from_date == u'' or to_date == u'': date_empty = True else: try: [y, m, d] = map(int,from_date.split('-')) start_date = date(y, m, d) [y, m, d] = map(int,to_date.split('-')) end_date = date(y, m, d) except: return {'error': True, 'report': [], 'template': 'reports.html'} for book in books: if book.id not in nums_of_rentals: nums_of_rentals.update({book.id: 0}) for rental in rentals: book_id = rental.reservation.book_copy.book.id if date_empty: nums_of_rentals[book_id] += 1 else: if rental.start_date.date() >= start_date and rental.start_date.date() <= end_date: nums_of_rentals[book_id] += 1 for book in books: book_infos.append({'title': book.title, 'num_of_rentals': nums_of_rentals[book.id], 'id' : book.id, }) sort_by = list(set(order_by) & set(ob_in_often_rented)) if sort_by: book_infos.sort(cmp=ob_rename_dict[sort_by[0]]) return {'report' : book_infos, 'template' : 'most_often_rented.html', 'error' : False, 'ordering' : sort_by, } ### elif report_type == u'most_often_reserved': book_infos = [] books = Book.objects.all() reservations = Reservation.objects.all() nums_of_reservations = {} date_empty = False if from_date == u'' or to_date == u'': date_empty = True else: try: [y, m, d] = map(int,from_date.split('-')) start_date = date(y, m, d) [y, m, d] = map(int,to_date.split('-')) end_date = date(y, m, d) except: return {'error': True, 'report': [], 'template': 'reports.html'} for book in books: if book.id not in nums_of_reservations: nums_of_reservations.update({book.id: 0}) for reservation in reservations: book_id = reservation.book_copy.book.id if date_empty: nums_of_reservations[book_id] += 1 else: if reservation.start_date >= start_date and reservation.start_date <= end_date: nums_of_reservations[book_id] += 1 for book in books: book_infos.append({'title': book.title, 'num_of_reservations': nums_of_reservations[book.id], 'id' : book.id, }) sort_by = list(set(order_by) & set(ob_in_often_reserved)) if sort_by: book_infos.sort(cmp=ob_rename_dict[sort_by[0]]) return {'report' : book_infos, 'template' : 'most_often_reserved.html', 'error' : False, 'ordering' : sort_by, } ### elif report_type == u'black_list': nums_of_cancels = {} nums_of_overdues = {} duration_of_overdues = {} user_infos = [] users = User.objects.all() reservations = Reservation.objects.filter(when_cancelled__isnull=False) rentals = [ r for r in Rental.objects.all() if r.end_date and r.reservation.end_date < r.end_date.date() ] date_empty = False if from_date == u'' or to_date == u'': date_empty = True else: try: [y, m, d] = map(int,from_date.split('-')) start_date = date(y, m, d) [y, m, d] = map(int,to_date.split('-')) end_date = date(y, m, d) except: return {'error': True, 'report': [], 'template': 'reports.html'} for user in users: nums_of_cancels.update({user.id: 0}) nums_of_overdues.update({user.id: 0}) duration_of_overdues.update({user.id: 0}) for rental in rentals: user_id = rental.reservation.for_whom.id if date_empty: nums_of_overdues[user_id] += 1 duration_of_overdues[user_id] += (rental.end_date.date() - rental.reservation.end_date).days else: if rental.reservation.start_date >= start_date and rental.reservation.start_date <= end_date: nums_of_overdues[user_id] += 1 duration_of_overdues[user_id] += (rental.end_date.date() - rental.reservation.end_date).days for reservation in reservations: user_id = reservation.for_whom.id if date_empty: nums_of_cancels[user_id] += 1 else: if reservation.start_date >= start_date and reservation.start_date <= end_date: nums_of_cancels[user_id] += 1 for user in users: user_infos.append({'name': user.last_name + u', ' + user.first_name, 'id' : user.id, 'num_of_cancels': nums_of_cancels[user.id], 'num_of_overdues' : nums_of_overdues[user.id], 'duration_of_overdues' : duration_of_overdues[user.id], }) sort_by = list(set(order_by) & set(ob_in_black_list)) if sort_by: user_infos.sort(cmp=ob_rename_dict[sort_by[0]]) return {'report' : user_infos, 'template' : 'black_list.html', 'error' : False, 'ordering' : sort_by, } ### elif report_type == u'lost_books': reported_data = get_report_data(u'status', from_date, to_date) book_infos = reported_data['report'] lost_books = filter(lambda b: not b['status'].is_available(), book_infos) sort_by = list(set(order_by) & set(ob_in_lost_books)) if sort_by: lost_books.sort(cmp=ob_rename_dict[sort_by[0]]) return {'report' : lost_books, 'template' : 'unavailable_status.html', 'error' : False, 'ordering' : sort_by, } else: return {'report': [], 'template': 'reports.html', 'error': True}
def get_time_bar_code_for_copy(config, book_copy, from_date, to_date): """ Returns time bar's html for given copy and date range. Args: book_copy -- instance of BookCopy config -- instance of Config from_date, to_date -- instances of datetime.date . Range from which records will be displayed. Returns: If from_date > to_date, then empty string is returned. """ assert isinstance(book_copy, BookCopy) assert isinstance(from_date, date) assert isinstance(to_date, date) date_range = (from_date, to_date) if from_date > to_date: return '' Q_start_inside_range = Q(start_date__gte=from_date) & Q(start_date__lte=to_date) # > |---<--------| Q_end_inside_range = Q(end_date__gte=from_date) & Q(end_date__lte=to_date) # |--------->--| < Q_covers_whole_range = Q(start_date__lte=from_date) & Q(end_date__gte=to_date) # |---->----<------| reservations = Reservation.objects.filter(book_copy=book_copy) \ .filter(when_cancelled=None) \ .filter(Q_start_inside_range | Q_end_inside_range | Q_covers_whole_range) \ .order_by('start_date') # .filter(Q(rental=None)|Q(rental__end_date=None)) \ segments = [] for r in reservations: seg = Segment(r.start_date, r.end_date) seg.who_reserved = r.for_whom rentals = list(r.rental_set.all()) seg.rental = rentals[0] if rentals else None seg.overdued = False if seg.rental: end = seg.rental.end_date.date() if seg.rental.end_date else seg.rental.reservation.end_date if r.end_date > today() else today() seg.overdued = r.end_date < end seg.overdue_time = end - r.end_date seg.end = end segments.append(seg) # NOTE: if one would like to add some extra informations to segments, it can be done above. # This is why segment is an object and not a tuple. # ok, we gathered segments, now let's create time bar on top of them tb = TimeBar(config, one=timedelta(1)) result_segments = tb.divide_colliding_segments(segments, shuffle=False) # detect collisions grouped_segs = tb.split_segments_by_depth(result_segments) # and collect them in groups # set number of colliding groups. Cut surplus. max_colliding_groups = 10 grouped_segs = grouped_segs[:max_colliding_groups] # "correct" each group, which means add "green segments" in between, cut/enlarge start and end dates properly result_segments = [] for group in grouped_segs: # each of operations below may drop some segments. It may happen that all segments will be dropped. filled_group = tb.add_segment_between_each_two(group) if filled_group: filled_group = tb.start_with_value(filled_group, date_range[0]) if filled_group: filled_group = tb.end_with_value(filled_group, date_range[1]) if filled_group: result_segments.append(filled_group) # if there is no reservations, then add available one-Segment group if not result_segments: group = [Segment(from_date, to_date, is_available=True)] result_segments.append(group) # create group which is a flattened version of time bar if len(result_segments) > 1: flattened_group = tb.get_flattened_group(result_segments) filled_group = tb.add_segment_between_each_two(flattened_group) if filled_group: filled_group = tb.start_with_value(filled_group, date_range[0]) if filled_group: filled_group = tb.end_with_value(filled_group, date_range[1]) flattened_group = filled_group for segment in flattened_group: segment.from_flattened_group = True result_segments.insert(0, flattened_group) # generate html html = tb.get_html_for_segments(result_segments) # add scale (or two scales if there is a lot of colliding segments) scale = tb.get_html_of_scale(result_segments) if result_segments else '' if len(result_segments) > 100000: html = scale + html + scale elif len(result_segments) > 0: html = scale + html return html
def show_user_rentals(request, user_id=False): ''' Desc: Shows user's current rentals. Allows returning books. ''' if not user_id: user = request.user else: try: user = User.objects.get(id=user_id) except User.DoesNotExist: return render_not_found(request, item_name='User') post = request.POST # a shorthand context = { 'first_name' : user.first_name, 'last_name' : user.last_name, 'email' : user.email, 'reader' : user, } # Return button clicked for a rental: # but we care about it only if he is supposed to if request.user.has_perm('baseapp.change_rental') and\ request.method == 'POST' and\ 'returned' in post: try: until_when = return_rental(librarian=request.user, rental_id=post['returned']) context['message'] = 'Successfully returned' messages.info(request, 'Successfully returned') except Rental.DoesNotExist: return render_not_found(request, item_name='Rental') except PermissionDenied: # this might happen if user doesn't have 'change_rental' permission return render_forbidden # logger for prolongation prol_logger = utils.get_logger('prolong') # sending prolongation request to librarians if request.method == 'POST' and 'prolong_request' in post: rental_id = int(post['prolong_rental_id']) prol_logger.info("request ;; user %d %s ;; rental_id %d", user.id, user.get_full_name(), rental_id) send_prolongation_request(request, rental_id, user) # prolonging rental if request.method == 'POST' and 'prolong' in post and request.user.has_perm('baseapp.change_rental'): rental_id = int(post['prolong_rental_id']) requester = user prolongator = request.user prol_logger.info("prolonging ;; prolongator %d %s ;; requester %d %s ;; rental_id %d", prolongator.id, prolongator.get_full_name(), requester.id, requester.get_full_name(), rental_id) prolong_rental(request, rental_id, requester, prolongator) # find user's rentals user_rentals = Rental.objects.filter(reservation__for_whom=user.id).filter(end_date__isnull=True) # and put them in a dict: days_left_to_enable_prolongation = Config().get_int('min_days_left_to_enable_prolongation') rent_list = [] for r in user_rentals: max_period = max_prolongation_period(r) days_left_to_rend = (r.reservation.end_date - utils.today()).days enabled_prolongation = days_left_to_rend <= days_left_to_enable_prolongation can_request_prolongation = enabled_prolongation and (request.user==r.reservation.for_whom) # owner of reservation can_prolong = enabled_prolongation \ and max_period > 0 \ and request.user.has_perm('baseapp.change_rental') \ and (request.user in r.reservation.book_copy.location.get_all_maintainers()) # msg('enabled_prolongation: %d' % enabled_prolongation) # msg('can_request_prolongation: %d' % can_request_prolongation) # msg('can_prolong: %d' % can_prolong) # msg('days_left_to_enable_prolongation: %d' % days_left_to_enable_prolongation) # msg('max_period: %d' % max_period) kopy = r.reservation.book_copy book = kopy.book rent_list.append({ 'id' : r.id, 'shelf_mark' : kopy.shelf_mark, 'title' : book.title, 'kopy' : kopy, 'book' : book, 'authors' : list(sorted([a.name for a in book.author.all()])), 'from_date' : r.start_date.date(), 'to_date' : r.reservation.end_date, 'returnable' : request.user in kopy.location.get_all_maintainers(), 'rental' : r, 'can_request_prolongation' : can_request_prolongation, 'can_prolong' : can_prolong, 'prolongation_length' : max_period, 'cannot_request_nor_prolong' : not (can_request_prolongation or can_prolong), }) # put rentals into context context['rows'] = rent_list # if user can change rentals then he can return books template = 'user_rentals_return_allowed.html' if request.user.has_perm('baseapp.change_rental') else 'user_rentals.html' return render_response(request, template, context)
def prolong_rental(request, rental_id, requester, prolongator): ''' Desc: Prolongs rental if possible. Args: request rental_id requester -- User. Owner of reservation and rental (one who is in possesion of book) prolongator ''' prol_logger = utils.get_logger('prolong') prol_logger.info("prolong_rental()....") assert isinstance(rental_id, int) assert isinstance(requester, User) assert isinstance(prolongator, User) r = None try: r = Rental.objects.get(pk=rental_id, reservation__for_whom=requester) except Rental.DoesNotExist: messages.error(request, "Rental wasn't found, unable to prolong.") return prol_logger.info("prolong_rental() preconditions valid, rental present.") days_left_to_enable_prolongation = Config().get_int('min_days_left_to_enable_prolongation') days_left_to_rend = (r.reservation.end_date - utils.today()).days enabled_prolongation = days_left_to_rend <= days_left_to_enable_prolongation max_period = max_prolongation_period(r) can_prolong = enabled_prolongation and max_period > 0 and prolongator.has_perm('baseapp.change_rental') prol_logger.info("prolong_rental() " + ";; rental_id %d " + ";; days_left_to_enable_prolongation %d " + ";; days_left_to_rend %d " + ";; enabled_prolongation %d " + ";; max_period %d " + ";; can_prolong %d " % r.id, days_left_to_enable_prolongation, days_left_to_rend, enabled_prolongation, max_period, can_prolong ) if prolongator not in r.reservation.book_copy.location.get_all_maintainers(): prol_logger.error("prolong_rental() prolongator not a location maintainer.") messages.error(request, "You cannot prolong this book - it's not from location you maintain.") return if not enabled_prolongation: prol_logger.error("prolong_rental() prolongator not enabled.") messages.error(request, "It is TOO EARLY to prolong this rental: please wait for %d days." % (days_left_to_rend - days_left_to_enable_prolongation)) return if max_period <= 0: prol_logger.error("prolong_rental() max_period less then zero.") messages.error(request, "Rental cannot be prolonged - probably someone reserved this book.") return if can_prolong: import datetime as dt old_end_date = r.reservation.end_date new_end_date = r.reservation.end_date + dt.timedelta(max_period) r.reservation.end_date = new_end_date r.reservation.save() prol_logger.info("prolong_rental() prolonged ;; rental_id %d " + ";; old_end_date %s " + ";; new_end_date %s " % r.id, str(old_end_date), str(new_end_date) ) mail.prolongated(r, prolongator, extra={'old_end_date': old_end_date, 'new_end_date': new_end_date, }) messages.info(request, "Prolonged for %d days." % max_period) else: prol_logger.error("prolong_rental() you cannot prolong the rental.") messages.error(request, "You cannot prolong the rental.")
def book_copies_status(copies): ''' Desc: Returns statuses of given copies. Args: copies -- list of BookCopy instances Returns: dict like { copy_id : {'copy' : given_copy_instance, 'status' : BookCopyStatus instance }, ... } ''' copies_ids = [ c.id for c in copies ] result = {} for kopy in copies: result[kopy.id] = {} result[kopy.id]['copy'] = kopy result[kopy.id]['status'] = None if not kopy.state.is_available: result[kopy.id]['status'] = BookCopyStatus(available=False, explanation=u'Unavailable: ' + kopy.state.name) # find rented copies rentals = get_active_rentals_for_copies(copies_ids) for rental in rentals: copy_id = rental.reservation.book_copy.id if not result[copy_id]['status']: user = rental.reservation.for_whom explanation = u'Rented until {0} by {1}'.format(rental.reservation.end_date.isoformat(), u'<a href="mailto:{0}">{0}</a>'.format(user.email)) result[copy_id]['status'] = BookCopyStatus(available=False, explanation=explanation) # find reserved copies, that can already be rented reservations = get_reservations_for_copies(copies_ids) for reservation in reservations: copy_id = reservation.book_copy.id if not result[copy_id]['status']: result[copy_id]['status'] = BookCopyStatus(available=False, explanation=u'Reserved') # finds first (in sense of timeline) reservation for each copy. rsvs = Reservation.objects.filter(book_copy__id__in=copies_ids)\ .filter(Q_reservation_active)\ .values('book_copy__id')\ .annotate(min_start_date=Min('start_date')) # update statuses for copies unstatused in code above -- copies are rentable, but we don't know for how long, yet. # rsvs contains information about nearest (but in future) reservation. config = Config() max_allowed = config.get_int('rental_duration') for rsv in rsvs: copy = rsv['book_copy__id'] if result[copy]['status']: continue try: min_start_date = rsv['min_start_date'] max_possible = (min_start_date - utils.today()).days - 1 if max_possible < 0: raise EntelibError('book_copy_status error: copy reserved') # this should not happen since all active reservations should already have status except ValueError: max_possible = max_allowed result[copy]['status'] = BookCopyStatus(available=True, nr_of_days=min(max_allowed, max_possible)) for key in result.keys(): if not result[key]['status']: result[key]['status'] = BookCopyStatus(available=True, nr_of_days=max_allowed) return result
def is_reservation_rentable(reservation): ''' Desc: Returns True if a reserved book can be rented; False otherwise ''' status = reservation_status(reservation) if isinstance(status, int) and status >= 0: return True else: return False # Q object filtering Reservation objects to be only active (which means not rented or cancelled or expired) Q_reservation_active = Q(when_cancelled=None) & Q(rental=None) & Q(end_date__gte=utils.today()) def reservation_status(reservation): ''' Desc: returns reservation status: integer means for how many days book can be rented string is explaining why rental is not possible ''' all = Reservation.objects.filter(book_copy=reservation.book_copy)\ .filter(rental=None)\ .filter(when_cancelled=None)\ .filter(end_date__gte=utils.today()) if reservation not in all: return 'Incorrect reservation'