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

Last change on this file since 15286 was 15286, checked in by Henrik Bettermann, 6 years ago

Increase password strength. Officers are now required
to set a password which has at least 8 characters, contains
at least one uppercase letter, one lowercase letter and one
digit.

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