PK …F6Æé´[[webadmin/web_ui.py# -*- coding: utf-8 -*- # # Copyright (C) 2005-2006 Edgewall Software # Copyright (C) 2005 Jonas Borgström # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Jonas Borgström import re from trac.core import * from trac.perm import IPermissionRequestor from trac.util import Markup from trac.web import IRequestHandler from trac.web.chrome import add_stylesheet, INavigationContributor, \ ITemplateProvider from trac.web.href import Href __all__ = ['IAdminPageProvider'] class IAdminPageProvider(Interface): """ Extension point interface for adding pages to the admin module. """ def get_admin_pages(self, req): """ Return a list of available admin pages. The pages returned by this function must be a tuple of the form (category, category_label, page, page_label). """ def process_admin_request(self, req, category, page, path_info): """ Process the request for the admin `page`. This function should return a tuple of the form (template, content_type) where `template` is the ClearSilver template to use (either a `neo_cs.CS` object, or the file name of the template) and `content_type` is the MIME type of the content. If `content_type` is `None`, "text/html" is assumed. """ class AdminModule(Component): implements(INavigationContributor, IRequestHandler, ITemplateProvider) page_providers = ExtensionPoint(IAdminPageProvider) # INavigationContributor methods def get_active_navigation_item(self, req): return 'admin' def get_navigation_items(self, req): """The 'Admin' navigation item is only visible if at least one admin page is available.""" pages, providers = self._get_pages(req) if pages: yield 'mainnav', 'admin', Markup(u'管ç†', self.env.href.admin()) # IRequestHandler methods def match_request(self, req): match = re.match('/admin(?:/([^/]+))?(?:/([^/]+))?(?:/(.*)$)?', req.path_info) if match: req.args['cat_id'] = match.group(1) req.args['page_id'] = match.group(2) req.args['path_info'] = match.group(3) return True def _get_pages(self, req): """Return a list of available admin pages.""" pages = [] providers = {} for provider in self.page_providers: p = list(provider.get_admin_pages(req)) for page in p: providers[(page[0], page[2])] = provider pages += p pages.sort() return pages, providers def process_request(self, req): pages, providers = self._get_pages(req) if not pages: raise TracError('No admin pages available') cat_id = req.args.get('cat_id') or pages[0][0] page_id = req.args.get('page_id') path_info = req.args.get('path_info') if not page_id: page_id = filter(lambda page: page[0] == cat_id, pages)[0][2] provider = providers.get((cat_id, page_id), None) if not provider: raise TracError('Unknown Admin Page') template, content_type = provider.process_admin_request(req, cat_id, page_id, path_info) req.hdf['admin.pages'] = [{'cat_id': page[0], 'cat_label': page[1], 'page_id': page[2], 'page_label': page[3], 'href': self.env.href.admin(page[0], page[2]) } for page in pages] req.hdf['admin.active_cat'] = cat_id req.hdf['admin.active_page'] = page_id if isinstance(template, basestring): req.hdf['admin.page_template'] = template else: req.hdf['admin.page_content'] = Markup(template.render()) add_stylesheet(req, 'admin/css/admin.css') return 'admin.cs', content_type # ITemplateProvider def get_htdocs_dirs(self): """Return the absolute path of a directory containing additional static resources (such as images, style sheets, etc). """ from pkg_resources import resource_filename return [('admin', resource_filename(__name__, 'htdocs'))] def get_templates_dirs(self): """Return the absolute path of the directory containing the provided ClearSilver templates. """ from pkg_resources import resource_filename return [resource_filename(__name__, 'templates')] PK“¸*4Åo'P  webadmin/__init__.py# -*- coding: utf-8 -*- # # Copyright (C) 2006 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. from webadmin.web_ui import * PK …F6˜ê8&$&$webadmin/plugin.py# -*- coding: utf-8 -*- # # Copyright (C) 2005-2006 Edgewall Software # Copyright (C) 2005-2006 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Christopher Lenz import email import inspect import os import shutil import sys from trac import ticket, __version__ as TRAC_VERSION from trac.core import * from webadmin.web_ui import IAdminPageProvider try: import pkg_resources except ImportError: pkg_resources = None def _find_base_path(path, module_name): base_path = os.path.splitext(path)[0] while base_path.replace(os.sep, '.').endswith(module_name): base_path = os.path.dirname(base_path) module_name = '.'.join(module_name.split('.')[:-1]) if not module_name: break return base_path TRAC_PATH = _find_base_path(sys.modules['trac.core'].__file__, 'trac.core') # Ideally, this wouldn't be hard-coded like this required_components = ('AboutModule', 'DefaultPermissionGroupProvider', 'Environment', 'EnvironmentSetup', 'PermissionSystem', 'RequestDispatcher', 'Mimeview', 'Chrome') class PluginAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('general', '一般', 'plugin', 'プラグイン') def process_admin_request(self, req, cat, page, _): req.perm.assert_permission('TRAC_ADMIN') if req.method == 'POST': if req.args.has_key('update'): self._do_update(req) elif req.args.has_key('install'): self._do_install(req) elif req.args.has_key('uninstall'): self._do_uninstall(req) else: self.log.warning('Unknown POST request: %s', req.args) anchor = '' if req.args.has_key('plugin'): anchor = '#no' + req.args.get('plugin') req.redirect(self.env.href.admin(cat, page) + anchor) self._render_view(req) return 'admin_plugin.cs', None # Internal methods def _do_install(self, req): """Install a plugin.""" if not req.args.has_key('plugin_file'): raise TracError, 'No file uploaded' upload = req.args['plugin_file'] if not upload.filename: raise TracError, 'No file uploaded' plugin_filename = upload.filename.replace('\\', '/').replace(':', '/') plugin_filename = os.path.basename(plugin_filename) if not plugin_filename: raise TracError, 'No file uploaded' if not plugin_filename.endswith('.egg') and \ not plugin_filename.endswith('.py'): raise TracError, 'Uploaded file is not a Python source file or egg' target_path = os.path.join(self.env.path, 'plugins', plugin_filename) if os.path.isfile(target_path): raise TracError, 'Plugin %s already installed' % plugin_filename self.log.info('Installing plugin %s', plugin_filename) flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL try: flags += os.O_BINARY except AttributeError: # OS_BINARY not available on every platform pass target_file = os.fdopen(os.open(target_path, flags), 'w') try: shutil.copyfileobj(upload.file, target_file) self.log.info('Plugin %s installed to %s', plugin_filename, target_path) finally: target_file.close() # TODO: Validate that the uploaded file is actually a valid Trac plugin def _do_uninstall(self, req): """Uninstall a plugin.""" plugin_filename = req.args.get('plugin_filename') if not plugin_filename: return plugin_path = os.path.join(self.env.path, 'plugins', plugin_filename) if not os.path.isfile(plugin_path): return self.log.info('Uninstalling plugin %s', plugin_filename) os.remove(plugin_path) def _do_update(self, req): """Update component enablement.""" components = req.args.getlist('component') enabled = req.args.getlist('enable') changes = False # FIXME: this needs to be more intelligent and minimize multiple # component names to prefix rules for component in components: is_enabled = self.env.is_component_enabled(component) if is_enabled != (component in enabled): self.config.set('components', component, is_enabled and 'disabled' or 'enabled') self.log.info('%sabling component %s', is_enabled and 'Dis' or 'En', component) changes = True if changes: self.config.save() def _render_view(self, req): plugins = {} plugins_dir = os.path.realpath(os.path.join(self.env.path, 'plugins')) from trac.core import ComponentMeta for component in ComponentMeta._components: module = sys.modules[component.__module__] dist = self._find_distribution(module) plugin_filename = None if os.path.realpath(os.path.dirname(dist.location)) == plugins_dir: plugin_filename = os.path.basename(dist.location) description = inspect.getdoc(component) if description: description = description.split('.', 1)[0] + '.' if dist.project_name not in plugins: readonly = True if plugin_filename and os.access(dist.location, os.F_OK + os.W_OK): readonly = False plugins[dist.project_name] = { 'name': dist.project_name, 'version': dist.version, 'path': dist.location, 'description': description, 'plugin_filename': plugin_filename, 'readonly': readonly, 'info': self._get_pkginfo(dist), 'components': [] } plugins[dist.project_name]['components'].append({ 'name': component.__name__, 'module': module.__name__, 'description': description, 'enabled': self.env.is_component_enabled(component), 'required': component.__name__ in required_components, }) def component_order(a, b): c = cmp(len(a['module'].split('.')), len(b['module'].split('.'))) if c == 0: c = cmp(a['module'].lower(), b['module'].lower()) if c == 0: c = cmp(a['name'].lower(), b['name'].lower()) return c for category in plugins: plugins[category]['components'].sort(component_order) req.hdf['title'] = 'Manage Plugins' req.hdf['admin.plugins.0'] = plugins['Trac'] addons = [key for key in plugins.keys() if key != 'Trac'] addons.sort() for idx, category in enumerate(addons): req.hdf['admin.plugins.%s' % (idx + 1)] = plugins[category] if not os.access(plugins_dir, os.F_OK + os.W_OK): req.hdf['admin.readonly'] = True def _find_distribution(self, module): # Determine the plugin that this component belongs to path = module.__file__ if path.endswith('.pyc') or path.endswith('.pyo'): path = path[:-1] if os.path.basename(path) == '__init__.py': path = os.path.dirname(path) path = _find_base_path(path, module.__name__) if path == TRAC_PATH: return pkg_resources.Distribution(project_name='Trac', version=TRAC_VERSION, location=path) for dist in pkg_resources.find_distributions(path, only=True): return dist else: # This is a plain Python source file, not an egg return pkg_resources.Distribution(project_name=module.__name__, version='', location=module.__file__) def _get_pkginfo(self, dist): attrs = ('author', 'author-email', 'license', 'home-page', 'summary', 'description') info = {} try: pkginfo = email.message_from_string(dist.get_metadata('PKG-INFO')) for attr in [key for key in attrs if key in pkginfo]: info[attr.lower().replace('-', '_')] = pkginfo[attr] except email.Errors.MessageError, e: self.log.warning('Failed to parse PKG-INFO file for %s: %s', dist, e, exc_info=True) return info PK …F6ž™/vXXwebadmin/logging.py# -*- coding: utf-8 -*- # # Copyright (C) 2005-2006 Edgewall Software # Copyright (C) 2005-2006 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Christopher Lenz import os from trac.core import * from webadmin.web_ui import IAdminPageProvider class LoggingAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('general', '一般', 'logging', 'ログ') def process_admin_request(self, req, cat, page, path_info): log_type = self.config.get('logging', 'log_type') log_level = self.config.get('logging', 'log_level').upper() log_file = self.config.get('logging', 'log_file', 'trac.log') log_dir = os.path.join(self.env.path, 'log') log_types = [ dict(name='', label=''), dict(name='stderr', label='Console', selected=log_type == 'stderr'), dict(name='file', label='File', selected=log_type == 'file'), dict(name='syslog', label='Syslog', disabled=os.name != 'posix', selected=log_type in ('unix', 'syslog')), dict(name='eventlog', label='Windows event log', disabled=os.name != 'nt', selected=log_type in ('winlog', 'eventlog', 'nteventlog')), ] log_levels = ['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'] if req.method == 'POST': changed = False new_type = req.args.get('log_type') if new_type and new_type not in ('stderr', 'file', 'syslog', 'eventlog'): raise TracError('Unknown log type %s' % new_type, 'Invalid log type') if new_type != log_type: self.config.set('logging', 'log_type', new_type or 'none') changed = True log_type = new_type if log_type: new_level = req.args.get('log_level') if new_level and new_level not in log_levels: raise TracError('Unknown log level %s' % new_level, 'Invalid log level') if new_level and new_level != log_level: self.config.set('logging', 'log_level', new_level) changed = True log_evel = new_level else: self.config.remove('logging', 'log_level') changed = True if log_type == 'file': new_file = req.args.get('log_file', 'trac.log') if new_file != log_file: self.config.set('logging', 'log_file', new_file or '') changed = True log_file = new_file if log_type == 'file' and not log_file: raise TracError('You must specify a log file', 'Missing field') else: self.config.remove('logging', 'log_file') changed = True if changed: self.config.save() req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.log'] = {'type': log_type, 'types': log_types, 'level': log_level, 'levels': log_levels, 'file': log_file, 'dir': log_dir} return 'admin_log.cs', None PK …F6Xºl l webadmin/perm.py# -*- coding: utf-8 -*- # # Copyright (C) 2005-2006 Edgewall Software # Copyright (C) 2005 Jonas Borgström # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Jonas Borgström import re from trac.core import * from trac.perm import PermissionSystem from webadmin.web_ui import IAdminPageProvider class PermissionAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider def get_admin_pages(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('general', '一般', 'perm', 'パーミッション') def process_admin_request(self, req, cat, page, path_info): perm = PermissionSystem(self.env) perms = perm.get_all_permissions() subject = req.args.get('subject') action = req.args.get('action') group = req.args.get('group') if req.method == 'POST': # Grant permission to subject if req.args.get('add') and subject and action: if action not in perm.get_actions(): raise TracError('Unknown action') perm.grant_permission(subject, action) req.redirect(self.env.href.admin(cat, page)) # Add subject to group elif req.args.get('add') and subject and group: perm.grant_permission(subject, group) req.redirect(self.env.href.admin(cat, page)) # Remove permissions action elif req.args.get('remove') and req.args.get('sel'): sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] for key in sel: subject, action = key.split(':', 1) if (subject, action) in perms: perm.revoke_permission(subject, action) req.redirect(self.env.href.admin(cat, page)) perms.sort(lambda a, b: cmp(a[0], b[0])) req.hdf['admin.actions'] = perm.get_actions() req.hdf['admin.perms'] = [{'subject': p[0], 'action': p[1], 'key': '%s:%s' % p } for p in perms] return 'admin_perm.cs', None PK …F6–%EGÆ?Æ?webadmin/ticket.py# -*- coding: utf-8 -*- # # Copyright (C) 2005-2006 Edgewall Software # Copyright (C) 2005 Jonas Borgström # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Jonas Borgström import time from trac import ticket from trac import util from trac.core import * from trac.perm import IPermissionRequestor, PermissionSystem from webadmin.web_ui import IAdminPageProvider class ComponentAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TICKET_ADMIN'): yield ('ticket', 'ãƒã‚±ãƒƒãƒˆ', 'components', 'コンãƒãƒ¼ãƒãƒ³ãƒˆ') def process_admin_request(self, req, cat, page, component): req.perm.assert_permission('TICKET_ADMIN') # Detail view? if component: comp = ticket.Component(self.env, component) if req.method == 'POST': if req.args.get('save'): comp.name = req.args.get('name') comp.owner = req.args.get('owner') comp.description = req.args.get('description') comp.update() req.redirect(self.env.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.component'] = { 'name': comp.name, 'owner': comp.owner, 'description': comp.description } else: if req.method == 'POST': # Add Component if req.args.get('add') and req.args.get('name'): comp = ticket.Component(self.env) comp.name = req.args.get('name') if req.args.get('owner'): comp.owner = req.args.get('owner') comp.insert() req.redirect(self.env.href.admin(cat, page)) # Remove components elif req.args.get('remove') and req.args.get('sel'): sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] if not sel: raise TracError, 'No component selected' db = self.env.get_db_cnx() for name in sel: comp = ticket.Component(self.env, name, db=db) comp.delete(db=db) db.commit() req.redirect(self.env.href.admin(cat, page)) # Set default component elif req.args.get('apply'): if req.args.get('default'): name = req.args.get('default') self.log.info('Setting default component to %s', name) self.config.set('ticket', 'default_component', name) self.config.save() req.redirect(self.env.href.admin(cat, page)) default = self.config.get('ticket', 'default_component') req.hdf['admin.components'] = \ [{'name': c.name, 'owner': c.owner, 'is_default': c.name == default, 'href': self.env.href.admin(cat, page, c.name) } for c in ticket.Component.select(self.env)] if self.config.getbool('ticket', 'restrict_owner'): perm = PermissionSystem(self.env) def valid_owner(username): return perm.get_user_permissions(username).get('TICKET_MODIFY') req.hdf['admin.owners'] = [username for username, name, email in self.env.get_known_users() if valid_owner(username)] return 'admin_component.cs', None class MilestoneAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TICKET_ADMIN'): yield ('ticket', 'ãƒã‚±ãƒƒãƒˆ', 'milestones', 'マイルストーン') def process_admin_request(self, req, cat, page, milestone): req.perm.assert_permission('TICKET_ADMIN') # Detail view? if milestone: mil = ticket.Milestone(self.env, milestone) if req.method == 'POST': if req.args.get('save'): mil.name = req.args.get('name') due = req.args.get('duedate', '') try: mil.due = due and util.parse_date(due) or 0 except ValueError, e: raise TracError(e, 'Invalid Date Format') if 'completed' in req.args: completed = req.args.get('completeddate', '') try: mil.completed = completed and \ util.parse_date(completed) or 0 except ValueError, e: raise TracError(e, 'Invalid Date Format') if mil.completed > time.time(): raise TracError('Completion date may not be in the ' 'future', 'Invalid Completion Date') else: mil.completed = 0 mil.description = req.args.get('description', '') mil.update() req.redirect(self.env.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.milestone'] = { 'name': mil.name, 'duedate': mil.due and util.format_datetime(mil.due) or '', 'description': mil.description } else: if req.method == 'POST': # Add Milestone if req.args.get('add') and req.args.get('name'): mil = ticket.Milestone(self.env) mil.name = req.args.get('name') if req.args.get('duedate'): mil.due = util.parse_date(req.args.get('duedate')) mil.insert() req.redirect(self.env.href.admin(cat, page)) # Remove milestone elif req.args.get('remove') and req.args.get('sel'): sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] if not sel: raise TracError, 'No milestone selected' db = self.env.get_db_cnx() for name in sel: mil = ticket.Milestone(self.env, name, db=db) mil.delete(db=db) db.commit() req.redirect(self.env.href.admin(cat, page)) # Set default milestone elif req.args.get('apply'): if req.args.get('default'): name = req.args.get('default') self.log.info('Setting default milestone to %s', name) self.config.set('ticket', 'default_milestone', name) self.config.save() req.redirect(self.env.href.admin(cat, page)) default = self.config.get('ticket', 'default_milestone') req.hdf['admin.milestones'] = \ [{'name': m.name, 'duedate': m.due and util.format_datetime(m.due) or '', 'is_default': m.name == default, 'href': self.env.href.admin(cat, page, m.name) } for m in ticket.Milestone.select(self.env)] req.hdf['admin.date_hint'] = util.get_date_format_hint() req.hdf['admin.datetime_hint'] = util.get_datetime_format_hint() req.hdf['admin.datetime_now'] = util.format_datetime() return 'admin_milestone.cs', None class VersionAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TICKET_ADMIN'): yield ('ticket', 'ãƒã‚±ãƒƒãƒˆ', 'versions', 'ãƒãƒ¼ã‚¸ãƒ§ãƒ³') def process_admin_request(self, req, cat, page, version): req.perm.assert_permission('TICKET_ADMIN') # Detail view? if version: ver = ticket.Version(self.env, version) if req.method == 'POST': if req.args.get('save'): ver.name = req.args.get('name') if req.args.get('time'): ver.time = util.parse_date(req.args.get('time')) ver.description = req.args.get('description') ver.update() req.redirect(self.env.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.version'] = { 'name': ver.name, 'time': ver.time and util.format_datetime(ver.time) or '', 'description': ver.description } else: if req.method == 'POST': # Add Version if req.args.get('add') and req.args.get('name'): ver = ticket.Version(self.env) ver.name = req.args.get('name') if req.args.get('time'): ver.time = util.parse_date(req.args.get('time')) ver.insert() req.redirect(self.env.href.admin(cat, page)) # Remove versions elif req.args.get('remove') and req.args.get('sel'): sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] if not sel: raise TracError, 'No version selected' db = self.env.get_db_cnx() for name in sel: ver = ticket.Version(self.env, name, db=db) ver.delete(db=db) db.commit() req.redirect(self.env.href.admin(cat, page)) # Set default version elif req.args.get('apply'): if req.args.get('default'): name = req.args.get('default') self.log.info('Setting default version to %s', name) self.config.set('ticket', 'default_version', name) self.config.save() req.redirect(self.env.href.admin(cat, page)) default = self.config.get('ticket', 'default_version') req.hdf['admin.versions'] = \ [{'name': v.name, 'time': v.time and util.format_datetime(v.time) or '', 'is_default': v.name == default, 'href': self.env.href.admin(cat, page, v.name) } for v in ticket.Version.select(self.env)] req.hdf['admin.date_hint'] = util.get_date_format_hint() req.hdf['admin.datetime_hint'] = util.get_datetime_format_hint() req.hdf['admin.datetime_now'] = util.format_datetime() return 'admin_version.cs', None class AbstractEnumAdminPage(Component): implements(IAdminPageProvider) abstract = True _type = 'unknown' _enum_cls = None _label = ('(Undefined)', '(Undefined)') # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TICKET_ADMIN'): yield ('ticket', 'ãƒã‚±ãƒƒãƒˆ', self._type, self._label[1]) def process_admin_request(self, req, cat, page, path_info): req.perm.assert_permission('TICKET_ADMIN') req.hdf['admin.enum'] = { 'label_singular': self._label[0], 'label_plural': self._label[1] } # Detail view? if path_info: enum = self._enum_cls(self.env, path_info) if req.method == 'POST': if req.args.get('save'): enum.name = req.args.get('name') enum.update() req.redirect(self.env.href.admin(cat, page)) elif req.args.get('cancel'): req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.enum'] = { 'name': enum.name, 'value': enum.value } else: default = self.config.get('ticket', 'default_%s' % self._type) if req.method == 'POST': # Add enum if req.args.get('add') and req.args.get('name'): enum = self._enum_cls(self.env) enum.name = req.args.get('name') enum.insert() req.redirect(self.env.href.admin(cat, page)) # Remove enums elif req.args.get('remove') and req.args.get('sel'): sel = req.args.get('sel') sel = isinstance(sel, list) and sel or [sel] if not sel: raise TracError, 'No enum selected' db = self.env.get_db_cnx() for name in sel: enum = self._enum_cls(self.env, name, db=db) enum.delete(db=db) db.commit() req.redirect(self.env.href.admin(cat, page)) # Appy changes elif req.args.get('apply'): # Set default value if req.args.get('default'): name = req.args.get('default') if name != default: self.log.info('Setting default %s to %s', self._type, name) self.config.set('ticket', 'default_%s' % self._type, name) self.config.save() # Change enum values order = dict([(key[6:], req.args.get(key)) for key in req.args.keys() if key.startswith('value_')]) values = dict([(val, True) for val in order.values()]) if len(order) != len(values): raise TracError, 'Order numbers must be unique' db = self.env.get_db_cnx() for enum in self._enum_cls.select(self.env, db=db): new_value = order[enum.value] if new_value != enum.value: enum.value = new_value enum.update(db=db) db.commit() req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.enums'] = [ {'name': e.name, 'value': e.value, 'is_default': e.name == default, 'href': self.env.href.admin(cat, page, e.name) } for e in self._enum_cls.select(self.env)] return 'admin_enum.cs', None class PriorityAdminPage(AbstractEnumAdminPage): _type = 'priority' _enum_cls = ticket.Priority _label = ('優先度', '優先度') class SeverityAdminPage(AbstractEnumAdminPage): _type = 'severity' _enum_cls = ticket.Severity _label = ('é‡è¦åº¦', 'é‡è¦åº¦') class TicketTypeAdminPage(AbstractEnumAdminPage): _type = 'type' _enum_cls = ticket.Type _label = ('分類', '分類') PK …F6R÷ù77webadmin/basics.py# -*- coding: utf-8 -*- # # Copyright (C) 2005-2006 Edgewall Software # Copyright (C) 2005-2006 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Christopher Lenz from trac.core import * from webadmin.web_ui import IAdminPageProvider class ProjectAdminPage(Component): implements(IAdminPageProvider) # IAdminPageProvider methods def get_admin_pages(self, req): if req.perm.has_permission('TRAC_ADMIN'): yield ('general', '一般', 'basic', '基本設定') def process_admin_request(self, req, cat, page, path_info): if req.method == "POST": self.config.set('project', 'name', req.args.get('name')) self.config.set('project', 'url', req.args.get('url')) self.config.set('project', 'descr', req.args.get('description')) self.config.save() req.redirect(self.env.href.admin(cat, page)) req.hdf['admin.project'] = { 'name': self.config.get('project', 'name'), 'description': self.config.get('project', 'descr'), 'url': self.config.get('project', 'url') } return 'admin_basics.cs', None PK †F6ç˜Illwebadmin/web_ui.pyc;ò ã0ÈEc@s–dkZdkTdklZdklZdklZdkl Z l Z l Z dk l Z dgZdefd„ƒYZd efd „ƒYZdS( N(s*(sIPermissionRequestor(sMarkup(sIRequestHandler(sadd_stylesheetsINavigationContributorsITemplateProvider(sHrefsIAdminPageProvidercBs tZdZd„Zd„ZRS(sI Extension point interface for adding pages to the admin module. cCsdS(s· Return a list of available admin pages. The pages returned by this function must be a tuple of the form (category, category_label, page, page_label). N((sselfsreq((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysget_admin_pages#scCsdS(s‰ Process the request for the admin `page`. This function should return a tuple of the form (template, content_type) where `template` is the ClearSilver template to use (either a `neo_cs.CS` object, or the file name of the template) and `content_type` is the MIME type of the content. If `content_type` is `None`, "text/html" is assumed. N((sselfsreqscategoryspages path_info((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysprocess_admin_request*s(s__name__s __module__s__doc__sget_admin_pagessprocess_admin_request(((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysIAdminPageProviders  s AdminModulecBsctZeeeeƒeeƒZd„Z d„Z d„Z d„Z d„Z d„Zd„ZRS(NcCsdSdS(Nsadmin((sselfsreq((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysget_active_navigation_item<sccsF|i|ƒ\}}|o&ddtd|iiiƒƒfVndS(s_The 'Admin' navigation item is only visible if at least one admin page is available.smainnavsadminu管ç†N( sselfs _get_pagessreqspagess providerssMarkupsenvshrefsadmin(sselfsreqspagess providers((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysget_navigation_items?s  cCsjtid|iƒ}|oJ|idƒ|id<|idƒ|id<|idƒ|idesisUnknown Admin Pages cat_labelis page_labelishrefs admin.pagessadmin.active_catsadmin.active_pagesadmin.page_templatesadmin.page_contentsadmin/css/admin.csssadmin.cs(sselfs _get_pagessreqspagess providerss TracErrorsargssgetscat_idspage_ids path_infosfiltersNonesprovidersprocess_admin_requeststemplates content_typesappends_[1]spagesenvshrefsadminshdfs isinstances basestringsMarkupsrendersadd_stylesheet( sselfsreqs_[1]s content_types providersspages path_infostemplatesproviderscat_idspage_idspages((scat_ids5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysprocess_request]s,!! …   cCs'dkl}d|tdƒfgSdS(s„Return the absolute path of a directory containing additional static resources (such as images, style sheets, etc). (sresource_filenamesadminshtdocsN(s pkg_resourcessresource_filenames__name__(sselfsresource_filename((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysget_htdocs_dirs€s cCs!dkl}|tdƒgSdS(siReturn the absolute path of the directory containing the provided ClearSilver templates. (sresource_filenames templatesN(s pkg_resourcessresource_filenames__name__(sselfsresource_filename((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pysget_templates_dirs‡s (s__name__s __module__s implementssINavigationContributorsIRequestHandlersITemplateProvidersExtensionPointsIAdminPageProviderspage_providerssget_active_navigation_itemsget_navigation_itemss match_requests _get_pagessprocess_requestsget_htdocs_dirssget_templates_dirs(((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pys AdminModule5s    # (sres trac.cores trac.permsIPermissionRequestors trac.utilsMarkupstrac.websIRequestHandlerstrac.web.chromesadd_stylesheetsINavigationContributorsITemplateProviders trac.web.hrefsHrefs__all__s InterfacesIAdminPageProviders Components AdminModule( sINavigationContributorsITemplateProviders__all__sIRequestHandlersIPermissionRequestorsMarkupsIAdminPageProvidersresHrefs AdminModulesadd_stylesheet((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/web_ui.pys?s      PK †F6ߤ‚á­­webadmin/__init__.pyc;ò ÷¾ÃCc@s dkTdS((s*N(swebadmin.web_ui(((s7build/bdist.solaris-2.10-i86pc/egg/webadmin/__init__.pys?sPK †F6¦œ6&L'L'webadmin/plugin.pyc;ò ã0ÈEc@s×dkZdkZdkZdkZdkZdklZlZdk Tdk l Z y dk Z Wne j o eZ nXd„ZeeididƒZdddd d d d d fZdefd„ƒYZdS(N(stickets __version__(s*(sIAdminPageProvidercCs€tii|ƒd}x_|itidƒi|ƒo?tii|ƒ}di |i dƒd ƒ}| oPqqW|SdS(Nis.iÿÿÿÿ( sosspathssplitexts base_pathsreplacessepsendswiths module_namesdirnamesjoinssplit(spaths module_names base_path((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pys_find_base_path s s trac.cores AboutModulesDefaultPermissionGroupProviders EnvironmentsEnvironmentSetupsPermissionSystemsRequestDispatchersMimeviewsChromesPluginAdminPagecBsZtZeeƒd„Zd„Zd„Zd„Zd„Zd„Z d„Z d„Z RS( Nccs+|iidƒoddddfVndS(Ns TRAC_ADMINsgenerals一般spluginsプラグイン(sreqspermshas_permission(sselfsreq((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pysget_admin_pages7scCs|iidƒ|idjoÜ|iidƒo|i|ƒn_|iidƒo|i|ƒn;|iidƒo|i |ƒn|i i d|iƒd}|iidƒod |ii dƒ}n|i|iii||ƒ|ƒn|i|ƒd tfSdS( Ns TRAC_ADMINsPOSTsupdatesinstalls uninstallsUnknown POST request: %sssplugins#nosadmin_plugin.cs(sreqspermsassert_permissionsmethodsargsshas_keysselfs _do_updates _do_installs _do_uninstallslogswarningsanchorsgetsredirectsenvshrefsadminscatspages _render_viewsNone(sselfsreqscatspages_sanchor((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pysprocess_admin_request;s' cCs­|iidƒ o td‚n|id}|i o td‚n|iiddƒiddƒ}ti i |ƒ}| o td‚n|i dƒ o|i dƒ o td‚nti i |ii d |ƒ}ti i|ƒotd |‚n|iid |ƒtititi}y|ti7}Wntj onXtiti||ƒd ƒ}z-ti|i|ƒ|iid ||ƒWd|iƒXdS(sInstall a plugin.s plugin_filesNo file uploadeds\s/s:s.eggs.pys0Uploaded file is not a Python source file or eggspluginssPlugin %s already installedsInstalling plugin %sswsPlugin %s installed to %sN( sreqsargsshas_keys TracErrorsuploadsfilenamesreplacesplugin_filenamesosspathsbasenamesendswithsjoinsselfsenvs target_pathsisfileslogsinfosO_CREATsO_WRONLYsO_EXCLsflagssO_BINARYsAttributeErrorsfdopensopens target_filesshutils copyfileobjsfilesclose(sselfsreqs target_filesplugin_filenamesuploads target_pathsflags((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pys _do_installQs8    ! "  cCs€|iidƒ}| odSntii|iid|ƒ}tii |ƒ odSn|i i d|ƒti |ƒdS(sUninstall a plugin.splugin_filenameNspluginssUninstalling plugin %s(sreqsargssgetsplugin_filenamesosspathsjoinsselfsenvs plugin_pathsisfileslogsinfosremove(sselfsreqsplugin_filenames plugin_path((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pys _do_uninstalluscCsÎ|iidƒ}|iidƒ}t}x…|D]}}|i i |ƒ}|||jjoR|i i d||odpdƒ|iid|odpd|ƒt}q1q1W|o|i iƒnd S( sUpdate component enablement.s componentsenables componentssdisabledsenableds%sabling component %ssDissEnN(sreqsargssgetlists componentssenabledsFalseschangess componentsselfsenvsis_component_enableds is_enabledsconfigssetslogsinfosTruessave(sselfsreqs is_enableds componentsenableds componentsschanges((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pys _do_update€s cCsøh}tiitii|iidƒƒ} dkl } x½| i D]²} t i | i}|i|ƒ}t} tiitii|iƒƒ| jotii|iƒ} nti| ƒ}|o|iddƒdd}n|i|jo¢t}| oti|ititi ƒo t!}nhd|i<d|i"<d|i<d |<d | <d |<d |i#|ƒ<d g<||i  cCsï|i}|idƒp |idƒo|d }ntii|ƒdjotii|ƒ}nt||iƒ}|t jo t i dddt d|ƒSnxCt i |d tƒD] }|Sq»Wt i d|idd d|iƒSdS( Ns.pycs.pyoiÿÿÿÿs __init__.pys project_namesTracsversionslocationsonlys(smodules__file__spathsendswithsossbasenamesdirnames_find_base_paths__name__s TRAC_PATHs pkg_resourcess Distributions TRAC_VERSIONsfind_distributionssTruesdist(sselfsmodulesdistspath((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/plugin.pys_find_distributionÍs     c Csßddddddf}h}yti|idƒƒ}x`gi}|D]!}||jo||ƒqJqJ~D]&}||||i ƒi dd ƒ?ss admin.actionsiskeys%s:%ss admin.permss admin_perm.cs(!sPermissionSystemsselfsenvspermsget_all_permissionsspermssreqsargssgetssubjectsactionsgroupsmethods get_actionss TracErrorsgrant_permissionsredirectshrefsadminscatspagessels isinstanceslistskeyssplitsrevoke_permissionssortshdfsappends_[1]spsNone(sselfsreqscatspages path_infosgroupspermssperms_[1]spskeysactionsselssubject((s3build/bdist.solaris-2.10-i86pc/egg/webadmin/perm.pysprocess_admin_request!s6 !#!#& 'U(s__name__s __module__s implementssIAdminPageProvidersget_admin_pagessprocess_admin_request(((s3build/bdist.solaris-2.10-i86pc/egg/webadmin/perm.pysPermissionAdminPages  (sres trac.cores trac.permsPermissionSystemswebadmin.web_uisIAdminPageProviders ComponentsPermissionAdminPage(sresPermissionSystemsPermissionAdminPagesIAdminPageProvider((s3build/bdist.solaris-2.10-i86pc/egg/webadmin/perm.pys?s   PK †F6@6ú<<webadmin/ticket.pyc;ò ã0ÈEc@sèdkZdklZdklZdkTdklZlZdkl Z de fd„ƒYZ de fd „ƒYZ d e fd „ƒYZ d e fd „ƒYZdefd„ƒYZdefd„ƒYZdefd„ƒYZdS(N(sticket(sutil(s*(sIPermissionRequestorsPermissionSystem(sIAdminPageProvidersComponentAdminPagecBs$tZeeƒd„Zd„ZRS(Nccs+|iidƒoddddfVndS(Ns TICKET_ADMINstickets ãƒã‚±ãƒƒãƒˆs componentssコンãƒãƒ¼ãƒãƒ³ãƒˆ(sreqspermshas_permission(sselfsreq((s5build/bdist.solaris-2.10-i86pc/egg/webadmin/ticket.pysget_admin_pages sc s|iidƒ|oti|i|ƒ} |i djo¹|i i dƒol|i i dƒ| _ |i i dƒ| _ |i i dƒ| _| iƒ|i|iii||ƒƒqõ|i i dƒo#|i|iii||ƒƒqõnhd| i <d| i <d| i<|id[ rŠÀÒÿÞIEND®B`‚PKn+ô2zae!webadmin/htdocs/js/admin.jsfunction toggleClass(element, class1, class2) { function indexOf(array, obj) { if (obj) { for (var i = 0; i < array.length; i++) { if (array[i] == obj) return i; } } return array.length; } var classNames = element.className.split(/\s+/) || []; var classIndex = indexOf(classNames, class1); if (classIndex >= classNames.length) { classIndex = indexOf(classNames, class2); var tmp = class1; class1 = class2; class2 = tmp; } classNames.splice(classIndex, class1 ? 1 : 0, class2); element.className = classNames.join(' '); } var fragmentId = document.location.hash; if (fragmentId) { fragmentId = fragmentId.substr(1); } function enableFolding(triggerId) { var trigger = document.getElementById(triggerId); if (!trigger) return; toggleClass(trigger.parentNode, triggerId != fragmentId ? "collapsed" : "expanded"); var link = document.createElement("a"); link.href = "#" + triggerId; trigger.parentNode.replaceChild(link, trigger); link.appendChild(trigger); trigger.style.cursor = "pointer"; addEvent(link, "click", function() { toggleClass(link.parentNode, "expanded", "collapsed"); }); } PKÌ…F6-Й   #webadmin/templates/admin_version.cs

ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ç®¡ç†

ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®ç·¨é›†:

ãƒãƒ¼ã‚¸ãƒ§ãƒ³ã®è¿½åŠ :
 åç§° 完了期é™ãƒ‡ãƒ•ォルト
checked="checked" >

You can remove all items from this list to completely hide this field from the user interface.

As long as you don't add any items to the list, this field will remain completely hidden from the user interface.

PK …F6΄IÓÓ"webadmin/templates/admin_basics.cs

基本設定

プロジェクト
PK …F6Q{od33 webadmin/templates/admin_perm.cs

パーミッションã®ç®¡ç†

パーミッションを追加ã™ã‚‹:

Grant permission for an action to a subject, which can be either a user or a group.

ユーザをグループã«è¿½åŠ :

Add a user or group to an existing permission group.

 ãƒ¦ãƒ¼ã‚¶æ¨©é™
PKÂ…F6æHß  %webadmin/templates/admin_milestone.cs

マイルストーンã®ç®¡ç†

マイルストーンã®ç·¨é›†:

マイルストーンã®è¿½åŠ :
 ãƒžã‚¤ãƒ«ã‚¹ãƒˆãƒ¼ãƒ³åç§° 完了期é™ãƒ‡ãƒ•ォルト
checked="checked" >

You can remove all items from this list to completely hide this field from the user interface.

As long as you don't add any items to the list, this field will remain completely hidden from the user interface.

PK²…F6á ÞXáá webadmin/templates/admin_enum.cs

ã®ç®¡ç†

ã®ç·¨é›†
ã®è¿½åŠ 
 åç§° デフォルト順åº
checked="checked" />

You can remove all items from this list to completely hide this field from the user interface.

As long as you don't add any items to the list, this field will remain completely hidden from the user interface.

PK …F6ƒºL-´ ´ "webadmin/templates/admin_plugin.cs

プラグインã®ç®¡ç†

プラグインã®ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«:

The web server does not have sufficient permissions to store files in the environment plugins directory. Upload a plugin packaged as Python egg.

disabled="disabled" />

disabled="disabled" />

作者:
ホームページ:
ライセンス:
コンãƒãƒ¼ãƒãƒ³ãƒˆæœ‰åй

checked="checked" disabled="disabled" />
PK …F6fƲ²webadmin/templates/admin.cs

管ç†

class="active">

  • PKª…F6ñ#â›+ + %webadmin/templates/admin_component.cs

    コンãƒãƒ¼ãƒãƒ³ãƒˆã®ç®¡ç†

    コンãƒãƒ¼ãƒãƒ³ãƒˆã®ç·¨é›†

    コンãƒãƒ¼ãƒãƒ³ãƒˆã®è¿½åŠ :
     åå‰ ã‚ªãƒ¼ãƒŠãƒ¼ãƒ‡ãƒ•ã‚©ãƒ«ãƒˆ
    checked="checked" >

    You can remove all items from this list to completely hide this field from the user interface.

    As long as you don't add any items to the list, this field will remain completely hidden from the user interface.

    PK …F6D⟊Šwebadmin/templates/admin_log.cs

    ログã®è¨­å®š

    If you specify a relative path, the log file will be stored inside the log directory of the project environment ().

    You may need to restart the server for these changes to take effect.

    PK †F6‘OúðEGG-INFO/PKG-INFOMetadata-Version: 1.0 Name: TracWebAdmin Version: 0.1.2dev Summary: Web interface for administration of Trac Home-page: http://projects.edgewall.com/trac/wiki/WebAdmin Author: Edgewall Software Author-email: info@edgewall.com License: BSD Description: UNKNOWN Platform: UNKNOWN PK †F6úÀýPPEGG-INFO/SOURCES.txtsetup.cfg setup.py TracWebAdmin.egg-info/PKG-INFO TracWebAdmin.egg-info/SOURCES.txt TracWebAdmin.egg-info/dependency_links.txt TracWebAdmin.egg-info/entry_points.txt TracWebAdmin.egg-info/top_level.txt webadmin/__init__.py webadmin/basics.py webadmin/logging.py webadmin/perm.py webadmin/plugin.py webadmin/ticket.py webadmin/web_ui.py PK †F6“×2EGG-INFO/dependency_links.txt PK †F6º 8¦ÚÚEGG-INFO/entry_points.txt[trac.plugins] webadmin.logging = webadmin.logging webadmin.ticket = webadmin.ticket webadmin.basics = webadmin.basics webadmin.perm = webadmin.perm webadmin.web_ui = webadmin.web_ui webadmin.plugin = webadmin.plugin PK †F6•4Ì EGG-INFO/top_level.txtwebadmin PK †F6“×2EGG-INFO/not-zip-safe PK …F6Æé´[[¤webadmin/web_ui.pyPK“¸*4Åo'P  ¤‹webadmin/__init__.pyPK …F6˜ê8&$&$¤Èwebadmin/plugin.pyPK …F6ž™/vXX¤;webadmin/logging.pyPK …F6Xºl l ¤§Jwebadmin/perm.pyPK …F6–%EGÆ?Æ?¤AUwebadmin/ticket.pyPK …F6R÷ù77¤7•webadmin/basics.pyPK †F6ç˜Ill¤ž›webadmin/web_ui.pycPK †F6ߤ‚á­­¤;·webadmin/__init__.pycPK †F6¦œ6&L'L'¤¸webadmin/plugin.pycPK †F64òƒ&Û Û ¤˜ßwebadmin/logging.pycPK †F6žK“Nî î ¤¥ìwebadmin/perm.pycPK †F6@6ú<<¤Â÷webadmin/ticket.pycPK †F6ljJ””¤4webadmin/basics.pycPK…F6³ÆÐ¬••¤Ë:webadmin/htdocs/css/admin.cssPKn+ô2ÇQ·¬ÚÚ!¤›Bwebadmin/htdocs/img/collapsed.pngPKn+ô2¡1ãã ¤´Cwebadmin/htdocs/img/expanded.pngPKn+ô2zae!¤ÕDwebadmin/htdocs/js/admin.jsPKÌ…F6-Й   #¤Jwebadmin/templates/admin_version.csPK …F6΄IÓÓ"¤ZVwebadmin/templates/admin_basics.csPK …F6Q{od33 ¤mYwebadmin/templates/admin_perm.csPKÂ…F6æHß  %¤Þ_webadmin/templates/admin_milestone.csPK²…F6á ÞXáá ¤Apwebadmin/templates/admin_enum.csPK …F6ƒºL-´ ´ "¤`ywebadmin/templates/admin_plugin.csPK …F6fƲ²¤T†webadmin/templates/admin.csPKª…F6ñ#â›+ + %¤?Šwebadmin/templates/admin_component.csPK …F6DâŸŠŠ¤­–webadmin/templates/admin_log.csPK †F6‘Oúð¤tEGG-INFO/PKG-INFOPK †F6úÀýPP¤¹žEGG-INFO/SOURCES.txtPK †F6“×2¤; EGG-INFO/dependency_links.txtPK †F6º 8¦ÚÚ¤w EGG-INFO/entry_points.txtPK †F6•4Ì ¤ˆ¡EGG-INFO/top_level.txtPK †F6“×2¤Å¡EGG-INFO/not-zip-safePK!! ù¡