Browse Source

Merge pull request #71 from coopiteasy/12.0-emc-rest-api

[12.0] Easy my Coop REST API
pull/117/head
Robin Keunen 4 years ago
committed by GitHub
parent
commit
1bfc65df8d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .isort.cfg
  2. 85
      easy_my_coop_api/README.rst
  3. 3
      easy_my_coop_api/__init__.py
  4. 24
      easy_my_coop_api/__manifest__.py
  5. 1
      easy_my_coop_api/controllers/__init__.py
  6. 25
      easy_my_coop_api/controllers/controllers.py
  7. 1
      easy_my_coop_api/models/__init__.py
  8. 33
      easy_my_coop_api/models/auth_api_key.py
  9. 2
      easy_my_coop_api/readme/CONTRIBUTORS.rst
  10. 1
      easy_my_coop_api/readme/DESCRIPTION.rst
  11. 7
      easy_my_coop_api/readme/ROADMAP.rst
  12. 12
      easy_my_coop_api/readme/USAGE.rst
  13. 2
      easy_my_coop_api/services/__init__.py
  14. 37
      easy_my_coop_api/services/ping_service.py
  15. 95
      easy_my_coop_api/services/schemas.py
  16. 167
      easy_my_coop_api/services/subscription_request_service.py
  17. 442
      easy_my_coop_api/static/description/index.html
  18. 3
      easy_my_coop_api/tests/__init__.py
  19. 106
      easy_my_coop_api/tests/common.py
  20. 42
      easy_my_coop_api/tests/test_ping.py
  21. 28
      easy_my_coop_api/tests/test_registry.py
  22. 143
      easy_my_coop_api/tests/test_subscription_requests.py
  23. 4
      oca_dependencies.txt

2
.isort.cfg

@ -9,4 +9,4 @@ line_length=79
known_odoo=odoo
known_odoo_addons=odoo.addons
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
known_third_party=addons,cStringIO,openerp,requests,setuptools,werkzeug,xlsxwriter
known_third_party=addons,cStringIO,lxml,openerp,requests,setuptools,werkzeug,xlsxwriter

85
easy_my_coop_api/README.rst

@ -0,0 +1,85 @@
================
Easy My Coop API
================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-coopiteasy%2Fvertical--cooperative-lightgray.png?logo=github
:target: https://github.com/coopiteasy/vertical-cooperative/tree/12.0/easy_my_coop_api
:alt: coopiteasy/vertical-cooperative
|badge1| |badge2| |badge3|
Open Easy My Coop to the world: RESTful API.
**Table of contents**
.. contents::
:local:
Usage
=====
To give access to the API to a structure, go to
- Settings > Technical (debug mode) > Auth API Key
- click create and select a user, save.
- communicate the API-KEY to the structure.
It is recommended to create a technical user for the structure belonging to the group "Easy My Coop User".
For example, for the structure Coop IT Easy, create partner with
- name = coopiteasy-api-user
- Application Accesses = Cooperative Management / User
- Platform Structure = Coop IT Easy
Known issues / Roadmap
======================
The API should generate and use an external id for records instead
of odoo's generated id. It would make importing and export data as
well as migrating across versions easier.
One way would be to use uuid but the default BaseRESTController
routes would need to be rewritten: they only take integer as ids.
Another way is to define a sequence per model.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/coopiteasy/vertical-cooperative/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/coopiteasy/vertical-cooperative/issues/new?body=module:%20easy_my_coop_api%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Coop IT Easy SCRLfs
Contributors
~~~~~~~~~~~~
* Coop IT Easy SCRLfs
* Robin Keunen <robin@coopiteasy.be>
Maintainers
~~~~~~~~~~~
This module is part of the `coopiteasy/vertical-cooperative <https://github.com/coopiteasy/vertical-cooperative/tree/12.0/easy_my_coop_api>`_ project on GitHub.
You are welcome to contribute.

3
easy_my_coop_api/__init__.py

@ -0,0 +1,3 @@
from . import controllers
from . import models
from . import services

24
easy_my_coop_api/__manifest__.py

@ -0,0 +1,24 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
{
"name": "Easy My Coop API",
"version": "12.0.0.0.1",
"depends": [
"base_rest",
"easy_my_coop",
"auth_api_key", # todo conf running_env = dev
],
"author": "Coop IT Easy SCRLfs",
"category": "Cooperative management",
"website": "www.coopiteasy.be",
"license": "AGPL-3",
"summary": """
Open Easy My Coop to the world: RESTful API.
""",
"data": [],
"demo": [],
"installable": True,
"application": False,
}

1
easy_my_coop_api/controllers/__init__.py

@ -0,0 +1 @@
from . import controllers

25
easy_my_coop_api/controllers/controllers.py

@ -0,0 +1,25 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo.http import route
from odoo.addons.base_rest.controllers import main
class UserController(main.RestController):
_root_path = "/api/"
_collection_name = "emc.services"
_default_auth = "api_key"
@route(
_root_path + "<string:_service_name>/test",
methods=["GET"],
auth="public",
csrf=False,
)
def test(self, _service_name):
return self._process_method(
_service_name, "test", _id=None, params=None
)

1
easy_my_coop_api/models/__init__.py

@ -0,0 +1 @@
from . import auth_api_key

33
easy_my_coop_api/models/auth_api_key.py

@ -0,0 +1,33 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import uuid
from odoo import api, fields, models
class AuthApiKey(models.Model):
_inherit = "auth.api.key"
def _default_key(self):
return uuid.uuid4()
# overloaded fields
# required is set to false to allow for a computed field,
# it will always be set.
name = fields.Char(required=False, compute="_compute_name", store=True)
key = fields.Char(default=_default_key)
@api.multi
@api.depends("user_id")
def _compute_name(self):
for key in self:
if key.user_id:
now = fields.Datetime.now()
key.name = "{login}-{now}".format(
now=fields.Datetime.to_string(now), login=key.user_id.login
)
else:
key.name = "no-user"

2
easy_my_coop_api/readme/CONTRIBUTORS.rst

@ -0,0 +1,2 @@
* Coop IT Easy SCRLfs
* Robin Keunen <robin@coopiteasy.be>

1
easy_my_coop_api/readme/DESCRIPTION.rst

@ -0,0 +1 @@
Open Easy My Coop to the world: RESTful API.

7
easy_my_coop_api/readme/ROADMAP.rst

@ -0,0 +1,7 @@
The API should generate and use an external id for records instead
of odoo's generated id. It would make importing and export data as
well as migrating across versions easier.
One way would be to use uuid but the default BaseRESTController
routes would need to be rewritten: they only take integer as ids.
Another way is to define a sequence per model.

12
easy_my_coop_api/readme/USAGE.rst

@ -0,0 +1,12 @@
To give access to the API to a structure, go to
- Settings > Technical (debug mode) > Auth API Key
- click create and select a user, save.
- communicate the API-KEY to the structure.
It is recommended to create a technical user for the structure belonging to the group "Easy My Coop User".
For example, for the structure Coop IT Easy, create partner with
- name = coopiteasy-api-user
- Application Accesses = Cooperative Management / User
- Platform Structure = Coop IT Easy

2
easy_my_coop_api/services/__init__.py

@ -0,0 +1,2 @@
from . import ping_service
from . import subscription_request_service

37
easy_my_coop_api/services/ping_service.py

@ -0,0 +1,37 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
# pylint: disable=consider-merging-classes-inherited
from odoo import _
from odoo.addons.component.core import Component
class PingService(Component):
_inherit = "base.rest.service"
_name = "emc.services"
_usage = "ping" # service_name
_collection = "emc.services"
_description = """
Ping services (test the api)
"""
def search(self):
return {"message": _("Called search on ping API")}
def test(self):
return {"message": _("Called ping on ping API")}
def _validator_test(self):
return {}
def _validator_return_test(self):
return {"message": {"type": "string"}}
def _validator_search(self):
return {}
def _validator_return_search(self):
return {"message": {"type": "string"}}

95
easy_my_coop_api/services/schemas.py

@ -0,0 +1,95 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import _
from odoo.fields import Date
def date_validator(field, value, error):
try:
Date.from_string(value)
except ValueError as e:
return error(
field, _("{} does not match format '%Y-%m-%d'".format(value))
)
S_SUBSCRIPTION_REQUEST_GET = {"_id": {"type": "integer"}}
S_SUBSCRIPTION_REQUEST_RETURN_GET = {
"id": {"type": "integer", "required": True},
"email": {"type": "string", "required": True, "empty": False},
"name": {"type": "string", "required": True, "empty": False},
"date": {"type": "string", "required": True, "empty": False},
"state": {"type": "string", "required": True, "empty": False},
"ordered_parts": {"type": "integer", "required": True},
"share_product": {
"type": "dict",
"schema": {
"id": {"type": "integer", "required": True},
"name": {"type": "string", "required": True, "empty": False},
},
},
"address": {
"type": "dict",
"schema": {
"street": {"type": "string", "required": True, "empty": False},
"zip_code": {"type": "string", "required": True, "empty": False},
"city": {"type": "string", "required": True, "empty": False},
"country": {"type": "string", "required": True, "empty": False},
},
},
"lang": {"type": "string", "required": True, "empty": False},
}
S_SUBSCRIPTION_REQUEST_SEARCH = {
"date_from": {"type": "string", "check_with": date_validator},
"date_to": {"type": "string", "check_with": date_validator},
}
S_SUBSCRIPTION_REQUEST_RETURN_SEARCH = {
"count": {"type": "integer", "required": True},
"rows": {
"type": "list",
"schema": {
"type": "dict",
"schema": S_SUBSCRIPTION_REQUEST_RETURN_GET,
},
},
}
S_SUBSCRIPTION_REQUEST_CREATE = {
"name": {"type": "string", "required": True, "empty": False},
"email": {"type": "string", "required": True, "empty": False},
"ordered_parts": {"type": "integer", "required": True},
"share_product": {"type": "integer", "required": True},
"address": {
"type": "dict",
"schema": {
"street": {"type": "string", "required": True, "empty": False},
"zip_code": {"type": "string", "required": True, "empty": False},
"city": {"type": "string", "required": True, "empty": False},
"country": {"type": "string", "required": True, "empty": False},
},
},
"lang": {"type": "string", "required": True, "empty": False},
}
S_SUBSCRIPTION_REQUEST_UPDATE = {
"name": {"type": "string"},
"email": {"type": "string"},
"ordered_parts": {"type": "integer"},
"state": {"type": "string"},
"address": {
"type": "dict",
"schema": {
"street": {"type": "string"},
"zip_code": {"type": "string"},
"city": {"type": "string"},
"country": {"type": "string"},
},
},
"lang": {"type": "string"},
"share_product": {"type": "integer"},
}

167
easy_my_coop_api/services/subscription_request_service.py

@ -0,0 +1,167 @@
# Copyright 2019 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
# pylint: disable=consider-merging-classes-inherited
import logging
from werkzeug.exceptions import BadRequest, NotFound
from odoo import _
from odoo.fields import Date
from odoo.addons.base_rest.http import wrapJsonException
from odoo.addons.component.core import Component
from . import schemas
_logger = logging.getLogger(__name__)
class SubscriptionRequestService(Component):
_inherit = "base.rest.service"
_name = "subscription.request.services"
_usage = "subscription-request"
_collection = "emc.services"
_description = """
Subscription requests
"""
def get(self, _id):
sr = self.env["subscription.request"].browse(_id)
if sr:
return self._to_dict(sr)
else:
raise wrapJsonException(
NotFound(_("No subscription request for id %s") % _id)
)
def search(self, date_from=None, date_to=None):
_logger.info("search from {} to {}".format(date_from, date_to))
domain = []
if date_from:
date_from = Date.from_string(date_from)
domain.append(("date", ">=", date_from))
if date_to:
date_to = Date.from_string(date_to)
domain.append(("date", "<=", date_to))
requests = self.env["subscription.request"].search(domain)
response = {
"count": len(requests),
"rows": [self._to_dict(sr) for sr in requests],
}
return response
def create(self, **params): # pylint: disable=method-required-super
params = self._prepare_create(params)
sr = self.env["subscription.request"].create(params)
return self._to_dict(sr)
def update(self, _id, **params):
params = self._prepare_update(params)
sr = self.env["subscription.request"].browse(_id)
if not sr:
raise wrapJsonException(
NotFound(_("No subscription request for id %s") % _id)
)
sr.write(params)
return self._to_dict(sr)
def _to_dict(self, sr):
sr.ensure_one()
return {
"id": sr.id,
"name": sr.name,
"email": sr.email,
"state": sr.state,
"date": Date.to_string(sr.date),
"ordered_parts": sr.ordered_parts,
"share_product": {
"id": sr.share_product_id.id,
"name": sr.share_product_id.name,
},
"address": {
"street": sr.address,
"zip_code": sr.zip_code,
"city": sr.city,
"country": sr.country_id.code,
},
"lang": sr.lang,
}
def _get_country(self, code):
country = self.env["res.country"].search([("code", "=", code)])
if country:
return country
else:
raise wrapJsonException(
BadRequest(_("No country for isocode %s") % code)
)
def _prepare_create(self, params):
address = params["address"]
country = self._get_country(address["country"])
return {
"name": params["name"],
"email": params["email"],
"ordered_parts": params["ordered_parts"],
"share_product_id": params["share_product"],
"address": address["street"],
"zip_code": address["zip_code"],
"city": address["city"],
"country_id": country.id,
"lang": params["lang"],
}
def _prepare_update(self, params):
if "address" in params:
address = params["address"]
if "country" in address:
country = self._get_country(address["country"]).id
address["country"] = country
else:
address = {}
params = {
"name": params.get("name"),
"email": params.get("email"),
"state": params.get("state"),
"ordered_parts": params.get("ordered_parts"),
"share_product_id": params.get("share_product"),
"address": address.get("street"),
"zip_code": address.get("zip_code"),
"city": address.get("city"),
"country_id": address.get("country"),
"lang": params.get("lang"),
}
params = {k: v for k, v in params.items() if v is not None}
return params
def _validator_get(self):
return schemas.S_SUBSCRIPTION_REQUEST_GET
def _validator_return_get(self):
return schemas.S_SUBSCRIPTION_REQUEST_RETURN_GET
def _validator_search(self):
return schemas.S_SUBSCRIPTION_REQUEST_SEARCH
def _validator_return_search(self):
return schemas.S_SUBSCRIPTION_REQUEST_RETURN_SEARCH
def _validator_create(self):
return schemas.S_SUBSCRIPTION_REQUEST_CREATE
def _validator_return_create(self):
return schemas.S_SUBSCRIPTION_REQUEST_RETURN_GET
def _validator_update(self):
return schemas.S_SUBSCRIPTION_REQUEST_UPDATE
def _validator_return_update(self):
return schemas.S_SUBSCRIPTION_REQUEST_RETURN_GET

