-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/support multiple internal users #140
Feature/support multiple internal users #140
Conversation
…ing user id . I.e a prerequisite to having multiple internal users
return "error retrieving details" | ||
found = self.internal_user_dict.get(uuid) | ||
if found: | ||
return found, 200 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have done like :
if uuid in elf.internal_user_dict
return true, 200
return False, 400
just for consistency in the return type. and log with error/warning user not found.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we try and use EAFP instead of LBYL? https://blogs.msdn.microsoft.com/pythonengineering/2016/06/29/idiomatic-python-eafp-versus-lbyl/
try:
return self.internal_user_dict[uuid], 200
except KeyError:
logger.exception(f'No internal user with uuid {uuid}'
return "error retrieving details", 404
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, don't inspect the uuid arg. If it's None then we'll still catch the exception, and if we set None as a key in a dictionary we have bigger problems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally I would support EAFP instead of LBYL . However in this case the existing case and party mocks are written so as to mimic the http response which would not raise an exception to the code but return an error . In this specific of the internal_user_service_mock it is something of a guess to assume it will be a http response . If it isnt and we do get exceptions I will change the mock implementation to suit. The uuid in elf.internal_user_dict needs to look in the values()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My suggestion doesn't change the return values, only the way in which the decision for what values to return is made
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok , misunderstood , changed
status_conditions.append(Status.actor == str(user.user_uuid)) | ||
else: | ||
status_conditions.append(Status.actor == constants.BRES_USER) | ||
return Retriever._retrieve_message_list_respondent(page, limit, user=user, ru_id=ru_id, survey=survey, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure about the python way to do things, but when there ar eso many parameters is not better to encapsulate them all in a Object ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be made cleaner by refactoring the is not None
below to just do dict key access on kwargs passed here. If a KeyError is raised, you know the key isn't passed and therefore can do the else
condition from below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with possibly moving the parameters to kwargs . However this how the existing code handles it. So whilst I agree its best to follow the boy scout principle , in this case I will raise a ticket to loop back depending on time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use the named tuple created in the calling function instead? It seems to create the Named Tuple when the request arrives but then unpack it to seperate variables in order to call these function when actually everything you need (apart from the user) is in the tuple
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am putting this on a tech debt ticket that I plan on coming back to asap
.order_by(t.c.max_date.asc()).paginate(page, limit, False) | ||
|
||
except Exception as e: | ||
logger.error('Error retrieving messages from database', error=e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be a logger.exception?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch , I copied existing code , changing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only able to get to _retrieve_message_list_internal so far. Will finish tomorrow
return "error retrieving details" | ||
found = self.internal_user_dict.get(uuid) | ||
if found: | ||
return found, 200 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we try and use EAFP instead of LBYL? https://blogs.msdn.microsoft.com/pythonengineering/2016/06/29/idiomatic-python-eafp-versus-lbyl/
try:
return self.internal_user_dict[uuid], 200
except KeyError:
logger.exception(f'No internal user with uuid {uuid}'
return "error retrieving details", 404
return "error retrieving details" | ||
found = self.internal_user_dict.get(uuid) | ||
if found: | ||
return found, 200 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, don't inspect the uuid arg. If it's None then we'll still catch the exception, and if we set None as a key in a dictionary we have bigger problems.
for row in self.statuses: | ||
if row.actor == actor: | ||
if row.actor == user.user_uuid or (user.is_internal and (row.actor == constants.NON_SPECIFIC_INTERNAL_USER or row.actor == constants.BRES_USER)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this here for the purposes of passing tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No , its needed so that the status's are retrieved correctly in the case that a respondent sends a message to a group . If the user is internal we want to see the status's even if the message was sent to a group .
@@ -91,12 +91,14 @@ def del_draft(draft_id): | |||
del_draft_status = "DELETE FROM securemessage.status WHERE msg_id='{0}' AND label='{1}'".format(draft_id, Labels.DRAFT.value) | |||
del_draft_event = "DELETE FROM securemessage.events WHERE msg_id='{0}'".format(draft_id) | |||
del_draft_inbox_status = "DELETE FROM securemessage.status WHERE msg_id='{0}' AND label='{1}'".format(draft_id, Labels.DRAFT_INBOX.value) | |||
del_actors = "DELETE FROM securemessage.actors where msg_id='{0}'".format(draft_id) | |||
del_draft_msg = "DELETE FROM securemessage.secure_message WHERE msg_id='{0}'".format(draft_id) | |||
|
|||
try: | |||
db.get_engine(app=db.get_app()).execute(del_draft_status) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this where we should be using db.session?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Possibly , I must admit I followed what was there. Not sure of the implications of changing to that . The draft is normally deleted in the context of sending the same message as a draft .Other functions in the same class do use the session so Its possible that the two might interfere with each other , but unless I did an investigation I could not be sure .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Session should be used. It encapsulates db.get_engine. See http://flask-sqlalchemy.pocoo.org/2.3/api/#sessions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added as a tech debt issue
del_draft_msg = "DELETE FROM securemessage.secure_message WHERE msg_id='{0}'".format(draft_id) | ||
|
||
try: | ||
db.get_engine(app=db.get_app()).execute(del_draft_status) | ||
db.get_engine(app=db.get_app()).execute(del_draft_inbox_status) | ||
db.get_engine(app=db.get_app()).execute(del_draft_event) | ||
db.get_engine(app=db.get_app()).execute(del_actors) | ||
db.get_engine(app=db.get_app()).execute(del_draft_msg) | ||
|
||
except Exception as e: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably worth changing this to logger.exception while we are at it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
status_conditions.append(Status.actor == str(user.user_uuid)) | ||
else: | ||
status_conditions.append(Status.actor == constants.BRES_USER) | ||
return Retriever._retrieve_message_list_respondent(page, limit, user=user, ru_id=ru_id, survey=survey, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be made cleaner by refactoring the is not None
below to just do dict key access on kwargs passed here. If a KeyError is raised, you know the key isn't passed and therefore can do the else
condition from below.
@@ -46,15 +52,73 @@ def retrieve_message_list(page, limit, user, ru_id=None, survey=None, cc=None, c | |||
|
|||
try: | |||
t = db.session.query(SecureMessage.msg_id, func.max(Events.date_time) # pylint:disable=no-member | |||
.label('max_date'))\ | |||
.label('max_date')) \ | |||
.join(Events).join(Status).outerjoin(Actors) \ | |||
.filter(and_(*conditions)) \ | |||
.filter(and_(*status_conditions)) \ | |||
.filter(or_(Events.event == EventsApi.SENT.value, Events.event == EventsApi.DRAFT_SAVED.value)) \ | |||
.group_by(SecureMessage.msg_id).subquery('t') | |||
|
|||
if descend: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you follow my approach above set this to be a default kwarg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have raised a ticket to look at cleaning this up as mentioned above . I agree it can be improved
.join(Events).join(Status).outerjoin(Actors) \ | ||
.filter(and_(*conditions)) \ | ||
.filter(or_(*actor_conditions)) \ | ||
.filter(~Status.label.in_(status_reject_conditions)) \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth a comment here that SQLAlchemy overrides the unary bitwise operator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
else: | ||
result = SecureMessage.query \ | ||
.filter(SecureMessage.msg_id == t.c.msg_id) \ | ||
.order_by(t.c.max_date.asc()).paginate(page, limit, False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sqlalchemy allows query building, so the duplicated code here can by completely removed apart from the .asc or .desc.
Or just do
if descend:
order = t.c.max_date.desc()
else:
order = t.c.max_date.asc()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch , done
secure_message/validation/user.py
Outdated
def is_valid_user(uuid): | ||
def is_valid_internal_user(uuid): | ||
_, status_code = internal_user_service.get_user_details(uuid) | ||
return True if status_code == 200 else False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can be simplified to
return status_code == 200
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
doh , thanks , done
del_draft_msg = "DELETE FROM securemessage.secure_message WHERE msg_id='{0}'".format(draft_id) | ||
|
||
try: | ||
db.get_engine(app=db.get_app()).execute(del_draft_status) | ||
db.get_engine(app=db.get_app()).execute(del_draft_inbox_status) | ||
db.get_engine(app=db.get_app()).execute(del_draft_event) | ||
db.get_engine(app=db.get_app()).execute(del_actors) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could we just call get_engine once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must admit I am not sure , I followed on from what was there before . I can look at this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think James comment above supersedes this now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tech debt ticket added
secure_message/validation/user.py
Outdated
_, status_code = party.get_user_details(uuid) | ||
return status_code == 200 | ||
return True if status_code == 200 else False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
del_draft_msg = "DELETE FROM securemessage.secure_message WHERE msg_id='{0}'".format(draft_id) | ||
|
||
try: | ||
db.get_engine(app=db.get_app()).execute(del_draft_status) | ||
db.get_engine(app=db.get_app()).execute(del_draft_inbox_status) | ||
db.get_engine(app=db.get_app()).execute(del_draft_event) | ||
db.get_engine(app=db.get_app()).execute(del_actors) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think James comment above supersedes this now
db.get_engine(app=db.get_app()).execute(del_draft_msg) | ||
|
||
except Exception as e: | ||
logger.error('Error deleting draft from database', msg_id=draft_id, error=e) | ||
logger.exception('Error deleting draft from database', msg_id=draft_id, error=e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logger.exception looks up the stack trace to find the exception so error=e is not needed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
Happy with the changes to my comments
|
||
|
||
def set_v1_resources(api): | ||
api.add_resource(Health, '/health') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these going to be updated following the tech session review yesterday?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the v1 resources aren't they?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you mean the health endpoint , I am not aware of any request to change that
status_conditions.append(Status.actor == str(user.user_uuid)) | ||
else: | ||
status_conditions.append(Status.actor == constants.BRES_USER) | ||
return Retriever._retrieve_message_list_respondent(page, limit, user=user, ru_id=ru_id, survey=survey, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use the named tuple created in the calling function instead? It seems to create the Named Tuple when the request arrives but then unpack it to seperate variables in order to call these function when actually everything you need (apart from the user) is in the tuple
|
||
except Exception as e: | ||
logger.error('Error retrieving messages from database', error=e) | ||
raise InternalServerError(description="Error retrieving messages from database") | ||
|
||
return True, result | ||
|
||
@staticmethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really confused to why these methods return True, result?
The calling function does an if on the status which is always true?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch . Me too , changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually tried to change as I agree it looks wrong . Get lots of failing tests . Raised a tech debt ticket . Plan on looping back to this
raise InternalServerError(description="Error retrieving messages from database") | ||
|
||
return True, result | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really confused to why these methods return True, result?
The calling functions do an if on the status but its always true so appears to be pointless, can we just remove it and return the result?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above , done
except Exception as e: | ||
logger.error('Error retrieving count of unread messages from database', error=e) | ||
logger.exception('Error retrieving count of unread messages from database', error=e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as above no need to log error=e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
if label is not None: | ||
valid_statuses.append(label) | ||
if label in [Labels.INBOX.value, Labels.ARCHIVE.value, Labels.UNREAD.value]: | ||
actor_conditions.append(Actors.sent_from_internal == False) # NOQA pylint:disable=singleton-comparison |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This to me sums up why the DB structure isn't correct because it shouldn't be this complicated to determine if the internal user has sent or received the message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thats not what its doing . Its adding predicates specifically only when someone has asked for only messages with specific labels to be returned . The simple sent_from_internal flag tells us if it was sent by an internal person
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems I misunderstood it then
|
||
|
||
def set_v1_resources(api): | ||
api.add_resource(Health, '/health') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are the v1 resources aren't they?
* master: Add simple makefile and update readme (#142)
Approved as tech debt tickets have been raised to fix the other comments and the understanding they'll be address asap in a separate PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks reasonable and tech debt issues have been raised so I'm happy for this to go in.
Issues raised on separate tickets
* master: Feature/support multiple internal users (#140) # Conflicts: # secure_message/api_mocks/internal_user_service_mock.py # secure_message/application.py # secure_message/repository/retriever.py # secure_message/resources/drafts.py # secure_message/validation/domain.py # tests/app/test_app.py # tests/app/test_retriever.py # tests/behavioural/features/draft_post.feature # tests/behavioural/features/drafts_get.feature # tests/behavioural/features/message_post.feature # tests/behavioural/features/messages_get.feature # tests/behavioural/features/steps/from_field.py # tests/behavioural/features/steps/secure_messaging_context_helper.py
What is the context of this PR?
This supports the changing of the database such that the existing tests all pass . It is a preparatory step prior to supporting multiple internal users. The reason for the PR is that we need a breaking change in order to support multiple users and hence we need to version the api .
Added Actors column which supports identifying if an actor is internal or external. Added support for a message to a group by using the keyword 'GROUP' in the to field. ( could not use empty as that would fail validation in the case of a user not setting an actor)