source: main/waeup.kofa/branches/uli-diazo-themed/src/waeup/kofa/browser/pages.py @ 11005

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

First adjustments in datacenter.

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