diff --git a/src/tagman/models.py b/src/tagman/models.py index 871bdb2..78ef28c 100644 --- a/src/tagman/models.py +++ b/src/tagman/models.py @@ -62,8 +62,26 @@ def get_query_set(self): By default return only those objects that are not flagged as 'system' tags. """ - return super(TagManager, self).get_query_set()\ - .exclude(group__system=not self.system_tags).filter(archived=self.archived) + return super(TagManager, self)\ + .get_query_set()\ + .exclude(group__system=not self.system_tags)\ + .filter(archived=self.archived) + + def get_tags_with_weight(self, ignore_models=[], composite_name=True): + """ + :param ignore_models: Models to ignore in Tag usage results. + :param composite_name: If True the group name will be prepended + to tag name for dict keys. + + Returns dictionary of tag name as key and tag weight (usage) as + value. + """ + tag_dict = {} + tags = super(TagManager, self).get_query_set() + for tag in tags: + tag_name = tag.__unicode__() if composite_name else tag.name + tag_dict[tag_name] = tag.tag_weight(ignore_models) + return tag_dict class Tag(models.Model): @@ -78,8 +96,8 @@ class Tag(models.Model): archived = models.BooleanField(default=False) objects = models.Manager() - sys_objects = TagManager(sys=True) - public_objects = TagManager(sys=False) + sys_objects = TagManager(sys=True, archived=False) + public_objects = TagManager(sys=False, archived=False) non_archived = TagManager(sys=False, archived=False) def save(self, *args, **kwargs): @@ -107,7 +125,7 @@ def archive(self): """ Set the archive flag to implement soft-delete """ - self.archived=True + self.archived = True self.save() @property @@ -120,61 +138,50 @@ def models_for_tag(self): instances tagged with this tag. @todo: This is *really* hacky. Can we do it more elegantly? """ - models = [] + models = set() for attribute in dir(self): if attribute[-4:] == '_set': # we just want the model name, not the set name model_name = attribute.split('_')[0] - #TODO: check if the list already contains the model name? - models.append(model_name) - # return the unique set of model names - #TODO: what is more efficent? - return set(models) + models.add(model_name) + # return the unique set of model names + return models - def tagged_model_items(self, model_cls=None, model_name="", limit=None, - only_auto=False, filter_dict=None): + def tagged_model_items(self, model_cls=None, model_name="", + only_auto=False): """ - Return a unique set of instances of a given model, the class for + Return a query_set of a given model, the class for which is passed into model_cls OR the name for which is passed in model_name, that are tagged with this tag. - If `only_auto`==True then return only auto-tagged instances. + If `only_auto`==True then return only auto-tagged sets. """ - def _get_models_items(query_set): - items = [] + def _get_model_query_set(set_name): + query_set = None try: - _set = getattr(self, query_set) + _set = getattr(self, set_name) except AttributeError: logger.exception("Set {0} not found on tag {1}".format( - query_set, + set_name, self )) else: - if filter_dict: - try: - items = _set.filter(**filter_dict)[:limit] - except FieldError, e: - logger.exception( - "Cannot apply filter {0} to set {1}" - .format( - filter_dict, - query_set - ) - ) - else: - items = _set.all()[:limit] - return set(items) + query_set = _set + return query_set if model_cls: cls_name = model_cls.__name__.lower() else: cls_name = model_name.lower() - model_set = set() if not only_auto: - model_set.update(_get_models_items("{0}_set".format(cls_name))) - model_set.update(_get_models_items("{0}_auto_tagged_set" - .format(cls_name))) + model_set = _get_model_query_set( + "{0}_set".format(cls_name) + ) + else: + model_set = _get_model_query_set( + "{0}_auto_tagged_set".format(cls_name) + ) return model_set @@ -185,38 +192,72 @@ def auto_tagged_model_items(self, model_cls=None, model_name="", and tag. See tagged_model_items which this calls with only_auto=True """ - return self.tagged_model_items(model_cls, model_name, limit, - only_auto=True) + return self.tagged_model_items(model_cls, model_name, only_auto=True) - def tagged_items(self, limit=None, only_auto=False, ignore_models=[], - filter_dict=None): + def tagged_items(self, only_auto=False, models=None, ignore_models=None): """ Return a dictionary, keyed on model name, with each value the - set of items of that model tagged with this tag. + query_set of items of that model tagged with this tag. + + :param models: + A list of model classes for which to retrieve items. If absent, + retrieve any model that has a foreign key to a tag. + :param ignore_models: + Model classes not to include in the list of retrieved items. """ - models = self.models_for_tag() - ignore_models = [model.lower() for model in ignore_models] + if models is None: + models = self.models_for_tag() + else: + models = [model.__name__.lower() for model in models] + + if ignore_models is None: + ignore_models = set() + else: + ignore_models = set([model.__name__.lower() + for model in ignore_models]) + rdict = {} for model in models: if model not in ignore_models: rdict[model] = self.tagged_model_items(model_name=model, - only_auto=only_auto, - filter_dict=filter_dict, - limit=limit) + only_auto=only_auto) return rdict - def unique_item_set(self, limit=None, only_auto=False, ignore_models=[], - filter_dict=None): + def tag_weight(self, ignore_models=[]): + """ + Returns the weight of a tag based on the tags usage. """ - Return the unique item set for a tag + weight = 0 + for model_set in self.tagged_items( + ignore_models=ignore_models + ).values(): + weight += model_set.count() + return weight + + def unique_item_set(self, limit=None, only_auto=False, models=None, + ignore_models=None, filter_dict=None): + """ + Return the unique item set for a tag. + + :param models: + Return only instances from these models. If absent, retrieve any + model that has a foreign key to Tag + :param ignore_models: + Do not retrieve instances of these models """ item_set = set() - tagged_items = self.tagged_items(limit=limit, - only_auto=only_auto, - filter_dict=filter_dict, + tagged_items = self.tagged_items(only_auto=only_auto, + models=models, ignore_models=ignore_models) # merge all tagged items into a unique set - item_set.update(itertools.chain(*tagged_items.values())) + for model_set in tagged_items.values(): + if model_set: + model_set = model_set.filter(**filter_dict) \ + if filter_dict else model_set.all() + item_set.update( + # query set evaluated here + model_set[:limit] + ) return item_set diff --git a/src/tagman/tests/test_models.py b/src/tagman/tests/test_models.py index 54b3586..e59a263 100644 --- a/src/tagman/tests/test_models.py +++ b/src/tagman/tests/test_models.py @@ -61,7 +61,7 @@ def test_get_tagged_model_items(self): self.item.tags.add(self.tag1) model_items =\ self.tag1.tagged_model_items(model_cls=self.item.__class__) - self.assertTrue(self.item in model_items) + self.assertTrue(self.item in list(model_items.all())) def test_get_tag_for_string(self): self.group.tag_set.add(self.tag1) @@ -98,11 +98,11 @@ def test_get_tagged_items(self): ignored_model_name = ignored_model.__class__.__name__.lower() # first check the default call returns both items = self.tag1.tagged_items() - self.assertTrue(self.item in items[item_model_name]) - self.assertTrue(ignored_model in items[ignored_model_name]) + self.assertTrue(self.item in list(items[item_model_name].all())) + self.assertTrue(ignored_model in list(items[ignored_model_name].all())) # now ignore the model and check that its not returned - items = self.tag1.tagged_items(ignore_models=[ignored_model_name]) - self.assertTrue(self.item in items[item_model_name]) + items = self.tag1.tagged_items(ignore_models=[IgnoreTestItem]) + self.assertTrue(self.item in list(items[item_model_name].all())) self.assertTrue(ignored_model_name not in items.keys()) def test_get_unique_item_set(self): @@ -217,7 +217,10 @@ def test_auto_tag_creation(self): def test_get_auto_tagged_items(self): self.tci.associate_auto_tags() auto_tag = self.tci.auto_tags.all()[0] - tci_models = auto_tag.tagged_model_items(model_cls=self.tci.__class__) + tci_models = list( + auto_tag.auto_tagged_model_items + (model_cls=self.tci.__class__).all() + ) self.assertTrue(self.tci in tci_models) def test_get_tci_from_auto_tag(self): @@ -226,6 +229,6 @@ def test_get_tci_from_auto_tag(self): our TCI based on its auto-tag. """ auto_tag = Tag.objects.get(slug='tci-slug') - item_set = auto_tag.auto_tagged_model_items(model_cls=TCI) + item_set = set(auto_tag.auto_tagged_model_items(model_cls=TCI).all()) self.assertEquals(len(item_set), 1) self.assertEquals(item_set.pop(), self.tci)