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

Mtd 11.0 wave 2 #4

Open
wants to merge 50 commits into
base: 11.0
Choose a base branch
from
Open

Mtd 11.0 wave 2 #4

wants to merge 50 commits into from

Conversation

ShreeOpusVL
Copy link

@ShreeOpusVL ShreeOpusVL commented Jan 29, 2019

Note from PA - I have re-done 11.0 branches and the mtd-11.0 branch from a clean 8.0 base, cherry picking all commits which were included in the previous PR. This was to get git out of the messy state it was in.

Once this is merged in and we can be sure that we didn't miss anything - branches origin/mtd-11.0-wave-1 and origin/mtd_w1 can be deleted. As far as we are concerned with PRs - those branches can be ignored for now

This commit contains successful completion of Hello world API and hello
application API. There are some changes which are still required but
at least we can establish a connection. The hello user step one getting
the authorisation code successfully achieved (still need to handle the
exceptions) and step two gaining the access code and refresh code also
works. Now need to receive the hello user message (step three from the
connection.)
Getting hello user message from HMRC completed and some UI changes have
also been made.
user groups created added new data files for hello world. partial
logging added to hello world.
some more log messages added and changes to some existing log messages
While testing found an error which is now been fixed. and made few minor
changes
Changes request by Manish
MTD admin should alsonot be able to create any endpoints.
Mtd user should not be able to make changes to the HMRC configuration
and should have create access to api_request_tracker and api_tokens
@PeterAlabaster PeterAlabaster removed their request for review January 29, 2019 17:57
…ject

and then comparing the object rather than formatting date time into
string for comperison. Also changed the name of a variable
Copy link

@PeterAlabaster PeterAlabaster left a comment

Choose a reason for hiding this comment

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

Much better and cleaner this time round, though please see my comments


# to determine which API the authorization code has been received for.
api_tracker = http.request.env['mtd.api_request_tracker'].search([('closed', '=', False)])
if len(api_tracker) != 1:

Choose a reason for hiding this comment

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

Should this not read more like:

if not api_tracker:
    logger.warn("...")
    raise exceptions.Warning()
elif len(api_tracker) > 1:
    logger.info("...")
    ....
    do something
    ...
else:
    for record in api_tracker:
    ....
    do something
    ....

so that handling for none, one, and more than one is seperate? I'm just suggesting this based on the comment below about user should never be in a state where they found no tracker record

Copy link
Contributor

@Nick-OpusVL Nick-OpusVL Jan 31, 2019

Choose a reason for hiding this comment

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

Agreed with @PeterAlabaster . It will keep comments, if any are necessary at all, brief. The log message can be more focussed too.

_name = 'mtd.api'
_description = "list of api table"

name = fields.Char('Name', required=True, readonly=True)

Choose a reason for hiding this comment

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

My previous review still stands on all field declarations:
name = fields.Char() is the same as name = fields.Char('Name') and name = fields.Char(string='Name').
The only times where a string parameter is required in a field is if the string differs from the fieldname.

See https://www.odoo.com/documentation/10.0/reference/orm.html#creating-models

By default, the field's label (user-visible name) is a capitalized version of the field name, this can be overridden with the string parameter:

So I don't think some of these are totally necessary

Copy link
Contributor

@Nick-OpusVL Nick-OpusVL Jan 31, 2019

Choose a reason for hiding this comment

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

I'm still in two minds about this. I think @james-opusvl is in the "be explicit" camp, whereas @PeterAlabaster is saying lean on the defaults. I can see benefits to both

I would say if you're labelling them explicitly, use the kwarg string='Label' rather than leaning on the positional arg order, which varies depending on the type of field. No need to go onto a new line if all you're passing in is string but do so if it takes the line over 80 chars.

@@ -0,0 +1,27 @@
from odoo import models, fields, api

################################################################

Choose a reason for hiding this comment

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

I would suggest making this a class docstring like so:

class MtdApiRequestTracker(models.Model):
    """
    This model is created to keep track of any pending requests
    for access token. If one request is in process we can not
    send a request for a new one. This also contains information
    regarding where the request was sent from so when redirected
    back to Odoo we redirect user back to where they started from
    """
    _name = 'mtd.api_request_tracker'
    _description = "authorisation request table"

@@ -0,0 +1,25 @@
from odoo import models, fields, api

##############################################################

Choose a reason for hiding this comment

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

The same applies for this one. Also suggest changing module to model

which_button_type_clicked = fields.Char(string="which_button")
path = fields.Char(string="sandbox_url")
current_record = fields.Char()
# need a variable which will keep the record id so that when we

Choose a reason for hiding this comment

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

This sentence cuts out half way through

('res_id', '=', self.id), ('model', '=', 'mtd.hello_world')
])

if endpoint_record.name == "mtd_hello_world_endpoint":

Choose a reason for hiding this comment

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

I think there is a more pythonic way of doing this, I will sit at your desk to run through it

Choose a reason for hiding this comment

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

In fact, is endpoint_record something which a user can edit? in which case it is probably better to have it hard-coded like this.

Choose a reason for hiding this comment

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

mapping = {
    'mtd_hello_world_endpoint': '_handle_mtd_hello_world_endpoint',
    'mtd_hello_application_endpoint': '_handle_mtd_hello_application_endpoint',
    'mtd_hello_user_endpoint': '_handle_mtd_hello_user_endpoint'
}
if not mapping.get(endpoint_record.name):
    raise exceptions.Warning("Could not connect to HMRC! \nThis is not a valid HMRC service connection")
return getattr(self, mapping.get(endpoint_record.name))

I think should work

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep another step in the right direction (except you've not called the method returned by getattr()), but why not:

request_handlers = {
   ...
}
try:
    handler_name = request_handlers[endpoint_record.name]
except KeyError:
    raise exceptions.Warning(...)
else:
    handle_request = getattr(self, handler_name)
    return handle_request()

or

handler_name = request_handlers.get(endpoint_record.name)
if handler_name:
    handle_request = getattr(self, handler_name)
    return handle_request()
else:
    raise exceptions.Warning(...)

It's also possible to define handler_name in the above thus:

handler_name = "_handle_" + endpoint_record.name
try:
    handle_request = getattr(self, handler_name)
except AttributeError:
    raise exceptions.Warning()
else:
    return handle_request()

BUT...

I think this actually fits the strategy pattern [1] but I don't know where the split between classes (or models in Odoo's case) would go without a much better understanding of how all these bits of logic and data are interacting.

[1] https://en.wikipedia.org/wiki/Strategy_pattern

_name = 'mtd.hello_world'
_description = "Hello world to test connection between Odoo and HMRC"

_logger = logging.getLogger(__name__)

Choose a reason for hiding this comment

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

if you make _logger global (put it just under from urllib.parse import urlparse) then you can change every instance of self._logger() to just _logger

Copy link
Contributor

Choose a reason for hiding this comment

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

Seconded.

Leave a couple of blank lines either side of it for pep8 though.

else:
return self.get_user_authorisation()

def _json_command(self, command, timeout=3, record_id=None):

Choose a reason for hiding this comment

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

These are pretty hefty functions. Is there any way that these could be reduced, like having common shared functionality (it may not be shared yet, but might be in future?) split out into smaller functions?

Copy link
Contributor

@Nick-OpusVL Nick-OpusVL Jan 31, 2019

Choose a reason for hiding this comment

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

Doesn't need to be shared functionality to be worthy of being in a method, but common stuff (as long as it's not only common by coincidence) should be. If something is a sequence of things that need to be done.

_this()
_that()
_the_other()

and each of those functions' bodies explain how to do this, that and the other, it's much easier to read. Just be careful not to mix levels of abstraction.

Copy link
Contributor

@Nick-OpusVL Nick-OpusVL left a comment

Choose a reason for hiding this comment

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

Improved, but I have more questions and suggestions. Some comments are in answer to Peter's so I hope it gives you the full conversation...

def get_user_authorization(self, **args):

# to determine which API the authorization code has been received for.
api_tracker = http.request.env['mtd.api_request_tracker'].search([('closed', '=', False)])
Copy link
Contributor

Choose a reason for hiding this comment

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

Pluralise this name - we can't assert there is only one at this stage.


# to determine which API the authorization code has been received for.
api_tracker = http.request.env['mtd.api_request_tracker'].search([('closed', '=', False)])
if len(api_tracker) != 1:
Copy link
Contributor

@Nick-OpusVL Nick-OpusVL Jan 31, 2019

Choose a reason for hiding this comment

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

Agreed with @PeterAlabaster . It will keep comments, if any are necessary at all, brief. The log message can be more focussed too.

