summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/taggit/__init__.py1
-rw-r--r--lib/taggit/admin.py16
-rw-r--r--lib/taggit/forms.py21
-rw-r--r--lib/taggit/locale/de/LC_MESSAGES/django.mobin0 -> 1331 bytes
-rw-r--r--lib/taggit/locale/de/LC_MESSAGES/django.po67
-rw-r--r--lib/taggit/locale/en/LC_MESSAGES/django.po68
-rw-r--r--lib/taggit/locale/he/LC_MESSAGES/django.mobin0 -> 847 bytes
-rw-r--r--lib/taggit/locale/he/LC_MESSAGES/django.po69
-rw-r--r--lib/taggit/locale/nl/LC_MESSAGES/django.mobin0 -> 1217 bytes
-rw-r--r--lib/taggit/locale/nl/LC_MESSAGES/django.po64
-rw-r--r--lib/taggit/locale/ru/LC_MESSAGES/django.mobin0 -> 1513 bytes
-rw-r--r--lib/taggit/locale/ru/LC_MESSAGES/django.po70
-rw-r--r--lib/taggit/managers.py244
-rw-r--r--lib/taggit/models.py160
-rw-r--r--lib/taggit/tests/__init__.py0
-rw-r--r--lib/taggit/tests/forms.py20
-rw-r--r--lib/taggit/tests/models.py143
-rwxr-xr-xlib/taggit/tests/runtests.py34
-rw-r--r--lib/taggit/tests/tests.py475
-rw-r--r--lib/taggit/utils.py126
-rw-r--r--lib/taggit/views.py18
-rw-r--r--lib/utils/pinboard.py558
-rw-r--r--lib/utils/pydelicious.py1045
23 files changed, 2154 insertions, 1045 deletions
diff --git a/lib/taggit/__init__.py b/lib/taggit/__init__.py
new file mode 100644
index 0000000..3a055a1
--- /dev/null
+++ b/lib/taggit/__init__.py
@@ -0,0 +1 @@
+VERSION = (0, 9, 2)
diff --git a/lib/taggit/admin.py b/lib/taggit/admin.py
new file mode 100644
index 0000000..5155b6f
--- /dev/null
+++ b/lib/taggit/admin.py
@@ -0,0 +1,16 @@
+from django.contrib import admin
+
+from taggit.models import Tag, TaggedItem
+
+
+class TaggedItemInline(admin.StackedInline):
+ model = TaggedItem
+
+class TagAdmin(admin.ModelAdmin):
+ list_display = ["name"]
+ inlines = [
+ TaggedItemInline
+ ]
+
+
+admin.site.register(Tag, TagAdmin)
diff --git a/lib/taggit/forms.py b/lib/taggit/forms.py
new file mode 100644
index 0000000..e0198bd
--- /dev/null
+++ b/lib/taggit/forms.py
@@ -0,0 +1,21 @@
+from django import forms
+from django.utils.translation import ugettext as _
+
+from taggit.utils import parse_tags, edit_string_for_tags
+
+
+class TagWidget(forms.TextInput):
+ def render(self, name, value, attrs=None):
+ if value is not None and not isinstance(value, basestring):
+ value = edit_string_for_tags([o.tag for o in value.select_related("tag")])
+ return super(TagWidget, self).render(name, value, attrs)
+
+class TagField(forms.CharField):
+ widget = TagWidget
+
+ def clean(self, value):
+ value = super(TagField, self).clean(value)
+ try:
+ return parse_tags(value)
+ except ValueError:
+ raise forms.ValidationError(_("Please provide a comma-separated list of tags."))
diff --git a/lib/taggit/locale/de/LC_MESSAGES/django.mo b/lib/taggit/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..3d9eaaf
--- /dev/null
+++ b/lib/taggit/locale/de/LC_MESSAGES/django.mo
Binary files differ
diff --git a/lib/taggit/locale/de/LC_MESSAGES/django.po b/lib/taggit/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 0000000..98ecdac
--- /dev/null
+++ b/lib/taggit/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,67 @@
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:26-0700\n"
+"PO-Revision-Date: 2010-09-07 09:26-0700\n"
+"Last-Translator: Jannis Leidel <jannis@leidel.info>\n"
+"Language-Team: German <de@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Bitte eine durch Komma getrennte Schlagwortliste eingeben."
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr "Schlagwörter"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "Eine durch Komma getrennte Schlagwortliste."
+
+#: models.py:10
+msgid "Name"
+msgstr "Name"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Kürzel"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Schlagwort"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s verschlagwortet mit %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Objekt-ID"
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr "Inhaltstyp"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Verschlagwortetes Objekt"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Verschlagwortete Objekte"
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
+"\" in your expression."
+msgstr ""
+"Bitte einen regulären Ausdruck eingeben. Fügen Sie \"(?i) \" dem "
+"Ausdruck hinzu, um nicht zwischen Groß- und Kleinschreibung zu "
+"unterscheiden."
diff --git a/lib/taggit/locale/en/LC_MESSAGES/django.po b/lib/taggit/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 0000000..c5642c7
--- /dev/null
+++ b/lib/taggit/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,68 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:45-0700\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr ""
+
+#: managers.py:39 managers.py:83 models.py:50
+msgid "Tags"
+msgstr ""
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr ""
+
+#: models.py:10
+msgid "Name"
+msgstr ""
+
+#: models.py:11
+msgid "Slug"
+msgstr ""
+
+#: models.py:49
+msgid "Tag"
+msgstr ""
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr ""
+
+#: models.py:100
+msgid "Object id"
+msgstr ""
+
+#: models.py:104 models.py:110
+msgid "Content type"
+msgstr ""
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr ""
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr ""
+
+#: contrib/suggest/models.py:57
+msgid ""
+"Enter a valid Regular Expression. To make it case-insensitive include \"(?i)"
+"\" in your expression."
+msgstr ""
diff --git a/lib/taggit/locale/he/LC_MESSAGES/django.mo b/lib/taggit/locale/he/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..562db71
--- /dev/null
+++ b/lib/taggit/locale/he/LC_MESSAGES/django.mo
Binary files differ
diff --git a/lib/taggit/locale/he/LC_MESSAGES/django.po b/lib/taggit/locale/he/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e27a878
--- /dev/null
+++ b/lib/taggit/locale/he/LC_MESSAGES/django.po
@@ -0,0 +1,69 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Django Taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-06-26 12:47-0500\n"
+"PO-Revision-Date: 2010-06-26 12:54-0600\n"
+"Last-Translator: Alex <alex.gaynor@gmail.com>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "נא לספק רשימה של תגים מופרדת עם פסיקים."
+
+#: managers.py:41
+#: managers.py:113
+#: models.py:18
+msgid "Tags"
+msgstr "תגיות"
+
+#: managers.py:114
+msgid "A comma-separated list of tags."
+msgstr "רשימה של תגים מופרדת עם פסיקים."
+
+#: models.py:10
+msgid "Name"
+msgstr "שם"
+
+#: models.py:11
+msgid "Slug"
+msgstr ""
+
+#: models.py:17
+msgid "Tag"
+msgstr "תג"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s מתויג עם %(tag)s"
+
+#: models.py:86
+msgid "Object id"
+msgstr ""
+
+#: models.py:87
+msgid "Content type"
+msgstr ""
+
+#: models.py:92
+msgid "Tagged Item"
+msgstr ""
+
+#: models.py:93
+msgid "Tagged Items"
+msgstr ""
+
+#: contrib/suggest/models.py:57
+msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
+msgstr ""
+
diff --git a/lib/taggit/locale/nl/LC_MESSAGES/django.mo b/lib/taggit/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..28e7b7e
--- /dev/null
+++ b/lib/taggit/locale/nl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/lib/taggit/locale/nl/LC_MESSAGES/django.po b/lib/taggit/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 0000000..7871b0b
--- /dev/null
+++ b/lib/taggit/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,64 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: django-taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-09-07 09:45-0700\n"
+"PO-Revision-Date: 2010-09-07 23:04+0100\n"
+"Last-Translator: Jeffrey Gelens <jeffrey@gelens.org>\n"
+"Language-Team: Dutch\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Geef een door komma gescheiden lijst van tags."
+
+#: managers.py:39
+#: managers.py:83
+#: models.py:50
+msgid "Tags"
+msgstr "Tags"
+
+#: managers.py:84
+msgid "A comma-separated list of tags."
+msgstr "Een door komma gescheiden lijst van tags."
+
+#: models.py:10
+msgid "Name"
+msgstr "Naam"
+
+#: models.py:11
+msgid "Slug"
+msgstr "Slug"
+
+#: models.py:49
+msgid "Tag"
+msgstr "Tag"
+
+#: models.py:56
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "%(object)s getagged met %(tag)s"
+
+#: models.py:100
+msgid "Object id"
+msgstr "Object-id"
+
+#: models.py:104
+#: models.py:110
+msgid "Content type"
+msgstr "Inhoudstype"
+
+#: models.py:138
+msgid "Tagged Item"
+msgstr "Object getagged"
+
+#: models.py:139
+msgid "Tagged Items"
+msgstr "Objecten getagged"
+
+#: contrib/suggest/models.py:57
+msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
+msgstr "Voer een valide reguliere expressie in. Voeg \"(?i)\" aan de expressie toe om deze hoofdletter ongevoelig te maken."
+
diff --git a/lib/taggit/locale/ru/LC_MESSAGES/django.mo b/lib/taggit/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..61a7e39
--- /dev/null
+++ b/lib/taggit/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/lib/taggit/locale/ru/LC_MESSAGES/django.po b/lib/taggit/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 0000000..42e3ebe
--- /dev/null
+++ b/lib/taggit/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,70 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Django Taggit\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-06-11 11:28+0700\n"
+"PO-Revision-Date: 2010-06-11 11:30+0700\n"
+"Last-Translator: Igor 'idle sign' Starikov <idlesign@yandex.ru>\n"
+"Language-Team: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Poedit-Language: Russian\n"
+
+#: forms.py:20
+msgid "Please provide a comma-separated list of tags."
+msgstr "Укажите метки через запятую."
+
+#: managers.py:41
+#: managers.py:101
+#: models.py:17
+msgid "Tags"
+msgstr "Метки"
+
+#: managers.py:102
+msgid "A comma-separated list of tags."
+msgstr "Список меток через запятую."
+
+#: models.py:9
+msgid "Name"
+msgstr "Название"
+
+#: models.py:10
+msgid "Slug"
+msgstr "Слаг"
+
+#: models.py:16
+msgid "Tag"
+msgstr "Метка"
+
+#: models.py:55
+#, python-format
+msgid "%(object)s tagged with %(tag)s"
+msgstr "элемент «%(object)s» с меткой «%(tag)s»"
+
+#: models.py:82
+msgid "Object id"
+msgstr "ID объекта"
+
+#: models.py:83
+msgid "Content type"
+msgstr "Тип содержимого"
+
+#: models.py:87
+msgid "Tagged Item"
+msgstr "Элемент с меткой"
+
+#: models.py:88
+msgid "Tagged Items"
+msgstr "Элементы с меткой"
+
+#: contrib/suggest/models.py:57
+msgid "Enter a valid Regular Expression. To make it case-insensitive include \"(?i)\" in your expression."
+msgstr "Введите регулярное выражение. Чтобы сделать его чувствительным к регистру укажите \"(?i)\"."
+
diff --git a/lib/taggit/managers.py b/lib/taggit/managers.py
new file mode 100644
index 0000000..55201b4
--- /dev/null
+++ b/lib/taggit/managers.py
@@ -0,0 +1,244 @@
+from django.contrib.contenttypes.generic import GenericRelation
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+from django.db.models.fields.related import ManyToManyRel, RelatedField, add_lazy_relation
+from django.db.models.related import RelatedObject
+from django.utils.text import capfirst
+from django.utils.translation import ugettext_lazy as _
+
+from taggit.forms import TagField
+from taggit.models import TaggedItem, GenericTaggedItemBase
+from taggit.utils import require_instance_manager
+
+
+try:
+ all
+except NameError:
+ # 2.4 compat
+ try:
+ from django.utils.itercompat import all
+ except ImportError:
+ # 1.1.X compat
+ def all(iterable):
+ for item in iterable:
+ if not item:
+ return False
+ return True
+
+
+class TaggableRel(ManyToManyRel):
+ def __init__(self):
+ self.related_name = None
+ self.limit_choices_to = {}
+ self.symmetrical = True
+ self.multiple = True
+ self.through = None
+
+
+class TaggableManager(RelatedField):
+ def __init__(self, verbose_name=_("Tags"),
+ help_text=_("A comma-separated list of tags."), through=None, blank=False):
+ self.through = through or TaggedItem
+ self.rel = TaggableRel()
+ self.verbose_name = verbose_name
+ self.help_text = help_text
+ self.blank = blank
+ self.editable = True
+ self.unique = False
+ self.creates_table = False
+ self.db_column = None
+ self.choices = None
+ self.serialize = False
+ self.null = True
+ self.creation_counter = models.Field.creation_counter
+ models.Field.creation_counter += 1
+
+ def __get__(self, instance, model):
+ if instance is not None and instance.pk is None:
+ raise ValueError("%s objects need to have a primary key value "
+ "before you can access their tags." % model.__name__)
+ manager = _TaggableManager(
+ through=self.through, model=model, instance=instance
+ )
+ return manager
+
+ def contribute_to_class(self, cls, name):
+ self.name = self.column = name
+ self.model = cls
+ cls._meta.add_field(self)
+ setattr(cls, name, self)
+ if not cls._meta.abstract:
+ if isinstance(self.through, basestring):
+ def resolve_related_class(field, model, cls):
+ self.through = model
+ self.post_through_setup(cls)
+ add_lazy_relation(
+ cls, self, self.through, resolve_related_class
+ )
+ else:
+ self.post_through_setup(cls)
+
+ def post_through_setup(self, cls):
+ self.use_gfk = (
+ self.through is None or issubclass(self.through, GenericTaggedItemBase)
+ )
+ self.rel.to = self.through._meta.get_field("tag").rel.to
+ if self.use_gfk:
+ tagged_items = GenericRelation(self.through)
+ tagged_items.contribute_to_class(cls, "tagged_items")
+
+ def save_form_data(self, instance, value):
+ getattr(instance, self.name).set(*value)
+
+ def formfield(self, form_class=TagField, **kwargs):
+ defaults = {
+ "label": capfirst(self.verbose_name),
+ "help_text": self.help_text,
+ "required": not self.blank
+ }
+ defaults.update(kwargs)
+ return form_class(**defaults)
+
+ def value_from_object(self, instance):
+ if instance.pk:
+ return self.through.objects.filter(**self.through.lookup_kwargs(instance))
+ return self.through.objects.none()
+
+ def related_query_name(self):
+ return self.model._meta.module_name
+
+ def m2m_reverse_name(self):
+ return self.through._meta.get_field_by_name("tag")[0].column
+
+ def m2m_target_field_name(self):
+ return self.model._meta.pk.name
+
+ def m2m_reverse_target_field_name(self):
+ return self.rel.to._meta.pk.name
+
+ def m2m_column_name(self):
+ if self.use_gfk:
+ return self.through._meta.virtual_fields[0].fk_field
+ return self.through._meta.get_field('content_object').column
+
+ def db_type(self, connection=None):
+ return None
+
+ def m2m_db_table(self):
+ return self.through._meta.db_table
+
+ def extra_filters(self, pieces, pos, negate):
+ if negate or not self.use_gfk:
+ return []
+ prefix = "__".join(["tagged_items"] + pieces[:pos-2])
+ cts = map(ContentType.objects.get_for_model, _get_subclasses(self.model))
+ if len(cts) == 1:
+ return [("%s__content_type" % prefix, cts[0])]
+ return [("%s__content_type__in" % prefix, cts)]
+
+ def bulk_related_objects(self, new_objs, using):
+ return []
+
+
+class _TaggableManager(models.Manager):
+ def __init__(self, through, model, instance):
+ self.through = through
+ self.model = model
+ self.instance = instance
+
+ def get_query_set(self):
+ return self.through.tags_for(self.model, self.instance)
+
+ def _lookup_kwargs(self):
+ return self.through.lookup_kwargs(self.instance)
+
+ @require_instance_manager
+ def add(self, *tags):
+ str_tags = set([
+ t
+ for t in tags
+ if not isinstance(t, self.through.tag_model())
+ ])
+ tag_objs = set(tags) - str_tags
+ # If str_tags has 0 elements Django actually optimizes that to not do a
+ # query. Malcolm is very smart.
+ existing = self.through.tag_model().objects.filter(
+ name__in=str_tags
+ )
+ tag_objs.update(existing)
+
+ for new_tag in str_tags - set(t.name for t in existing):
+ tag_objs.add(self.through.tag_model().objects.create(name=new_tag))
+
+ for tag in tag_objs:
+ self.through.objects.get_or_create(tag=tag, **self._lookup_kwargs())
+
+ @require_instance_manager
+ def set(self, *tags):
+ self.clear()
+ self.add(*tags)
+
+ @require_instance_manager
+ def remove(self, *tags):
+ self.through.objects.filter(**self._lookup_kwargs()).filter(
+ tag__name__in=tags).delete()
+
+ @require_instance_manager
+ def clear(self):
+ self.through.objects.filter(**self._lookup_kwargs()).delete()
+
+ def most_common(self):
+ return self.get_query_set().annotate(
+ num_times=models.Count(self.through.tag_relname())
+ ).order_by('-num_times')
+
+ @require_instance_manager
+ def similar_objects(self):
+ lookup_kwargs = self._lookup_kwargs()
+ lookup_keys = sorted(lookup_kwargs)
+ qs = self.through.objects.values(*lookup_kwargs.keys())
+ qs = qs.annotate(n=models.Count('pk'))
+ qs = qs.exclude(**lookup_kwargs)
+ qs = qs.filter(tag__in=self.all())
+ qs = qs.order_by('-n')
+
+ # TODO: This all feels like a bit of a hack.
+ items = {}
+ if len(lookup_keys) == 1:
+ # Can we do this without a second query by using a select_related()
+ # somehow?
+ f = self.through._meta.get_field_by_name(lookup_keys[0])[0]
+ objs = f.rel.to._default_manager.filter(**{
+ "%s__in" % f.rel.field_name: [r["content_object"] for r in qs]
+ })
+ for obj in objs:
+ items[(getattr(obj, f.rel.field_name),)] = obj
+ else:
+ preload = {}
+ for result in qs:
+ preload.setdefault(result['content_type'], set())
+ preload[result["content_type"]].add(result["object_id"])
+
+ for ct, obj_ids in preload.iteritems():
+ ct = ContentType.objects.get_for_id(ct)
+ for obj in ct.model_class()._default_manager.filter(pk__in=obj_ids):
+ items[(ct.pk, obj.pk)] = obj
+
+ results = []
+ for result in qs:
+ obj = items[
+ tuple(result[k] for k in lookup_keys)
+ ]
+ obj.similar_tags = result["n"]
+ results.append(obj)
+ return results
+
+
+def _get_subclasses(model):
+ subclasses = [model]
+ for f in model._meta.get_all_field_names():
+ field = model._meta.get_field_by_name(f)[0]
+ if (isinstance(field, RelatedObject) and
+ getattr(field.field.rel, "parent_link", None)):
+ subclasses.extend(_get_subclasses(field.model))
+ return subclasses
diff --git a/lib/taggit/models.py b/lib/taggit/models.py
new file mode 100644
index 0000000..d8a0a41
--- /dev/null
+++ b/lib/taggit/models.py
@@ -0,0 +1,160 @@
+import django
+from django.contrib.contenttypes.models import ContentType
+from django.contrib.contenttypes.generic import GenericForeignKey
+from django.db import models, IntegrityError, transaction
+from django.template.defaultfilters import slugify as default_slugify
+from django.utils.translation import ugettext_lazy as _, ugettext
+
+
+class TagBase(models.Model):
+ name = models.CharField(verbose_name=_('Name'), max_length=100)
+ slug = models.SlugField(verbose_name=_('Slug'), unique=True, max_length=100)
+
+ def __unicode__(self):
+ return self.name
+
+ class Meta:
+ abstract = True
+
+ def save(self, *args, **kwargs):
+ if not self.pk and not self.slug:
+ self.slug = self.slugify(self.name)
+ if django.VERSION >= (1, 2):
+ from django.db import router
+ using = kwargs.get("using") or router.db_for_write(
+ type(self), instance=self)
+ # Make sure we write to the same db for all attempted writes,
+ # with a multi-master setup, theoretically we could try to
+ # write and rollback on different DBs
+ kwargs["using"] = using
+ trans_kwargs = {"using": using}
+ else:
+ trans_kwargs = {}
+ i = 0
+ while True:
+ i += 1
+ try:
+ sid = transaction.savepoint(**trans_kwargs)
+ res = super(TagBase, self).save(*args, **kwargs)
+ transaction.savepoint_commit(sid, **trans_kwargs)
+ return res
+ except IntegrityError:
+ transaction.savepoint_rollback(sid, **trans_kwargs)
+ self.slug = self.slugify(self.name, i)
+ else:
+ return super(TagBase, self).save(*args, **kwargs)
+
+ def slugify(self, tag, i=None):
+ slug = default_slugify(tag)
+ if i is not None:
+ slug += "_%d" % i
+ return slug
+
+
+class Tag(TagBase):
+ class Meta:
+ verbose_name = _("Tag")
+ verbose_name_plural = _("Tags")
+
+
+
+class ItemBase(models.Model):
+ def __unicode__(self):
+ return ugettext("%(object)s tagged with %(tag)s") % {
+ "object": self.content_object,
+ "tag": self.tag
+ }
+
+ class Meta:
+ abstract = True
+
+ @classmethod
+ def tag_model(cls):
+ return cls._meta.get_field_by_name("tag")[0].rel.to
+
+ @classmethod
+ def tag_relname(cls):
+ return cls._meta.get_field_by_name('tag')[0].rel.related_name
+
+ @classmethod
+ def lookup_kwargs(cls, instance):
+ return {
+ 'content_object': instance
+ }
+
+ @classmethod
+ def bulk_lookup_kwargs(cls, instances):
+ return {
+ "content_object__in": instances,
+ }
+
+
+class TaggedItemBase(ItemBase):
+ if django.VERSION < (1, 2):
+ tag = models.ForeignKey(Tag, related_name="%(class)s_items")
+ else:
+ tag = models.ForeignKey(Tag, related_name="%(app_label)s_%(class)s_items")
+
+ class Meta:
+ abstract = True
+
+ @classmethod
+ def tags_for(cls, model, instance=None):
+ if instance is not None:
+ return cls.tag_model().objects.filter(**{
+ '%s__content_object' % cls.tag_relname(): instance
+ })
+ return cls.tag_model().objects.filter(**{
+ '%s__content_object__isnull' % cls.tag_relname(): False
+ }).distinct()
+
+
+class GenericTaggedItemBase(ItemBase):
+ object_id = models.IntegerField(verbose_name=_('Object id'), db_index=True)
+ if django.VERSION < (1, 2):
+ content_type = models.ForeignKey(
+ ContentType,
+ verbose_name=_('Content type'),
+ related_name="%(class)s_tagged_items"
+ )
+ else:
+ content_type = models.ForeignKey(
+ ContentType,
+ verbose_name=_('Content type'),
+ related_name="%(app_label)s_%(class)s_tagged_items"
+ )
+ content_object = GenericForeignKey()
+
+ class Meta:
+ abstract=True
+
+ @classmethod
+ def lookup_kwargs(cls, instance):
+ return {
+ 'object_id': instance.pk,
+ 'content_type': ContentType.objects.get_for_model(instance)
+ }
+
+ @classmethod
+ def bulk_lookup_kwargs(cls, instances):
+ # TODO: instances[0], can we assume there are instances.
+ return {
+ "object_id__in": [instance.pk for instance in instances],
+ "content_type": ContentType.objects.get_for_model(instances[0]),
+ }
+
+ @classmethod
+ def tags_for(cls, model, instance=None):
+ ct = ContentType.objects.get_for_model(model)
+ kwargs = {
+ "%s__content_type" % cls.tag_relname(): ct
+ }
+ if instance is not None:
+ kwargs["%s__object_id" % cls.tag_relname()] = instance.pk
+ return cls.tag_model().objects.filter(**kwargs).distinct()
+
+
+class TaggedItem(GenericTaggedItemBase, TaggedItemBase):
+ class Meta:
+ verbose_name = _("Tagged Item")
+ verbose_name_plural = _("Tagged Items")
diff --git a/lib/taggit/tests/__init__.py b/lib/taggit/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/lib/taggit/tests/__init__.py
diff --git a/lib/taggit/tests/forms.py b/lib/taggit/tests/forms.py
new file mode 100644
index 0000000..2cdc6a8
--- /dev/null
+++ b/lib/taggit/tests/forms.py
@@ -0,0 +1,20 @@
+from django import forms
+
+from taggit.tests.models import Food, DirectFood, CustomPKFood, OfficialFood
+
+
+class FoodForm(forms.ModelForm):
+ class Meta:
+ model = Food
+
+class DirectFoodForm(forms.ModelForm):
+ class Meta:
+ model = DirectFood
+
+class CustomPKFoodForm(forms.ModelForm):
+ class Meta:
+ model = CustomPKFood
+
+class OfficialFoodForm(forms.ModelForm):
+ class Meta:
+ model = OfficialFood
diff --git a/lib/taggit/tests/models.py b/lib/taggit/tests/models.py
new file mode 100644
index 0000000..a0e21e0
--- /dev/null
+++ b/lib/taggit/tests/models.py
@@ -0,0 +1,143 @@
+from django.db import models
+
+from taggit.managers import TaggableManager
+from taggit.models import (TaggedItemBase, GenericTaggedItemBase, TaggedItem,
+ TagBase, Tag)
+
+
+class Food(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager()
+
+ def __unicode__(self):
+ return self.name
+
+class Pet(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager()
+
+ def __unicode__(self):
+ return self.name
+
+class HousePet(Pet):
+ trained = models.BooleanField()
+
+
+# Test direct-tagging with custom through model
+
+class TaggedFood(TaggedItemBase):
+ content_object = models.ForeignKey('DirectFood')
+
+class TaggedPet(TaggedItemBase):
+ content_object = models.ForeignKey('DirectPet')
+
+class DirectFood(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through="TaggedFood")
+
+class DirectPet(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through=TaggedPet)
+
+ def __unicode__(self):
+ return self.name
+
+class DirectHousePet(DirectPet):
+ trained = models.BooleanField()
+
+
+# Test custom through model to model with custom PK
+
+class TaggedCustomPKFood(TaggedItemBase):
+ content_object = models.ForeignKey('CustomPKFood')
+
+class TaggedCustomPKPet(TaggedItemBase):
+ content_object = models.ForeignKey('CustomPKPet')
+
+class CustomPKFood(models.Model):
+ name = models.CharField(max_length=50, primary_key=True)
+
+ tags = TaggableManager(through=TaggedCustomPKFood)
+
+ def __unicode__(self):
+ return self.name
+
+class CustomPKPet(models.Model):
+ name = models.CharField(max_length=50, primary_key=True)
+
+ tags = TaggableManager(through=TaggedCustomPKPet)
+
+ def __unicode__(self):
+ return self.name
+
+class CustomPKHousePet(CustomPKPet):
+ trained = models.BooleanField()
+
+# Test custom through model to a custom tag model
+
+class OfficialTag(TagBase):
+ official = models.BooleanField()
+
+class OfficialThroughModel(GenericTaggedItemBase):
+ tag = models.ForeignKey(OfficialTag, related_name="tagged_items")
+
+class OfficialFood(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through=OfficialThroughModel)
+
+ def __unicode__(self):
+ return self.name
+
+class OfficialPet(models.Model):
+ name = models.CharField(max_length=50)
+
+ tags = TaggableManager(through=OfficialThroughModel)
+
+ def __unicode__(self):
+ return self.name
+
+class OfficialHousePet(OfficialPet):
+ trained = models.BooleanField()
+
+
+class Media(models.Model):
+ tags = TaggableManager()
+
+ class Meta:
+ abstract = True
+
+class Photo(Media):
+ pass
+
+class Movie(Media):
+ pass
+
+
+class ArticleTag(Tag):
+ class Meta:
+ proxy = True
+
+ def slugify(self, tag, i=None):
+ slug = "category-%s" % tag.lower()
+
+ if i is not None:
+ slug += "-%d" % i
+ return slug
+
+class ArticleTaggedItem(TaggedItem):
+ class Meta:
+ proxy = True
+
+ @classmethod
+ def tag_model(self):
+ return ArticleTag
+
+class Article(models.Model):
+ title = models.CharField(max_length=100)
+
+ tags = TaggableManager(through=ArticleTaggedItem)
diff --git a/lib/taggit/tests/runtests.py b/lib/taggit/tests/runtests.py
new file mode 100755
index 0000000..23bfb91
--- /dev/null
+++ b/lib/taggit/tests/runtests.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+import os
+import sys
+
+from django.conf import settings
+
+if not settings.configured:
+ settings.configure(
+ DATABASE_ENGINE='sqlite3',
+ INSTALLED_APPS=[
+ 'django.contrib.contenttypes',
+ 'taggit',
+ 'taggit.tests',
+ ]
+ )
+
+from django.test.simple import run_tests
+
+
+def runtests(*test_args):
+ if not test_args:
+ test_args = ['tests']
+ parent = os.path.join(
+ os.path.dirname(os.path.abspath(__file__)),
+ "..",
+ "..",
+ )
+ sys.path.insert(0, parent)
+ failures = run_tests(test_args, verbosity=1, interactive=True)
+ sys.exit(failures)
+
+
+if __name__ == '__main__':
+ runtests(*sys.argv[1:])
diff --git a/lib/taggit/tests/tests.py b/lib/taggit/tests/tests.py
new file mode 100644
index 0000000..e627701
--- /dev/null
+++ b/lib/taggit/tests/tests.py
@@ -0,0 +1,475 @@
+from unittest import TestCase as UnitTestCase
+
+import django
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import connection
+from django.test import TestCase, TransactionTestCase
+
+from taggit.managers import TaggableManager
+from taggit.models import Tag, TaggedItem
+from taggit.tests.forms import (FoodForm, DirectFoodForm, CustomPKFoodForm,
+ OfficialFoodForm)
+from taggit.tests.models import (Food, Pet, HousePet, DirectFood, DirectPet,
+ DirectHousePet, TaggedPet, CustomPKFood, CustomPKPet, CustomPKHousePet,
+ TaggedCustomPKPet, OfficialFood, OfficialPet, OfficialHousePet,
+ OfficialThroughModel, OfficialTag, Photo, Movie, Article)
+from taggit.utils import parse_tags, edit_string_for_tags
+
+
+class BaseTaggingTest(object):
+ def assert_tags_equal(self, qs, tags, sort=True, attr="name"):
+ got = map(lambda tag: getattr(tag, attr), qs)
+ if sort:
+ got.sort()
+ tags.sort()
+ self.assertEqual(got, tags)
+
+ def assert_num_queries(self, n, f, *args, **kwargs):
+ original_DEBUG = settings.DEBUG
+ settings.DEBUG = True
+ current = len(connection.queries)
+ try:
+ f(*args, **kwargs)
+ self.assertEqual(
+ len(connection.queries) - current,
+ n,
+ )
+ finally:
+ settings.DEBUG = original_DEBUG
+
+ def _get_form_str(self, form_str):
+ if django.VERSION >= (1, 3):
+ form_str %= {
+ "help_start": '<span class="helptext">',
+ "help_stop": "</span>"
+ }
+ else:
+ form_str %= {
+ "help_start": "",
+ "help_stop": ""
+ }
+ return form_str
+
+ def assert_form_renders(self, form, html):
+ self.assertEqual(str(form), self._get_form_str(html))
+
+class BaseTaggingTestCase(TestCase, BaseTaggingTest):
+ pass
+
+class BaseTaggingTransactionTestCase(TransactionTestCase, BaseTaggingTest):
+ pass
+
+
+class TagModelTestCase(BaseTaggingTransactionTestCase):
+ food_model = Food
+ tag_model = Tag
+
+ def test_unique_slug(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("Red", "red")
+
+ def test_update(self):
+ special = self.tag_model.objects.create(name="special")
+ special.save()
+
+ def test_add(self):
+ apple = self.food_model.objects.create(name="apple")
+ yummy = self.tag_model.objects.create(name="yummy")
+ apple.tags.add(yummy)
+
+ def test_slugify(self):
+ a = Article.objects.create(title="django-taggit 1.0 Released")
+ a.tags.add("awesome", "release", "AWESOME")
+ self.assert_tags_equal(a.tags.all(), [
+ "category-awesome",
+ "category-release",
+ "category-awesome-1"
+ ], attr="slug")
+
+class TagModelDirectTestCase(TagModelTestCase):
+ food_model = DirectFood
+ tag_model = Tag
+
+class TagModelCustomPKTestCase(TagModelTestCase):
+ food_model = CustomPKFood
+ tag_model = Tag
+
+class TagModelOfficialTestCase(TagModelTestCase):
+ food_model = OfficialFood
+ tag_model = OfficialTag
+
+class TaggableManagerTestCase(BaseTaggingTestCase):
+ food_model = Food
+ pet_model = Pet
+ housepet_model = HousePet
+ taggeditem_model = TaggedItem
+ tag_model = Tag
+
+ def test_add_tag(self):
+ apple = self.food_model.objects.create(name="apple")
+ self.assertEqual(list(apple.tags.all()), [])
+ self.assertEqual(list(self.food_model.tags.all()), [])
+
+ apple.tags.add('green')
+ self.assert_tags_equal(apple.tags.all(), ['green'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green'])
+
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add('green')
+ self.assert_tags_equal(pear.tags.all(), ['green'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green'])
+
+ apple.tags.add('red')
+ self.assert_tags_equal(apple.tags.all(), ['green', 'red'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
+
+ self.assert_tags_equal(
+ self.food_model.tags.most_common(),
+ ['green', 'red'],
+ sort=False
+ )
+
+ apple.tags.remove('green')
+ self.assert_tags_equal(apple.tags.all(), ['red'])
+ self.assert_tags_equal(self.food_model.tags.all(), ['green', 'red'])
+ tag = self.tag_model.objects.create(name="delicious")
+ apple.tags.add(tag)
+ self.assert_tags_equal(apple.tags.all(), ["red", "delicious"])
+
+ apple.delete()
+ self.assert_tags_equal(self.food_model.tags.all(), ["green"])
+
+ def test_add_queries(self):
+ apple = self.food_model.objects.create(name="apple")
+ # 1 query to see which tags exist
+ # + 3 queries to create the tags.
+ # + 6 queries to create the intermediary things (including SELECTs, to
+ # make sure we don't double create.
+ self.assert_num_queries(10, apple.tags.add, "red", "delicious", "green")
+
+ pear = self.food_model.objects.create(name="pear")
+ # 1 query to see which tags exist
+ # + 4 queries to create the intermeidary things (including SELECTs, to
+ # make sure we dont't double create.
+ self.assert_num_queries(5, pear.tags.add, "green", "delicious")
+
+ self.assert_num_queries(0, pear.tags.add)
+
+ def test_require_pk(self):
+ food_instance = self.food_model()
+ self.assertRaises(ValueError, lambda: food_instance.tags.all())
+
+ def test_delete_obj(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red")
+ self.assert_tags_equal(apple.tags.all(), ["red"])
+ strawberry = self.food_model.objects.create(name="strawberry")
+ strawberry.tags.add("red")
+ apple.delete()
+ self.assert_tags_equal(strawberry.tags.all(), ["red"])
+
+ def test_delete_bulk(self):
+ apple = self.food_model.objects.create(name="apple")
+ kitty = self.pet_model.objects.create(pk=apple.pk, name="kitty")
+
+ apple.tags.add("red", "delicious", "fruit")
+ kitty.tags.add("feline")
+
+ self.food_model.objects.all().delete()
+
+ self.assert_tags_equal(kitty.tags.all(), ["feline"])
+
+ def test_lookup_by_tag(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red", "green")
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green")
+
+ self.assertEqual(
+ list(self.food_model.objects.filter(tags__name__in=["red"])),
+ [apple]
+ )
+ self.assertEqual(
+ list(self.food_model.objects.filter(tags__name__in=["green"])),
+ [apple, pear]
+ )
+
+ kitty = self.pet_model.objects.create(name="kitty")
+ kitty.tags.add("fuzzy", "red")
+ dog = self.pet_model.objects.create(name="dog")
+ dog.tags.add("woof", "red")
+ self.assertEqual(
+ list(self.food_model.objects.filter(tags__name__in=["red"]).distinct()),
+ [apple]
+ )
+
+ tag = self.tag_model.objects.get(name="woof")
+ self.assertEqual(list(self.pet_model.objects.filter(tags__in=[tag])), [dog])
+
+ cat = self.housepet_model.objects.create(name="cat", trained=True)
+ cat.tags.add("fuzzy")
+
+ self.assertEqual(
+ map(lambda o: o.pk, self.pet_model.objects.filter(tags__name__in=["fuzzy"])),
+ [kitty.pk, cat.pk]
+ )
+
+ def test_exclude(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("red", "green", "delicious")
+
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green", "delicious")
+
+ guava = self.food_model.objects.create(name="guava")
+
+ self.assertEqual(
+ map(lambda o: o.pk, self.food_model.objects.exclude(tags__name__in=["red"])),
+ [pear.pk, guava.pk],
+ )
+
+ def test_similarity_by_tag(self):
+ """Test that pears are more similar to apples than watermelons"""
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("green", "juicy", "small", "sour")
+
+ pear = self.food_model.objects.create(name="pear")
+ pear.tags.add("green", "juicy", "small", "sweet")
+
+ watermelon = self.food_model.objects.create(name="watermelon")
+ watermelon.tags.add("green", "juicy", "large", "sweet")
+
+ similar_objs = apple.tags.similar_objects()
+ self.assertEqual(similar_objs, [pear, watermelon])
+ self.assertEqual(map(lambda x: x.similar_tags, similar_objs), [3, 2])
+
+ def test_tag_reuse(self):
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("juicy", "juicy")
+ self.assert_tags_equal(apple.tags.all(), ['juicy'])
+
+ def test_query_traverse(self):
+ spot = self.pet_model.objects.create(name='Spot')
+ spike = self.pet_model.objects.create(name='Spike')
+ spot.tags.add('scary')
+ spike.tags.add('fluffy')
+ lookup_kwargs = {
+ '%s__name' % self.pet_model._meta.module_name: 'Spot'
+ }
+ self.assert_tags_equal(
+ self.tag_model.objects.filter(**lookup_kwargs),
+ ['scary']
+ )
+
+ def test_taggeditem_unicode(self):
+ ross = self.pet_model.objects.create(name="ross")
+ # I keep Ross Perot for a pet, what's it to you?
+ ross.tags.add("president")
+
+ self.assertEqual(
+ unicode(self.taggeditem_model.objects.all()[0]),
+ "ross tagged with president"
+ )
+
+ def test_abstract_subclasses(self):
+ p = Photo.objects.create()
+ p.tags.add("outdoors", "pretty")
+ self.assert_tags_equal(
+ p.tags.all(),
+ ["outdoors", "pretty"]
+ )
+
+ m = Movie.objects.create()
+ m.tags.add("hd")
+ self.assert_tags_equal(
+ m.tags.all(),
+ ["hd"],
+ )
+
+
+class TaggableManagerDirectTestCase(TaggableManagerTestCase):
+ food_model = DirectFood
+ pet_model = DirectPet
+ housepet_model = DirectHousePet
+ taggeditem_model = TaggedPet
+
+class TaggableManagerCustomPKTestCase(TaggableManagerTestCase):
+ food_model = CustomPKFood
+ pet_model = CustomPKPet
+ housepet_model = CustomPKHousePet
+ taggeditem_model = TaggedCustomPKPet
+
+ def test_require_pk(self):
+ # TODO with a charfield pk, pk is never None, so taggit has no way to
+ # tell if the instance is saved or not
+ pass
+
+class TaggableManagerOfficialTestCase(TaggableManagerTestCase):
+ food_model = OfficialFood
+ pet_model = OfficialPet
+ housepet_model = OfficialHousePet
+ taggeditem_model = OfficialThroughModel
+ tag_model = OfficialTag
+
+ def test_extra_fields(self):
+ self.tag_model.objects.create(name="red")
+ self.tag_model.objects.create(name="delicious", official=True)
+ apple = self.food_model.objects.create(name="apple")
+ apple.tags.add("delicious", "red")
+
+ pear = self.food_model.objects.create(name="Pear")
+ pear.tags.add("delicious")
+
+ self.assertEqual(
+ map(lambda o: o.pk, self.food_model.objects.filter(tags__official=False)),
+ [apple.pk],
+ )
+
+
+class TaggableFormTestCase(BaseTaggingTestCase):
+ form_class = FoodForm
+ food_model = Food
+
+ def test_form(self):
+ self.assertEqual(self.form_class.base_fields.keys(), ['name', 'tags'])
+
+ f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy'})
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+ f.save()
+ apple = self.food_model.objects.get(name='apple')
+ self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy'])
+
+ f = self.form_class({'name': 'apple', 'tags': 'green, red, yummy, delicious'}, instance=apple)
+ f.save()
+ apple = self.food_model.objects.get(name='apple')
+ self.assert_tags_equal(apple.tags.all(), ['green', 'red', 'yummy', 'delicious'])
+ self.assertEqual(self.food_model.objects.count(), 1)
+
+ f = self.form_class({"name": "raspberry"})
+ self.assertFalse(f.is_valid())
+
+ f = self.form_class(instance=apple)
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+
+ apple.tags.add('has,comma')
+ f = self.form_class(instance=apple)
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="&quot;has,comma&quot;, delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+
+ apple.tags.add('has space')
+ f = self.form_class(instance=apple)
+ self.assert_form_renders(f, """<tr><th><label for="id_name">Name:</label></th><td><input id="id_name" type="text" name="name" value="apple" maxlength="50" /></td></tr>
+<tr><th><label for="id_tags">Tags:</label></th><td><input type="text" name="tags" value="&quot;has space&quot;, &quot;has,comma&quot;, delicious, green, red, yummy" id="id_tags" /><br />%(help_start)sA comma-separated list of tags.%(help_stop)s</td></tr>""")
+
+ def test_formfield(self):
+ tm = TaggableManager(verbose_name='categories', help_text='Add some categories', blank=True)
+ ff = tm.formfield()
+ self.assertEqual(ff.label, 'Categories')
+ self.assertEqual(ff.help_text, u'Add some categories')
+ self.assertEqual(ff.required, False)
+
+ self.assertEqual(ff.clean(""), [])
+
+ tm = TaggableManager()
+ ff = tm.formfield()
+ self.assertRaises(ValidationError, ff.clean, "")
+
+class TaggableFormDirectTestCase(TaggableFormTestCase):
+ form_class = DirectFoodForm
+ food_model = DirectFood
+
+class TaggableFormCustomPKTestCase(TaggableFormTestCase):
+ form_class = CustomPKFoodForm
+ food_model = CustomPKFood
+
+class TaggableFormOfficialTestCase(TaggableFormTestCase):
+ form_class = OfficialFoodForm
+ food_model = OfficialFood
+
+
+class TagStringParseTestCase(UnitTestCase):
+ """
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+
+ def test_with_simple_space_delimited_tags(self):
+ """
+ Test with simple space-delimited tags.
+ """
+ self.assertEqual(parse_tags('one'), [u'one'])
+ self.assertEqual(parse_tags('one two'), [u'one', u'two'])
+ self.assertEqual(parse_tags('one two three'), [u'one', u'three', u'two'])
+ self.assertEqual(parse_tags('one one two two'), [u'one', u'two'])
+
+ def test_with_comma_delimited_multiple_words(self):
+ """
+ Test with comma-delimited multiple words.
+ An unquoted comma in the input will trigger this.
+ """
+ self.assertEqual(parse_tags(',one'), [u'one'])
+ self.assertEqual(parse_tags(',one two'), [u'one two'])
+ self.assertEqual(parse_tags(',one two three'), [u'one two three'])
+ self.assertEqual(parse_tags('a-one, a-two and a-three'),
+ [u'a-one', u'a-two and a-three'])
+
+ def test_with_double_quoted_multiple_words(self):
+ """
+ Test with double-quoted multiple words.
+ A completed quote will trigger this. Unclosed quotes are ignored.
+ """
+ self.assertEqual(parse_tags('"one'), [u'one'])
+ self.assertEqual(parse_tags('"one two'), [u'one', u'two'])
+ self.assertEqual(parse_tags('"one two three'), [u'one', u'three', u'two'])
+ self.assertEqual(parse_tags('"one two"'), [u'one two'])
+ self.assertEqual(parse_tags('a-one "a-two and a-three"'),
+ [u'a-one', u'a-two and a-three'])
+
+ def test_with_no_loose_commas(self):
+ """
+ Test with no loose commas -- split on spaces.
+ """
+ self.assertEqual(parse_tags('one two "thr,ee"'), [u'one', u'thr,ee', u'two'])
+
+ def test_with_loose_commas(self):
+ """
+ Loose commas - split on commas
+ """
+ self.assertEqual(parse_tags('"one", two three'), [u'one', u'two three'])
+
+ def test_tags_with_double_quotes_can_contain_commas(self):
+ """
+ Double quotes can contain commas
+ """
+ self.assertEqual(parse_tags('a-one "a-two, and a-three"'),
+ [u'a-one', u'a-two, and a-three'])
+ self.assertEqual(parse_tags('"two", one, one, two, "one"'),
+ [u'one', u'two'])
+
+ def test_with_naughty_input(self):
+ """
+ Test with naughty input.
+ """
+ # Bad users! Naughty users!
+ self.assertEqual(parse_tags(None), [])
+ self.assertEqual(parse_tags(''), [])
+ self.assertEqual(parse_tags('"'), [])
+ self.assertEqual(parse_tags('""'), [])
+ self.assertEqual(parse_tags('"' * 7), [])
+ self.assertEqual(parse_tags(',,,,,,'), [])
+ self.assertEqual(parse_tags('",",",",",",","'), [u','])
+ self.assertEqual(parse_tags('a-one "a-two" and "a-three'),
+ [u'a-one', u'a-three', u'a-two', u'and'])
+
+ def test_recreation_of_tag_list_string_representations(self):
+ plain = Tag.objects.create(name='plain')
+ spaces = Tag.objects.create(name='spa ces')
+ comma = Tag.objects.create(name='com,ma')
+ self.assertEqual(edit_string_for_tags([plain]), u'plain')
+ self.assertEqual(edit_string_for_tags([plain, spaces]), u'"spa ces", plain')
+ self.assertEqual(edit_string_for_tags([plain, spaces, comma]), u'"com,ma", "spa ces", plain')
+ self.assertEqual(edit_string_for_tags([plain, comma]), u'"com,ma", plain')
+ self.assertEqual(edit_string_for_tags([comma, spaces]), u'"com,ma", "spa ces"')
diff --git a/lib/taggit/utils.py b/lib/taggit/utils.py
new file mode 100644
index 0000000..1b5e5a7
--- /dev/null
+++ b/lib/taggit/utils.py
@@ -0,0 +1,126 @@
+from django.utils.encoding import force_unicode
+from django.utils.functional import wraps
+
+
+def parse_tags(tagstring):
+ """
+ Parses tag input, with multiple word input being activated and
+ delineated by commas and double quotes. Quotes take precedence, so
+ they may contain commas.
+
+ Returns a sorted list of unique tag names.
+
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+ if not tagstring:
+ return []
+
+ tagstring = force_unicode(tagstring)
+
+ # Special case - if there are no commas or double quotes in the
+ # input, we don't *do* a recall... I mean, we know we only need to
+ # split on spaces.
+ if u',' not in tagstring and u'"' not in tagstring:
+ words = list(set(split_strip(tagstring, u' ')))
+ words.sort()
+ return words
+
+ words = []
+ buffer = []
+ # Defer splitting of non-quoted sections until we know if there are
+ # any unquoted commas.
+ to_be_split = []
+ saw_loose_comma = False
+ open_quote = False
+ i = iter(tagstring)
+ try:
+ while True:
+ c = i.next()
+ if c == u'"':
+ if buffer:
+ to_be_split.append(u''.join(buffer))
+ buffer = []
+ # Find the matching quote
+ open_quote = True
+ c = i.next()
+ while c != u'"':
+ buffer.append(c)
+ c = i.next()
+ if buffer:
+ word = u''.join(buffer).strip()
+ if word:
+ words.append(word)
+ buffer = []
+ open_quote = False
+ else:
+ if not saw_loose_comma and c == u',':
+ saw_loose_comma = True
+ buffer.append(c)
+ except StopIteration:
+ # If we were parsing an open quote which was never closed treat
+ # the buffer as unquoted.
+ if buffer:
+ if open_quote and u',' in buffer:
+ saw_loose_comma = True
+ to_be_split.append(u''.join(buffer))
+ if to_be_split:
+ if saw_loose_comma:
+ delimiter = u','
+ else:
+ delimiter = u' '
+ for chunk in to_be_split:
+ words.extend(split_strip(chunk, delimiter))
+ words = list(set(words))
+ words.sort()
+ return words
+
+
+def split_strip(string, delimiter=u','):
+ """
+ Splits ``string`` on ``delimiter``, stripping each resulting string
+ and returning a list of non-empty strings.
+
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+ if not string:
+ return []
+
+ words = [w.strip() for w in string.split(delimiter)]
+ return [w for w in words if w]
+
+
+def edit_string_for_tags(tags):
+ """
+ Given list of ``Tag`` instances, creates a string representation of
+ the list suitable for editing by the user, such that submitting the
+ given string representation back without changing it will give the
+ same list of tags.
+
+ Tag names which contain commas will be double quoted.
+
+ If any tag name which isn't being quoted contains whitespace, the
+ resulting string of tag names will be comma-delimited, otherwise
+ it will be space-delimited.
+
+ Ported from Jonathan Buchanan's `django-tagging
+ <http://django-tagging.googlecode.com/>`_
+ """
+ names = []
+ for tag in tags:
+ name = tag.name
+ if u',' in name or u' ' in name:
+ names.append('"%s"' % name)
+ else:
+ names.append(name)
+ return u', '.join(sorted(names))
+
+
+def require_instance_manager(func):
+ @wraps(func)
+ def inner(self, *args, **kwargs):
+ if self.instance is None:
+ raise TypeError("Can't call %s with a non-instance manager" % func.__name__)
+ return func(self, *args, **kwargs)
+ return inner
diff --git a/lib/taggit/views.py b/lib/taggit/views.py
new file mode 100644
index 0000000..68e955b
--- /dev/null
+++ b/lib/taggit/views.py
@@ -0,0 +1,18 @@
+from django.contrib.contenttypes.models import ContentType
+from django.shortcuts import get_object_or_404
+from django.views.generic.list_detail import object_list
+
+from taggit.models import TaggedItem, Tag
+
+
+def tagged_object_list(request, slug, queryset, **kwargs):
+ if callable(queryset):
+ queryset = queryset()
+ tag = get_object_or_404(Tag, slug=slug)
+ qs = queryset.filter(pk__in=TaggedItem.objects.filter(
+ tag=tag, content_type=ContentType.objects.get_for_model(queryset.model)
+ ).values_list("object_id", flat=True))
+ if "extra_context" not in kwargs:
+ kwargs["extra_context"] = {}
+ kwargs["extra_context"]["tag"] = tag
+ return object_list(request, qs, **kwargs)
diff --git a/lib/utils/pinboard.py b/lib/utils/pinboard.py
new file mode 100644
index 0000000..e05935e
--- /dev/null
+++ b/lib/utils/pinboard.py
@@ -0,0 +1,558 @@
+#!/usr/bin/env python
+"""Python-Pinboard
+
+Python module for access to pinboard <http://pinboard.in/> via its API.
+Recommended: Python 2.6 or later (untested on previous versions)
+
+This library was built on top of Paul Mucur's original work on the python-delicious
+which was supported for python 2.3. Morgan became a contributor and ported this library
+to pinboard.in when it was announced in December 2010 that delicious servers may be
+shutting down.
+
+The port to pinboard resulted in the inclusion of gzip support
+
+"""
+
+__version__ = "1.0"
+__license__ = "BSD"
+__copyright__ = "Copyright 2011, Morgan Craft"
+__author__ = "Morgan Craft <http://www.morgancraft.com/>"
+
+#TODO:
+# Should text be properly escaped for XML? Or that not this module's
+# responsibility?
+# Create test suite
+
+
+_debug = 0
+
+# The user agent string sent to pinboard.in when making requests. If you are
+# using this module in your own application, you should probably change this.
+USER_AGENT = "Python-Pinboard/%s +http://morgancraft.com/service_layer/python-pinboard/" % __version__
+
+
+import urllib
+import urllib2
+import sys
+import re
+import time
+## added to handle gzip compression from server
+import StringIO
+import gzip
+
+from xml.dom import minidom
+try:
+ StringTypes = basestring
+except:
+ try:
+ # Python 2.2 does not have basestring
+ from types import StringTypes
+ except:
+ # Python 2.0 and 2.1 do not have StringTypes
+ from types import StringType, UnicodeType
+ StringTypes = None
+try:
+ ListType = list
+ TupleType = tuple
+except:
+ from types import ListType, TupleType
+
+# Taken from Mark Pilgrim's amazing Universal Feed Parser
+# <http://feedparser.org/>
+try:
+ UserDict = dict
+except NameError:
+ from UserDict import UserDict
+try:
+ import datetime
+except:
+ datetime = None
+
+
+# The URL of the Pinboard API
+PINBOARD_API = "https://api.pinboard.in/v1"
+AUTH_HANDLER_REALM = 'API'
+AUTH_HANDLER_URI = "https://api.pinboard.in/"
+
+def open(username, password):
+ """Open a connection to a pinboard.in account"""
+ return PinboardAccount(username, password)
+
+def connect(username, password):
+ """Open a connection to a pinboard.in account"""
+ return open(username, password)
+
+
+# Custom exceptions
+
+class PinboardError(Exception):
+ """Error in the Python-Pinboard module"""
+ pass
+
+class ThrottleError(PinboardError):
+ """Error caused by pinboard.in throttling requests"""
+ def __init__(self, url, message):
+ self.url = url
+ self.message = message
+ def __str__(self):
+ return "%s: %s" % (self.url, self.message)
+
+class AddError(PinboardError):
+ """Error adding a post to pinboard.in"""
+ pass
+
+class DeleteError(PinboardError):
+ """Error deleting a post from pinboard.in"""
+ pass
+
+class BundleError(PinboardError):
+ """Error bundling tags on pinboard.in"""
+ pass
+
+class DeleteBundleError(PinboardError):
+ """Error deleting a bundle from pinboard.in"""
+ pass
+
+class RenameTagError(PinboardError):
+ """Error renaming a tag in pinboard.in"""
+ pass
+
+class DateParamsError(PinboardError):
+ '''Date params error'''
+ pass
+
+class PinboardAccount(UserDict):
+ """A pinboard.in account"""
+
+ # Used to track whether all posts have been downloaded yet.
+ __allposts = 0
+ __postschanged = 0
+
+ # Time of last request so that the one second limit can be enforced.
+ __lastrequest = None
+
+ # Special methods
+
+ def __init__(self, username, password):
+ UserDict.__init__(self)
+ # Authenticate the URL opener so that it can access Pinboard
+ if _debug:
+ sys.stderr.write("Initialising Pinboard Account object.\n")
+ auth_handler = urllib2.HTTPBasicAuthHandler()
+ auth_handler.add_password("API", "https://api.pinboard.in/", \
+ username, password)
+ opener = urllib2.build_opener(auth_handler)
+ opener.addheaders = [("User-agent", USER_AGENT), ('Accept-encoding', 'gzip')]
+ urllib2.install_opener(opener)
+ if _debug:
+ sys.stderr.write("URL opener with HTTP authenticiation installed globally.\n")
+
+
+ if _debug:
+ sys.stderr.write("Time of last update loaded into class dictionary.\n")
+
+ def __getitem__(self, key):
+ try:
+ return UserDict.__getitem__(self, key)
+ except KeyError:
+ if key == "tags":
+ return self.tags()
+ elif key == "dates":
+ return self.dates()
+ elif key == "posts":
+ return self.posts()
+ elif key == "bundles":
+ return self.bundles()
+
+ def __setitem__(self, key, value):
+ if key == "posts":
+ if _debug:
+ sys.stderr.write("The value of posts has been changed.\n")
+ self.__postschanged = 1
+ return UserDict.__setitem__(self, key, value)
+
+
+ def __request(self, url):
+
+ # Make sure that it has been at least 1 second since the last
+ # request was made. If not, halt execution for approximately one
+ # seconds.
+ if self.__lastrequest and (time.time() - self.__lastrequest) < 2:
+ if _debug:
+ sys.stderr.write("It has been less than two seconds since the last request; halting execution for one second.\n")
+ time.sleep(1)
+ if _debug and self.__lastrequest:
+ sys.stderr.write("The delay between requests was %d.\n" % (time.time() - self.__lastrequest))
+ self.__lastrequest = time.time()
+ if _debug:
+ sys.stderr.write("Opening %s.\n" % url)
+
+ try:
+ ## for pinboard a gzip request is made
+ raw_xml = urllib2.urlopen(url)
+ compresseddata = raw_xml.read()
+ ## bing unpackaging gzipped stream buffer
+ compressedstream = StringIO.StringIO(compresseddata)
+ gzipper = gzip.GzipFile(fileobj=compressedstream)
+ xml = gzipper.read()
+
+ except urllib2.URLError, e:
+ raise e
+
+ self["headers"] = {}
+ for header in raw_xml.headers.headers:
+ (name, value) = header.split(": ")
+ self["headers"][name.lower()] = value[:-2]
+ if raw_xml.headers.status == "503":
+ raise ThrottleError(url, \
+ "503 HTTP status code returned by pinboard.in")
+ if _debug:
+ sys.stderr.write("%s opened successfully.\n" % url)
+ return minidom.parseString(xml)
+
+
+
+
+ def posts(self, tag="", date="", todt="", fromdt="", count=0):
+ """Return pinboard.in bookmarks as a list of dictionaries.
+
+ This should be used without arguments as rarely as possible by
+ combining it with the lastupdate attribute to only get all posts when
+ there is new content as it places a large load on the pinboard.in
+ servers.
+
+ """
+ query = {}
+
+ ## if a date is passed then a ranged set of date params CANNOT be passed
+ if date and (todt or fromdt):
+ raise DateParamsError
+
+ if not count and not date and not todt and not fromdt and not tag:
+ path = "all"
+
+ # If attempting to load all of the posts from pinboard.in, and
+ # a previous download has been done, check to see if there has
+ # been an update; if not, then just return the posts stored
+ # inside the class.
+ if _debug:
+ sys.stderr.write("Checking to see if a previous download has been made.\n")
+ if not self.__postschanged and self.__allposts and \
+ self.lastupdate() == self["lastupdate"]:
+ if _debug:
+ sys.stderr.write("It has; returning old posts instead.\n")
+ return self["posts"]
+ elif not self.__allposts:
+ if _debug:
+ sys.stderr.write("Making note of request for all posts.\n")
+ self.__allposts = 1
+ elif date:
+ path = "get"
+ elif todt or fromdt:
+ path = "all"
+ else:
+ path = "recent"
+ if count:
+ query["count"] = count
+ if tag:
+ query["tag"] = tag
+
+ ##todt
+ if todt and (isinstance(todt, ListType) or isinstance(todt, TupleType)):
+ query["todt"] = "-".join([str(x) for x in todt[:3]])
+ elif todt and (todt and isinstance(todt, datetime.datetime) or \
+ isinstance(todt, datetime.date)):
+ query["todt"] = "-".join([str(todt.year), str(todt.month), str(todt.day)])
+ elif todt:
+ query["todt"] = todt
+
+ ## fromdt
+ if fromdt and (isinstance(fromdt, ListType) or isinstance(fromdt, TupleType)):
+ query["fromdt"] = "-".join([str(x) for x in fromdt[:3]])
+ elif fromdt and (fromdt and isinstance(fromdt, datetime.datetime) or \
+ isinstance(fromdt, datetime.date)):
+ query["fromdt"] = "-".join([str(fromdt.year), str(fromdt.month), str(fromdt.day)])
+ elif fromdt:
+ query["fromdt"] = fromdt
+
+ if date and (isinstance(date, ListType) or isinstance(date, TupleType)):
+ query["dt"] = "-".join([str(x) for x in date[:3]])
+ elif date and (datetime and isinstance(date, datetime.datetime) or \
+ isinstance(date, datetime.date)):
+ query["dt"] = "-".join([str(date.year), str(date.month), str(date.day)])
+ elif date:
+ query["dt"] = date
+
+ postsxml = self.__request("%s/posts/%s?%s" % (PINBOARD_API, path, \
+ urllib.urlencode(query))).getElementsByTagName("post")
+ posts = []
+ if _debug:
+ sys.stderr.write("Parsing posts XML into a list of dictionaries.\n")
+
+ # For each post, extract every attribute (splitting tags into sub-lists)
+ # and insert as a dictionary into the `posts` list.
+ for post in postsxml:
+ postdict = {}
+ for (name, value) in post.attributes.items():
+ if name == u"tag":
+ name = u"tags"
+ value = value.split(" ")
+ if name == u"time":
+ postdict[u"time_parsed"] = time.strptime(value, "%Y-%m-%dT%H:%M:%SZ")
+ postdict[name] = value
+ if self.has_key("posts") and isinstance(self["posts"], ListType) \
+ and postdict not in self["posts"]:
+ self["posts"].append(postdict)
+ posts.append(postdict)
+ if _debug:
+ sys.stderr.write("Inserting posts list into class attribute.\n")
+ if not self.has_key("posts"):
+ self["posts"] = posts
+ if _debug:
+ sys.stderr.write("Resetting marker so module doesn't think posts has been changed.\n")
+ self.__postschanged = 0
+ return posts
+
+ def tags(self):
+ """Return a dictionary of tags with the number of posts in each one"""
+ tagsxml = self.__request("%s/tags/get?" % \
+ PINBOARD_API).getElementsByTagName("tag")
+ tags = []
+ if _debug:
+ sys.stderr.write("Parsing tags XML into a list of dictionaries.\n")
+ for tag in tagsxml:
+ tagdict = {}
+ for (name, value) in tag.attributes.items():
+ if name == u"tag":
+ name = u"name"
+ elif name == u"count":
+ value = int(value)
+ tagdict[name] = value
+ if self.has_key("tags") and isinstance(self["tags"], ListType) \
+ and tagdict not in self["tags"]:
+ self["tags"].append(tagdict)
+ tags.append(tagdict)
+ if _debug:
+ sys.stderr.write("Inserting tags list into class attribute.\n")
+ if not self.has_key("tags"):
+ self["tags"] = tags
+ return tags
+
+ def bundles(self):
+ """Return a dictionary of all bundles"""
+ bundlesxml = self.__request("%s/tags/bundles/all" % \
+ PINBOARD_API).getElementsByTagName("bundle")
+ bundles = []
+ if _debug:
+ sys.stderr.write("Parsing bundles XML into a list of dictionaries.\n")
+ for bundle in bundlesxml:
+ bundledict = {}
+ for (name, value) in bundle.attributes.items():
+ bundledict[name] = value
+ if self.has_key("bundles") and isinstance(self["bundles"], ListType) \
+ and bundledict not in self["bundles"]:
+ self["bundles"].append(bundledict)
+ bundles.append(bundledict)
+ if _debug:
+ sys.stderr.write("Inserting bundles list into class attribute.\n")
+ if not self.has_key("bundles"):
+ self["bundles"] = bundles
+ return bundles
+
+ def dates(self, tag=""):
+ """Return a dictionary of dates with the number of posts at each date"""
+ if tag:
+ query = urllib.urlencode({"tag":tag})
+ else:
+ query = ""
+ datesxml = self.__request("%s/posts/dates?%s" % \
+ (PINBOARD_API, query)).getElementsByTagName("date")
+ dates = []
+ if _debug:
+ sys.stderr.write("Parsing dates XML into a list of dictionaries.\n")
+ for date in datesxml:
+ datedict = {}
+ for (name, value) in date.attributes.items():
+ if name == u"date":
+ datedict[u"date_parsed"] = time.strptime(value, "%Y-%m-%d")
+ elif name == u"count":
+ value = int(value)
+ datedict[name] = value
+ if self.has_key("dates") and isinstance(self["dates"], ListType) \
+ and datedict not in self["dates"]:
+ self["dates"].append(datedict)
+ dates.append(datedict)
+ if _debug:
+ sys.stderr.write("Inserting dates list into class attribute.\n")
+ if not self.has_key("dates"):
+ self["dates"] = dates
+ return dates
+
+
+ # Methods to modify pinboard.in content
+
+ def add(self, url, description, extended="", tags=(), date="", toread="no"):
+ """Add a new post to pinboard.in"""
+ query = {}
+ query["url"] = url
+ query ["description"] = description
+ query["toread"] = toread
+ if extended:
+ query["extended"] = extended
+ if tags and (isinstance(tags, TupleType) or isinstance(tags, ListType)):
+ query["tags"] = " ".join(tags)
+ elif tags and (StringTypes and isinstance(tags, StringTypes)) or \
+ (not StringTypes and (isinstance(tags, StringType) or \
+ isinstance(tags, UnicodeType))):
+ query["tags"] = tags
+
+ # This is a rather rudimentary way of parsing date strings into
+ # ISO8601 dates: if the date string is shorter than the required
+ # 20 characters then it is assumed that it is a partial date
+ # such as "2005-3-31" or "2005-3-31T20:00" and it is split into a
+ # list along non-numerals. Empty elements are then removed
+ # and then this is passed to the tuple/list case where
+ # the tuple/list is padded with necessary 0s and then formatted
+ # into an ISO8601 date string. This does not take into account
+ # time zones.
+ if date and (StringTypes and isinstance(tags, StringTypes)) or \
+ (not StringTypes and (isinstance(tags, StringType) or \
+ isinstance(tags, UnicodeType))) and len(date) < 20:
+ date = re.split("\D", date)
+ while '' in date:
+ date.remove('')
+ if date and (isinstance(date, ListType) or isinstance(date, TupleType)):
+ date = list(date)
+ if len(date) > 2 and len(date) < 6:
+ for i in range(6 - len(date)):
+ date.append(0)
+ query["dt"] = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ" % tuple(date)
+ elif date and (datetime and (isinstance(date, datetime.datetime) \
+ or isinstance(date, datetime.date))):
+ query["dt"] = "%.4d-%.2d-%.2dT%.2d:%.2d:%.2dZ" % date.utctimetuple()[:6]
+ elif date:
+ query["dt"] = date
+ try:
+ response = self.__request("%s/posts/add?%s" % (PINBOARD_API, \
+ urllib.urlencode(query)))
+ if response.firstChild.getAttribute("code") != u"done":
+ raise AddError
+ if _debug:
+ sys.stderr.write("Post, %s (%s), added to pinboard.in\n" \
+ % (description, url))
+ except:
+ if _debug:
+ sys.stderr.write("Unable to add post, %s (%s), to pinboard.in\n" \
+ % (description, url))
+
+ def bundle(self, bundle, tags):
+ """Bundle a set of tags together"""
+ query = {}
+ query["bundle"] = bundle
+ if tags and (isinstance(tags, TupleType) or isinstance(tags, ListType)):
+ query["tags"] = " ".join(tags)
+ elif tags and isinstance(tags, StringTypes):
+ query["tags"] = tags
+ try:
+ response = self.__request("%s/tags/bundles/set?%s" % (PINBOARD_API, \
+ urllib.urlencode(query)))
+ if response.firstChild.getAttribute("code") != u"done":
+ raise BundleError
+ if _debug:
+ sys.stderr.write("Tags, %s, bundled into %s.\n" \
+ % (repr(tags), bundle))
+ except:
+ if _debug:
+ sys.stderr.write("Unable to bundle tags, %s, into %s to pinboard.in\n" \
+ % (repr(tags), bundle))
+
+ def delete(self, url):
+ """Delete post from pinboard.in by its URL"""
+ try:
+ response = self.__request("%s/posts/delete?%s" % (PINBOARD_API, \
+ urllib.urlencode({"url":url})))
+ if response.firstChild.getAttribute("code") != u"done":
+ raise DeleteError
+ if _debug:
+ sys.stderr.write("Post, %s, deleted from pinboard.in\n" \
+ % url)
+ except:
+ if _debug:
+ sys.stderr.write("Unable to delete post, %s, from pinboard.in\n" \
+ % url)
+
+ def delete_bundle(self, name):
+ """Delete bundle from pinboard.in by its name"""
+ try:
+ response = self.__request("%s/tags/bundles/delete?%s" % (PINBOARD_API, \
+ urllib.urlencode({"bundle":name})))
+ if response.firstChild.getAttribute("code") != u"done":
+ raise DeleteBundleError
+ if _debug:
+ sys.stderr.write("Bundle, %s, deleted from pinboard.in\n" \
+ % name)
+ except:
+ if _debug:
+ sys.stderr.write("Unable to delete bundle, %s, from pinboard.in\n" \
+ % name)
+
+ def rename_tag(self, old, new):
+ """Rename a tag"""
+ query = {"old":old, "new":new}
+ try:
+ response = self.__request("%s/tags/rename?%s" % (PINBOARD_API, \
+ urllib.urlencode(query)))
+ if response.firstChild.getAttribute("code") != u"done":
+ raise RenameTagError
+ if _debug:
+ sys.stderr.write("Tag, %s, renamed to %s\n" \
+ % (old, new))
+ except:
+ if _debug:
+ sys.stderr.write("Unable to rename %s tag to %s in pinboard.in\n" \
+ % (old, new))
+
+if __name__ == "__main__":
+ if sys.argv[1:][0] == '-v' or sys.argv[1:][0] == '--version':
+ print __version__
+
+#REVISION HISTORY
+## leaving as legacy for now, this should probably removed now for pinboard.in
+#0.1 - 29/3/2005 - PEM - Initial version.
+#0.2 - 30/3/2005 - PEM - Now using urllib's urlencode to handle query building
+# and the class now extends dict (or failing that: UserDict).
+#0.3 - 30/3/2005 - PEM - Rewrote doc strings and improved the metaphor that the
+# account is a dictionary by adding posts, tags and dates to the account
+# object when they are called. This has the added benefit of reducing
+# requests to delicious as one need only call posts(), dates() and tags()
+# once and they are stored inside the class instance until deletion.
+#0.4 - 30/3/2005 - PEM - Added private __request method to handle URL requests
+# to del.icio.us and implemented throttle detection.
+#0.5 - 30/3/2005 - PEM - Now implements every part of the API specification
+#0.6 - 30/3/2005 - PEM - Heavily vetted code to conform with PEP 8: use of
+# isinstance(), use of `if var` and `if not var` instead of comparison to
+# empty strings and changed all string delimiters to double primes for
+# consistency.
+#0.7 - 31/3/2005 - PEM - Made it so that when a fetching operation such as
+# posts() or tags() is used, only new posts are added to the class dictionary
+# in part to increase efficiency and to prevent, say, an all posts call of
+# posts() being overwritten by a specific request such as posts(tag="ruby")
+# Added more intelligent date handling for adding posts; will now attempt to
+# format any *reasonable* string, tuple or list into an ISO8601 date. Also
+# changed the command to get the lastupdate as it was convoluted. The
+# all posts command now checks to see if del.icio.us has been updated since
+# it was last called, again, this is to reduce the load on the servers and
+# increase speed a little. Changed the version string to a pre-1.0 release
+# Subversion-generated one because I am lazy.
+#0.8 - 1/4/2005 - PEM - Improved intelligence of posts caching: will only
+# re-download all posts if the posts attribute has been changed. Added
+# the mandatory delay between requests of at least one second. Changed the
+# crude string replace method to encode ampersands with a more intelligent
+# regular expression.
+#0.9 - 2/4/2005 - PEM - Now uses datetime objects when possible.
+#0.10 - 4/4/2005 - PEM - Uses the time module when the datetime module is
+# unavailable (such as versions of Python prior to 2.3). Now uses time
+# tuples instead of datetime objects when outputting for compatibility and
+# consistency. Time tuples are a new attribute: "date_parsed", with the
+# original string format of the date (or datetime) in "date" etc. Now stores
+# the headers of each request.
diff --git a/lib/utils/pydelicious.py b/lib/utils/pydelicious.py
deleted file mode 100644
index 8e45843..0000000
--- a/lib/utils/pydelicious.py
+++ /dev/null
@@ -1,1045 +0,0 @@
-"""Library to access del.icio.us data via Python.
-
-An introduction to the project is given in the README.
-pydelicious is released under the BSD license. See license.txt for details
-and the copyright holders.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-TODO:
- - distribute license, readme docs via setup.py?
- - automatic release build?
-"""
-import sys
-import os
-import time
-import datetime
-import locale
-import httplib
-import urllib2
-from urllib import urlencode, quote_plus
-from StringIO import StringIO
-from pprint import pformat
-
-v = sys.version_info
-if v[0] >= 2 and v[1] >= 5:
- from hashlib import md5
-else:
- from md5 import md5
-
-try:
- from elementtree.ElementTree import parse as parse_xml
-except ImportError:
- # Python 2.5 and higher
- from xml.etree.ElementTree import parse as parse_xml
-
-try:
- import feedparser
-except ImportError:
- print >>sys.stderr, \
- "Feedparser not available, no RSS parsing."
- feedparser = None
-
-
-### Static config
-
-__version__ = '0.5.3'
-__author__ = 'Frank Timmermann <regenkind_at_gmx_dot_de>'
- # GP: does not respond to emails
-__contributors__ = [
- 'Greg Pinero',
- 'Berend van Berkum <berend+pydelicious@dotmpe.com>']
-__url__ = 'http://code.google.com/p/pydelicious/'
-# Old URL: 'http://deliciouspython.python-hosting.com/'
-__author_email__ = ""
-__docformat__ = "restructuredtext en"
-__description__ = "pydelicious.py allows you to access the web service of " \
- "del.icio.us via it's API through Python."
-__long_description__ = "The goal is to design an easy to use and fully " \
- "functional Python interface to del.icio.us."
-
-DLCS_OK_MESSAGES = ('done', 'ok')
-"Known text values of positive del.icio.us <result/> answers"
-DLCS_WAIT_TIME = 4
-"Time to wait between API requests"
-DLCS_REQUEST_TIMEOUT = 444
-"Seconds before socket triggers timeout"
-#DLCS_API_REALM = 'del.icio.us API'
-DLCS_API_HOST = 'api.del.icio.us'
-DLCS_API_PATH = 'v1'
-DLCS_API = "https://%s/%s" % (DLCS_API_HOST, DLCS_API_PATH)
-DLCS_RSS = 'http://del.icio.us/rss/'
-DLCS_FEEDS = 'http://feeds.delicious.com/v2/'
-
-PREFERRED_ENCODING = locale.getpreferredencoding()
-# XXX: might need to check sys.platform/encoding combinations here, ie
-#if sys.platform == 'darwin' || PREFERRED_ENCODING == 'macroman:
-# PREFERRED_ENCODING = 'utf-8'
-if not PREFERRED_ENCODING:
- PREFERRED_ENCODING = 'iso-8859-1'
-
-ISO_8601_DATETIME = '%Y-%m-%dT%H:%M:%SZ'
-
-USER_AGENT = 'pydelicious/%s %s' % (__version__, __url__)
-
-DEBUG = 0
-if 'DLCS_DEBUG' in os.environ:
- DEBUG = int(os.environ['DLCS_DEBUG'])
- if DEBUG:
- print >>sys.stderr, \
- "Set DEBUG to %i from DLCS_DEBUG env." % DEBUG
-
-HTTP_PROXY = None
-if 'HTTP_PROXY' in os.environ:
- HTTP_PROXY = os.environ['HTTP_PROXY']
- if DEBUG:
- print >>sys.stderr, \
- "Set HTTP_PROXY to %i from env." % HTTP_PROXY
-
-### Timeoutsocket hack taken from FeedParser.py
-
-# timeoutsocket allows feedparser to time out rather than hang forever on ultra-
-# slow servers. Python 2.3 now has this functionality available in the standard
-# socket library, so under 2.3 you don't need to install anything. But you
-# probably should anyway, because the socket module is buggy and timeoutsocket
-# is better.
-try:
- import timeoutsocket # http://www.timo-tasi.org/python/timeoutsocket.py
- timeoutsocket.setDefaultSocketTimeout(DLCS_REQUEST_TIMEOUT)
-except ImportError:
- import socket
- if hasattr(socket, 'setdefaulttimeout'):
- socket.setdefaulttimeout(DLCS_REQUEST_TIMEOUT)
-if DEBUG: print >>sys.stderr, \
- "Set socket timeout to %s seconds" % DLCS_REQUEST_TIMEOUT
-
-
-### Utility classes
-
-class _Waiter:
- """Waiter makes sure a certain amount of time passes between
- successive calls of `Waiter()`.
-
- Some attributes:
- :last: time of last call
- :wait: the minimum time needed between calls
- :waited: the number of calls throttled
-
- pydelicious.Waiter is an instance created when the module is loaded.
- """
- def __init__(self, wait):
- self.wait = wait
- self.waited = 0
- self.lastcall = 0;
-
- def __call__(self):
- tt = time.time()
- wait = self.wait
-
- timeago = tt - self.lastcall
-
- if timeago < wait:
- wait = wait - timeago
- if DEBUG>0: print >>sys.stderr, "Waiting %s seconds." % wait
- time.sleep(wait)
- self.waited += 1
- self.lastcall = tt + wait
- else:
- self.lastcall = tt
-
-Waiter = _Waiter(DLCS_WAIT_TIME)
-
-
-class PyDeliciousException(Exception):
- """Standard pydelicious error"""
-class PyDeliciousThrottled(Exception): pass
-class PyDeliciousUnauthorized(Exception): pass
-
-class DeliciousError(Exception):
- """Raised when the server responds with a negative answer"""
-
- @staticmethod
- def raiseFor(error_string, path, **params):
- if error_string == 'item already exists':
- raise DeliciousItemExistsError, params['url']
- else:
- raise DeliciousError, "%s, while calling <%s?%s>" % (error_string,
- path, urlencode(params))
-
-class DeliciousItemExistsError(DeliciousError):
- """Raised then adding an already existing post."""
-
-
-class HTTPErrorHandler(urllib2.HTTPDefaultErrorHandler):
-
- def http_error_401(self, req, fp, code, msg, headers):
- raise PyDeliciousUnauthorized, "Check credentials."
-
- def http_error_503(self, req, fp, code, msg, headers):
- # Retry-After?
- errmsg = "Try again later."
- if 'Retry-After' in headers:
- errmsg = "You may try again after %s" % headers['Retry-After']
- raise PyDeliciousThrottled, errmsg
-
-
-### Utility functions
-
-def dict0(d):
- "Removes empty string values from dictionary"
- return dict([(k,v) for k,v in d.items()
- if v=='' and isinstance(v, basestring)])
-
-
-def delicious_datetime(str):
- """Parse a ISO 8601 formatted string to a Python datetime ...
- """
- return datetime.datetime(*time.strptime(str, ISO_8601_DATETIME)[0:6])
-
-
-def http_request(url, user_agent=USER_AGENT, retry=4, opener=None):
- """Retrieve the contents referenced by the URL using urllib2.
-
- Retries up to four times (default) on exceptions.
- """
- request = urllib2.Request(url, headers={'User-Agent':user_agent})
-
- if not opener:
- opener = urllib2.build_opener()
-
- # Remember last error
- e = None
-
- # Repeat request on time-out errors
- tries = retry;
- while tries:
- try:
- return opener.open(request)
-
- except urllib2.HTTPError, e:
- # reraise unexpected protocol errors as PyDeliciousException
- raise PyDeliciousException, "%s" % e
-
- except urllib2.URLError, e:
- # xxx: Ugly check for time-out errors
- #if len(e)>0 and 'timed out' in arg[0]:
- print >> sys.stderr, "%s, %s tries left." % (e, tries)
- Waiter()
- tries = tries - 1
- #else:
- # tries = None
-
- # Give up
- raise PyDeliciousException, \
- "Unable to retrieve data at '%s', %s" % (url, e)
-
-
-def build_api_opener(host, user, passwd, extra_handlers=() ):
- """
- Build a urllib2 style opener with HTTP Basic authorization for one host
- and additional error handling. If HTTP_PROXY is set a proxyhandler is also
- added.
- """
-
- global DEBUG
-
- if DEBUG: httplib.HTTPConnection.debuglevel = 1
-
- password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
- password_manager.add_password(None, host, user, passwd)
- auth_handler = urllib2.HTTPBasicAuthHandler(password_manager)
-
- extra_handlers += ( HTTPErrorHandler(), )
- if HTTP_PROXY:
- extra_handlers += ( urllib2.ProxyHandler( {'http': HTTP_PROXY} ), )
-
- return urllib2.build_opener(auth_handler, *extra_handlers)
-
-
-def dlcs_api_opener(user, passwd):
- "Build an opener for DLCS_API_HOST, see build_api_opener()"
-
- return build_api_opener(DLCS_API_HOST, user, passwd)
-
-
-def dlcs_api_request(path, params='', user='', passwd='', throttle=True,
- opener=None):
- """Retrieve/query a path within the del.icio.us API.
-
- This implements a minimum interval between calls to avoid
- throttling. [#]_ Use param 'throttle' to turn this behaviour off.
-
- .. [#] http://del.icio.us/help/api/
- """
- if throttle:
- Waiter()
-
- if params:
- url = "%s/%s?%s" % (DLCS_API, path, urlencode(params))
- else:
- url = "%s/%s" % (DLCS_API, path)
-
- if DEBUG: print >>sys.stderr, \
- "dlcs_api_request: %s" % url
-
- if not opener:
- opener = dlcs_api_opener(user, passwd)
-
- fl = http_request(url, opener=opener)
-
- if DEBUG>2: print >>sys.stderr, \
- pformat(fl.info().headers)
-
- return fl
-
-
-def dlcs_encode_params(params, usercodec=PREFERRED_ENCODING):
- """Turn all param values (int, list, bool) into utf8 encoded strings.
- """
-
- if params:
- for key in params.keys():
- if isinstance(params[key], bool):
- if params[key]:
- params[key] = 'yes'
- else:
- params[key] = 'no'
-
- elif isinstance(params[key], int):
- params[key] = str(params[key])
-
- elif not params[key]:
- # strip/ignore empties other than False or 0
- del params[key]
- continue
-
- elif isinstance(params[key], list):
- params[key] = " ".join(params[key])
-
- elif not isinstance(params[key], unicode):
- params[key] = params[key].decode(usercodec)
-
- assert isinstance(params[key], basestring)
-
- params = dict([ (k, v.encode('utf8'))
- for k, v in params.items() if v])
-
- return params
-
-
-def dlcs_parse_xml(data, split_tags=False):
- """Parse any del.icio.us XML document and return Python data structure.
-
- Recognizes all XML document formats as returned by the version 1 API and
- translates to a JSON-like data structure (dicts 'n lists).
-
- Returned instance is always a dictionary. Examples::
-
- {'posts': [{'url':'...','hash':'...',},],}
- {'tags':['tag1', 'tag2',]}
- {'dates': [{'count':'...','date':'...'},], 'tag':'', 'user':'...'}
- {'result':(True, "done")}
- # etcetera.
- """
- # TODO: split_tags is not implemented
-
- if DEBUG>3: print >>sys.stderr, "dlcs_parse_xml: parsing from ", data
-
- if not hasattr(data, 'read'):
- data = StringIO(data)
-
- doc = parse_xml(data)
- root = doc.getroot()
- fmt = root.tag
-
- # Split up into three cases: Data, Result or Update
- if fmt in ('tags', 'posts', 'dates', 'bundles'):
-
- # Data: expect a list of data elements, 'resources'.
- # Use `fmt` (without last 's') to find data elements, elements
- # don't have contents, attributes contain all the data we need:
- # append to list
- elist = [el.attrib for el in doc.findall(fmt[:-1])]
-
- # Return list in dict, use tagname of rootnode as keyname.
- data = {fmt: elist}
-
- # Root element might have attributes too, append dict.
- data.update(root.attrib)
-
- return data
-
- elif fmt == 'result':
-
- # Result: answer to operations
- if root.attrib.has_key('code'):
- msg = root.attrib['code']
- else:
- msg = root.text
-
- # XXX: Return {'result':(True, msg)} for /known/ O.K. messages,
- # use (False, msg) otherwise. Move this to DeliciousAPI?
- v = msg in DLCS_OK_MESSAGES
- return {fmt: (v, msg)}
-
- elif fmt == 'update':
-
- # Update: "time"
- return {fmt: {
- 'time':time.strptime(root.attrib['time'], ISO_8601_DATETIME) }}
-
- else:
- raise PyDeliciousException, "Unknown XML document format '%s'" % fmt
-
-
-def dlcs_rss_request(tag="", popular=0, user="", url=''):
- """Parse a RSS request.
-
- This requests old (now undocumented?) URL paths that still seem to work.
- """
-
- tag = quote_plus(tag)
- user = quote_plus(user)
-
- if url != '':
- # http://del.icio.us/rss/url/efbfb246d886393d48065551434dab54
- url = DLCS_RSS + 'url/%s' % md5(url).hexdigest()
-
- elif user != '' and tag != '':
- url = DLCS_RSS + '%(user)s/%(tag)s' % {'user':user, 'tag':tag}
-
- elif user != '' and tag == '':
- # http://del.icio.us/rss/delpy
- url = DLCS_RSS + '%s' % user
-
- elif popular == 0 and tag == '':
- url = DLCS_RSS
-
- elif popular == 0 and tag != '':
- # http://del.icio.us/rss/tag/apple
- # http://del.icio.us/rss/tag/web2.0
- url = DLCS_RSS + "tag/%s" % tag
-
- elif popular == 1 and tag == '':
- url = DLCS_RSS + 'popular/'
-
- elif popular == 1 and tag != '':
- url = DLCS_RSS + 'popular/%s' % tag
-
- if DEBUG:
- print 'dlcs_rss_request', url
-
- rss = http_request(url).read()
-
- # assert feedparser, "dlcs_rss_request requires feedparser to be installed."
- if not feedparser:
- return rss
-
- rss = feedparser.parse(rss)
-
- posts = []
- for e in rss.entries:
- if e.has_key("links") and e["links"]!=[] and e["links"][0].has_key("href"):
- url = e["links"][0]["href"]
- elif e.has_key("link"):
- url = e["link"]
- elif e.has_key("id"):
- url = e["id"]
- else:
- url = ""
- if e.has_key("title"):
- description = e['title']
- elif e.has_key("title_detail") and e["title_detail"].has_key("title"):
- description = e["title_detail"]['value']
- else:
- description = ''
- try: tags = e['categories'][0][1]
- except:
- try: tags = e["category"]
- except: tags = ""
- if e.has_key("modified"):
- dt = e['modified']
- else:
- dt = ""
- if e.has_key("summary"):
- extended = e['summary']
- elif e.has_key("summary_detail"):
- e['summary_detail']["value"]
- else:
- extended = ""
- if e.has_key("author"):
- user = e['author']
- else:
- user = ""
- # time = dt ist weist auf ein problem hin
- # die benennung der variablen ist nicht einheitlich
- # api senden und
- # xml bekommen sind zwei verschiedene schuhe :(
- posts.append({'url':url, 'description':description, 'tags':tags,
- 'dt':dt, 'extended':extended, 'user':user})
- return posts
-
-
-delicious_v2_feeds = {
- #"Bookmarks from the hotlist"
- '': "%(format)s",
- #"Recent bookmarks"
- 'recent': "%(format)s/recent",
- #"Recent bookmarks by tag"
- 'tagged': "%(format)s/tag/%(tags)s",
- #"Popular bookmarks"
- 'popular': "%(format)s/popular",
- #"Popular bookmarks by tag"
- 'popular_tagged': "%(format)s/popular/%(tag)s",
- #"Recent site alerts (as seen in the top-of-page alert bar on the site)"
- 'alerts': "%(format)s/alerts",
- #"Bookmarks for a specific user"
- 'user': "%(format)s/%(username)s",
- #"Bookmarks for a specific user by tag(s)"
- 'user_tagged': "%(format)s/%(username)s/%(tags)s",
- #"Public summary information about a user (as seen in the network badge)"
- 'user_info': "%(format)s/userinfo/%(username)s",
- #"A list of all public tags for a user"
- 'user_tags': "%(format)s/tags/%(username)s",
- #"Bookmarks from a user's subscriptions"
- 'user_subscription': "%(format)s/subscriptions/%(username)s",
- #"Private feed for a user's inbox bookmarks from others"
- 'user_inbox': "%(format)s/inbox/%(username)s?private=%(key)s",
- #"Bookmarks from members of a user's network"
- 'user_network': "%(format)s/network/%(username)s",
- #"Bookmarks from members of a user's network by tag"
- 'user_network_tagged': "%(format)s/network/%(username)s/%(tags)s",
- #"A list of a user's network members"
- 'user_network_member': "%(format)s/networkmembers/%(username)s",
- #"A list of a user's network fans"
- 'user_network_fan': "%(format)s/networkfans/%(username)s",
- #"Recent bookmarks for a URL"
- 'url': "%(format)s/url/%(urlmd5)s",
- #"Summary information about a URL (as seen in the tagometer)"
- 'urlinfo': "json/urlinfo/%(urlmd5)s",
-}
-
-def dlcs_feed(name_or_url, url_map=delicious_v2_feeds, count=15, **params):
-
- """
- Request and parse a feed. See delicious_v2_feeds for available names and
- required parameters. Format defaults to json.
- """
-
-# http://delicious.com/help/feeds
-# TODO: plain or fancy
-
- format = params.setdefault('format', 'json')
- if count == 'all':
-# TODO: fetch all
- print >>sys.stderr, "! Maxcount 100 "
- count = 100
-
- if name_or_url in url_map:
- params['count'] = count
- url = DLCS_FEEDS + url_map[name_or_url] % params
-
- else:
- url = name_or_url
-
- if DEBUG:
- print 'dlcs_feed', url
-
- feed = http_request(url).read()
-
- if format == 'rss':
- if feedparser:
- rss = feedparser.parse(feed)
- return rss
-
- else:
- return feed
-
- elif format == 'json':
- return feed
-
-
-### Main module class
-
-class DeliciousAPI:
-
- """A single-user Python facade to the del.icio.us HTTP API.
-
- See http://delicious.com/help/api.
-
- Methods ``request`` and ``request_raw`` represent the core. For all API
- paths there are furthermore methods (e.g. posts_add for 'posts/all') with
- an explicit declaration of parameters and documentation.
- """
-
- def __init__(self, user, passwd, codec=PREFERRED_ENCODING,
- api_request=dlcs_api_request, xml_parser=dlcs_parse_xml,
- build_opener=dlcs_api_opener, encode_params=dlcs_encode_params):
-
- """Initialize access to the API for ``user`` with ``passwd``.
-
- ``codec`` sets the encoding of the arguments, which defaults to the
- users preferred locale.
-
- The ``api_request`` and ``xml_parser`` parameters by default point to
- functions within this package with standard implementations which
- request and parse a resource. See ``dlcs_api_request()`` and
- ``dlcs_parse_xml()``.
-
- Parameter ``build_opener`` is a callable that, provided with the
- credentials, should build a urllib2 opener for the delicious API server
- with HTTP authentication. See ``dlcs_api_opener()`` for the default
- implementation.
-
- ``encode_params`` finally preprocesses API parameters before
- they are passed to ``api_request``.
- """
-
- assert user != ""
- self.user = user
- self.passwd = passwd
- self.codec = codec
-
- # Implement communication to server and parsing of respons messages:
- assert callable(encode_params)
- self._encode_params = encode_params
- assert callable(build_opener)
- self._opener = build_opener(user, passwd)
- assert callable(api_request)
- self._api_request = api_request
- assert callable(xml_parser)
- self._parse_response = xml_parser
-
- ### Core functionality
-
- def request(self, path, _raw=False, **params):
- """Sends a request message to `path` in the API, and parses the results
- from XML. Use with ``_raw=True`` or ``call request_raw()`` directly
- to get the filehandler and process the response message manually.
-
- Calls to some paths will return a `result` message, i.e.::
-
- <result code="..." />
-
- or::
-
- <result>...</result>
-
- These should all be parsed to ``{'result':(Boolean, MessageString)}``,
- this method raises a ``DeliciousError`` on negative `result` answers.
- Positive answers are silently accepted and nothing is returned.
-
- Using ``_raw=True`` bypasses all parsing and never raises
- ``DeliciousError``.
-
- See ``dlcs_parse_xml()`` and ``self.request_raw()``."""
-
- if _raw:
- # return answer
- return self.request_raw(path, **params)
-
- else:
- params = self._encode_params(params, self.codec)
-
- # get answer and parse
- fl = self._api_request(path, params=params, opener=self._opener)
- rs = self._parse_response(fl)
-
- if type(rs) == dict and 'result' in rs:
- if not rs['result'][0]:
- # Raise an error for negative 'result' answers
- errmsg = ""
- if len(rs['result'])>0:
- errmsg = rs['result'][1]
- DeliciousError.raiseFor(errmsg, path, **params)
-
- else:
- # not out-of-the-oridinary result, OK
- return
-
- return rs
-
- def request_raw(self, path, **params):
- """Calls the path in the API, returns the filehandle. Returned file-
- like instances have an ``HTTPMessage`` instance with HTTP header
- information available. Use ``filehandle.info()`` or refer to the
- ``urllib2.openurl`` documentation.
- """
- # see `request()` on how the response can be handled
- params = self._encode_params(params, self.codec)
- return self._api_request(path, params=params, opener=self._opener)
-
- ### Explicit declarations of API paths, their parameters and docs
-
- # Tags
- def tags_get(self, **kwds):
- """Returns a list of tags and the number of times it is used by the
- user.
- ::
-
- <tags>
- <tag tag="TagName" count="888">
- """
- return self.request("tags/get", **kwds)
-
- def tags_delete(self, tag, **kwds):
- """Delete an existing tag.
-
- &tag={TAG}
- (required) Tag to delete
- """
- return self.request('tags/delete', tag=tag, **kwds)
-
- def tags_rename(self, old, new, **kwds):
- """Rename an existing tag with a new tag name. Returns a `result`
- message or raises an ``DeliciousError``. See ``self.request()``.
-
- &old={TAG}
- (required) Tag to rename.
- &new={TAG}
- (required) New tag name.
- """
- return self.request("tags/rename", old=old, new=new, **kwds)
-
- # Posts
- def posts_update(self, **kwds):
- """Returns the last update time for the user. Use this before calling
- `posts_all` to see if the data has changed since the last fetch.
- ::
-
- <update time="CCYY-MM-DDThh:mm:ssZ">
- """
- return self.request("posts/update", **kwds)
-
- def posts_dates(self, tag="", **kwds):
- """Returns a list of dates with the number of posts at each date.
- ::
-
- <dates>
- <date date="CCYY-MM-DD" count="888">
-
- &tag={TAG}
- (optional) Filter by this tag
- """
- return self.request("posts/dates", tag=tag, **kwds)
-
- def posts_get(self, tag="", dt="", url="", hashes=[], meta=True, **kwds):
- """Returns posts matching the arguments. If no date or url is given,
- most recent date will be used.
- ::
-
- <posts dt="CCYY-MM-DD" tag="..." user="...">
- <post ...>
-
- &tag={TAG} {TAG} ... {TAG}
- (optional) Filter by this/these tag(s).
- &dt={CCYY-MM-DDThh:mm:ssZ}
- (optional) Filter by this date, defaults to the most recent date on
- which bookmarks were saved.
- &url={URL}
- (optional) Fetch a bookmark for this URL, regardless of date.
- &hashes={MD5} {MD5} ... {MD5}
- (optional) Fetch multiple bookmarks by one or more URL MD5s
- regardless of date.
- &meta=yes
- (optional) Include change detection signatures on each item in a
- 'meta' attribute. Clients wishing to maintain a synchronized local
- store of bookmarks should retain the value of this attribute - its
- value will change when any significant field of the bookmark
- changes.
- """
- return self.request("posts/get", tag=tag, dt=dt, url=url,
- hashes=hashes, meta=meta, **kwds)
-
- def posts_recent(self, tag="", count="", **kwds):
- """Returns a list of the most recent posts, filtered by argument.
- ::
-
- <posts tag="..." user="...">
- <post ...>
-
- &tag={TAG}
- (optional) Filter by this tag.
- &count={1..100}
- (optional) Number of items to retrieve (Default:15, Maximum:100).
- """
- return self.request("posts/recent", tag=tag, count=count, **kwds)
-
- def posts_all(self, tag="", start=None, results=None, fromdt=None,
- todt=None, meta=True, hashes=False, **kwds):
- """Returns all posts. Please use sparingly. Call the `posts_update`
- method to see if you need to fetch this at all.
- ::
-
- <posts tag="..." user="..." update="CCYY-MM-DDThh:mm:ssZ">
- <post ...>
-
- &tag
- (optional) Filter by this tag.
- &start={#}
- (optional) Start returning posts this many results into the set.
- &results={#}
- (optional) Return this many results.
- &fromdt={CCYY-MM-DDThh:mm:ssZ}
- (optional) Filter for posts on this date or later
- &todt={CCYY-MM-DDThh:mm:ssZ}
- (optional) Filter for posts on this date or earlier
- &meta=yes
- (optional) Include change detection signatures on each item in a
- 'meta' attribute. Clients wishing to maintain a synchronized local
- store of bookmarks should retain the value of this attribute - its
- value will change when any significant field of the bookmark
- changes.
- &hashes
- (optional, exclusive) Do not fetch post details but a posts
- manifest with url- and meta-hashes. Other options do not apply.
- """
- if hashes:
- return self.request("posts/all", hashes=hashes, **kwds)
- else:
- return self.request("posts/all", tag=tag, fromdt=fromdt, todt=todt,
- start=start, results=results, meta=meta, **kwds)
-
- def posts_add(self, url, description, extended="", tags="", dt="",
- replace=False, shared=True, **kwds):
- """Add a post to del.icio.us. Returns a `result` message or raises an
- ``DeliciousError``. See ``self.request()``.
-
- &url (required)
- the url of the item.
- &description (required)
- the description of the item.
- &extended (optional)
- notes for the item.
- &tags (optional)
- tags for the item (space delimited).
- &dt (optional)
- datestamp of the item (format "CCYY-MM-DDThh:mm:ssZ").
- Requires a LITERAL "T" and "Z" like in ISO8601 at
- http://www.cl.cam.ac.uk/~mgk25/iso-time.html for example:
- "1984-09-01T14:21:31Z"
- &replace=no (optional) - don't replace post if given url has already
- been posted.
- &shared=yes (optional) - wether the item is public.
- """
- return self.request("posts/add", url=url, description=description,
- extended=extended, tags=tags, dt=dt,
- replace=replace, shared=shared, **kwds)
-
- def posts_delete(self, url, **kwds):
- """Delete a post from del.icio.us. Returns a `result` message or
- raises an ``DeliciousError``. See ``self.request()``.
-
- &url (required)
- the url of the item.
- """
- return self.request("posts/delete", url=url, **kwds)
-
- # Bundles
- def bundles_all(self, **kwds):
- """Retrieve user bundles from del.icio.us.
- ::
-
- <bundles>
- <bundel name="..." tags=...">
- """
- return self.request("tags/bundles/all", **kwds)
-
- def bundles_set(self, bundle, tags, **kwds):
- """Assign a set of tags to a single bundle, wipes away previous
- settings for bundle. Returns a `result` messages or raises an
- ``DeliciousError``. See ``self.request()``.
-
- &bundle (required)
- the bundle name.
- &tags (required)
- list of tags.
- """
- if type(tags)==list:
- tags = " ".join(tags)
- return self.request("tags/bundles/set", bundle=bundle, tags=tags,
- **kwds)
-
- def bundles_delete(self, bundle, **kwds):
- """Delete a bundle from del.icio.us. Returns a `result` message or
- raises an ``DeliciousError``. See ``self.request()``.
-
- &bundle (required)
- the bundle name.
- """
- return self.request("tags/bundles/delete", bundle=bundle, **kwds)
-
- ### Utils
-
- # Lookup table for del.icio.us url-path to DeliciousAPI method.
- paths = {
- 'tags/get': 'tags_get',
- 'tags/delete': 'tags_delete',
- 'tags/rename': 'tags_rename',
- 'posts/update': 'posts_update',
- 'posts/dates': 'posts_dates',
- 'posts/get': 'posts_get',
- 'posts/recent': 'posts_recent',
- 'posts/all': 'posts_all',
- 'posts/add': 'posts_add',
- 'posts/delete': 'posts_delete',
- 'tags/bundles/all': 'bundles_all',
- 'tags/bundles/set': 'bundles_set',
- 'tags/bundles/delete': 'bundles_delete',
- }
- def get_method(self, path):
- return getattr(self, self.paths[path])
-
- def get_url(self, url):
- """Return the del.icio.us url at which the HTML page with posts for
- ``url`` can be found.
- """
- return "http://del.icio.us/url/?url=%s" % (url,)
-
- def __repr__(self):
- return "DeliciousAPI(%s)" % self.user
-
-
-### Convenience functions on this package
-
-def apiNew(user, passwd):
- "Creates a new DeliciousAPI object, requires user(name) and passwd."
- return DeliciousAPI(user=user, passwd=passwd)
-
-def add(user, passwd, url, description, tags="", extended="", dt=None,
- replace=False):
- apiNew(user, passwd).posts_add(url=url, description=description,
- extended=extended, tags=tags, dt=dt, replace=replace)
-
-def get(user, passwd, tag="", dt=None, count=0, hashes=[]):
- "Returns a list of posts for the user"
- posts = apiNew(user, passwd).posts_get(
- tag=tag, dt=dt, hashes=hashes)['posts']
- if count: posts = posts[:count]
- return posts
-
-def get_update(user, passwd):
- "Returns the last update time for the user."
- return apiNew(user, passwd).posts_update()['update']['time']
-
-def get_all(user, passwd, tag="", start=0, results=100, fromdt=None,
- todt=None):
- "Returns a list with all posts. Please use sparingly. See `get_updated`"
- return apiNew(user, passwd).posts_all(tag=tag, start=start,
- results=results, fromdt=fromdt, todt=todt, meta=True)['posts']
-
-def get_tags(user, passwd):
- "Returns a list with all tags for user."
- return apiNew(user=user, passwd=passwd).tags_get()['tags']
-
-def delete(user, passwd, url):
- "Delete the URL from the del.icio.us account."
- apiNew(user, passwd).posts_delete(url=url)
-
-def rename_tag(user, passwd, oldtag, newtag):
- "Rename the tag for the del.icio.us account."
- apiNew(user=user, passwd=passwd).tags_rename(old=oldtag, new=newtag)
-
-
-### RSS functions
-
-def getrss(tag="", popular=0, url='', user=""):
- """Get posts from del.icio.us via parsing RSS.
-
- tag (opt) sort by tag
- popular (opt) look for the popular stuff
- user (opt) get the posts by a user, this striks popular
- url (opt) get the posts by url
- """
- return dlcs_rss_request(tag=tag, popular=popular, user=user, url=url)
-
-def get_userposts(user):
- "parse RSS for user"
- return getrss(user=user)
-
-def get_tagposts(tag):
- "parse RSS for tag"
- return getrss(tag=tag)
-
-def get_urlposts(url):
- "parse RSS for URL"
- return getrss(url=url)
-
-def get_popular(tag=""):
- "parse RSS for popular URLS for tag"
- return getrss(tag=tag, popular=1)
-
-
-### JSON feeds
-# TODO: untested
-
-def json_posts(user, count=15, tag=None, raw=True):
- """
- user
- count=### the number of posts you want to get (default is 15, maximum
- is 100)
- raw a raw JSON object is returned, instead of an object named
- Delicious.posts
- """
- url = "http://del.icio.us/feeds/json/" + \
- dlcs_encode_params({0:user})[0]
- if tag: url += '/'+dlcs_encode_params({0:tag})[0]
-
- return dlcs_feed(url, count=count, raw=raw)
-
-
-def json_tags(user, atleast, count, sort='alpha', raw=True, callback=None):
- """
- user
- atleast=### include only tags for which there are at least ###
- number of posts.
- count=### include ### tags, counting down from the top.
- sort={alpha|count} construct the object with tags in alphabetic order
- (alpha), or by count of posts (count).
- callback=NAME wrap the object definition in a function call NAME(...),
- thus invoking that function when the feed is executed.
- raw a pure JSON object is returned, instead of code that
- will construct an object named Delicious.tags.
- """
- url = 'http://del.icio.us/feeds/json/tags/' + \
- dlcs_encode_params({0:user})[0]
- return dlcs_feed(url, atleast=atleast, count=count, sort=sort, raw=raw,
- callback=callback)
-
-
-def json_network(user, raw=True, callback=None):
- """
- callback=NAME wrap the object definition in a function call NAME(...)
- ?raw a raw JSON object is returned, instead of an object named
- Delicious.posts
- """
- url = 'http://del.icio.us/feeds/json/network/' + \
- dlcs_encode_params({0:user})[0]
- return dlcs_feed(url, raw=raw, callback=callback)
-
-
-def json_fans(user, raw=True, callback=None):
- """
- callback=NAME wrap the object definition in a function call NAME(...)
- ?raw a pure JSON object is returned, instead of an object named
- Delicious.
- """
- url = 'http://del.icio.us/feeds/json/fans/' + \
- dlcs_encode_params({0:user})[0]
- return dlcs_feed(url, raw=raw, callback=callback)
-
-
-### delicious V2 feeds
-
-def getfeed(name, **params):
- return dlcs_feed(name, **params)
-