[ADD] website_project: Portal access to Projects

Split project to move portal features to a new module named website_project.

Portal users may now access their projects and tasks throught the
website_portal My account page. The Backend portal menuitem is removed.

Simplify the privacy settings of project, to be visible to portal users the
project must be in 'portal' mode.

Original authors: Vipul Bhatt, Florian Wintjens
This commit is contained in:
Antony Lesuisse
2016-07-06 01:17:43 +02:00
parent 05e0bf3f1b
commit ccf606e026
20 changed files with 569 additions and 129 deletions
-1
View File
@@ -13,7 +13,6 @@
'product',
'analytic',
'mail',
'portal',
'resource',
'web_kanban',
'web_planner',
-4
View File
@@ -65,14 +65,10 @@
<field name="date_start" eval="time.strftime('%Y-%m-01 10:00:00')"/>
<field name="name">Website for Sales &amp; WMS</field>
<field name="color">3</field>
<field name="partner_id" ref="portal.partner_demo_portal"/>
<field name="privacy_visibility">portal</field>
<field name="user_id" ref="base.user_demo"/>
<field name="alias_model">project.task</field>
<field name="type_ids" eval="[(4, ref('project_stage_0')), (4, ref('project_stage_1')), (4, ref('project_stage_2')), (4, ref('project_stage_3'))]"/>
</record>
<function model="project.project" name="message_subscribe"
eval="[ref('project_project_1')], [ref('portal.partner_demo_portal')]"/>
<record id="project_project_2" model="project.project">
<field name="name">Research &amp; Development</field>
+11 -16
View File
@@ -104,14 +104,6 @@ class Project(models.Model):
""" Overriden in project_issue to offer more options """
return [('project.task', "Tasks")]
def _get_visibility_selection(self):
""" Overriden in portal_project to offer more options """
return [
('employees', _('Visible by all employees')),
('followers', _('On invitation only')),
('portal', _('Shared with a customer'))
]
@api.multi
def attachment_tree_view(self):
self.ensure_one()
@@ -171,7 +163,6 @@ class Project(models.Model):
# Lambda indirection method to avoid passing a copy of the overridable method when declaring the field
_alias_models = lambda self: self._get_alias_models()
_visibility_selection = lambda self: self._get_visibility_selection()
active = fields.Boolean(default=True,
help="If the active field is set to False, it will allow you to hide the project without removing it.")
@@ -203,15 +194,19 @@ class Project(models.Model):
"with Tasks (or optionally Issues if the Issue Tracker module is installed).")
alias_model = fields.Selection(_alias_models, string="Alias Model", index=True, required=True, default='project.task',
help="The kind of document created when an email is received on this project's email alias")
privacy_visibility = fields.Selection(_visibility_selection, string='Privacy', required=True,
privacy_visibility = fields.Selection([
('followers', _('On invitation only')),
('employees', _('Visible by all employees')),
('portal', _('Visible by following customers')),
],
string='Privacy', required=True,
default='employees',
help="Holds visibility of the tasks or issues that belong to the current project:\n"
"- Portal : employees see everything;\n"
" if portal is activated, portal users see the tasks or issues followed by\n"
" them or by someone of their company\n"
"- Employees Only: employees see all tasks or issues\n"
"- Followers Only: employees see only the followed tasks or issues; if portal\n"
" is activated, portal users see the followed tasks or issues.")
"- On invitation only: Employees may only see the followed project, tasks or issues\n"
"- Visible by all employees: Employees may see all project, tasks or issues\n"
"- Visible by following customers: employees see everything;\n"
" if website is activated, portal users may see project, tasks or issues followed by\n"
" them or by someone of their company\n")
doc_count = fields.Integer(compute='_compute_attached_docs_count', string="Number of documents attached")
date_start = fields.Date(string='Start Date')
date = fields.Date(string='Expiration Date', index=True, track_visibility='onchange')
@@ -3,7 +3,6 @@ access_project_project,project.project,model_project_project,project.group_proje
access_project_project_manager,project.project,model_project_project,project.group_project_manager,1,1,1,1
access_account_analytic_account_user,account.analytic.account,analytic.model_account_analytic_account,project.group_project_user,1,0,0,0
access_account_analytic_account_manager,account.analytic.account,analytic.model_account_analytic_account,project.group_project_manager,1,1,1,1
access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0
access_project_task_type_user,project.task.type.user,model_project_task_type,base.group_user,1,0,0,0
access_project_task_type_project_user,project.task.type.project.user,model_project_task_type,project.group_project_user,1,0,0,0
access_project_task_type_manager,project.task.type manager,model_project_task_type,project.group_project_manager,1,1,1,1
@@ -21,8 +20,4 @@ access_resource_calendar_attendance,project.resource_calendar_attendance user,re
access_resource_calendar_leaves_user,resource.calendar.leaves user,resource.model_resource_calendar_leaves,project.group_project_user,1,1,1,1
access_project_tags_all,project.project_tags_all,model_project_tags,,1,0,0,0
access_project_tags_manager,project.project_tags_manager,model_project_tags,project.group_project_manager,1,1,1,1
access_project_tags_portal,project_tags_portal,project.model_project_tags,base.group_portal,1,0,0,0
access_mail_alias,mail.alias,mail.model_mail_alias,project.group_project_manager,1,1,1,1
access_project_portal,project__portal,project.model_project_project,base.group_portal,1,0,0,0
access_task_portal,task_portal,project.model_project_task,base.group_portal,1,0,0,0
access_task_type_portal,task_type_portal,project.model_project_task_type,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
3 access_project_project_manager project.project model_project_project project.group_project_manager 1 1 1 1
4 access_account_analytic_account_user account.analytic.account analytic.model_account_analytic_account project.group_project_user 1 0 0 0
5 access_account_analytic_account_manager account.analytic.account analytic.model_account_analytic_account project.group_project_manager 1 1 1 1
access_account_analytic_account_portal account_analytic_account analytic.model_account_analytic_account base.group_portal 1 0 0 0
6 access_project_task_type_user project.task.type.user model_project_task_type base.group_user 1 0 0 0
7 access_project_task_type_project_user project.task.type.project.user model_project_task_type project.group_project_user 1 0 0 0
8 access_project_task_type_manager project.task.type manager model_project_task_type project.group_project_manager 1 1 1 1
20 access_resource_calendar_leaves_user resource.calendar.leaves user resource.model_resource_calendar_leaves project.group_project_user 1 1 1 1
21 access_project_tags_all project.project_tags_all model_project_tags 1 0 0 0
22 access_project_tags_manager project.project_tags_manager model_project_tags project.group_project_manager 1 1 1 1
access_project_tags_portal project_tags_portal project.model_project_tags base.group_portal 1 0 0 0
23 access_mail_alias mail.alias mail.model_mail_alias project.group_project_manager 1 1 1 1
access_project_portal project__portal project.model_project_project base.group_portal 1 0 0 0
access_task_portal task_portal project.model_project_task base.group_portal 1 0 0 0
access_task_type_portal task_type_portal project.model_project_task_type base.group_portal 1 0 0 0
+5 -43
View File
@@ -43,10 +43,10 @@
</record>
<record model="ir.rule" id="project_public_members_rule">
<field name="name">Project: employees: portal, employees or followers</field>
<field name="name">Project: employees: following required for follower-only projects</field>
<field name="model_id" ref="model_project_project"/>
<field name="domain_force">['|',
('privacy_visibility', 'in', ['portal', 'employees']),
('privacy_visibility', '!=', 'followers'),
('message_partner_ids', 'in', [user.partner_id.id])
]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
@@ -63,15 +63,13 @@
</record>
<record model="ir.rule" id="task_visibility_rule">
<field name="name">Project/Task: employees: portal or employee or (followers and following)</field>
<field name="name">Project/Task: employees: follow required for follower-only projects</field>
<field name="model_id" ref="model_project_task"/>
<field name="domain_force">[
'|',
('project_id.privacy_visibility', 'in', ['portal', 'employees']),
('project_id.privacy_visibility', '!=', 'followers'),
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
'|',
('message_partner_ids', 'in', [user.partner_id.id]),
# to subscribe check access to the record, follower is not enough at creation
@@ -87,42 +85,6 @@
<field name="groups" eval="[(4,ref('project.group_project_manager'))]"/>
</record>
<!-- Portal -->
<record model="ir.rule" id="portal_project_rule">
<field name="name">Project: portal users: portal or following</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="domain_force">[
'|',
'&amp;',
('privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('privacy_visibility', '=', 'followers'),
('message_partner_ids', 'in', [user.partner_id.id])
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="portal_task_rule">
<field name="name">Project/Task: portal users: (portal and colleagues following) or (followers and following)</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
'|',
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_id.privacy_visibility', '=', 'followers'),
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
'&amp;',
# on employee project can receive messages but not access the object
('project_id.privacy_visibility', '!=', 'employees'),
('message_partner_ids', 'in', [user.partner_id.id])
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record model="ir.rule" id="report_project_task_user_report_comp_rule">
<field name="name">Task Analysis multi-company</field>
<field name="model_id" ref="model_report_project_task_user"/>
+2 -49
View File
@@ -31,37 +31,6 @@ class TestPortalProjectBase(TestProjectBase):
class TestPortalProject(TestPortalProjectBase):
@mute_logger('openerp.addons.base.ir.ir_model')
def test_portal_project_access_rights(self):
pigs = self.project_pigs
pigs.write({'privacy_visibility': 'portal'})
# Do: Alfred reads project -> ok (employee ok public)
pigs.sudo(self.user_projectuser).read(['user_id'])
# Test: all project tasks visible
tasks = self.env['project.task'].sudo(self.user_projectuser).search([('project_id', '=', pigs.id)])
self.assertEqual(tasks, self.task_1 | self.task_2 | self.task_3 | self.task_4 | self.task_5 | self.task_6,
'access rights: project user should see all tasks of a portal project')
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
# Test: no project task searchable
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_noone).search, [('project_id', '=', pigs.id)])
# Data: task follower
pigs.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id])
self.task_1.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
self.task_3.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
# Do: Chell reads project -> ok (portal ok public)
pigs.sudo(self.user_portal).read(['user_id'])
# Do: Donovan reads project -> ko (public ko portal)
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
# Test: no access right to project.task
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_public).search, [])
# Data: task follower cleaning
self.task_1.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
self.task_3.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
@mute_logger('openerp.addons.base.ir.ir_model')
def test_employee_project_access_rights(self):
pigs = self.project_pigs
@@ -76,11 +45,6 @@ class TestPortalProject(TestPortalProjectBase):
'access rights: project user cannot see all tasks of an employees project')
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
# Do: Chell reads project -> ko (portal ko employee)
self.assertRaises(AccessError, pigs.sudo(self.user_portal).read, ['user_id'])
# Test: no project task visible + assigned
tasks = self.env['project.task'].sudo(self.user_portal).search([('project_id', '=', pigs.id)])
self.assertFalse(tasks.ids, 'access rights: portal user should not see tasks of an employees project, even if assigned')
# Do: Donovan reads project -> ko (public ko employee)
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
# Do: project user is employee and can create a task
@@ -104,27 +68,16 @@ class TestPortalProject(TestPortalProjectBase):
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
# Do: Chell reads project -> ko (portal ko employee)
self.assertRaises(AccessError, pigs.sudo(self.user_portal).read, ['user_id'])
# Test: no project task visible
tasks = self.env['project.task'].sudo(self.user_portal).search([('project_id', '=', pigs.id)])
self.assertEqual(tasks, self.task_3,
'access rights: portal user should not see tasks of a not-followed followers project, only assigned')
# Do: Donovan reads project -> ko (public ko employee)
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
# Data: subscribe Alfred, Chell and Donovan as follower
pigs.message_subscribe_users(user_ids=[self.user_projectuser.id, self.user_portal.id, self.user_public.id])
self.task_1.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id, self.user_projectuser.id])
self.task_3.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id, self.user_projectuser.id])
pigs.message_subscribe_users(user_ids=[self.user_projectuser.id])
# Do: Alfred reads project -> ok (follower ok followers)
prout = pigs.sudo(self.user_projectuser)
prout.invalidate_cache()
prout.read(['user_id'])
# Do: Chell reads project -> ok (follower ok follower)
pigs.sudo(self.user_portal).read(['user_id'])
# Do: Donovan reads project -> ko (public ko follower even if follower)
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
# Do: project user is follower of the project and can create a task
+2 -2
View File
@@ -32,7 +32,7 @@ class TestProjectBase(TestMail):
# Test 'Pigs' project
cls.project_pigs = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
'name': 'Pigs',
'privacy_visibility': 'portal',
'privacy_visibility': 'employees',
'alias_name': 'project+pigs',
'partner_id': cls.partner_1.id})
# Already-existing tasks in Pigs
@@ -48,7 +48,7 @@ class TestProjectBase(TestMail):
# Test 'Goats' project, same as 'Pigs', but with 2 stages
cls.project_goats = cls.env['project.project'].with_context({'mail_create_nolog': True}).create({
'name': 'Goats',
'privacy_visibility': 'portal',
'privacy_visibility': 'followers',
'alias_name': 'project+goats',
'partner_id': cls.partner_1.id,
'type_ids': [
+1 -6
View File
@@ -107,8 +107,7 @@
<page string="Settings">
<group>
<field name="privacy_visibility" widget="radio"/>
<field name="partner_id" string="Customer"
attrs="{'invisible':[('privacy_visibility','in',['employees', 'followers'])]}"/>
<field name="partner_id" string="Customer"/>
<field name="user_id" string="Project Manager"
attrs="{'readonly':[('active','=',False)]}"
groups="base.group_no_one"/>
@@ -744,10 +743,6 @@
</record>
<menuitem action="project_tags_action" id="menu_project_tags_act" parent="menu_project_config" groups="base.group_no_one"/>
<!-- Portal menuitems -->
<menuitem name="Projects" id="portal_services_projects" parent="portal.portal_projects"
action="open_view_project_all" sequence="10"/>
<!-- Reporting menus -->
<menuitem id="base.menu_project_report" name="Reports"
groups="project.group_project_manager"
-1
View File
@@ -12,5 +12,4 @@ Allows your customers to manage their account from a beautiful web interface.
'data': [
'views/templates.xml',
],
'installable': True,
}
+3 -2
View File
@@ -19,7 +19,7 @@
</t>
<t t-if="group['date_begin'] != date">
<li>
<a t-ignore="True" t-attf-href="#{default_url}?date_begin=#{group['date_begin']}&amp;date_end=#{group['date_end']}"><t t-esc="group['name']"/><span class="pull-right badge" t-esc="group['item_count']"/></a>
<a t-ignore="True" t-attf-href="#{default_url}?#{keep_query()}&amp;date_begin=#{group['date_begin']}&amp;date_end=#{group['date_end']}"><t t-esc="group['name']"/><span class="pull-right badge" t-esc="group['item_count']"/></a>
</li>
</t>
</t>
@@ -77,7 +77,7 @@
</t>
</template>
<template id="website_portal.custom_panel" inherit_id='website_portal.portal_layout' customize_show="True" name="Editable Panel" active="False">
<template id="custom_panel" inherit_id="portal_layout" customize_show="True" name="Portal Editable Panel" active="False">
<xpath expr="//div[@id='o_my_sidebar']" position="inside">
<div class="oe_structure">
<h3 class="page-header">Custom Panel</h3>
@@ -197,4 +197,5 @@
<li><a href="/my/home" role="menuitem">My Account</a></li>
</xpath>
</template>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
+20
View File
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Website Project",
'description': """
Website portal for Project and Tasks
====================================
""",
'category': 'Website',
'depends': ['project', 'website_portal'],
'data': [
'security/project_security.xml',
'security/ir.model.access.csv',
'views/project_templates.xml',
],
'demo': [
'demo/project_demo.xml',
],
'auto_install': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import main
+130
View File
@@ -0,0 +1,130 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import OrderedDict
from odoo import http, _
from odoo.http import request
from odoo.addons.website_portal.controllers.main import website_account
class WebsiteAccount(website_account):
def _prepare_portal_layout_values(self):
values = super(WebsiteAccount, self)._prepare_portal_layout_values()
project_count = request.env['project.project'].search_count([('privacy_visibility','=','portal')])
task_count = request.env['project.task'].search_count([('project_id.privacy_visibility','=','portal')])
values.update({
'project_count': project_count,
'task_count': task_count,
})
return values
@http.route(['/my/projects', '/my/projects/page/<int:page>'], type='http', auth="user", website=True)
def my_projects(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
Project = request.env['project.project']
sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc'},
'name': {'label': _('Name'), 'order': 'name'},
}
domain = [('privacy_visibility','=','portal')]
order = sortings.get(sortby, sortings['date'])['order']
# archive groups - Default Group By 'create_date'
archive_groups = self._get_archive_groups('project.project', domain)
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# pager
pager = request.website.pager(
url="/my/projects",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
total=values['project_count'],
page=page,
step=self._items_per_page
)
# content according to pager and archive selected
projects = Project.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
values.update({
'date': date_begin,
'date_end': date_end,
'sortings': sortings,
'sortby': sortby,
'projects': projects,
'page_name': 'project',
'archive_groups': archive_groups,
'default_url': '/my/projects',
'pager': pager
})
return request.website.render("website_project.my_projects", values)
@http.route(['/my/project/<model("project.project"):project>'], type='http', auth="user", website=True)
def my_project(self, project=None, **kw):
return request.website.render("website_project.my_project", {'project': project})
@http.route(['/my/tasks', '/my/tasks/page/<int:page>'], type='http', auth="user", website=True)
def my_tasks(self, page=1, date_begin=None, date_end=None, project=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
sortings = {
'date': {'label': _('Newest'), 'order': 'create_date desc'},
'name': {'label': _('Name'), 'order': 'name'},
'stage': {'label': _('Stage'), 'order': 'stage_id'},
'update': {'label': _('Last Stage Update'), 'order': 'date_last_stage_update desc'},
}
projects = request.env['project.project'].search([('privacy_visibility', '=', 'portal')])
project_filters = {
'all': {'label': _('All'), 'domain': []},
}
for proj in projects:
project_filters.update({
str(proj.id): {'label': proj.name, 'domain': [('project_id', '=', proj.id)]}
})
domain = [('project_id.privacy_visibility', '=', 'portal')]
domain += project_filters.get(project, project_filters['all'])['domain']
order = sortings.get(sortby, sortings['date'])['order']
# archive groups - Default Group By 'create_date'
archive_groups = self._get_archive_groups('project.task', domain)
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# pager
pager = request.website.pager(
url="/my/tasks",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby, 'project': project},
total=values['task_count'],
page=page,
step=self._items_per_page
)
# content according to pager and archive selected
tasks = request.env['project.task'].search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
values.update({
'date': date_begin,
'date_end': date_end,
'project_filters': OrderedDict(sorted(project_filters.items())),
'projects': projects,
'project': project,
'sortings': sortings,
'sortby': sortby,
'tasks': tasks,
'page_name': 'task',
'archive_groups': archive_groups,
'default_url': '/my/tasks',
'pager': pager
})
return request.website.render("website_project.my_tasks", values)
@http.route(['/my/task/<model("project.task"):task>'], type='http', auth="user", website=True)
def my_task(self, task=None, **kw):
return request.website.render("website_project.my_task", {'task': task, 'user': request.env.user})
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="project.project_project_1" model="project.project">
<field name="partner_id" ref="portal.partner_demo_portal"/>
<field name="privacy_visibility">portal</field>
</record>
<function model="project.project" name="message_subscribe"
eval="[ref('project.project_project_1')], [ref('portal.partner_demo_portal')]"/>
</data>
</odoo>
@@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_analytic_account_portal,account_analytic_account,analytic.model_account_analytic_account,base.group_portal,1,0,0,0
access_project_tags_portal,project_tags_portal,project.model_project_tags,base.group_portal,1,0,0,0
access_project_portal,project__portal,project.model_project_project,base.group_portal,1,0,0,0
access_task_portal,task_portal,project.model_project_task,base.group_portal,1,0,0,0
access_task_type_portal,task_type_portal,project.model_project_task_type,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_analytic_account_portal account_analytic_account analytic.model_account_analytic_account base.group_portal 1 0 0 0
3 access_project_tags_portal project_tags_portal project.model_project_tags base.group_portal 1 0 0 0
4 access_project_portal project__portal project.model_project_project base.group_portal 1 0 0 0
5 access_task_portal task_portal project.model_project_task base.group_portal 1 0 0 0
6 access_task_type_portal task_type_portal project.model_project_task_type base.group_portal 1 0 0 0
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Portal -->
<record id="project.portal_project_rule" model="ir.rule">
<field name="name">Project: portal users: portal and following</field>
<field name="model_id" ref="project.model_project_project"/>
<field name="domain_force">[
'&amp;',
('privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="project.portal_task_rule" model="ir.rule">
<field name="name">Project/Task: portal users: (portal and following project) or (portal and following task)</field>
<field name="model_id" ref="project.model_project_task"/>
<field name="domain_force">[
'|',
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
'&amp;',
('project_id.privacy_visibility', '=', 'portal'),
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</data>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import test_access_rights
@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.addons.project.tests.test_access_rights import TestPortalProjectBase
from odoo.exceptions import AccessError
from odoo.tools import mute_logger
class TestPortalProject(TestPortalProjectBase):
@mute_logger('openerp.addons.base.ir.ir_model')
def test_portal_project_access_rights(self):
pigs = self.project_pigs
pigs.write({'privacy_visibility': 'portal'})
# Do: Alfred reads project -> ok (employee ok public)
pigs.sudo(self.user_projectuser).read(['user_id'])
# Test: all project tasks visible
tasks = self.env['project.task'].sudo(self.user_projectuser).search([('project_id', '=', pigs.id)])
self.assertEqual(tasks, self.task_1 | self.task_2 | self.task_3 | self.task_4 | self.task_5 | self.task_6,
'access rights: project user should see all tasks of a portal project')
# Do: Bert reads project -> crash, no group
self.assertRaises(AccessError, pigs.sudo(self.user_noone).read, ['user_id'])
# Test: no project task searchable
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_noone).search, [('project_id', '=', pigs.id)])
# Data: task follower
pigs.sudo(self.user_projectmanager).message_subscribe_users(user_ids=[self.user_portal.id])
self.task_1.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
self.task_3.sudo(self.user_projectuser).message_subscribe_users(user_ids=[self.user_portal.id])
# Do: Chell reads project -> ok (portal ok public)
pigs.sudo(self.user_portal).read(['user_id'])
# Do: Donovan reads project -> ko (public ko portal)
self.assertRaises(AccessError, pigs.sudo(self.user_public).read, ['user_id'])
# Test: no access right to project.task
self.assertRaises(AccessError, self.env['project.task'].sudo(self.user_public).search, [])
# Data: task follower cleaning
self.task_1.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
self.task_3.sudo(self.user_projectuser).message_unsubscribe_users(user_ids=[self.user_portal.id])
@@ -0,0 +1,295 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="portal_layout" name="Portal layout: project menu entry" inherit_id="website_portal.portal_layout" priority="28">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="project_count" t-att-class="page_name == 'project' and 'active' or ''">
<a href="/my/projects">Projects</a>
</li>
<li t-if="task_count" t-att-class="page_name == 'task' and 'active' or ''">
<a href="/my/tasks">Tasks</a>
</li>
</xpath>
</template>
<template id="portal_my_home" name="Portal My Home: project entries" inherit_id="website_portal.portal_my_home" priority="28">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="project_count" class="page-header">
<a href="/my/projects">Your Projects
<small class="ml8">
<span class='badge'><t t-esc="project_count"/></span>
</small>
</a>
</h3>
<h3 t-if="task_count" class="page-header">
<a href="/my/tasks">Your Tasks
<small class="ml8">
<span class='badge'><t t-esc="task_count"/></span>
</small>
</a>
</h3>
</xpath>
</template>
<template id="my_projects" name="My Projects">
<t t-call="website_portal.portal_layout">
<div class="row">
<h3 class="page-header">
Your Projects
<div class="dropdown pull-right mr8">
<button id="sortby" class="btn btn-default" type="button" data-toggle="dropdown">
<span class="fa fa-sort fa-lg" /> <span t-esc="sortings.get(sortby, {}).get('label', 'Newest')"/>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="sortby">
<li t-foreach="sortings" t-as="option" t-attf-class="#{sortby == option and 'active'}">
<a t-att-href="default_url + '?' + keep_query('date_begin', 'date_end', sortby=option)"><span t-esc="sortings.get(option).get('label')"/></a>
</li>
</ul>
</div>
</h3>
</div>
<div class="panel panel-default">
<t t-if="not projects">
<div class="alert alert-warning mt8" role="alert">
There are no projects.
</div>
</t>
<table t-if="projects" class="table table-hover status_table">
<thead>
<tr class="active">
<th class="col-md-6">Name</th>
<th></th>
</tr>
</thead>
<tbody>
<tr t-foreach="projects" t-as="project">
<td>
<a t-attf-href="/my/project/#{project.id}?{{ keep_query() }}"><span t-field="project.name"/></a>
</td>
<td>
<a t-if="project.use_tasks" t-attf-href="/my/tasks?project=#{project.id}">
<t t-esc="project.task_count" />
<t t-esc="project.label_tasks" />
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</t>
</template>
<template id="my_project" name="My Project">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-4">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/projects?#{keep_query()}">My Projects</a></li>
<li><span t-field="project.name"/></li>
</ol>
</div>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
Project - <span t-field="project.name"/>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class='row'>
<div class="col-md-6">
<t t-if="project.partner_id">
<strong>Customer</strong>
<div t-if="project.partner_id">
<address t-field="project.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
</div>
</t>
<t t-if="project.user_id">
<strong>Project Manager</strong>
<div>
<address t-field="project.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
</div>
</t>
</div>
<div class="col-md-6">
<div class="pull-right">
<a t-attf-href="/my/tasks?project=#{project.id}" class="btn btn-default btn-lg">
<span class="fa fa-tasks" />
<span t-esc="project.task_count" />
<span t-field="project.label_tasks" />
</a>
</div>
</div>
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="project"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
<template id="my_tasks" name="My Tasks">
<t t-call="website_portal.portal_layout">
<h3 class="page-header">
Your Tasks
<div class="dropdown pull-right mr8">
<button id="sortby" class="btn btn-default" type="button" data-toggle="dropdown">
<span class="fa fa-sort fa-lg" /> <span t-esc="sortings.get(sortby, {}).get('label', 'Newest')"/>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="sortby">
<li t-foreach="sortings" t-as="option" t-att-class="sortby == option and 'active'">
<a t-attf-href="/my/tasks?{{ keep_query('date_begin', 'date_end', 'project', sortby=option) }}"><span t-esc="sortings.get(option).get('label')"/></a>
</li>
</ul>
</div>
<div class="dropdown pull-right mr8">
<button id="project_filters" class="btn btn-default" type="button" data-toggle="dropdown">
Projects: <span t-esc="project_filters.get(project, {}).get('label', 'All')"/>
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="project_filters">
<li t-foreach="project_filters" t-as="option" t-att-class="project == option and 'active'">
<a t-attf-href="/my/tasks?{{ keep_query('date_begin', 'date_end', 'sortby', project=option) }}"><span t-esc="project_filters.get(option).get('label')"/></a>
</li>
</ul>
</div>
</h3>
<div class="panel panel-default">
<t t-if="not tasks">
<div class="alert alert-warning mt8" role="alert">
There are no tasks.
</div>
</t>
<table t-if="tasks" class="table table-hover status_table">
<thead>
<tr class="active">
<th class="col-md-10">Task</th>
<th>Stage</th>
</tr>
</thead>
<tbody>
<t t-foreach="tasks" t-as="task">
<tr>
<td>
<a t-attf-href="/my/task/#{task.id}?{{ keep_query() }}"><span t-field="task.name"/></a>
</td>
<td>
<span class="label label-info" title="Current stage of the task" t-esc="task.stage_id.name" />
</td>
</tr>
</t>
</tbody>
</table>
</div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</t>
</template>
<template id="my_task" name="My Task">
<t t-call="website.layout">
<div id="wrap">
<div class="container">
<div class="oe_structure">
<div class="row">
<div class="col-sm-6">
<ol class="breadcrumb mt8">
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
<li><a t-attf-href="/my/tasks?#{keep_query()}">My Tasks</a></li>
<li><span t-field="task.name"/></li>
</ol>
</div>
</div>
</div>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<h4>
Task - <span t-field="task.name"/>
<span t-field="task.stage_id.name" class="pull-right label label-info" title="Current stage of this task"/>
<a class="btn btn-info" t-att-href="'/web#return_label=Website&amp;model=project.task&amp;id=%s&amp;view_type=form' % (task.id)" groups="project.group_project_user">Edit Task</a>
</h4>
</div>
</div>
</div>
<div class="panel-body">
<div class="mb8" t-if="user.partner_id.id in task.sudo().project_id.message_partner_ids.ids">
<strong>Project:</strong> <a t-attf-href="/my/project/#{task.project_id.id}" t-field="task.project_id.name"/>
</div>
<div class='row'>
<div class="col-md-6">
<div class="mb8">
<strong>Date:</strong> <span t-field="task.create_date" t-field-options='{"widget": "date"}'/>
</div>
<div t-if="task.user_id">
<strong>Assigned to</strong>
<div>
<address t-field="task.user_id" t-field-options='{"widget": "contact", "fields": ["name", "email", "phone"]}'/>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb8" t-if="task.date_deadline">
<strong>Deadline:</strong> <span t-field="task.date_deadline" t-field-options='{"widget": "date"}'/>
</div>
<div t-if="task.partner_id">
<strong>Reported by</strong>
<div>
<address t-field="task.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "email"]}'/>
</div>
</div>
</div>
</div>
<div class="container" t-if="task.description">
<p t-field="task.description" />
</div>
</div>
</div>
<div class="row mt32">
<div class="col-md-12">
<h4><strong>Message and communication history</strong></h4>
</div>
<div class="col-md-10 col-md-offset-1 mt16">
<t t-call="website_mail.message_thread">
<t t-set="object" t-value="task"/>
<t t-set="chatter_mode" t-value="'json'"/>
</t>
</div>
</div>
</div>
</div>
</div>
</t>
</template>
</odoo>