diff --git a/addons/web/controllers/vcard.py b/addons/web/controllers/vcard.py index d0b2d4f6986..5bd42e1a91c 100644 --- a/addons/web/controllers/vcard.py +++ b/addons/web/controllers/vcard.py @@ -1,20 +1,48 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import importlib.util +import io +import zipfile + import odoo.http as http +from odoo.exceptions import UserError from odoo.http import request, content_disposition class Partner(http.Controller): - @http.route('/web/partner//vcard', type='http', auth="user") - def download_vcard(self, partner, **kwargs): - content = partner._get_vcard_file() - if not content: - return request.not_found() - return request.make_response(content, [ - ('Content-Type', 'text/vcard'), - ('Content-Length', len(content)), - ('Content-Disposition', content_disposition('%s.vcf' % partner.name)) - ]) + @http.route(['/web_enterprise/partner//vcard', + '/web/partner/vcard'], type='http', auth="user") + def download_vcard(self, partner_ids=None, partner=None, **kwargs): + if importlib.util.find_spec('vobject') is None: + raise UserError('vobject library is not installed') + + if partner_ids: + partner_ids = list(filter(None, (int(pid) for pid in partner_ids.split(',') if pid.isdigit()))) + partners = request.env['res.partner'].browse(partner_ids) + if len(partners) > 1: + with io.BytesIO() as buffer: + with zipfile.ZipFile(buffer, 'w') as zipf: + for partner in partners: + filename = f"{partner.name or partner.email}.vcf" + content = partner._get_vcard_file() + zipf.writestr(filename, content) + + return request.make_response(buffer.getvalue(), [ + ('Content-Type', 'application/zip'), + ('Content-Length', len(content)), + ('Content-Disposition', content_disposition('Contacts.zip')) + ]) + + if partner or partners: + partner = partner or partners + content = partner._get_vcard_file() + return request.make_response(content, [ + ('Content-Type', 'text/vcard'), + ('Content-Length', len(content)), + ('Content-Disposition', content_disposition(f"{partner.name or partner.email}.vcf")), + ]) + + return request.not_found() diff --git a/addons/web/tests/test_partner.py b/addons/web/tests/test_partner.py index 8263266dd6e..321a2d16808 100644 --- a/addons/web/tests/test_partner.py +++ b/addons/web/tests/test_partner.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import io import logging import unittest +import zipfile +from odoo.fields import Command from odoo.tests.common import HttpCase, tagged from base64 import b64decode +from odoo.tools import mute_logger _logger = logging.getLogger(__name__) try: @@ -24,7 +28,7 @@ class TestPartnerVCard(HttpCase): if not vobject: raise unittest.SkipTest("Skip tests when `vobject` Python module is not found.") - self.partner = self.env['res.partner'].create({ + self.partners = self.env['res.partner'].create([{ 'name': 'John Doe', 'email': 'john.doe@test.example.com', 'mobile': '+1 202 555 0888', @@ -35,25 +39,49 @@ class TestPartnerVCard(HttpCase): 'country_id': self.env.ref('base.us').id, 'zip': '97648', 'website': 'https://test.exemple.com', - }) + }, { + 'name': 'shut', + 'email': 'shut@test.example.com', + 'mobile': '+1 202 555 0999', + 'phone': '+1 202 555 0123', + 'function': 'Developer', + 'street': 'Donutville Maximum-Security Orphanarium', + 'city': 'Washington DC', + 'country_id': self.env.ref('base.us').id, + 'zip': '97649', + 'website': 'https://test.example.com', + }]) self.authenticate("admin", "admin") - def test_fetch_partner_vcard(self): - res = self.url_open('/web/partner/%d/vcard' % self.partner.id) - vcard = vobject.readOne(res.text) - self.assertEqual(vcard.contents["n"][0].value.family, self.partner.name, "Vcard should have the same name") - self.assertEqual(vcard.contents["adr"][0].value.street, self.partner.street, "Vcard should have the same street") - self.assertEqual(vcard.contents["adr"][0].value.city, self.partner.city, "Vcard should have the same city") - self.assertEqual(vcard.contents["adr"][0].value.code, self.partner.zip, "Vcard should have the same zip") + def check_vcard_contents(self, vcard, partner): + self.assertEqual(vcard.contents["n"][0].value.family, partner.name, "Vcard should have the same name") + self.assertEqual(vcard.contents["adr"][0].value.street, partner.street, "Vcard should have the same street") + self.assertEqual(vcard.contents["adr"][0].value.city, partner.city, "Vcard should have the same city") + self.assertEqual(vcard.contents["adr"][0].value.code, partner.zip, "Vcard should have the same zip") self.assertEqual(vcard.contents["adr"][0].value.country, self.env.ref('base.us').name, "Vcard should have the same country") - self.assertEqual(vcard.contents["email"][0].value, self.partner.email, "Vcard should have the same email") - self.assertEqual(vcard.contents["url"][0].value, self.partner.website, "Vcard should have the same website") + self.assertEqual(vcard.contents["email"][0].value, partner.email, "Vcard should have the same email") + self.assertEqual(vcard.contents["url"][0].value, partner.website, "Vcard should have the same website") self.assertEqual(vcard.contents["tel"][0].params['TYPE'], ["work"], "Vcard should have the same phone") - self.assertEqual(vcard.contents["tel"][0].value, self.partner.phone, "Vcard should have the same phone") + self.assertEqual(vcard.contents["tel"][0].value, partner.phone, "Vcard should have the same phone") self.assertEqual(vcard.contents["tel"][1].params['TYPE'], ["cell"], "Vcard should have the same mobile") - self.assertEqual(vcard.contents["tel"][1].value, self.partner.mobile, "Vcard should have the same mobile") - self.assertEqual(vcard.contents["title"][0].value, self.partner.function, "Vcard should have the same function") - self.assertEqual(len(vcard.contents['photo'][0].value), len(b64decode(self.partner.avatar_512)), "Vcard should have the same photo") + self.assertEqual(vcard.contents["tel"][1].value, partner.mobile, "Vcard should have the same mobile") + self.assertEqual(vcard.contents["title"][0].value, partner.function, "Vcard should have the same function") + self.assertEqual(len(vcard.contents['photo'][0].value), len(b64decode(partner.avatar_512)), "Vcard should have the same photo") + + def test_fetch_single_partner_vcard(self): + res = self.url_open('/web_enterprise/partner/%d/vcard' % self.partners[0].id) + vcard = vobject.readOne(res.text) + self.check_vcard_contents(vcard, self.partners[0]) + + def test_fetch_multiple_partners_vcard(self): + res = self.url_open('/web/partner/vcard?partner_ids=%s,%s' + % (self.partners[0].id, self.partners[1].id)) + with io.BytesIO(res.content) as buffer: + with zipfile.ZipFile(buffer, 'r') as zipf: + vcfFileList = zipf.namelist() + for i, vcfFile in enumerate(vcfFileList): + vcardFile = zipf.read(vcfFile).decode() + self.check_vcard_contents(vobject.readOne(vcardFile), self.partners[i]) @unittest.skip def test_not_exist_partner_vcard(self): @@ -61,3 +89,16 @@ class TestPartnerVCard(HttpCase): self.partner.unlink() res = self.url_open('/web/partner/%d/vcard' % partner_id) self.assertEqual(res.status_code, 404) + + def test_check_partner_access_for_user(self): + self.env['res.users'].create({ + 'groups_id': [Command.set([self.env.ref('base.group_public').id])], + 'name': 'Test User', + 'login': 'testuser', + 'password': 'testuser', + }) + self.authenticate('testuser', 'testuser') + with mute_logger('odoo.http'): # mute 403 warning + res = self.url_open('/web/partner/vcard?partner_ids=%s,%s' % + (self.partners[0].id, self.partners[1].id)) + self.assertEqual(res.status_code, 403) diff --git a/addons/web/views/partner_view.xml b/addons/web/views/partner_view.xml index 5c15aca0e87..4d7ac311a9f 100644 --- a/addons/web/views/partner_view.xml +++ b/addons/web/views/partner_view.xml @@ -4,12 +4,12 @@ Download (vCard) - form + form,list code action = { 'type': 'ir.actions.act_url', - 'url': '/web/partner/%d/vcard' % record.id, + 'url': '/web/partner/vcard?partner_ids=' + ','.join(map(str, records.ids)), 'target': 'download', }