"\nWe can have more than one if there was an initial request and authorisation it was never completed."+
"\nIf in this state we need to reset the tracker and get user to create a new connection " +
"\napi_tracer = {}".format(api_tracker))
for record in api_tracker:
Copy link
Contributor

Choose a reason for hiding this comment

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

Equivalent:

api_trackers.write({'closed': True})

(I assume you now mean closed = True as that's what you're searching on earlier, apologies if not).

Copy link
Contributor

Choose a reason for hiding this comment

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

The log messages are a bit verbose. What you're about to do or the results of what you've just done are fine, but don't embed pseudocode in them.

_name = 'mtd.api_tokens'
_description = "api token table"

api_id = fields.Char('API Id', required=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this map onto the HMRC API?

_description = "api token table"

api_id = fields.Char('API Id', required=True)
api_name = fields.Char('API Name', required=True)
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this map onto the HMRC API? I presume it's different from the api_id.

# refresh_token = token_record.refresh_token if token_record else ""

header_items = {"Accept": "application/vnd.hmrc.1.0+json"}
if self.which_button_type_clicked == "application":
Copy link
Contributor

Choose a reason for hiding this comment

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

This is smelling like strategy based on endpoint again, but not completely sure.

Copy link
Author

Choose a reason for hiding this comment

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

Yes headers are different for different endpoints

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't mean that, I was talking about the switching on which_button_type_clicked.

Copy link
Author

@ShreeOpusVL ShreeOpusVL Feb 4, 2019

Choose a reason for hiding this comment

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

Yes based on the endpoints, headers are different which is why I have used an internal variable to find out which endpoint I was in. Originally we just had one screen with three different buttons which is why I used the variable name "which_button_type_clicked."

Copy link
Contributor

Choose a reason for hiding this comment

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

What does it mean now then?

Copy link
Author

@ShreeOpusVL ShreeOpusVL Feb 4, 2019

Choose a reason for hiding this comment

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

which endpoint did the user come from hello world, application or user. I will change the name of the variable.

)
response = requests.get(authorisation_url, timeout=3)
# response_token = json.loads(req.text)
self._logger.info(
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if some of these belong at debug rather than info level.

else:
response_token = json.loads(response.text)
error_message = (
"Date {date} Time {time} \n".format(date=datetime.utcnow().date(), time=datetime.utcnow().time())
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you want to display times to users in UTC?

Copy link
Author

Choose a reason for hiding this comment

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

yes in the time zone they are in right? That's what you suggested in the previous pr.

Copy link
Contributor

Choose a reason for hiding this comment

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

No they're in Europe/London. When BST starts these strings will be 1 hour out.

The ORM's date/datetime/time fields work in UTC (which is the context in which I suggested using utcnow()), but when formatting a string for humans you need to use local time.

else:
return self.get_user_authorisation()

def _json_command(self, command, timeout=3, record_id=None):
Copy link
Contributor

@Nick-OpusVL Nick-OpusVL Jan 31, 2019

Choose a reason for hiding this comment

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

Doesn't need to be shared functionality to be worthy of being in a method, but common stuff (as long as it's not only common by coincidence) should be. If something is a sequence of things that need to be done.

_this()
_that()
_the_other()

and each of those functions' bodies explain how to do this, that and the other, it's much easier to read. Just be careful not to mix levels of abstraction.

_name = 'mtd.api'
_description = "list of api table"

name = fields.Char('Name', required=True, readonly=True)
Copy link
Contributor

@Nick-OpusVL Nick-OpusVL Jan 31, 2019

Choose a reason for hiding this comment

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

I'm still in two minds about this. I think @james-opusvl is in the "be explicit" camp, whereas @PeterAlabaster is saying lean on the defaults. I can see benefits to both

I would say if you're labelling them explicitly, use the kwarg string='Label' rather than leaning on the positional arg order, which varies depending on the type of field. No need to go onto a new line if all you're passing in is string but do so if it takes the line over 80 chars.

adding an extra selection option for the variable
adding an if to make sure we have a value for the error variable only
then we need to add a new line
Needed to change the varible name in the data file as the variable name
in the module had been changed.
Whether the response is positive or negative we still need to close the
request tracker so that user can place a new request if required.
We need to be able to inherit the hello world module for other mtd
modules and use the authorisation steps.
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.

3 participants