source: simplemultiprojectplugin/trunk/simplemultiproject/roadmap.py

Last change on this file was 18037, checked in by Cinc-th, 3 years ago

SimpleMultiProjectPlugin: new preference items to hide milestones and/or versions on the Roadmap page. Some refactoring and cleanups

Refs #13956

  • Property svn:executable set to *
File size: 12.2 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2015-2021 Cinc
4#
5# License: 3-clause BSD
6#
7from pkg_resources import get_distribution, parse_version, resource_filename
8from simplemultiproject.api import IRoadmapDataProvider
9from simplemultiproject.compat import JTransformer
10from simplemultiproject.model import Project
11from simplemultiproject.permission import PERM_TEMPLATE, SmpPermissionPolicy
12from simplemultiproject.session import get_project_filter_settings, \
13    get_filter_settings
14from simplemultiproject.smp_model import SmpMilestone, SmpVersion
15from trac.config import OrderedExtensionsOption
16from trac.core import *
17from trac.util.translation import _
18from trac.web.api import IRequestFilter
19from trac.web.chrome import add_script, add_script_data, add_stylesheet, Chrome, ITemplateProvider
20
21
22__all__ = ['SmpRoadmapModule']
23
24
25class SmpRoadmapModule(Component):
26    """Manage roadmap page for projects.
27
28    This component allows to filter roadmap entries by project. It is possible to group entries by project.
29    """
30
31    implements(IRequestFilter, IRoadmapDataProvider, ITemplateProvider)
32
33    data_provider = OrderedExtensionsOption(
34        'simple-multi-project', 'roadmap_data_provider', IRoadmapDataProvider,
35        default="",
36        doc="""Specify the order of plugins providing data for roadmap page""")
37
38    data_filters = OrderedExtensionsOption(
39        'simple-multi-project', 'roadmap_data_filters', IRoadmapDataProvider,
40        default="",
41        doc="""Specify the order of plugins filtering data for roadmap page""")
42
43    # Api changes regarding Genshi started after v1.2. This not only affects templates but also fragment
44    # creation using trac.util.html.tag and friends
45    pre_1_3 = parse_version(get_distribution("Trac").version) < parse_version('1.3')
46    group_tmpl = None
47
48    def __init__(self):
49        self.smp_milestone = SmpMilestone(self.env)
50        self.smp_version = SmpVersion(self.env)
51
52    def _load_template(self):
53        chrome = Chrome(self.env)
54        if self.pre_1_3:
55            self.group_tmpl = chrome.load_template("smp_roadmap.html", None)
56        else:
57            self.group_tmpl = chrome.load_template("smp_roadmap_jinja.html", False)
58
59    # Unused preference item
60    # if get_filter_settings(req, 'roadmap', 'smp_hideprojdesc'):
61    #     hide.append('projectdescription')
62    # prefs = """<div>
63    #               <input type="checkbox" id="hideprojectdescription" name="smp_hideprojdesc" value="1"
64    #                      {hideprjdescchk} />
65    #               <label for="hideprojectdescription">{hideprjdesclabel}</label>
66    #         </div>"""
67
68    def create_hide_milestone_item(self, req):
69        prefs = """
70        <div>
71              <input type="checkbox" id="hidemilestones" name="smp_hidemilestones" value="1"
72                     {hidemschk} />
73              <label for="hidemilestones">{hidemslabel}</label>
74        </div>"""
75        hidemschk = ' checked="checked"' if get_filter_settings(req, 'roadmap', 'smp_hidemilestones') else '',
76        return prefs.format(hidemslabel=_('Hide milestones'), hidemschk=hidemschk)
77
78    # IRequestFilter methods
79
80    def pre_process_request(self, req, handler):
81        return handler
82
83    def post_process_request(self, req, template, data, content_type):
84        """Call extensions adding data or filtering data in the
85        appropriate order.
86        """
87        if data:
88            path_elms = req.path_info.split('/')
89            if len(path_elms) > 1 and path_elms[1] == 'roadmap':
90                add_stylesheet(req, "simplemultiproject/css/simplemultiproject.css")
91
92                for provider in self.data_provider:
93                    data = provider.add_data(req, data)
94
95                for provider in self.data_filters:
96                    data = provider.filter_data(req, data)
97
98                # Add project table to preferences on roadmap page
99                # xpath: //form[@id="prefs"]
100                xform = JTransformer('form#prefs')
101                filter_list = [xform.prepend(create_proj_table(self, req, 'roadmap'))]
102
103                # Add 'group' check box
104                group_proj = get_filter_settings(req, 'roadmap', 'smp_group')
105                chked = ' checked="1"' if group_proj else ''
106                # xpath: //form[@id="prefs"]
107                xform = JTransformer('form#prefs')
108                filter_list.append(xform.prepend(u'<div>'
109                                                 u'<input type="hidden" name="smp_update" value="group" />'
110                                                 u'<input type="checkbox" id="groupbyproject" name="smp_group" '
111                                                 u'value="1"%s/>'
112                                                 u'<label for="groupbyproject">Group by project</label></div><br />' %
113                                                 chked))
114                # Add preference checkbox
115                xform = JTransformer('#prefs div.buttons')
116                filter_list.append(xform.before(self.create_hide_milestone_item(req)))
117
118                if chked:
119                    if not self.group_tmpl:
120                        self._load_template()
121                    # Add new grouped content
122                    # xpath: //div[@class="milestones"]
123                    xform = JTransformer('div.milestones')
124                    chrome = Chrome(self.env)
125                    data = chrome.populate_data(req, data)
126                    if self.pre_1_3:
127                        filter_list.append(xform.before(self.group_tmpl.generate(**data).render('html')))
128                    else:
129                        filter_list.append(xform.before(chrome.render_template_string(self.group_tmpl, data)))
130                    filter_list.append(xform.remove())  # Remove default milestone entries
131
132                add_script_data(req, {'smp_filter': filter_list})
133                add_script(req, 'simplemultiproject/js/jtransform.js')
134
135        return template, data, content_type
136
137    # IRoadmapDataProvider
138
139    def add_projects_to_dict(self, req, data):
140        """Add allowed projects to the data dict.
141
142        :param req: a Request object
143        :param data: dictionary holding data for template
144        :return None
145
146        This checks if the user has access to the projects. If not a project won't be added to the list of available
147        projects. Closed projects are ignored, too.
148        """
149        usr_projects = SmpPermissionPolicy.active_projects_by_permission(req, Project.select(self.env))
150        data.update({'projects': usr_projects,
151                     'project_ids': [project.id for project in usr_projects]})
152
153    def add_project_info_to_milestones(self, data):
154        # Do the milestone updates
155        data['ms_without_prj'] = False
156        if data.get('milestones'):
157            # Add info about linked projects
158            for item in data.get('milestones'):
159                # Used in smp_roadmap.html to check if there is a ms - proj link
160                ids_for_ms = self.smp_milestone.get_project_ids_for_resource_item('milestone', item.name)
161                if not ids_for_ms:
162                    item.id_project = []  # Milestones without a project are for all
163                    data['ms_without_prj'] = True
164                else:
165                    item.id_project = ids_for_ms
166
167    def add_data(self, req, data):
168        # Get all projects user has access to.
169        self.add_projects_to_dict(req, data)
170        self.add_project_info_to_milestones(data)
171        # self.add_project_info_to_versions(data)
172
173        return data
174
175    def filter_data(self, req, data):
176
177        if data and get_filter_settings(req, 'roadmap', 'smp_hidemilestones'):
178            data['milestones'] = []
179            data['milestone_stats'] = []
180
181        filter_proj = get_project_filter_settings(req, 'roadmap', 'smp_projects', 'All')
182        if 'All' in filter_proj:
183            return data
184
185        # Remove projects from dict which are not selected. The template will loop over this data.
186        if 'projects' in data:
187            filtered = []
188            for project in data['projects']:
189                if project.name in filter_proj:
190                    filtered.append(project)
191            data['projects'] = filtered
192
193        if 'milestones' in data:
194            item_stats = data.get('milestone_stats')
195            filtered_items = []
196            filtered_item_stats = []
197            for idx, ms in enumerate(data['milestones']):
198                ms_proj = self.smp_milestone.get_project_names_for_item(ms.name)
199                # Milestones without linked projects are good for every project
200                if not ms_proj:
201                    filtered_items.append(ms)
202                    filtered_item_stats.append(item_stats[idx])
203                else:
204                    # List of project names
205                    for name in ms_proj:
206                        if name in filter_proj:
207                            filtered_items.append(ms)
208                            filtered_item_stats.append(item_stats[idx])
209                            break  # Only add a milestone once
210            data['milestones'] = filtered_items
211            data['milestone_stats'] = filtered_item_stats
212
213        # TODO: Is this still needed?
214        if 'versions' in data:
215            item_stats = data.get('version_stats')
216            filtered_items = []
217            filtered_item_stats = []
218            for idx, ms in enumerate(data['versions']):
219                ms_proj = self.smp_version.get_project_names_for_item(ms.name)
220                # Versions without linked projects are good for every project
221                if not ms_proj:
222                    filtered_items.append(ms)
223                    filtered_item_stats.append(item_stats[idx])
224                else:
225                    # List of project names
226                    for name in ms_proj:
227                        if name in filter_proj:
228                            filtered_items.append(ms)
229                            filtered_item_stats.append(item_stats[idx])
230                            break  # Only add a version once
231
232            data['versions'] = filtered_items
233            data['version_stats'] = filtered_item_stats
234
235        return data
236
237    # ITemplateProvider methods
238
239    def get_templates_dirs(self):
240        self.log.info(resource_filename(__name__, 'templates'))
241        return [resource_filename(__name__, 'templates')]
242
243    def get_htdocs_dirs(self):
244        return [('simplemultiproject', resource_filename(__name__, 'htdocs'))]
245
246
247def div_from_projects(all_projects, filter_prj, size):
248    """Create the project select div for the preference pane on Roadmap and timeline page."""
249    # Don't change indentation here without fixing the test cases
250    div_templ = u"""<div style="overflow:hidden;">
251<div>
252<label>Filter Project:</label>
253</div>
254<div>
255<input type="hidden" name="smp_update" value="filter">
256<select id="Filter-Projects" name="smp_projects" multiple size="{size}" style="overflow:auto;">
257    <option value="All"{all_selected}>All</option>
258    {options}
259</select>
260</div>
261<br>
262</div>"""
263    option_tmpl = u"""<option value="{p_name}"{sel}>
264        {p_name}
265    </option>"""
266
267    options = u""
268    for item in all_projects:
269        sel = u' selected' if item.name in filter_prj else u''
270        options += option_tmpl.format(p_name=item.name, sel=sel)
271
272    return div_templ.format(size=size, all_selected='' if filter_prj else u' selected', options=options)
273
274
275def create_proj_table(self, req, session_name='roadmap'):
276    """Create a select tag holding valid projects (means not closed) for
277    the current user.
278
279    @param self: Component instance holding the Environment object
280    @param req      : Trac request object
281
282    @return DIV tag holding a project select control with label
283    """
284    projects = Project.select(self.env)
285    filtered_projects = SmpPermissionPolicy.active_projects_by_permission(req, projects)
286
287    if filtered_projects:
288        size = len(filtered_projects) + 1  # Account for 'All' option
289    else:
290        return u'<div><p>No projects defined.</p><br></div>'
291
292    if size > 5:
293        size = 5
294
295    # list of currently selected projects. The info is stored in the request or session data
296    filter_prj = get_project_filter_settings(req, session_name, 'smp_projects', 'All')
297    if 'All' in filter_prj:
298        filter_prj = []
299
300    return div_from_projects(filtered_projects, filter_prj, size)
Note: See TracBrowser for help on using the repository browser.