Skip to content
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

Merged
merged 11 commits into from
Feb 19, 2018

Conversation

AndrewTorrance
Copy link

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)

return "error retrieving details"
found = self.internal_user_dict.get(uuid)
if found:
return found, 200
Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Author

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()

Copy link
Contributor

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

Copy link
Author

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,
Copy link
Contributor

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 ?

Copy link
Contributor

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.

Copy link
Author

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.

Copy link
Contributor

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

Copy link
Author

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)
Copy link
Contributor

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?

Copy link
Author

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

Copy link
Contributor

@JamesGardiner JamesGardiner left a 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
Copy link
Contributor

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
Copy link
Contributor

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)):
Copy link
Contributor

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?

Copy link
Author

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)
Copy link
Contributor

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?

Copy link
Author

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 .

Copy link
Contributor

@JamesGardiner JamesGardiner Feb 13, 2018

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

Copy link
Author

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:
Copy link
Contributor

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.

Copy link
Author

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,
Copy link
Contributor

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:
Copy link
Contributor

@JamesGardiner JamesGardiner Feb 12, 2018

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.

Copy link
Author

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)) \
Copy link
Contributor

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

Copy link
Author

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)
Copy link
Contributor

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()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch , done

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
Copy link
Contributor

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

Copy link
Author

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)
Copy link
Contributor

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?

Copy link
Author

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

Copy link
Contributor

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tech debt ticket added

_, status_code = party.get_user_details(uuid)
return status_code == 200
return True if status_code == 200 else False
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Copy link
Author

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)
Copy link
Contributor

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)
Copy link
Contributor

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

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@warrenbailey warrenbailey dismissed their stale review February 13, 2018 14:50

Happy with the changes to my comments



def set_v1_resources(api):
api.add_resource(Health, '/health')
Copy link
Contributor

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?

Copy link
Contributor

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?

Copy link
Author

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,
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Author

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.

Copy link
Author

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

Copy link
Contributor

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?

Copy link
Author

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)
Copy link
Contributor

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

Copy link
Author

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
Copy link
Contributor

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

Copy link
Author

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

Copy link
Contributor

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')
Copy link
Contributor

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)
@warrenbailey
Copy link
Contributor

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.

Copy link
Contributor

@insacuri insacuri left a 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.

@AndrewTorrance AndrewTorrance dismissed JamesGardiner’s stale review February 19, 2018 10:21

Issues raised on separate tickets

@AndrewTorrance AndrewTorrance merged commit 08470f4 into master Feb 19, 2018
AndrewTorrance pushed a commit that referenced this pull request Feb 19, 2018
* 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
@dcdarrell9 dcdarrell9 deleted the feature/support-multiple-internal-users branch February 28, 2018 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants