def test_unbind(self): dbStub.addResult([]) c = CQDECategoryStore() self.assertTrue(c.unbind_category_project(1, 2)) self.assertTrue(dbStub.cursors[0].query.lower().startswith( "delete from project_categories")) self.assertTrue(dbStub.closed)
def _show_regular_page(self, req): req_data = {} # Resolve some static data needed on page (categories and project count) cs = CQDECategoryStore() all_categories = cs.get_all_categories() all_contexts = cs.get_contexts() combined_context_ids, combined_contexts, non_combined_contexts = self.get_combined_contexts( all_contexts) # Total count of projects and count of projects in categories direct_category_counts = Projects().get_project_counts_per_category( req.authname) req_data['direct_category_counts'] = direct_category_counts req_data['combined_context_ids'] = combined_context_ids req_data['non_combined_contexts'] = non_combined_contexts self.set_category_data_into_request_data(all_categories, req_data) add_stylesheet(req, 'multiproject/css/multiproject.css') add_script(req, 'multiproject/js/jquery.ba-bbq.js') add_script(req, 'multiproject/js/explore.js') return "find.html", { 'top_categories': req_data['top_categories'], 'categories': req_data['root_categories_of_combined_contexts'], 'nonbrowsable_cntxt': req_data['non_empty_non_combined_contexts'], 'selected_tab': self.DEFAULT_TAB, 'tabs': self.TABS}, \ None
def process_request(self, req): """ Process request for listing, creating and removing projects """ categories = [] q_filter = req.args.get('q') limit = req.args.get('limit') contexts = req.args.get('contexts[]') if contexts: if not isinstance(contexts, list): contexts = [contexts] # Second option: provide contexts in comma separated list if not contexts: contexts = req.args.get('contexts') if contexts: contexts = contexts.split(',') if q_filter: catstore = CQDECategoryStore() categories = catstore.get_category_by_name_filter(q_filter, limit, contexts) # Construct response in JSON list format data = '' try: data = json.dumps([category.name.encode("utf-8").split("#")[0] for category in categories]) except Exception, e: self.log.exception('Returning JSON from categories failed')
def _show_regular_page(self, req): req_data = {} # Resolve some static data needed on page (categories and project count) cs = CQDECategoryStore() all_categories = cs.get_all_categories() all_contexts = cs.get_contexts() combined_context_ids, combined_contexts, non_combined_contexts = self.get_combined_contexts(all_contexts) # Total count of projects and count of projects in categories direct_category_counts = Projects().get_project_counts_per_category(req.authname) req_data['direct_category_counts'] = direct_category_counts req_data['combined_context_ids'] = combined_context_ids req_data['non_combined_contexts'] = non_combined_contexts self.set_category_data_into_request_data(all_categories, req_data) add_stylesheet(req, 'multiproject/css/multiproject.css') add_script(req, 'multiproject/js/jquery.ba-bbq.js') add_script(req, 'multiproject/js/explore.js') return "find.html", { 'top_categories': req_data['top_categories'], 'categories': req_data['root_categories_of_combined_contexts'], 'nonbrowsable_cntxt': req_data['non_empty_non_combined_contexts'], 'selected_tab': self.DEFAULT_TAB, 'tabs': self.TABS}, \ None
def test_fetch_context_data_error(self): dbStub.addResult(categories) dbStub.addResult(subcategories) dbStub.setExceptions(True) c = CQDECategoryStore() data = c.fetch_context_data() self.assertFalse(data) self.assertTrue(dbStub.closed)
def test_fetch_available_categories_error(self): dbStub.addResult(categories) dbStub.addResult(subcategories) dbStub.setExceptions(True) c = CQDECategoryStore() data = c.fetch_available_categories(1, 2, 3) self.assertFalse(data) self.assertTrue(dbStub.closed)
def _get_project_categories(self, project): """ Create list of categories. License, Language and Development status are shown separately and are therefore taken out from the category listing. """ # Get api cs = CQDECategoryStore() # Get categories and specialcase contexts categories = cs.get_all_project_categories(project.id) contexts = cs.get_contexts() combined_categories = [] separated_categories_per_context = {} categories_by_any_context_id = {} context_by_id = {} # Setup the contexts dicts for context in contexts: context_by_id[context.context_id] = context if context.summary_name: # If context has summary name, it is shown as separated in the summary page new_list = [] categories_by_any_context_id[context.context_id] = new_list separated_categories_per_context[context.context_id] = new_list else: categories_by_any_context_id[context.context_id] = combined_categories sort_opts = {'key':lambda c:c.name, 'cmp':lambda x,y: cmp(x.lower(), y.lower())} # Now, categories_by_any_context_id has key for each context id for category in categories: categories_by_any_context_id[category.context].append(category) for context_id in separated_categories_per_context: separated_categories_per_context[context_id].sort(**sort_opts) combined_categories.sort(**sort_opts) # Sort alphabetically, case-insensitively context_order = [c.context_id for c in sorted(map(lambda id:context_by_id[id], separated_categories_per_context.keys()), key=lambda c:c.summary_name)] languages = [] for context_id in separated_categories_per_context: context = context_by_id[context_id] if (context.summary_name and context.summary_name.lower() in ('language','natural language')): # Here, we expect that the description contains the language tag. # http://www.w3schools.com/tags/att_meta_http_equiv.asp # http://www.w3.org/TR/2011/WD-html-markup-20110113/meta.http-equiv.content-language.html languages = [c.description for c in separated_categories_per_context[context_id]] return (combined_categories, separated_categories_per_context, context_by_id, context_order, languages)
def test_fetch_selected_categories_error(self): dbStub.addResult(contextlist) # 2 contexts dbStub.addResult(cat_list) dbStub.addResult([]) dbStub.setExceptions(True) c = CQDECategoryStore() data = c.fetch_selected_categories(2) self.assertFalse(data) self.assertTrue(dbStub.closed)
def get_project_categories(self, projects): """ Returns a map of project categories project_id => category string """ cs = CQDECategoryStore() project_categories = {} for project in projects: cats = cs.get_all_project_categories(project.id, ordered=True) cats_joined = ', '.join([cat.name.split("#")[0] for cat in cats]) project_categories[project.id] = cats_joined return project_categories
def test_fetch_available_categories(self): dbStub.addResult(categories) dbStub.addResult(subcategories) dbStub.addResult([]) # end recursion c = CQDECategoryStore() data = c.fetch_available_categories(1, 2, 3) self.assertTrue(data) self.assertEquals(len(data), 3) self.assertEquals(data[3]['name'], 'cats') childs = data[3]['childs'] self.assertTrue(childs) self.assertEquals(len(childs), 2) self.assertEquals(childs[6]['name'], 'subcat1') self.assertTrue(dbStub.closed)
def test_fetch_selected_categories(self): dbStub.addResult(contextlist) # 2 contexts dbStub.addResult(cat_list) dbStub.addResult([]) c = CQDECategoryStore() data = c.fetch_selected_categories(2) self.assertTrue(data) self.assertEquals(len(data), 2) self.assertEquals(len(data[1]), 3) self.assertEquals(len(data[2]), 0) self.assertEquals(data[1][0].name, 'cats') self.assertEquals(data[1][1].name, 'dogs') self.assertEquals(data[1][2].name, 'bugs') self.assertTrue(dbStub.closed)
def setUp(self): dbStub.addResult([[1]]) dbStub.addResult([]) self.cm = trac.core.ComponentManager() self.req = DummyReq() self.path = 'path' # dummy env.path self.project_name = "path" self.bind_called = False self.unbind_called = False self.orig_bind = CQDECategoryStore( ).bind_category_project #@UndefinedVariable self.orig_unbind = CQDECategoryStore( ).unbind_category_project #@UndefinedVariable CQDECategoryStore().bind_category_project = self.bind CQDECategoryStore().unbind_category_project = self.unbind
def test_fetch_context_data(self): dbStub.addResult(contextlist) # 2 contexts dbStub.addResult(category_tree) # 3 categories dbStub.addResult([]) # no subcategories for each of the 3 categ. dbStub.addResult([]) dbStub.addResult([]) dbStub.addResult(subcategories) # 2 project_categories dbStub.addResult([]) c = CQDECategoryStore() data = c.fetch_context_data() self.assertTrue(data) self.assertEquals(len(data), 2) self.assertEquals(data[1]['name'], 'context1') self.assertEquals(len(data[1]['categories']), 3) self.assertTrue(dbStub.closed)
def getProjectCategories(self, req): """ Returns projects categorizations """ categorylist = [] store = CQDECategoryStore() for context in store.get_contexts(): contextcategories = [] categories = store.get_categories(context.context_id) for category in categories: contextcategories.append(category.name) categorylist.append(context.name) categorylist.append(contextcategories) return categorylist
def render_admin_panel(self, req, cat, page, path_info): req.perm.require("TRAC_ADMIN") self.categorystore = CQDECategoryStore() data = {} if req.method == 'POST': if req.args.get('add'): self.add_category(req) if req.args.get('remove'): self.remove_category(req) if req.args.get('edit'): self.edit_category(req) if req.args.get('move'): self.move_category_to_new_context(req) if req.args.get('merge'): self.merge_to_category(req) if req.args.get('reposition'): self.move_category_to_new_parent(req) contexts = self.categorystore.get_contexts() context_by_id = {} categories_per_context = {} for context in contexts: context_by_id[context.context_id] = context categories_per_context[context.context_id] = \ self.categorystore.get_all_categories_in_context(context.context_id) is_rdf_description = lambda x: x.startswith('http://') data['context_by_id'] = context_by_id data['categories_per_context'] = categories_per_context data['envurl'] = req.base_path data['invalid_categories'] = self.categorystore.get_categories_with_invalid_context() data['is_rdf_description'] = is_rdf_description add_script(req, 'multiproject/js/jquery-ui.js') add_script(req, 'multiproject/js/categorization.js') add_stylesheet(req, 'multiproject/css/jquery-ui.css') return 'admin_categoryeditor.html', data
def __init__(self): self.categ = CQDECategoryStore() self.first_context = None
class CategorizationAdminPanel(Component): """ Trac admin panel component for categorizing project """ implements(IAdminPanelProvider, IRequestHandler) def __init__(self): self.categ = CQDECategoryStore() self.first_context = None # IAdminPanelProvider interface requirement def get_admin_panels(self, req): """ Admin panel navigation items """ if 'TRAC_ADMIN' in req.perm: yield ('general', 'General', 'categorization', 'Categorization') # IAdminPanelProvider interface requirement def render_admin_panel(self, req, cat, page, path_info): """ Renders categorization admin panel """ req.perm.require('TRAC_ADMIN') context_data = self._get_context_data() if req.args.get('add_and_bind'): self.add_and_bind_category(req, context_data) add_script(req, 'multiproject/js/jquery-ui.js') add_script(req, 'multiproject/js/categorization.js') add_stylesheet(req, 'multiproject/css/jquery-ui.css') all_contexts = context_data['all_contexts'] not_separate_context_ids = context_data['not_separate_context_ids'] context_by_id = context_data['context_by_id'] combined_context_ids = context_data['combined_context_ids'] # First index is reserved for main context contexts = [None] main_context_id = context_data['main_context_id'] for context in all_contexts: if context.context_id in not_separate_context_ids: continue contexts.append(context) if main_context_id != -1: contexts[0] = context_by_id[main_context_id] else: # No main context id -> drop the first one contexts = contexts[1:] main_addable_contexts = [] for context_id in not_separate_context_ids: if context_by_id[context_id].edit_type == Context.EDIT_TYPE_ADD: main_addable_contexts.append(context_by_id[context_id]) cats_per_context, project_cats_per_context = self._get_categories_data(context_data) data = {} data['env'] = req.base_path data['contexts'] = contexts data['categories_per_context'] = cats_per_context data['project_categories_per_context'] = project_cats_per_context data['main_addable_contexts'] = main_addable_contexts data['not_separate_context_ids'] = not_separate_context_ids return 'admin_categories.html', data def match_request(self, req): """ Path to categorization for ajax requests """ return re.match(r'/categories(?:_trac)?(?:/.*)?$', req.path_info) def process_request(self, req): """ Ajax request processing. Binding and unbinding categories """ req.perm.require('TRAC_ADMIN') # Get parameters action = req.args.get('action') context_key = req.args.get('context_key') category_key = req.args.get('category_key') context_data = self._get_context_data(context_key) project_key = context_data['project_key'] # Select action if "bind" == action: #if req.method != 'POST': # raise TracError('POST request required') self.categ.bind_category_project(project_key, category_key) elif "unbind" == action: #if req.method != 'POST': # raise TracError('POST request required') self.categ.unbind_category_project(project_key, category_key) return self.show_categories(req, context_key, context_data) def show_categories(self, req, context_key, context_data): """ Shows those categories that belongs into project and context that is given. """ context_key = safe_int(context_key) if (context_key in context_data['combined_context_ids'] or context_key in context_data['autocomplete_context_ids']): raise TracError('Cannot show combined categories') context_id = context_key # categories for main context are stored to index -1 if context_key == context_data['main_context_id']: context_id = -1 cats_per_context, project_cats_per_context = self._get_categories_data(context_data) data = { 'context': context_data['context_by_id'][context_key], 'categories': cats_per_context[context_id], 'project_categories': project_cats_per_context[context_id], 'env': req.base_path } return 'categories.html', data, None def add_and_bind_category(self, req, context_data): """ Called by render_admin_panel """ req.perm.require('TRAC_ADMIN') if req.method != 'POST': raise TracError('POST request required') category_name = req.args.get('category_name', '').strip() cat = self.categ.get_category_by_name(category_name) if not cat: context_id = safe_int(req.args.get('context_id', '').strip()) if not context_id or context_id not in context_data['context_by_id']: add_warning(req, _("Invalid context")) return if not category_name: add_warning(req, _("Invalid category")) return context = context_data['context_by_id'][context_id] if context.edit_type != context.EDIT_TYPE_ADD: add_warning(req, _("Adding category into context %(context_name) is not allowed", context_name=context.name)) return try: self.categ.add_category(category_name, category_name, context_id, None) cat = self.categ.get_category_by_name(category_name) except Exception as e: add_warning(req, _('Category %(what)s cannot be added.', what = category_name) + _(str(e))) return project_key = context_data['project_key'] if cat and project_key and self.categ.bind_category_project(project_key, cat.category_id): add_notice(req, _('Category %(what)s has been added.', what = category_name)) else: add_warning(req, _('Category %(what)s cannot be added.', what = category_name)) def _get_context_data(self, context_id = None): """ Returns dict with keys and values (type means here context.admin_type):: combined_context_ids: ids of contexts of type combined but not autocomplete not_separate_context_ids: set of ids of contexts of type combined, main, or autocomplete autocomplete_context_ids: ids of contexts of type autocomplete main_context_id: id of the context with type main, defaults to -1 context_by_id: context_id => context mapping all_contexts: all contexts involved_context_ids: ids of contexts for which the categories are needed to be fetched context_id: safe context id of the request :returns: dict with data """ main_context_id = -1 combined_context_ids = [] autocomplete_context_ids = [] not_separate_context_ids = set([]) context_by_id = {} all_contexts = self.categ.get_contexts() context_id = safe_int(context_id) project_key = Project.get(self.env).id for context in all_contexts: context_by_id[context.context_id] = context if context.admin_type == context.ADMIN_TYPE_MAIN: if main_context_id != -1: self.log.warning("Invalid category data: Duplicate main contexts") context.admin_type = context.ADMIN_TYPE_COMBINED else: main_context_id = context.context_id not_separate_context_ids.add(context.context_id) if context.admin_type == context.ADMIN_TYPE_COMBINED: combined_context_ids.append(context.context_id) not_separate_context_ids.add(context.context_id) elif context.admin_type == context.ADMIN_TYPE_AUTOCOMPLETE: autocomplete_context_ids.append(context.context_id) not_separate_context_ids.add(context.context_id) if main_context_id == -1: self.log.warning('Invalid category data: Main context not set!') involved_context_ids = [] if context_id: if context_id not in context_by_id: raise TracError('Invalid context id provided %s' %context_id.__class__) if context_id == main_context_id: involved_context_ids = [main_context_id]+combined_context_ids+autocomplete_context_ids else: involved_context_ids = [context_id] else: involved_context_ids = [context.context_id for context in all_contexts] data = {'project_key': project_key, 'autocomplete_context_ids': autocomplete_context_ids, 'combined_context_ids': combined_context_ids, 'not_separate_context_ids': not_separate_context_ids, 'main_context_id': main_context_id, 'context_by_id': context_by_id, 'all_contexts': all_contexts, 'involved_context_ids': involved_context_ids, 'context_id': context_id} return data def _get_categories_data(self, context_data): """ Returns 'categories_per_context' and 'project_categories_per_context', both being dictionaries with context_id keys and values, which are dictionaries with category_id keys and category values. """ context_by_id = context_data['context_by_id'] main_context_id = context_data['main_context_id'] project_key = context_data['project_key'] categories_per_context = {-1: {}} project_categories_per_context = {-1: {}} for i_context_id in context_data['involved_context_ids']: context = context_by_id[i_context_id] if context.admin_type in (context.ADMIN_TYPE_MAIN, context.ADMIN_TYPE_COMBINED): categories_per_context[-1].update( self.categ.get_all_categories_in_context(i_context_id)) project_categories_per_context[-1].update( self._get_categories_by_project(project_key, i_context_id) ) elif context.admin_type == context.ADMIN_TYPE_AUTOCOMPLETE: # categories_per_context[i_context_id] -- don't show anywhere! project_categories_per_context[-1].update( self._get_categories_by_project(project_key, i_context_id)) else: categories_per_context[i_context_id] = \ self.categ.get_all_categories_in_context(i_context_id) project_categories_per_context[i_context_id] = \ self._get_categories_by_project(project_key, i_context_id) categories_per_context[main_context_id] = categories_per_context[-1] project_categories_per_context[main_context_id] = project_categories_per_context[-1] return categories_per_context, project_categories_per_context def _get_categories_by_project(self, project_key, context_id): category_dict = {} for category in self.categ.get_categories_by_project(project_key, context_id): category_dict[category.category_id] = category return category_dict def _get_context_name(self, context_key, all_contexts = None): if not all_contexts: all_contexts = self.categ.get_contexts() for c in all_contexts: if c.context_id == int(context_key): return c.name return ""
class CategoryEditorAdminPanel(Component): """ Trac component for editing category structure """ implements(IAdminPanelProvider) def get_admin_panels(self, req): if 'TRAC_ADMIN' in req.perm: yield ('general', _('General'), 'categoryeditor', _('Edit categories')) def render_admin_panel(self, req, cat, page, path_info): req.perm.require("TRAC_ADMIN") self.categorystore = CQDECategoryStore() data = {} if req.method == 'POST': if req.args.get('add'): self.add_category(req) if req.args.get('remove'): self.remove_category(req) if req.args.get('edit'): self.edit_category(req) if req.args.get('move'): self.move_category_to_new_context(req) if req.args.get('merge'): self.merge_to_category(req) if req.args.get('reposition'): self.move_category_to_new_parent(req) contexts = self.categorystore.get_contexts() context_by_id = {} categories_per_context = {} for context in contexts: context_by_id[context.context_id] = context categories_per_context[context.context_id] = \ self.categorystore.get_all_categories_in_context(context.context_id) is_rdf_description = lambda x: x.startswith('http://') data['context_by_id'] = context_by_id data['categories_per_context'] = categories_per_context data['envurl'] = req.base_path data['invalid_categories'] = self.categorystore.get_categories_with_invalid_context() data['is_rdf_description'] = is_rdf_description add_script(req, 'multiproject/js/jquery-ui.js') add_script(req, 'multiproject/js/categorization.js') add_stylesheet(req, 'multiproject/css/jquery-ui.css') return 'admin_categoryeditor.html', data def add_category(self, req): req.perm.require('TRAC_ADMIN') context = req.args.get('context', '').strip() category = req.args.get('category', '').strip() parent = req.args.get('parent', '').strip() if not context or not category: return if parent == "NONE" or parent == '': parent = None try: self.categorystore.add_category(category, category, context, parent) add_notice(req, _('Category %(what)s has been added.', what=category)) except Exception as e: add_warning(req, _('Category %(what)s cannot be added. ', what=category) + _(str(e))) def remove_category(self, req): req.perm.require('TRAC_ADMIN') category = self._translate_req_to_category(req, 'removed'); if not category: return try: self.categorystore.remove_category(category.category_id) add_notice(req, _('Category has been removed.')) except Exception as e: add_warning(req, _('Category was not removed. ') + _(str(e))) def edit_category(self, req): req.perm.require('TRAC_ADMIN') category_id = req.args.get('edited_category_id', '') if category_id.isdigit(): category = self.categorystore.get_category_by_id(category_id) if not category_id or not category: add_warning(req, _('Invalid category id provided.')) return category_name = req.args.get('edited_category_name', '') category_description = req.args.get('edited_category_description', '') if not category_name or not category_description: add_warning(req, _('Category name and description cannot be empty. "%s" "%s"'%(category_name, category_description))) return if category.name == category_name and category.description == category_description: add_warning(req, _('Category name and description are already as requested.')) return try: self.categorystore.edit_category(category.category_id, category_name, category_description) add_notice(req, _('Category has been edited.')) except Exception as e: add_warning(req, _('Category was not edited. ') + _(str(e))) def move_category_to_new_context(self, req): req.perm.require('TRAC_ADMIN') new_context = req.args.get('newcontext', '').strip() # possible params involved in req: 'moved_category_id' or 'moved_category_name' category = self._translate_req_to_category(req, 'moved') if not new_context or not category: return try: self.categorystore.move_category_to_root_of_context(category.category_id, new_context) add_notice(req, _('Category %(what)s has been moved.', what=category.name)) except Exception as e: add_warning(req, _('Category %(what)s cannot be moved. ', what=category.name) + _(str(e))) def move_category_to_new_parent(self, req): req.perm.require('TRAC_ADMIN') new_context = req.args.get('newcontext', '').strip() # possible params involved in req: 'repositioned_category_id' or 'repositioned_category_name' category = self._translate_req_to_category(req, 'repositioned') root_category = req.args.get('root_category') parent_category_id = None parent_category = None if not root_category: # possible params involved in req: 'new_parent_category_id' or 'new_parent_category_name' parent_category = self._translate_req_to_category(req, 'new_parent') if parent_category: parent_category_id = parent_category.category_id if not category or (not root_category and not parent_category): add_warning(req, _('Invalid parameters, nothing done. ')) return if root_category: try: self.categorystore.move_category_to_root_of_context(category.category_id, category.context) add_notice(req, _('Category %(what)s has been moved to be root category.', what=category.name)) except Exception as e: add_warning(req, _('Category %(what)s was not moved to be root category. ', what=category.name) + _(str(e))) else: try: self.categorystore.move_category_to_new_parent(category.category_id, parent_category_id) add_notice(req, _('Category %(what)s has been moved to parent %(parent)s.', what=category.name, parent=parent_category.name)) except Exception as e: add_warning(req, _('Category %(what)s was not moved to parent %(parent)s. ', what=category.name, parent=parent_category.name) + _(str(e))) def merge_to_category(self, req): req.perm.require('TRAC_ADMIN') # possible params involved in req: target_category_name, target_category_id target_category = self._translate_req_to_category(req, 'target') # possible params involved in req: merged_category_id, merged_category_name merged_category = self._translate_req_to_category(req, 'merged') if not target_category or not merged_category: add_warning(req, _('Invalid merge category parameters: No corresponding category found.')) return try: self.categorystore.merge_category_to_category(merged_category.category_id, target_category.category_id) add_notice(req, _('Category %(merged)s has been merged to %(target)s.', merged = merged_category.name, target = target_category.name)) except Exception as e: add_warning(req, _('Category %(what)s was not merged. ', what=merged_category.name) + _(str(e))) def _translate_req_to_category(self, req, prefix): """ :returns: :py:class:`multiproject.core.categories.Category` or None """ category_id = req.args.get(prefix + '_category_id', '').strip() category_name = req.args.get(prefix + '_category_name','').strip() if category_id.isdigit(): category = self.categorystore.get_category_by_id(int(category_id)) else: self.log.debug("searching %s with: %s " % (prefix, category_name)) category = self.categorystore.get_category_by_name(category_name) return category
def search(self, keywords, category_ids, username='******', order_by='newest', sub_page=1, limit=5, all_categories=None): """ Search for projects with fulltext and categories for explore projects. .. Note:: Order by featured is not supported anymore. :param order_by: str either 'newest' or 'active' :param all_categories: equal to CQDECategoryStore.get_all_categories() """ if not all_categories: all_categories = CQDECategoryStore().get_all_categories() limit = safe_int(limit) limit_attr = {'limit_start': (int(sub_page) - 1) * limit, 'limit': limit} wheres = [" u.username IN ('anonymous')"] if username != 'anonymous': # Alternative implementation by using EXISTS sub query wheres = [" u.username IN ('anonymous', '%s')" % safe_string(username)] joins = [] activity_statement = self._activity_statement() # Sorting by featured is no longer supported. select_columns = """ DISTINCT p.project_id, p.project_name, p.environment_name, p.description, p.author, p.created, p.updated, p.published, p.parent_id, p.icon_name, p.trac_environment_key, {activity_statement} AS a """.format(activity_statement=activity_statement) select_count = """COUNT(DISTINCT p.project_id)""" if category_ids: # Given categories search_cat_list = [] cat_ids_in_search = set([]) list_of_or_category_ids = [] and_category_ids = [] for cat_id in category_ids: if not all_categories.has_key(cat_id): continue if cat_id in cat_ids_in_search: continue cat_ids_in_search.add(cat_id) category = all_categories[cat_id] if category.has_children(): categories_to_go = [] categories_to_go.extend(category.children) current_categories = [cat_id] # Counter for avoiding infinite loops counter = len(all_categories) + 1 while categories_to_go and counter > 0: child_cat = categories_to_go.pop() if child_cat.category_id in cat_ids_in_search: # categories are wrong somehow. Should not happen when used properly. continue categories_to_go.extend(child_cat.children) cat_ids_in_search.add(child_cat.category_id) current_categories.append(child_cat.category_id) counter -= 1 if counter == 0: conf.log.critical("Infinite loop when searching all sub categories of %s " % cat_id) list_of_or_category_ids.append(current_categories) else: and_category_ids.append(cat_id) # Important special case when searching with one category: if len(and_category_ids) == 1: joins += ["INNER JOIN project_categories AS pc ON pc.project_key = p.project_id "] wheres += ["pc.category_key = %s" % safe_int(and_category_ids[0])] elif and_category_ids: # Join id:s into in clause wrapped in quotes id_list_str = ', '.join([str(safe_int(id)) for id in and_category_ids]) wheres += [ """ p.project_id IN (SELECT project_categories.project_key FROM project_categories WHERE category_key IN (%s) GROUP BY project_key HAVING COUNT(*) = %s)""" % (id_list_str, len(and_category_ids)) ] for or_category_ids in list_of_or_category_ids: id_list_str = ', '.join([str(safe_int(id)) for id in or_category_ids]) wheres += [ """ EXISTS (SELECT project_categories.project_key FROM project_categories WHERE project_key = p.project_id AND category_key IN (%s))""" % id_list_str ] if keywords: wheres += [ "CONCAT(p.project_name COLLATE utf8_general_ci,p.environment_name, p.description) LIKE '%{0}%'" .format(safe_string(kw.replace('%','\\%'))) for kw in keywords.split() if kw] # Merge where statements where_str = '\n AND '.join(wheres) # Merge join statements join_str = " ".join(joins) order_by_str = "" if order_by == "active": order_by_str = " ORDER BY a DESC " elif order_by == "recent": order_by_str = " ORDER BY IFNULL(p.published, p.created) DESC " # Note: If project_activity is not calculated, the project will not be shown here! query_template = """ SELECT {select_clause} FROM project_user_visibility AS v INNER JOIN projects AS p ON p.project_id = v.project_id INNER JOIN user AS u ON u.user_id = v.user_id INNER JOIN project_activity AS pa ON pa.project_key = p.project_id {join_str} WHERE {where_str} {order_by} {limit} """ # The count_query doesn't have order_by, and limit parts. # Here, if we would form query_template so that it already contained the parts # common to both query and count_query, i.e., where_str, join_str, wher_str), # there would be possibility to put ' {anything} ' or ' %(anything)s ' into where_str. # Thus, safer to do the formatting once for all in both cases. query = query_template.format(join_str=join_str, where_str=where_str, select_clause=select_columns, order_by=order_by_str, limit= "LIMIT %(limit_start)d, %(limit)d " % limit_attr) count_query = query_template.format(join_str=join_str, where_str=where_str, select_clause = select_count, order_by = '', limit = '') conf.log.debug("Explore projects search query: %s",query) conf.log.debug("Explore projects search count_query: %s",count_query) query_count = self._get_single_result(count_query) or 0 projects, activities = self.queryProjectObjectsForSearch(query) return projects, activities, query_count
def test_unbind_error(self): dbStub.addResult([]) dbStub.setExceptions(True) c = CQDECategoryStore() self.assertFalse(c.unbind_category_project(1, 2)) self.assertTrue(dbStub.closed)
def tearDown(self): dbStub.reset() self.cm = None self.req = None CQDECategoryStore().bind_category_project = self.orig_bind CQDECategoryStore().unbind_category_project = self.orig_unbind
class CategoryEditorAdminPanel(Component): """ Trac component for editing category structure """ implements(IAdminPanelProvider) def get_admin_panels(self, req): if 'TRAC_ADMIN' in req.perm: yield ('general', _('General'), 'categoryeditor', _('Edit categories')) def render_admin_panel(self, req, cat, page, path_info): req.perm.require("TRAC_ADMIN") self.categorystore = CQDECategoryStore() data = {} if req.method == 'POST': if req.args.get('add'): self.add_category(req) if req.args.get('remove'): self.remove_category(req) if req.args.get('edit'): self.edit_category(req) if req.args.get('move'): self.move_category_to_new_context(req) if req.args.get('merge'): self.merge_to_category(req) if req.args.get('reposition'): self.move_category_to_new_parent(req) contexts = self.categorystore.get_contexts() context_by_id = {} categories_per_context = {} for context in contexts: context_by_id[context.context_id] = context categories_per_context[context.context_id] = \ self.categorystore.get_all_categories_in_context(context.context_id) is_rdf_description = lambda x: x.startswith('http://') data['context_by_id'] = context_by_id data['categories_per_context'] = categories_per_context data['envurl'] = req.base_path data['invalid_categories'] = self.categorystore.get_categories_with_invalid_context() data['is_rdf_description'] = is_rdf_description add_script(req, 'multiproject/js/jquery-ui.js') add_script(req, 'multiproject/js/categorization.js') add_stylesheet(req, 'multiproject/css/jquery-ui.css') return 'admin_categoryeditor.html', data def add_category(self, req): req.perm.require('TRAC_ADMIN') context = req.args.get('context', '').strip() category = req.args.get('category', '').strip() parent = req.args.get('parent', '').strip() license_url = req.args.get('license_url', '').strip() if not context or not category: return if license_url: category = category + "#" + license_url if parent == "NONE" or parent == '': parent = None try: self.categorystore.add_category(category, category, context, parent) add_notice(req, _('Category %(what)s has been added.', what=category)) except Exception as e: add_warning(req, _('Category %(what)s cannot be added. ', what=category) + _(str(e))) def remove_category(self, req): req.perm.require('TRAC_ADMIN') category = self._translate_req_to_category(req, 'removed'); if not category: return try: self.categorystore.remove_category(category.category_id) add_notice(req, _('Category has been removed.')) except Exception as e: add_warning(req, _('Category was not removed. ') + _(str(e))) def edit_category(self, req): req.perm.require('TRAC_ADMIN') category_id = req.args.get('edited_category_id', '') if category_id.isdigit(): category = self.categorystore.get_category_by_id(category_id) if not category_id or not category: add_warning(req, _('Invalid category id provided.')) return category_name = req.args.get('edited_category_name', '') category_description = req.args.get('edited_category_description', '') if len(category_description.split("#")) == 2: category_name = category_name + "#" + category_description.split("#")[1] if not category_name or not category_description: add_warning(req, _('Category name and description cannot be empty. "%s" "%s"'%(category_name, category_description))) return if category.name == category_name and category.description == category_description: add_warning(req, _('Category name and description are already as requested.')) return try: self.categorystore.edit_category(category.category_id, category_name, category_description) add_notice(req, _('Category has been edited.')) except Exception as e: add_warning(req, _('Category was not edited. ') + _(str(e))) def move_category_to_new_context(self, req): req.perm.require('TRAC_ADMIN') new_context = req.args.get('newcontext', '').strip() # possible params involved in req: 'moved_category_id' or 'moved_category_name' category = self._translate_req_to_category(req, 'moved') if not new_context or not category: return try: self.categorystore.move_category_to_root_of_context(category.category_id, new_context) add_notice(req, _('Category %(what)s has been moved.', what=category.name)) except Exception as e: add_warning(req, _('Category %(what)s cannot be moved. ', what=category.name) + _(str(e))) def move_category_to_new_parent(self, req): req.perm.require('TRAC_ADMIN') new_context = req.args.get('newcontext', '').strip() # possible params involved in req: 'repositioned_category_id' or 'repositioned_category_name' category = self._translate_req_to_category(req, 'repositioned') root_category = req.args.get('root_category') parent_category_id = None parent_category = None if not root_category: # possible params involved in req: 'new_parent_category_id' or 'new_parent_category_name' parent_category = self._translate_req_to_category(req, 'new_parent') if parent_category: parent_category_id = parent_category.category_id if not category or (not root_category and not parent_category): add_warning(req, _('Invalid parameters, nothing done. ')) return if root_category: try: self.categorystore.move_category_to_root_of_context(category.category_id, category.context) add_notice(req, _('Category %(what)s has been moved to be root category.', what=category.name)) except Exception as e: add_warning(req, _('Category %(what)s was not moved to be root category. ', what=category.name) + _(str(e))) else: try: self.categorystore.move_category_to_new_parent(category.category_id, parent_category_id) add_notice(req, _('Category %(what)s has been moved to parent %(parent)s.', what=category.name, parent=parent_category.name)) except Exception as e: add_warning(req, _('Category %(what)s was not moved to parent %(parent)s. ', what=category.name, parent=parent_category.name) + _(str(e))) def merge_to_category(self, req): req.perm.require('TRAC_ADMIN') # possible params involved in req: target_category_name, target_category_id target_category = self._translate_req_to_category(req, 'target') # possible params involved in req: merged_category_id, merged_category_name merged_category = self._translate_req_to_category(req, 'merged') if not target_category or not merged_category: add_warning(req, _('Invalid merge category parameters: No corresponding category found.')) return try: self.categorystore.merge_category_to_category(merged_category.category_id, target_category.category_id) add_notice(req, _('Category %(merged)s has been merged to %(target)s.', merged = merged_category.name, target = target_category.name)) except Exception as e: add_warning(req, _('Category %(what)s was not merged. ', what=merged_category.name) + _(str(e))) def _translate_req_to_category(self, req, prefix): """ :returns: :py:class:`multiproject.core.categories.Category` or None """ category_id = req.args.get(prefix + '_category_id', '').strip() category_name = req.args.get(prefix + '_category_name','').strip() if category_id.isdigit(): category = self.categorystore.get_category_by_id(int(category_id)) else: self.log.debug("searching %s with: %s " % (prefix, category_name)) category = self.categorystore.get_category_by_name(category_name) return category
class CategorizationAdminPanel(Component): """ Trac admin panel component for categorizing project """ implements(IAdminPanelProvider, IRequestHandler) def __init__(self): self.categ = CQDECategoryStore() self.first_context = None # IAdminPanelProvider interface requirement def get_admin_panels(self, req): """ Admin panel navigation items """ if 'TRAC_ADMIN' in req.perm: yield ('general', 'General', 'categorization', 'Categorization') # IAdminPanelProvider interface requirement def render_admin_panel(self, req, cat, page, path_info): """ Renders categorization admin panel """ req.perm.require('TRAC_ADMIN') context_data = self._get_context_data() if req.args.get('add_and_bind'): self.add_and_bind_category(req, context_data) add_script(req, 'multiproject/js/jquery-ui.js') add_script(req, 'multiproject/js/categorization.js') add_stylesheet(req, 'multiproject/css/jquery-ui.css') all_contexts = context_data['all_contexts'] not_separate_context_ids = context_data['not_separate_context_ids'] context_by_id = context_data['context_by_id'] combined_context_ids = context_data['combined_context_ids'] # First index is reserved for main context contexts = [None] main_context_id = context_data['main_context_id'] for context in all_contexts: if context.context_id in not_separate_context_ids: continue contexts.append(context) if main_context_id != -1: contexts[0] = context_by_id[main_context_id] else: # No main context id -> drop the first one contexts = contexts[1:] main_addable_contexts = [] for context_id in not_separate_context_ids: if context_by_id[context_id].edit_type == Context.EDIT_TYPE_ADD: main_addable_contexts.append(context_by_id[context_id]) cats_per_context, project_cats_per_context = self._get_categories_data( context_data) data = {} data['env'] = req.base_path data['contexts'] = contexts data['categories_per_context'] = cats_per_context data['project_categories_per_context'] = project_cats_per_context data['main_addable_contexts'] = main_addable_contexts data['not_separate_context_ids'] = not_separate_context_ids return 'admin_categories.html', data def match_request(self, req): """ Path to categorization for ajax requests """ return re.match(r'/categories(?:_trac)?(?:/.*)?$', req.path_info) def process_request(self, req): """ Ajax request processing. Binding and unbinding categories """ req.perm.require('TRAC_ADMIN') # Get parameters action = req.args.get('action') context_key = req.args.get('context_key') category_key = req.args.get('category_key') context_data = self._get_context_data(context_key) project_key = context_data['project_key'] # Select action if "bind" == action: #if req.method != 'POST': # raise TracError('POST request required') self.categ.bind_category_project(project_key, category_key) elif "unbind" == action: #if req.method != 'POST': # raise TracError('POST request required') self.categ.unbind_category_project(project_key, category_key) return self.show_categories(req, context_key, context_data) def show_categories(self, req, context_key, context_data): """ Shows those categories that belongs into project and context that is given. """ context_key = safe_int(context_key) if (context_key in context_data['combined_context_ids'] or context_key in context_data['autocomplete_context_ids']): raise TracError('Cannot show combined categories') context_id = context_key # categories for main context are stored to index -1 if context_key == context_data['main_context_id']: context_id = -1 cats_per_context, project_cats_per_context = self._get_categories_data( context_data) data = { 'context': context_data['context_by_id'][context_key], 'categories': cats_per_context[context_id], 'project_categories': project_cats_per_context[context_id], 'env': req.base_path } return 'categories.html', data, None def add_and_bind_category(self, req, context_data): """ Called by render_admin_panel """ req.perm.require('TRAC_ADMIN') if req.method != 'POST': raise TracError('POST request required') category_name = req.args.get('category_name', '') l_url = req.args.get('license_url') if category_name and l_url: category_name = category_name.strip() l_url = l_url.strip() category_name = category_name + "#" + l_url cat = self.categ.get_category_by_name(category_name) if not cat: context_id = safe_int(req.args.get('context_id', '').strip()) if not context_id or context_id not in context_data[ 'context_by_id']: add_warning(req, _("Invalid context")) return if not category_name: add_warning(req, _("Invalid category")) return context = context_data['context_by_id'][context_id] if context.edit_type != context.EDIT_TYPE_ADD: add_warning( req, _("Adding category into context %(context_name) is not allowed", context_name=context.name)) return try: self.categ.add_category(category_name, category_name, context_id, None) cat = self.categ.get_category_by_name(category_name) except Exception as e: add_warning( req, _('Category %(what)s cannot be added.', what=category_name) + _(str(e))) return project_key = context_data['project_key'] if cat and project_key and self.categ.bind_category_project( project_key, cat.category_id): add_notice( req, _('Category %(what)s has been added.', what=category_name.split("#")[0])) else: add_warning( req, _('Category %(what)s cannot be added.', what=category_name.split("#")[0])) def _get_context_data(self, context_id=None): """ Returns dict with keys and values (type means here context.admin_type):: combined_context_ids: ids of contexts of type combined but not autocomplete not_separate_context_ids: set of ids of contexts of type combined, main, or autocomplete autocomplete_context_ids: ids of contexts of type autocomplete main_context_id: id of the context with type main, defaults to -1 context_by_id: context_id => context mapping all_contexts: all contexts involved_context_ids: ids of contexts for which the categories are needed to be fetched context_id: safe context id of the request :returns: dict with data """ main_context_id = -1 combined_context_ids = [] autocomplete_context_ids = [] not_separate_context_ids = set([]) context_by_id = {} all_contexts = self.categ.get_contexts() context_id = safe_int(context_id) project_key = Project.get(self.env).id for context in all_contexts: context_by_id[context.context_id] = context if context.admin_type == context.ADMIN_TYPE_MAIN: if main_context_id != -1: self.log.warning( "Invalid category data: Duplicate main contexts") context.admin_type = context.ADMIN_TYPE_COMBINED else: main_context_id = context.context_id not_separate_context_ids.add(context.context_id) if context.admin_type == context.ADMIN_TYPE_COMBINED: combined_context_ids.append(context.context_id) not_separate_context_ids.add(context.context_id) elif context.admin_type == context.ADMIN_TYPE_AUTOCOMPLETE: autocomplete_context_ids.append(context.context_id) not_separate_context_ids.add(context.context_id) if main_context_id == -1: self.log.warning('Invalid category data: Main context not set!') involved_context_ids = [] if context_id: if context_id not in context_by_id: raise TracError('Invalid context id provided %s' % context_id.__class__) if context_id == main_context_id: involved_context_ids = [ main_context_id ] + combined_context_ids + autocomplete_context_ids else: involved_context_ids = [context_id] else: involved_context_ids = [ context.context_id for context in all_contexts ] data = { 'project_key': project_key, 'autocomplete_context_ids': autocomplete_context_ids, 'combined_context_ids': combined_context_ids, 'not_separate_context_ids': not_separate_context_ids, 'main_context_id': main_context_id, 'context_by_id': context_by_id, 'all_contexts': all_contexts, 'involved_context_ids': involved_context_ids, 'context_id': context_id } return data def _get_categories_data(self, context_data): """ Returns 'categories_per_context' and 'project_categories_per_context', both being dictionaries with context_id keys and values, which are dictionaries with category_id keys and category values. """ context_by_id = context_data['context_by_id'] main_context_id = context_data['main_context_id'] project_key = context_data['project_key'] categories_per_context = {-1: {}} project_categories_per_context = {-1: {}} for i_context_id in context_data['involved_context_ids']: context = context_by_id[i_context_id] if context.admin_type in (context.ADMIN_TYPE_MAIN, context.ADMIN_TYPE_COMBINED): categories_per_context[-1].update( self.categ.get_all_categories_in_context(i_context_id)) project_categories_per_context[-1].update( self._get_categories_by_project(project_key, i_context_id)) elif context.admin_type == context.ADMIN_TYPE_AUTOCOMPLETE: # categories_per_context[i_context_id] -- don't show anywhere! project_categories_per_context[-1].update( self._get_categories_by_project(project_key, i_context_id)) else: categories_per_context[i_context_id] = \ self.categ.get_all_categories_in_context(i_context_id) project_categories_per_context[i_context_id] = \ self._get_categories_by_project(project_key, i_context_id) categories_per_context[main_context_id] = categories_per_context[-1] project_categories_per_context[ main_context_id] = project_categories_per_context[-1] return categories_per_context, project_categories_per_context def _get_categories_by_project(self, project_key, context_id): category_dict = {} for category in self.categ.get_categories_by_project( project_key, context_id): category_dict[category.category_id] = category return category_dict def _get_context_name(self, context_key, all_contexts=None): if not all_contexts: all_contexts = self.categ.get_contexts() for c in all_contexts: if c.context_id == int(context_key): return c.name return ""
def test_unbind(self): dbStub.addResult([]) c = CQDECategoryStore() self.assertTrue(c.unbind_category_project(1, 2)) self.assertTrue(dbStub.cursors[0].query.lower().startswith("delete from project_categories")) self.assertTrue(dbStub.closed)
def _get_project_categories(self, project): """ Create list of categories. License, Language and Development status are shown separately and are therefore taken out from the category listing. """ # Get api cs = CQDECategoryStore() # Get categories and specialcase contexts categories = cs.get_all_project_categories(project.id) contexts = cs.get_contexts() combined_categories = [] separated_categories_per_context = {} categories_by_any_context_id = {} context_by_id = {} # Setup the contexts dicts for context in contexts: context_by_id[context.context_id] = context if context.summary_name: # If context has summary name, it is shown as separated in the summary page new_list = [] categories_by_any_context_id[context.context_id] = new_list separated_categories_per_context[context.context_id] = new_list else: categories_by_any_context_id[ context.context_id] = combined_categories sort_opts = { 'key': lambda c: c.name, 'cmp': lambda x, y: cmp(x.lower(), y.lower()) } # Now, categories_by_any_context_id has key for each context id for category in categories: categories_by_any_context_id[category.context].append(category) for context_id in separated_categories_per_context: separated_categories_per_context[context_id].sort(**sort_opts) combined_categories.sort(**sort_opts) # Sort alphabetically, case-insensitively context_order = [ c.context_id for c in sorted(map(lambda id: context_by_id[id], separated_categories_per_context.keys()), key=lambda c: c.summary_name) ] languages = [] for context_id in separated_categories_per_context: context = context_by_id[context_id] if (context.summary_name and context.summary_name.lower() in ('language', 'natural language')): # Here, we expect that the description contains the language tag. # http://www.w3schools.com/tags/att_meta_http_equiv.asp # http://www.w3.org/TR/2011/WD-html-markup-20110113/meta.http-equiv.content-language.html languages = [ c.description for c in separated_categories_per_context[context_id] ] return (combined_categories, separated_categories_per_context, context_by_id, context_order, languages)