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

Last change on this file since 17574 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: 17.9 KB
Line 
1## $Id: browser.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"""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 grok.getSite()['hostels'].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': grok.getSite()['hostels'].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 = grok.getSite()['hostels'].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 ReleaseExpiredAllocationsPage2(ReleaseExpiredAllocationsPage):
253    """Release all expired allocated beds in a single hostel.
254    """
255    grok.context(IHostel)
256    grok.name('releaseexpired2')
257
258class HostelsStatisticsPage(KofaDisplayFormPage):
259    """Some statistics about beds in hostels.
260    """
261    grok.context(IHostelsContainer)
262    grok.name('statistics')
263    grok.require('waeup.manageHostels')
264    grok.template('containerstatistics')
265    label = _('Bed Statistics')
266
267class HostelAddFormPage(KofaAddFormPage):
268    """Add-form to add a hostel.
269    """
270    grok.context(IHostelsContainer)
271    grok.require('waeup.manageHostels')
272    grok.name('addhostel')
273    #grok.template('hosteladdpage')
274    form_fields = grok.AutoFields(IHostel).omit('hostel_id')
275    label = _('Add hostel')
276    pnav = 5
277    doclink = DOCLINK + '/hostels.html#accommodation-section'
278
279    @action(_('Create hostel'))
280    def addHostel(self, **data):
281        hostel = container = createObject(u'waeup.Hostel')
282        self.applyData(hostel, **data)
283        hostel.hostel_id = data['hostel_name'].lower().replace(
284            ' ','-').replace('_','-').replace('.','')
285        try:
286            self.context.addHostel(hostel)
287        except KeyError:
288            self.flash(_('The hostel already exists.'), type='warning')
289            return
290        self.flash(_('Hostel created.'))
291        self.context.writeLogMessage(self, 'added: % s' % data['hostel_name'])
292        self.redirect(self.url(self.context[hostel.hostel_id], 'index'))
293        return
294
295class HostelDisplayFormPage(KofaDisplayFormPage):
296    """ Page to display hostel data
297    """
298    grok.context(IHostel)
299    grok.name('index')
300    grok.require('waeup.viewHostels')
301    grok.template('hostelpage')
302    form_fields = grok.AutoFields(IHostel)
303    pnav = 5
304
305    @property
306    def label(self):
307        return self.context.hostel_name
308
309class ReleaseExpiredAllocationsActionButton2(ReleaseExpiredAllocationsActionButton):
310    grok.order(2)
311    grok.context(IHostel)
312    grok.view(HostelDisplayFormPage)
313    target = 'releaseexpired2'
314    text = _('Release expired bed space allocations in this hostel')
315
316class HostelManageActionButton(ManageActionButton):
317    grok.order(1)
318    grok.context(IHostel)
319    grok.view(HostelDisplayFormPage)
320    grok.require('waeup.manageHostels')
321    text = _('Manage')
322    target = 'manage'
323
324class HostelManageFormPage(KofaEditFormPage):
325    """ View to edit hostel data
326    """
327    grok.context(IHostel)
328    grok.name('manage')
329    grok.require('waeup.manageHostels')
330    form_fields = grok.AutoFields(IHostel).omit('hostel_id')
331    grok.template('hostelmanagepage')
332    label = _('Manage hostel')
333    pnav = 5
334    taboneactions = [_('Save')]
335    tabtwoactions = [_('Update all beds'),
336        _('Switch reservation of selected beds'),
337        _('Switch blockade of selected beds'),
338        _('Release selected beds'),
339        _('Clear hostel')]
340    not_occupied = NOT_OCCUPIED
341    doclink = DOCLINK + '/hostels.html#browser-pages'
342
343    @property
344    def students_url(self):
345        return self.url(grok.getSite(),'students')
346
347    @action(_('Save'), style='primary')
348    def save(self, **data):
349        msave(self, **data)
350        return
351
352    @action(_('Update all beds'), style='primary',
353            warning=_('Attention: The updater removes all reservation and '
354                        'blockade flags of existing beds. '
355                        'You really want to update?'))
356    def updateBeds(self, **data):
357        if not grok.getSite()['configuration'].maintmode_enabled_by:
358            self.flash(
359                _('Portal must be in maintenance mode for bed updates.'),
360                type='danger')
361            self.redirect(self.url(self.context, '@@manage')+'#tab2')
362            return
363        removed, added, modified, modified_beds = self.context.updateBeds()
364        message = '%d empty beds removed, %d beds added, %d occupied beds modified (%s)' % (
365            removed, added, modified, modified_beds)
366        if modified > 50:
367            flash_message = _(
368                '${a} empty beds removed, ${b} beds added, '
369                + '${c} occupied beds modified',
370                mapping = {'a':removed, 'b':added, 'c':modified})
371        else:
372            flash_message = _(
373                '${a} empty beds removed, ${b} beds added, '
374                + '${c} occupied beds modified (${d})',
375                mapping = {'a':removed, 'b':added, 'c':modified, 'd':modified_beds})
376        self.flash(flash_message)
377        self.context.writeLogMessage(self, message)
378        self.redirect(self.url(self.context, '@@manage')+'#tab2')
379        return
380
381    def _switchBeds(self, switch_type):
382        form = self.request.form
383        if 'val_id' in form:
384            child_id = form['val_id']
385        else:
386            self.flash(_('No item selected.'), type='warning')
387            self.redirect(self.url(self.context, '@@manage')+'#tab2')
388            return
389        if not isinstance(child_id, list):
390            child_id = [child_id]
391        switched = [] # for log file
392        switched_translated = [] # for flash message
393        # Here we know that the cookie has been set
394        preferred_language = self.request.cookies.get('kofa.language')
395        for bed_id in child_id:
396            message = self.context[bed_id].switchBed(switch_type)
397            switched.append('%s (%s)' % (bed_id,message))
398            m_translated = translate(message, 'waeup.kofa',
399                target_language=preferred_language)
400            switched_translated.append('%s (%s)' % (bed_id,m_translated))
401        if len(switched):
402            message = ', '.join(switched)
403            m_translated = ', '.join(switched_translated)
404            if len(switched) > 50:
405                self.flash(_('Successfully switched ${a} beds.',
406                    mapping = {'a':len(switched)}))
407            else:
408                self.flash(_('Successfully switched beds: ${a}',
409                    mapping = {'a':m_translated}))
410            self.context.writeLogMessage(self, 'switched: %s' % message)
411            self.redirect(self.url(self.context, '@@manage')+'#tab2')
412            return
413
414
415    @action(_('Switch reservation of selected beds'))
416    def switchReservations(self, **data):
417        self._switchBeds('reserved')
418        return
419
420    @action(_('Switch blockade of selected beds'))
421    def switchBockade(self, **data):
422        self._switchBeds('blocked')
423        return
424
425    @action(_('Release selected beds'))
426    def releaseBeds(self, **data):
427        form = self.request.form
428        if 'val_id' in form:
429            child_id = form['val_id']
430        else:
431            self.flash(_('No item selected.'), type='warning')
432            self.redirect(self.url(self.context, '@@manage')+'#tab2')
433            return
434        if not isinstance(child_id, list):
435            child_id = [child_id]
436        released = []
437        for bed_id in child_id:
438            message = self.context[bed_id].releaseBed()
439            if message:
440                released.append('%s (%s)' % (bed_id,message))
441        if len(released):
442            message = ', '.join(released)
443            self.flash(_('Successfully released beds: ${a}',
444                mapping = {'a':message}))
445            self.context.writeLogMessage(self, 'released: %s' % message)
446            self.redirect(self.url(self.context, '@@manage')+'#tab2')
447        else:
448            self.flash(_('No allocated bed selected.'), type='warning')
449            self.redirect(self.url(self.context, '@@manage')+'#tab2')
450        return
451
452    @jsaction(_('Clear hostel'), style='danger')
453    def clearHostel(self, **data):
454        self.context.clearHostel()
455        self.flash(_('Hostel cleared.'))
456        self.context.writeLogMessage(self, 'cleared')
457        self.redirect(self.url(self.context, '@@manage')+'#tab2')
458        return
459
460class BedManageFormPage(KofaEditFormPage):
461    """ View to edit bed data
462    """
463    grok.context(IBed)
464    grok.name('index')
465    grok.require('waeup.manageHostels')
466    form_fields = grok.AutoFields(IBed).omit(
467        'bed_id', 'bed_number', 'bed_type')
468    label = _('Allocate student')
469    pnav = 5
470    doclink = DOCLINK + '/hostels.html#manage-bed'
471
472    @action(_('Save'))
473    def save(self, **data):
474        if data['owner'] == NOT_OCCUPIED:
475            self.flash(_('No valid student id.'), type='warning')
476            self.redirect(self.url(self.context))
477            return
478        msave(self, **data)
479        self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
480        return
481
482    def update(self):
483        if self.context.bed_type.endswith('blocked'):
484            # No student can reside in a blocked bed.
485            self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
486        if self.context.owner != NOT_OCCUPIED:
487            # Don't use this form for exchanging students.
488            # Beds must be released first before they can be allocated to
489            # other students.
490            self.redirect(self.url(self.context.__parent__, '@@manage')+'#tab2')
491        return
Note: See TracBrowser for help on using the repository browser.