From 073ef9fe75ce68cc6db63093886d175a204b8347 Mon Sep 17 00:00:00 2001 From: Karim Gillani Date: Fri, 17 May 2019 17:40:07 -0700 Subject: [PATCH 1/2] Sprint 11 2 (#265) * Sprint 11 4 (#261) * Fixed Edit Group Exam Modal to Disable Past Date Selection Exam dates can no longer be edited to a date that is in the past. Also fixed eratic form validation that prevented the submit button from being pressed when certain dates were selected. * Fixed Issue with Appointment Booking Modal Notes Notes were being captured but not displayed in the modal. Fixed. Also adjusted calendar start time to 8:30am. * Fixed Errors in Branch Agenda Previously this component was more complex and it relied on an invisible full-calendar instance to consume and parse the event data however this approach was causing issues. Since the Agenda has recently been simplified and no longer supports multiple views etc., there is no need for full-calendar. Have re-implemented using only moment.js and eliminated the errors related to full-calendar * Deleted Date for Invigilators and Rooms Client was flooded with requests for how to delete invigilators and rooms upon launch. To accomodate for this request but maintain data integrity, a deleted date was added to both the invigilator and room models, such that if these fields were set to a date in the admin panels for these objects, they will no longer show up in the application drop downs or calendars. * Allow Office Mgr to Edit Group/Session Exam Details Logic controlling access to the full version of the Edit Exam Modal did not consider the ita_designate key. Office Manager previously saw only the fields-limited CSR version. Changed so that office mgr has same access as GA role. Also fixed: -liaison_designate were unable to edit office of pesticide exams. -the exam_type field of the EditExamModal was listing exam_types which did not make sense. Disabled the ability to switch exam_type on a pesticide exam or challenger exam at all and restricted changing 'other' 'individual ita' and 'group ita' to exam type options of their catagory only. -adjusted the other field-display and field-disable logic to ensure access to necessary features for office managers -set the time of the editExamSuccess banner to 3 seconds instead of 6 * Admin Panel - Room -> Office Name Sort Clients required rooms to be sorted by office name after testing in production. * Typo (#262) Missing command from sprint-11-4 PR. * Sprint 11 6 (#263) * Admin Panel - Room Searching Client required rooms to be searchable through admin panels. * Backend Changes for Tracking Number Length Bookings application required backend changes to reflect tracking number length of 255 chars. Alembic would not generate a migration file for the length change, so the last migration file in the alembic chain was modified to see this change happen. * Return Exam fix: Action Taken Length & Deleted Invigilator Changes -Action Taken field now displays an error at the maximum input length and prevents further input -Adjusted the exam_inventory table to display the life-ring error symbol on any exam assigned to a deleted invigilator and adjusted the filters to show such exams as not ready. -Changed a call to previous method name which was revised in EditGrouoBooking modal * AddExamForm Modal exam_name length validation Modified existing validation logic to check if exam_name field exceeds 50 characters in length and displays error / prevents advancing until corrected. * Added exam_name length validation to EditExamModal Added validation and an error message as per the Return Exam modal at the 50 character limit. * Fixed Error in Appointments with double clicking Fixed error where double clicking the appointments page would cause two modals to open. Also removed stray console.log from return_exam_modal * Delete Room Admin Warning Clients required there to be a warning given to admin users if a room is about to have a deleted date applied to it, but it currently being used for bookings in the future. The flask admin on_model_change method was used to detect changes in the deleted date field before submission, and looking at the booking model for instances of such a room being used with start times greater than today (doesn't care about historical instances). If the room isn't in use, the room can be deleted. If the room is in use, and error message propagates telling the user that they were unable to apply the deleted date. Any other changes that the user might make on top of applying a deleted date to a room in use will be saved. * Changed displayed hours of Appointments Calendar to 8.30 am - 5 pm (#264) --- api/app/admin/invigilator.py | 27 +- api/app/admin/room.py | 48 ++- api/app/models/bookings/exam.py | 2 +- api/app/models/bookings/invigilator.py | 1 + api/app/models/bookings/room.py | 1 + .../bookings/invigilator/invigilator_list.py | 4 +- api/app/resources/bookings/room/room_list.py | 3 +- .../schemas/bookings/invigilator_schema.py | 1 + api/app/schemas/bookings/room_schema.py | 1 + api/migrations/versions/2c9c7262d221_.py | 32 ++ frontend/src/agenda/agenda.vue | 354 +++++++++--------- frontend/src/appointments/appointments.vue | 14 +- frontend/src/booking/other-booking-modal.vue | 1 - .../src/exams/add-exam-form-controller.vue | 5 + frontend/src/exams/edit-exam-form-modal.vue | 136 ++++--- frontend/src/exams/edit-group-exam-modal.vue | 16 +- frontend/src/exams/exam-inventory-table.vue | 17 +- frontend/src/exams/return-exam-form-modal.vue | 37 +- frontend/src/exams/success-exam-alert.vue | 2 +- frontend/src/layout/nav.vue | 2 +- frontend/src/store/appointments-module.js | 2 +- frontend/src/store/index.js | 2 +- 22 files changed, 429 insertions(+), 279 deletions(-) create mode 100644 api/migrations/versions/2c9c7262d221_.py diff --git a/api/app/admin/invigilator.py b/api/app/admin/invigilator.py index 9b4ce8ad3..852d4ca64 100644 --- a/api/app/admin/invigilator.py +++ b/api/app/admin/invigilator.py @@ -20,7 +20,6 @@ class InvigilatorConfig(Base): roles_allowed = ['SUPPORT', 'GA'] - delete_allowed = ['SUPPORT'] def is_accessible(self): return current_user.is_authenticated and current_user.role.role_code in self.roles_allowed @@ -31,15 +30,9 @@ def get_query(self): elif current_user.role.role_code == 'GA': return self.session.query(self.model).filter_by(office_id=current_user.office_id) - # Check to see whether or not the user can delete records based on their role - def _handle_view(self, name, **kwargs): - if current_user.role.role_code in self.delete_allowed: - self.can_delete = True - else: - self.can_delete = False - create_modal = False edit_modal = False + can_delete = False column_list = [ 'office.office_name', @@ -48,16 +41,19 @@ def _handle_view(self, name, **kwargs): 'contact_email', 'contract_number', 'contract_expiry_date', - 'invigilator_notes' + 'invigilator_notes', + 'deleted' ] form_excluded_columns = [ 'bookings' ] - column_labels = {'office.office_name': 'Office Name'} + column_labels = {'office.office_name': 'Office Name', + 'deleted': 'Deleted'} - column_searchable_list = {'invigilator_name'} + column_searchable_list = {'invigilator_name', + 'deleted'} form_create_rules = ( 'office', @@ -66,7 +62,8 @@ def _handle_view(self, name, **kwargs): 'contact_email', 'contract_number', 'contract_expiry_date', - 'invigilator_notes' + 'invigilator_notes', + 'deleted' ) form_edit_rules = ( @@ -76,14 +73,16 @@ def _handle_view(self, name, **kwargs): 'contact_email', 'contract_number', 'contract_expiry_date', - 'invigilator_notes' + 'invigilator_notes', + 'deleted' ) column_sortable_list = [ 'invigilator_name', 'contact_email', 'contract_number', - 'contract_expiry_date' + 'contract_expiry_date', + 'deleted' ] column_default_sort = 'invigilator_name' diff --git a/api/app/admin/room.py b/api/app/admin/room.py index ad077ac45..7a80b51ca 100644 --- a/api/app/admin/room.py +++ b/api/app/admin/room.py @@ -12,10 +12,15 @@ See the License for the specific language governing permissions and limitations under the License.''' -from app.models.bookings import Room +from app.models.bookings import Booking, Room from .base import Base +from flask import flash, request +from flask_admin.babel import gettext +from flask_admin.model.helpers import get_mdict_item_or_list from flask_login import current_user from qsystem import db +from datetime import datetime +import pytz class RoomConfig(Base): @@ -30,6 +35,25 @@ def get_query(self): elif current_user.role.role_code == 'GA': return self.session.query(self.model).filter_by(office_id=current_user.office_id) + def on_model_change(self, form, model, is_created): + + room_id = get_mdict_item_or_list(request.args, 'id') + today = datetime.now() + today_aware = pytz.utc.localize(today) + + booking_room = Booking.query.filter_by(room_id=room_id)\ + .filter(Booking.start_time > today_aware).count() + + specific_room = Room.query.filter_by(room_id=room_id).first() + room_name = specific_room.room_name + + if model.deleted is not None and booking_room > 0: + message = "'" + room_name + "' is currently being used for bookings. " \ + "Reschedule bookings that use this room before setting the deleted date." + flash(gettext(message), 'warning') + model.deleted = None + form.deleted.data = None + create_modal = False edit_modal = False can_delete = False @@ -38,7 +62,8 @@ def get_query(self): 'office.office_name', 'room_name', 'capacity', - 'color' + 'color', + 'deleted' ] form_excluded_columns = [ @@ -46,27 +71,36 @@ def get_query(self): 'booking' ] - column_labels = {'office.office_name': 'Office Name'} + column_labels = {'office.office_name': 'Office Name', + 'deleted': 'Deleted'} form_create_rules = ( 'office', 'room_name', 'capacity', - 'color' + 'color', + 'deleted' ) form_edit_rules = ( 'office', 'room_name', 'capacity', - 'color' + 'color', + 'deleted' ) column_sortable_list = [ 'room_name', 'capacity', - 'color' + 'color', + 'deleted', + 'office.office_name' ] + column_searchable_list = { + 'office.office_name' + } + -RoomModelView = RoomConfig(Room, db.session) \ No newline at end of file +RoomModelView = RoomConfig(Room, db.session) diff --git a/api/app/models/bookings/exam.py b/api/app/models/bookings/exam.py index 466be29fc..8f8c2d367 100644 --- a/api/app/models/bookings/exam.py +++ b/api/app/models/bookings/exam.py @@ -33,7 +33,7 @@ class Exam(Base): exam_method = db.Column(db.String(15), nullable=False) deleted_date = db.Column(db.String(50), nullable=True) exam_returned_date = db.Column(db.DateTime, nullable=True) - exam_returned_tracking_number = db.Column(db.String(50), nullable=True) + exam_returned_tracking_number = db.Column(db.String(255), nullable=True) exam_written_ind = db.Column(db.Integer, nullable=False, default=0) offsite_location = db.Column(db.String(50), nullable=True) diff --git a/api/app/models/bookings/invigilator.py b/api/app/models/bookings/invigilator.py index 0dff3f95b..25823b366 100644 --- a/api/app/models/bookings/invigilator.py +++ b/api/app/models/bookings/invigilator.py @@ -26,6 +26,7 @@ class Invigilator(Base): contact_email = db.Column(db.String(50), nullable=True) contract_number = db.Column(db.String(50), nullable=False) contract_expiry_date = db.Column(db.String(50), nullable=False) + deleted = db.Column(db.DateTime, nullable=True) bookings = db.relationship("Booking") office = db.relationship("Office", lazy="joined") diff --git a/api/app/models/bookings/room.py b/api/app/models/bookings/room.py index 8d5fd2d76..2a77f7846 100644 --- a/api/app/models/bookings/room.py +++ b/api/app/models/bookings/room.py @@ -23,6 +23,7 @@ class Room(Base): room_name = db.Column(db.String(50), nullable=False) capacity = db.Column(db.Integer, nullable=False) color = db.Column(db.String(25), nullable=False) + deleted = db.Column(db.DateTime, nullable=True) booking = db.relationship("Booking") office = db.relationship("Office", lazy='joined') diff --git a/api/app/resources/bookings/invigilator/invigilator_list.py b/api/app/resources/bookings/invigilator/invigilator_list.py index 60dc1867b..bef62c749 100644 --- a/api/app/resources/bookings/invigilator/invigilator_list.py +++ b/api/app/resources/bookings/invigilator/invigilator_list.py @@ -33,7 +33,9 @@ def get(self): csr = CSR.find_by_username(g.oidc_token_info['username']) try: - invigilators = Invigilator.query.filter_by(office_id=csr.office_id) + invigilators = Invigilator.query.filter_by(office_id=csr.office_id)\ + .filter(Invigilator.deleted.is_(None)) + result = self.invigilator_schema.dump(invigilators) return {'invigilators': result.data, 'errors': result.errors}, 200 diff --git a/api/app/resources/bookings/room/room_list.py b/api/app/resources/bookings/room/room_list.py index 1040ef68b..6f451568d 100644 --- a/api/app/resources/bookings/room/room_list.py +++ b/api/app/resources/bookings/room/room_list.py @@ -33,7 +33,8 @@ def get(self): csr = CSR.find_by_username(g.oidc_token_info['username']) try: - rooms = Room.query.filter_by(office_id=csr.office_id) + rooms = Room.query.filter_by(office_id=csr.office_id)\ + .filter(Room.deleted.is_(None)) result = self.rooms_schema.dump(rooms) return {'rooms': result.data, 'errors': result.errors}, 200 diff --git a/api/app/schemas/bookings/invigilator_schema.py b/api/app/schemas/bookings/invigilator_schema.py index 04e8ca07b..2c414a4bb 100644 --- a/api/app/schemas/bookings/invigilator_schema.py +++ b/api/app/schemas/bookings/invigilator_schema.py @@ -33,5 +33,6 @@ class Meta: invigilator_id = fields.Int(dump_only=True) invigilator_name = fields.Str() invigilator_notes = fields.Str() + deleted = fields.Str() office = fields.Nested(OfficeSchema()) diff --git a/api/app/schemas/bookings/room_schema.py b/api/app/schemas/bookings/room_schema.py index bf6397514..1dd375651 100644 --- a/api/app/schemas/bookings/room_schema.py +++ b/api/app/schemas/bookings/room_schema.py @@ -30,5 +30,6 @@ class Meta: color = fields.Str() room_id = fields.Int(dump_only=True) room_name = fields.Str() + deleted = fields.Str() office = fields.Nested(OfficeSchema()) diff --git a/api/migrations/versions/2c9c7262d221_.py b/api/migrations/versions/2c9c7262d221_.py new file mode 100644 index 000000000..3069e2cc3 --- /dev/null +++ b/api/migrations/versions/2c9c7262d221_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 2c9c7262d221 +Revises: 3a98e5395000 +Create Date: 2019-05-15 10:16:23.063868 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '2c9c7262d221' +down_revision = '3a98e5395000' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('invigilator', sa.Column('deleted', sa.DateTime(), nullable=True)) + op.add_column('room', sa.Column('deleted', sa.DateTime(), nullable=True)) + op.alter_column('exam', 'exam_returned_tracking_number', existing_type=sa.VARCHAR(length=50), type_=sa.String(length=255)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('room', 'deleted') + op.drop_column('invigilator', 'deleted') + op.alter_column('exam', 'exam_returned_tracking_number', existing_type=sa.VARCHAR(length=255), type_=sa.String(length=50)) + # ### end Alembic commands ### diff --git a/frontend/src/agenda/agenda.vue b/frontend/src/agenda/agenda.vue index 51bb70112..bb896e4f8 100644 --- a/frontend/src/agenda/agenda.vue +++ b/frontend/src/agenda/agenda.vue @@ -1,89 +1,63 @@