Index: /main/waeup.kofa/trunk/CHANGES.txt
===================================================================
--- /main/waeup.kofa/trunk/CHANGES.txt	(revision 17411)
+++ /main/waeup.kofa/trunk/CHANGES.txt	(revision 17412)
@@ -4,4 +4,7 @@
 1.8.2.dev0 (unreleased)
 =======================
+
+* Add `ReleaseExpiredAllocationsPage2` which allows to release unpaid beds
+  in single hostels.
 
 * Show matric number on `TranscriptOfficerLandingPage`.
Index: /main/waeup.kofa/trunk/src/waeup/kofa/hostels/browser.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/hostels/browser.py	(revision 17411)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/hostels/browser.py	(revision 17412)
@@ -135,5 +135,5 @@
     @property
     def target_url(self):
-        if self.target and self.context.allocation_expiration:
+        if self.target and grok.getSite()['hostels'].allocation_expiration:
             return self.view.url(self.view.context, self.target)
         return
@@ -146,5 +146,5 @@
               "booking if maintenance fee is not " + \
               "paid.'")
-        msg = _(msg, mapping={'a': self.context.allocation_expiration,})
+        msg = _(msg, mapping={'a': grok.getSite()['hostels'].allocation_expiration,})
         portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
         return "return window.confirm(%s);" % translate(
@@ -218,5 +218,5 @@
 
     def update(self):
-        n = self.context.allocation_expiration
+        n = grok.getSite()['hostels'].allocation_expiration
         if not n:
             self.flash(
@@ -250,4 +250,10 @@
         return
 
+class ReleaseExpiredAllocationsPage2(ReleaseExpiredAllocationsPage):
+    """Release all expired allocated beds in a single hostel.
+    """
+    grok.context(IHostel)
+    grok.name('releaseexpired2')
+
 class HostelsStatisticsPage(KofaDisplayFormPage):
     """Some statistics about beds in hostels.
@@ -300,4 +306,11 @@
     def label(self):
         return self.context.hostel_name
+
+class ReleaseExpiredAllocationsActionButton2(ReleaseExpiredAllocationsActionButton):
+    grok.order(2)
+    grok.context(IHostel)
+    grok.view(HostelDisplayFormPage)
+    target = 'releaseexpired2'
+    text = _('Release expired bed space allocations in this hostel')
 
 class HostelManageActionButton(ManageActionButton):
Index: /main/waeup.kofa/trunk/src/waeup/kofa/hostels/hostel.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/hostels/hostel.py	(revision 17411)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/hostels/hostel.py	(revision 17412)
@@ -21,5 +21,5 @@
 import grok
 from zope.event import notify
-from zope.component import getUtility, createObject
+from zope.component import queryUtility, getUtility, createObject
 from zope.component.interfaces import IFactory, ComponentLookupError
 from zope.catalog.interfaces import ICatalog
@@ -138,4 +138,19 @@
         return removed_counter, added_counter, modified_counter, modified_beds
 
+    def releaseExpiredAllocations(self, n=7):
+        """Release bed in hostel if bed allocation has expired. Allocation expires
+        after `n` days if maintenance fee has not been paid.
+        """
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        results = cat.searchResults(bed_type=(None,None))
+        released = []
+        for bed in results:
+            if not bed.bed_id.startswith(self.hostel_id):
+                continue
+            student_id = bed.releaseBedIfMaintenanceNotPaid(n=n)
+            if student_id:
+                released.append('%s (%s)' % (bed.bed_id,student_id))
+        return released
+
     def writeLogMessage(self, view, message):
         ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
Index: /main/waeup.kofa/trunk/src/waeup/kofa/hostels/tests.py
===================================================================
--- /main/waeup.kofa/trunk/src/waeup/kofa/hostels/tests.py	(revision 17411)
+++ /main/waeup.kofa/trunk/src/waeup/kofa/hostels/tests.py	(revision 17412)
@@ -164,5 +164,5 @@
         # Create a bed
         bed = Bed()
-        bed.bed_id = u'hall_block_room_bed'
+        bed.bed_id = u'hall-x_block_room_bed'
         bed.bed_number = 1
         bed.bed_type = u'regular_male_fr'
@@ -191,5 +191,5 @@
     layer = FunctionalLayer
 
-    def test_release_expired_allocations(self):
+    def test_release_expired_allocations_portalwide(self):
         self.app['hostels'].allocation_expiration = 7
         self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
@@ -200,9 +200,9 @@
         self.student['accommodation'].addBedTicket(bedticket)
         self.app[
-            'hostels']['hall-x']['hall_block_room_bed'].owner = self.student_id
+            'hostels']['hall-x']['hall-x_block_room_bed'].owner = self.student_id
         notify(grok.ObjectModifiedEvent(
-            self.app['hostels']['hall-x']['hall_block_room_bed']))
-        self.assertEqual(
-            self.app['hostels']['hall-x']['hall_block_room_bed'].bed_type,
+            self.app['hostels']['hall-x']['hall-x_block_room_bed']))
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall-x_block_room_bed'].bed_type,
             'regular_male_fr')
         results = cat.searchResults(owner=(self.student_id, self.student_id))
@@ -218,5 +218,5 @@
         self.browser.open(self.container_path + '/releaseexpired')
         self.assertTrue(
-            'Successfully released beds: hall_block_room_bed (K1000000)'
+            'Successfully released beds: hall-x_block_room_bed (K1000000)'
             in self.browser.contents)
         results = cat.searchResults(owner=(self.student_id, self.student_id))
@@ -226,8 +226,8 @@
         # The owner has been removed and the bed reserved.
         self.assertEqual(
-            self.app['hostels']['hall-x']['hall_block_room_bed'].owner,
+            self.app['hostels']['hall-x']['hall-x_block_room_bed'].owner,
             NOT_OCCUPIED)
         self.assertEqual(
-            self.app['hostels']['hall-x']['hall_block_room_bed'].bed_type,
+            self.app['hostels']['hall-x']['hall-x_block_room_bed'].bed_type,
             'regular_male_reserved')
         # Accommodation session can't be changed if hostels are not empty.
@@ -242,5 +242,48 @@
         self.assertTrue(
             'hostels.browser.ReleaseExpiredAllocationsPage - hostels - '
-            'released: hall_block_room_bed (K1000000)'
+            'released: hall-x_block_room_bed (K1000000)'
+            in logcontent)
+        return
+
+    def test_release_expired_allocations_in_hostel(self):
+        self.app['hostels'].allocation_expiration = 7
+        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
+        cat = queryUtility(ICatalog, name='beds_catalog')
+        bedticket = BedTicket()
+        bedticket.booking_session = 2004
+        bedticket.bed_coordinates = u'anything'
+        delta = timedelta(days=10)
+        bedticket.booking_date = datetime.utcnow() - delta
+        self.student['accommodation'].addBedTicket(bedticket)
+        self.app[
+            'hostels']['hall-x']['hall-x_block_room_bed'].owner = self.student_id
+        notify(grok.ObjectModifiedEvent(
+            self.app['hostels']['hall-x']['hall-x_block_room_bed']))
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall-x_block_room_bed'].bed_type,
+            'regular_male_fr')
+        results = cat.searchResults(owner=(self.student_id, self.student_id))
+        self.assertEqual(len(results), 1)
+        grok.getSite()['configuration'].maintmode_enabled_by = u'any_id'
+        self.browser.open(self.container_path + '/hall-x/releaseexpired2')
+        self.assertTrue(
+            'Successfully released beds: hall-x_block_room_bed (K1000000)'
+            in self.browser.contents)
+        results = cat.searchResults(owner=(self.student_id, self.student_id))
+        self.assertEqual(len(results), 0)
+        self.assertMatches(bedticket.display_coordinates,
+            '-- booking expired (2015-10-14 08:35:38 UTC) --')
+        # The owner has been removed and the bed reserved.
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall-x_block_room_bed'].owner,
+            NOT_OCCUPIED)
+        self.assertEqual(
+            self.app['hostels']['hall-x']['hall-x_block_room_bed'].bed_type,
+            'regular_male_reserved')
+        # Releasing is logged.
+        logcontent = open(self.logfile).read()
+        self.assertTrue(
+            'hostels.browser.ReleaseExpiredAllocationsPage2 - hall-x - '
+            'released: hall-x_block_room_bed (K1000000)'
             in logcontent)
         return
@@ -249,7 +292,7 @@
         utils = getUtility(IHostelsUtils)
         self.app['hostels']['hall-x'][
-            'hall_block_room_bed'].owner = NOT_OCCUPIED
+            'hall-x_block_room_bed'].owner = NOT_OCCUPIED
         notify(grok.ObjectModifiedEvent(
-            self.app['hostels']['hall-x']['hall_block_room_bed']))
+            self.app['hostels']['hall-x']['hall-x_block_room_bed']))
         stats = utils.getBedStatistics()
         self.assertEqual(stats,
@@ -266,7 +309,7 @@
              )
         self.app[
-            'hostels']['hall-x']['hall_block_room_bed'].owner = self.student_id
+            'hostels']['hall-x']['hall-x_block_room_bed'].owner = self.student_id
         notify(grok.ObjectModifiedEvent(
-            self.app['hostels']['hall-x']['hall_block_room_bed']))
+            self.app['hostels']['hall-x']['hall-x_block_room_bed']))
         stats = utils.getBedStatistics()
         self.assertEqual(stats,
@@ -299,9 +342,9 @@
         results = [x for x in results] # Turn results generator into list
         assert len(results) == 1
-        assert results[0] is self.app['hostels']['hall-x']['hall_block_room_bed']
+        assert results[0] is self.app['hostels']['hall-x']['hall-x_block_room_bed']
 
     def test_search_by_owner(self):
         # We can find a certain bed
-        myobj = self.app['hostels']['hall-x']['hall_block_room_bed']
+        myobj = self.app['hostels']['hall-x']['hall-x_block_room_bed']
         myobj.owner = u'abc'
         notify(grok.ObjectModifiedEvent(myobj))
@@ -310,5 +353,5 @@
         results = [x for x in results] # Turn results generator into list
         assert len(results) == 1
-        assert results[0] is self.app['hostels']['hall-x']['hall_block_room_bed']
+        assert results[0] is self.app['hostels']['hall-x']['hall-x_block_room_bed']
 
 class HostelsUITests(HostelsFullSetup):
@@ -622,6 +665,6 @@
             result,
             'bed_id,bed_number,bed_type,owner,hall,block,room,bed,'
-            'special_handling,sex,bt\r\nhall_block_room_bed,1,regular_male_fr,,'
-            'hall,block,room,bed,regular,male,fr\r\n'
+            'special_handling,sex,bt\r\nhall-x_block_room_bed,1,regular_male_fr,,'
+            'hall-x,block,room,bed,regular,male,fr\r\n'
             )
         return
