source: main/waeup.aaue/trunk/src/waeup/aaue/applicants/browser.py @ 14842

Last change on this file since 14842 was 14840, checked in by Henrik Bettermann, 7 years ago

Show screening date and venue only if student has submitted the application.

  • Property svn:keywords set to Id
File size: 20.8 KB
RevLine 
[10298]1## $Id: browser.py 14840 2017-09-06 07:26:54Z 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"""UI components for basic applicants and related components.
19"""
20import grok
21import os
[13996]22from zope.component import getUtility, getAdapter
23from zope.i18n import translate
[14027]24from hurry.workflow.interfaces import IWorkflowState
[10298]25from waeup.kofa.interfaces import (
[13996]26    IExtFileStore, IFileStoreNameChooser, IKofaUtils)
[10298]27from zope.formlib.textwidgets import BytesDisplayWidget
28from waeup.kofa.utils.helpers import string_from_bytes, file_size
[13996]29from waeup.kofa.applicants.browser import (
30    ApplicantCheckStatusPage, ApplicantBaseDisplayFormPage)
[14027]31from waeup.kofa.applicants.workflow import STARTED, PAID
[13538]32from waeup.kofa.applicants.viewlets import PDFActionButton
[14460]33from waeup.kofa.applicants.interfaces import IApplicantRegisterUpdate
[13996]34from waeup.kofa.browser.layout import UtilityView
35from waeup.kofa.students.interfaces import IStudentsUtils
36from waeup.kofa.interfaces import IPDF
[13997]37from waeup.kofa.browser.viewlets import ManageActionButton
[10298]38from waeup.aaue.interfaces import MessageFactory as _
39from kofacustom.nigeria.applicants.browser import (
40    NigeriaApplicantDisplayFormPage,
41    NigeriaApplicantManageFormPage,
42    NigeriaApplicantEditFormPage,
43    NigeriaPDFApplicationSlip,
[10929]44    NigeriaApplicantRegistrationPage,
[11755]45    NigeriaExportPDFPaymentSlipPage,
[13545]46    )
47from kofacustom.nigeria.applicants.interfaces import OMIT_DISPLAY_FIELDS
[10298]48from waeup.aaue.applicants.interfaces import (
49    ICustomUGApplicant,
[13544]50    ICustomUGApplicantEdit,
[13996]51    ITranscriptApplicant,
[14304]52    ICertificateRequest,
[13996]53    ICustomApplicant
[11291]54    )
[10298]55
[13545]56UG_OMIT_FIELDS = (
57      'hq_type', 'hq_fname', 'hq_matric_no',
58      'hq_degree', 'hq_school', 'hq_session', 'hq_disc',
59      'hq_type2', 'hq_fname2', 'hq_matric_no2',
60      'hq_degree2', 'hq_school2', 'hq_session2', 'hq_disc2',
61      'hq_type3', 'hq_fname3', 'hq_matric_no3',
[13679]62      'hq_degree3', 'hq_school3', 'hq_session3', 'hq_disc3',
63      'nysc_year',
64      'nysc_location',
65      'nysc_lga',
66      'employer',
67      'emp_position',
68      'emp_start',
69      'emp_end',
70      'emp_reason',
71      'employer2',
72      'emp2_position',
73      'emp2_start',
74      'emp2_end',
75      'emp2_reason',
76      'former_matric',
[13545]77      )
[13546]78UG_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + (
[13977]79    'jamb_subjects_list', 'master_sheet_number') + UG_OMIT_FIELDS
[13545]80UG_OMIT_PDF_FIELDS = UG_OMIT_DISPLAY_FIELDS + UG_OMIT_FIELDS + (
[14363]81      'alr_fname', 'alr_no', 'alr_date',
[13545]82      'alr_results', 'notice')
[13546]83UG_OMIT_MANAGE_FIELDS = (
84    'special_application','jamb_subjects_list',) + UG_OMIT_FIELDS
85UG_OMIT_EDIT_FIELDS = UG_OMIT_MANAGE_FIELDS + OMIT_DISPLAY_FIELDS + (
[13545]86    'student_id',
87    'notice',
88    'jamb_age',
89    'jamb_subjects',
90    'jamb_score',
91    'jamb_reg_number',
[13679]92    'aggregate',
[13977]93    'master_sheet_number',
[13996]94    'screening_venue',
95    'screening_score',
96    'screening_date'
[13679]97    )
[10298]98
[14209]99UDE_OMIT_FIELDS = (
100      'nysc_year',
101      'nysc_location',
102      'nysc_lga',
103      'employer',
104      'emp_position',
105      'emp_start',
106      'emp_end',
107      'emp_reason',
108      'employer2',
109      'emp2_position',
110      'emp2_start',
111      'emp2_end',
112      'emp2_reason',
113      'former_matric',
114      )
115UDE_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + (
116    'jamb_subjects_list', 'master_sheet_number') + UDE_OMIT_FIELDS
117UDE_OMIT_PDF_FIELDS = UDE_OMIT_DISPLAY_FIELDS + UDE_OMIT_FIELDS + (
[14363]118      'alr_fname', 'alr_no', 'alr_date',
[14209]119      'alr_results', 'notice')
120UDE_OMIT_MANAGE_FIELDS = (
121    'special_application','jamb_subjects_list',) + UDE_OMIT_FIELDS
122UDE_OMIT_EDIT_FIELDS = UDE_OMIT_MANAGE_FIELDS + OMIT_DISPLAY_FIELDS + (
123    'student_id',
124    'notice',
125    'jamb_age',
126    'jamb_subjects',
127    'jamb_score',
128    'jamb_reg_number',
129    'aggregate',
130    'master_sheet_number',
131    'screening_venue',
132    'screening_score',
133    'screening_date'
134    )
135
[13545]136#UG_OMIT_PDF_FIELDS = tuple([
137#    element for element in UG_OMIT_PDF_FIELDS if not element == 'phone'])
[11291]138
[13545]139#UG_OMIT_PDF_FIELDS += (
140#      'reg_number','alr_fname', 'alr_no', 'alr_date',
141#      'alr_results', 'notice'
142#      )
[13538]143
[13545]144PG_OMIT_FIELDS = (
145    'fst_sit_fname',
146    'fst_sit_no',
147    'fst_sit_date',
148    'fst_sit_type',
149    'fst_sit_results',
150    'scd_sit_fname',
151    'scd_sit_no',
152    'scd_sit_date',
153    'scd_sit_type',
154    'scd_sit_results',
[13977]155    #'programme_type',
[13546]156    'jamb_age',
157    'jamb_subjects',
158    'jamb_score',
159    'jamb_reg_number',
160    'aggregate'
[13545]161    )
162PG_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + (
163    'jamb_subjects_list',) + PG_OMIT_FIELDS
164PG_OMIT_PDF_FIELDS = PG_OMIT_DISPLAY_FIELDS + PG_OMIT_FIELDS + (
165      'reg_number','alr_fname', 'alr_no', 'alr_date',
[13679]166      'alr_results', 'notice',
167      'nysc_year',
168      'nysc_location',
169      'nysc_lga',
170      'former_matric',
171      )
[13545]172PG_OMIT_MANAGE_FIELDS = (
173    'special_application','jamb_subjects_list',) + PG_OMIT_FIELDS
[13546]174PG_OMIT_EDIT_FIELDS = PG_OMIT_MANAGE_FIELDS + OMIT_DISPLAY_FIELDS + (
[13545]175    'student_id',
176    'notice',
[13546]177    )
[13545]178
[13679]179PTEE_OMIT_FIELDS = (
180    'jamb_age',
181    'jamb_subjects',
182    'jamb_score',
183    'jamb_reg_number',
184    'aggregate'
185    )
186PTEE_OMIT_DISPLAY_FIELDS = OMIT_DISPLAY_FIELDS + (
187    'jamb_subjects_list',) + PTEE_OMIT_FIELDS
188PTEE_OMIT_PDF_FIELDS = PTEE_OMIT_DISPLAY_FIELDS + PTEE_OMIT_FIELDS + (
189      'reg_number','alr_fname', 'alr_no', 'alr_date',
190      'alr_results', 'notice',
191      'nysc_year',
192      'nysc_location',
193      'nysc_lga',
194      'employer',
195      'emp_position',
196      'emp_start',
197      'emp_end',
198      'emp_reason',
199      'employer2',
200      'emp2_position',
201      'emp2_start',
202      'emp2_end',
203      'emp2_reason',
204      'former_matric',
205    )
206PTEE_OMIT_MANAGE_FIELDS = (
207    'special_application','jamb_subjects_list',) + PTEE_OMIT_FIELDS
208PTEE_OMIT_EDIT_FIELDS = PTEE_OMIT_MANAGE_FIELDS + OMIT_DISPLAY_FIELDS + (
209    'student_id',
210    'notice',
211    )
212
[14150]213UPDATE_OMIT_FIELDS = (
214    'firstname',
215    'middlename',
216    'lastname',
217    'sex',
218    'lga',
219    'course1',
220    )
221
[10298]222class CustomApplicantDisplayFormPage(NigeriaApplicantDisplayFormPage):
223    """A display view for applicant data.
224    """
225
226    @property
227    def form_fields(self):
[13538]228        if self.target is not None and self.target == 'trans':
[13544]229            form_fields = grok.AutoFields(ITranscriptApplicant).omit(
[14580]230                'locked', 'suspended')
[13544]231            form_fields['dispatch_address'].custom_widget = BytesDisplayWidget
232            form_fields['perm_address'].custom_widget = BytesDisplayWidget
233            return form_fields
[14304]234        if self.target is not None and self.target == 'cert':
235            form_fields = grok.AutoFields(ICertificateRequest).omit(
[14580]236                'locked', 'suspended')
[14304]237            form_fields['dispatch_address'].custom_widget = BytesDisplayWidget
238            form_fields['perm_address'].custom_widget = BytesDisplayWidget
239            return form_fields
[13538]240        # AAUE is using the same interface for all regular applications.
[11291]241        form_fields = grok.AutoFields(ICustomUGApplicant)
[13538]242        if self.target is not None and self.target.startswith('pg'):
243            for field in PG_OMIT_DISPLAY_FIELDS:
244                form_fields = form_fields.omit(field)
[14150]245        elif self.target is not None and self.target in ('ptee',):
[13679]246            for field in PTEE_OMIT_DISPLAY_FIELDS:
[10298]247                form_fields = form_fields.omit(field)
[14209]248        elif self.target is not None and self.target in ('ude',):
249            for field in UDE_OMIT_DISPLAY_FIELDS:
250                form_fields = form_fields.omit(field)
[10298]251        else:
252            for field in UG_OMIT_DISPLAY_FIELDS:
253                form_fields = form_fields.omit(field)
254        form_fields['perm_address'].custom_widget = BytesDisplayWidget
255        form_fields['notice'].custom_widget = BytesDisplayWidget
256        if not getattr(self.context, 'student_id'):
257            form_fields = form_fields.omit('student_id')
258        if not getattr(self.context, 'screening_score'):
259            form_fields = form_fields.omit('screening_score')
[14840]260        if not getattr(self.context, 'screening_venue') or \
261            self.context.state not in ('submitted', 'admitted', 'created'):
[10298]262            form_fields = form_fields.omit('screening_venue')
[14840]263        if not getattr(self.context, 'screening_date') or \
264            self.context.state not in ('submitted', 'admitted', 'created'):
[10298]265            form_fields = form_fields.omit('screening_date')
266        return form_fields
267
[14371]268    def getCourseAdmitted(self):
269        """Return link, title and code in html format to the certificate
270           admitted.
271        """
272        if self.layout.isApplicant():
273            return ''
274        course_admitted = self.context.course_admitted
275        if getattr(course_admitted, '__parent__',None):
276            url = self.url(course_admitted)
277            title = course_admitted.title
278            code = course_admitted.code
279            return '<a href="%s">%s - %s</a>' %(url,code,title)
280        return ''
281
[13538]282class CustomPDFActionButton(PDFActionButton):
283
284    @property
285    def target_url(self):
[13540]286        if self.context.state in ('initialized', 'started', 'paid') \
[14304]287            or self.context.special or self.view.target in ('trans', 'cert'):
[13540]288            return
289        return self.view.url(self.view.context, self.target)
[13538]290
291
[10298]292class CustomPDFApplicationSlip(NigeriaPDFApplicationSlip):
293
[10311]294    column_two_fields = ('applicant_id', 'reg_number',
295        'firstname', 'middlename', 'lastname', 'sex', 'date_of_birth')
[13679]296    #two_columns_design_fields = [
297    #    'fst_sit_fname', 'fst_sit_no', 'fst_sit_date',
298    #    'fst_sit_type', 'fst_sit_results',
299    #    'scd_sit_fname', 'scd_sit_no', 'scd_sit_date',
300    #    'scd_sit_type', 'scd_sit_results']
[10311]301
[14228]302
303    def _getCourseAdmittedLink(self, view):
304        return None
305
306    def _getDeptAndFaculty(self):
307        return [None, None]
308
[10301]309    @property
310    def note(self):
[14057]311        note = getattr(self.context.__parent__, 'application_slip_notice', None)
312        if note:
313            return '<br /><br />' + note
[10301]314        if self.context.sex == 'm':
315            pronoun = 'he'
316        else:
317            pronoun = 'she'
318        return '''
[10897]319The applicant has acknowledged that, if discovered at any time that %s does not possess
320any of the qualifications which %s claims %s has obtained, %s will be expelled from the
[14224]321University not be re-admitted for the same or any other programme, even if %s has
322upgraded previous qualifications or possess additional qualifications.
323
[10331]324''' % (
[10301]325    pronoun, pronoun, pronoun, pronoun, pronoun)
[10298]326
327    @property
328    def form_fields(self):
[13538]329        # AAUE is using the same interface for all regular applications.
[11291]330        form_fields = grok.AutoFields(ICustomUGApplicant)
[13538]331        if self.target is not None and self.target.startswith('pg'):
332            for field in PG_OMIT_PDF_FIELDS:
333                form_fields = form_fields.omit(field)
[14150]334        elif self.target is not None and self.target in ('ptee',):
[13679]335            for field in PTEE_OMIT_PDF_FIELDS:
[10298]336                form_fields = form_fields.omit(field)
[14209]337        elif self.target is not None and self.target in ('ude',):
338            for field in UDE_OMIT_PDF_FIELDS:
339                form_fields = form_fields.omit(field)
[10298]340        else:
341            for field in UG_OMIT_PDF_FIELDS:
342                form_fields = form_fields.omit(field)
343        if not getattr(self.context, 'student_id'):
344            form_fields = form_fields.omit('student_id')
345        if not getattr(self.context, 'screening_score'):
346            form_fields = form_fields.omit('screening_score')
347        if not getattr(self.context, 'screening_venue'):
348            form_fields = form_fields.omit('screening_venue')
349        if not getattr(self.context, 'screening_date'):
350            form_fields = form_fields.omit('screening_date')
351        return form_fields
352
353class CustomApplicantManageFormPage(NigeriaApplicantManageFormPage):
354    """A full edit view for applicant data.
355    """
356
357    @property
358    def form_fields(self):
[13538]359        if self.target is not None and self.target == 'trans':
[13544]360            form_fields = grok.AutoFields(ITranscriptApplicant)
[13538]361            form_fields['applicant_id'].for_display = True
362            return form_fields
[14304]363        if self.target is not None and self.target == 'cert':
364            form_fields = grok.AutoFields(ICertificateRequest)
365            form_fields['applicant_id'].for_display = True
366            return form_fields
[13538]367        # AAUE is using the same interface for all regular applications.
[11291]368        form_fields = grok.AutoFields(ICustomUGApplicant)
[13538]369        if self.target is not None and self.target.startswith('pg'):
370            for field in PG_OMIT_MANAGE_FIELDS:
371                form_fields = form_fields.omit(field)
[14150]372        elif self.target is not None and self.target in ('ptee',):
[13679]373            for field in PTEE_OMIT_MANAGE_FIELDS:
[10298]374                form_fields = form_fields.omit(field)
[14209]375        elif self.target is not None and self.target in ('ude',):
376            for field in UDE_OMIT_MANAGE_FIELDS:
377                form_fields = form_fields.omit(field)
[10298]378        else:
379            for field in UG_OMIT_MANAGE_FIELDS:
380                form_fields = form_fields.omit(field)
381        form_fields['student_id'].for_display = True
382        form_fields['applicant_id'].for_display = True
383        return form_fields
384
385class CustomApplicantEditFormPage(NigeriaApplicantEditFormPage):
386    """An applicant-centered edit view for applicant data.
387    """
388
[14027]389    def unremovable(self, ticket):
390        return True
391
392    # AAUE applicants never see the 'Remove Selected Tickets' button.
[10298]393    @property
[14027]394    def display_actions(self):
395        # If the form is unlocked, applicants are allowed to save the form
[14029]396        # and remove unused tickets.
397        actions = [[_('Save')], []]
[14027]398        # Only in state started they can also add tickets.
[14822]399        if self.context.state == STARTED:
[14029]400            actions = [[_('Save')],
401                [_('Add online payment ticket')]]
[14027]402        # In state paid, they can submit the data and further add tickets
403        # if the application is special.
[14822]404        elif self.context.special and self.context.state == PAID:
[14027]405            actions = [[_('Save'), _('Finally Submit')],
[14029]406                [_('Add online payment ticket')]]
[14822]407        elif self.context.state == PAID:
[14029]408            actions = [[_('Save'), _('Finally Submit')], []]
[14027]409        return actions
410
411    @property
[10298]412    def form_fields(self):
[13538]413        if self.target is not None and self.target == 'trans':
[13544]414            form_fields = grok.AutoFields(ITranscriptApplicant).omit(
415                'locked', 'suspended')
[13538]416            form_fields['applicant_id'].for_display = True
[14472]417            form_fields['reg_number'].for_display = True
[13538]418            return form_fields
[14304]419        if self.target is not None and self.target == 'cert':
420            form_fields = grok.AutoFields(ICertificateRequest).omit(
421                'locked', 'suspended')
422            form_fields['applicant_id'].for_display = True
[14472]423            form_fields['reg_number'].for_display = True
[14696]424            # Additional omissions
425            if self.context.__parent__.code == 'cert5':
426                for field in ('firstname', 'middlename', 'lastname'):
427                    form_fields[field].for_display = True
[14304]428            return form_fields
[13538]429        # AAUE is using the same interface for all regular applications.
[11291]430        form_fields = grok.AutoFields(ICustomUGApplicantEdit)
[13538]431        if self.target is not None and self.target.startswith('pg'):
432            for field in PG_OMIT_EDIT_FIELDS:
433                form_fields = form_fields.omit(field)
[14150]434        elif self.target is not None and self.target in ('ptee',):
[13679]435            for field in PTEE_OMIT_EDIT_FIELDS:
[10298]436                form_fields = form_fields.omit(field)
[14209]437        elif self.target is not None and self.target in ('ude',):
438            for field in UDE_OMIT_EDIT_FIELDS:
439                form_fields = form_fields.omit(field)
[10298]440        else:
441            for field in UG_OMIT_EDIT_FIELDS:
442                form_fields = form_fields.omit(field)
[14150]443        # Additional omissions
[14696]444        if self.target is not None and self.target in ('ude', 'utme'):
[14150]445            for field in UPDATE_OMIT_FIELDS:
446                form_fields[field].for_display = True
[10298]447        form_fields['applicant_id'].for_display = True
448        form_fields['reg_number'].for_display = True
449        return form_fields
[10929]450
451class CustomApplicantRegistrationPage(NigeriaApplicantRegistrationPage):
452    """Captcha'd registration page for applicants.
453    """
454
[14460]455    @property
456    def form_fields(self):
457        form_fields = None
458        if self.context.mode == 'update':
459            form_fields = grok.AutoFields(IApplicantRegisterUpdate).select(
460                'lastname','reg_number','email')
[14465]461            target = getattr(self.context, 'prefix', None)
[14460]462            if target in ('trans', 'cert'):
463                form_fields.get('reg_number').field.title = u'Matriculation Number'
464        else: #if self.context.mode == 'create':
465            form_fields = grok.AutoFields(ICustomUGApplicantEdit).select(
466                'firstname', 'middlename', 'lastname', 'email', 'phone')
467        return form_fields
468
[10929]469    def _redirect(self, email, password, applicant_id):
470        # Forward email and credentials to landing page.
471        self.redirect(self.url(self.context, 'registration_complete',
472            data = dict(email=email, password=password,
473            applicant_id=applicant_id)))
[11755]474        return
475
[14579]476    @property
477    def _postfix(self):
478        """Alumni records have to be imported into several containers.
479        Therefore a string must be added to their registration number
480        to make it unique.
481        """
482        if self.context.prefix in ('trans', 'cert'):
483            return self.context.code
484        return ''
485
[11755]486class CustomExportPDFPaymentSlipPage(NigeriaExportPDFPaymentSlipPage):
487
488    @property
489    def payment_slip_download_warning(self):
[13422]490        return ''
491
492class CustomApplicantCheckStatusPage(ApplicantCheckStatusPage):
493    """Captcha'd status checking page for applicants.
494    """
495    grok.template('applicantcheckstatus')
[13996]496
[14822]497class ScreeningInvitationActionButton(ManageActionButton):
[13997]498    grok.order(8) # This button should always be the last one.
499    grok.context(ICustomApplicant)
500    grok.view(CustomApplicantDisplayFormPage)
501    grok.require('waeup.viewApplication')
502    icon = 'actionicon_pdf.png'
503    text = _('Download screening invitation letter')
504    target = 'screening_invitation.pdf'
[13996]505
[13997]506    @property
507    def target_url(self):
[14822]508        if not self.context.screening_date or not self.context.state in (
[14824]509            'submitted', 'admitted', 'created'):
[13997]510            return ''
511        return self.view.url(self.view.context, self.target)
512
[13996]513class ExportScreeningInvitationLetter(UtilityView, grok.View):
514    """Deliver a slip with only screening data.
515    This form page is available only in AAUE.
516    """
[13997]517    grok.context(ICustomApplicant)
[13996]518    grok.name('screening_invitation.pdf')
519    grok.require('waeup.viewApplication')
520    prefix = 'form'
521
522    label = u'Screening Invitation Letter'
523
524    form_fields = []
525
526    @property
527    def note(self):
528        if self.context.screening_date:
[13999]529            year = self.context.__parent__.year
530            session = '%s/%s' % (year, year + 1)
531            sdate = self.context.screening_date
532            stime = ''
533            if '@' in self.context.screening_date:
534                sdate = self.context.screening_date.split('@')[0].strip()
535                stime = self.context.screening_date.split('@')[1].strip()
[13996]536            return """
537<br /><br /><br /><br /><font size='12'>
538Dear %s,
539<br /><br />
[13999]540You are invited to the Ambrose Alli University %s Admissions Screening Exercise.
[13996]541<br /><br />
[13999]542<strong>Date: %s
[13996]543<br /><br />
[13999]544Time: %s
545<br /><br />
[13997]546Venue: %s
547</strong>
[13996]548<br /><br />
[14015]549Please bring this letter of invitation and the downloaded application form along with you on your screening date.
550<br /><br />
551You are expected to be available 30 minutes before the commencement of your Screening.
[13996]552</font>
553
[13999]554""" % (
555       self.context.display_fullname,
556       session,
557       sdate,
558       stime,
[13996]559       self.context.screening_venue)
560        return
561
562    @property
563    def title(self):
564        return None
565
566    def update(self):
[14822]567        if not self.context.screening_date or not self.context.state in (
[14824]568            'submitted', 'admitted', 'created'):
[13996]569            self.flash(_('Forbidden'), type="warning")
570            self.redirect(self.url(self.context))
571
572    def render(self):
573        applicantview = ApplicantBaseDisplayFormPage(self.context, self.request)
574        students_utils = getUtility(IStudentsUtils)
575        return students_utils.renderPDF(self,'screening_data.pdf',
[14015]576            self.context, applicantview, note=self.note)
Note: See TracBrowser for help on using the repository browser.