Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update tags with optional replace existing tags flag. #3

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,8 @@ language: python
python:
- "2.7"
env:
- TOX_ENV=py27-django14
- TOX_ENV=py27-django15
- TOX_ENV=py27-django16
- TOX_ENV=py27-django17
- TOX_ENV=py27-django18
- TOX_ENV=py33-django15
- TOX_ENV=py33-django16
- TOX_ENV=py33-django17
- TOX_ENV=py33-django18
install:
Expand Down
3 changes: 3 additions & 0 deletions tagging/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from tagging.models import Tag, TaggedItem


class ModelTagManager(models.Manager):
"""
A manager for retrieving tags for a particular model.
Expand All @@ -25,6 +26,7 @@ def related(self, tags, *args, **kwargs):
def usage(self, *args, **kwargs):
return Tag.objects.usage_for_model(self.model, *args, **kwargs)


class ModelTaggedItemManager(models.Manager):
"""
A manager for retrieving model instances based on their tags.
Expand All @@ -47,6 +49,7 @@ def with_any(self, tags, queryset=None):
else:
return TaggedItem.objects.get_union_by_model(queryset, tags)


class TagDescriptor(object):
"""
A descriptor which provides access to a ``ModelTagManager`` for
Expand Down
142 changes: 105 additions & 37 deletions tagging/models.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
"""
Models and managers for generic tagging.
"""
# Python 2.3 compatibility
try:
set
except NameError:
from sets import Set as set

from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.conf import settings

from django.db import connection, models
from django.db.models.query import QuerySet
from django.utils.translation import ugettext_lazy as _

from tagging.settings import DEFAULT_FORCE_LOWERCASE_TAGS
from tagging.utils import calculate_cloud, get_tag_list, get_queryset_and_model, parse_tag_input
from tagging.utils import (
calculate_cloud,
get_tag_list,
get_queryset_and_model,
parse_tag_input,
)
from tagging.utils import LOGARITHMIC

qn = connection.ops.quote_name
Expand All @@ -25,25 +23,35 @@
# Managers #
############


class TagManager(models.Manager):
def update_tags(self, obj, tag_names):
def update_tags(self, obj, tag_names, replace_existing=True):
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actual change

"""
Update tags associated with an object.
"""
ctype = ContentType.objects.get_for_model(obj)
current_tags = list(self.filter(items__content_type__pk=ctype.pk,
items__object_id=obj.pk))
updated_tag_names = parse_tag_input(tag_names)
if getattr(settings, 'FORCE_LOWERCASE_TAGS', DEFAULT_FORCE_LOWERCASE_TAGS):
force_lowercase = getattr(
settings,
'FORCE_LOWERCASE_TAGS',
DEFAULT_FORCE_LOWERCASE_TAGS,
)
if force_lowercase:
updated_tag_names = [t.lower() for t in updated_tag_names]

# Remove tags which no longer apply
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actual change

tags_for_removal = [tag for tag in current_tags \
if tag.name not in updated_tag_names]
if len(tags_for_removal):
TaggedItem._default_manager.filter(content_type__pk=ctype.pk,
object_id=obj.pk,
tag__in=tags_for_removal).delete()
if replace_existing:
# Remove tags which no longer apply
tags_for_removal = [tag for tag in current_tags
if tag.name not in updated_tag_names]
if len(tags_for_removal):
TaggedItem._default_manager.filter(
content_type__pk=ctype.pk,
object_id=obj.pk,
tag__in=tags_for_removal,
).delete()

# Add new tags
current_tag_names = [tag.name for tag in current_tags]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bellow are pep8 fixes. check the tests ;)

for tag_name in updated_tag_names:
Expand All @@ -59,9 +67,16 @@ def add_tag(self, obj, tag_name):
if not len(tag_names):
raise AttributeError(_('No tags were given: "%s".') % tag_name)
if len(tag_names) > 1:
raise AttributeError(_('Multiple tags were given: "%s".') % tag_name)
raise AttributeError(
_('Multiple tags were given: "%s".') % tag_name
)
tag_name = tag_names[0]
if getattr(settings, 'FORCE_LOWERCASE_TAGS', DEFAULT_FORCE_LOWERCASE_TAGS):
force_lowercase = getattr(
settings,
'FORCE_LOWERCASE_TAGS',
DEFAULT_FORCE_LOWERCASE_TAGS,
)
if force_lowercase:
tag_name = tag_name.lower()
tag, created = self.get_or_create(name=tag_name)
ctype = ContentType.objects.get_for_model(obj)
Expand All @@ -77,12 +92,21 @@ def get_for_object(self, obj):
return self.filter(items__content_type__pk=ctype.pk,
items__object_id=obj.pk)

def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None):
def _get_usage(
self,
model,
counts=False,
min_count=None,
extra_joins=None,
extra_criteria=None,
params=None,
):
"""
Perform the custom SQL query for ``usage_for_model`` and
``usage_for_queryset``.
"""
if min_count is not None: counts = True
if min_count is not None:
counts = True

model_table = qn(model._meta.db_table)
model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
Expand Down Expand Up @@ -114,7 +138,10 @@ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extr
params.append(min_count)

cursor = connection.cursor()
cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
cursor.execute(
query % (extra_joins, extra_criteria, min_count_sql),
params
)
tags = []
for row in cursor.fetchall():
t = self.model(*row[:2])
Expand All @@ -123,7 +150,13 @@ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extr
tags.append(t)
return tags

def usage_for_model(self, model, counts=False, min_count=None, filters=None):
def usage_for_model(
self,
model,
counts=False,
min_count=None,
filters=None
):
"""
Obtain a list of tags associated with instances of the given
Model class.
Expand All @@ -141,7 +174,8 @@ def usage_for_model(self, model, counts=False, min_count=None, filters=None):
of field lookups to be applied to the given Model as the
``filters`` argument.
"""
if filters is None: filters = {}
if filters is None:
filters = {}

queryset = model._default_manager.filter()
for f in filters.items():
Expand Down Expand Up @@ -183,7 +217,14 @@ def usage_for_queryset(self, queryset, counts=False, min_count=None):
extra_criteria = 'AND %s' % where
else:
extra_criteria = ''
return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params)
return self._get_usage(
queryset.model,
counts,
min_count,
extra_joins,
extra_criteria,
params,
)

def related_for_model(self, tags, model, counts=False, min_count=None):
"""
Expand All @@ -198,13 +239,15 @@ def related_for_model(self, tags, model, counts=False, min_count=None):
greater than or equal to ``min_count`` will be returned.
Passing a value for ``min_count`` implies ``counts=True``.
"""
if min_count is not None: counts = True
if min_count is not None:
counts = True
tags = get_tag_list(tags)
tag_count = len(tags)
tagged_item_table = qn(TaggedItem._meta.db_table)
query = """
SELECT %(tag)s.id, %(tag)s.name%(count_sql)s
FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
FROM %(tagged_item)s
INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
AND %(tagged_item)s.object_id IN
(
Expand All @@ -221,12 +264,20 @@ def related_for_model(self, tags, model, counts=False, min_count=None):
%(min_count_sql)s
ORDER BY %(tag)s.name ASC""" % {
'tag': qn(self.model._meta.db_table),
'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
'count_sql': (
counts and
', COUNT(%s.object_id)' % tagged_item_table or
''
),
'tagged_item': tagged_item_table,
'content_type_id': ContentType.objects.get_for_model(model).pk,
'tag_id_placeholders': ','.join(['%s'] * tag_count),
'tag_count': tag_count,
'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
'min_count_sql': (
min_count is not None and
('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or
''
),
}

params = [tag.pk for tag in tags] * 2
Expand Down Expand Up @@ -272,6 +323,7 @@ def cloud_for_model(self, model, steps=4, distribution=LOGARITHMIC,
min_count=min_count))
return calculate_cloud(tags, steps, distribution)


class TaggedItemManager(models.Manager):
"""
FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING``
Expand Down Expand Up @@ -410,7 +462,12 @@ def get_related(self, obj, queryset_or_model, num=None):
related_content_type = ContentType.objects.get_for_model(model)
query = """
SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
FROM
%(model)s,
%(tagged_item)s,
%(tag)s,
%(tagged_item)s
related_tagged_item
WHERE %(tagged_item)s.object_id = %%s
AND %(tagged_item)s.content_type_id = %(content_type_id)s
AND %(tag)s.id = %(tagged_item)s.tag_id
Expand Down Expand Up @@ -446,10 +503,11 @@ def get_related(self, obj, queryset_or_model, num=None):
cursor.execute(query, params)
object_ids = [row[0] for row in cursor.fetchall()]
if len(object_ids) > 0:
# Use in_bulk here instead of an id__in lookup, because id__in would
# Use in_bulk here instead of an id__in lookup,
# because id__in would
# clobber the ordering.
object_dict = queryset.in_bulk(object_ids)
return [object_dict[object_id] for object_id in object_ids \
return [object_dict[object_id] for object_id in object_ids
if object_id in object_dict]
else:
return []
Expand All @@ -458,11 +516,17 @@ def get_related(self, obj, queryset_or_model, num=None):
# Models #
##########


class Tag(models.Model):
"""
A tag.
"""
name = models.CharField(_('name'), max_length=50, unique=True, db_index=True)
name = models.CharField(
_('name'),
max_length=50,
unique=True,
db_index=True,
)

objects = TagManager()

Expand All @@ -474,14 +538,18 @@ class Meta:
def __unicode__(self):
return self.name


class TaggedItem(models.Model):
"""
Holds the relationship between a tag and the item being tagged.
"""
tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
content_type = models.ForeignKey(ContentType, verbose_name=_('content type'))
object_id = models.PositiveIntegerField(_('object id'), db_index=True)
object = generic.GenericForeignKey('content_type', 'object_id')
tag = models.ForeignKey(Tag, verbose_name=_('tag'), related_name='items')
content_type = models.ForeignKey(
ContentType,
verbose_name=_('content type'),
)
object_id = models.PositiveIntegerField(_('object id'), db_index=True)
object = generic.GenericForeignKey('content_type', 'object_id')

objects = TaggedItemManager()

Expand Down
7 changes: 6 additions & 1 deletion tagging/tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

from tagging.fields import TagField


class Perch(models.Model):
size = models.IntegerField()
smelly = models.BooleanField(default=True)


class Parrot(models.Model):
state = models.CharField(max_length=50)
perch = models.ForeignKey(Perch, null=True)
Expand All @@ -16,6 +18,7 @@ def __unicode__(self):
class Meta:
ordering = ['state']


class Link(models.Model):
name = models.CharField(max_length=50)

Expand All @@ -25,6 +28,7 @@ def __unicode__(self):
class Meta:
ordering = ['name']


class Article(models.Model):
name = models.CharField(max_length=50)

Expand All @@ -34,9 +38,10 @@ def __unicode__(self):
class Meta:
ordering = ['name']


class FormTest(models.Model):
tags = TagField('Test', help_text='Test')


class FormTestNull(models.Model):
tags = TagField(null=True)

Loading