[IMP] website_project_issue: Portal access to Issues
Move portal features (mainly ir.rule) from project_issue to website_project_issue. Remove backend menu item, issues should be accessed from the website_portal My account page, the backend portal menuitem is removed. Add the dependency on website_portal to display the issue count on projects. Original authors: Vipul Bhatt, Florian Wintjens
This commit is contained in:
@@ -19,8 +19,10 @@ class Project(models.Model):
|
||||
res.append(("project.issue", "Issues"))
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def _compute_issue_count(self):
|
||||
self.issue_count = self.env['project.issue'].search_count([('project_id', '=', self.id), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
|
||||
for project in self:
|
||||
project.issue_count = self.env['project.issue'].search_count([('project_id', '=', project.id), '|', ('stage_id.fold', '=', False), ('stage_id', '=', False)])
|
||||
|
||||
def _issue_needaction_count(self):
|
||||
issue_data = self.env['project.issue'].read_group([('project_id', 'in', self.ids), ('message_needaction', '=', True)], ['project_id'], ['project_id'])
|
||||
|
||||
@@ -10,40 +10,16 @@
|
||||
</record>
|
||||
|
||||
<record id="issue_user_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue: employees: employee or (followers and following)</field>
|
||||
<field name="name">Project/Issue: employees: no need to follow or (following project or following issue</field>
|
||||
<field name="model_id" ref="model_project_issue"/>
|
||||
<field name="domain_force">['|',
|
||||
('project_id.privacy_visibility', '=', 'employees'),
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
('project_id.privacy_visibility', '!=', 'followers'),
|
||||
'|',
|
||||
('project_id.message_partner_ids', 'in', [user.partner_id.id]),
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4,ref('base.group_user'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="portal_issue_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue: portal users: (portal and colleagues following) or (followers and following)</field>
|
||||
<field name="model_id" ref="project_issue.model_project_issue"/>
|
||||
<field name="domain_force">['|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('project_id.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="project_issue.issue_user_rule">
|
||||
<field name="name">Project/Issue: employees: portal, employee or (followers and following)</field>
|
||||
<field name="domain_force">['|',
|
||||
('project_id.privacy_visibility', 'in', ['portal', 'employees']),
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -267,26 +267,4 @@
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Portal -->
|
||||
<record id="open_view_issue" model="ir.actions.act_window">
|
||||
<field name="name">Issues</field>
|
||||
<field name="res_model">project.issue</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="project_issue.project_issue_tree_view"/>
|
||||
<field name="domain" eval=""/>
|
||||
<field name="context">{"search_default_user_id":'', "search_default_draft":'', "search_default_todo":'', "portal":'True'}</field>
|
||||
<field name="search_view_id" ref="project_issue.view_project_issue_filter"/>
|
||||
<field name="target">current</field>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click to create an issue.
|
||||
</p><p>
|
||||
You can track your issues from this menu and the action we will take.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
<menuitem id="portal_services_issues" name="Issues" parent="portal.portal_projects"
|
||||
action="open_view_issue" sequence="10"/>
|
||||
</odoo>
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
{
|
||||
'name': 'Website Project Issue',
|
||||
'category': 'Hidden',
|
||||
'version': '0.1',
|
||||
'complexity': 'easy',
|
||||
'description': """
|
||||
This module adds project issues inside your account's page on website if project_issue and website_portal are installed.
|
||||
==================================================================================================
|
||||
""",
|
||||
'depends': ['project_issue', 'website_portal'],
|
||||
'depends': ['project_issue', 'website_project'],
|
||||
'data': [
|
||||
'security/portal_security.xml',
|
||||
'security/project_issue_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/project_issue_templates.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'auto_install': True,
|
||||
'category': 'Hidden',
|
||||
}
|
||||
|
||||
@@ -1,58 +1,83 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
from openerp import http
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from openerp import http, _
|
||||
from openerp.addons.website_portal.controllers.main import website_account
|
||||
from openerp.http import request
|
||||
|
||||
|
||||
class WebsiteAccount(website_account):
|
||||
|
||||
@http.route()
|
||||
def account(self):
|
||||
response = super(WebsiteAccount, self).account()
|
||||
# TDE FIXME: shouldn't that be mnaged by the access rule itself ?
|
||||
# portal projects where you or someone from your company are a follower
|
||||
user = request.env.user
|
||||
issue_count = request.env['project.issue'].sudo().search_count([
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id, user.partner_id.id])
|
||||
])
|
||||
response.qcontext.update({'issue_count': issue_count})
|
||||
return response
|
||||
|
||||
def _prepare_portal_layout_values(self):
|
||||
values = super(WebsiteAccount, self)._prepare_portal_layout_values()
|
||||
# domain is needed to hide non portal project for employee
|
||||
issue_count = request.env['project.issue'].search_count([('project_id.privacy_visibility','=','portal')])
|
||||
values.update({
|
||||
'issue_count': issue_count,
|
||||
})
|
||||
return values
|
||||
|
||||
@http.route(['/my/issues', '/my/issues/page/<int:page>'], type='http', auth="user", website=True)
|
||||
def portal_my_issues(self, page=1, date_begin=None, date_end=None, **kw):
|
||||
def my_issues(self, page=1, date_begin=None, date_end=None, project=None, sortby=None, **kw):
|
||||
values = self._prepare_portal_layout_values()
|
||||
ProjectIssue = request.env['project.issue']
|
||||
domain = []
|
||||
|
||||
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.issue', domain)
|
||||
if date_begin and date_end:
|
||||
domain += [('create_date', '>=', date_begin), ('create_date', '<', date_end)]
|
||||
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
|
||||
# pager
|
||||
issue_count = ProjectIssue.search_count(domain)
|
||||
pager = request.website.pager(
|
||||
url="/my/issues",
|
||||
url_args={'date_begin': date_begin, 'date_end': date_end},
|
||||
total=issue_count,
|
||||
total=values['issue_count'],
|
||||
page=page,
|
||||
step=self._items_per_page
|
||||
)
|
||||
# content according to pager and archive selected
|
||||
project_issues = ProjectIssue.search(domain, order="stage_id", limit=self._items_per_page, offset=pager['offset'])
|
||||
project_issues = request.env['project.issue'].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,
|
||||
'issues': project_issues,
|
||||
'page_name': 'issue',
|
||||
'archive_groups': archive_groups,
|
||||
'default_url': '/my/issues',
|
||||
'pager': pager
|
||||
})
|
||||
return request.website.render("website_project_issue.portal_my_issues", values)
|
||||
return request.website.render("website_project_issue.my_issues", values)
|
||||
|
||||
@http.route(['/my/issues/<int:issue_id>'], type='http', auth="user", website=True)
|
||||
def issues_followup(self, issue_id=None, **kw):
|
||||
def my_issues_issue(self, issue_id=None, **kw):
|
||||
issue = request.env['project.issue'].browse(issue_id)
|
||||
return request.website.render("website_project_issue.issues_followup", {'issue': issue})
|
||||
return request.website.render("website_project_issue.my_issues_issue", {'issue': issue})
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="1">
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="portal_issue_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue: portal users: (portal and colleagues following) or (followers and following)</field>
|
||||
<field name="model_id" ref="project_issue.model_project_issue"/>
|
||||
<field name="domain_force">['|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
|
||||
</record>
|
||||
<record id="portal_issue_rule" model="ir.rule">
|
||||
<field name="name">Project/Issue: portal users: (portal and (following project or following issue)</field>
|
||||
<field name="model_id" ref="project_issue.model_project_issue"/>
|
||||
<field name="domain_force">[
|
||||
'|',
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'portal'),
|
||||
('project_id.message_partner_ids', 'child_of', [user.partner_id.commercial_partner_id.id]),
|
||||
'&',
|
||||
('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>
|
||||
|
||||
<record model="ir.rule" id="project_issue.issue_user_rule">
|
||||
<field name="name">Project/Issue: employees: portal, employee or (followers and following)</field>
|
||||
<field name="domain_force">['|',
|
||||
('project_id.privacy_visibility', 'in', ['portal', 'employees']),
|
||||
'&',
|
||||
('project_id.privacy_visibility', '=', 'followers'),
|
||||
('message_partner_ids', 'in', [user.partner_id.id]),
|
||||
]</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -84,18 +84,6 @@ class TestPortalIssue(TestPortalProjectBase):
|
||||
# ----------------------------------------
|
||||
self.project_pigs.write({'privacy_visibility': 'followers'})
|
||||
|
||||
# Do: Alfred reads project -> ko (employee ko followers)
|
||||
# Test: no project issue visible
|
||||
issues = Issue.sudo(self.user_projectuser.id).search([('project_id', '=', pigs_id)])
|
||||
self.assertEqual(set(issues.ids), set([self.issue_4.id]),
|
||||
'access rights: employee user should not see issues of a not-followed followers project, only assigned')
|
||||
|
||||
# Do: Chell reads project -> ko (portal ko employee)
|
||||
# Test: no project issue visible
|
||||
issues = Issue.sudo(self.user_portal.id).search([('project_id', '=', pigs_id)])
|
||||
self.assertEqual(set(issues.ids), set([self.issue_5.id]),
|
||||
'access rights: portal user should not see issues of a not-followed followers project, only assigned')
|
||||
|
||||
# Data: subscribe Alfred, Chell and Donovan as follower
|
||||
self.project_pigs.message_subscribe_users(user_ids=[self.user_projectuser.id, self.user_portal.id, self.user_public.id])
|
||||
self.issue_1.sudo(self.user_projectmanager.id).message_subscribe_users(user_ids=[self.user_portal.id, self.user_projectuser.id])
|
||||
@@ -104,11 +92,12 @@ class TestPortalIssue(TestPortalProjectBase):
|
||||
# Do: Alfred reads project -> ok (follower ok followers)
|
||||
# Test: followed + assigned issues visible
|
||||
issues = Issue.sudo(self.user_projectuser.id).search([('project_id', '=', pigs_id)])
|
||||
self.assertEqual(set(issues.ids), set([self.issue_1.id, self.issue_3.id, self.issue_4.id]),
|
||||
|
||||
self.assertEqual(set(issues.ids), test_issue_ids,
|
||||
'access rights: employee user should not see followed + assigned issues of a follower project')
|
||||
|
||||
# Do: Chell reads project -> ok (follower ok follower)
|
||||
# Test: followed + assigned issues visible
|
||||
issues = Issue.sudo(self.user_portal.id).search([('project_id', '=', pigs_id)])
|
||||
self.assertEqual(set(issues.ids), set([self.issue_1.id, self.issue_3.id, self.issue_5.id]),
|
||||
'access rights: employee user should not see followed + assigned issues of a follower project')
|
||||
self.assertFalse(set(issues.ids),
|
||||
'access rights: portal user should not see issues of a follower project')
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="portal_my_home_menu_issue" name="Portal layout : issues menu entry" inherit_id="website_portal.portal_layout" priority="30">
|
||||
<template id="portal_layout" name="Portal layout: Issues menu entry" inherit_id="website_portal.portal_layout" priority="30">
|
||||
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
|
||||
<li t-att-class="page_name == 'issue' and 'active' or ''">
|
||||
<a href="/my/issues">Issues</a>
|
||||
@@ -9,7 +9,7 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_home_issue" name="Portal My Home : issue entries" inherit_id="website_portal.portal_my_home" priority="30">
|
||||
<template id="portal_my_home" name="Portal My Home: Issue entries" inherit_id="website_portal.portal_my_home" priority="30">
|
||||
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
|
||||
<h3 class="page-header">
|
||||
<a href="/my/issues">Your Issues
|
||||
@@ -26,39 +26,67 @@
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="portal_my_issues" name="My Issues" priority="30">
|
||||
<template id="my_issues" name="My Issues">
|
||||
<t t-call="website_portal.portal_layout">
|
||||
<h3 class="page-header">Your Issues</h3>
|
||||
<t t-if="not issues">
|
||||
<p>There are currently no issues for your account.</p>
|
||||
</t>
|
||||
<t t-if="issues">
|
||||
<table class="table table-hover status_table">
|
||||
<h3 class="page-header">
|
||||
Your Issues
|
||||
<div class="dropdown pull-right mr8">
|
||||
<button id="sortby" class="btn btn-default" type="button" data-toggle="dropdown">
|
||||
<span class="fa fa-sort" /> <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-att-href="default_url + '?' + 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-att-href="default_url + '?' + 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 issues">
|
||||
<div class="alert alert-warning mt8" role="alert">
|
||||
There are no issues.
|
||||
</div>
|
||||
</t>
|
||||
<table t-if="issues" class="table table-hover status_table">
|
||||
<thead>
|
||||
<tr class="active">
|
||||
<th>Issue</th>
|
||||
<th>Description</th><th/>
|
||||
<th>Status</th>
|
||||
<th class="col-md-8">Description</th>
|
||||
<th>Stage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<t t-foreach="issues" t-as="issue">
|
||||
<tr>
|
||||
<td>
|
||||
<a t-attf-href="/my/issues/#{issue.id}?{{ keep_query() }}">Issue <t t-esc="issue.id"/></a>
|
||||
<a t-attf-href="/my/issues/#{issue.id}?{{ keep_query() }}">Issue <t t-esc="issue.id" /></a>
|
||||
</td>
|
||||
<td><span t-field="issue.name"/></td>
|
||||
<td>
|
||||
<span class="label label-info" title="Current stage of the issue" t-esc="issue.stage_id.name" />
|
||||
</td>
|
||||
<td><span t-field="issue.name"/></td><td/>
|
||||
<td><span t-field="issue.stage_id.name"/></td>
|
||||
</tr>
|
||||
</t>
|
||||
</table>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="website.pager"/>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="pager" class="o_portal_pager text-center">
|
||||
<t t-call="website.pager"/>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="issues_followup" name="Project Issues">
|
||||
<template id="my_issues_issue" name="My Issue">
|
||||
<t t-call="website.layout">
|
||||
<div id="wrap">
|
||||
<div class="container">
|
||||
@@ -122,4 +150,29 @@
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="my_projects" inherit_id="website_project.my_projects">
|
||||
<xpath expr="//table/thead/tr/th[last()]" position="after">
|
||||
<th></th>
|
||||
</xpath>
|
||||
<xpath expr="//table/tbody/tr/td[last()]" position="after">
|
||||
<td>
|
||||
<a t-if="project.use_issues" t-attf-href="/my/issues?project=#{project.id}">
|
||||
<t t-esc="project.issue_count" />
|
||||
<t t-esc="project.label_issues" />
|
||||
</a>
|
||||
</td>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<template id="my_project" inherit_id="website_project.my_project">
|
||||
<xpath expr="//div[@class='panel-body']/div[@class='row']/div[@class='col-md-6'][last()]/div[@class='pull-right']" position="before">
|
||||
<a t-attf-href="/my/issues?project=#{project.id}" class="btn btn-default btn-lg pull-right ml8">
|
||||
<span class="fa fa-bug" />
|
||||
<span t-esc="project.issue_count" />
|
||||
<span t-field="project.label_issues" />
|
||||
</a>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user