source: main/waeup.kofa/trunk/src/waeup/kofa/hostels/browser.py @ 17388

Last change on this file since 17388 was 17313, checked in by Henrik Bettermann, 20 months ago

Allow beds to be blocked so that no student can be allocated to such a bed space (in contrast to reserved beds)

  • Property svn:keywords set to Id
File size: 17.4 KB
Line 
1## $Id: browser.py 17313 2023-01-25 08:52:51Z 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"""UI components for hostels and related components.
19"""
20import grok
21import sys
22from zope.i18n import translate
23from zope.component import getUtility, createObject
24from zope.catalog.interfaces import ICatalog
25from waeup.kofa.browser.layout import (
26    KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
27    NullValidator, UtilityView)
28from waeup.kofa.browser.breadcrumbs import Breadcrumb
29from waeup.kofa.browser.layout import default_primary_nav_template
30from waeup.kofa.browser.pages import delSubobjects
31from waeup.kofa.browser.viewlets import (
32    ManageActionButton, PrimaryNavTab)
33from waeup.kofa.browser.layout import jsaction, action
34from waeup.kofa.interfaces import IKofaObject, IKofaUtils, DOCLINK
35from waeup.kofa.interfaces import MessageFactory as _
36from waeup.kofa.hostels.vocabularies import NOT_OCCUPIED
37from waeup.kofa.hostels.hostel import Hostel
38from waeup.kofa.hostels.interfaces import (
39    IHostelsContainer, IHostel, IBed)
40from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
41
42# Save function used for save methods in manager pages
43def msave(view, **data):
44    changed_fields = view.applyData(view.context, **data)
45    # Turn list of lists into single list
46    if changed_fields:
47        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
48    fields_string = ' + '.join(changed_fields)
49    view.context._p_changed = True
50    view.flash(_('Form has been saved.'))
51    if fields_string:
52        view.context.writeLogMessage(view, 'saved: %s' % fields_string)
53    return
54
55class HostelsTab(PrimaryNavTab):
56    """Hostels tab in primary navigation.
57    """
58
59    grok.context(IKofaObject)
60    grok.order(5)
61    grok.require('waeup.viewHostels')
62    grok.name('hostelstab')
63    template = default_primary_nav_template
64    pnav = 5
65    tab_title = _(u'Hostels')
66
67    @property
68    def link_target(self):
69        return self.view.application_url('hostels')
70
71class HostelsBreadcrumb(Breadcrumb):
72    """A breadcrumb for the hostels container.
73    """
74    grok.context(IHostelsContainer)
75    title = _(u'Hostels')
76
77class HostelBreadcrumb(Breadcrumb):
78    """A breadcrumb for the hostel container.
79    """
80    grok.context(IHostel)
81
82    def title(self):
83        return self.context.hostel_name
84
85class BedBreadcrumb(Breadcrumb):
86    """A breadcrumb for the hostel container.
87    """
88    grok.context(IBed)
89
90    def title(self):
91        co = self.context.coordinates
92        return _('Block ${a}, Room ${b}, Bed ${c}',
93            mapping = {'a':co[1], 'b':co[2], 'c':co[3]})
94
95class HostelsContainerPage(KofaDisplayFormPage):
96    """The standard view for hostels containers.
97    """
98    grok.context(IHostelsContainer)
99    grok.name('index')
100    grok.require('waeup.viewHostels')
101    grok.template('containerpage')
102    label = _('Accommodation Section')
103    pnav = 5
104    form_fields = grok.AutoFields(IHostelsContainer)
105    form_fields[
106        'startdate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
107    form_fields[
108        'enddate'].custom_widget = FriendlyDatetimeDisplayWidget('le')
109
110class HostelsContainerManageActionButton(ManageActionButton):
111    grok.order(1)
112    grok.context(IHostelsContainer)
113    grok.view(HostelsContainerPage)
114    grok.require('waeup.manageHostels')
115    text = _('Manage accommodation section')
116
117class HostelsStatisticsActionButton(ManageActionButton):
118    grok.order(2)
119    grok.context(IHostelsContainer)
120    grok.view(HostelsContainerPage)
121    grok.require('waeup.manageHostels')
122    icon = 'actionicon_statistics.png'
123    text = _('Bed statistics')
124    target = 'statistics'
125
126class ReleaseExpiredAllocationsActionButton(ManageActionButton):
127    grok.order(3)
128    grok.context(IHostelsContainer)
129    grok.view(HostelsContainerPage)
130    grok.require('waeup.manageHostels')
131    icon = 'actionicon_sweep.png'
132    target = 'releaseexpired'
133    text = _('Release all expired bed space allocations')
134
135    @property
136    def target_url(self):
137        if self.target and self.context.allocation_expiration:
138            return self.view.url(self.view.context, self.target)
139        return
140
141    @property
142    def onclick(self):
143        msg = _("'All expired bed space allocations will be annulled. " + \
144              "Are you sure? \\n\\n" + \
145              "Bed space allocation expires ${a} days after accommodation " + \
146              "booking if maintenance fee is not " + \
147              "paid.'")
148        msg = _(msg, mapping={'a': self.context.allocation_expiration,})
149        portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
150        return "return window.confirm(%s);" % translate(
151            msg, 'waeup.kofa', target_language=portal_language)
152
153class HostelsContainerManagePage(KofaEditFormPage):
154    """The manage page for hostel containers.
155    """
156    grok.context(IHostelsContainer)
157    grok.name('manage')
158    grok.require('waeup.manageHostels')
159    grok.template('containermanagepage')
160    pnav = 5
161    label = _('Manage accommodation section')
162    form_fields = grok.AutoFields(IHostelsContainer)
163    taboneactions = [_('Save')]
164    tabtwoactions = [_('Add hostel'),
165        _('Clear all hostels'),
166        _('Remove selected')]
167    doclink = DOCLINK + '/hostels.html#accommodation-section'
168
169    # It's quite dangerous to remove entire hostels with its content (beds).
170    # Thus, this remove method should be combined with an archiving function.
171    @jsaction(_('Remove selected'))
172    def delHostels(self, **data):
173        form = self.request.form
174        if 'val_id' in form:
175            deleted = []
176            child_id = form['val_id']
177            if not isinstance(child_id, list):
178                child_id = [child_id]
179            for id in child_id:
180                deleted.append(id)
181            self.context.writeLogMessage(
182                self, 'deleted: % s' % ', '.join(deleted))
183        delSubobjects(self, redirect='@@manage', tab='2')
184        return
185
186    @action(_('Add hostel'), validator=NullValidator)
187    def addSubunit(self, **data):
188        self.redirect(self.url(self.context, 'addhostel'))
189        return
190
191    @jsaction(_('Clear all hostels'), style='danger')
192    def clearHostels(self, **data):
193        self.context.clearAllHostels()
194        self.flash(_('All hostels cleared.'))
195        self.context.writeLogMessage(self, 'all hostels cleared')
196        self.redirect(self.url(self.context, '@@manage')+'#tab2')
197        return
198
199    @action(_('Save'), style='primary')
200    def save(self, **data):
201        if data['accommodation_session'] != self.context.accommodation_session:
202            catalog = getUtility(ICatalog, name='beds_catalog')
203            beds = catalog.searchResults(bed_type=(None,None))
204            if len(beds) and not self.request.principal.id == 'admin':
205                self.flash(_('You can\'t change the booking session '
206                             'before clearing all hostels.'),
207                           type='warning')
208                return
209        msave(self, **data)
210        return
211
212class ReleaseExpiredAllocationsPage(UtilityView, grok.View):
213    """Release all expired allocated beds.
214    """
215    grok.context(IHostelsContainer)
216    grok.name('releaseexpired')
217    grok.require('waeup.manageHostels')
218
219    def update(self):
220        n = self.context.allocation_expiration
221        if not n:
222            self.flash(
223                _('Forbidden'), type='danger')
224            self.redirect(self.url(self.context))
225            return
226        if not grok.getSite()['configuration'].maintmode_enabled_by:
227            self.flash(
228                _('Portal must be in maintenance mode for releasing expired '
229                  'bed allocations.'),
230                type='danger')
231            self.redirect(self.url(self.context))
232            return
233        released = self.context.releaseExpiredAllocations(n)
234        if len(released):
235            message = ', '.join(released)
236            self.context.writeLogMessage(self, 'released: %s' % message)
237            if len(released) > 50:
238                flash_msg = _('Successfully released ${a} beds.',
239                    mapping = {'a':len(released)})
240            else:
241                flash_msg = _('Successfully released beds: ${a}',
242                    mapping = {'a':message})
243        else:
244            flash_msg = _('No bed released.')
245        self.flash(flash_msg, type='success')
246        self.redirect(self.url(self.context))
247        return
248
249    def render(self):
250        return
251
252class HostelsStatisticsPage(KofaDisplayFormPage):
253    """Some statistics about beds in hostels.
254    """
255    grok.context(IHostelsContainer)
256    grok.name('statistics')
257    grok.require('waeup.manageHostels')
258    grok.template('containerstatistics')
259    label = _('Bed Statistics')
260
261class HostelAddFormPage(KofaAddFormPage):
262    """Add-form to add a hostel.
263    """
264    grok.context(IHostelsContainer)
265    grok.require('waeup.manageHostels')
266    grok.name('addhostel')
267    #grok.template('hosteladdpage')
268    form_fields = grok.AutoFields(IHostel).omit('hostel_id')
269    label = _('Add hostel')
270    pnav = 5
271    doclink = DOCLINK + '/hostels.html#accommodation-section'
272
273    @action(_('Create hostel'))
274    def addHostel(self, **data):
275        hostel = container = createObject(u'waeup.Hostel')
276        self.applyData(hostel, **data)
277        hostel.hostel_id = data['hostel_name'].lower().replace(
278            ' ','-').replace('_','-').replace('.','')
279        try:
280            self.context.addHostel(hostel)
281        except KeyError:
282            self.flash(_('The hostel already exists.'), type='warning')
283            return
284        self.flash(_('Hostel created.'))
285        self.context.writeLogMessage(self, 'added: % s' % data['hostel_name'])
286        self.redirect(self.url(self.context[hostel.hostel_id], 'index'))
287        return
288
289class HostelDisplayFormPage(KofaDisplayFormPage):
290    """ Page to display hostel data
291    """
292    grok.context(IHostel)
293    grok.name('index')
294    grok.require('waeup.viewHostels')
295    grok.template('hostelpage')
296    form_fields = grok.AutoFields(IHostel)
297    pnav = 5
298
299    @property
300    def label(self):
301        return self.context.hostel_name
302
303class HostelManageActionButton(ManageActionButton):
304    grok.order(1)
305    grok.context(IHostel)
306    grok.view(HostelDisplayFormPage)
307    grok.require('waeup.manageHostels')
308    text = _('Manage')
309    target = 'manage'
310
311class HostelManageFormPage(KofaEditFormPage):
312    """ View to edit hostel data
313    """
314    grok.context(IHostel)
315    grok.name('manage')
316    grok.require('waeup.manageHostels')
317    form_fields = grok.AutoFields(IHostel).omit('hostel_id')
318    grok.template('hostelmanagepage')
319    label = _('Manage hostel')
320    pnav = 5
321    taboneactions = [_('Save')]
322    tabtwoactions = [_('Update all beds'),
323        _('Switch reservation of selected beds'),
324        _('Switch blockade of selected beds'),
325        _('Release selected beds'),
326        _('Clear hostel')]
327    not_occupied = NOT_OCCUPIED
328    doclink = DOCLINK + '/hostels.html#browser-pages'
329
330    @property
331    def students_url(self):
332        return self.url(grok.getSite(),'students')
333
334    @action(_('Save'), style='primary')
335    def save(self, **data):
336        msave(self, **data)
337        return
338
339    @action(_('Update all beds'), style='primary',
340            warning=_('Attention: The updater removes all reservation and '
341                        'blockade flags of existing beds. '
342                        'You really want to update?'))
343    def updateBeds(self, **data):
344        if not grok.getSite()['configuration'].maintmode_enabled_by:
345            self.flash(
346                _('Portal must be in maintenance mode for bed updates.'),
347                type='danger')
348            self.redirect(self.url(self.context, '@@manage')+'#tab2')
349            return
350        removed, added, modified, modified_beds = self.context.updateBeds()
351        message = '%d empty beds removed, %d beds added, %d occupied beds modified (%s)' % (
352            removed, added, modified, modified_beds)
353        if modified > 50:
354            flash_message = _(
355                '${a} empty beds removed, ${b} beds added, '
356                + '${c} occupied beds modified',
357                mapping = {'a':removed, 'b':added, 'c':modified})
358        else:
359            flash_message = _(
360                '${a} empty beds removed, ${b} beds added, '
361                + '${c} occupied beds modified (${d})',
362                mapping = {'a':removed, 'b':added, 'c':modified, 'd':modified_beds})
363        self.flash(flash_message)
364        self.context.writeLogMessage(self, message)
365        self.redirect(self.url(self.context, '@@manage')+'#tab2')
366        return
367
368    def _switchBeds(self, switch_type):
369        form = self.request.form
370        if 'val_id' in form:
371            child_id = form['val_id']
372        else:
373            self.flash(_('No item selected.'), type='warning')
374            self.redirect(self.url(self.context, '@@manage')+'#tab2')
375            return
376        if not isinstance(child_id, list):
377            child_id = [child_id]
378        switched = [] # for log file
379        switched_translated = [] # for flash message
380        # Here we know that the cookie has been set
381        preferred_language = self.request.cookies.get('kofa.language')
382        for bed_id in child_id:
383            message = self.context[bed_id].switchBed(switch_type)
384            switched.append('%s (%s)' % (bed_id,message))
385            m_translated = translate(message, 'waeup.kofa',
386                target_language=preferred_language)
387            switched_translated.append('%s (%s)' % (bed_id,m_translated))
388        if len(switched):
389            message = ', '.join(switched)
390            m_translated = ', '.join(switched_translated)
391            if len(switched) > 50:
392                self.flash(_('Successfully switched ${a} beds.',
393                    mapping = {'a':len(switched)}))
394            else:
395                self.flash(_('Successfully switched beds: ${a}',
396                    mapping = {'a':m_translated}))
397            self.context.writeLogMessage(self, 'switched: %s' % message)
398            self.redirect(self.url(self.context, '@@manage')+'#tab2')
399            return
400
401
402    @action(_('Switch reservation of selected beds'))
403    def switchReservations(self, **data):
404        self._switchBeds('reserved')
405        return
406
407    @action(_('Switch blockade of selected beds'))
408    def switchBockade(self, **data):
409        self._switchBeds('blocked')
410        return
411
412    @action(_('Release selected beds'))
413    def releaseBeds(self, **data):
414        form = self.request.form
415        if 'val_id' in form:
416            child_id = form['val_id']
417        else:
418            self.flash(_('No item selected.'), type='warning')
419            self.redirect(self.url(self.context, '@@manage')+'#tab2')
420            return
421        if not isinstance(child_id, list):
422            child_id = [child_id]
423        released = []
424        for bed_id in child_id:
425            message = self.context[bed_id].releaseBed()
426            if message:
427                released.append('%s (%s)' % (bed_id,message))
428        if len(released):
429            message = ', '.join(released)
430            self.flash(_('Successfully released beds: ${a}',
431                mapping = {'a':message}))
432            self.context.writeLogMessage(self, 'released: %s' % message)
433            self.redirect(self.url(self.context, '@@manage')+'#tab2')
434        else:
435            self.flash(_('No allocated bed selected.'), type='warning')
436            self.redirect(self.url(self.context, '@@manage')+'#tab2')
437        return
438
439    @jsaction(_('Clear hostel'), style='danger')
440    def clearHostel(self, **data):
441        self.context.clearHostel()
442        self.flash(_('Hostel cleared.'))
443        self.context.writeLogMessage(self, 'cleared')
444        self.redirect(self.url(self.context, '@@manage')+'#tab2')
445        return
446
447class BedManageFormPage(KofaEditFormPage):
448    """ View to edit bed data
449    """
450    grok.context(IBed)
451    grok.name('index')
452    grok.require('waeup.manageHostels')
453    form_fields = grok.AutoFields(IBed).omit(
454        'bed_id', 'bed_number', 'bed_type')
455    label = _('Allocate student')
456    pnav = 5
457    doclink = DOCLINK + '/hostels.html#manage-bed'
458
459    @action(_('Save'))
460    def save(self, **data):
461        if data['owner'] == NOT_OCCUPIED:
462            self.flash(_('No valid student id.'), type='warning')
463            self.redirect(self.url(self.context))
464            return
465        msave(self, **data)
466        self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
467        return
468
469    def update(self):
470        if self.context.bed_type.endswith('blocked'):
471            # No student can reside in a blocked bed.
472            self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
473        if self.context.owner != NOT_OCCUPIED:
474            # Don't use this form for exchanging students.
475            # Beds must be released first before they can be allocated to
476            # other students.
477            self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
478        return
Note: See TracBrowser for help on using the repository browser.