source: main/waeup.kofa/trunk/src/waeup/kofa/hostels/hostel.py @ 17930

Last change on this file since 17930 was 17412, checked in by Henrik Bettermann, 18 months ago

Add ReleaseExpiredAllocationsPage2 which allows to release unpaid beds in single hostels.

  • Property svn:keywords set to Id
File size: 14.4 KB
RevLine 
[7195]1## $Id: hostel.py 17412 2023-05-17 18:21:10Z henrik $
2##
[6951]3## Copyright (C) 2011 Uli Fouquet & Henrik Bettermann
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""
19These are the hostels.
20"""
21import grok
[7003]22from zope.event import notify
[17412]23from zope.component import queryUtility, getUtility, createObject
[15708]24from zope.component.interfaces import IFactory, ComponentLookupError
25from zope.catalog.interfaces import ICatalog
26from zope.catalog.field import FieldIndex
[6951]27from datetime import datetime
[7811]28from waeup.kofa.utils.helpers import attrs_to_fields
29from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
[9414]30from waeup.kofa.hostels.interfaces import IHostel, IBed
[7811]31from waeup.kofa.students.interfaces import IBedTicket
[15708]32from waeup.kofa.interfaces import IKofaUtils, IKofaPluggable
[7811]33from waeup.kofa.interfaces import MessageFactory as _
[15741]34from waeup.kofa.utils.helpers import now, reindex_cat
[6951]35
36class Hostel(grok.Container):
37    """This is a hostel.
38    """
39    grok.implements(IHostel)
40    grok.provides(IHostel)
41
[9196]42    @property
43    def bed_statistics(self):
44        total = len(self.keys())
45        booked = 0
46        for value in self.values():
47            if value.owner != NOT_OCCUPIED:
48                booked += 1
49        return {'booked':booked, 'total':total}
50
51    def clearHostel(self):
[9197]52        """Remove all beds
[9196]53        """
[9197]54        keys = [i for i in self.keys()] # create deep copy
55        for bed in keys:
56            del self[bed]
57        return
[9196]58
[6963]59    def addBed(self, bed):
[6970]60        """Add a bed.
[6963]61        """
62        if not IBed.providedBy(bed):
63            raise TypeError(
64                'Hostels contain only IBed instances')
[6970]65        self[bed.bed_id] = bed
[6963]66        return
67
[6970]68    def updateBeds(self):
69        """Fill hostel with beds or update beds.
70        """
71        added_counter = 0
72        modified_counter = 0
[6978]73        removed_counter = 0
[6988]74        modified_beds = u''
[6978]75
76        # Remove all empty beds. Occupied beds remain in hostel!
77        keys = list(self.keys()) # create list copy
78        for key in keys:
79            if self[key].owner == NOT_OCCUPIED:
80                del self[key]
81                self._p_changed = True
82                removed_counter += 1
[6998]83            else:
84                self[key].bed_number = 9999
85        remaining = len(keys) - removed_counter
[6978]86
[6970]87        blocks_for_female = getattr(self,'blocks_for_female',[])
88        blocks_for_male = getattr(self,'blocks_for_male',[])
89        beds_for_fresh = getattr(self,'beds_for_fresh',[])
90        beds_for_pre = getattr(self,'beds_for_pre',[])
91        beds_for_returning = getattr(self,'beds_for_returning',[])
92        beds_for_final = getattr(self,'beds_for_final',[])
[6971]93        beds_for_all = getattr(self,'beds_for_all',[])
[6970]94        all_blocks = blocks_for_female + blocks_for_male
95        all_beds = (beds_for_pre + beds_for_fresh +
[6971]96            beds_for_returning + beds_for_final + beds_for_all)
[13352]97        floor_base = 100
98        if self.rooms_per_floor > 99:
99            floor_base = 1000
[6970]100        for block in all_blocks:
101            sex = 'male'
102            if block in blocks_for_female:
103                sex = 'female'
104            for floor in range(1,int(self.floors_per_block)+1):
105                for room in range(1,int(self.rooms_per_floor)+1):
106                    for bed in all_beds:
[13352]107                        room_nr = floor*floor_base + room
[6970]108                        bt = 'all'
[13346]109                        if bed in beds_for_fresh:
[6970]110                            bt = 'fr'
111                        elif bed in beds_for_pre:
112                            bt = 'pr'
113                        elif bed in beds_for_final:
114                            bt = 'fi'
115                        elif bed in beds_for_returning:
116                            bt = 're'
[6973]117                        bt = u'%s_%s_%s' % (self.special_handling,sex,bt)
[13168]118                        uid = u'%s_%s_%d_%s' % (
119                            self.hostel_id,block,room_nr,bed)
[9701]120                        if uid in self:
[6970]121                            bed = self[uid]
[13170]122                            # Renumber remaining bed
[6998]123                            bed.bed_number = len(self) + 1 - remaining
124                            remaining -= 1
[6970]125                            if bed.bed_type != bt:
126                                bed.bed_type = bt
127                                modified_counter += 1
[9448]128                                modified_beds += '%s, ' % uid
129                                notify(grok.ObjectModifiedEvent(bed))
[6970]130                        else:
[15633]131                            bed = createObject(u'waeup.Bed')
[6970]132                            bed.bed_id = uid
133                            bed.bed_type = bt
[6998]134                            bed.bed_number = len(self) + 1 - remaining
[6970]135                            bed.owner = NOT_OCCUPIED
136                            self.addBed(bed)
137                            added_counter +=1
[6988]138        return removed_counter, added_counter, modified_counter, modified_beds
[6970]139
[17412]140    def releaseExpiredAllocations(self, n=7):
141        """Release bed in hostel if bed allocation has expired. Allocation expires
142        after `n` days if maintenance fee has not been paid.
143        """
144        cat = queryUtility(ICatalog, name='beds_catalog')
145        results = cat.searchResults(bed_type=(None,None))
146        released = []
147        for bed in results:
148            if not bed.bed_id.startswith(self.hostel_id):
149                continue
150            student_id = bed.releaseBedIfMaintenanceNotPaid(n=n)
151            if student_id:
152                released.append('%s (%s)' % (bed.bed_id,student_id))
153        return released
154
[13166]155    def writeLogMessage(self, view, message):
156        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
157        self.__parent__.logger.info(
158            '%s - %s - %s' % (ob_class, self.__name__, message))
159        return
160
[6951]161Hostel = attrs_to_fields(Hostel)
[6963]162
163class Bed(grok.Container):
164    """This is a bed.
165    """
[9414]166    grok.implements(IBed)
[6963]167    grok.provides(IBed)
168
[9199]169    @property
170    def coordinates(self):
[6974]171        """Determine the coordinates from the bed_id.
[6963]172        """
[6974]173        return self.bed_id.split('_')
[6963]174
[9199]175    # The following property attributes are only needed
[13170]176    # for the exporter to ease evaluation with Excel.
[9199]177
178    @property
179    def hall(self):
180        return self.coordinates[0]
181
182    @property
183    def block(self):
184        return self.coordinates[1]
185
186    @property
187    def room(self):
188        return self.coordinates[2]
189
190    @property
191    def bed(self):
192        return self.coordinates[3]
193
194    @property
195    def special_handling(self):
196        return self.bed_type.split('_')[0]
197
198    @property
199    def sex(self):
200        return self.bed_type.split('_')[1]
201
202    @property
203    def bt(self):
204        return self.bed_type.split('_')[2]
205
206
[6996]207    def bookBed(self, student_id):
[6998]208        if self.owner == NOT_OCCUPIED:
209            self.owner = student_id
[7003]210            notify(grok.ObjectModifiedEvent(self))
[6998]211            return None
212        else:
213            return self.owner
[6996]214
[17313]215    def switchBed(self, switch_type):
216        """Switches bed. `switch_type` is either `reserved` or `blocked`.
[6974]217        """
218        sh, sex, bt = self.bed_type.split('_')
[9199]219        hostel_id, block, room_nr, bed = self.coordinates
[6975]220        hostel = self.__parent__
[6988]221        beds_for_fresh = getattr(hostel,'beds_for_fresh',[])
222        beds_for_pre = getattr(hostel,'beds_for_pre',[])
223        beds_for_returning = getattr(hostel,'beds_for_returning',[])
224        beds_for_final = getattr(hostel,'beds_for_final',[])
[6976]225        bed_string = u'%s_%s_%s' % (block, room_nr, bed)
[17313]226        if bt == switch_type:
[6974]227            bt = 'all'
228            if bed in beds_for_fresh:
229                bt = 'fr'
230            elif bed in beds_for_pre:
231                bt = 'pr'
232            elif bed in beds_for_final:
233                bt = 'fi'
234            elif bed in beds_for_returning:
235                bt = 're'
236            bt = u'%s_%s_%s' % (sh, sex, bt)
[17313]237            message = _(u'un' + switch_type)
[7718]238        else:
[17313]239            bt = u'%s_%s_%s' % (sh, sex, switch_type)
240            message = switch_type
[6974]241        self.bed_type = bt
[9448]242        notify(grok.ObjectModifiedEvent(self))
[6988]243        return message
[6974]244
[7042]245    def releaseBed(self):
[13533]246        """Release bed.
247        """
[7042]248        if self.owner == NOT_OCCUPIED:
[7070]249            return
[13316]250        old_owner = self.owner
251        self.owner = NOT_OCCUPIED
252        notify(grok.ObjectModifiedEvent(self))
253        accommodation_session = grok.getSite()[
254            'hostels'].accommodation_session
255        try:
256            bedticket = grok.getSite()['students'][old_owner][
257                          'accommodation'][str(accommodation_session)]
258        except KeyError:
259            return '%s without bed ticket' % old_owner
260        bedticket.bed = None
261        tz = getUtility(IKofaUtils).tzinfo
262        timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
263        bedticket.bed_coordinates = u'-- booking cancelled on %s --' % (
264            timestamp,)
265        return old_owner
266
267    def releaseBedIfMaintenanceNotPaid(self, n=7):
[15944]268        """Release bed if maintenance fee has not been paid on time or
269        if bed ticket does not exist. Reserve bed so that it cannot be
270        automatically booked by someone else.
[13533]271        """
[13316]272        if self.owner == NOT_OCCUPIED:
273            return
274        accommodation_session = grok.getSite()[
275            'hostels'].accommodation_session
276        try:
277            bedticket = grok.getSite()['students'][self.owner][
278                          'accommodation'][str(accommodation_session)]
279        except KeyError:
[15944]280            old_owner = self.owner
[15417]281            self.owner = NOT_OCCUPIED
282            sh, sex, bt = self.bed_type.split('_')
283            bt = u'%s_%s_reserved' % (sh, sex)
284            self.bed_type = bt
285            notify(grok.ObjectModifiedEvent(self))
[15944]286            ##: Add logging message
287            return "%s wob" % old_owner # owner without bed ticket
[13316]288        if bedticket.maint_payment_made:
289            return
290        jetzt = datetime.utcnow()
291        days_ago = getattr(jetzt - bedticket.booking_date, 'days')
[15305]292        if days_ago >= n:
[13318]293            old_owner = self.owner
[7042]294            self.owner = NOT_OCCUPIED
[13533]295            sh, sex, bt = self.bed_type.split('_')
296            bt = u'%s_%s_reserved' % (sh, sex)
297            self.bed_type = bt
[7042]298            notify(grok.ObjectModifiedEvent(self))
299            bedticket.bed = None
[8183]300            tz = getUtility(IKofaUtils).tzinfo
[8234]301            timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
[13316]302            bedticket.bed_coordinates = u'-- booking expired (%s) --' % (
[8186]303                timestamp,)
[13318]304            return old_owner
[13316]305        return
[7042]306
[13166]307    def writeLogMessage(self, view, message):
308        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
309        self.__parent__.__parent__.logger.info(
310            '%s - %s - %s' % (ob_class, self.__name__, message))
311        return
[6963]312
313Bed = attrs_to_fields(Bed)
[7006]314
[9202]315class HostelFactory(grok.GlobalUtility):
316    """A factory for hostels.
317
318    We need this factory for the hostel processor.
319    """
320    grok.implements(IFactory)
321    grok.name(u'waeup.Hostel')
322    title = u"Create a new hostel.",
323    description = u"This factory instantiates new hostel instances."
324
325    def __call__(self, *args, **kw):
326        return Hostel()
327
328    def getInterfaces(self):
329        return implementedBy(Hostel)
330
[15633]331class BedFactory(grok.GlobalUtility):
332    """A factory for  beds.
[9202]333
[15633]334    We need this factory to ease customization.
335    """
336    grok.implements(IFactory)
337    grok.name(u'waeup.Bed')
338    title = u"Create a new bed.",
339    description = u"This factory instantiates new bed instances."
340
341    def __call__(self, *args, **kw):
342        return Bed()
343
344    def getInterfaces(self):
345        return implementedBy(Bed)
346
[15708]347class HostelsPlugin(grok.GlobalUtility):
348    """A plugin to update beds_catalog
349    """
350    grok.implements(IKofaPluggable)
351    grok.name('hostels')
352    log_prefix = 'HostelsPlugin'
[15633]353
[15708]354    def setup(self, site, name, logger):
355        return
356
357    def update(self, site, name, logger):
358        site_name = getattr(site, '__name__', '<Unnamed Site>')
359        nothing_to_do = True
360        # Add bed_id index
361        try:
362            cat = getUtility(ICatalog, name='beds_catalog')
363            if 'bed_id' not in cat.keys():
364                nothing_to_do = False
[15779]365                # replace original `updateIndex` method
366                def updateIndexReplacement(index):
367                    reindex_cat(cat)
368                cat._updateIndex = cat.updateIndex
369                cat.updateIndex = updateIndexReplacement
370                # setup catalog
371                cat[u'bed_id'] = FieldIndex(field_name=u'bed_id')
372                cat.updateIndex = cat._updateIndex  # undo changes
[15708]373                logger.info(
[15779]374                    '%s: bed_id index added to beds_catalog.'
[15708]375                    % self.log_prefix)
[15779]376                reindex_cat(cat)
377                logger.info(
378                    '%s: beds_catalog updated.'
379                    % self.log_prefix)
[15708]380        except ComponentLookupError: # in unit tests
381            pass
382        if nothing_to_do:
383            logger.info(
384                '%s: Updating site at %s: Nothing to do.' % (
385                    self.log_prefix, site_name,)
386                )
387        return
388
389
[7006]390@grok.subscribe(IBedTicket, grok.IObjectRemovedEvent)
391def handle_bedticket_removed(bedticket, event):
392    """If a bed ticket is deleted, we make sure that also the owner attribute
393    of the bed is cleared (set to NOT_OCCUPIED).
394    """
[7068]395    if bedticket.bed != None:
396        bedticket.bed.owner = NOT_OCCUPIED
397        notify(grok.ObjectModifiedEvent(bedticket.bed))
[9202]398
[13440]399@grok.subscribe(IBed, grok.IObjectRemovedEvent)
400def handle_bed_removed(bed, event):
401    """If a bed is deleted, we make sure that the bed object is
402    removed also from the owner's bed ticket.
403    """
404    if bed.owner == NOT_OCCUPIED:
405        return
406    accommodation_session = grok.getSite()['hostels'].accommodation_session
407    try:
408        bedticket = grok.getSite()['students'][bed.owner][
409                      'accommodation'][str(accommodation_session)]
410    except KeyError:
411        return
412    bedticket.bed = None
Note: See TracBrowser for help on using the repository browser.