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

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

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

  • Property svn:keywords set to Id
File size: 14.4 KB
Line 
1## $Id: hostel.py 17412 2023-05-17 18:21:10Z henrik $
2##
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
22from zope.event import notify
23from zope.component import queryUtility, getUtility, createObject
24from zope.component.interfaces import IFactory, ComponentLookupError
25from zope.catalog.interfaces import ICatalog
26from zope.catalog.field import FieldIndex
27from datetime import datetime
28from waeup.kofa.utils.helpers import attrs_to_fields
29from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
30from waeup.kofa.hostels.interfaces import IHostel, IBed
31from waeup.kofa.students.interfaces import IBedTicket
32from waeup.kofa.interfaces import IKofaUtils, IKofaPluggable
33from waeup.kofa.interfaces import MessageFactory as _
34from waeup.kofa.utils.helpers import now, reindex_cat
35
36class Hostel(grok.Container):
37    """This is a hostel.
38    """
39    grok.implements(IHostel)
40    grok.provides(IHostel)
41
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):
52        """Remove all beds
53        """
54        keys = [i for i in self.keys()] # create deep copy
55        for bed in keys:
56            del self[bed]
57        return
58
59    def addBed(self, bed):
60        """Add a bed.
61        """
62        if not IBed.providedBy(bed):
63            raise TypeError(
64                'Hostels contain only IBed instances')
65        self[bed.bed_id] = bed
66        return
67
68    def updateBeds(self):
69        """Fill hostel with beds or update beds.
70        """
71        added_counter = 0
72        modified_counter = 0
73        removed_counter = 0
74        modified_beds = u''
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
83            else:
84                self[key].bed_number = 9999
85        remaining = len(keys) - removed_counter
86
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',[])
93        beds_for_all = getattr(self,'beds_for_all',[])
94        all_blocks = blocks_for_female + blocks_for_male
95        all_beds = (beds_for_pre + beds_for_fresh +
96            beds_for_returning + beds_for_final + beds_for_all)
97        floor_base = 100
98        if self.rooms_per_floor > 99:
99            floor_base = 1000
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:
107                        room_nr = floor*floor_base + room
108                        bt = 'all'
109                        if bed in beds_for_fresh:
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'
117                        bt = u'%s_%s_%s' % (self.special_handling,sex,bt)
118                        uid = u'%s_%s_%d_%s' % (
119                            self.hostel_id,block,room_nr,bed)
120                        if uid in self:
121                            bed = self[uid]
122                            # Renumber remaining bed
123                            bed.bed_number = len(self) + 1 - remaining
124                            remaining -= 1
125                            if bed.bed_type != bt:
126                                bed.bed_type = bt
127                                modified_counter += 1
128                                modified_beds += '%s, ' % uid
129                                notify(grok.ObjectModifiedEvent(bed))
130                        else:
131                            bed = createObject(u'waeup.Bed')
132                            bed.bed_id = uid
133                            bed.bed_type = bt
134                            bed.bed_number = len(self) + 1 - remaining
135                            bed.owner = NOT_OCCUPIED
136                            self.addBed(bed)
137                            added_counter +=1
138        return removed_counter, added_counter, modified_counter, modified_beds
139
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
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
161Hostel = attrs_to_fields(Hostel)
162
163class Bed(grok.Container):
164    """This is a bed.
165    """
166    grok.implements(IBed)
167    grok.provides(IBed)
168
169    @property
170    def coordinates(self):
171        """Determine the coordinates from the bed_id.
172        """
173        return self.bed_id.split('_')
174
175    # The following property attributes are only needed
176    # for the exporter to ease evaluation with Excel.
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
207    def bookBed(self, student_id):
208        if self.owner == NOT_OCCUPIED:
209            self.owner = student_id
210            notify(grok.ObjectModifiedEvent(self))
211            return None
212        else:
213            return self.owner
214
215    def switchBed(self, switch_type):
216        """Switches bed. `switch_type` is either `reserved` or `blocked`.
217        """
218        sh, sex, bt = self.bed_type.split('_')
219        hostel_id, block, room_nr, bed = self.coordinates
220        hostel = self.__parent__
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',[])
225        bed_string = u'%s_%s_%s' % (block, room_nr, bed)
226        if bt == switch_type:
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)
237            message = _(u'un' + switch_type)
238        else:
239            bt = u'%s_%s_%s' % (sh, sex, switch_type)
240            message = switch_type
241        self.bed_type = bt
242        notify(grok.ObjectModifiedEvent(self))
243        return message
244
245    def releaseBed(self):
246        """Release bed.
247        """
248        if self.owner == NOT_OCCUPIED:
249            return
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):
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.
271        """
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:
280            old_owner = self.owner
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))
286            ##: Add logging message
287            return "%s wob" % old_owner # owner without bed ticket
288        if bedticket.maint_payment_made:
289            return
290        jetzt = datetime.utcnow()
291        days_ago = getattr(jetzt - bedticket.booking_date, 'days')
292        if days_ago >= n:
293            old_owner = self.owner
294            self.owner = NOT_OCCUPIED
295            sh, sex, bt = self.bed_type.split('_')
296            bt = u'%s_%s_reserved' % (sh, sex)
297            self.bed_type = bt
298            notify(grok.ObjectModifiedEvent(self))
299            bedticket.bed = None
300            tz = getUtility(IKofaUtils).tzinfo
301            timestamp = now(tz).strftime("%Y-%m-%d %H:%M:%S %Z")
302            bedticket.bed_coordinates = u'-- booking expired (%s) --' % (
303                timestamp,)
304            return old_owner
305        return
306
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
312
313Bed = attrs_to_fields(Bed)
314
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
331class BedFactory(grok.GlobalUtility):
332    """A factory for  beds.
333
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
347class HostelsPlugin(grok.GlobalUtility):
348    """A plugin to update beds_catalog
349    """
350    grok.implements(IKofaPluggable)
351    grok.name('hostels')
352    log_prefix = 'HostelsPlugin'
353
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
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
373                logger.info(
374                    '%s: bed_id index added to beds_catalog.'
375                    % self.log_prefix)
376                reindex_cat(cat)
377                logger.info(
378                    '%s: beds_catalog updated.'
379                    % self.log_prefix)
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
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    """
395    if bedticket.bed != None:
396        bedticket.bed.owner = NOT_OCCUPIED
397        notify(grok.ObjectModifiedEvent(bedticket.bed))
398
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.