1# -*- coding: utf-8 -*- 2# 3# Copyright (C) 2004-2021 Edgewall Software 4# Copyright (C) 2004-2005 Daniel Lundin <daniel@edgewall.com> 5# All rights reserved. 6# 7# This software is licensed as described in the file COPYING, which 8# you should have received as part of this distribution. The terms 9# are also available at https://trac.edgewall.org/wiki/TracLicense. 10# 11# This software consists of voluntary contributions made by many 12# individuals. For the exact contribution history, see the revision 13# history and logs, available at https://trac.edgewall.org/log/. 14# 15# Author: Daniel Lundin <daniel@edgewall.com> 16 17import math 18import pkg_resources 19import re 20 21from trac.core import * 22from trac.prefs.api import IPreferencePanelProvider 23from trac.util import as_float, lazy 24from trac.util.datefmt import all_timezones, get_timezone, localtz 25from trac.util.html import tag 26from trac.util.translation import _, Locale, deactivate,\ 27 get_available_locales, get_locale_name, \ 28 make_activable 29from trac.web.api import HTTPNotFound, IRequestHandler, \ 30 is_valid_default_handler 31from trac.web.chrome import Chrome, INavigationContributor, \ 32 ITemplateProvider, add_notice, add_stylesheet, \ 33 add_warning 34 35 36class PreferencesModule(Component): 37 """Displays the preference panels and dispatch control to the 38 individual panels""" 39 40 implements(INavigationContributor, IRequestHandler, ITemplateProvider) 41 42 panel_providers = ExtensionPoint(IPreferencePanelProvider) 43 44 # INavigationContributor methods 45 46 def get_active_navigation_item(self, req): 47 return 'prefs' 48 49 def get_navigation_items(self, req): 50 panels = self._get_panels(req)[0] 51 if panels: 52 yield 'metanav', 'prefs', tag.a(_("Preferences"), 53 href=req.href.prefs()) 54 55 # IRequestHandler methods 56 57 def match_request(self, req): 58 match = re.match('/prefs(?:/([^/]+))?$', req.path_info) 59 if match: 60 req.args['panel_id'] = match.group(1) 61 return True 62 63 def process_request(self, req): 64 if req.is_xhr and req.method == 'POST' and 'save_prefs' in req.args: 65 self._do_save_xhr(req) 66 67 panels, providers = self._get_panels(req) 68 if not panels: 69 raise HTTPNotFound(_("No preference panels available")) 70 71 panels = [] 72 child_panels = {} 73 providers = {} 74 for provider in self.panel_providers: 75 for panel in provider.get_preference_panels(req) or []: 76 if len(panel) == 3: 77 name, label, parent = panel 78 child_panels.setdefault(parent, []).append((name, label)) 79 else: 80 name = panel[0] 81 panels.append(panel) 82 providers[name] = provider 83 panels = sorted(panels, key=lambda p: (p[0] or '',) + p[1:]) 84 85 panel_id = req.args.get('panel_id') 86 if panel_id is None: 87 panel_id = panels[1][0] \ 88 if len(panels) > 1 and panels[0][0] == 'advanced' \ 89 else panels[0][0] 90 chosen_provider = providers.get(panel_id) 91 if not chosen_provider: 92 raise HTTPNotFound(_("Unknown preference panel '%(panel)s'", 93 panel=panel_id)) 94 95 session_data = {'session': req.session} 96 97 # Render child preference panels. 98 chrome = Chrome(self.env) 99 children = [] 100 if child_panels.get(panel_id): 101 for name, label in child_panels[panel_id]: 102 ctemplate, cdata = \ 103 providers[name].render_preference_panel(req, name) 104 cdata.update(session_data) 105 rendered = chrome.render_fragment(req, ctemplate, cdata) 106 children.append((name, label, rendered)) 107 108 resp = chosen_provider.render_preference_panel(req, panel_id) 109 data = resp[1] 110 111 data.update(session_data) 112 data.update({ 113 'active_panel': panel_id, 114 'panels': panels, 115 'children': children, 116 }) 117 118 add_stylesheet(req, 'common/css/prefs.css') 119 return resp 120 121 # ITemplateProvider methods 122 123 def get_htdocs_dirs(self): 124 return [] 125 126 def get_templates_dirs(self): 127 return [pkg_resources.resource_filename('trac.prefs', 'templates')] 128 129 # Internal methods 130 131 def _get_panels(self, req): 132 """Return a list of available preference panels.""" 133 panels = [] 134 providers = {} 135 for provider in self.panel_providers: 136 p = list(provider.get_preference_panels(req) or []) 137 for panel in p: 138 providers[panel[0]] = provider 139 panels += p 140 141 return panels, providers 142 143 def _do_save_xhr(self, req): 144 for key in req.args: 145 if key not in ('save_prefs', 'panel_id', '__FORM_TOKEN'): 146 req.session[key] = req.args[key] 147 req.session.save() 148 req.send_no_content() 149 150 151class AdvancedPreferencePanel(Component): 152 153 implements(IPreferencePanelProvider) 154 155 _form_fields = ('newsid',) 156 157 # IPreferencePanelProvider methods 158 159 def get_preference_panels(self, req): 160 if not req.is_authenticated: 161 yield 'advanced', _("Advanced") 162 163 def render_preference_panel(self, req, panel): 164 if req.method == 'POST': 165 if 'restore' in req.args: 166 self._do_load(req) 167 else: 168 _do_save(req, panel, self._form_fields) 169 return 'prefs_advanced.html', {'session_id': req.session.sid} 170 171 def _do_load(self, req): 172 if not req.is_authenticated: 173 oldsid = req.args.get('loadsid') 174 if oldsid: 175 req.session.get_session(oldsid) 176 add_notice(req, _("The session has been loaded.")) 177 178 179class GeneralPreferencePanel(Component): 180 181 implements(IPreferencePanelProvider) 182 183 _form_fields = ('name', 'email') 184 185 # IPreferencePanelProvider methods 186 187 def get_preference_panels(self, req): 188 yield None, _("General") 189 190 def render_preference_panel(self, req, panel): 191 if req.method == 'POST': 192 _do_save(req, panel, self._form_fields) 193 return 'prefs_general.html', {} 194 195 196class LocalizationPreferencePanel(Component): 197 198 implements(IPreferencePanelProvider) 199 200 _form_fields = ('tz', 'lc_time', 'dateinfo', 'language') 201 202 # IPreferencePanelProvider methods 203 204 def get_preference_panels(self, req): 205 yield 'localization', _("Localization") 206 207 def render_preference_panel(self, req, panel): 208 if req.method == 'POST': 209 if Locale and \ 210 req.args.get('language') != req.session.get('language'): 211 # reactivate translations with new language setting 212 # when changed 213 del req.locale # for re-negotiating locale 214 deactivate() 215 make_activable(lambda: req.locale, self.env.path) 216 _do_save(req, panel, self._form_fields) 217 218 default_timezone_id = self.config.get('trac', 'default_timezone') 219 default_timezone = get_timezone(default_timezone_id) or localtz 220 default_time_format = \ 221 self.config.get('trac', 'default_dateinfo_format') or 'relative' 222 default_date_format = \ 223 self.config.get('trac', 'default_date_format') or 'locale' 224 225 data = { 226 'timezones': all_timezones, 227 'timezone': get_timezone, 228 'default_timezone': default_timezone, 229 'default_time_format': default_time_format, 230 'default_date_format': default_date_format, 231 'localtz': localtz, 232 'has_babel': False, 233 } 234 if Locale: 235 locale_ids = get_available_locales() 236 locales = [Locale.parse(locale) for locale in locale_ids] 237 # use locale identifiers from get_available_locales() instead 238 # of str(locale) to prevent storing expanded locale identifier 239 # to session, e.g. zh_Hans_CN and zh_Hant_TW, since Babel 1.0. 240 # see #11258. 241 languages = sorted((id_, locale.display_name) 242 for id_, locale in zip(locale_ids, locales)) 243 default_language_id = self.config.get('trac', 'default_language') 244 default_language = get_locale_name(default_language_id) or \ 245 _("Browser's language") 246 data['locales'] = locales 247 data['languages'] = languages 248 data['default_language'] = default_language 249 data['has_babel'] = True 250 return 'prefs_localization.html', data 251 252 253class UserInterfacePreferencePanel(Component): 254 255 implements(IPreferencePanelProvider) 256 257 _request_handlers = ExtensionPoint(IRequestHandler) 258 259 _form_fields = ('accesskeys', 'default_handler','ui.auto_preview_timeout', 260 'ui.hide_help', 'ui.use_symbols', 'wiki_fullwidth') 261 262 # IPreferencePanelProvider methods 263 264 def get_preference_panels(self, req): 265 yield 'userinterface', _("User Interface") 266 267 def render_preference_panel(self, req, panel): 268 if req.method == 'POST': 269 _do_save(req, panel, self._form_fields) 270 271 data = { 272 'project_default_handler': self._project_default_handler, 273 'valid_default_handlers': self._valid_default_handlers, 274 'default_auto_preview_timeout': self._auto_preview_timeout, 275 } 276 return 'prefs_userinterface.html', data 277 278 # Internal methods 279 280 @property 281 def _auto_preview_timeout(self): 282 return self.config.getfloat('trac', 'auto_preview_timeout') or 0 283 284 @property 285 def _project_default_handler(self): 286 return self.config.get('trac', 'default_handler') or 'WikiModule' 287 288 @lazy 289 def _valid_default_handlers(self): 290 return sorted(handler.__class__.__name__ 291 for handler in self._request_handlers 292 if is_valid_default_handler(handler)) 293 294 295def _do_save(req, panel, form_fields): 296 for field in form_fields: 297 val = req.args.get(field, '').strip() 298 if val: 299 if field == 'ui.auto_preview_timeout': 300 fval = as_float(val, default=None) 301 if fval is None or math.isinf(fval) or math.isnan(fval) \ 302 or fval < 0: 303 add_warning(req, _("Discarded invalid value \"%(val)s\" " 304 "for auto preview timeout.", val=val)) 305 continue 306 if field == 'tz' and 'tz' in req.session and \ 307 val not in all_timezones: 308 del req.session[field] 309 elif field == 'newsid': 310 req.session.change_sid(val) 311 else: 312 req.session[field] = val 313 elif (field in req.args or field + '_cb' in req.args) and \ 314 field in req.session: 315 del req.session[field] 316 add_notice(req, _("Your preferences have been saved.")) 317 req.redirect(req.href.prefs(panel)) 318