Changeset 9217


Ignore:
Timestamp:
21 Sep 2012, 11:21:05 (12 years ago)
Author:
uli
Message:

Merge changes from uli-async-update back into trunk.

Location:
main/waeup.kofa/trunk
Files:
16 added
40 edited

Legend:

Unmodified
Added
Removed
  • main/waeup.kofa/trunk/buildout-zeo.cfg

    r8662 r9217  
    5050    </filestorage>
    5151  </blobstorage>
     52  <filestorage async>
     53    path ${zope_conf:filestorage}/Data.async.fs
     54  </filestorage>
    5255  <eventlog>
    5356    # This sets up logging to a file.
  • main/waeup.kofa/trunk/etc/debug.ini.in

    r8796 r9217  
    55
    66[loggers]
    7 keys = root, wsgi
     7keys = root, wsgi, async, async_trace
    88
    99[handlers]
    10 keys = console, accesslog
     10keys = console, accesslog, asynclog, async_tracelog
    1111
    1212[formatters]
     
    1818[formatter_accesslog]
    1919format = %(message)s
     20
     21[handler_asynclog]
     22class = FileHandler
     23args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
     24        'a')
     25level = INFO
     26formatter = generic
     27
     28[handler_async_tracelog]
     29class = FileHandler
     30args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
     31        'a')
     32level = INFO
     33formatter = generic
    2034
    2135[handler_console]
     
    4054handlers = accesslog
    4155qualname = wsgi
     56propagate = 0
     57
     58[logger_async]
     59level = INFO
     60handlers = asynclog
     61qualname = zc.async
     62propagate = 0
     63
     64[logger_async_trace]
     65level = INFO
     66handlers = async_tracelog
     67qualname = zc.async.trace
    4268propagate = 0
    4369
     
    6692# set the name of the zope.conf file
    6793zope_conf = %(here)s/zope.conf
     94env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
  • main/waeup.kofa/trunk/etc/deploy.ini.in

    r8796 r9217  
    55
    66[loggers]
    7 keys = root, wsgi
     7keys = root, wsgi, async, async_trace
    88
    99[handlers]
    10 keys = console, accesslog
     10keys = console, accesslog, asynclog, async_tracelog
    1111
    1212[formatters]
     
    2323propagate = 0
    2424
     25[logger_async]
     26level = INFO
     27handlers = asynclog
     28qualname = zc.async
     29propagate = 0
     30
     31[logger_async_trace]
     32level = INFO
     33handlers = async_tracelog
     34qualname = zc.async.trace
     35propagate = 0
     36
    2537[handler_console]
    2638class = StreamHandler
     
    3547level = INFO
    3648formatter = accesslog
     49
     50[handler_asynclog]
     51class = FileHandler
     52args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
     53        'a')
     54level = INFO
     55formatter = generic
     56
     57[handler_async_tracelog]
     58class = FileHandler
     59args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
     60        'a')
     61level = INFO
     62formatter = generic
    3763
    3864[formatter_generic]
     
    5985# set the name of the zope.conf file
    6086zope_conf = %(here)s/zope.conf
     87env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
  • main/waeup.kofa/trunk/etc/profile.ini.in

    r8796 r9217  
    66
    77[loggers]
    8 keys = root, wsgi
     8keys = root, wsgi, async, async_trace
    99
    1010[handlers]
    11 keys = console, accesslog
     11keys = console, accesslog, asynclog, async_tracelog
    1212
    1313[formatters]
     
    2424propagate = 0
    2525
     26[logger_async]
     27level = INFO
     28handlers = asynclog
     29qualname = zc.async
     30propagate = 0
     31
     32[logger_async_trace]
     33level = INFO
     34handlers = async_tracelog
     35qualname = zc.async.trace
     36propagate = 0
     37
    2638[handler_console]
    2739class = StreamHandler
     
    3648level = INFO
    3749formatter = accesslog
     50
     51[handler_asynclog]
     52class = FileHandler
     53args = (os.path.join(r'${zope_conf:logfiles}', 'async.log'),
     54        'a')
     55level = INFO
     56formatter = generic
     57
     58[handler_async_tracelog]
     59class = FileHandler
     60args = (os.path.join(r'${zope_conf:logfiles}', 'async_trace.log'),
     61        'a')
     62level = INFO
     63formatter = generic
    3864
    3965[formatter_generic]
     
    6591# set the name of the zope.conf file
    6692zope_conf = %(here)s/zope.conf
     93env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
  • main/waeup.kofa/trunk/etc/site.zcml.in

    r8796 r9217  
    55  <include package="${kofa_params:devel_pkg}" />
    66  <include package="${kofa_params:devel_pkg}" file="mail.zcml" />
     7  <!-- install job container
     8
     9       install a job container for asynchronous jobs. Pick one of the
     10       two possibilites: 'single' for installation in the one big ZODB
     11       or 'multidb' for setup with a dedicated ZODB for job handling.
     12  -->
     13  <!-- include package="waeup.kofa" file="async_single.zcml" / -->
     14  <include package="waeup.kofa" file="async_multidb.zcml" />
     15
     16  <!-- install dispatcher and other needed async components
     17
     18       install other needed components for asynchronous jobs. Pick one
     19       of the two possibilities: 'basic_dispatcher' for single ZODB
     20       setup, 'multidb_dispatcher' for setup with a dedicated ZODB for
     21       job handling. Make sure the setting matches the setting picked
     22       above. -->
     23  <!-- include package="zc.async" file="basic_dispatcher_policy.zcml" / -->
     24  <include package="zc.async" file="multidb_dispatcher_policy.zcml" />
    725
    826  <!-- Where should the datacenter reside by default? -->
  • main/waeup.kofa/trunk/etc/zeo1.ini.in

    r8796 r9217  
    55
    66[loggers]
    7 keys = root, wsgi
     7keys = root, wsgi, async, async_trace
    88
    99[handlers]
    10 keys = console, accesslog
     10keys = console, accesslog, asynclog, async_tracelog
    1111
    1212[formatters]
     
    2323propagate = 0
    2424
     25[logger_async]
     26level = INFO
     27handlers = asynclog
     28qualname = zc.async
     29propagate = 0
     30
     31[logger_async_trace]
     32level = INFO
     33handlers = async_tracelog
     34qualname = zc.async.trace
     35propagate = 0
     36
    2537[handler_console]
    2638class = StreamHandler
     
    3547level = INFO
    3648formatter = accesslog
     49
     50[handler_asynclog]
     51class = FileHandler
     52args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1_async.log'),
     53        'a')
     54level = INFO
     55formatter = generic
     56
     57[handler_async_tracelog]
     58class = FileHandler
     59args = (os.path.join(r'${zope_conf:logfiles}', 'zeo1_async_trace.log'),
     60        'a')
     61level = INFO
     62formatter = generic
    3763
    3864[formatter_generic]
     
    6187# set the name of the zope.conf file
    6288zope_conf = %(here)s/zope_zeo1.conf
     89env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid1.txt
  • main/waeup.kofa/trunk/etc/zeo2.ini.in

    r8796 r9217  
    55
    66[loggers]
    7 keys = root, wsgi
     7keys = root, wsgi, async, async_trace
    88
    99[handlers]
    10 keys = console, accesslog
     10keys = console, accesslog, asynclog, async_tracelog
    1111
    1212[formatters]
     
    2929formatter = generic
    3030
     31[logger_async]
     32level = INFO
     33handlers = asynclog
     34qualname = zc.async
     35propagate = 0
     36
     37[logger_async_trace]
     38level = INFO
     39handlers = async_tracelog
     40qualname = zc.async.trace
     41propagate = 0
     42
    3143[handler_accesslog]
    3244class = FileHandler
     
    3547level = INFO
    3648formatter = accesslog
     49
     50[handler_asynclog]
     51class = FileHandler
     52args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2_async.log'),
     53        'a')
     54level = INFO
     55formatter = generic
     56
     57[handler_async_tracelog]
     58class = FileHandler
     59args = (os.path.join(r'${zope_conf:logfiles}', 'zeo2_async_trace.log'),
     60        'a')
     61level = INFO
     62formatter = generic
    3763
    3864[formatter_generic]
     
    5480use = egg:Paste#http
    5581host = ${kofa_params:host}
    56 port = ${kofa_params:zeo1_port}
     82port = ${kofa_params:zeo2_port}
    5783threadpool_workers = ${kofa_params:threadpool_workers}
    5884use_threadpool = True
     
    6187# set the name of the zope.conf file
    6288zope_conf = %(here)s/zope_zeo2.conf
     89env_vars = ZC_ASYNC_UUID ${buildout:directory}/var/uuid2.txt
  • main/waeup.kofa/trunk/etc/zope.conf.in

    r5495 r9217  
    2828</zodb>
    2929
     30<zodb async>
     31  <filestorage>
     32    # create true
     33    path ${zope_conf:filestorage}/Data.async.fs
     34  </filestorage>
     35</zodb>
     36
    3037<eventlog>
    3138  # This sets up logging to a file.
  • main/waeup.kofa/trunk/etc/zope_zeo1.conf.in

    r8122 r9217  
    2626    server localhost:8100
    2727    storage 1
    28     # ZEO client cache, in bytes
     28     # ZEO client cache, in bytes
    2929    cache-size 20MB
    3030    # Uncomment to have a persistent disk cache
     
    6161</zodb>
    6262
     63<zodb async>
     64  <zeoclient>
     65    server localhost:8100
     66    storage async
     67    name async
     68    # ZEO client cache, in bytes
     69    cache-size 20MB
     70    # Uncomment to have a persistent disk cache
     71    client ${zope_conf_zeo_1:filestorage}/zeo1_async
     72  </zeoclient>
     73</zodb>
     74
    6375<eventlog>
    6476  # This sets up logging to a file.
  • main/waeup.kofa/trunk/etc/zope_zeo2.conf.in

    r8122 r9217  
    6161</zodb>
    6262
     63<zodb async>
     64  <zeoclient>
     65    server localhost:8100
     66    storage async
     67    name async
     68    # ZEO client cache, in bytes
     69    cache-size 20MB
     70    # Uncomment to have a persistent disk cache
     71    client ${zope_conf_zeo_2:filestorage}/zeo2_async
     72  </zeoclient>
     73</zodb>
     74
     75
    6376<eventlog>
    6477  # This sets up logging to a file.
  • main/waeup.kofa/trunk/setup.py

    r8492 r9217  
    3737    'zope.sendmail',
    3838    'ulif.loghandlers',
     39    'zc.async[z3]',
    3940    ],
    4041
     
    122123      analyze = waeup.kofa.maintenance:db_analyze
    123124      [paste.app_factory]
    124       main = grokcore.startup:application_factory
    125       debug = grokcore.startup:debug_application_factory
     125      main = waeup.kofa.startup:env_app_factory
     126      debug = waeup.kofa.startup:env_debug_app_factory
    126127
    127128      """,
  • main/waeup.kofa/trunk/src/waeup/kofa/accesscodes/browser.py

    r8387 r9217  
    2323from hurry.workflow.interfaces import InvalidTransitionError
    2424from waeup.kofa.browser.resources import datatable
    25 from waeup.kofa.browser import KofaPage, KofaAddFormPage, NullValidator
     25from waeup.kofa.browser.layout import KofaPage, KofaAddFormPage, NullValidator
    2626from waeup.kofa.browser.breadcrumbs import Breadcrumb
    2727from waeup.kofa.browser.viewlets import (
  • main/waeup.kofa/trunk/src/waeup/kofa/app.py

    r8846 r9217  
    1818import grok
    1919from zope.authentication.interfaces import IAuthentication
    20 from zope.component import getUtilitiesFor
     20from zope.component import getUtility, getUtilitiesFor
    2121from zope.component.interfaces import ObjectEvent
    2222from zope.pluggableauth import PluggableAuthentication
    2323from waeup.kofa.authentication import setup_authentication
    2424from waeup.kofa.datacenter import DataCenter
    25 from waeup.kofa.students.container import StudentsContainer
    26 from waeup.kofa.hostels.container import HostelsContainer
    2725from waeup.kofa.mandates.container import MandatesContainer
    2826from waeup.kofa.interfaces import (
    29     IUniversity, IKofaPluggable, IObjectUpgradeEvent, )
     27    IUniversity, IKofaPluggable, IObjectUpgradeEvent, IJobManager,
     28    VIRT_JOBS_CONTAINER_NAME)
    3029from waeup.kofa.userscontainer import UsersContainer
    3130from waeup.kofa.utils.logger import Logger
     
    5554        the like.
    5655        """
     56        from waeup.kofa.students.container import StudentsContainer
     57        from waeup.kofa.hostels.container import HostelsContainer
     58
    5759        self['users'] = UsersContainer()
    5860        self['datacenter'] = DataCenter()
     
    6971            plugin.setup(self, name, self.logger)
    7072        return
     73
     74    def traverse(self, name):
     75        if name == VIRT_JOBS_CONTAINER_NAME:
     76            return getUtility(IJobManager)
     77        return None
    7178
    7279    def updatePlugins(self):
  • main/waeup.kofa/trunk/src/waeup/kofa/applicants/applicant.py

    r9121 r9217  
    2929from zope.schema.interfaces import RequiredMissing, ConstraintNotSatisfied
    3030from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
    31 from waeup.kofa.app import University
    3231from waeup.kofa.image import KofaImageFile
    3332from waeup.kofa.imagestorage import DefaultFileStoreHandler
    3433from waeup.kofa.interfaces import (
    3534    IObjectHistory, IFileStoreHandler, IFileStoreNameChooser, IKofaUtils,
    36     IExtFileStore, IPDF, IUserAccount)
     35    IExtFileStore, IPDF, IUserAccount, IUniversity)
    3736from waeup.kofa.interfaces import MessageFactory as _
    3837from waeup.kofa.students.vocabularies import RegNumNotInSource
     
    189188    """A catalog indexing :class:`Applicant` instances in the ZODB.
    190189    """
    191     grok.site(University)
     190    grok.site(IUniversity)
    192191    grok.name('applicants_catalog')
    193192    grok.context(IApplicant)
  • main/waeup.kofa/trunk/src/waeup/kofa/applicants/browser.py

    r9178 r9217  
    3838    INITIALIZED, STARTED, PAID, SUBMITTED, ADMITTED)
    3939from waeup.kofa.browser import (
    40     KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
     40#    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
    4141    DEFAULT_PASSPORT_IMAGE_PATH)
     42from waeup.kofa.browser.layout import (
     43    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage)
    4244from waeup.kofa.browser.interfaces import ICaptchaManager
    4345from waeup.kofa.browser.breadcrumbs import Breadcrumb
  • main/waeup.kofa/trunk/src/waeup/kofa/applicants/interfaces.py

    r9115 r9217  
    2727from zope.schema.interfaces import (
    2828    ValidationError, ISource, IContextSourceBinder)
     29from waeup.kofa.browser.interfaces import IApplicantBase
    2930from waeup.kofa.schema import TextLineChoice, FormattedDate
    3031from waeup.kofa.interfaces import (
     
    311312    'year'].order =  IApplicantsContainer['year'].order
    312313
    313 class IApplicantBaseData(IKofaObject):
     314class IApplicantBaseData(IApplicantBase):
    314315    """The data for an applicant.
    315316
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/__init__.py

    r7819 r9217  
    11import os
    22
    3 from waeup.kofa.browser.layout import (
    4     KofaPage, KofaForm, KofaLayout, KofaDisplayFormPage, KofaEditFormPage,
    5     KofaAddFormPage, NullValidator)
    6 from waeup.kofa.browser.pages import ContactAdminForm
     3#from waeup.kofa.browser.layout import (
     4#    KofaPage, KofaForm, KofaLayout, KofaDisplayFormPage, KofaEditFormPage,
     5#    KofaAddFormPage, NullValidator)
     6#from waeup.kofa.browser.pages import ContactAdminForm
    77
    88IMAGE_PATH = os.path.join(
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/breadcrumbs.py

    r7819 r9217  
    2626    IConfigurationContainer, ISessionConfiguration)
    2727from waeup.kofa.interfaces import MessageFactory as _
    28 from waeup.kofa.browser import interfaces
    29 from waeup.kofa.browser.interfaces import (IBreadcrumb,
    30     IBreadcrumbIgnorable, IBreadcrumbContainer)
     28from waeup.kofa.browser.interfaces import (
     29    IBreadcrumb, IBreadcrumbIgnorable, IBreadcrumbContainer, IKofaObject,
     30    IUniversity, IFacultiesContainer, IUsersContainer, IDataCenter, IFaculty,
     31    IDepartment, ICourse, ICertificate, ICoursesContainer, ICertificateCourse,
     32    ICertificatesContainer,
     33    )
    3134
    3235class Breadcrumb(grok.Adapter):
     
    3437    """
    3538    grok.provides(IBreadcrumb)
    36     grok.context(interfaces.IKofaObject)
     39    grok.context(IKofaObject)
    3740    grok.name('index')
    3841
     
    8790    """A breadcrumb for university index pages.
    8891    """
    89     grok.context(interfaces.IUniversity)
     92    grok.context(IUniversity)
    9093    title = _(u'Home')
    9194    parent = None
     
    100103    itself is bound to.
    101104    """
    102     grok.context(interfaces.IUniversity)
     105    grok.context(IUniversity)
    103106    grok.name('manage')
    104107    title = _(u'Portal Settings')
     
    113116    """A breadcrumb for faculty containers.
    114117    """
    115     grok.context(interfaces.IFacultiesContainer)
     118    grok.context(IFacultiesContainer)
    116119    title = _(u'Academics')
    117120
     
    119122    """A breadcrumb for administration areas of University instances.
    120123    """
    121     grok.context(interfaces.IUniversity)
     124    grok.context(IUniversity)
    122125    grok.name('administration')
    123126    title = _(u'Administration')
     
    145148    """A breadcrumb for user containers.
    146149    """
    147     grok.context(interfaces.IUsersContainer)
     150    grok.context(IUsersContainer)
    148151    title = _(u'Portal Users')
    149152    parent_viewname = 'administration'
     
    152155    """A breadcrumb for data centers.
    153156    """
    154     grok.context(interfaces.IDataCenter)
     157    grok.context(IDataCenter)
    155158    title = _(u'Data Center')
    156159    parent_viewname = 'administration'
     
    159162    """A breadcrumb for faculties.
    160163    """
    161     grok.context(interfaces.IFaculty)
     164    grok.context(IFaculty)
    162165
    163166    @property
     
    168171    """A breadcrumb for departments.
    169172    """
    170     grok.context(interfaces.IDepartment)
     173    grok.context(IDepartment)
    171174
    172175class CourseBreadcrumb(FacultyBreadcrumb):
    173176    """A breadcrumb for courses.
    174177    """
    175     grok.context(interfaces.ICourse)
     178    grok.context(ICourse)
    176179
    177180class CertificateBreadcrumb(FacultyBreadcrumb):
    178181    """A breadcrumb for certificates.
    179182    """
    180     grok.context(interfaces.ICertificate)
     183    grok.context(ICertificate)
    181184
    182185class CoursesContainerBreadcrumb(Breadcrumb):
    183186    """ We don't want course container breadcrumbs.
    184187    """
    185     grok.context(interfaces.ICoursesContainer)
     188    grok.context(ICoursesContainer)
    186189    grok.implements(IBreadcrumbIgnorable)
    187190
     
    189192    """ We don't want course container breadcrumbs.
    190193    """
    191     grok.context(interfaces.ICertificatesContainer)
     194    grok.context(ICertificatesContainer)
    192195    grok.implements(IBreadcrumbIgnorable)
    193196
     
    195198    """ We don't want course container breadcrumbs.
    196199    """
    197     grok.context(interfaces.ICertificateCourse)
     200    grok.context(ICertificateCourse)
    198201    @property
    199202    def title(self):
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/captcha.py

    r8127 r9217  
    2828from zope.interface import Interface
    2929from zope.publisher.interfaces.http import IHTTPRequest
    30 from waeup.kofa.browser import KofaPage, resources
     30from waeup.kofa.browser import resources
     31from waeup.kofa.browser.layout import KofaPage
    3132from waeup.kofa.browser.interfaces import (
    3233    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/interfaces.py

    r8257 r9217  
    2121from zope.interface import Interface, Attribute
    2222from waeup.kofa.interfaces import (
    23     IKofaObject, IUniversity, IUsersContainer, IDataCenter)
     23    IKofaObject, IUniversity, IUsersContainer, IDataCenter, validate_email)
     24from waeup.kofa.interfaces import MessageFactory as _
    2425from waeup.kofa.university.interfaces import (
    2526    IFacultiesContainer, IFaculty, IFacultyAdd, IDepartment, IDepartmentAdd,
     
    196197        If no `title` is given, nothing will be rendered.
    197198        """
     199
     200class IChangePassword(IKofaObject):
     201    """Interface needed for change pasword page.
     202
     203    """
     204    identifier = schema.TextLine(
     205        title = _(u'Unique Identifier'),
     206        description = _(
     207            u'User Name, Student or Applicant Id, Matriculation or '
     208            u'Registration Number'),
     209        required = True,
     210        readonly = False,
     211        )
     212
     213    email = schema.ASCIILine(
     214        title = _(u'Email Address'),
     215        required = True,
     216        constraint=validate_email,
     217        )
     218
     219class IStudentNavigationBase(IKofaObject):
     220    """Objects that provide student navigation (whatever it is) should
     221       implement this interface.
     222    """
     223    student = Attribute('''Some student object that has a '''
     224                        ''' `display_fullname` attribute.''')
     225
     226class IApplicantBase(IKofaObject):
     227    """Some Applicant.
     228    """
     229    display_fullname = Attribute('''Fullname.''')
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/layout.py

    r9088 r9217  
    3636from waeup.kofa.interfaces import MessageFactory as _
    3737from waeup.kofa.utils.helpers import to_timezone
    38 from waeup.kofa.browser.interfaces import ITheme
     38from waeup.kofa.browser.interfaces import (
     39    ITheme, IStudentNavigationBase, IApplicantBase)
    3940from waeup.kofa.browser.theming import get_all_themes, KofaThemeBase
    40 from waeup.kofa.students.interfaces import IStudentNavigation
    41 from waeup.kofa.applicants.interfaces import IApplicant
    4241from waeup.kofa.authentication import get_principal_role_manager
    4342
     
    288287        """Return the student name.
    289288        """
    290         if IStudentNavigation.providedBy(self.context):
     289        if IStudentNavigationBase.providedBy(self.context):
    291290            return self.context.student.display_fullname
    292291        return
     
    295294        """Return the applicant name.
    296295        """
    297         if IApplicant.providedBy(self.context):
     296        if IApplicantBase.providedBy(self.context):
     297            # XXX: shouldn't that be `display_fullname???`
    298298            return self.context.fullname
    299299        return
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/pages.py

    r9172 r9217  
    1818""" Viewing components for Kofa objects.
    1919"""
    20 import copy
    2120import csv
    2221import grok
     
    2423import re
    2524import sys
    26 import time
    27 import re
    2825from urllib import urlencode
    2926from zope import schema
     
    3532    getUtilitiesFor,
    3633    )
    37 #from zope.component.interfaces import Invalid
    3834from zope.event import notify
    39 from zope.securitypolicy.interfaces import (
    40     IPrincipalRoleManager, IPrincipalRoleMap)
     35from zope.securitypolicy.interfaces import IPrincipalRoleManager
    4136from zope.session.interfaces import ISession
    4237from zope.password.interfaces import IPasswordManager
    43 from waeup.kofa.browser import (
     38from waeup.kofa.browser.layout import (
    4439    KofaPage, KofaForm, KofaEditFormPage, KofaAddFormPage,
    4540    KofaDisplayFormPage, NullValidator)
     
    4843    IDepartment, IDepartmentAdd, ICourse, ICourseAdd, ICertificate,
    4944    ICertificateAdd, ICertificateCourse, ICertificateCourseAdd,
    50     ICaptchaManager)
     45    ICaptchaManager, IChangePassword)
    5146from waeup.kofa.browser.layout import jsaction, action, UtilityView
    52 from waeup.kofa.browser.resources import warning, datepicker, tabs, datatable
     47from waeup.kofa.browser.resources import (
     48    warning, tabs, datatable)
    5349from waeup.kofa.interfaces import MessageFactory as _
    5450from waeup.kofa.interfaces import(
     
    5652    IKofaXMLImporter, IKofaXMLExporter, IBatchProcessor,
    5753    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
    58     ISessionConfiguration, ISessionConfigurationAdd,
    59     IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,
    60     IChangePassword)
     54    ISessionConfiguration, ISessionConfigurationAdd, IJobManager,
     55    IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,)
    6156from waeup.kofa.permissions import (
    6257    get_users_with_local_roles, get_all_roles, get_all_users)
     58
    6359from waeup.kofa.students.catalog import search as searchstudents
    6460from waeup.kofa.university.catalog import search
    6561from waeup.kofa.university.vocabularies import course_levels
    6662from waeup.kofa.authentication import LocalRoleSetEvent
    67 #from waeup.kofa.widgets.restwidget import ReSTDisplayWidget
    6863from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
    69 from waeup.kofa.authentication import get_principal_role_manager
    7064from waeup.kofa.utils.helpers import get_user_account
    7165from waeup.kofa.mandates.mandate import PasswordMandate
     
    797791    def delFiles(self, **data):
    798792        form = self.request.form
    799         logger = self.context.logger
    800793        if form.has_key('val_id'):
    801794            child_id = form['val_id']
     
    11961189        return False
    11971190
    1198     @property
    1199     def nextstep(self):
    1200         return self.url(self.context, '@@import4')
    1201 
    12021191    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
    12031192        datatable.need()
     
    13901379    label = _('Download portal data as CSV file')
    13911380    pnav = 0
    1392     export_button = _(u'Download')
     1381    export_button = _(u'Create CSV file')
     1382    _running_exports = None
    13931383
    13941384    def getExporters(self):
     
    13981388        return sorted(title_name_tuples)
    13991389
    1400     def update(self, export=None, exporter=None):
    1401         if None in (export, exporter):
    1402             return
    1403         self.redirect(
    1404             self.url(self.context, 'export.csv') + '?exporter=%s' % exporter)
     1390    def job_finished(self, status):
     1391        return status == 'completed'
     1392
     1393    def getRunningExports(self):
     1394        """Returns running exports as list of tuples.
     1395
     1396        Only exports triggered by the current user (identified by
     1397        principal.id) are returned.
     1398
     1399        Each tuple has the form (<STATUS>, <STATUS_TITLE>, <EXPORTER_NAME>).
     1400
     1401        ``STATUS``:
     1402           the status as machine readable string (something like
     1403           ``'completed'``)
     1404
     1405        ``STATUS_TITLE``:
     1406           status of export as translated string.
     1407
     1408        ``EXPORTER_NAME``:
     1409           string representing the exporter title used when triggering
     1410           the export job.
     1411        """
     1412        if self._running_exports is None:
     1413            self._running_exports = self._getRunningExports()
     1414        return self._running_exports
     1415
     1416    def _getRunningExports(self):
     1417        result = self.context.get_export_jobs_status(self.user_id)
     1418        return result
     1419
     1420    def update(self, export=None, start_export=None, exporter=None,
     1421               discard=None, download=None):
     1422        self.user_id = self.request.principal.id
     1423        if discard:
     1424            myjobs = self.context.get_running_export_jobs(self.user_id)
     1425            for entry in myjobs:
     1426                self.context.delete_export_entry(entry)
     1427                self.flash(_('Discarded export result'))
     1428            return
     1429        if download:
     1430            myjobs = self.context.get_running_export_jobs(self.user_id)
     1431            if not len(myjobs):
     1432                self.flash(_('This export was already discarded.'))
     1433                return
     1434            job_id = myjobs[0][0]
     1435            self.redirect(
     1436                self.url(self.context, 'export.csv') + '?job_id=%s' % job_id)
     1437            return
     1438        if None in (start_export, exporter):
     1439            return
     1440        job_id = self.context.start_export_job(
     1441            exporter, self.request.principal.id)
     1442        self.redirect(self.url(self.context, 'export'))
    14051443        return
    14061444
     
    14101448    grok.require('waeup.manageDataCenter')
    14111449
    1412     def render(self, exporter=None):
    1413         if exporter is None:
    1414             return
    1415         ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
    1416         self.context.logger.info(
    1417             '%s - exported: %s' % (ob_class, exporter))
    1418         exporter = getUtility(ICSVExporter, name=exporter)
    1419         csv_data = exporter.export_all(grok.getSite())
    1420         #csv_data.seek(0)
     1450    def render(self, exporter=None, job_id=None):
     1451
     1452        manager = getUtility(IJobManager)
     1453        job = manager.get(job_id)
     1454        if job is None:
     1455            return
     1456        if hasattr(job.result, 'traceback'):
     1457            # XXX: Some error happened. Do something more approriate here...
     1458            return
     1459        path = job.result
     1460        if not os.path.exists(path):
     1461            # XXX: Do something more appropriate here...
     1462            return
     1463        result = open(path, 'rb').read()
     1464        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
     1465        filename = "%s_%s" % (acronym, os.path.basename(path))
    14211466        self.response.setHeader(
    14221467            'Content-Type', 'text/csv; charset=UTF-8')
    1423         acronym = grok.getSite()['configuration'].acronym.replace(' ','')
    1424         filename = "%s%s.csv" % (acronym, exporter.title.title().replace(' ',''))
    14251468        self.response.setHeader(
    1426             'Content-Disposition:', 'attachment; filename="%s' % filename)
    1427         return csv_data
     1469            'Content-Disposition', 'attachment; filename="%s' % filename)
     1470        # remove job and running_exports entry from context
     1471        self.context.delete_export_entry(
     1472            self.context.entry_from_job_id(job_id))
     1473        return result
    14281474
    14291475class ExportXMLPage(grok.View):
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/resources.py

    r8126 r9217  
    302302    waeup_kofa, 'recaptcha_white.js'
    303303    )
     304
     305#: A page reloader (reloads after 3 secs)
     306page_reloader = ResourceInclusion(
     307    waeup_kofa, 'page_reloader.js', depends=[jquery])
     308
     309#: Disables page reloader (which is triggered by a JavaScript timer).
     310page_not_reloader = ResourceInclusion(
     311    waeup_kofa, 'page_not_reloader.js', depends=[jquery])
     312
     313loadbar = ResourceInclusion(
     314    waeup_kofa, 'loadbar.js', depends=[jquery])
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/templates/datacenterexportpage.pt

    r7974 r9217  
    11<div i18n:domain="waeup.kofa" class="row">
    2   <div class="span4 columns">
    32  <div class="span8 columns">
    4     <p i18n:translate="">Here you can download parts of portal data.</p>
    5     <p i18n:translate="">
    6       Please pick the type of objects you want to export from the
    7       selection below.
    8     </p>
    9   </div>
     3    <div class="span8 columns">
     4      <p i18n:translate="">
     5        Here you can create CSV files from parts of portal data.
     6      </p>
     7      <p i18n:translate="">
     8        Please pick the type of objects you want to export from the
     9        selection below.
     10      </p>
     11      <p i18n:translate="">
     12        The file will be generated and then be made available for you
     13        to download. You must download or discard any existing export
     14        file before creating a new one.
     15      </p>
     16    </div>
    1017
    11   <form  method="POST">
    12     <fieldset>
     18    <form  method="POST">
     19      <fieldset>
    1320        <div class="clearfix">
    14           <label for="exporter">Data Type:</label>
    15           <div class="input">
    16             <select name="exporter">
    17               <span tal:repeat="items view/getExporters" tal:omit-tag="">
    18                 <option
    19                     tal:define="name python: items[1]; title python: items[0]"
    20                     tal:attributes="value name">
    21                   <span tal:replace="title">TITLE</span>
    22                 </option>
     21          <label for="running_exports"
     22                 tal:condition="view/getRunningExports">Running Export:</label>
     23          <div class="input span6">
     24            <div tal:repeat="items view/getRunningExports">
     25              <div>
     26                Data type:
     27                <span tal:content="python: items[2]">exporter</span><br />
     28                Status:
     29                <span tal:content="python: items[1]">status</span><br />
     30                <br /><br />
     31              </div>
     32              <div>
     33                <span tal:condition="not: python: view.job_finished(items[0])">
     34                  <img src=""
     35                       tal:attributes="src static/ajax-loader.gif"
     36                       alt="Loading..."
     37                       class="spinner" />
     38                  <input type="submit" name="reload" value="Reload"
     39                         class="btn primary" />
     40                </span>
     41                <div>
     42                  <span tal:condition="python: view.job_finished(items[0])">
     43                    <input type="submit" name="download" value="Download"
     44                           class="btn primary" />
     45                    <input type="submit" name="discard" value="Discard"
     46                         class="btn secondary" />
     47                  </span>
     48                </div>
     49              </div>
     50            </div>
     51
     52            <div>
     53              <tal:loading_bar content="structure provider:loadingbar" />
     54            </div>
     55          </div>
     56
     57          <div class="clearfix"
     58               tal:condition="not: view/getRunningExports">
     59            <label for="exporter">Data Type:</label>
     60            <div class="input">
     61              <select name="exporter">
     62                <span tal:repeat="items view/getExporters" tal:omit-tag="">
     63                  <option
     64                      tal:define="name python: items[1]; title python: items[0]"
     65                      tal:attributes="value name">
     66                    <span tal:replace="title">TITLE</span>
     67                  </option>
     68                </span>
     69              </select>
     70              <span class="help-inline" i18n:translate="">
     71                Type of objects to export
    2372              </span>
    24             </select>
    25             <span class="help-inline" i18n:translate="">
    26               Type of objects to export
    27             </span>
     73            </div>
    2874          </div>
    29         </div>
    30         <div class="input">
    31           <input i18n:translate="" type="submit" class="btn primary"
    32                  name="export" tal:attributes="value view/export_button" />
     75          <div class="input"
     76               tal:condition="not: view/getRunningExports">
     77            <input i18n:translate="" type="submit" class="btn primary"
     78                   name="start_export"
     79                   tal:attributes="value view/export_button" />
     80          </div>
    3381        </div>
    3482      </fieldset>
  • main/waeup.kofa/trunk/src/waeup/kofa/browser/tests/test_browser.py

    r9034 r9217  
    2121import shutil
    2222import tempfile
    23 import pytz
    24 from datetime import datetime, timedelta
    25 from StringIO import StringIO
    2623import os
    27 import grok
    28 from zope.event import notify
    29 from zope.component import createObject, queryUtility
     24from zc.async.testing import wait_for_result
     25from zope.component import createObject, getUtility
    3026from zope.component.hooks import setSite, clearSite
    31 from zope.catalog.interfaces import ICatalog
    3227from zope.security.interfaces import Unauthorized
    33 from zope.securitypolicy.interfaces import IPrincipalRoleManager
    3428from zope.testbrowser.testing import Browser
    35 from hurry.workflow.interfaces import IWorkflowInfo, IWorkflowState
    3629from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
    3730from waeup.kofa.app import University
     31from waeup.kofa.interfaces import IJobManager
     32from waeup.kofa.tests.test_async import FunctionalAsyncTestCase
    3833from waeup.kofa.university.faculty import Faculty
    3934from waeup.kofa.university.department import Department
     
    106101        shutil.rmtree(self.dc_root)
    107102
    108 
    109103class DataCenterUITests(UniversitySetup):
    110104    # Tests for DataCenter class views and pages
     
    149143        return
    150144
    151     def test_export(self):
     145
     146class DataCenterUIExportTests(UniversitySetup, FunctionalAsyncTestCase):
     147    # Tests for DataCenter class views and pages
     148
     149    layer = FunctionalLayer
     150
     151    def wait_for_export_job_completed(self):
     152        # helper function waiting until the current export job is completed
     153        manager = getUtility(IJobManager)
     154        job_id = self.app['datacenter'].running_exports[0][0]
     155        job = manager.get(job_id)
     156        wait_for_result(job)
     157        return job_id
     158
     159    def stored_in_datacenter(self, job_id):
     160        # tell whether job_id is stored in datacenter's running jobs list
     161        for entry in list(self.app['datacenter'].running_exports):
     162            if entry[0] == job_id:
     163                return True
     164        return False
     165
     166    def test_export_start(self):
     167        # we can trigger export file creation
    152168        self.browser.addHeader('Authorization', 'Basic mgr:mgrpw')
    153169        self.browser.open(self.datacenter_path)
     
    156172        self.browser.getLink("Export data").click()
    157173        self.browser.getControl(name="exporter").value = ['faculties']
     174        self.browser.getControl("Create CSV file").click()
     175        self.assertEqual(self.browser.headers['Status'], '200 Ok')
     176        return
     177
     178    def test_export_download(self):
     179        # we can download a generated export result
     180        self.test_export_start()
     181        # while the export file is created, we get a reload button
     182        # (or a loading bar if javascript is enabled)...
     183        self.browser.getControl("Reload").click()
     184        # ...which is displayed as long as the job is not finished.
     185        # When the job is finished and we reload the page...
     186        job_id = self.wait_for_export_job_completed()
     187        try:
     188            self.browser.getControl("Reload").click()
     189        except LookupError:
     190            # if the job completed very fast, we will get the download
     191            # link immediately
     192            pass
     193        # ...we can download the result
    158194        self.browser.getControl("Download").click()
    159         self.assertEqual(self.browser.headers['Status'], '200 Ok')
    160         self.assertEqual(self.browser.headers['Content-Type'],
     195        self.assertEqual(self.browser.headers['content-type'],
    161196                         'text/csv; charset=UTF-8')
    162         self.assertTrue ('WAeUP.KofaFaculties.csv' in
    163             self.browser.headers['content-disposition'])
     197        self.assertEqual(self.browser.headers['content-disposition'],
     198                         'attachment; filename="WAeUP.Kofa_faculties.csv')
    164199        self.assertEqual(self.browser.contents,
    165200            'code,title,title_prefix,users_with_local_roles\r\n'
    166201            'fac1,Unnamed Faculty,faculty,[]\r\n')
    167         logfile = os.path.join(
    168             self.app['datacenter'].storage, 'logs', 'datacenter.log')
    169         logcontent = open(logfile).read()
    170         self.assertTrue('zope.mgr - browser.pages.ExportCSVView - '
    171                         'exported: faculties' in logcontent)
     202
     203        # after download, the job and the result file are removed
     204        manager = getUtility(IJobManager)
     205        result = manager.get(job_id)
     206        self.assertEqual(result, None)
     207        self.assertEqual(self.stored_in_datacenter(job_id), False)
     208        #logfile = os.path.join(
     209        #    self.app['datacenter'].storage, 'logs', 'datacenter.log')
     210        #logcontent = open(logfile).read()
     211        #self.assertTrue('zope.mgr - browser.pages.ExportCSVView - '
     212        #                'exported: faculties' in logcontent)
     213        return
     214
     215    def test_export_discard(self):
     216        # we can discard a generated export result
     217        self.test_export_start()
     218        self.wait_for_export_job_completed()
     219        self.browser.open(self.datacenter_path + '/@@export')
     220        self.browser.getControl("Discard").click()
     221        self.assertTrue('Discarded export result' in self.browser.contents)
    172222        return
    173223
  • main/waeup.kofa/trunk/src/waeup/kofa/catalog.py

    r7819 r9217  
    1919"""
    2020import grok
    21 from grok import index
    22 from hurry.query import Eq
    2321from hurry.query.interfaces import IQuery
    2422from hurry.query.query import Query
     
    2624from zope.component import getUtility
    2725from zope.intid.interfaces import IIntIds
    28 
    29 from waeup.kofa.app import University
    3026from waeup.kofa.interfaces import IQueryResultItem
    3127
  • main/waeup.kofa/trunk/src/waeup/kofa/configure.zcml

    r8889 r9217  
    33           xmlns:i18n="http://namespaces.zope.org/i18n">
    44
    5   <!-- This will load and configure most packages we need. -->
     5  <include package="waeup.kofa" file="locales.zcml" />
    66  <include package="grok" />
    7 
    8   <!-- Meta configurations. -->
    9   <include package="zope.sendmail" file="meta.zcml" />
    107  <include package="waeup.kofa" file="meta.zcml" />
    11 
    12   <!-- Extra packages we have to configure -->
    13   <include package="grokcore.message" />
    14   <include package="grokui.admin" />
    15   <include package="hurry.zoperesource" />
    16   <include package="megrok.layout" />
    17   <include package="zc.sourcefactory" />
    18   <include package="zope.app.authentication" />
    19   <include package="zope.sendmail" />
     8  <includeDependencies package="." />
    209
    2110  <!-- Comment the following line if you don't want dolmen.beaker,
     
    2615       be found by the includeDependencies directive. -->
    2716  <include package="dolmen.beaker" file="configure.zcml" />
     17  <grok:grok package="." />
    2818
    29   <include package="waeup.kofa" file="locales.zcml" />
    30   <grok:grok package="waeup.kofa" />
    31   <!-- The following configurations are large (or expected to become
    32        large) and therefore 'outsourced' - the only ethical way to do
    33        outsourcing ;-)
    34   -->
     19  <!-- include package="." file="async.zcml" / -->
    3520  <includeOverrides package="waeup.kofa.widgets" file="overrides.zcml" />
    3621  <includeOverrides package="waeup.kofa.utils" file="overrides.zcml" />
  • main/waeup.kofa/trunk/src/waeup/kofa/datacenter.py

    r9074 r9217  
    3131                                   IDataCenterStorageMovedEvent,
    3232                                   IDataCenterConfig)
     33from waeup.kofa.utils.batching import ExportJobContainer
    3334from waeup.kofa.utils.helpers import copy_filesystem_tree, merge_csv_files
    3435from waeup.kofa.utils.logger import Logger
     
    3738RE_LOGFILE_BACKUP_NAME = re.compile('^.+\.\d+$')
    3839
    39 class DataCenter(grok.Container, Logger):
     40class DataCenter(grok.Container, Logger, ExportJobContainer):
    4041    """A data center contains CSV files.
    4142    """
  • main/waeup.kofa/trunk/src/waeup/kofa/ftesting.zcml

    r7811 r9217  
    99  <includeOverrides package="waeup.kofa" />
    1010  <include package="waeup.kofa" file="mail.zcml" />
     11  <include package="waeup.kofa" file="async_single.zcml" />
    1112
    1213  <!-- Where should the datacenter reside by default? -->
  • main/waeup.kofa/trunk/src/waeup/kofa/hostels/browser.py

    r9199 r9217  
    2222from zope.i18n import translate
    2323from zope.component import getUtility
    24 from waeup.kofa.browser import (
     24from waeup.kofa.browser.layout import (
    2525    KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
    2626    NullValidator)
  • main/waeup.kofa/trunk/src/waeup/kofa/interfaces.py

    r9115 r9217  
    1919import re
    2020import codecs
     21import zc.async.interfaces
    2122import zope.i18nmessageid
    2223from datetime import datetime
     
    2930from zope.component import getUtility
    3031from zope.component.interfaces import IObjectEvent
    31 from zope.container.interfaces import INameChooser
     32from zope.configuration.fields import Path
     33from zope.container.interfaces import INameChooser, IContainer
    3234from zope.interface import Interface, Attribute
    3335from zope.schema.interfaces import IObject
     
    3941DELETION_MARKER = 'XXX'
    4042IGNORE_MARKER = '<IGNORE>'
     43WAEUP_KEY = 'waeup.kofa'
     44VIRT_JOBS_CONTAINER_NAME = 'jobs'
    4145
    4246CREATED = 'created'
     
    4953REGISTERED = 'courses registered'
    5054VALIDATED = 'courses validated'
     55
     56#: A dict giving job status as tuple (<STRING>, <TRANSLATED_STRING>),
     57#: the latter for UI purposes.
     58JOB_STATUS_MAP = {
     59    zc.async.interfaces.NEW: ('new', _('new')),
     60    zc.async.interfaces.COMPLETED: ('completed', _('completed')),
     61    zc.async.interfaces.PENDING: ('pending', _('pending')),
     62    zc.async.interfaces.ACTIVE: ('active', _('active')),
     63    zc.async.interfaces.ASSIGNED: ('assigned', _('assigned')),
     64    zc.async.interfaces.CALLBACKS: ('callbacks', _('callbacks')),
     65    }
    5166
    5267#default_rest_frontpage = u'' + codecs.open(os.path.join(
     
    10821097        """
    10831098
    1084 from zope.configuration.fields import Path
     1099
    10851100class IDataCenterConfig(Interface):
    10861101    path = Path(
     
    10911106        )
    10921107
    1093 class IChangePassword(IKofaObject):
    1094     """Interface needed for change pasword page.
    1095 
    1096     """
    1097     identifier = schema.TextLine(
    1098         title = _(u'Unique Identifier'),
    1099         description = _(
    1100             u'User Name, Student or Applicant Id, Matriculation or '
    1101             u'Registration Number'),
    1102         required = True,
    1103         readonly = False,
    1104         )
    1105 
    1106     email = schema.ASCIILine(
    1107         title = _(u'Email Address'),
    1108         required = True,
    1109         constraint=validate_email,
    1110         )
     1108#
     1109# Asynchronous job handling and related
     1110#
     1111class IJobManager(IKofaObject):
     1112    """A manager for asynchronous running jobs (tasks).
     1113    """
     1114    def put(job, site=None):
     1115        """Put a job into task queue.
     1116
     1117        If no `site` is given, queue job in context of current local
     1118        site.
     1119
     1120        Returns a job_id to identify the put job. This job_id is
     1121        needed for further references to the job.
     1122        """
     1123
     1124    def jobs(site=None):
     1125        """Get an iterable of jobs stored.
     1126        """
     1127
     1128    def get(job_id, site=None):
     1129        """Get the job with id `job_id`.
     1130
     1131        For the `site` parameter see :meth:`put`.
     1132        """
     1133
     1134    def remove(job_id, site=None):
     1135        """Remove job with `job_id` from stored jobs.
     1136        """
     1137
     1138    def start_test_job(site=None):
     1139        """Start a test job.
     1140        """
     1141
     1142class IProgressable(Interface):
     1143    """A component that can indicate its progress status.
     1144    """
     1145    percent = schema.Float(
     1146        title = u'Percent of job done already.',
     1147        )
     1148
     1149class IJobContainer(IContainer):
     1150    """A job container contains IJob objects.
     1151    """
     1152
     1153class IExportJob(zc.async.interfaces.IJob):
     1154    def __init__(site, exporter_name):
     1155        pass
     1156
     1157class IExportJobContainer(Interface):
     1158    """A component that contains (maybe virtually) export jobs.
     1159    """
     1160    def start_export_job(exporter_name, user_id):
     1161        """Start asynchronous export job.
     1162
     1163        `exporter_name` is the name of an exporter utility to be used.
     1164
     1165        `user_id` is the ID of the user that triggers the export.
     1166
     1167        The job_id is stored along with exporter name and user id in a
     1168        persistent list.
     1169
     1170        Returns the job ID of the job started.
     1171        """
     1172
     1173    def get_running_export_jobs(user_id=None):
     1174        """Get export jobs for user with `user_id` as list of tuples.
     1175
     1176        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
     1177        that order. The ``<exporter_name>`` is the utility name of the
     1178        used exporter.
     1179
     1180        If `user_id` is ``None``, all running jobs are returned.
     1181        """
     1182
     1183    def get_export_jobs_status(user_id=None):
     1184        """Get running/completed export jobs for `user_id` as list of tuples.
     1185
     1186        Each tuple holds ``<raw status>, <status translated>,
     1187        <exporter title>`` in that order, where ``<status
     1188        translated>`` and ``<exporter title>`` are translated strings
     1189        representing the status of the job and the human readable
     1190        title of the exporter used.
     1191        """
     1192
     1193    def delete_export_entry(entry):
     1194        """Delete the export denoted by `entry`.
     1195
     1196        Removes `entry` from the local `running_exports` list and also
     1197        removes the regarding job via the local job manager.
     1198
     1199        `entry` is a tuple ``(<job id>, <exporter name>, <user id>)``
     1200        as created by :meth:`start_export_job` or returned by
     1201        :meth:`get_running_export_jobs`.
     1202        """
     1203
     1204    def entry_from_job_id(job_id):
     1205        """Get entry tuple for `job_id`.
     1206
     1207        Returns ``None`` if no such entry can be found.
     1208        """
  • main/waeup.kofa/trunk/src/waeup/kofa/students/browser.py

    r9204 r9217  
    3333    invalidate_accesscode, get_access_code)
    3434from waeup.kofa.accesscodes.workflow import USED
    35 from waeup.kofa.browser import (
     35from waeup.kofa.browser.layout import (
    3636    KofaPage, KofaEditFormPage, KofaAddFormPage, KofaDisplayFormPage,
    37     ContactAdminForm, KofaForm, NullValidator)
     37    KofaForm, NullValidator)
     38from waeup.kofa.browser.pages import ContactAdminForm
    3839from waeup.kofa.browser.breadcrumbs import Breadcrumb
    3940from waeup.kofa.browser.resources import datepicker, datatable, tabs, warning
  • main/waeup.kofa/trunk/src/waeup/kofa/students/catalog.py

    r9170 r9217  
    6363        self.state = context.state
    6464        self.translated_state = context.translated_state
    65         #try:
    66         #    current_level = course_levels.getTerm(
    67         #        context['studycourse'].current_level).title
    68         #except LookupError:
    69         #    current_level = None
    70         #self.current_level = current_level
    71 
    72         self.current_level = context['studycourse'].current_level
    73 
     65        try:
     66            current_level = course_levels.getTerm(
     67                context['studycourse'].current_level).title
     68        except LookupError:
     69            current_level = None
     70        self.current_level = current_level
    7471        try:
    7572            current_session = academic_sessions_vocab.getTerm(
  • main/waeup.kofa/trunk/src/waeup/kofa/students/interfaces.py

    r9182 r9217  
    2121from zope import schema
    2222from zc.sourcefactory.contextual import BasicContextualSourceFactory
     23from waeup.kofa.browser.interfaces import IStudentNavigationBase
    2324from waeup.kofa.interfaces import (
    2425    IKofaObject, academic_sessions_vocab, validate_email, ICSVExporter)
     
    131132    unique_student_id = Attribute("""A unique student id.""")
    132133
    133 class IStudentNavigation(IKofaObject):
     134class IStudentNavigation(IStudentNavigationBase):
    134135    """Interface needed for student navigation, logging, etc.
    135136
  • main/waeup.kofa/trunk/src/waeup/kofa/tests/test_app.py

    r7811 r9217  
    2323from zope.interface.verify import verifyClass, verifyObject
    2424from waeup.kofa.app import University
    25 from waeup.kofa.interfaces import IUniversity
     25from waeup.kofa.interfaces import (
     26    IUniversity, IJobManager, VIRT_JOBS_CONTAINER_NAME)
    2627from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
    2728
     
    6263        self.app.updatePlugins()
    6364        self.assertTrue('accesscodes' in self.app.keys())
     65        return
     66
     67    def test_jobs(self):
     68        # We can get the global job manager when traversing to it...
     69        result = self.app.traverse(VIRT_JOBS_CONTAINER_NAME)
     70        self.assertTrue(IJobManager.providedBy(result))
     71        return
  • main/waeup.kofa/trunk/src/waeup/kofa/tests/test_datacenter.py

    r8725 r9217  
    1010from zope.interface.verify import verifyObject, verifyClass
    1111from waeup.kofa.datacenter import DataCenter
    12 from waeup.kofa.interfaces import IDataCenter, IDataCenterConfig
     12from waeup.kofa.interfaces import (
     13    IDataCenter, IDataCenterConfig, IExportJobContainer)
    1314
    1415class DataCenterLogQueryTests(unittest.TestCase):
     
    180181        return
    181182
    182     def test_iface(self):
     183    def test_ifaces(self):
    183184        # we comply with interfaces
    184185        obj = DataCenter()
    185186        verifyClass(IDataCenter, DataCenter)
     187        verifyClass(IExportJobContainer, DataCenter)
    186188        verifyObject(IDataCenter, obj)
     189        verifyObject(IExportJobContainer, obj)
    187190        return
    188191
  • main/waeup.kofa/trunk/src/waeup/kofa/university/course.py

    r9170 r9217  
    2121from zope.catalog.interfaces import ICatalog
    2222from zope.interface import implementedBy
    23 from zope.schema import getFields
    24 from zope.intid.interfaces import IIntIds
    2523from zope.component import getUtility
    2624from zope.component.interfaces import IFactory, ComponentLookupError
    27 from waeup.kofa.interfaces import IKofaPluggable
    2825from waeup.kofa.university.interfaces import ICourse, ICourseAdd
    2926
     
    4037                 credits=0,
    4138                 passmark=40,
    42                  semester=1,
    43                  former_course=False,
    44                  **kw):
     39                 semester=1, **kw):
    4540        super(Course, self).__init__(**kw)
    4641        self.title = title
     
    4944        self.passmark = passmark
    5045        self.semester = semester
    51         self.former_course = former_course
    5246
    5347    def longtitle(self):
     
    8983        cert._p_changed = True
    9084    return
    91 
    92 class CoursesPlugin(grok.GlobalUtility):
    93     """A plugin that updates courses.
    94     """
    95 
    96     grok.implements(IKofaPluggable)
    97     grok.name('courses')
    98 
    99     deprecated_attributes = []
    100 
    101     def setup(self, site, name, logger):
    102         return
    103 
    104     def update(self, site, name, logger):
    105         cat = getUtility(ICatalog, name='courses_catalog')
    106         results = cat.apply({'code':(None,None)})
    107         uidutil = getUtility(IIntIds, context=cat)
    108         items = getFields(ICourse).items()
    109         for r in results:
    110             o = uidutil.getObject(r)
    111             # Add new attributes
    112             for i in items:
    113                 if not hasattr(o,i[0]):
    114                     setattr(o,i[0],i[1].missing_value)
    115                     logger.info(
    116                         'CoursesPlugin: %s attribute %s added.' % (
    117                         o.code,i[0]))
    118             # Remove deprecated attributes
    119             for i in self.deprecated_attributes:
    120                 try:
    121                     delattr(o,i)
    122                     logger.info(
    123                         'CoursesPlugin: %s attribute %s deleted.' % (
    124                         o.code,i))
    125                 except AttributeError:
    126                     pass
    127         return
  • main/waeup.kofa/trunk/src/waeup/kofa/university/interfaces.py

    r9170 r9217  
    168168        )
    169169
    170     former_course = schema.Bool(
    171         title = _(u'Former Course'),
    172         description = _(
    173             u'If this attribute is being set all certificate courses '
    174             'referring to this course will be automatically deleted.'),
    175         required = False,
    176         default = False,
    177         )
    178 
    179170    def longtitle():
    180171        """
  • main/waeup.kofa/trunk/src/waeup/kofa/utils/batching.py

    r9170 r9217  
    2525import datetime
    2626import os
     27import shutil
    2728import tempfile
    2829import time
    2930from cStringIO import StringIO
    30 from zope.component import createObject
     31from persistent.list import PersistentList
     32from zope.component import createObject, getUtility
     33from zope.component.hooks import setSite
    3134from zope.interface import Interface
    3235from zope.schema import getFields
    3336from zope.event import notify
     37from waeup.kofa.async import AsyncJob
    3438from waeup.kofa.interfaces import (
    35     IBatchProcessor, FatalCSVError, IObjectConverter,
    36     ICSVExporter, IGNORE_MARKER, DuplicationError)
     39    IBatchProcessor, FatalCSVError, IObjectConverter, IJobManager,
     40    ICSVExporter, IGNORE_MARKER, DuplicationError, JOB_STATUS_MAP,
     41    IExportJobContainer, IExportJob)
    3742
    3843class BatchProcessor(grok.GlobalUtility):
     
    361366                    self.writeFailedRow(
    362367                        failed_writer, string_row,
    363                         "Cannot remove: no such entry")
     368                        "Cannot remove: no such entry.")
    364369                    continue
    365370                self.delEntry(row, site)
     
    498503        """
    499504        raise NotImplementedError
     505
     506
     507def export_job(site, exporter_name):
     508    """Export all entries delivered by exporter and store it in a temp file.
     509
     510    `site` gives the site to search. It will be passed to the exporter
     511    and also be set as 'current site' as the function is used in
     512    asynchronous jobs which run in their own threads and have no site
     513    set initially. Therefore `site` must also be a valid value for use
     514    with `zope.component.hooks.setSite()`.
     515
     516    `exporter_name` is the utility name under which the desired
     517    exporter was registered with the ZCA.
     518
     519    The resulting CSV file will be stored in a new temporary directory
     520    (using :func:`tempfile.mkdtemp`). It will be named after the
     521    exporter used with `.csv` filename extension.
     522
     523    Returns the path to the created CSV file.
     524
     525    .. note:: It is the callers responsibility to clean up the used
     526              file and its parent directory.
     527    """
     528    setSite(site)
     529    exporter = getUtility(ICSVExporter, name=exporter_name)
     530    output_dir = tempfile.mkdtemp()
     531    filename = '%s.csv' % exporter_name
     532    output_path = os.path.join(output_dir, filename)
     533    exporter.export_all(site, filepath=output_path)
     534    return output_path
     535
     536class AsyncExportJob(AsyncJob):
     537    """An IJob that exports data to CSV files.
     538
     539    `AsyncExportJob` instances are regular `AsyncJob` instances with a
     540    different constructor API. Instead of a callable to execute, you
     541    must pass a `site` and some `exporter_name` to trigger an export.
     542
     543    The real work is done when an instance of this class is put into a
     544    queue. See :mod:`waeup.kofa.async` to learn more about
     545    asynchronous jobs.
     546
     547    The `exporter_name` must be the name under which an ICSVExporter
     548    utility was registered with the ZCA.
     549
     550    The `site` must be a valid site  or ``None``.
     551
     552    The result of an `AsyncExportJob` is the path to generated CSV
     553    file. The file will reside in a temporary directory that should be
     554    removed after being used.
     555    """
     556    grok.implements(IExportJob)
     557
     558    def __init__(self, site, exporter_name):
     559        super(AsyncExportJob, self).__init__(
     560            export_job, site, exporter_name)
     561
     562class ExportJobContainer(object):
     563    """A mix-in that provides functionality for asynchronous export jobs.
     564    """
     565    grok.implements(IExportJobContainer)
     566    running_exports = PersistentList()
     567
     568    def start_export_job(self, exporter_name, user_id):
     569        """Start asynchronous export job.
     570
     571        `exporter_name` is the name of an exporter utility to be used.
     572
     573        `user_id` is the ID of the user that triggers the export.
     574
     575        The job_id is stored along with exporter name and user id in a
     576        persistent list.
     577
     578        Returns the job ID of the job started.
     579        """
     580        site = grok.getSite()
     581        manager = getUtility(IJobManager)
     582        job = AsyncExportJob(site, exporter_name)
     583        job_id = manager.put(job)
     584        # Make sure that the persisted list is stored in ZODB
     585        self.running_exports = PersistentList(self.running_exports)
     586        self.running_exports.append((job_id, exporter_name, user_id))
     587        return job_id
     588
     589    def get_running_export_jobs(self, user_id=None):
     590        """Get export jobs for user with `user_id` as list of tuples.
     591
     592        Each tuples holds ``<job_id>, <exporter_name>, <user_id>`` in
     593        that order. The ``<exporter_name>`` is the utility name of the
     594        used exporter.
     595
     596        If `user_id` is ``None``, all running jobs are returned.
     597        """
     598        entries = []
     599        to_delete = []
     600        manager = getUtility(IJobManager)
     601        for entry in self.running_exports:
     602            if user_id is not None and entry[2] != user_id:
     603                continue
     604            if manager.get(entry[0]) is None:
     605                to_delete.append(entry)
     606                continue
     607            entries.append(entry)
     608        if to_delete:
     609            self.running_exports = PersistentList(
     610                [x for x in self.running_exports if x not in to_delete])
     611        return entries
     612
     613    def get_export_jobs_status(self, user_id=None):
     614        """Get running/completed export jobs for `user_id` as list of tuples.
     615
     616        Each tuple holds ``<raw status>, <status translated>,
     617        <exporter title>`` in that order, where ``<status
     618        translated>`` and ``<exporter title>`` are translated strings
     619        representing the status of the job and the human readable
     620        title of the exporter used.
     621        """
     622        entries = self.get_running_export_jobs(user_id)
     623        result = []
     624        manager = getUtility(IJobManager)
     625        for entry in entries:
     626            job = manager.get(entry[0])
     627            if job is None:
     628                continue
     629            status, status_translated = JOB_STATUS_MAP[job.status]
     630            exporter_name = getUtility(ICSVExporter, name=entry[1]).title
     631            result.append((status, status_translated, exporter_name))
     632        return result
     633
     634    def delete_export_entry(self, entry):
     635        """Delete the export denoted by `entry`.
     636
     637        Removes given entry from the local `running_exports` list and also
     638        removes the regarding job via the local job manager.
     639
     640        `entry` must be a tuple ``(<job id>, <exporter name>, <user
     641        id>)`` as created by :meth:`start_export_job` or returned by
     642        :meth:`get_running_export_jobs`.
     643        """
     644        manager = getUtility(IJobManager)
     645        job = manager.get(entry[0])
     646        if job is not None:
     647            # remove created export file
     648            if isinstance(job.result, basestring):
     649                if os.path.exists(os.path.dirname(job.result)):
     650                    shutil.rmtree(os.path.dirname(job.result))
     651        manager.remove(entry[0], self)
     652        new_entries = [x for x in self.running_exports
     653                       if x != entry]
     654        self.running_exports = PersistentList(new_entries)
     655        return
     656
     657    def entry_from_job_id(self, job_id):
     658        """Get entry tuple for `job_id`.
     659
     660        Returns ``None`` if no such entry can be found.
     661        """
     662        for entry in self.running_exports:
     663            if entry[0] == job_id:
     664                return entry
     665        return None
  • main/waeup.kofa/trunk/src/waeup/kofa/utils/tests/test_batching.py

    r8380 r9217  
    2323import tempfile
    2424import unittest
     25from zc.async.interfaces import IJob, COMPLETED
    2526from zope import schema
    26 from zope.component import provideUtility
     27from zope.component import provideUtility, getGlobalSiteManager
    2728from zope.component.factory import Factory
    2829from zope.component.hooks import clearSite
     
    3031from zope.interface import Interface, implements, verify
    3132from waeup.kofa.app import University
    32 from waeup.kofa.interfaces import ICSVExporter, IBatchProcessor
     33from waeup.kofa.interfaces import (
     34    ICSVExporter, IBatchProcessor, IExportJobContainer, IJobManager,
     35    IExportJob)
    3336from waeup.kofa.testing import FunctionalLayer, FunctionalTestCase
    34 from waeup.kofa.utils.batching import ExporterBase
     37from waeup.kofa.utils.batching import (
     38    ExporterBase, BatchProcessor, export_job, AsyncExportJob,
     39    ExportJobContainer)
    3540
    3641optionflags = (
     
    7176        self.owner = owner
    7277        self.taxpayer = taxpayer
    73 #Cave = attrs_to_fields(Cave)
    7478
    7579stoneville = dict
    7680
    77 from waeup.kofa.utils.batching import BatchProcessor
     81SAMPLE_DATA = """name,dinoports,owner,taxpayer
     82Barneys Home,2,Barney,1
     83Wilmas Asylum,1,Wilma,1
     84Freds Dinoburgers,10,Fred,0
     85Joeys Drive-in,110,Joey,0
     86"""
     87
    7888class CaveProcessor(BatchProcessor):
    7989    util_name = 'caveprocessor'
    80     #grok.name(util_name)
    8190    name = 'Cave Processor'
    8291    iface = ICave
     
    136145        # Provide sample data
    137146        self.newcomers_csv = os.path.join(self.workdir, 'newcomers.csv')
    138         open(self.newcomers_csv, 'wb').write(
    139             """name,dinoports,owner,taxpayer
    140 Barneys Home,2,Barney,1
    141 Wilmas Asylum,1,Wilma,1
    142 Freds Dinoburgers,10,Fred,0
    143 Joeys Drive-in,110,Joey,0
    144 """)
     147        open(self.newcomers_csv, 'wb').write(SAMPLE_DATA)
    145148        self.setupLogger()
    146149        self.stoneville = stoneville
     
    336339        self.assertEqual(result, None)
    337340        return
     341
     342
     343class CaveExporter(ExporterBase):
     344    # A minimal fake exporter suitable to be called by export_jobs
     345    fields = ('name', 'dinoports', 'owner', 'taxpayer')
     346    title = u'Dummy cave exporter'
     347
     348    def export_all(self, site, filepath=None):
     349        if filepath is None:
     350            return SAMPLE_DATA
     351        open(filepath, 'wb').write(SAMPLE_DATA)
     352        return
     353
     354class ExportJobTests(unittest.TestCase):
     355    # Test asynchronous export functionality (simple cases)
     356
     357    def setUp(self):
     358        # register a suitable ICSVExporter as named utility
     359        self.exporter = CaveExporter()
     360        self.gsm = getGlobalSiteManager()
     361        self.gsm.registerUtility(
     362            self.exporter, ICSVExporter, name='cave_exporter')
     363
     364    def tearDown(self):
     365        self.gsm.unregisterUtility(self.exporter)
     366
     367    def test_export_job_func(self):
     368        # the export_job func does really export data...
     369        result_path = export_job(None, 'cave_exporter')
     370        self.assertTrue(os.path.isfile(result_path))
     371        contents = open(result_path, 'rb').read()
     372        shutil.rmtree(os.path.dirname(result_path))
     373        self.assertEqual(contents, SAMPLE_DATA)
     374        return
     375
     376    def test_export_job_interfaces(self):
     377        # the AsyncExportJob implements promised interfaces correctly...
     378        job = AsyncExportJob(None, None)
     379        verify.verifyClass(IJob, AsyncExportJob)
     380        verify.verifyObject(IJob, job)
     381        verify.verifyClass(IExportJob, AsyncExportJob)
     382        verify.verifyObject(IExportJob, job)
     383        return
     384
     385
     386class FakeJob(object):
     387
     388    status = COMPLETED
     389    result = None
     390
     391class FakeJobWithResult(FakeJob):
     392
     393    def __init__(self):
     394        dir_path = tempfile.mkdtemp()
     395        self.result = os.path.join(dir_path, 'fake.csv')
     396        open(self.result, 'wb').write('a fake result')
     397        return
     398
     399class FakeJobManager(object):
     400
     401    _jobs = dict()
     402    _curr_num = 1
     403
     404    def get(self, job_id):
     405        if job_id == '3':
     406            return FakeJob()
     407        return self._jobs.get(job_id, None)
     408
     409    def put(self, job):
     410        num = str(self._curr_num)
     411        self._jobs[num] = job
     412        self._curr_num += 1
     413        return num
     414
     415    def remove(self, job_id, site):
     416        if job_id in self._jobs:
     417            del self._jobs[job_id]
     418        return
     419
     420class ExportJobContainerTests(unittest.TestCase):
     421    # Test ExportJobContainer
     422
     423    def setUp(self):
     424        # register a suitable ICSVExporter as named utility
     425        self.exporter = CaveExporter()
     426        self.job_manager = FakeJobManager()
     427        self.gsm = getGlobalSiteManager()
     428        self.gsm.registerUtility(
     429            self.exporter, ICSVExporter, name='cave_exporter')
     430        self.gsm.registerUtility(
     431            self.job_manager, IJobManager)
     432
     433    def tearDown(self):
     434        self.gsm.unregisterUtility(self.exporter)
     435        self.gsm.unregisterUtility(self.job_manager, IJobManager)
     436
     437    def test_export_job_interfaces(self):
     438        # the ExportJobContainer implements promised interfaces correctly...
     439        container = ExportJobContainer()
     440        verify.verifyClass(IExportJobContainer, ExportJobContainer)
     441        verify.verifyObject(IExportJobContainer, container)
     442        return
     443
     444    def test_start_export_job(self):
     445        # we can start jobs
     446        container = ExportJobContainer()
     447        container.start_export_job('cave_exporter', 'bob')
     448        result = self.job_manager._jobs.values()[0]
     449        self.assertTrue(IJob.providedBy(result))
     450        self.assertEqual(
     451            container.running_exports,
     452            [('1', 'cave_exporter', 'bob')]
     453            )
     454        return
     455
     456    def test_get_running_export_jobs_all(self):
     457        # we can get export jobs of all users
     458        container = ExportJobContainer()
     459        container.start_export_job('cave_exporter', 'bob')
     460        container.start_export_job('cave_exporter', 'alice')
     461        result = container.get_running_export_jobs()
     462        self.assertEqual(
     463            result,
     464            [('1', 'cave_exporter', 'bob'),
     465             ('2', 'cave_exporter', 'alice')]
     466            )
     467        return
     468
     469    def test_get_running_export_jobs_user(self):
     470        # we can get the export jobs running for a certain user
     471        container = ExportJobContainer()
     472        container.start_export_job('cave_exporter', 'bob')
     473        container.start_export_job('cave_exporter', 'alice')
     474        result1 = container.get_running_export_jobs(user_id='alice')
     475        result2 = container.get_running_export_jobs(user_id='foo')
     476        self.assertEqual(
     477            result1, [('2', 'cave_exporter', 'alice')])
     478        self.assertEqual(
     479            result2, [])
     480        return
     481
     482    def test_get_running_export_jobs_only_if_exist(self):
     483        # we get only jobs that are accessible through the job manager...
     484        container = ExportJobContainer()
     485        container.start_export_job('cave_exporter', 'bob')
     486        container.start_export_job('cave_exporter', 'bob')
     487        self.assertTrue(
     488            ('2', 'cave_exporter', 'bob') in container.running_exports)
     489        # we remove the second entry from job manager
     490        del self.job_manager._jobs['2']
     491        result = container.get_running_export_jobs(user_id='bob')
     492        self.assertEqual(
     493            result, [('1', 'cave_exporter', 'bob')])
     494        self.assertTrue(
     495            ('2', 'cave_exporter', 'bob') not in container.running_exports)
     496        return
     497
     498    def test_get_export_job_status(self):
     499        # we can get the stati of jobs...
     500        container = ExportJobContainer()
     501        container.start_export_job('cave_exporter', 'alice')
     502        container.start_export_job('cave_exporter', 'bob')
     503        container.start_export_job('cave_exporter', 'bob')
     504        result = container.get_export_jobs_status(user_id='bob')
     505        # we'll get the raw value, a translation and the title of the
     506        # exporter
     507        self.assertEqual(
     508            result,
     509            [('new', u'new', u'Dummy cave exporter'),
     510             ('completed', u'completed', u'Dummy cave exporter')]
     511            )
     512        return
     513
     514    def test_delete_export_entry(self):
     515        # we can remove export entries in local lists and the job
     516        # manager as well...
     517        container = ExportJobContainer()
     518        container.start_export_job('cave_exporter', 'bob')
     519        entry = container.running_exports[0]
     520        container.delete_export_entry(entry)
     521        # both, running_exports list and job manager are empty now
     522        self.assertEqual(
     523            container.running_exports, [])
     524        self.assertEqual(
     525            self.job_manager._jobs, {})
     526        return
     527
     528    def test_delete_export_entry_remove_file(self):
     529        # any result files of exports are deleted as well
     530        container = ExportJobContainer()
     531        entry = ('4', 'cave_exporter', 'bob')
     532        container.running_exports = [entry]
     533        fake_job = FakeJobWithResult()
     534        self.job_manager._jobs['4'] = fake_job
     535        self.assertTrue(os.path.isfile(fake_job.result))
     536        container.delete_export_entry(entry)
     537        self.assertTrue(not os.path.exists(fake_job.result))
     538        return
     539
     540    def test_entry_from_job_id(self):
     541        # we can get an entry for a job_id if the id exists
     542        container = ExportJobContainer()
     543        entry = ('4', 'cave_exporter', 'bob')
     544        container.running_exports = [entry]
     545        fake_job = FakeJobWithResult()
     546        self.job_manager._jobs['4'] = fake_job
     547        result1 = container.entry_from_job_id(None)
     548        result2 = container.entry_from_job_id('4')
     549        result3 = container.entry_from_job_id('23')
     550        self.assertEqual(result1, None)
     551        self.assertEqual(result2, ('4', 'cave_exporter', 'bob'))
     552        self.assertEqual(result3, None)
     553        return
Note: See TracChangeset for help on using the changeset viewer.