source: main/waeup.kofa/trunk/src/waeup/kofa/browser/pages.py @ 10646

Last change on this file since 10646 was 10646, checked in by Henrik Bettermann, 11 years ago

Implement page to find students in faculties.

  • Property svn:keywords set to Id
File size: 85.4 KB
Line 
1## $Id: pages.py 10646 2013-09-24 12:39:44Z 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""" Viewing components for Kofa objects.
19"""
20# XXX: All csv ops should move to a dedicated module soon
21import unicodecsv as csv
22import grok
23import os
24import re
25import sys
26from datetime import datetime, timedelta
27from urllib import urlencode
28from hurry.query import Eq, Text
29from hurry.query.query import Query
30from zope import schema
31from zope.i18n import translate
32from zope.authentication.interfaces import (
33    IAuthentication, IUnauthenticatedPrincipal, ILogout)
34from zope.catalog.interfaces import ICatalog
35from zope.component import (
36    getUtility, queryUtility, createObject, getAllUtilitiesRegisteredFor,
37    getUtilitiesFor,
38    )
39from zope.event import notify
40from zope.security import checkPermission
41from zope.securitypolicy.interfaces import IPrincipalRoleManager
42from zope.session.interfaces import ISession
43from zope.password.interfaces import IPasswordManager
44from waeup.kofa.browser.layout import (
45    KofaPage, KofaForm, KofaEditFormPage, KofaAddFormPage,
46    KofaDisplayFormPage, NullValidator)
47from waeup.kofa.browser.interfaces import (
48    IUniversity, IFacultiesContainer, IFaculty, IFacultyAdd,
49    IDepartment, IDepartmentAdd, ICourse, ICourseAdd, ICertificate,
50    ICertificateAdd, ICertificateCourse, ICertificateCourseAdd,
51    ICaptchaManager, IChangePassword)
52from waeup.kofa.browser.layout import jsaction, action, UtilityView
53from waeup.kofa.browser.resources import (
54    warning, tabs, datatable)
55from waeup.kofa.interfaces import MessageFactory as _
56from waeup.kofa.interfaces import(
57    IKofaObject, IUsersContainer, IUserAccount, IDataCenter,
58    IKofaXMLImporter, IKofaXMLExporter, IBatchProcessor,
59    ILocalRolesAssignable, DuplicationError, IConfigurationContainer,
60    ISessionConfiguration, ISessionConfigurationAdd, IJobManager,
61    IPasswordValidator, IContactForm, IKofaUtils, ICSVExporter,
62    academic_sessions_vocab)
63from waeup.kofa.permissions import (
64    get_users_with_local_roles, get_all_roles, get_all_users,
65    get_users_with_role)
66
67from waeup.kofa.university.catalog import search
68from waeup.kofa.university.vocabularies import course_levels
69from waeup.kofa.authentication import LocalRoleSetEvent
70from waeup.kofa.widgets.htmlwidget import HTMLDisplayWidget
71from waeup.kofa.utils.helpers import get_user_account
72from waeup.kofa.mandates.mandate import PasswordMandate
73from waeup.kofa.datacenter import DataCenterFile
74
75from waeup.kofa.students.export import EXPORTER_NAMES as STUDENT_EXPORTERS
76from waeup.kofa.students.catalog import StudentQueryResultItem
77
78FORBIDDEN_CHARACTERS = (160,)
79
80grok.context(IKofaObject)
81grok.templatedir('templates')
82
83def add_local_role(view, tab, **data):
84    localrole = view.request.form.get('local_role', None)
85    user = view.request.form.get('user', None)
86    if user is None or localrole is None:
87        view.flash('No user selected.')
88        view.redirect(view.url(view.context, '@@manage')+'?tab%s' % tab)
89        return
90    role_manager = IPrincipalRoleManager(view.context)
91    role_manager.assignRoleToPrincipal(localrole, user)
92    notify(LocalRoleSetEvent(view.context, localrole, user, granted=True))
93    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
94    grok.getSite().logger.info(
95        '%s - added: %s|%s' % (ob_class, user, localrole))
96    view.redirect(view.url(view.context, u'@@manage')+'?tab%s' % tab)
97    return
98
99def del_local_roles(view, tab, **data):
100    child_ids = view.request.form.get('role_id', None)
101    if child_ids is None:
102        view.flash(_('No local role selected.'))
103        view.redirect(view.url(view.context, '@@manage')+'?tab%s' % tab)
104        return
105    if not isinstance(child_ids, list):
106        child_ids = [child_ids]
107    deleted = []
108    role_manager = IPrincipalRoleManager(view.context)
109    for child_id in child_ids:
110        localrole = child_id.split('|')[1]
111        user_name = child_id.split('|')[0]
112        try:
113            role_manager.unsetRoleForPrincipal(localrole, user_name)
114            notify(LocalRoleSetEvent(
115                    view.context, localrole, user_name, granted=False))
116            deleted.append(child_id)
117        except:
118            view.flash('Could not remove %s: %s: %s' % (
119                    child_id, sys.exc_info()[0], sys.exc_info()[1]))
120    if len(deleted):
121        view.flash(
122            _('Local role successfully removed: ${a}',
123            mapping = {'a':', '.join(deleted)}))
124        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
125        grok.getSite().logger.info(
126            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
127    view.redirect(view.url(view.context, u'@@manage')+'?tab%s' % tab)
128    return
129
130def delSubobjects(view, redirect, tab=None, subcontainer=None):
131    form = view.request.form
132    if 'val_id' in form:
133        child_id = form['val_id']
134    else:
135        view.flash(_('No item selected.'))
136        if tab:
137            view.redirect(view.url(view.context, redirect)+'?tab%s' % tab)
138        else:
139            view.redirect(view.url(view.context, redirect))
140        return
141    if not isinstance(child_id, list):
142        child_id = [child_id]
143    deleted = []
144    for id in child_id:
145        try:
146            if subcontainer:
147                container = getattr(view.context, subcontainer, None)
148                del container[id]
149            else:
150                del view.context[id]
151            deleted.append(id)
152        except:
153            view.flash('Could not delete %s: %s: %s' % (
154                    id, sys.exc_info()[0], sys.exc_info()[1]))
155    if len(deleted):
156        view.flash(_('Successfully removed: ${a}',
157            mapping = {'a': ', '.join(deleted)}))
158        ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
159        grok.getSite().logger.info(
160            '%s - removed: %s' % (ob_class, ', '.join(deleted)))
161    if tab:
162        view.redirect(view.url(view.context, redirect)+'?tab%s' % tab)
163    else:
164        view.redirect(view.url(view.context, redirect))
165    return
166
167def getPreviewTable(view, n):
168    """Get transposed table with n sample record.
169
170    The first column contains the headers.
171    """
172    if not view.reader:
173        return
174    header = view.getPreviewHeader()
175    num = 0
176    data = []
177    for line in view.reader:
178        if num > n-1:
179            break
180        num += 1
181        data.append(line)
182    result = []
183    for name in header:
184        result_line = []
185        result_line.append(name)
186        for d in data:
187            result_line.append(d[name])
188        result.append(result_line)
189    return result
190
191# Save function used for save methods in pages
192def msave(view, **data):
193    changed_fields = view.applyData(view.context, **data)
194    # Turn list of lists into single list
195    if changed_fields:
196        changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
197    fields_string = ' + '.join(changed_fields)
198    view.flash(_('Form has been saved.'))
199    ob_class = view.__implemented__.__name__.replace('waeup.kofa.','')
200    if fields_string:
201        grok.getSite().logger.info('%s - %s - saved: %s' % (ob_class, view.context.__name__, fields_string))
202    return
203
204def doll_up(view, user=None):
205    """Doll up export jobs for displaying in table.
206    """
207    job_entries = view.context.get_running_export_jobs(user)
208    job_manager = getUtility(IJobManager)
209    entries = []
210    for job_id, exporter_name, user_id in job_entries:
211        job = job_manager.get(job_id)
212        exporter = getUtility(ICSVExporter, name=exporter_name)
213        exporter_title = getattr(exporter, 'title', 'Unknown')
214        args = ', '.join(['%s=%s' % (item[0], item[1])
215            for item in job.kwargs.items()])
216        status = job.finished and 'ready' or 'running'
217        status = job.failed and 'FAILED' or status
218        start_time = getattr(job, 'begin_after', None)
219        if start_time:
220            start_time = start_time.astimezone(
221                getUtility(
222                    IKofaUtils).tzinfo).strftime("%Y-%m-%d %H:%M:%S %Z")
223        download_url = view.url(view.context, 'download_export',
224                                data=dict(job_id=job_id))
225        new_entry = dict(
226            job=job_id,
227            creator=user_id,
228            args=args,
229            exporter=exporter_title,
230            status=status,
231            start_time=start_time,
232            download_url=download_url,
233            show_download_button = (job.finished and not job.failed),
234            show_refresh_button = not job.finished,
235            show_discard_button = job.finished,)
236        entries.append(new_entry)
237    return entries
238
239class LocalRoleAssignmentUtilityView(object):
240    """A view mixin with useful methods for local role assignment.
241
242    """
243
244    def getLocalRoles(self):
245        roles = ILocalRolesAssignable(self.context)
246        return roles()
247
248    def getUsers(self):
249        return get_all_users()
250
251    def getUsersWithLocalRoles(self):
252        return get_users_with_local_roles(self.context)
253
254#
255# Login/logout and language switch pages...
256#
257
258class LoginPage(KofaPage):
259    """A login page, available for all objects.
260    """
261    grok.name('login')
262    grok.context(IKofaObject)
263    grok.require('waeup.Public')
264    label = _(u'Login')
265    camefrom = None
266    login_button = label
267
268    def _comment(self, student):
269        return getattr(student, 'suspended_comment', None)
270
271    def update(self, SUBMIT=None, camefrom=None):
272        self.camefrom = camefrom
273        if SUBMIT is not None:
274            if self.request.principal.id != 'zope.anybody':
275                self.flash(_('You logged in.'))
276                if self.request.principal.user_type == 'student':
277                    student = grok.getSite()['students'][
278                        self.request.principal.id]
279                    rel_link = '/students/%s' % self.request.principal.id
280                    if student.personal_data_expired:
281                        rel_link = '/students/%s/edit_personal' % (
282                            self.request.principal.id)
283                        self.flash(
284                          _('Your personal data record is outdated. Please update.'))
285                    self.redirect(self.application_url() + rel_link)
286                    return
287                elif self.request.principal.user_type == 'applicant':
288                    container, application_number = self.request.principal.id.split('_')
289                    rel_link = '/applicants/%s/%s' % (
290                        container, application_number)
291                    self.redirect(self.application_url() + rel_link)
292                    return
293                if not self.camefrom:
294                    # User might have entered the URL directly. Let's beam
295                    # him back to our context.
296                    self.redirect(self.url(self.context))
297                    return
298                self.redirect(self.camefrom)
299                return
300            # Display appropriate flash message if credentials are correct
301            # but student has been deactivated or a temporary password
302            # has been set.
303            login = self.request.form['form.login']
304            if len(login) == 8 and login in grok.getSite()['students']:
305                student = grok.getSite()['students'][login]
306                password = self.request.form['form.password']
307                passwordmanager = getUtility(IPasswordManager, 'SSHA')
308                if student.password is not None and \
309                    passwordmanager.checkPassword(student.password, password):
310                    # The student entered valid credentials.
311                    # First we check if a temporary password has been set.
312                    delta = timedelta(minutes=10)
313                    now = datetime.utcnow()
314                    temp_password_dict = getattr(student, 'temp_password', None)
315                    if temp_password_dict is not None and \
316                        now < temp_password_dict.get('timestamp', now) + delta:
317                        self.flash(
318                            _('Your account has been temporarily deactivated.'))
319                        return
320                    # Now we know that the student is suspended.
321                    comment = self._comment(student)
322                    if comment:
323                        self.flash(comment)
324                    else:
325                        self.flash(_('Your account has been deactivated.'))
326                    return
327            self.flash(_('You entered invalid credentials.'))
328            return
329
330
331class LogoutPage(KofaPage):
332    """A logout page. Calling this page will log the current user out.
333    """
334    grok.context(IKofaObject)
335    grok.require('waeup.Public')
336    grok.name('logout')
337
338    def update(self):
339        if not IUnauthenticatedPrincipal.providedBy(self.request.principal):
340            auth = getUtility(IAuthentication)
341            ILogout(auth).logout(self.request)
342            self.flash(_("You have been logged out. Thanks for using WAeUP Kofa!"))
343        self.redirect(self.application_url())
344
345
346class LanguageChangePage(KofaPage):
347    """ Language switch
348    """
349    grok.context(IKofaObject)
350    grok.name('change_language')
351    grok.require('waeup.Public')
352
353    def update(self, lang='en', view_name='@@index'):
354        self.response.setCookie('kofa.language', lang, path='/')
355        self.redirect(self.url(self.context, view_name))
356        return
357
358    def render(self):
359        return
360
361#
362# Contact form...
363#
364
365class ContactAdminForm(KofaForm):
366    grok.name('contactadmin')
367    #grok.context(IUniversity)
368    grok.template('contactform')
369    grok.require('waeup.Authenticated')
370    pnav = 2
371    form_fields = grok.AutoFields(IContactForm).select('body')
372
373    def update(self):
374        super(ContactAdminForm, self).update()
375        self.form_fields.get('body').field.default = None
376        return
377
378    @property
379    def config(self):
380        return grok.getSite()['configuration']
381
382    def label(self):
383        return _(u'Contact ${a}', mapping = {'a': self.config.name_admin})
384
385    @property
386    def get_user_account(self):
387        return get_user_account(self.request)
388
389    @action(_('Send message now'), style='primary')
390    def send(self, *args, **data):
391        fullname = self.request.principal.title
392        try:
393            email = self.request.principal.email
394        except AttributeError:
395            email = self.config.email_admin
396        username = self.request.principal.id
397        usertype = getattr(self.request.principal,
398                           'user_type', 'system').title()
399        kofa_utils = getUtility(IKofaUtils)
400        success = kofa_utils.sendContactForm(
401                fullname,email,
402                self.config.name_admin,self.config.email_admin,
403                username,usertype,self.config.name,
404                data['body'],self.config.email_subject)
405        # Success is always True if sendContactForm didn't fail.
406        # TODO: Catch exceptions.
407        if success:
408            self.flash(_('Your message has been sent.'))
409        return
410
411class EnquiriesForm(ContactAdminForm):
412    """Captcha'd page to let anonymous send emails to the administrator.
413    """
414    grok.name('enquiries')
415    grok.require('waeup.Public')
416    pnav = 2
417    form_fields = grok.AutoFields(IContactForm).select(
418                          'fullname', 'email_from', 'body')
419
420    def update(self):
421        super(EnquiriesForm, self).update()
422        # Handle captcha
423        self.captcha = getUtility(ICaptchaManager).getCaptcha()
424        self.captcha_result = self.captcha.verify(self.request)
425        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
426        return
427
428    @action(_('Send now'), style='primary')
429    def send(self, *args, **data):
430        if not self.captcha_result.is_valid:
431            # Captcha will display error messages automatically.
432            # No need to flash something.
433            return
434        kofa_utils = getUtility(IKofaUtils)
435        success = kofa_utils.sendContactForm(
436                data['fullname'],data['email_from'],
437                self.config.name_admin,self.config.email_admin,
438                u'None',u'Anonymous',self.config.name,
439                data['body'],self.config.email_subject)
440        if success:
441            self.flash(_('Your message has been sent.'))
442        else:
443            self.flash(_('A smtp server error occurred.'))
444        return
445
446#
447# University related pages...
448#
449
450class UniversityPage(KofaDisplayFormPage):
451    """ The main university page.
452    """
453    grok.require('waeup.Public')
454    grok.name('index')
455    grok.context(IUniversity)
456    pnav = 0
457    label = ''
458
459    @property
460    def frontpage(self):
461        lang = self.request.cookies.get('kofa.language')
462        html = self.context['configuration'].frontpage_dict.get(lang,'')
463        if html =='':
464            portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
465            html = self.context[
466                'configuration'].frontpage_dict.get(portal_language,'')
467        if html =='':
468            return _(u'<h1>Welcome to WAeUP.Kofa</h1>')
469        else:
470            return html
471
472class AdministrationPage(KofaPage):
473    """ The administration overview page.
474    """
475    grok.name('administration')
476    grok.context(IUniversity)
477    grok.require('waeup.managePortal')
478    label = _(u'Administration')
479    pnav = 0
480
481class RSS20Feed(grok.View):
482    """An RSS 2.0 feed.
483    """
484    grok.name('feed.rss')
485    grok.context(IUniversity)
486    grok.require('waeup.Public')
487    grok.template('universityrss20feed')
488
489    name = 'General news feed'
490    description = 'waeup.kofa now supports RSS 2.0 feeds :-)'
491    language = None
492    date = None
493    buildDate = None
494    editor = None
495    webmaster = None
496
497    @property
498    def title(self):
499        return getattr(grok.getSite(), 'name', u'Sample University')
500
501    @property
502    def contexttitle(self):
503        return self.name
504
505    @property
506    def link(self):
507        return self.url(grok.getSite())
508
509    def update(self):
510        self.response.setHeader('Content-Type', 'text/xml; charset=UTF-8')
511
512    def entries(self):
513        return ()
514
515#
516# User container pages...
517#
518
519class UsersContainerPage(KofaPage):
520    """Overview page for all local users.
521    """
522    grok.require('waeup.manageUsers')
523    grok.context(IUsersContainer)
524    grok.name('index')
525    label = _('Portal Users')
526    manage_button = _(u'Manage')
527    delete_button = _(u'Remove')
528
529    def update(self, userid=None, adduser=None, manage=None, delete=None):
530        datatable.need()
531        if manage is not None and userid is not None:
532            self.redirect(self.url(userid) + '/@@manage')
533        if delete is not None and userid is not None:
534            self.context.delUser(userid)
535            self.flash(_('User account ${a} successfully deleted.',
536                mapping = {'a':  userid}))
537            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
538            self.context.__parent__.logger.info(
539                '%s - removed: %s' % (ob_class, userid))
540
541    def getLocalRoles(self, account):
542        local_roles = account.getLocalRoles()
543        local_roles_string = ''
544        site_url = self.url(grok.getSite())
545        for local_role in local_roles.keys():
546            role_title = getattr(
547                dict(get_all_roles()).get(local_role, None), 'title', None)
548            objects_string = ''
549            for object in local_roles[local_role]:
550                objects_string += '<a href="%s">%s</a>, ' %(self.url(object),
551                    self.url(object).replace(site_url,''))
552            local_roles_string += '%s: <br />%s <br />' %(role_title,
553                objects_string.rstrip(', '))
554        return local_roles_string
555
556    def getSiteRoles(self, account):
557        site_roles = account.roles
558        site_roles_string = ''
559        for site_role in site_roles:
560            role_title = dict(get_all_roles())[site_role].title
561            site_roles_string += '%s <br />' % role_title
562        return site_roles_string
563
564class AddUserFormPage(KofaAddFormPage):
565    """Add a user account.
566    """
567    grok.require('waeup.manageUsers')
568    grok.context(IUsersContainer)
569    grok.name('add')
570    grok.template('usereditformpage')
571    form_fields = grok.AutoFields(IUserAccount)
572    label = _('Add user')
573
574    @action(_('Add user'), style='primary')
575    def addUser(self, **data):
576        name = data['name']
577        title = data['title']
578        email = data['email']
579        phone = data['phone']
580        description = data['description']
581        #password = data['password']
582        roles = data['roles']
583        form = self.request.form
584        password = form.get('password', None)
585        password_ctl = form.get('control_password', None)
586        if password:
587            validator = getUtility(IPasswordValidator)
588            errors = validator.validate_password(password, password_ctl)
589            if errors:
590                self.flash( ' '.join(errors))
591                return
592        try:
593            self.context.addUser(name, password, title=title, email=email,
594                                 phone=phone, description=description,
595                                 roles=roles)
596            self.flash(_('User account ${a} successfully added.',
597                mapping = {'a': name}))
598            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
599            self.context.__parent__.logger.info(
600                '%s - added: %s' % (ob_class, name))
601        except KeyError:
602            self.status = self.flash('The userid chosen already exists '
603                                  'in the database.')
604            return
605        self.redirect(self.url(self.context))
606
607class UserManageFormPage(KofaEditFormPage):
608    """Manage a user account.
609    """
610    grok.context(IUserAccount)
611    grok.name('manage')
612    grok.template('usereditformpage')
613    grok.require('waeup.manageUsers')
614    form_fields = grok.AutoFields(IUserAccount).omit('name')
615
616    def label(self):
617        return _("Edit user ${a}", mapping = {'a':self.context.__name__})
618
619    def setUpWidgets(self, ignore_request=False):
620        super(UserManageFormPage,self).setUpWidgets(ignore_request)
621        self.widgets['title'].displayWidth = 30
622        self.widgets['description'].height = 3
623        return
624
625    @action(_('Save'), style='primary')
626    def save(self, **data):
627        form = self.request.form
628        password = form.get('password', None)
629        password_ctl = form.get('control_password', None)
630        if password:
631            validator = getUtility(IPasswordValidator)
632            errors = validator.validate_password(password, password_ctl)
633            if errors:
634                self.flash( ' '.join(errors))
635                return
636        changed_fields = self.applyData(self.context, **data)
637        if changed_fields:
638            changed_fields = reduce(lambda x,y: x+y, changed_fields.values())
639        else:
640            changed_fields = []
641        if password:
642            # Now we know that the form has no errors and can set password ...
643            self.context.setPassword(password)
644            changed_fields.append('password')
645        fields_string = ' + '.join(changed_fields)
646        if fields_string:
647            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
648            self.context.__parent__.logger.info(
649                '%s - %s edited: %s' % (
650                ob_class, self.context.name, fields_string))
651        self.flash(_('User settings have been saved.'))
652        return
653
654    @action(_('Cancel'), validator=NullValidator)
655    def cancel(self, **data):
656        self.redirect(self.url(self.context.__parent__))
657        return
658
659class ContactUserForm(ContactAdminForm):
660    grok.name('contactuser')
661    grok.context(IUserAccount)
662    grok.template('contactform')
663    grok.require('waeup.manageUsers')
664    pnav = 0
665    form_fields = grok.AutoFields(IContactForm).select('body')
666
667    def label(self):
668        return _(u'Send message to ${a}', mapping = {'a':self.context.title})
669
670    @action(_('Send message now'), style='primary')
671    def send(self, *args, **data):
672        try:
673            email = self.request.principal.email
674        except AttributeError:
675            email = self.config.email_admin
676        usertype = getattr(self.request.principal,
677                           'user_type', 'system').title()
678        kofa_utils = getUtility(IKofaUtils)
679        success = kofa_utils.sendContactForm(
680                self.request.principal.title,email,
681                self.context.title,self.context.email,
682                self.request.principal.id,usertype,self.config.name,
683                data['body'],self.config.email_subject)
684        # Success is always True if sendContactForm didn't fail.
685        # TODO: Catch exceptions.
686        if success:
687            self.flash(_('Your message has been sent.'))
688        return
689
690class UserEditFormPage(UserManageFormPage):
691    """Edit a user account by user
692    """
693    grok.name('index')
694    grok.require('waeup.editUser')
695    form_fields = grok.AutoFields(IUserAccount).omit(
696        'name', 'description', 'roles')
697    label = _(u"My Preferences")
698
699    def setUpWidgets(self, ignore_request=False):
700        super(UserManageFormPage,self).setUpWidgets(ignore_request)
701        self.widgets['title'].displayWidth = 30
702
703class MyRolesPage(KofaPage):
704    """Display site roles and local roles assigned to officers.
705    """
706    grok.name('my_roles')
707    grok.require('waeup.editUser')
708    grok.context(IUserAccount)
709    grok.template('myrolespage')
710    label = _(u"My Roles")
711
712    @property
713    def getLocalRoles(self):
714        local_roles = get_user_account(self.request).getLocalRoles()
715        local_roles_userfriendly = {}
716        for local_role in local_roles:
717            role_title = dict(get_all_roles())[local_role].title
718            local_roles_userfriendly[role_title] = local_roles[local_role]
719        return local_roles_userfriendly
720
721    @property
722    def getSiteRoles(self):
723        site_roles = get_user_account(self.request).roles
724        site_roles_userfriendly = []
725        for site_role in site_roles:
726            role_title = dict(get_all_roles())[site_role].title
727            site_roles_userfriendly.append(role_title)
728        return site_roles_userfriendly
729
730#
731# Search pages...
732#
733
734class SearchPage(KofaPage):
735    """General search page for the academics section.
736    """
737    grok.context(IFacultiesContainer)
738    grok.name('search')
739    grok.template('searchpage')
740    grok.require('waeup.viewAcademics')
741    label = _(u"Search Academic Section")
742    pnav = 1
743    search_button = _(u'Search')
744
745    def update(self, *args, **kw):
746        datatable.need()
747        form = self.request.form
748        self.hitlist = []
749        self.query = ''
750        if not 'query' in form:
751            return
752        query = form['query']
753        self.query = query
754        self.hitlist = search(query=self.query, view=self)
755        return
756
757#
758# Configuration pages...
759#
760
761class ConfigurationContainerDisplayFormPage(KofaDisplayFormPage):
762    """View page of the configuration container.
763    """
764    grok.require('waeup.managePortalConfiguration')
765    grok.name('view')
766    grok.context(IConfigurationContainer)
767    pnav = 0
768    label = _(u'View portal configuration')
769    form_fields = grok.AutoFields(IConfigurationContainer)
770    form_fields['frontpage'].custom_widget = HTMLDisplayWidget
771
772class ConfigurationContainerManageFormPage(KofaEditFormPage):
773    """Manage page of the configuration container. We always use the
774    manage page in the UI not the view page, thus we use the index name here.
775    """
776    grok.require('waeup.managePortalConfiguration')
777    grok.name('index')
778    grok.context(IConfigurationContainer)
779    grok.template('configurationmanagepage')
780    pnav = 0
781    label = _(u'Edit portal configuration')
782    taboneactions = [_('Save'), _('Update plugins')]
783    tabtwoactions = [
784        _('Add session configuration'),
785        _('Remove selected')]
786    form_fields = grok.AutoFields(IConfigurationContainer).omit('frontpage_dict')
787
788    def update(self):
789        tabs.need()
790        self.tab1 = self.tab2 = ''
791        qs = self.request.get('QUERY_STRING', '')
792        if not qs:
793            qs = 'tab1'
794        setattr(self, qs, 'active')
795        datatable.need()
796        warning.need()
797        return super(ConfigurationContainerManageFormPage, self).update()
798
799    def _frontpage(self):
800        view = ConfigurationContainerDisplayFormPage(
801            self.context,self.request)
802        view.setUpWidgets()
803        return view.widgets['frontpage']()
804
805    @action(_('Save'), style='primary')
806    def save(self, **data):
807        msave(self, **data)
808        self.context.frontpage_dict = self._frontpage()
809        return
810
811    @action(_('Add session configuration'), validator=NullValidator,
812            style='primary')
813    def addSubunit(self, **data):
814        self.redirect(self.url(self.context, '@@add'))
815        return
816
817    def getSessionConfigurations(self):
818        """Get a list of all stored session configuration objects.
819        """
820        for key, val in self.context.items():
821            url = self.url(val)
822            session_string = val.getSessionString()
823            title = _('Session ${a} Configuration',
824                      mapping = {'a':session_string})
825            yield(dict(url=url, name=key, title=title))
826
827    @jsaction(_('Remove selected'))
828    def delSessonConfigurations(self, **data):
829        delSubobjects(self, redirect='@@index', tab='2')
830        return
831
832    @action(_('Update plugins'), validator=NullValidator)
833    def updatePlugins(self, **data):
834        grok.getSite().updatePlugins()
835        self.flash(_('Plugins were updated. See log file for details.'))
836        return
837
838class SessionConfigurationAddFormPage(KofaAddFormPage):
839    """Add a session configuration object to configuration container.
840    """
841    grok.context(IConfigurationContainer)
842    grok.name('add')
843    grok.require('waeup.managePortalConfiguration')
844    label = _('Add session configuration')
845    form_fields = grok.AutoFields(ISessionConfigurationAdd)
846    pnav = 0
847
848    @action(_('Add Session Configuration'), style='primary')
849    def addSessionConfiguration(self, **data):
850        sessionconfiguration = createObject(u'waeup.SessionConfiguration')
851        self.applyData(sessionconfiguration, **data)
852        try:
853            self.context.addSessionConfiguration(sessionconfiguration)
854            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
855            self.context.__parent__.logger.info(
856                '%s - added: %s' % (
857                ob_class, sessionconfiguration.academic_session))
858        except KeyError:
859            self.flash(_('The session chosen already exists.'))
860            return
861        self.redirect(self.url(self.context, '@@index')+'?tab2')
862        return
863
864    @action(_('Cancel'), validator=NullValidator)
865    def cancel(self):
866        self.redirect(self.url(self.context, '@@index')+'?tab2')
867        return
868
869class SessionConfigurationManageFormPage(KofaEditFormPage):
870    """Manage session configuration object.
871    """
872    grok.context(ISessionConfiguration)
873    grok.name('index')
874    grok.require('waeup.managePortalConfiguration')
875    form_fields = grok.AutoFields(ISessionConfiguration)
876    pnav = 0
877
878    @property
879    def label(self):
880        session_string = self.context.getSessionString()
881        return _('Edit academic session ${a} configuration',
882            mapping = {'a':session_string})
883
884    @action(_('Save'), style='primary')
885    def save(self, **data):
886        msave(self, **data)
887        self.redirect(self.url(self.context.__parent__, '@@index')+'?tab2')
888        return
889
890    @action(_('Cancel'), validator=NullValidator)
891    def cancel(self):
892        self.redirect(self.url(self.context.__parent__, '@@index')+'?tab2')
893        return
894
895#
896# Datacenter pages...
897#
898
899class DatacenterPage(KofaEditFormPage):
900    grok.context(IDataCenter)
901    grok.name('index')
902    grok.require('waeup.manageDataCenter')
903    label = _(u'Data Center')
904    pnav = 0
905
906    def update(self):
907        datatable.need()
908        warning.need()
909        return super(DatacenterPage, self).update()
910
911    @jsaction(_('Remove selected'))
912    def delFiles(self, **data):
913        form = self.request.form
914        if 'val_id' in form:
915            child_id = form['val_id']
916        else:
917            self.flash(_('No item selected.'))
918            return
919        if not isinstance(child_id, list):
920            child_id = [child_id]
921        deleted = []
922        for id in child_id:
923            fullpath = os.path.join(self.context.storage, id)
924            try:
925                os.remove(fullpath)
926                deleted.append(id)
927            except OSError:
928                self.flash(_('OSError: The file could not be deleted.'))
929                return
930        if len(deleted):
931            self.flash(_('Successfully deleted: ${a}',
932                mapping = {'a': ', '.join(deleted)}))
933            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
934            self.context.logger.info(
935                '%s - deleted: %s' % (ob_class, ', '.join(deleted)))
936        return
937
938class DatacenterFinishedPage(KofaEditFormPage):
939    grok.context(IDataCenter)
940    grok.name('processed')
941    grok.require('waeup.manageDataCenter')
942    label = _(u'Processed Files')
943    pnav = 0
944
945    def update(self):
946        datatable.need()
947        return super(DatacenterFinishedPage, self).update()
948
949class DatacenterUploadPage(KofaPage):
950    grok.context(IDataCenter)
951    grok.name('upload')
952    grok.require('waeup.manageDataCenter')
953    label = _(u'Upload portal data as CSV file')
954    pnav = 0
955    max_files = 20
956    upload_button =_(u'Upload')
957    cancel_button =_(u'Cancel')
958
959    def getPreviewHeader(self):
960        """Get the header fields of uploaded CSV file.
961        """
962        reader = csv.reader(open(self.fullpath, 'rb'))
963        return reader.next()
964
965    def getImporters(self):
966        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
967        importers = sorted(
968            [dict(title=x.name, name=x.util_name) for x in importers])
969        return importers
970
971    def _notifyImportManagers(self, filename,
972        normalized_filename, importer, import_mode):
973        """Send email to Import Managers
974        """
975        # Get information about file
976        self.fullpath = os.path.join(self.context.storage, normalized_filename)
977        uploadfile = DataCenterFile(self.fullpath)
978        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
979        table = getPreviewTable(self, 3)
980        mail_table = ''
981        for line in table:
982            header = line[0]
983            data = str(line[1:]).strip('[').strip(']')
984            mail_table += '%s: %s ...\n' % (line[0], data)
985        # Collect all recipient addresses
986        kofa_utils = getUtility(IKofaUtils)
987        import_managers = get_users_with_role(
988            'waeup.ImportManager', grok.getSite())
989        rcpt_addrs = ','.join(
990            [user['user_email'] for user in import_managers if
991                user['user_email'] is not None])
992        if rcpt_addrs:
993            config = grok.getSite()['configuration']
994            fullname = self.request.principal.title
995            try:
996                email = self.request.principal.email
997            except AttributeError:
998                email = config.email_admin
999            username = self.request.principal.id
1000            usertype = getattr(self.request.principal,
1001                               'user_type', 'system').title()
1002            rcpt_name = _('Import Manager')
1003            subject = translate(
1004                      _('${a}: ${b} uploaded',
1005                      mapping = {'a':config.acronym, 'b':filename}),
1006                      'waeup.kofa',
1007                      target_language=kofa_utils.PORTAL_LANGUAGE)
1008            text = _("""File: ${a}
1009Importer: ${b}
1010Import Mode: ${c}
1011Datasets: ${d}
1012
1013${e}
1014
1015Comment by Import Manager:""", mapping = {'a':normalized_filename,
1016                'b':importer,
1017                'c':import_mode,
1018                'd':uploadfile.lines - 1,
1019                'e':mail_table})
1020            success = kofa_utils.sendContactForm(
1021                    fullname,email,
1022                    rcpt_name,rcpt_addrs,
1023                    username,usertype,config.name,
1024                    text,subject)
1025            if success:
1026                self.flash(
1027                    _('All import managers have been notified by email.'))
1028            else:
1029                self.flash(_('An smtp server error occurred.'))
1030            return
1031
1032    def update(self, uploadfile=None, import_mode=None,
1033               importer=None, CANCEL=None, SUBMIT=None):
1034        number_of_pendings = len(self.context.getPendingFiles())
1035        if number_of_pendings > self.max_files:
1036            self.flash(
1037                _('Maximum number of files in the data center exceeded.'))
1038            self.redirect(self.url(self.context))
1039            return
1040        if CANCEL is not None:
1041            self.redirect(self.url(self.context))
1042            return
1043        if not uploadfile:
1044            return
1045        try:
1046            filename = uploadfile.filename
1047            #if 'pending' in filename:
1048            #    self.flash(_("You can't re-upload pending data files."))
1049            #    return
1050            if not filename.endswith('.csv'):
1051                self.flash(_("Only csv files are allowed."))
1052                return
1053            normalized_filename = self.getNormalizedFileName(filename)
1054            finished_file = os.path.join(
1055                self.context.storage, 'finished', normalized_filename)
1056            unfinished_file = os.path.join(
1057                self.context.storage, 'unfinished', normalized_filename)
1058            if os.path.exists(finished_file) or os.path.exists(unfinished_file):
1059                self.flash(_("File with same name was uploaded earlier."))
1060                return
1061            target = os.path.join(self.context.storage, normalized_filename)
1062            filecontent = uploadfile.read()
1063            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1064            logger = self.context.logger
1065
1066            # Forbid certain characters in import files.
1067            for element in filecontent:
1068                try:
1069                    if ord(element) in FORBIDDEN_CHARACTERS:
1070                      self.flash(_(
1071                          "Your file contains forbidden characters. "
1072                          "Please replace."))
1073                      logger.info('%s - invalid file uploaded: %s' %
1074                          (ob_class, target))
1075                      return
1076                except TypeError:
1077                    self.flash(_(
1078                        "Your file contains forbidden characters. "
1079                        "Please replace."))
1080                    logger.info('%s - invalid file uploaded: %s' %
1081                        (ob_class, target))
1082                    return
1083
1084            open(target, 'wb').write(filecontent)
1085            os.chmod(target, 0664)
1086            logger.info('%s - uploaded: %s' % (ob_class, target))
1087            self._notifyImportManagers(filename,
1088                normalized_filename, importer, import_mode)
1089
1090        except IOError:
1091            self.flash('Error while uploading file. Please retry.')
1092            self.flash('I/O error: %s' % sys.exc_info()[1])
1093            return
1094        self.redirect(self.url(self.context))
1095
1096    def getNormalizedFileName(self, filename):
1097        """Build sane filename.
1098
1099        An uploaded file foo.csv will be stored as foo_USERNAME.csv
1100        where username is the principal id of the currently logged in
1101        user.
1102
1103        Spaces in filename are replaced by underscore.
1104        Pending data filenames remain unchanged.
1105        """
1106        if filename.endswith('.pending.csv'):
1107            return filename
1108        username = self.request.principal.id
1109        filename = filename.replace(' ', '_')
1110        # Only accept typical filname chars...
1111        filtered_username = ''.join(re.findall('[a-zA-Z0-9_\.\-]', username))
1112        base, ext = os.path.splitext(filename)
1113        return '%s_%s%s' % (base, filtered_username, ext.lower())
1114
1115    def getImporters(self):
1116        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
1117        importer_props = []
1118        for x in importers:
1119            iface_fields = schema.getFields(x.iface)
1120            available_fields = []
1121            for key in iface_fields.keys():
1122                iface_fields[key] = (iface_fields[key].__class__.__name__,
1123                    iface_fields[key].required)
1124            for value in x.available_fields:
1125                available_fields.append(
1126                    dict(f_name=value,
1127                         f_type=iface_fields.get(value, (None, False))[0],
1128                         f_required=iface_fields.get(value, (None, False))[1]
1129                         )
1130                    )
1131            available_fields = sorted(available_fields, key=lambda k: k['f_name'])
1132            importer_props.append(
1133                dict(title=x.name, name=x.util_name, fields=available_fields))
1134        return sorted(importer_props, key=lambda k: k['title'])
1135
1136class FileDownloadView(UtilityView, grok.View):
1137    grok.context(IDataCenter)
1138    grok.name('download')
1139    grok.require('waeup.manageDataCenter')
1140
1141    def update(self, filename=None):
1142        self.filename = self.request.form['filename']
1143        return
1144
1145    def render(self):
1146        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1147        self.context.logger.info(
1148            '%s - downloaded: %s' % (ob_class, self.filename))
1149        self.response.setHeader(
1150            'Content-Type', 'text/csv; charset=UTF-8')
1151        self.response.setHeader(
1152            'Content-Disposition:', 'attachment; filename="%s' %
1153            self.filename.replace('finished/',''))
1154        fullpath = os.path.join(self.context.storage, self.filename)
1155        return open(fullpath, 'rb').read()
1156
1157class SkeletonDownloadView(UtilityView, grok.View):
1158    grok.context(IDataCenter)
1159    grok.name('skeleton')
1160    grok.require('waeup.manageDataCenter')
1161
1162    def update(self, processorname=None):
1163        self.processorname = self.request.form['name']
1164        self.filename = ('%s_000.csv' %
1165            self.processorname.replace('processor','import'))
1166        return
1167
1168    def render(self):
1169        #ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1170        #self.context.logger.info(
1171        #    '%s - skeleton downloaded: %s' % (ob_class, self.filename))
1172        self.response.setHeader(
1173            'Content-Type', 'text/csv; charset=UTF-8')
1174        self.response.setHeader(
1175            'Content-Disposition:', 'attachment; filename="%s' % self.filename)
1176        processor = getUtility(IBatchProcessor, name=self.processorname)
1177        csv_data = processor.get_csv_skeleton()
1178        return csv_data
1179
1180class DatacenterImportStep1(KofaPage):
1181    """Manual import step 1: choose file
1182    """
1183    grok.context(IDataCenter)
1184    grok.name('import1')
1185    grok.template('datacenterimport1page')
1186    grok.require('waeup.manageDataCenter')
1187    label = _(u'Process CSV file')
1188    pnav = 0
1189    cancel_button =_(u'Cancel')
1190
1191    def getFiles(self):
1192        files = self.context.getPendingFiles(sort='date')
1193        for file in files:
1194            name = file.name
1195            if not name.endswith('.csv') and not name.endswith('.pending'):
1196                continue
1197            yield file
1198
1199    def update(self, filename=None, select=None, cancel=None):
1200        if cancel is not None:
1201            self.flash(_('Import aborted.'))
1202            self.redirect(self.url(self.context))
1203            return
1204        if select is not None:
1205            # A filename was selected
1206            session = ISession(self.request)['waeup.kofa']
1207            session['import_filename'] = select
1208            self.redirect(self.url(self.context, '@@import2'))
1209
1210class DatacenterImportStep2(KofaPage):
1211    """Manual import step 2: choose processor
1212    """
1213    grok.context(IDataCenter)
1214    grok.name('import2')
1215    grok.template('datacenterimport2page')
1216    grok.require('waeup.manageDataCenter')
1217    label = _(u'Process CSV file')
1218    pnav = 0
1219    cancel_button =_(u'Cancel')
1220    back_button =_(u'Back to step 1')
1221    proceed_button =_(u'Proceed to step 3')
1222
1223    filename = None
1224    mode = 'create'
1225    importer = None
1226    mode_locked = False
1227
1228    def getPreviewHeader(self):
1229        """Get the header fields of attached CSV file.
1230        """
1231        reader = csv.reader(open(self.fullpath, 'rb'))
1232        return reader.next()
1233
1234    def getPreviewTable(self):
1235        return getPreviewTable(self, 3)
1236
1237    def getImporters(self):
1238        importers = getAllUtilitiesRegisteredFor(IBatchProcessor)
1239        importers = sorted(
1240            [dict(title=x.name, name=x.util_name) for x in importers])
1241        return importers
1242
1243    def getModeFromFilename(self, filename):
1244        """Lookup filename or path and return included mode name or None.
1245        """
1246        if not filename.endswith('.pending.csv'):
1247            return None
1248        base = os.path.basename(filename)
1249        parts = base.rsplit('.', 3)
1250        if len(parts) != 4:
1251            return None
1252        if parts[1] not in ['create', 'update', 'remove']:
1253            return None
1254        return parts[1]
1255
1256    def getWarnings(self):
1257        import sys
1258        result = []
1259        try:
1260            headerfields = self.getPreviewHeader()
1261            headerfields_clean = list(set(headerfields))
1262            if len(headerfields) > len(headerfields_clean):
1263                result.append(
1264                    _("Double headers: each column name may only appear once. "))
1265        except:
1266            fatal = '%s' % sys.exc_info()[1]
1267            result.append(fatal)
1268        if result:
1269            warnings = ""
1270            for line in result:
1271                warnings += line + '<br />'
1272            warnings += _('Replace imported file!')
1273            return warnings
1274        return False
1275
1276    def update(self, mode=None, importer=None,
1277               back1=None, cancel=None, proceed=None):
1278        session = ISession(self.request)['waeup.kofa']
1279        self.filename = session.get('import_filename', None)
1280
1281        if self.filename is None or back1 is not None:
1282            self.redirect(self.url(self.context, '@@import1'))
1283            return
1284        if cancel is not None:
1285            self.flash(_('Import aborted.'))
1286            self.redirect(self.url(self.context))
1287            return
1288        self.mode = mode or session.get('import_mode', self.mode)
1289        filename_mode = self.getModeFromFilename(self.filename)
1290        if filename_mode is not None:
1291            self.mode = filename_mode
1292            self.mode_locked = True
1293        self.importer = importer or session.get('import_importer', None)
1294        session['import_importer'] = self.importer
1295        if self.importer and 'update' in self.importer:
1296            if self.mode != 'update':
1297                self.flash(_('Update mode only!'))
1298                self.mode_locked = True
1299                self.mode = 'update'
1300                proceed = None
1301        session['import_mode'] = self.mode
1302        if proceed is not None:
1303            self.redirect(self.url(self.context, '@@import3'))
1304            return
1305        self.fullpath = os.path.join(self.context.storage, self.filename)
1306        warnings = self.getWarnings()
1307        if not warnings:
1308            self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1309        else:
1310            self.reader = ()
1311            self.flash(warnings)
1312
1313class DatacenterImportStep3(KofaPage):
1314    """Manual import step 3: modify header
1315    """
1316    grok.context(IDataCenter)
1317    grok.name('import3')
1318    grok.template('datacenterimport3page')
1319    grok.require('waeup.manageDataCenter')
1320    label = _(u'Process CSV file')
1321    pnav = 0
1322    cancel_button =_(u'Cancel')
1323    reset_button =_(u'Reset')
1324    update_button =_(u'Set headerfields')
1325    back_button =_(u'Back to step 2')
1326    proceed_button =_(u'Perform import')
1327
1328    filename = None
1329    mode = None
1330    importername = None
1331
1332    @property
1333    def nextstep(self):
1334        return self.url(self.context, '@@import4')
1335
1336    def getPreviewHeader(self):
1337        """Get the header fields of attached CSV file.
1338        """
1339        reader = csv.reader(open(self.fullpath, 'rb'))
1340        return reader.next()
1341
1342    def getPreviewTable(self):
1343        """Get transposed table with 1 sample record.
1344
1345        The first column contains the headers.
1346        """
1347        if not self.reader:
1348            return
1349        headers = self.getPreviewHeader()
1350        num = 0
1351        data = []
1352        for line in self.reader:
1353            if num > 0:
1354                break
1355            num += 1
1356            data.append(line)
1357        result = []
1358        field_num = 0
1359        for name in headers:
1360            result_line = []
1361            result_line.append(field_num)
1362            field_num += 1
1363            for d in data:
1364                result_line.append(d[name])
1365            result.append(result_line)
1366        return result
1367
1368    def getPossibleHeaders(self):
1369        """Get the possible headers.
1370
1371        The headers are described as dicts {value:internal_name,
1372        title:displayed_name}
1373        """
1374        result = [dict(title='<IGNORE COL>', value='--IGNORE--')]
1375        headers = self.importer.getHeaders()
1376        result.extend([dict(title=x, value=x) for x in headers])
1377        return result
1378
1379    def getWarnings(self):
1380        import sys
1381        result = []
1382        try:
1383            self.importer.checkHeaders(self.headerfields, mode=self.mode)
1384        except:
1385            fatal = '%s' % sys.exc_info()[1]
1386            result.append(fatal)
1387        if result:
1388            warnings = ""
1389            for line in result:
1390                warnings += line + '<br />'
1391            warnings += _('Edit headers or replace imported file!')
1392            return warnings
1393        return False
1394
1395    def update(self, headerfield=None, back2=None, cancel=None, proceed=None):
1396        datatable.need()
1397        session = ISession(self.request)['waeup.kofa']
1398        self.filename = session.get('import_filename', None)
1399        self.mode = session.get('import_mode', None)
1400        self.importername = session.get('import_importer', None)
1401
1402        if None in (self.filename, self.mode, self.importername):
1403            self.redirect(self.url(self.context, '@@import2'))
1404            return
1405        if back2 is not None:
1406            self.redirect(self.url(self.context ,'@@import2'))
1407            return
1408        if cancel is not None:
1409            self.flash(_('Import aborted.'))
1410            self.redirect(self.url(self.context))
1411            return
1412
1413        self.fullpath = os.path.join(self.context.storage, self.filename)
1414        self.headerfields = headerfield or self.getPreviewHeader()
1415        session['import_headerfields'] = self.headerfields
1416
1417        if proceed is not None:
1418            self.redirect(self.url(self.context, '@@import4'))
1419            return
1420        self.importer = getUtility(IBatchProcessor, name=self.importername)
1421        self.reader = csv.DictReader(open(self.fullpath, 'rb'))
1422        warnings = self.getWarnings()
1423        if warnings:
1424            self.flash(warnings)
1425
1426class DatacenterImportStep4(KofaPage):
1427    """Manual import step 4: do actual import
1428    """
1429    grok.context(IDataCenter)
1430    grok.name('import4')
1431    grok.template('datacenterimport4page')
1432    grok.require('waeup.importData')
1433    label = _(u'Process CSV file')
1434    pnav = 0
1435    back_button =_(u'Process next')
1436
1437    filename = None
1438    mode = None
1439    importername = None
1440    headerfields = None
1441    warnnum = None
1442
1443    def update(self, back=None, finish=None, showlog=None):
1444        if finish is not None:
1445            self.redirect(self.url(self.context, '@@import1'))
1446            return
1447        session = ISession(self.request)['waeup.kofa']
1448        self.filename = session.get('import_filename', None)
1449        self.mode = session.get('import_mode', None)
1450        self.importername = session.get('import_importer', None)
1451        # If the import file contains only one column
1452        # the import_headerfields attribute is a string.
1453        ihf = session.get('import_headerfields', None)
1454        if not isinstance(ihf, list):
1455            self.headerfields = ihf.split()
1456        else:
1457            self.headerfields = ihf
1458
1459        if None in (self.filename, self.mode, self.importername,
1460                    self.headerfields):
1461            self.redirect(self.url(self.context, '@@import3'))
1462            return
1463
1464        if showlog is not None:
1465            logfilename = "datacenter.log"
1466            session['logname'] = logfilename
1467            self.redirect(self.url(self.context, '@@show'))
1468            return
1469
1470        self.fullpath = os.path.join(self.context.storage, self.filename)
1471        self.importer = getUtility(IBatchProcessor, name=self.importername)
1472
1473        # Perform batch processing...
1474        # XXX: This might be better placed in datacenter module.
1475        (linenum, self.warn_num,
1476         fin_path, pending_path) = self.importer.doImport(
1477            self.fullpath, self.headerfields, self.mode,
1478            self.request.principal.id, logger=self.context.logger)
1479        # Put result files in desired locations...
1480        self.context.distProcessedFiles(
1481            self.warn_num == 0, self.fullpath, fin_path, pending_path,
1482            self.mode)
1483
1484        if self.warn_num:
1485            self.flash(_('Processing of ${a} rows failed.',
1486                mapping = {'a':self.warn_num}))
1487        self.flash(_('Successfully processed ${a} rows.',
1488            mapping = {'a':linenum - self.warn_num}))
1489
1490class DatacenterLogsOverview(KofaPage):
1491    grok.context(IDataCenter)
1492    grok.name('logs')
1493    grok.template('datacenterlogspage')
1494    grok.require('waeup.manageDataCenter')
1495    label = _(u'Show logfiles')
1496    pnav = 0
1497    back_button = _(u'Back to Data Center')
1498    show_button = _(u'Show')
1499
1500    def update(self, back=None):
1501        if back is not None:
1502            self.redirect(self.url(self.context))
1503            return
1504        self.files = self.context.getLogFiles()
1505
1506class DatacenterLogsFileview(KofaPage):
1507    grok.context(IDataCenter)
1508    grok.name('show')
1509    grok.template('datacenterlogsshowfilepage')
1510    grok.require('waeup.manageDataCenter')
1511    title = _(u'Data Center')
1512    pnav = 0
1513    search_button = _('Search')
1514    back_button = _('Back')
1515    placeholder = _('Enter a regular expression here...')
1516
1517    def label(self):
1518        return "Logfile %s" % self.filename
1519
1520    def update(self, back=None, query=None, logname=None):
1521        if os.name != 'posix':
1522            self.flash(
1523                _('Log files can only be searched ' +
1524                  'on Unix-based operating systems.'))
1525            self.redirect(self.url(self.context, '@@logs'))
1526            return
1527        if back is not None or logname is None:
1528            self.redirect(self.url(self.context, '@@logs'))
1529            return
1530        self.filename = logname
1531        self.query = query
1532        if search is None or not query:
1533            return
1534        try:
1535            self.result = ''.join(
1536                self.context.queryLogfiles(logname, query))
1537        except ValueError:
1538            self.flash(_('Invalid search expression.'))
1539            return
1540        if not self.result:
1541            self.flash(_('No search results found.'))
1542        return
1543
1544class DatacenterSettings(KofaPage):
1545    grok.context(IDataCenter)
1546    grok.name('manage')
1547    grok.template('datacentermanagepage')
1548    grok.require('waeup.managePortal')
1549    label = _('Edit data center settings')
1550    pnav = 0
1551    save_button =_(u'Save')
1552    reset_button =_(u'Reset')
1553    cancel_button =_(u'Cancel')
1554
1555    def update(self, newpath=None, move=False, overwrite=False,
1556               save=None, cancel=None):
1557        if move:
1558            move = True
1559        if overwrite:
1560            overwrite = True
1561        if newpath is None:
1562            return
1563        if cancel is not None:
1564            self.redirect(self.url(self.context))
1565            return
1566        try:
1567            not_copied = self.context.setStoragePath(newpath, move=move)
1568            for name in not_copied:
1569                self.flash(_('File already existed (not copied): ${a}',
1570                    mapping = {'a':name}))
1571        except:
1572            self.flash(_('Given storage path cannot be used.'))
1573            self.flash(_('Error: ${a}', mapping = {'a':sys.exc_info()[1]}))
1574            return
1575        if newpath:
1576            self.flash(_('New storage path succefully set.'))
1577            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1578            self.context.logger.info(
1579                '%s - storage path set: %s' % (ob_class, newpath))
1580            self.redirect(self.url(self.context))
1581        return
1582
1583class ExportCSVPage(KofaPage):
1584    grok.context(IDataCenter)
1585    grok.name('export')
1586    grok.template('datacenterexportpage')
1587    grok.require('waeup.exportData')
1588    label = _('Download portal data as CSV file')
1589    pnav = 0
1590    export_button = _(u'Create CSV file')
1591
1592    def getExporters(self):
1593        utils = getUtilitiesFor(ICSVExporter)
1594        title_name_tuples = [
1595            (util.title, name) for name, util in utils
1596            if not name in STUDENT_EXPORTERS]
1597        # The exporter for access codes requires a special permission.
1598        if not checkPermission('waeup.manageACBatches', self.context):
1599            title_name_tuples.remove((u'AccessCodes', u'accesscodes'))
1600        return sorted(title_name_tuples)
1601
1602    def update(self, CREATE=None, DISCARD=None, exporter=None, job_id=None):
1603        if CREATE:
1604            job_id = self.context.start_export_job(
1605                exporter, self.request.principal.id)
1606            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1607            self.context.logger.info(
1608                '%s - exported: %s, job_id=%s' % (ob_class, exporter, job_id))
1609        if DISCARD and job_id:
1610            entry = self.context.entry_from_job_id(job_id)
1611            self.context.delete_export_entry(entry)
1612            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1613            self.context.logger.info(
1614                '%s - discarded: job_id=%s' % (ob_class, job_id))
1615            self.flash(_('Discarded export') + ' %s' % job_id)
1616        self.entries = doll_up(self, user=None)
1617        return
1618
1619class ExportCSVView(grok.View):
1620    grok.context(IDataCenter)
1621    grok.name('download_export')
1622    grok.require('waeup.exportData')
1623
1624    def render(self, job_id=None):
1625        manager = getUtility(IJobManager)
1626        job = manager.get(job_id)
1627        if job is None:
1628            return
1629        if hasattr(job.result, 'traceback'):
1630            # XXX: Some error happened. Do something more approriate here...
1631            return
1632        path = job.result
1633        if not os.path.exists(path):
1634            # XXX: Do something more appropriate here...
1635            return
1636        result = open(path, 'rb').read()
1637        acronym = grok.getSite()['configuration'].acronym.replace(' ','')
1638        filename = "%s_%s" % (acronym, os.path.basename(path))
1639        filename = filename.replace('.csv', '_%s.csv' % job_id)
1640        self.response.setHeader(
1641            'Content-Type', 'text/csv; charset=UTF-8')
1642        self.response.setHeader(
1643            'Content-Disposition', 'attachment; filename="%s' % filename)
1644        # remove job and running_exports entry from context
1645        #self.context.delete_export_entry(
1646        #    self.context.entry_from_job_id(job_id))
1647        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1648        self.context.logger.info(
1649            '%s - downloaded: %s, job_id=%s' % (ob_class, filename, job_id))
1650        return result
1651
1652class ExportXMLPage(grok.View):
1653    """Deliver an XML representation of the context.
1654    """
1655    grok.name('export.xml')
1656    grok.require('waeup.managePortal')
1657
1658    def render(self):
1659        exporter = IKofaXMLExporter(self.context)
1660        xml = exporter.export().read()
1661        self.response.setHeader(
1662            'Content-Type', 'text/xml; charset=UTF-8')
1663        return xml
1664
1665class ImportXMLPage(KofaPage):
1666    """Replace the context object by an object created from an XML
1667       representation.
1668
1669       XXX: This does not work for ISite objects, i.e. for instance
1670            for complete University objects.
1671    """
1672    grok.name('importxml')
1673    grok.require('waeup.managePortal')
1674
1675    def update(self, xmlfile=None, CANCEL=None, SUBMIT=None):
1676        if CANCEL is not None:
1677            self.redirect(self.url(self.context))
1678            return
1679        if not xmlfile:
1680            return
1681        importer = IKofaXMLImporter(self.context)
1682        obj = importer.doImport(xmlfile)
1683        if type(obj) != type(self.context):
1684            return
1685        parent = self.context.__parent__
1686        name = self.context.__name__
1687        self.context = obj
1688        if hasattr(parent, name):
1689            setattr(parent, name, obj)
1690        else:
1691            del parent[name]
1692            parent[name] = obj
1693            pass
1694        return
1695
1696
1697#
1698# FacultiesContainer pages...
1699#
1700
1701class FacultiesContainerPage(KofaPage):
1702    """ Index page for faculty containers.
1703    """
1704    grok.context(IFacultiesContainer)
1705    grok.require('waeup.viewAcademics')
1706    grok.name('index')
1707    label = _('Academic Section')
1708    pnav = 1
1709    grok.template('facultypage')
1710
1711class FacultiesContainerManageFormPage(KofaEditFormPage):
1712    """Manage the basic properties of a `Faculty` instance.
1713    """
1714    grok.context(IFacultiesContainer)
1715    grok.name('manage')
1716    grok.require('waeup.manageAcademics')
1717    grok.template('facultiescontainermanagepage')
1718    pnav = 1
1719    taboneactions = [_('Add faculty'), _('Remove selected'),_('Cancel')]
1720    subunits = _('Faculties')
1721
1722    @property
1723    def label(self):
1724        return _('Manage academic section')
1725
1726    def update(self):
1727        warning.need()
1728        return super(FacultiesContainerManageFormPage, self).update()
1729
1730    @jsaction(_('Remove selected'))
1731    def delFaculties(self, **data):
1732        if not checkPermission('waeup.managePortal', self.context):
1733            self.flash(_('You are not allowed to remove entire faculties.'))
1734            return
1735        delSubobjects(self, redirect='@@manage', tab='1')
1736        return
1737
1738    @action(_('Add faculty'), validator=NullValidator)
1739    def addFaculty(self, **data):
1740        self.redirect(self.url(self.context, '@@add'))
1741        return
1742
1743    @action(_('Cancel'), validator=NullValidator)
1744    def cancel(self, **data):
1745        self.redirect(self.url(self.context))
1746        return
1747
1748
1749class FacultyAddFormPage(KofaAddFormPage):
1750    """ Page form to add a new faculty to a faculty container.
1751    """
1752    grok.context(IFacultiesContainer)
1753    grok.require('waeup.manageAcademics')
1754    grok.name('add')
1755    label = _('Add faculty')
1756    form_fields = grok.AutoFields(IFacultyAdd)
1757    pnav = 1
1758
1759    @action(_('Add faculty'), style='primary')
1760    def addFaculty(self, **data):
1761        faculty = createObject(u'waeup.Faculty')
1762        self.applyData(faculty, **data)
1763        try:
1764            self.context.addFaculty(faculty)
1765        except KeyError:
1766            self.flash(_('The faculty code chosen already exists.'))
1767            return
1768        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1769        self.context.__parent__.logger.info(
1770            '%s - added: %s' % (ob_class, faculty.code))
1771        self.redirect(self.url(self.context, u'@@manage')+'?tab1')
1772        return
1773
1774    @action(_('Cancel'), validator=NullValidator)
1775    def cancel(self, **data):
1776        self.redirect(self.url(self.context))
1777
1778#
1779# Faculty pages
1780#
1781class FacultyPage(KofaPage):
1782    """Index page of faculties.
1783    """
1784    grok.context(IFaculty)
1785    grok.require('waeup.viewAcademics')
1786    grok.name('index')
1787    pnav = 1
1788
1789    @property
1790    def label(self):
1791        return _('Departments')
1792
1793class FacultyManageFormPage(KofaEditFormPage,
1794                            LocalRoleAssignmentUtilityView):
1795    """Manage the basic properties of a `Faculty` instance.
1796    """
1797    grok.context(IFaculty)
1798    grok.name('manage')
1799    grok.require('waeup.manageAcademics')
1800    grok.template('facultymanagepage')
1801    pnav = 1
1802    subunits = _('Departments')
1803    taboneactions = [_('Save'),_('Cancel')]
1804    tabtwoactions = [_('Add department'), _('Remove selected'),_('Cancel')]
1805    tabthreeactions1 = [_('Remove selected local roles')]
1806    tabthreeactions2 = [_('Add local role')]
1807
1808    form_fields = grok.AutoFields(IFaculty)
1809
1810    @property
1811    def label(self):
1812        return _('Manage faculty')
1813
1814    def update(self):
1815        tabs.need()
1816        self.tab1 = self.tab2 = self.tab3 = ''
1817        qs = self.request.get('QUERY_STRING', '')
1818        if not qs:
1819            qs = 'tab1'
1820        setattr(self, qs, 'active')
1821        warning.need()
1822        datatable.need()
1823        return super(FacultyManageFormPage, self).update()
1824
1825    @jsaction(_('Remove selected'))
1826    def delDepartments(self, **data):
1827        if not checkPermission('waeup.managePortal', self.context):
1828            self.flash(_('You are not allowed to remove entire departments.'))
1829            return
1830        delSubobjects(self, redirect='@@manage', tab='2')
1831        return
1832
1833    @action(_('Save'), style='primary')
1834    def save(self, **data):
1835        return msave(self, **data)
1836
1837    @action(_('Cancel'), validator=NullValidator)
1838    def cancel(self, **data):
1839        self.redirect(self.url(self.context))
1840        return
1841
1842    @action(_('Add department'), validator=NullValidator)
1843    def addSubunit(self, **data):
1844        self.redirect(self.url(self.context, '@@add'))
1845        return
1846
1847    @action(_('Add local role'), validator=NullValidator)
1848    def addLocalRole(self, **data):
1849        return add_local_role(self, '3', **data)
1850
1851    @action(_('Remove selected local roles'))
1852    def delLocalRoles(self, **data):
1853        return del_local_roles(self,3,**data)
1854
1855class FindStudentsPage(KofaPage):
1856    """Search students in faculty.
1857    """
1858    grok.context(IFaculty)
1859    grok.name('find_students')
1860    grok.require('waeup.showStudents')
1861    grok.template('findstudentspage')
1862    label = _('Find students')
1863    search_button = _('Find student')
1864    pnav = 1
1865
1866    def _find_students(self,query=None, searchtype=None, view=None):
1867        hitlist = []
1868        if searchtype in ('fullname',):
1869            results = Query().searchResults(
1870                Text(('students_catalog', searchtype), query) &
1871                Eq(('students_catalog', 'faccode'), self.context.code)
1872                )
1873        else:
1874            results = Query().searchResults(
1875                Eq(('students_catalog', searchtype), query) &
1876                Eq(('students_catalog', 'faccode'), self.context.code)
1877                )
1878        for result in results:
1879            hitlist.append(StudentQueryResultItem(result, view=view))
1880        return hitlist
1881
1882    def update(self, *args, **kw):
1883        datatable.need()
1884        form = self.request.form
1885        self.hitlist = []
1886        if 'searchterm' in form and form['searchterm']:
1887            self.searchterm = form['searchterm']
1888            self.searchtype = form['searchtype']
1889        elif 'old_searchterm' in form:
1890            self.searchterm = form['old_searchterm']
1891            self.searchtype = form['old_searchtype']
1892        else:
1893            if 'search' in form:
1894                self.flash(_('Empty search string'))
1895            return
1896        self.hitlist = self._find_students(query=self.searchterm,
1897            searchtype=self.searchtype, view=self)
1898        if not self.hitlist:
1899            self.flash(_('No student found.'))
1900        return
1901
1902class DepartmentAddFormPage(KofaAddFormPage):
1903    """Add a department to a faculty.
1904    """
1905    grok.context(IFaculty)
1906    grok.name('add')
1907    grok.require('waeup.manageAcademics')
1908    label = _('Add department')
1909    form_fields = grok.AutoFields(IDepartmentAdd)
1910    pnav = 1
1911
1912    @action(_('Add department'), style='primary')
1913    def addDepartment(self, **data):
1914        department = createObject(u'waeup.Department')
1915        self.applyData(department, **data)
1916        try:
1917            self.context.addDepartment(department)
1918        except KeyError:
1919            self.flash(_('The code chosen already exists in this faculty.'))
1920            return
1921        self.status = self.flash(
1922            _("Department ${a} added.", mapping = {'a':data['code']}))
1923        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
1924        self.context.__parent__.__parent__.logger.info(
1925            '%s - added: %s' % (ob_class, data['code']))
1926        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
1927        return
1928
1929    @action(_('Cancel'), validator=NullValidator)
1930    def cancel(self, **data):
1931        self.redirect(self.url(self.context))
1932        return
1933
1934#
1935# Department pages
1936#
1937class DepartmentPage(KofaPage):
1938    """Department index page.
1939    """
1940    grok.context(IDepartment)
1941    grok.require('waeup.viewAcademics')
1942    grok.name('index')
1943    pnav = 1
1944    label = _('Courses and Certificates')
1945
1946    def update(self):
1947        tabs.need()
1948        datatable.need()
1949        super(DepartmentPage, self).update()
1950        return
1951
1952    def getCourses(self):
1953        """Get a list of all stored courses.
1954        """
1955        for key, val in self.context.courses.items():
1956            url = self.url(val)
1957            yield(dict(url=url, name=key, container=val))
1958
1959    def getCertificates(self):
1960        """Get a list of all stored certificates.
1961        """
1962        for key, val in self.context.certificates.items():
1963            url = self.url(val)
1964            yield(dict(url=url, name=key, container=val))
1965
1966class DepartmentManageFormPage(KofaEditFormPage,
1967                               LocalRoleAssignmentUtilityView):
1968    """Manage the basic properties of a `Department` instance.
1969    """
1970    grok.context(IDepartment)
1971    grok.name('manage')
1972    grok.require('waeup.manageAcademics')
1973    pnav = 1
1974    grok.template('departmentmanagepage')
1975    taboneactions = [_('Save'),_('Cancel')]
1976    tabtwoactions = [_('Add course'), _('Remove selected courses'),_('Cancel')]
1977    tabthreeactions = [_('Add certificate'), _('Remove selected certificates'),
1978                       _('Cancel')]
1979    tabfouractions1 = [_('Remove selected local roles')]
1980    tabfouractions2 = [_('Add local role')]
1981
1982    form_fields = grok.AutoFields(IDepartment)
1983
1984    @property
1985    def label(self):
1986        return _('Manage department')
1987
1988    def getCourses(self):
1989        """Get a list of all stored courses.
1990        """
1991        for key, val in self.context.courses.items():
1992            url = self.url(val)
1993            yield(dict(url=url, name=key, container=val))
1994
1995    def getCertificates(self):
1996        """Get a list of all stored certificates.
1997        """
1998        for key, val in self.context.certificates.items():
1999            url = self.url(val)
2000            yield(dict(url=url, name=key, container=val))
2001
2002    def update(self):
2003        tabs.need()
2004        self.tab1 = self.tab2 = self.tab3 = self.tab4 = ''
2005        qs = self.request.get('QUERY_STRING', '')
2006        if not qs:
2007            qs = 'tab1'
2008        setattr(self, qs, 'active')
2009        warning.need()
2010        datatable.need()
2011        super(DepartmentManageFormPage, self).update()
2012        return
2013
2014    @action(_('Save'), style='primary')
2015    def save(self, **data):
2016        return msave(self, **data)
2017
2018    @jsaction(_('Remove selected courses'))
2019    def delCourses(self, **data):
2020        delSubobjects(
2021            self, redirect='@@manage', tab='2', subcontainer='courses')
2022        return
2023
2024    @jsaction(_('Remove selected certificates'))
2025    def delCertificates(self, **data):
2026        if not checkPermission('waeup.managePortal', self.context):
2027            self.flash(_('You are not allowed to remove certificates.'))
2028            return
2029        delSubobjects(
2030            self, redirect='@@manage', tab='3', subcontainer='certificates')
2031        return
2032
2033    @action(_('Add course'), validator=NullValidator)
2034    def addCourse(self, **data):
2035        self.redirect(self.url(self.context, 'addcourse'))
2036        return
2037
2038    @action(_('Add certificate'), validator=NullValidator)
2039    def addCertificate(self, **data):
2040        self.redirect(self.url(self.context, 'addcertificate'))
2041        return
2042
2043    @action(_('Cancel'), validator=NullValidator)
2044    def cancel(self, **data):
2045        self.redirect(self.url(self.context))
2046        return
2047
2048    @action(_('Add local role'), validator=NullValidator)
2049    def addLocalRole(self, **data):
2050        return add_local_role(self, 4, **data)
2051
2052    @action(_('Remove selected local roles'))
2053    def delLocalRoles(self, **data):
2054        return del_local_roles(self,4,**data)
2055
2056class CourseAddFormPage(KofaAddFormPage):
2057    """Add-form to add course to a department.
2058    """
2059    grok.context(IDepartment)
2060    grok.name('addcourse')
2061    grok.require('waeup.manageAcademics')
2062    label = _(u'Add course')
2063    form_fields = grok.AutoFields(ICourseAdd)
2064    pnav = 1
2065
2066    @action(_('Add course'))
2067    def addCourse(self, **data):
2068        course = createObject(u'waeup.Course')
2069        self.applyData(course, **data)
2070        try:
2071            self.context.courses.addCourse(course)
2072        except DuplicationError, error:
2073            # signals duplication error
2074            entries = error.entries
2075            for entry in entries:
2076                # do not use error.msg but render a more detailed message instead
2077                # and show links to all certs with same code
2078                message = _('A course with same code already exists: ')
2079                message += '<a href="%s">%s</a>' % (
2080                    self.url(entry), getattr(entry, '__name__', u'Unnamed'))
2081                self.flash(message)
2082            self.redirect(self.url(self.context, u'@@addcourse'))
2083            return
2084        message = _(u'Course ${a} successfully created.', mapping = {'a':course.code})
2085        self.flash(message)
2086        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2087        self.context.__parent__.__parent__.__parent__.logger.info(
2088            '%s - added: %s' % (ob_class, data['code']))
2089        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
2090        return
2091
2092    @action(_('Cancel'), validator=NullValidator)
2093    def cancel(self, **data):
2094        self.redirect(self.url(self.context))
2095        return
2096
2097class CertificateAddFormPage(KofaAddFormPage):
2098    """Add-form to add certificate to a department.
2099    """
2100    grok.context(IDepartment)
2101    grok.name('addcertificate')
2102    grok.require('waeup.manageAcademics')
2103    label = _(u'Add certificate')
2104    form_fields = grok.AutoFields(ICertificateAdd)
2105    pnav = 1
2106
2107    @action(_('Add certificate'))
2108    def addCertificate(self, **data):
2109        certificate = createObject(u'waeup.Certificate')
2110        self.applyData(certificate, **data)
2111        try:
2112            self.context.certificates.addCertificate(certificate)
2113        except DuplicationError, error:
2114            # signals duplication error
2115            entries = error.entries
2116            for entry in entries:
2117                # do not use error.msg but render a more detailed message instead
2118                # and show links to all certs with same code
2119                message = _('A certificate with same code already exists: ')
2120                message += '<a href="%s">%s</a>' % (
2121                    self.url(entry), getattr(entry, '__name__', u'Unnamed'))
2122                self.flash(message)
2123            self.redirect(self.url(self.context, u'@@addcertificate'))
2124            return
2125        message = _(u'Certificate ${a} successfully created.', mapping = {'a':certificate.code})
2126        self.flash(message)
2127        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2128        self.context.__parent__.__parent__.__parent__.logger.info(
2129            '%s - added: %s' % (ob_class, data['code']))
2130        self.redirect(self.url(self.context, u'@@manage')+'?tab3')
2131        return
2132
2133    @action(_('Cancel'), validator=NullValidator)
2134    def cancel(self): #, **data):
2135        self.redirect(self.url(self.context))
2136        return
2137
2138#
2139# Courses pages
2140#
2141class CoursePage(KofaDisplayFormPage):
2142    """Course index page.
2143    """
2144    grok.context(ICourse)
2145    grok.name('index')
2146    grok.require('waeup.viewAcademics')
2147    pnav = 1
2148    form_fields = grok.AutoFields(ICourse)
2149
2150    @property
2151    def label(self):
2152        return '%s (%s)' % (self.context.title, self.context.code)
2153
2154class CourseManageFormPage(KofaEditFormPage,
2155                           LocalRoleAssignmentUtilityView):
2156    """Edit form page for courses.
2157    """
2158    grok.context(ICourse)
2159    grok.name('manage')
2160    grok.require('waeup.manageAcademics')
2161    grok.template('coursemanagepage')
2162    label = _(u'Edit course')
2163    pnav = 1
2164    taboneactions = [_('Save'),_('Cancel')]
2165    tabtwoactions1 = [_('Remove selected local roles')]
2166    tabtwoactions2 = [_('Add local role')]
2167
2168    form_fields = grok.AutoFields(ICourse)
2169
2170    def update(self):
2171        tabs.need()
2172        self.tab1 = self.tab2 = ''
2173        qs = self.request.get('QUERY_STRING', '')
2174        if not qs:
2175            qs = 'tab1'
2176        setattr(self, qs, 'active')
2177        warning.need()
2178        datatable.need()
2179        return super(CourseManageFormPage, self).update()
2180
2181    @action(_('Save'), style='primary')
2182    def save(self, **data):
2183        return msave(self, **data)
2184
2185    @action(_('Cancel'), validator=NullValidator)
2186    def cancel(self, **data):
2187        self.redirect(self.url(self.context))
2188        return
2189
2190    @action(_('Add local role'), validator=NullValidator)
2191    def addLocalRole(self, **data):
2192        return add_local_role(self, 2, **data)
2193
2194    @action(_('Remove selected local roles'))
2195    def delLocalRoles(self, **data):
2196        return del_local_roles(self,2,**data)
2197
2198#
2199# Certificate pages
2200#
2201class CertificatePage(KofaDisplayFormPage):
2202    """Index page for certificates.
2203    """
2204    grok.context(ICertificate)
2205    grok.name('index')
2206    grok.require('waeup.viewAcademics')
2207    pnav = 1
2208    form_fields = grok.AutoFields(ICertificate)
2209    grok.template('certificatepage')
2210
2211    @property
2212    def label(self):
2213        return "%s (%s)" % (self.context.title, self.context.code)
2214
2215    def update(self):
2216        datatable.need()
2217        return super(CertificatePage, self).update()
2218
2219class CertificateManageFormPage(KofaEditFormPage,
2220                                LocalRoleAssignmentUtilityView):
2221    """Manage the properties of a `Certificate` instance.
2222    """
2223    grok.context(ICertificate)
2224    grok.name('manage')
2225    grok.require('waeup.manageAcademics')
2226    pnav = 1
2227    label = _('Edit certificate')
2228
2229    form_fields = grok.AutoFields(ICertificate)
2230
2231    pnav = 1
2232    grok.template('certificatemanagepage')
2233    taboneactions = [_('Save'),_('Cancel')]
2234    tabtwoactions = [_('Add certificate course'),
2235                     _('Remove selected certificate courses'),_('Cancel')]
2236    tabthreeactions1 = [_('Remove selected local roles')]
2237    tabthreeactions2 = [_('Add local role')]
2238
2239    @property
2240    def label(self):
2241        return _('Manage certificate')
2242
2243    def update(self):
2244        tabs.need()
2245        self.tab1 = self.tab2 = self.tab3 = ''
2246        qs = self.request.get('QUERY_STRING', '')
2247        if not qs:
2248            qs = 'tab1'
2249        setattr(self, qs, 'active')
2250        warning.need()
2251        datatable.need()
2252        return super(CertificateManageFormPage, self).update()
2253
2254    @action(_('Save'), style='primary')
2255    def save(self, **data):
2256        return msave(self, **data)
2257
2258    @jsaction(_('Remove selected certificate courses'))
2259    def delCertificateCourses(self, **data):
2260        delSubobjects(self, redirect='@@manage', tab='2')
2261        return
2262
2263    @action(_('Add certificate course'), validator=NullValidator)
2264    def addCertificateCourse(self, **data):
2265        self.redirect(self.url(self.context, 'addcertificatecourse'))
2266        return
2267
2268    @action(_('Cancel'), validator=NullValidator)
2269    def cancel(self, **data):
2270        self.redirect(self.url(self.context))
2271        return
2272
2273    @action(_('Add local role'), validator=NullValidator)
2274    def addLocalRole(self, **data):
2275        return add_local_role(self, 3, **data)
2276
2277    @action(_('Remove selected local roles'))
2278    def delLocalRoles(self, **data):
2279        return del_local_roles(self,3,**data)
2280
2281
2282class CertificateCourseAddFormPage(KofaAddFormPage):
2283    """Add-page to add a course ref to a certificate
2284    """
2285    grok.context(ICertificate)
2286    grok.name('addcertificatecourse')
2287    grok.require('waeup.manageAcademics')
2288    form_fields = grok.AutoFields(ICertificateCourseAdd)
2289    pnav = 1
2290    label = _('Add certificate course')
2291
2292    @action(_('Add certificate course'))
2293    def addCertcourse(self, **data):
2294        try:
2295            self.context.addCertCourse(**data)
2296        except KeyError:
2297            self.status = self.flash(_('The chosen certificate course is already '
2298                                  'part of this certificate.'))
2299            return
2300        self.status = self.flash(
2301            _("certificate course ${a}_${b} added.",
2302            mapping = {'a': data['course'].code, 'b': data['level']}))
2303        code = "%s_%s" % (data['course'].code, data['level'])
2304        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2305        grok.getSite().logger.info('%s - added: %s' % (ob_class, code))
2306        self.redirect(self.url(self.context, u'@@manage')+'?tab2')
2307        return
2308
2309    @action(_('Cancel'), validator=NullValidator)
2310    def cancel(self, **data):
2311        self.redirect(self.url(self.context))
2312        return
2313
2314
2315#
2316# Certificate course pages...
2317#
2318class CertificateCoursePage(KofaPage):
2319    """CertificateCourse index page.
2320    """
2321    grok.context(ICertificateCourse)
2322    grok.name('index')
2323    grok.require('waeup.viewAcademics')
2324    pnav = 1
2325    #form_fields = grok.AutoFields(ICertificateCourse)
2326
2327    @property
2328    def label(self):
2329        return '%s' % (self.context.longtitle())
2330
2331    @property
2332    def leveltitle(self):
2333        return course_levels.getTerm(self.context.level).title
2334
2335class CertificateCourseManageFormPage(KofaEditFormPage):
2336    """Manage the basic properties of a `CertificateCourse` instance.
2337    """
2338    grok.context(ICertificateCourse)
2339    grok.name('manage')
2340    grok.require('waeup.manageAcademics')
2341    form_fields = grok.AutoFields(ICertificateCourse)
2342    label = _('Edit certificate course')
2343    pnav = 1
2344
2345    @action(_('Save and return'), style='primary')
2346    def saveAndReturn(self, **data):
2347        parent = self.context.__parent__
2348        if self.context.level == data['level']:
2349            msave(self, **data)
2350        else:
2351            # The level changed. We have to create a new certcourse as
2352            # the key of the entry will change as well...
2353            old_level = self.context.level
2354            data['course'] = self.context.course
2355            parent.addCertCourse(**data)
2356            parent.delCertCourses(data['course'].code, level=old_level)
2357            self.flash(_('Form has been saved.'))
2358            old_code = "%s_%s" % (data['course'].code, old_level)
2359            new_code = "%s_%s" % (data['course'].code, data['level'])
2360            ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2361            grok.getSite().logger.info(
2362                '%s - renamed: %s to %s' % (ob_class, old_code, new_code))
2363        self.redirect(self.url(parent))
2364        return
2365
2366    @action(_('Cancel'), validator=NullValidator)
2367    def cancel(self, **data):
2368        self.redirect(self.url(self.context))
2369        return
2370
2371class ChangePasswordRequestPage(KofaForm):
2372    """Captcha'd page for all kind of users to request a password change.
2373    """
2374    grok.context(IUniversity)
2375    grok.name('changepw')
2376    grok.require('waeup.Anonymous')
2377    grok.template('changepw')
2378    label = _('Send me a new password')
2379    form_fields = grok.AutoFields(IChangePassword)
2380
2381    def update(self):
2382        # Handle captcha
2383        self.captcha = getUtility(ICaptchaManager).getCaptcha()
2384        self.captcha_result = self.captcha.verify(self.request)
2385        self.captcha_code = self.captcha.display(self.captcha_result.error_code)
2386        return
2387
2388    def _searchUser(self, identifier, email):
2389        # Search student
2390        cat = queryUtility(ICatalog, name='students_catalog')
2391        results = cat.searchResults(
2392            #reg_number=(identifier, identifier),
2393            email=(email,email))
2394        for result in results:
2395            if result.student_id == identifier or \
2396                result.reg_number == identifier or \
2397                result.matric_number == identifier:
2398                return result
2399        # Search applicant
2400        cat = queryUtility(ICatalog, name='applicants_catalog')
2401        if cat is not None:
2402            results = cat.searchResults(
2403                #reg_number=(identifier, identifier),
2404                email=(email,email))
2405            for result in results:
2406                if result.applicant_id == identifier or \
2407                    result.reg_number == identifier:
2408                    return result
2409        # Search portal user
2410        user = grok.getSite()['users'].get(identifier, None)
2411        if user is not None and user.email == email:
2412            return user
2413        return None
2414
2415    @action(_('Send login credentials to email address'), style='primary')
2416    def request(self, **data):
2417        if not self.captcha_result.is_valid:
2418            # Captcha will display error messages automatically.
2419            # No need to flash something.
2420            return
2421        # Search student or applicant
2422        identifier = data['identifier']
2423        email = data['email']
2424        user = self._searchUser(identifier, email)
2425        if user is None:
2426            self.flash(_('No record found.'))
2427            return
2428        # Change password
2429        kofa_utils = getUtility(IKofaUtils)
2430        password = kofa_utils.genPassword()
2431        mandate = PasswordMandate()
2432        mandate.params['password'] = password
2433        mandate.params['user'] = user
2434        site = grok.getSite()
2435        site['mandates'].addMandate(mandate)
2436        # Send email with credentials
2437        args = {'mandate_id':mandate.mandate_id}
2438        mandate_url = self.url(site) + '/mandate?%s' % urlencode(args)
2439        url_info = u'Confirmation link: %s' % mandate_url
2440        msg = _('You have successfully requested a password for the')
2441        success = kofa_utils.sendCredentials(
2442            IUserAccount(user),password,url_info,msg)
2443        if success:
2444            self.flash(_('An email with your user name and password ' +
2445                'has been sent to ${a}.', mapping = {'a':email}))
2446        else:
2447            self.flash(_('An smtp server error occurred.'))
2448        ob_class = self.__implemented__.__name__.replace('waeup.kofa.','')
2449        self.context.logger.info(
2450            '%s - %s - %s' % (ob_class, data['identifier'], data['email']))
2451        return
Note: See TracBrowser for help on using the repository browser.