Skip to content

Commit

Permalink
Sprint 4 (sjrumsby#162)
Browse files Browse the repository at this point in the history
* New snowplow calls (sjrumsby#160)

* Final changes for new Snowplow events, states, flows and contexts
* Updated DB for additional Snowplow events

* Put more info in the Service Now ticket header (sjrumsby#161)

* Make Updates to an Existing Exam Record

CSRs require the ability to update existing exams records in the exam inventory table. This comes in the form of two actions listed as dropdown items in the exam inventory table now. The first action (Edit Exam) shows the user basic information that is displayed in the table, and allows the user to edit said data, then submit the data to the database. The second action (Return Exam) allows the user to enter or edit information with respect to the exam being returned (an ind field and tracking information). Both actions upon submit display a relevent success or failure message depending on the return method response.

* Created edit-booking component and related changes

Created a component edit-booking-modal to handle both exam and other type events.  Supports all editable fields for a booking.  Dynamically updates the calendar in background and then posts changes on submit or reverts on cancel.  Also revamped layout to eliminate the need for the windowResize listener to calculate the viewport size.  All layout-related divs now styled with "position: relative" or "position: fixed" so browser can manage this task automatically.  This was necessary for changes introduced to SchedulingIndicator - component now appears at bottom of screen with footer.

* Fix Bootstrap Data for Consistency

During the last demo, it was found out that the bootstrap data for exam types had a typo in it. For consistency purposes during live demos, this data was amended to follow an XML file containing actual exam type values provided to Olivewood from John McColl.

* Added API call for getting all offices

This is requried by LIAISON CSRs who enter group exams on behalf of all offices. No tests have been developed at this point, and the authorization on the call has been left out.

* npm Package Update

* Link Single Exam to Booking Feature

Room Bookings users require easy access to editing information with respect to a single exam. With this being said, once an exam is booked, the booking itself might need to be changed. The exam inventory table presents an easy way for users to filter through all exams, and the actions field presents a way for the user to link directly to a particular exams booking quickly. Based on logic if a booking for an exam is present, a dropdown in the actions tab will show up called "Update Booking", and once clicked, the user is taken directly to the DAY view of the booking calendar where this exam is currently booked.

Future enhancements to this may include the user being brought directly to the edit booking modal to avoid confusion.

* Fix Issues With Logins

When logging in, if the username does not exist in the Q System database, the call to /csrs/me will 500, leading to a poor front end experience. To clean this up, changes were made such that if the username did not exist /csrs/me returned a 404 response. The frontend had an bootstrap alert added such that if the username name did not exist BUT the user was logged in, the user would be warned saying to contact an administrator. The final step was to provisioni csr usernames in the Database for Sean, Scott, Adam, Chris and Karim for future use.

* Add Offsite Location Field

During development of the capture group exam component of the exams feature, it was found out that the exams model/schema required an offsite location field to accomodate offices in the greater vancouver area. This field was added to both the exams model and schema, and tested in postman to make sure that the exams GET endpoint worked.

* Code Clean-up: Redundant Vue Action

During end of sprint code clean-up, it was found out that the examsOnLogin action in the index.js file was actually not used, and therefore redundant. This was cleaned up, and the bookings features tested on the front end to ensure that no negative side effects were created post action removal.

* Edit/Return Exam Modal Styling

During testing across multiple browsers of the edit and return exam modals, with their success/failure messages, it was found that there were some styling issues with respect to modal size. Issues being modal/messages modal size and whether or not there were scroll bars present depending on the size of screen. These issues were addresses in this PR.

* Update Exams - Move from Div to B-Modal

During the development of the update/return/bookings actions on the exams invetory table, early development used nested divs to create modals for actions instead of using bootstrap modals. As final clean-up of this sprint task, the actions had to move towards using b-modal, as well as using b-alert to tell the user whether or not the PUT methods taken on exam objects were successful or erroneous.

* Sprint 4 - Swagger/Postman Updates

During the development of the bookings app in sprint 4, a field was added to the exams model/schema, and a GET method was added to list all offices in theq. These changes needed to be reflected in the bookings postman tests, as well as in the swagger yaml file. The postman tests were written and tested before submission of this PR, and were verified to be working as expected.

* Issue with Liason Role Creating Exams

During development of the bookings feature, it was found out that liasons could not POST exams from one office to a different office (ie// office_id 1 -> office_id 3). Since Liaisons require the ability to create exams in all offices, a change needed to be made to the exam POST end-point, checking for the csr role code to be LIAISON. Test data was also added to the manage.py file such that the role code LIASON exists now. A normal POST of an individual ITA exam was done in order to the test that previous functionality was not changed.

* Add Group Exam Modal/ITA Group Exam Capture

Created a new set of steps and questions in the state corresponding to the data requirements for the capture of Group ITA Exams to drive the add exam modal.  Created new question components and modified the modal to support the capture ofgroup exams as needed.

* Updated migrations to work with upstream migrations

* Updated Login.vue to correcly log errors for refreshing tokens
  • Loading branch information
sjrumsby authored and gil0109 committed Jan 23, 2019
1 parent baa6eaf commit d923cf7
Show file tree
Hide file tree
Showing 58 changed files with 5,310 additions and 3,126 deletions.
2 changes: 2 additions & 0 deletions api/app/models/bookings/exam.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Exam(Base):
deleted_date = db.Column(db.String(50), nullable=True)
exam_returned_ind = db.Column(db.Integer, nullable=False, default=0)
exam_returned_tracking_number = db.Column(db.String(50), nullable=True)
offsite_location = db.Column(db.String(50), nullable=True)


booking = db.relationship("Booking")
exam_type = db.relationship("ExamType")
Expand Down
1 change: 0 additions & 1 deletion api/app/models/theq/citizen.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ class Citizen(Base):
cs_id = db.Column(db.Integer, db.ForeignKey('citizenstate.cs_id'), nullable=False)
start_time = db.Column(db.DateTime, nullable=False)
accurate_time_ind = db.Column(db.Integer, nullable=False, default=1)
service_count = db.Column(db.Integer, nullable=False, default=1)
priority = db.Column(db.Integer, nullable=False, default=2)

service_reqs = db.relationship('ServiceReq', lazy='joined', order_by='ServiceReq.sr_id')
Expand Down
34 changes: 25 additions & 9 deletions api/app/models/theq/service_req.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ServiceReq(Base):
channel_id = db.Column(db.Integer, db.ForeignKey('channel.channel_id'), nullable=False)
service_id = db.Column(db.Integer, db.ForeignKey('service.service_id'), nullable=False)
sr_state_id = db.Column(db.Integer, db.ForeignKey('srstate.sr_state_id'), nullable=False)
sr_number = db.Column(db.Integer, default=1, nullable=False)

channel = db.relationship('Channel')
periods = db.relationship('Period', backref=db.backref("request_periods", lazy=False), lazy='joined', order_by='Period.period_id')
Expand All @@ -41,17 +42,31 @@ def get_active_period(self):

return sorted_periods[-1]

def invite(self, csr, snowplow_event="use_period"):
def invite(self, csr, invite_type, sr_count = 1):
active_period = self.get_active_period()
if active_period.ps.ps_name in ["Invited", "Being Served", "On hold"]:
raise TypeError("You cannot invite a citizen that has already been invited")

# Calculate what Snowplow event to call.
if (snowplow_event == "use_period"):
if (active_period.ps.ps_name == "Waiting"):
# If a generic invite type, event is either invitecitizen or returninvite.
if invite_type == "generic":
# If only one SR, one period, an invitecitizen call, from First Time in Line state.
if sr_count == 1 and len(self.periods) == 2:
snowplow_event = "invitecitizen"
# Otherwise from the Back in Line state.
else:
snowplow_event = "returninvite"

# A specific invite type. Event is invitefromlist, returnfromlist or invitefromhold
else:
# If only one SR, one period, an invitefromlist call, from First Time in Line state.
if sr_count == 1 and len(self.periods) == 2:
snowplow_event = "invitefromlist"
# Either from back in line or hold state.
else:
snowplow_event = "invitefromhold"
if active_period.ps.ps_name == "Waiting":
snowplow_event = "returnfromlist"
else:
snowplow_event = "invitefromhold"

active_period.time_end = datetime.now()
# db.session.add(active_period)
Expand All @@ -68,7 +83,7 @@ def invite(self, csr, snowplow_event="use_period"):

self.periods.append(new_period)

SnowPlow.snowplow_event(self.citizen_id, csr, snowplow_event)
SnowPlow.snowplow_event(self.citizen_id, csr, snowplow_event, current_sr_number=self.sr_number)

def add_to_queue(self, csr, snowplow_event):

Expand All @@ -87,7 +102,7 @@ def add_to_queue(self, csr, snowplow_event):
)
self.periods.append(new_period)

SnowPlow.snowplow_event(self.citizen_id, csr, snowplow_event)
SnowPlow.snowplow_event(self.citizen_id, csr, snowplow_event, current_sr_number=self.sr_number)

def begin_service(self, csr, snowplow_event):
active_period = self.get_active_period()
Expand All @@ -112,7 +127,8 @@ def begin_service(self, csr, snowplow_event):

# Calculate number of active periods, for Snowplow call.
period_count = len(self.periods)
SnowPlow.snowplow_event(self.citizen_id, csr, snowplow_event, period_count = period_count)
SnowPlow.snowplow_event(self.citizen_id, csr, snowplow_event, period_count = period_count,
current_sr_number = self.sr_number)

def place_on_hold(self, csr):
active_period = self.get_active_period()
Expand All @@ -131,7 +147,7 @@ def place_on_hold(self, csr):

self.periods.append(new_period)

SnowPlow.snowplow_event(self.citizen_id, csr, "hold")
SnowPlow.snowplow_event(self.citizen_id, csr, "hold", current_sr_number = self.sr_number)

def finish_service(self, csr, clear_comments=True):
active_period = self.get_active_period()
Expand Down
2 changes: 1 addition & 1 deletion api/app/resources/bookings/exam/exam_post.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def post(self):
logging.warning("WARNING: %s", warning)
return {"message": warning}, 422

if exam.office_id == csr.office_id:
if exam.office_id == csr.office_id or csr.role_code == "LIAISON":

db.session.add(exam)
db.session.commit()
Expand Down
15 changes: 11 additions & 4 deletions api/app/resources/theq/citizen/citizen_add_to_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ def post(self, id):
if active_service_request is None:
return {"message": "Citizen has no active service requests"}

# Figure out what Snowplow call to make.
snowplow_call = "returntoqueue"
if len(citizen.service_reqs) == 1 and len(active_service_request.periods) == 1:
snowplow_call = "addtoqueue"
# Figure out what Snowplow call to make. Default is addtoqueue
snowplow_call = "addtoqueue"
if len(citizen.service_reqs) != 1 or len(active_service_request.periods) != 1:
active_period = active_service_request.get_active_period()
if active_period.ps.ps_name == "Invited":
snowplow_call = "queuefromprep"
elif active_period.ps.ps_name == "Being Served":
snowplow_call = "returntoqueue"
else:
# TODO: Put in a Feedback Slack/Service now call here.
return {"message": "Invalid citizen/period state. "}

active_service_request.add_to_queue(csr, snowplow_call)

Expand Down
4 changes: 2 additions & 2 deletions api/app/resources/theq/citizen/citizen_begin_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ def post(self, id):
snowplow_event = "beginservice"
if active_period.ps.ps_name == "On hold":
snowplow_event = "invitefromhold"
if active_period.ps.ps_name == "Waiting":
snowplow_event = "invitefromlist"
if active_period.ps.ps_name == "Ticket Creation":
snowplow_event = "servecitizen"

active_service_request.begin_service(csr, snowplow_event)
except TypeError:
Expand Down
14 changes: 12 additions & 2 deletions api/app/resources/theq/citizen/citizen_finish_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ def post(self, id):
if active_service_request is None:
return {"message": "Citizen has no active service requests"}

quantity = active_service_request.quantity
SnowPlow.snowplow_event(citizen.citizen_id, csr, "finish",
quantity = active_service_request.quantity,
current_sr_number= active_service_request.sr_number)

active_sr_id = active_service_request.sr_id
active_service_request.finish_service(csr, self.clear_comments_flag)
citizen_state = CitizenState.query.filter_by(cs_state_name="Received Services").first()
citizen.cs_id = citizen_state.cs_id
Expand All @@ -53,7 +57,13 @@ def post(self, id):
db.session.add(citizen)
db.session.commit()

SnowPlow.snowplow_event(citizen.citizen_id, csr, "finish", quantity = quantity)
# Loop to stop all services in the service stopped state (which are all except the active service)
if len(citizen.service_reqs) != 1:
for sr in citizen.service_reqs:
if sr.sr_id != active_sr_id:
SnowPlow.snowplow_event(citizen.citizen_id, csr, "finishstopped",
quantity = sr.quantity,
current_sr_number= sr.sr_number)

socketio.emit('citizen_invited', {}, room='sb-%s' % csr.office.office_number)
result = self.citizen_schema.dump(citizen)
Expand Down
2 changes: 1 addition & 1 deletion api/app/resources/theq/citizen/citizen_generic_invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def post(self):
active_service_request = citizen.get_active_service_request()

try:
active_service_request.invite(csr, "invitecitizen")
active_service_request.invite(csr, invite_type="generic", sr_count = len(citizen.service_reqs))
except TypeError:
return {"message": "Error inviting citizen. Please try again."}, 400

Expand Down
27 changes: 25 additions & 2 deletions api/app/resources/theq/citizen/citizen_left.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,25 @@ def post(self, id):
citizen = Citizen.query.filter_by(citizen_id=id, office_id=csr.office_id).first()
sr_state = SRState.get_state_by_name("Complete")

# Create parameters for and make snowplow call. Default is no service request, CSR pressed cancel.
quantity = 0
sr_number = 1
active_sr = 0
status = "service-creation"
if len(citizen.service_reqs) != 0:
active_service_request = citizen.get_active_service_request()
quantity = active_service_request.quantity
sr_number = active_service_request.sr_number
active_sr = active_service_request.sr_id
active_period = active_service_request.get_active_period()
if active_period.ps.ps_name == "Invited":
status = "at-prep"
else:
status = "being-served"

SnowPlow.snowplow_event(citizen.citizen_id, csr, ("left/" + status),
quantity = quantity, current_sr_number= sr_number)

for service_request in citizen.service_reqs:

service_request.sr_state_id = sr_state.sr_state_id
Expand All @@ -45,6 +64,12 @@ def post(self, id):
if p.time_end is None:
p.time_end = datetime.now()

# Make snowplow calls to finish any stopped services
if service_request.sr_id != active_sr:
SnowPlow.snowplow_event(citizen.citizen_id, csr, "finishstopped",
quantity = service_request.quantity,
current_sr_number= service_request.sr_number)

citizen.cs = CitizenState.query.filter_by(cs_state_name='Left before receiving services').first()
if self.clear_comments_flag:
citizen.citizen_comments = None
Expand All @@ -55,8 +80,6 @@ def post(self, id):
db.session.add(citizen)
db.session.commit()

SnowPlow.snowplow_event(citizen.citizen_id, csr, "customerleft")

socketio.emit('citizen_invited', {}, room='sb-%s' % csr.office.office_number)
result = self.citizen_schema.dump(citizen)
socketio.emit('update_active_citizen', result.data, room=csr.office_id)
Expand Down
2 changes: 1 addition & 1 deletion api/app/resources/theq/citizen/citizen_specific_invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def post(self, id):
return {"message": "Citizen has no active service requests"}, 400

try:
active_service_request.invite(csr)
active_service_request.invite(csr, invite_type="specific", sr_count = len(citizen.service_reqs))
except TypeError:
return {"message": "Citizen has already been invited"}, 400

Expand Down
4 changes: 4 additions & 0 deletions api/app/resources/theq/csrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class CsrSelf(Resource):
def get(self):
try:
csr = CSR.find_by_username(g.oidc_token_info['username'])

if not csr:
return {'Message': 'User Not Found'}, 404

db.session.add(csr)
active_sr_state = SRState.get_state_by_name("Active")
today = datetime.now()
Expand Down
26 changes: 25 additions & 1 deletion api/app/resources/theq/feedback.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,17 @@ def send_to_service_now(params):
# as the email of this assignment group.
c = pysnow.Client(instance = instance, user=user, password=password)
incident = c.resource(api_path='/table/incident')
user = Feedback.extract_string(params, "Username: ", "\n", 0)
ticket = Feedback.extract_string(params, "Ticket Number: ","\n", 0)
msg = Feedback.extract_string(params, "Message: ", "", 50)
short_desc = "TheQ Feedback (User: " + user + "; Ticket: " + ticket + "; Msg: " + msg + ")"
new_record = {
'category': 'Inquiry / Help',
'cmdb_ci': 'CFMS',
'impact': '2 - Some Customers',
'urgency': '2 - High',
'priority': 'High',
'short_description': 'TheQ Feedback',
'short_description': short_desc,
'description': params,
'assignment_group': 'Service Delivery Tech Services (GARMS)'
}
Expand All @@ -117,6 +121,26 @@ def send_to_service_now(params):
else:
return {"message": "Service Now incident not created"}, 400

@staticmethod
def extract_string(big_string, key, endstr, max_if_not_found):
extracted = "Unknown"
start = big_string.find(key)
if start >= 0:
# End string specified.
if len(endstr) != 0:
end = big_string.find(endstr, start+len(key))
else:
end = -1

if end >= 0:
extracted = big_string[start+len(key):end]
else:
if max_if_not_found > 0:
extracted = big_string[start+len(key):]
extracted = extracted[0:min(max_if_not_found, len(extracted))]

return extracted

@staticmethod
def combine_results(slack_result, service_now_result):

Expand Down
44 changes: 44 additions & 0 deletions api/app/resources/theq/offices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'''Copyright 2018 Province of British Columbia
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.'''

from flask import g
from flask_restplus import Resource
from qsystem import api, db, oidc
from sqlalchemy import exc
from app.models.theq import CSR, Office
from app.schemas.theq import OfficeSchema


@api.route("/offices/", methods=["GET"])
class OfficeList(Resource):

office_schema = OfficeSchema(many=True)

@oidc.accept_token(require_token=True)
def get(self):
try:
csr = CSR.find_by_username(g.oidc_token_info['username'])

# if csr.role.role_code != "LIAISON":
# return {'message': 'You do not have permission to view this end-point'}, 403

offices = Office.query.filter(Office.deleted.is_(None))
result = self.office_schema.dump(offices)

return {'offices': result.data,
'errors': result.errors}

except exc.SQLAlchemyError as e:
print(e)
return {'message': 'API is down'}, 500
9 changes: 5 additions & 4 deletions api/app/resources/theq/service_requests_detail.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,12 @@ def post(self, id):
complete_service_state = SRState.get_state_by_name("Complete")

# Find the currently active service_request and close it
current_sr_number = 0
for req in service_request.citizen.service_reqs:
if req.sr_state_id == active_service_state.sr_state_id:
req.sr_state_id = complete_service_state.sr_state_id
req.finish_service(csr, clear_comments=False)
current_sr_number = req.sr_number
db.session.add(req)

# Then set the requested service to active
Expand All @@ -105,12 +107,11 @@ def post(self, id):
db.session.add(new_period)
db.session.add(service_request)

citizen_obj = Citizen.query.get(service_request.citizen_id)
citizen_obj.service_count = citizen_obj.service_count + 1

db.session.commit()

SnowPlow.choose_service(service_request, csr, "additionalservice")
# To make service active, stop current service, restart previous service.
SnowPlow.snowplow_event(service_request.citizen.citizen_id, csr, "stopservice", current_sr_number=current_sr_number)
SnowPlow.snowplow_event(service_request.citizen.citizen_id, csr, "restartservice", current_sr_number=service_request.sr_number)

citizen_result = self.citizen_schema.dump(service_request.citizen)
socketio.emit('update_active_citizen', citizen_result.data, room=csr.office_id)
Expand Down
Loading

0 comments on commit d923cf7

Please sign in to comment.