## $Id: browser.py 15975 2020-01-31 21:52:05Z henrik $
##
## Copyright (C) 2012 Uli Fouquet & Henrik Bettermann
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 2 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
##
import grok
from zope.i18n import translate
from zope.security import checkPermission
from zope.schema.interfaces import ConstraintNotSatisfied
from zope.formlib.textwidgets import BytesDisplayWidget
from zope.component import getUtility
from hurry.workflow.interfaces import IWorkflowInfo
from waeup.kofa.interfaces import (
REQUESTED, IExtFileStore, IKofaUtils, IObjectHistory)
from waeup.kofa.widgets.datewidget import FriendlyDatetimeDisplayWidget
from waeup.kofa.browser.layout import action, KofaEditFormPage, UtilityView
from waeup.kofa.students.browser import (
StudentBaseEditFormPage,
StudyLevelEditFormPage, StudyLevelDisplayFormPage,
StudentBasePDFFormPage, ExportPDFCourseRegistrationSlip,
CourseTicketDisplayFormPage, StudentTriggerTransitionFormPage,
msave, emit_lock_message,
StudentActivateView, StudentDeactivateView,
ExportPDFTranscriptSlip,
PaymentsManageFormPage,
StartClearancePage)
from waeup.kofa.students.workflow import (CREATED, ADMITTED, PAID,
CLEARANCE, REQUESTED, RETURNING, CLEARED, REGISTERED, VALIDATED,
GRADUATED, FORBIDDEN_POSTGRAD_TRANS)
from waeup.kofa.students.interfaces import IStudentsUtils, ICourseTicket
from waeup.kofa.students.workflow import FORBIDDEN_POSTGRAD_TRANS
from kofacustom.nigeria.students.browser import (
NigeriaOnlinePaymentDisplayFormPage,
NigeriaStudentBaseManageFormPage,
NigeriaStudentClearanceEditFormPage,
NigeriaOnlinePaymentAddFormPage,
NigeriaExportPDFPaymentSlip,
NigeriaExportPDFClearanceSlip,
NigeriaExportPDFBedTicketSlip,
NigeriaStudentPersonalDisplayFormPage,
NigeriaStudentPersonalManageFormPage,
NigeriaStudentPersonalEditFormPage,
NigeriaAccommodationManageFormPage,
NigeriaAccommodationDisplayFormPage,
NigeriaStudentBaseDisplayFormPage,
)
from waeup.uniben.students.interfaces import (
ICustomStudent,
ICustomStudentBase,
ICustomStudentOnlinePayment,
ICustomStudentStudyCourse,
ICustomStudentStudyLevel,
ICustomUGStudentClearance,
ICustomPGStudentClearance,
ICustomStudentPersonal,
ICustomStudentPersonalEdit)
from waeup.uniben.interfaces import MessageFactory as _
class CustomStudentBaseDisplayFormPage(NigeriaStudentBaseDisplayFormPage):
""" Page to display student base data
"""
form_fields = grok.AutoFields(ICustomStudentBase).omit(
'password', 'suspended', 'suspended_comment',
'flash_notice', 'provisionally_cleared')
form_fields[
'financial_clearance_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
#class CustomStudentBaseManageFormPage(NigeriaStudentBaseManageFormPage):
# """ View to manage student base data
# """
# form_fields = grok.AutoFields(ICustomStudentBase).omit(
# 'student_id', 'adm_code', 'suspended',
# 'financially_cleared_by', 'financial_clearance_date')
class CustomOnlinePaymentDisplayFormPage(NigeriaOnlinePaymentDisplayFormPage):
""" Page to view an online payment ticket
"""
grok.context(ICustomStudentOnlinePayment)
form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item')
form_fields[
'creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
form_fields[
'payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
class CustomStartClearancePage(StartClearancePage):
@property
def with_ac(self):
if self.context.faccode == 'DCOEM':
return False
return True
class CustomStudentClearanceEditFormPage(NigeriaStudentClearanceEditFormPage):
""" View to edit student clearance data by student
"""
@property
def form_fields(self):
if self.context.is_postgrad:
form_fields = grok.AutoFields(ICustomPGStudentClearance).omit(
'clearance_locked', 'nysc_location', 'clr_code', 'officer_comment',
'physical_clearance_date')
else:
form_fields = grok.AutoFields(ICustomUGStudentClearance).omit(
'clearance_locked', 'clr_code', 'officer_comment',
'physical_clearance_date')
form_fields['date_of_birth'].for_display = True
form_fields['nationality'].for_display = True
form_fields['lga'].for_display = True
return form_fields
def dataNotComplete(self):
store = getUtility(IExtFileStore)
if not store.getFileByContext(self.context, attr=u'birth_certificate.jpg'):
return _('No birth certificate uploaded.')
if not store.getFileByContext(self.context, attr=u'ref_let.jpg'):
return _('No guarantor/referee letter uploaded.')
if not store.getFileByContext(self.context, attr=u'acc_let.jpg'):
return _('No acceptance letter uploaded.')
if not store.getFileByContext(self.context, attr=u'fst_sit_scan.jpg'):
return _('No first sitting result uploaded.')
#if not store.getFileByContext(self.context, attr=u'scd_sit_scan.jpg'):
# return _('No second sitting result uploaded.')
if not store.getFileByContext(self.context, attr=u'secr_cults.jpg'):
return _('No affidavit of non-membership of secret cults uploaded.')
return False
class CustomOnlinePaymentAddFormPage(NigeriaOnlinePaymentAddFormPage):
""" Page to add an online payment ticket
"""
form_fields = grok.AutoFields(ICustomStudentOnlinePayment).select(
'p_combi')
class CustomExportPDFPaymentSlip(NigeriaExportPDFPaymentSlip):
"""Deliver a PDF slip of the context.
"""
grok.context(ICustomStudentOnlinePayment)
form_fields = grok.AutoFields(ICustomStudentOnlinePayment).omit(
'provider_amt', 'gateway_amt', 'thirdparty_amt', 'p_item', 'p_combi')
form_fields['creation_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
form_fields['payment_date'].custom_widget = FriendlyDatetimeDisplayWidget('le')
class CustomExportPDFClearanceSlip(NigeriaExportPDFClearanceSlip):
"""Deliver a PDF slip of the context.
"""
@property
def omit_fields(self):
omit_fields = ('password', 'suspended', 'suspended_comment',
'phone', 'adm_code', 'email', 'date_of_birth',
'flash_notice')
if self.context.is_jupeb:
omit_fields += ('faculty', 'department')
return omit_fields
@property
def label(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
line0 = ''
if self.context.is_jupeb:
line0 = 'Joint Universities Preliminary Examinations Board (JUPEB)\n'
line1 = translate(_('Clearance Slip of'),
'waeup.kofa', target_language=portal_language) \
+ ' %s' % self.context.display_fullname
return '%s%s' % (line0, line1)
def _sigsInFooter(self):
isStudent = getattr(
self.request.principal, 'user_type', None) == 'student'
if not isStudent and self.context.state in (CLEARED, RETURNING):
return (_('Date, Student Signature'),
_('Date, Clearance Officer Signature'),
)
return ()
def render(self):
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
return students_utils.renderPDF(
self, 'clearance_slip.pdf',
self.context.student, studentview, signatures=self._signatures(),
sigs_in_footer=self._sigsInFooter(), show_scans=False,
omit_fields=self.omit_fields)
class ExportClearanceInvitationSlip(UtilityView, grok.View):
"""Deliver an invitation letter to physical clearance.
This form page is available only in Uniben.
"""
grok.context(ICustomStudent)
grok.name('clearance_invitation_slip.pdf')
grok.require('waeup.viewStudent')
prefix = 'form'
label = u'Invitation Letter for Physical Clearance'
omit_fields = (
'suspended', 'phone', 'email',
'adm_code', 'suspended_comment',
'date_of_birth', 'current_level',
'department', 'current_mode',
'entry_session', 'matric_number', 'sex',
'flash_notice')
form_fields = []
@property
def note(self):
if self.context.physical_clearance_date:
return """
Dear %s,
You are invited for your physical clearance on:
%s.
Please bring along this letter of invitation to the University Main Auditorium
on your clearance date.
Signed,
The Registrar
""" % (self.context.display_fullname, self.context.physical_clearance_date)
return
def update(self):
if self.context.student.state != REQUESTED \
or not self.context.student.physical_clearance_date:
self.flash(_('Forbidden'), type="warning")
self.redirect(self.url(self.context))
def render(self):
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
return students_utils.renderPDF(
self, 'clearance_invitation_slip',
self.context.student, studentview,
omit_fields=self.omit_fields,
note=self.note)
class ExportExaminationScheduleSlip(UtilityView, grok.View):
"""Deliver a examination schedule slip.
This form page is available only in Uniben and AAUE.
"""
grok.context(ICustomStudent)
grok.name('examination_schedule_slip.pdf')
grok.require('waeup.viewStudent')
prefix = 'form'
label = u'Examination Schedule Slip'
omit_fields = (
'suspended', 'phone', 'email',
'adm_code', 'suspended_comment',
'date_of_birth', 'current_level',
'current_mode',
'entry_session',
'flash_notice')
form_fields = []
@property
def note(self):
return """
Your examination date, time and venue is scheduled as follows:
%s
""" % self.context.flash_notice
return
def update(self):
if not self.context.flash_notice \
or not 'exam' in self.context.flash_notice.lower():
self.flash(_('Forbidden'), type="warning")
self.redirect(self.url(self.context))
def render(self):
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
return students_utils.renderPDF(
self, 'examination_schedule_slip',
self.context.student, studentview,
omit_fields=self.omit_fields,
note=self.note)
class SwitchLibraryAccessView(UtilityView, grok.View):
""" Switch the library attribute
"""
grok.context(ICustomStudent)
grok.name('switch_library_access')
grok.require('waeup.switchLibraryAccess')
def update(self):
if self.context.library:
self.context.library = False
self.context.writeLogMessage(self, 'library access disabled')
self.flash(_('Library access disabled'))
else:
self.context.library = True
self.context.writeLogMessage(self, 'library access enabled')
self.flash(_('Library access enabled'))
self.redirect(self.url(self.context))
return
def render(self):
return
class ExportJHLIdCard(UtilityView, grok.View):
"""Deliver an id card for the John Harris Library.
"""
grok.context(ICustomStudent)
grok.name('jhl_idcard.pdf')
grok.require('waeup.viewStudent')
prefix = 'form'
label = u"John Harris Library Clearance"
omit_fields = (
'suspended', 'email', 'phone',
'adm_code', 'suspended_comment',
'date_of_birth',
'current_mode', 'certificate',
'entry_session',
'flash_notice')
form_fields = []
def _sigsInFooter(self):
isStudent = getattr(
self.request.principal, 'user_type', None) == 'student'
if isStudent:
return ''
return (_("Date, Reader's Signature"),
_("Date, Circulation Librarian's Signature"),
)
def update(self):
if not self.context.library:
self.flash(_('Forbidden!'), type="danger")
self.redirect(self.url(self.context))
return
@property
def note(self):
return """
This is to certify that the bearer whose photograph and other details appear
overleaf is a registered user of John Harris Library, University of Benin.
The card is not transferable. A replacement fee is charged for a loss,
mutilation or otherwise. If found, please, return to John Harris Library,
University of Benin, Benin City.
"""
return
def render(self):
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
return students_utils.renderPDF(
self, 'jhl_idcard',
self.context.student, studentview,
omit_fields=self.omit_fields,
sigs_in_footer=self._sigsInFooter(),
note=self.note)
class ExportJUPEBResultSlip(ExportExaminationScheduleSlip):
"""Deliver a JUPEB result slip.
This form page is available only in Uniben.
"""
grok.name('jupeb_result_slip.pdf')
label = u'JUPEB Result Slip'
@property
def note(self):
return """
%s
Key: A = 5, B = 4, C = 3, D = 2, E = 1, F = 0, X = Absent, Q = Cancelled
""" % self.context.flash_notice
return
def update(self):
if not self.context.flash_notice or not self.context.is_jupeb \
or not 'results' in self.context.flash_notice.lower():
self.flash(_('Forbidden'), type="warning")
self.redirect(self.url(self.context))
class CustomStudentPersonalDisplayFormPage(
NigeriaStudentPersonalDisplayFormPage):
""" Page to display student personal data
"""
form_fields = grok.AutoFields(ICustomStudentPersonal)
form_fields['perm_address'].custom_widget = BytesDisplayWidget
form_fields['next_kin_address'].custom_widget = BytesDisplayWidget
form_fields[
'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
class CustomStudentPersonalManageFormPage(
NigeriaStudentPersonalManageFormPage):
""" Page to manage personal data
"""
form_fields = grok.AutoFields(ICustomStudentPersonal)
form_fields['personal_updated'].for_display = True
form_fields[
'personal_updated'].custom_widget = FriendlyDatetimeDisplayWidget('le')
class CstomStudentPersonalEditFormPage(NigeriaStudentPersonalEditFormPage):
""" Page to edit personal data
"""
form_fields = grok.AutoFields(
ICustomStudentPersonalEdit).omit('personal_updated')
class StudyCourseCOEditFormPage(KofaEditFormPage):
""" Page to edit the student study course data by clearance officers.
This form page is available only in Uniben.
"""
grok.context(ICustomStudentStudyCourse)
grok.name('edit_level')
grok.require('waeup.clearStudent')
label = _('Edit current level')
pnav = 4
form_fields = grok.AutoFields(
ICustomStudentStudyCourse).select('current_level')
def update(self):
if not (self.context.is_current and
self.context.student.state == REQUESTED):
emit_lock_message(self)
return
super(StudyCourseCOEditFormPage, self).update()
return
@action(_('Save'), style='primary')
def save(self, **data):
try:
msave(self, **data)
except ConstraintNotSatisfied:
# The selected level might not exist in certificate
self.flash(_('Current level not available for certificate.'))
return
#notify(grok.ObjectModifiedEvent(self.context.__parent__))
return
class CustomStudyLevelEditFormPage(StudyLevelEditFormPage):
""" Page to edit the student study level data by students.
"""
grok.template('studyleveleditpage')
class CustomStudyLevelDisplayFormPage(StudyLevelDisplayFormPage):
""" Page to display student study levels
"""
grok.template('studylevelpage')
class CustomExportPDFCourseRegistrationSlip(
ExportPDFCourseRegistrationSlip):
"""Deliver a PDF slip of the context.
"""
form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit(
'level_verdict', 'gpa', 'level', 'transcript_remark')
def update(self):
if self.context.student.state != REGISTERED \
or self.context.student.current_level != self.context.level:
self.flash(_('Forbidden'), type="warning")
self.redirect(self.url(self.context))
@property
def tabletitle(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
tabletitle = []
tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('1st Trimester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('2nd Trimester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('3rd Trimester Courses'), 'waeup.kofa',
target_language=portal_language))
return tabletitle
def render(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
Code = translate('Code', 'waeup.kofa', target_language=portal_language)
Title = translate('Title', 'waeup.kofa', target_language=portal_language)
Dept = translate('Dept.', 'waeup.kofa', target_language=portal_language)
Faculty = translate('Faculty', 'waeup.kofa', target_language=portal_language)
Cred = translate(_('Credits'), 'waeup.uniben', target_language=portal_language)
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
tabledata = []
tableheader = []
for i in range(1,7):
tabledata.append(sorted(
[value for value in self.context.values() if value.semester == i],
key=lambda value: str(value.semester) + value.code))
tableheader.append([(Code,'code', 2.5),
(Title,'title', 5),
(Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
(Cred, 'credits', 1.5),
])
return students_utils.renderPDF(
self, 'course_registration_slip.pdf',
self.context.student, studentview,
tableheader=tableheader,
tabledata=tabledata,
omit_fields=self.omit_fields
)
class ExportPDFCourseResultSlip(ExportPDFCourseRegistrationSlip):
"""Deliver a PDF slip of the context.
"""
grok.name('course_result_slip.pdf')
form_fields = grok.AutoFields(ICustomStudentStudyLevel).omit('level')
@property
def tabletitle(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
tabletitle = []
tabletitle.append(translate(_('1st Semester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('2nd Semester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('Level Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('1st Trimester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('2nd Trimester Courses'), 'waeup.kofa',
target_language=portal_language))
tabletitle.append(translate(_('3rd Trimester Courses'), 'waeup.kofa',
target_language=portal_language))
return tabletitle
@property
def label(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
lang = self.request.cookies.get('kofa.language', portal_language)
level_title = translate(self.context.level_title, 'waeup.kofa',
target_language=lang)
return translate(_('Course Result Slip'),
'waeup.uniben', target_language=portal_language) \
+ ' %s' % level_title
def render(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
Code = translate('Code', 'waeup.kofa', target_language=portal_language)
Title = translate('Title', 'waeup.kofa', target_language=portal_language)
Dept = translate('Dept.', 'waeup.kofa', target_language=portal_language)
Faculty = translate('Faculty', 'waeup.kofa', target_language=portal_language)
Cred = translate(_('Credits'), 'waeup.uniben', target_language=portal_language)
Grade = translate('Grade', 'waeup.kofa', target_language=portal_language)
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
tabledata = []
tableheader = []
for i in range(1,7):
tabledata.append(sorted(
[value for value in self.context.values() if value.semester == i],
key=lambda value: str(value.semester) + value.code))
tableheader.append([(Code,'code', 2.5),
(Title,'title', 5),
(Dept,'dcode', 1.5), (Faculty,'fcode', 1.5),
(Cred, 'credits', 1.5),
(Grade, 'grade', 1.5),
])
return students_utils.renderPDF(
self, 'course_result_slip.pdf',
self.context.student, studentview,
tableheader=tableheader,
tabledata=tabledata,
omit_fields=self.omit_fields
)
class CustomCourseTicketDisplayFormPage(CourseTicketDisplayFormPage):
""" Page to display course tickets
"""
form_fields = grok.AutoFields(ICourseTicket).omit('score')
class CustomStudentActivateView(StudentActivateView):
""" Activate student account
"""
def update(self):
self.context.suspended = False
self.context.writeLogMessage(self, 'account activated')
history = IObjectHistory(self.context)
history.addMessage('Student account activated', user='undisclosed')
self.flash(_('Student account has been activated.'))
self.redirect(self.url(self.context))
return
class CustomStudentDeactivateView(StudentDeactivateView):
""" Deactivate student account
"""
def update(self):
self.context.suspended = True
self.context.writeLogMessage(self, 'account deactivated')
history = IObjectHistory(self.context)
history.addMessage('Student account deactivated', user='undisclosed')
self.flash(_('Student account has been deactivated.'))
self.redirect(self.url(self.context))
return
class CustomExportPDFTranscriptSlip(ExportPDFTranscriptSlip):
"""Deliver a PDF slip of the context.
"""
#def _sigsInFooter(self):
# isStudent = getattr(
# self.request.principal, 'user_type', None) == 'student'
# if not isStudent:
# return (_('D. R. (Exams & Records)'),_('Current Dean of Faculty'),)
# return ()
#def _signatures(self):
# return ([(
# 'Current HD
D. R. (Exams & Records)
'
# 'For: Registrar')],)
def render(self):
portal_language = getUtility(IKofaUtils).PORTAL_LANGUAGE
Term = translate(_('Term'), 'waeup.kofa', target_language=portal_language)
Code = translate(_('Code'), 'waeup.kofa', target_language=portal_language)
Title = translate(_('Title'), 'waeup.kofa', target_language=portal_language)
Cred = translate(_('Credits'), 'waeup.kofa', target_language=portal_language)
#Score = translate(_('Score'), 'waeup.kofa', target_language=portal_language)
Grade = translate(_('Grade'), 'waeup.kofa', target_language=portal_language)
studentview = StudentBasePDFFormPage(self.context.student,
self.request, self.omit_fields)
students_utils = getUtility(IStudentsUtils)
tableheader = [(Code,'code', 2.5),
(Title,'title', 8.5),
(Term, 'semester', 1.5),
(Cred, 'credits', 1.5),
#(Score, 'score', 1.5),
(Grade, 'grade', 1.5),
]
pdfstream = students_utils.renderPDFTranscript(
self, 'transcript.pdf',
self.context.student, studentview,
omit_fields=self.omit_fields,
tableheader=tableheader,
signatures=self._signatures(),
sigs_in_footer=self._sigsInFooter(),
digital_sigs=self._digital_sigs(),
save_file=self._save_file(),
)
if not pdfstream:
self.redirect(self.url(self.context.student))
return
return pdfstream
class CustomExportPDFBedTicketSlip(NigeriaExportPDFBedTicketSlip):
"""Deliver a PDF slip of the context.
"""
omit_fields = ('password', 'suspended', 'suspended_comment',
'phone', 'adm_code', 'email', 'date_of_birth', 'flash_notice')
class CustomPaymentsManageFormPage(PaymentsManageFormPage):
""" Page to manage the student payments. This manage form page is for
both students and students officers. Uniben does not allow students
to remove any payment ticket.
"""
@property
def manage_payments_allowed(self):
return checkPermission('waeup.manageStudent', self.context)
class CustomAccommodationDisplayFormPage(NigeriaAccommodationDisplayFormPage):
""" Page to view bed tickets.
"""
with_hostel_selection = True
class CustomAccommodationManageFormPage(NigeriaAccommodationManageFormPage):
""" Page to manage bed tickets.
This manage form page is for both students and students officers.
"""
with_hostel_selection = True
class CustomStudentBaseEditFormPage(StudentBaseEditFormPage):
""" View to edit student base data
"""
form_fields = grok.AutoFields(ICustomStudentBase).select(
'email', 'phone')
form_fields['email'].field.required = True