442
easy_my_coop_api/static/description/index.html

@ -0,0 +1,442 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
<title>Easy My Coop API</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="easy-my-coop-api">
<h1 class="title">Easy My Coop API</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/coopiteasy/vertical-cooperative/tree/12.0/easy_my_coop_api"><img alt="coopiteasy/vertical-cooperative" src="https://img.shields.io/badge/github-coopiteasy%2Fvertical--cooperative-lightgray.png?logo=github" /></a></p>
<p>Open Easy My Coop to the world: RESTful API.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#known-issues-roadmap" id="id2">Known issues / Roadmap</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>To give access to the API to a structure, go to</p>
<ul class="simple">
<li>Settings &gt; Technical (debug mode) &gt; Auth API Key</li>
<li>click create and select a user, save.</li>
<li>communicate the API-KEY to the structure.</li>
</ul>
<p>It is recommended to create a technical user for the structure belonging to the group “Easy My Coop User”.
For example, for the structure Coop IT Easy, create partner with</p>
<ul class="simple">
<li>name = coopiteasy-api-user</li>
<li>Application Accesses = Cooperative Management / User</li>
<li>Platform Structure = Coop IT Easy</li>
</ul>
</div>
<div class="section" id="known-issues-roadmap">
<h1><a class="toc-backref" href="#id2">Known issues / Roadmap</a></h1>
<p>The API should generate and use an external id for records instead
of odoo’s generated id. It would make importing and export data as
well as migrating across versions easier.</p>
<p>One way would be to use uuid but the default BaseRESTController
routes would need to be rewritten: they only take integer as ids.
Another way is to define a sequence per model.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/coopiteasy/vertical-cooperative/issues">GitHub Issues</a>.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/coopiteasy/vertical-cooperative/issues/new?body=module:%20easy_my_coop_api%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Coop IT Easy SCRLfs</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li>Coop IT Easy SCRLfs</li>
<li>Robin Keunen &lt;<a class="reference external" href="mailto:robin&#64;coopiteasy.be">robin&#64;coopiteasy.be</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
<p>This module is part of the <a class="reference external" href="https://github.com/coopiteasy/vertical-cooperative/tree/12.0/easy_my_coop_api">coopiteasy/vertical-cooperative</a> project on GitHub.</p>
<p>You are welcome to contribute.</p>
</div>
</div>
</div>
</body>
</html>

3
easy_my_coop_api/tests/__init__.py

@ -0,0 +1,3 @@
from . import test_ping
from . import test_registry
from . import test_subscription_requests

106
easy_my_coop_api/tests/common.py

@ -0,0 +1,106 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import json
import requests
from lxml import html
import odoo
from odoo.fields import Date
from odoo.addons.base_rest.tests.common import BaseRestCase
HOST = "127.0.0.1"
PORT = odoo.tools.config["http_port"]
def _add_api_key(headers):
key_dict = {"API-KEY": "api-key"}
if headers:
headers.update(key_dict)
else:
headers = key_dict
return headers
class BaseEMCRestCase(BaseRestCase):
@classmethod
def setUpClass(cls, *args, **kwargs):
super().setUpClass(*args, **kwargs)
cls.AuthApiKey = cls.env["auth.api.key"]
emc_manager = cls.env.ref("easy_my_coop.res_users_manager_emc_demo")
cls.api_key_test = cls.AuthApiKey.create(
{"name": "test-key", "key": "api-key", "user_id": emc_manager.id}
)
def setUp(self):
super().setUp()
self.session = requests.Session()
self.demo_request_1 = self.browse_ref(
"easy_my_coop.subscription_request_1_demo"
)
self.demo_share_product = self.demo_request_1.share_product_id
date = Date.to_string(self.demo_request_1.date)
self.demo_request_1_dict = {
"id": self.demo_request_1.id,
"name": "Manuel Dublues",
"email": "manuel@demo.net",
"date": date,
"state": "draft",
"ordered_parts": 3,
"share_product": {
"id": self.demo_share_product.id,
"name": self.demo_share_product.name,
},
"address": {
"street": "schaerbeekstraat",
"zip_code": "1111",
"city": "Brussels",
"country": "BE",
},
"lang": "en_US",
}
def http_get(self, url, headers=None):
headers = _add_api_key(headers)
if url.startswith("/"):
url = "http://{}:{}{}".format(HOST, PORT, url)
return self.session.get(url, headers=headers)
def http_get_content(self, route, headers=None):
response = self.http_get(route, headers=headers)
self.assertEquals(response.status_code, 200)
content = response.content.decode("utf-8")
return json.loads(content)
def http_post(self, url, data, headers=None):
headers = _add_api_key(headers)
if url.startswith("/"):
url = "http://{}:{}{}".format(HOST, PORT, url)
return self.session.post(url, json=data, headers=headers)
@staticmethod
def html_doc(response):
"""Get an HTML LXML document."""
return html.fromstring(response.content)
def login(self, login, password):
url = "/web/login"
response = self.http_get(url)
self.assertEquals(response.status_code, 200)
doc = self.html_doc(response)
token = doc.xpath("//input[@name='csrf_token']")[0].get("value")
response = self.http_post(
url=url,
data={"login": login, "password": password, "csrf_token": token},
)
self.assertEquals(response.status_code, 200)
return response

42
easy_my_coop_api/tests/test_ping.py

