{"action":"created","comment":{"url":"https://api.github.com/repos/odoo/odoo/pulls/comments/23233127","id":23233127,"diff_hunk":"@@ -0,0 +1,428 @@\n+# -*- coding: utf-8 -*-\n+import base64\n+from datetime import datetime, timedelta\n+import logging\n+import requests\n+import simplejson\n+import urllib2\n+from urlparse import urlparse, urlunparse\n+import werkzeug.urls\n+\n+from openerp import models, fields, api, _\n+from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT\n+from openerp.exceptions import Warning, AccessError\n+\n+BASE_API_URL = \"https://api.linkedin.com/v1\"\n+COMPANY_FIELDS = [\"id\", \"name\", \"logo-url\", \"description\", \"industry\", \"website-url\", \"locations\", \"universal-name\"]\n+PEOPLE_FIELDS = [\"id\", \"picture-url\", \"public-profile-url\", \"first-name\", \"last-name\", \"formatted-name\", \"location\", \"phone-numbers\", \"im-accounts\", \"main-address\", \"headline\", \"positions\", \"summary\", \"specialties\", \"email-address\"]\n+_logger = logging.getLogger(__name__)\n+\n+\n+class except_auth(Exception):\n+    \"\"\" Class for authorization exceptions \"\"\"\n+    def __init__(self, name, value, status=None):\n+        Exception.__init__(self)\n+        self.name = name\n+        self.code = 401  # HTTP status code for authorization\n+        if status is not None:\n+            self.code = status\n+        self.args = (self.code, name, value)\n+\n+\n+class linkedin_users(models.Model):\n+    _inherit = 'res.users'\n+\n+    linkedin_token = fields.Char(string=\"LinkedIn Token\")\n+    linkedin_token_validity = fields.Datetime(string=\"LinkedIn Token Validity\")\n+\n+\n+class web_linkedin_settings(models.TransientModel):\n+    _inherit = 'sale.config.settings'\n+\n+    api_key = fields.Char(string=\"API Key\", help=\"LinkedIn API Key\")\n+    secret_key = fields.Char(string=\"Secret Key\", help=\"LinkedIn Secret Key\")\n+    linkedin_domain = fields.Char(string=\"Linkedin Domain\")\n+    redirect_url = fields.Char(related='linkedin_domain', string=\"Linkedin Domain\")\n+    linkedin_cus_sync = fields.Boolean(\"Show tutorial to know how to get my 'API key' and my 'Secret key'\")\n+    company_name = fields.Char(\"Company\", default=lambda self: self.env.user.company_id.name)\n+\n+    @api.model\n+    def get_default_linkedin(self, fields):\n+        key = self.env['ir.config_parameter'].get_param(\"web.linkedin.apikey\") or \"\"\n+        dom = self.env['ir.config_parameter'].get_param('web.base.url')\n+        secret_key = self.env['ir.config_parameter'].get_param('web.linkedin.secretkey')\n+        return {'api_key': key, 'linkedin_domain': dom + \"/linkedin/authentication\", 'secret_key': secret_key}\n+\n+    @api.one\n+    def set_linkedin(self):\n+        config_obj = self.env['ir.config_parameter']\n+        apikey = self.api_key or \"\"\n+        secret_key = self.secret_key or \"\"\n+        config_obj.set_param(\"web.linkedin.apikey\", apikey, groups=['base.group_users'])\n+        config_obj.set_param(\"web.linkedin.secretkey\", secret_key, groups=['base.group_users'])\n+\n+    @api.model\n+    def create(self, vals):\n+        if vals.get('api_key'):\n+            self.env['linkedin'].check_valid_api_key(vals.get('api_key'))\n+        return super(web_linkedin_settings, self).create(vals)\n+\n+    @api.multi\n+    def write(self, vals):\n+        if vals.get('api_key'):\n+            self.env['linkedin'].check_valid_api_key(vals.get('api_key'))\n+        return  super(web_linkedin_settings, self).write(vals)\n+\n+\n+class web_linkedin_fields(models.Model):\n+    _inherit = 'res.partner'\n+\n+    @api.model\n+    def linkedin_check_similar_partner(self, linkedin_datas):\n+        res = []\n+        for linkedin_data in linkedin_datas:\n+            partners = self.env['res.partner'].search_read([\"|\", (\"linkedin_id\", \"=\", linkedin_data['id']),\n+                    \"&\", (\"linkedin_id\", \"=\", False),\n+                    \"|\", (\"name\", \"ilike\", linkedin_data['firstName'] + \"%\" + linkedin_data['lastName']), (\"name\", \"ilike\", linkedin_data['lastName'] + \"%\" + linkedin_data['firstName'])], [\"image\", \"mobile\", \"phone\", \"parent_id\", \"name\", \"email\", \"function\", \"linkedin_id\"])\n+            if partners:\n+                partner = partners[0]\n+                if partner['linkedin_id'] and partner['linkedin_id'] != linkedin_data['id']:\n+                    partner.pop('id')\n+                if partner['parent_id']:\n+                    partner['parent_id'] = partner['parent_id'][0]\n+                for key, val in partner.items():\n+                    if not val:\n+                        partner.pop(key)\n+                res.append(partner)\n+            else:\n+                res.append({})\n+        return res\n+\n+    linkedin_id = fields.Char(string=\"LinkedIn ID\")\n+    linkedin_url = fields.Char(string=\"LinkedIn url\")\n+\n+\n+class linkedin(models.AbstractModel):\n+    _name = 'linkedin'\n+\n+    def check_valid_api_key(self, api_key):\n+        status = \"\"\n+        params = {\n+            'response_type': 'code',\n+            'client_id': api_key,\n+            'state': True,\n+            'redirect_uri': self.get_param_parameter('web.base.url') + '/linkedin/authentication',\n+        }\n+\n+        url = self.get_uri_oauth(a='authorization') + \"?%s\" % werkzeug.url_encode(params)\n+        try:\n+            response = requests.post(url)\n+            response.raise_for_status()\n+            status = response.status_code\n+        except requests.exceptions.ConnectionError:\n+            _logger.exception(\"Either there is no connection or remote server is down !\")\n+        except Exception as e:\n+            if status != 200:\n+                raise Warning(_('Something went wrong!, Please check your API Key.'))\n+        return True\n+\n+    def sync_linkedin_contacts(self, from_url):\n+        \"\"\"\n+            This method will import all first level contact from LinkedIn,\n+            It may raise AccessError, because user may or may not have create or write access,\n+            Here if user does not have one of the right from create or write then this method will allow at least for allowed operation,\n+            AccessError is handled as a special case, AccessError wil not raise exception instead it will return result with warning and status=AccessError.\n+        \"\"\"\n+        if not self.need_authorization():\n+            headers = {'Content-type': 'application/json', 'Accept': 'text/plain', 'x-li-format': 'json'}\n+            params = {\n+                'oauth2_access_token': self.env.user.linkedin_token\n+            }\n+            connection_uri = \"/people/~/connections:{people_fields}\".format(people_fields='(' + ','.join(PEOPLE_FIELDS) + ')')\n+            status, res = self.send_request(connection_uri, params=params, headers=headers, type=\"GET\")\n+            return self.update_contacts(res) if not isinstance(res, str) else False\n+        return {'status': 'need_auth', 'url': self._get_authorize_uri(from_url=from_url, scope=True)}\n+\n+    def update_contacts(self, records):\n+        li_records = dict((d['id'], d) for d in records.get('values', []))\n+        result = {'_total': len(records.get('values', [])), 'fail_warnings': [], 'status': ''}\n+        records_to_create, records_to_update = self.check_create_or_update(li_records)\n+        #Do not raise exception for AccessError, if user doesn't have one of the right from create or write\n+        try:\n+            self.create_contacts(records_to_create)\n+        except AccessError, e:\n+            result['fail_warnings'].append((e[0], str(len(records_to_create)) + \" records are not created\\n\" + e[1]))\n+            result['status'] = \"AccessError\"\n+        try:\n+            self.write_contacts(records_to_update)\n+        except AccessError, e:\n+            result['fail_warnings'].append((e[0], str(len(records_to_update)) + \" records are not updated\\n\" + e[1]))\n+            result['status'] = \"AccessError\"\n+        return result\n+\n+    def check_create_or_update(self, records):\n+        records_to_update = {}\n+        records_to_create = []\n+        ids = records.keys()\n+        read_res = self.env['res.partner'].search_read([('linkedin_id', 'in', ids)], ['linkedin_id'])\n+        to_update = [x['linkedin_id'] for x in read_res]\n+        to_create = list(set(ids).difference(to_update))\n+        for id in to_create:\n+            records_to_create.append(records.get(id))\n+        for res in read_res:\n+            records_to_update[res['id']] = records.get(res['linkedin_id'])\n+        return records_to_create, records_to_update\n+\n+    def create_contacts(self, records_to_create):\n+        for record in records_to_create:\n+            if record['id'] != 'private':\n+                vals = self.create_data_dict(record)\n+                self.env['res.partner'].create(vals)\n+        return True\n+\n+    #Currently all fields are re-written\n+    def write_contacts(self, records_to_update):\n+        for id, record in records_to_update.iteritems():\n+            vals = self.create_data_dict(record)\n+            partner = self.env['res.partner'].search([('id', '=', id)])\n+            partner.write(vals)\n+        return True\n+\n+    def create_data_dict(self, record):\n+        data_dict = {\n+            'name': record.get('formattedName', record.get(\"firstName\", \"\")),\n+            'linkedin_url': record.get('publicProfileUrl', False),\n+            'linkedin_id': record.get('id', False),\n+        }\n+        #TODO: Should we add: email-address,summary\n+        positions = (record.get('positions') or {}).get('values', [])\n+        for position in positions:\n+            if position.get('isCurrent'):\n+                data_dict['function'] = position.get('title')\n+                company_name = False\n+                if position.get('company'):\n+                    company_name = position['company'].get('name')\n+                #To avoid recursion, it is quite possible that connection name and company_name is same\n+                #in such cases import goes fail meanwhile due to osv exception, hence skipped such connection for parent_id\n+                if company_name != data_dict['name']:\n+                    parent = self.env['res.partner'].search([('name', '=', company_name)], limit=1)\n+                    if parent:\n+                        data_dict['parent_id'] = parent.id\n+\n+        image = record.get('pictureUrl') and self.url2binary(record['pictureUrl']) or False\n+        data_dict['image'] = image\n+\n+        phone_numbers = (record.get('phoneNumbers') or {}).get('values', [])\n+        for phone in phone_numbers:\n+            if phone.get('phoneType') == 'mobile':\n+                data_dict['mobile'] = phone['phoneNumber']\n+            else:\n+                data_dict['phone'] = phone['phoneNumber']\n+        return data_dict\n+\n+    def get_search_popup_data(self, offset=0, limit=5, **kw):\n+        \"\"\"\n+            This method will return all needed data for LinkedIn Search Popup.\n+            It returns companies(including search by universal name), people, current user data and it may return warnings if any\n+        \"\"\"\n+        result_data = {'warnings': []}\n+        companies = {}\n+        people = {}\n+        headers = {'Content-type': 'application/json', 'Accept': 'text/plain', 'x-li-format': 'json'}\n+        params = {\n+            'oauth2_access_token': self.env.user.linkedin_token\n+        }\n+\n+        #Profile Information of current user\n+        profile_uri = \"/people/~:(first-name,last-name)\"\n+        status, res = self.with_context(kw.get('local_context') or {}).send_request(profile_uri, params=params, headers=headers, type=\"GET\")\n+        result_data['current_profile'] = res\n+\n+        status, companies, warnings = self.get_company_data(offset, limit, params=params, headers=headers, **kw)\n+        result_data['companies'] = companies\n+        result_data['warnings'] += warnings\n+        status, people, warnings = self.get_people_data(offset, limit, params=params, headers=headers, **kw)\n+        if status:\n+            result_data['people_status'] = status\n+        result_data['people'] = people\n+        result_data['warnings'] += warnings\n+        return result_data\n+\n+    @api.model\n+    def get_company_data(self, offset=0, limit=5, params={}, headers={}, **kw):\n+        \"\"\"This method will fetch company data for LinkedIn Company Search Popup\n+            :param offset: no of company load after show more\n+            :param limit: No of companies to load\n+            :param params: send request Param data like Access token etc\n+            :param headers: headers of requests\n+            :param kw: arguments to controller to search\n+            :returns: linkedin company search data, status if error and warnings\n+        \"\"\"\n+        companies = {}\n+        universal_company = {}\n+        warnings = []\n+        if not params:\n+            params = {'oauth2_access_token': self.env.user.linkedin_token}\n+        if not headers:\n+            headers = {'Content-type': 'application/json', 'Accept': 'text/plain', 'x-li-format': 'json'}\n+        #search by universal-name\n+        if kw.get('search_uid'):\n+            universal_search_uri = \"/companies/universal-name={company_name}:{company_fields}\".format(company_name=kw['search_uid'], company_fields='(' + ','.join(COMPANY_FIELDS) + ')')\n+            status, universal_company = self.with_context(kw.get('local_context') or {}).send_request(universal_search_uri, params=params, headers=headers, type=\"GET\")\n+        #Companies search\n+        search_params = dict(params.copy(), keywords=kw.get('search_term', \"\") or \"\", start=offset, count=limit)\n+        company_search_uri = \"/company-search:(companies:{company_fields})\".format(company_fields='(' + ','.join(COMPANY_FIELDS) + ')')\n+        status, companies = self.with_context(kw.get('local_context') or {}).send_request(company_search_uri, params=search_params, headers=headers, type=\"GET\")\n+        if companies and companies['companies'].get('values') and universal_company:\n+            companies['companies']['values'].append(universal_company)\n+        return status, companies, warnings\n+\n+    @api.model\n+    def get_people_data(self, offset=0, limit=5, params={}, headers={}, **kw):\n+        \"\"\"This method will fetch people data for LinkedIn people Search Popup\n+            :param offset: no of people load after show more\n+            :param limit: No of people to load\n+            :param params: send request Param data like Access token etc\n+            :param headers: headers of requests\n+            :param kw: arguments to controller to search\n+            :returns: linkedin people search data, status if error and warnings\n+        \"\"\"\n+        people = {}\n+        public_profile = {}\n+        warnings = []\n+        if not params:\n+            params = {'oauth2_access_token': self.env.user.linkedin_token}\n+        if not headers:\n+            headers = {'Content-type': 'application/json', 'Accept': 'text/plain', 'x-li-format': 'json'}\n+        if kw.get('search_uid'):\n+            #this code may returns 400 bad request error, as per linked API doc the call is proper\n+            #but generated url may not have proper public url and may raise 400 or 410 status hence added a warning in response and handle warning at client side\n+            try:\n+                public_profile_url = werkzeug.url_quote_plus(\"http://www.linkedin.com/pub/%s\" % (kw['search_uid']))\n+                profile_uri = \"/people/url={public_profile_url}:{people_fields}\".format(public_profile_url=public_profile_url, people_fields='(' + ','.join(PEOPLE_FIELDS) + ')')\n+                status, public_profile = self.with_context(kw.get('local_context') or {}).send_request(profile_uri, params=params, headers=headers, type=\"GET\")\n+\n+            except urllib2.HTTPError, e:\n+                if e.code in (400, 410):\n+                    warnings.append([_('LinkedIn error'), _('LinkedIn is temporary down for the searches by url.')])\n+                elif e.code in (401):\n+                    raise e\n+        search_params = dict(params.copy(), keywords=kw.get('search_term', \"\") or \"\", start=offset, count=limit)\n+        #Note: People search is allowed to only vetted API access request, please go through following link\n+        #https://help.linkedin.com/app/api-dvr\n+        people_search_uri = \"/people-search:(people:{people_fields})\".format(people_fields='(' + ','.join(PEOPLE_FIELDS) + ')')\n+        status, people = self.with_context(kw.get('local_context') or {}).send_request(people_search_uri, params=search_params, headers=headers, type=\"GET\")\n+        if people and people['people'].get('values') and public_profile:\n+            people['people']['values'].append(public_profile)\n+        return status, people, warnings\n+\n+    @api.model\n+    def get_people_from_company(self, company_universalname, limit, from_url):\n+        if not self.need_authorization():\n+            headers = {'Content-type': 'application/json', 'Accept': 'text/plain', 'x-li-format': 'json'}\n+            params = {\n+                'oauth2_access_token': self.env.user.linkedin_token,\n+                'company-name': company_universalname,\n+                'current-company': 'true',\n+                'count': limit\n+            }\n+            people_criteria_uri = \"/people-search:(people:{people_fields})\".format(people_fields='(' + ','.join(PEOPLE_FIELDS) + ')')\n+            status, res = self.send_request(people_criteria_uri, params=params, headers=headers, type=\"GET\")\n+            return res\n+        else:\n+            return {'status': 'need_auth', 'url': self._get_authorize_uri(from_url=from_url)}\n+\n+    def send_request(self, uri, pre_uri=BASE_API_URL, params={}, headers={}, type=\"GET\"):\n+        result = \"\"\n+        status = \"\"\n+        try:\n+            if type.upper() == \"GET\":\n+                data = werkzeug.url_encode(params)\n+                req = urllib2.Request(pre_uri + uri + \"?\" + data)\n+                for header_key, header_val in headers.iteritems():\n+                    req.add_header(header_key, header_val)\n+            elif type.upper() == 'POST':\n+                req = urllib2.Request(pre_uri + uri, params, headers)\n+            else:\n+                raise ('Method not supported [%s] not in [GET, POST]!' % (type))\n+            request = urllib2.urlopen(req)\n+            status = request.getcode()\n+            if int(status) in (204, 404):  # Page not found, no response\n+                result = {}\n+            else:\n+                content = request.read()\n+                result = simplejson.loads(content)\n+        except urllib2.HTTPError, e:\n+            #Should simply raise exception or simply add logger\n+            if e.code in (400, 410):\n+                raise e\n+\n+            if e.code == 401:\n+                raise except_auth('AuthorizationError', {'url': self._get_authorize_uri(from_url=self.env.context.get('from_url'), scope=self.env.context.get('scope'))})\n+            #TODO: Should handle 403 for throttle limit and should display user freindly message\n+            status = e.code\n+            _logger.exception(\"Bad linkedin request : %s !\" % e.read())\n+        except urllib2.URLError, e:\n+            _logger.exception(\"Either there is no connection or remote server is down !\")\n+        return (status, result)\n+\n+    def _get_authorize_uri(self, from_url, scope=False):\n+        \"\"\" This method return the url needed to allow this instance of OpenErp to access linkedin application \"\"\"\n+        state_obj = dict(d=scope, f=from_url)\n+        base_url = self.get_param_parameter('web.base.url')\n+        client_id = self.get_param_parameter('web.linkedin.apikey')\n+\n+        params = {\n+            'response_type': 'code',\n+            'client_id': client_id,\n+            'state': simplejson.dumps(state_obj),\n+            'redirect_uri': base_url + '/linkedin/authentication',\n+        }\n+\n+        uri = self.get_uri_oauth(a='authorization') + \"?%s\" % werkzeug.url_encode(params)\n+        return uri\n+\n+    def set_all_tokens(self, token_datas):\n+        data = {\n+            'linkedin_token': token_datas.get('access_token'),\n+            'linkedin_token_validity': datetime.now() + timedelta(seconds=token_datas.get('expires_in'))\n+        }\n+        self.env.user.sudo().write(data)\n+\n+    def need_authorization(self):\n+        if not self.env.user.linkedin_token_validity or \\\n+                datetime.strptime(self.env.user.linkedin_token_validity.split('.')[0], DEFAULT_SERVER_DATETIME_FORMAT) < (datetime.now() + timedelta(minutes=1)):\n+            return True\n+        return False\n+\n+    def get_param_parameter(self, parameter):\n+        return self.env['ir.config_parameter'].sudo().get_param(parameter)","path":"addons/web_linkedin/models/web_linkedin.py","position":399,"original_position":399,"commit_id":"f7683a0f77930080fd5b8005d51c24c436390e78","original_commit_id":"f7683a0f77930080fd5b8005d51c24c436390e78","user":{"login":"mart-e","id":564822,"avatar_url":"https://avatars.githubusercontent.com/u/564822?v=3","gravatar_id":"","url":"https://api.github.com/users/mart-e","html_url":"https://github.com/mart-e","followers_url":"https://api.github.com/users/mart-e/followers","following_url":"https://api.github.com/users/mart-e/following{/other_user}","gists_url":"https://api.github.com/users/mart-e/gists{/gist_id}","starred_url":"https://api.github.com/users/mart-e/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mart-e/subscriptions","organizations_url":"https://api.github.com/users/mart-e/orgs","repos_url":"https://api.github.com/users/mart-e/repos","events_url":"https://api.github.com/users/mart-e/events{/privacy}","received_events_url":"https://api.github.com/users/mart-e/received_events","type":"User","site_admin":false},"body":"Oh please don't do that. You just created a public method that allow to access any config parameter. :cry: ","created_at":"2015-01-20T16:18:14Z","updated_at":"2015-01-20T16:18:14Z","html_url":"https://github.com/odoo/odoo/pull/3184#discussion_r23233127","pull_request_url":"https://api.github.com/repos/odoo/odoo/pulls/3184","_links":{"self":{"href":"https://api.github.com/repos/odoo/odoo/pulls/comments/23233127"},"html":{"href":"https://github.com/odoo/odoo/pull/3184#discussion_r23233127"},"pull_request":{"href":"https://api.github.com/repos/odoo/odoo/pulls/3184"}}},"pull_request":{"url":"https://api.github.com/repos/odoo/odoo/pulls/3184","id":23003825,"html_url":"https://github.com/odoo/odoo/pull/3184","diff_url":"https://github.com/odoo/odoo/pull/3184.diff","patch_url":"https://github.com/odoo/odoo/pull/3184.patch","issue_url":"https://api.github.com/repos/odoo/odoo/issues/3184","number":3184,"state":"open","locked":false,"title":"Master linkedin restapi msh","user":{"login":"tde-banana-odoo","id":7501424,"avatar_url":"https://avatars.githubusercontent.com/u/7501424?v=3","gravatar_id":"","url":"https://api.github.com/users/tde-banana-odoo","html_url":"https://github.com/tde-banana-odoo","followers_url":"https://api.github.com/users/tde-banana-odoo/followers","following_url":"https://api.github.com/users/tde-banana-odoo/following{/other_user}","gists_url":"https://api.github.com/users/tde-banana-odoo/gists{/gist_id}","starred_url":"https://api.github.com/users/tde-banana-odoo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/tde-banana-odoo/subscriptions","organizations_url":"https://api.github.com/users/tde-banana-odoo/orgs","repos_url":"https://api.github.com/users/tde-banana-odoo/repos","events_url":"https://api.github.com/users/tde-banana-odoo/events{/privacy}","received_events_url":"https://api.github.com/users/tde-banana-odoo/received_events","type":"User","site_admin":false},"body":"- Task: https://www.odoo.com/web?#id=8830&view_type=form&model=project.task&menu_id=3942&action=327\r\n- Pad: https://pad.odoo.com/p/r.esmRR7w6RxXjBBlx","created_at":"2014-10-20T10:03:16Z","updated_at":"2015-01-20T16:18:14Z","closed_at":null,"merged_at":null,"merge_commit_sha":"60863ae6d513dd6fa1607f1b9d30627d26cda6d2","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/odoo/odoo/pulls/3184/commits","review_comments_url":"https://api.github.com/repos/odoo/odoo/pulls/3184/comments","review_comment_url":"https://api.github.com/repos/odoo/odoo/pulls/comments/{number}","comments_url":"https://api.github.com/repos/odoo/odoo/issues/3184/comments","statuses_url":"https://api.github.com/repos/odoo/odoo/statuses/f7683a0f77930080fd5b8005d51c24c436390e78","head":{"label":"odoo-dev:master-linkedin-restapi-msh","ref":"master-linkedin-restapi-msh","sha":"f7683a0f77930080fd5b8005d51c24c436390e78","user":{"login":"odoo-dev","id":7466976,"avatar_url":"https://avatars.githubusercontent.com/u/7466976?v=3","gravatar_id":"","url":"https://api.github.com/users/odoo-dev","html_url":"https://github.com/odoo-dev","followers_url":"https://api.github.com/users/odoo-dev/followers","following_url":"https://api.github.com/users/odoo-dev/following{/other_user}","gists_url":"https://api.github.com/users/odoo-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/odoo-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odoo-dev/subscriptions","organizations_url":"https://api.github.com/users/odoo-dev/orgs","repos_url":"https://api.github.com/users/odoo-dev/repos","events_url":"https://api.github.com/users/odoo-dev/events{/privacy}","received_events_url":"https://api.github.com/users/odoo-dev/received_events","type":"Organization","site_admin":false},"repo":{"id":19853934,"name":"odoo","full_name":"odoo-dev/odoo","owner":{"login":"odoo-dev","id":7466976,"avatar_url":"https://avatars.githubusercontent.com/u/7466976?v=3","gravatar_id":"","url":"https://api.github.com/users/odoo-dev","html_url":"https://github.com/odoo-dev","followers_url":"https://api.github.com/users/odoo-dev/followers","following_url":"https://api.github.com/users/odoo-dev/following{/other_user}","gists_url":"https://api.github.com/users/odoo-dev/gists{/gist_id}","starred_url":"https://api.github.com/users/odoo-dev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odoo-dev/subscriptions","organizations_url":"https://api.github.com/users/odoo-dev/orgs","repos_url":"https://api.github.com/users/odoo-dev/repos","events_url":"https://api.github.com/users/odoo-dev/events{/privacy}","received_events_url":"https://api.github.com/users/odoo-dev/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/odoo-dev/odoo","description":"RD Dev Repository","fork":true,"url":"https://api.github.com/repos/odoo-dev/odoo","forks_url":"https://api.github.com/repos/odoo-dev/odoo/forks","keys_url":"https://api.github.com/repos/odoo-dev/odoo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/odoo-dev/odoo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/odoo-dev/odoo/teams","hooks_url":"https://api.github.com/repos/odoo-dev/odoo/hooks","issue_events_url":"https://api.github.com/repos/odoo-dev/odoo/issues/events{/number}","events_url":"https://api.github.com/repos/odoo-dev/odoo/events","assignees_url":"https://api.github.com/repos/odoo-dev/odoo/assignees{/user}","branches_url":"https://api.github.com/repos/odoo-dev/odoo/branches{/branch}","tags_url":"https://api.github.com/repos/odoo-dev/odoo/tags","blobs_url":"https://api.github.com/repos/odoo-dev/odoo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/odoo-dev/odoo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/odoo-dev/odoo/git/refs{/sha}","trees_url":"https://api.github.com/repos/odoo-dev/odoo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/odoo-dev/odoo/statuses/{sha}","languages_url":"https://api.github.com/repos/odoo-dev/odoo/languages","stargazers_url":"https://api.github.com/repos/odoo-dev/odoo/stargazers","contributors_url":"https://api.github.com/repos/odoo-dev/odoo/contributors","subscribers_url":"https://api.github.com/repos/odoo-dev/odoo/subscribers","subscription_url":"https://api.github.com/repos/odoo-dev/odoo/subscription","commits_url":"https://api.github.com/repos/odoo-dev/odoo/commits{/sha}","git_commits_url":"https://api.github.com/repos/odoo-dev/odoo/git/commits{/sha}","comments_url":"https://api.github.com/repos/odoo-dev/odoo/comments{/number}","issue_comment_url":"https://api.github.com/repos/odoo-dev/odoo/issues/comments/{number}","contents_url":"https://api.github.com/repos/odoo-dev/odoo/contents/{+path}","compare_url":"https://api.github.com/repos/odoo-dev/odoo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/odoo-dev/odoo/merges","archive_url":"https://api.github.com/repos/odoo-dev/odoo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/odoo-dev/odoo/downloads","issues_url":"https://api.github.com/repos/odoo-dev/odoo/issues{/number}","pulls_url":"https://api.github.com/repos/odoo-dev/odoo/pulls{/number}","milestones_url":"https://api.github.com/repos/odoo-dev/odoo/milestones{/number}","notifications_url":"https://api.github.com/repos/odoo-dev/odoo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/odoo-dev/odoo/labels{/name}","releases_url":"https://api.github.com/repos/odoo-dev/odoo/releases{/id}","created_at":"2014-05-16T11:16:19Z","updated_at":"2015-01-15T06:11:53Z","pushed_at":"2015-01-20T16:13:51Z","git_url":"git://github.com/odoo-dev/odoo.git","ssh_url":"git@github.com:odoo-dev/odoo.git","clone_url":"https://github.com/odoo-dev/odoo.git","svn_url":"https://github.com/odoo-dev/odoo","homepage":"https://www.odoo.com","size":982178,"stargazers_count":38,"watchers_count":38,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":17,"mirror_url":null,"open_issues_count":3,"forks":17,"open_issues":3,"watchers":38,"default_branch":"master"}},"base":{"label":"odoo:master","ref":"master","sha":"ee9e4d36727703c613daa16fc090804bf5016c43","user":{"login":"odoo","id":6368483,"avatar_url":"https://avatars.githubusercontent.com/u/6368483?v=3","gravatar_id":"","url":"https://api.github.com/users/odoo","html_url":"https://github.com/odoo","followers_url":"https://api.github.com/users/odoo/followers","following_url":"https://api.github.com/users/odoo/following{/other_user}","gists_url":"https://api.github.com/users/odoo/gists{/gist_id}","starred_url":"https://api.github.com/users/odoo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odoo/subscriptions","organizations_url":"https://api.github.com/users/odoo/orgs","repos_url":"https://api.github.com/users/odoo/repos","events_url":"https://api.github.com/users/odoo/events{/privacy}","received_events_url":"https://api.github.com/users/odoo/received_events","type":"Organization","site_admin":false},"repo":{"id":19745004,"name":"odoo","full_name":"odoo/odoo","owner":{"login":"odoo","id":6368483,"avatar_url":"https://avatars.githubusercontent.com/u/6368483?v=3","gravatar_id":"","url":"https://api.github.com/users/odoo","html_url":"https://github.com/odoo","followers_url":"https://api.github.com/users/odoo/followers","following_url":"https://api.github.com/users/odoo/following{/other_user}","gists_url":"https://api.github.com/users/odoo/gists{/gist_id}","starred_url":"https://api.github.com/users/odoo/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/odoo/subscriptions","organizations_url":"https://api.github.com/users/odoo/orgs","repos_url":"https://api.github.com/users/odoo/repos","events_url":"https://api.github.com/users/odoo/events{/privacy}","received_events_url":"https://api.github.com/users/odoo/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/odoo/odoo","description":"Odoo (formerly OpenERP). Open Source Apps To Grow Your Business.","fork":false,"url":"https://api.github.com/repos/odoo/odoo","forks_url":"https://api.github.com/repos/odoo/odoo/forks","keys_url":"https://api.github.com/repos/odoo/odoo/keys{/key_id}","collaborators_url":"https://api.github.com/repos/odoo/odoo/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/odoo/odoo/teams","hooks_url":"https://api.github.com/repos/odoo/odoo/hooks","issue_events_url":"https://api.github.com/repos/odoo/odoo/issues/events{/number}","events_url":"https://api.github.com/repos/odoo/odoo/events","assignees_url":"https://api.github.com/repos/odoo/odoo/assignees{/user}","branches_url":"https://api.github.com/repos/odoo/odoo/branches{/branch}","tags_url":"https://api.github.com/repos/odoo/odoo/tags","blobs_url":"https://api.github.com/repos/odoo/odoo/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/odoo/odoo/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/odoo/odoo/git/refs{/sha}","trees_url":"https://api.github.com/repos/odoo/odoo/git/trees{/sha}","statuses_url":"https://api.github.com/repos/odoo/odoo/statuses/{sha}","languages_url":"https://api.github.com/repos/odoo/odoo/languages","stargazers_url":"https://api.github.com/repos/odoo/odoo/stargazers","contributors_url":"https://api.github.com/repos/odoo/odoo/contributors","subscribers_url":"https://api.github.com/repos/odoo/odoo/subscribers","subscription_url":"https://api.github.com/repos/odoo/odoo/subscription","commits_url":"https://api.github.com/repos/odoo/odoo/commits{/sha}","git_commits_url":"https://api.github.com/repos/odoo/odoo/git/commits{/sha}","comments_url":"https://api.github.com/repos/odoo/odoo/comments{/number}","issue_comment_url":"https://api.github.com/repos/odoo/odoo/issues/comments/{number}","contents_url":"https://api.github.com/repos/odoo/odoo/contents/{+path}","compare_url":"https://api.github.com/repos/odoo/odoo/compare/{base}...{head}","merges_url":"https://api.github.com/repos/odoo/odoo/merges","archive_url":"https://api.github.com/repos/odoo/odoo/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/odoo/odoo/downloads","issues_url":"https://api.github.com/repos/odoo/odoo/issues{/number}","pulls_url":"https://api.github.com/repos/odoo/odoo/pulls{/number}","milestones_url":"https://api.github.com/repos/odoo/odoo/milestones{/number}","notifications_url":"https://api.github.com/repos/odoo/odoo/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/odoo/odoo/labels{/name}","releases_url":"https://api.github.com/repos/odoo/odoo/releases{/id}","created_at":"2014-05-13T15:38:58Z","updated_at":"2015-01-20T16:01:11Z","pushed_at":"2015-01-20T16:06:30Z","git_url":"git://github.com/odoo/odoo.git","ssh_url":"git@github.com:odoo/odoo.git","clone_url":"https://github.com/odoo/odoo.git","svn_url":"https://github.com/odoo/odoo","homepage":"https://www.odoo.com","size":1022928,"stargazers_count":1845,"watchers_count":1845,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":1505,"mirror_url":null,"open_issues_count":1631,"forks":1505,"open_issues":1631,"watchers":1845,"default_branch":"8.0"}},"_links":{"self":{"href":"https://api.github.com/repos/odoo/odoo/pulls/3184"},"html":{"href":"https://github.com/odoo/odoo/pull/3184"},"issue":{"href":"https://api.github.com/repos/odoo/odoo/issues/3184"},"comments":{"href":"https://api.github.com/repos/odoo/odoo/issues/3184/comments"},"review_comments":{"href":"https://api.github.com/repos/odoo/odoo/pulls/3184/comments"},"review_comment":{"href":"https://api.github.com/repos/odoo/odoo/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/odoo/odoo/pulls/3184/commits"},"statuses":{"href":"https://api.github.com/repos/odoo/odoo/statuses/f7683a0f77930080fd5b8005d51c24c436390e78"}}}}