@ -0,0 +1,42 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import json
import requests
from odoo.addons.base_rest.controllers.main import _PseudoCollection
from odoo.addons.component.core import WorkContext
from .common import HOST, PORT, BaseEMCRestCase
class TestPing(BaseEMCRestCase):
def test_public_service(self):
collection = _PseudoCollection("emc.services", self.env)
emc_services_env = WorkContext(
model_name="rest.service.registration", collection=collection
)
service = emc_services_env.component(usage="ping")
result = service.test()
self.assertTrue("message" in result)
def test_ping_route(self):
# public route
path = "/api/ping/test"
url = "http://{}:{}{}".format(HOST, PORT, path)
response = requests.get(url)
self.assertEquals(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
self.assertTrue("message" in content)
def test_search_route(self):
response = self.http_get("/api/ping")
self.assertEquals(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
self.assertTrue("message" in content)

28
easy_my_coop_api/tests/test_registry.py

@ -0,0 +1,28 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import odoo
from odoo.http import controllers_per_module
from odoo.addons.base_rest.tests.common import BaseRestCase
from ..controllers.controllers import UserController
HOST = "127.0.0.1"
PORT = odoo.tools.config["http_port"]
class TestControllerRegistry(BaseRestCase):
def test_controller_registry(self):
controllers = controllers_per_module["easy_my_coop_api"]
self.assertEqual(len(controllers), 1)
self.assertIn(
(
"odoo.addons.easy_my_coop_api"
".controllers.controllers.UserController",
UserController,
),
controllers,
)

143
easy_my_coop_api/tests/test_subscription_requests.py

@ -0,0 +1,143 @@
# Copyright 2020 Coop IT Easy SCRL fs
# Robin Keunen <robin@coopiteasy.be>
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import json
from datetime import timedelta
import odoo
from odoo.fields import Date
from odoo.addons.base_rest.controllers.main import _PseudoCollection
from odoo.addons.component.core import WorkContext
from .common import BaseEMCRestCase
class TestSRController(BaseEMCRestCase):
def setUp(self):
super().setUp()
collection = _PseudoCollection("emc.services", self.env)
emc_services_env = WorkContext(
model_name="rest.service.registration", collection=collection
)
self.service = emc_services_env.component(usage="subscription-request")
def test_service(self):
# kept as example
# useful if you need to change data in database and check db type
result = self.service.get(self.demo_request_1.id)
self.assertEquals(self.demo_request_1_dict, result)
all_sr = self.service.search()
self.assertTrue(all_sr)
sr_date = self.demo_request_1.date
date_from = Date.to_string(sr_date - timedelta(days=1))
date_to = Date.to_string(sr_date + timedelta(days=1))
date_sr = self.service.search(date_from=date_from, date_to=date_to)
self.assertTrue(date_sr)
def test_route_get(self):
id_ = self.demo_request_1.id
route = "/api/subscription-request/%s" % id_
content = self.http_get_content(route)
self.assertEquals(self.demo_request_1_dict, content)
@odoo.tools.mute_logger("odoo.addons.base_rest.http")
def test_route_get_returns_not_found(self):
route = "/api/subscription-request/%s" % "99999"
response = self.http_get(route)
self.assertEquals(response.status_code, 404)
def test_route_get_string_returns_method_not_allowed(self):
route = "/api/subscription-request/%s" % "abc"
response = self.http_get(route)
self.assertEquals(response.status_code, 405)
def test_route_search_all(self):
route = "/api/subscription-request"
content = self.http_get_content(route)
self.assertIn(self.demo_request_1_dict, content["rows"])
def test_route_search_by_date(self):
sr_date = self.demo_request_1.date
date_from = Date.to_string(sr_date - timedelta(days=1))
date_to = Date.to_string(sr_date + timedelta(days=1))
route = "/api/subscription-request?date_from=%s" % date_from
content = self.http_get_content(route)
self.assertIn(self.demo_request_1_dict, content["rows"])
route = "/api/subscription-request?date_to=%s" % date_to
content = self.http_get_content(route)
self.assertIn(self.demo_request_1_dict, content["rows"])
route = "/api/subscription-request?date_from={}&date_to={}".format(
date_from, date_to
)
content = self.http_get_content(route)
self.assertIn(self.demo_request_1_dict, content["rows"])
route = "/api/subscription-request?date_from=%s" % "2300-01-01"
content = self.http_get_content(route)
self.assertEquals(content["count"], 0)
route = "/api/subscription-request?date_to=%s" % "1900-01-01"
content = self.http_get_content(route)
self.assertEquals(content["count"], 0)
@odoo.tools.mute_logger("odoo.addons.base_rest.http")
def test_route_search_acd_date_returns_bad_request(self):
route = "/api/subscription-request?date_from=%s" % "20200101"
response = self.http_get(route)
self.assertEquals(response.status_code, 400)
def test_route_create(self):
url = "/api/subscription-request"
data = {
"name": "Lisa des Danses",
"email": "lisa@desdanses.be",
"ordered_parts": 3,
"share_product": self.demo_share_product.id,
"address": {
"street": "schaerbeekstraat",
"zip_code": "1111",
"city": "Brussels",
"country": "BE",
},
"lang": "en_US",
}
response = self.http_post(url, data=data)
self.assertEquals(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
content.pop("id") # can't know id in advance
expected = {
**data,
**{
"date": Date.to_string(Date.today()),
"state": "draft",
"share_product": {
"id": self.demo_share_product.id,
"name": self.demo_share_product.name,
},
},
}
self.assertEquals(expected, content)
def test_route_update(self):
url = "/api/subscription-request/%s" % self.demo_request_1.id
data = {"state": "done"}
response = self.http_post(url, data=data)
self.assertEquals(response.status_code, 200)
content = json.loads(response.content.decode("utf-8"))
expected = self.demo_request_1_dict
expected["state"] = "done"
self.assertEquals(expected, content)

4
oca_dependencies.txt

@ -1,5 +1,7 @@
# List the OCA project dependencies, one per line
# Add a repository url and branch if you need a forked version
partner-contact
addons https://github.com/coopiteasy/addons
partner-contact
rest-framework
server-auth
Loading…
Cancel
Save