├── taggit_suggest ├── __init__.py ├── admin.py ├── utils.py ├── tests.py └── models.py ├── AUTHORS.txt ├── runtests.py ├── setup.py ├── README.txt └── LICENSE.txt /taggit_suggest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | djangot-taggit-suggestion was written by Frank Wiles and 2 | originally part of django-taggit by Alex Gaynor. 3 | 4 | The following is a list of much appreciated contributors: 5 | Alex Gaynor 6 | Alex Cabrera 7 | -------------------------------------------------------------------------------- /taggit_suggest/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from taggit.admin import TaggedItemInline 4 | from taggit_suggest.models import TagKeyword, TagRegex 5 | from taggit.models import Tag 6 | 7 | 8 | class TagKeywordInline(admin.StackedInline): 9 | model = TagKeyword 10 | 11 | 12 | class TagRegxInline(admin.StackedInline): 13 | model = TagRegex 14 | 15 | 16 | class TagSuggestAdmin(admin.ModelAdmin): 17 | inlines = [ 18 | TaggedItemInline, 19 | TagKeywordInline, 20 | TagRegxInline, 21 | ] 22 | 23 | 24 | admin.site.unregister(Tag) 25 | admin.site.register(Tag, TagSuggestAdmin) 26 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from os.path import dirname, abspath 5 | 6 | from django.conf import settings 7 | 8 | if not settings.configured: 9 | settings.configure( 10 | DATABASE_ENGINE='sqlite3', 11 | DATABASE_NAME='test.db', 12 | INSTALLED_APPS=[ 13 | 'django.contrib.contenttypes', 14 | 'taggit', 15 | 'taggit_suggest', 16 | ] 17 | ) 18 | 19 | from django.test.simple import run_tests 20 | 21 | 22 | def runtests(*test_args): 23 | if not test_args: 24 | test_args = ['taggit_suggest'] 25 | parent = dirname(abspath(__file__)) 26 | sys.path.insert(0, parent) 27 | failures = run_tests(test_args, verbosity=1, interactive=True) 28 | sys.exit(failures) 29 | 30 | 31 | if __name__ == '__main__': 32 | runtests(*sys.argv[1:]) 33 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | f = open(os.path.join(os.path.dirname(__file__), 'README.txt')) 5 | readme = f.read() 6 | f.close() 7 | 8 | setup( 9 | name='django-taggit-suggest', 10 | version='0.2', 11 | description='django-taggit-suggest is an add on for django-taggit to suggest tags for a given piece of text based on keywords, regular expressions, and existing tags.', 12 | long_description=readme, 13 | author='Frank Wiles', 14 | author_email='frank@revsys.com', 15 | url='http://github.com/frankwiles/django-taggit-suggest/tree/master', 16 | packages=find_packages(), 17 | zip_safe=False, 18 | classifiers=[ 19 | 'Development Status :: 4 - Beta', 20 | 'Environment :: Web Environment', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Operating System :: OS Independent', 24 | 'Programming Language :: Python', 25 | 'Framework :: Django', 26 | ], 27 | test_suite='runtests.runtests' 28 | ) 29 | 30 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | taggit_suggest 2 | ====================== 3 | 4 | This add on module allows you to easily associate keywords and regular 5 | expressions with a Tag object. This is useful to help keep your database 6 | from getting filled up with several similar tags that really represent the 7 | same thing or concept. 8 | 9 | For example, if your site is a humor site you might want to collapse all of 10 | #fun, #funny, #funnies, #hilarious, #rofl, and #lol into one tag #funny. The 11 | ``suggest_tags()`` function in ``taggit_suggest.utils`` will give you a 12 | list of tags that seem appropriate for the text content given to it. 13 | 14 | Unlike the rest of ``django-taggit``, ``taggit_suggest`` requires Django 1.2. 15 | 16 | Usage 17 | ===== 18 | 19 | Put ``'taggit_suggest'`` into ``INSTALLED_APPS`` and run a syncdb to 20 | create the necessary models. This will add ``Keywords`` and 21 | ``Regular Expression`` inlines to the default ``django-taggit`` admin. Once 22 | you've populated those based on your site you can do a simple: 23 | 24 | .. sourcecode:: python 25 | 26 | from taggit_suggest.utils import suggest_tags 27 | 28 | tags = suggest_tags(content='Some textual content...') 29 | 30 | 31 | TODO 32 | ==== 33 | 34 | * In a later version I hope to a simple way to help determine keywords for you 35 | automatically, by learning from your past tags and content. 36 | -------------------------------------------------------------------------------- /taggit_suggest/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from taggit_suggest.models import TagKeyword, TagRegex 4 | from taggit.models import Tag 5 | 6 | 7 | def _suggest_keywords(content): 8 | """ 9 | Suggest by keywords 10 | """ 11 | suggested_keywords = set() 12 | keywords = TagKeyword.objects.all() 13 | 14 | for k in keywords: 15 | # Use the stem if available, otherwise use the whole keyword 16 | if k.stem: 17 | if k.stem in content: 18 | suggested_keywords.add(k.tag_id) 19 | elif k.keyword in content: 20 | suggested_keywords.add(k.tag_id) 21 | 22 | return suggested_keywords 23 | 24 | def _suggest_regexes(content): 25 | """ 26 | Suggest by regular expressions 27 | """ 28 | # Grab all regular expressions and compile them 29 | suggested_regexes = set() 30 | regex_keywords = TagRegex.objects.all() 31 | 32 | # Look for our regular expressions in the content 33 | for r in regex_keywords: 34 | if re.search(r.regex, content): 35 | suggested_regexes.add(r.tag_id) 36 | 37 | return suggested_regexes 38 | 39 | def suggest_tags(content): 40 | """ 41 | Suggest tags based on text content 42 | """ 43 | suggested_keywords = _suggest_keywords(content) 44 | suggested_regexes = _suggest_regexes(content) 45 | suggested_tag_ids = suggested_keywords | suggested_regexes 46 | 47 | return Tag.objects.filter(id__in=suggested_tag_ids) 48 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) Frank Wiles and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-taggit nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /taggit_suggest/tests.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.test import TestCase 3 | 4 | from taggit_suggest.models import TagKeyword, TagRegex 5 | from taggit_suggest.utils import suggest_tags 6 | from taggit.models import Tag 7 | 8 | 9 | class SuggestCase(TestCase): 10 | def test_simple_suggest(self): 11 | ku_tag = Tag.objects.create(name='ku') 12 | TagKeyword.objects.create( 13 | tag=ku_tag, 14 | keyword='kansas university' 15 | ) 16 | 17 | suggested_tags = suggest_tags('I used to be a student at kansas university') 18 | self.assertTrue(ku_tag in suggested_tags) 19 | 20 | def test_regex_suggest(self): 21 | ku_tag = Tag.objects.create(name='ku') 22 | TagRegex.objects.create( 23 | tag=ku_tag, 24 | name='Find University of Kansas', 25 | regex='University\s+of\s+Kansas' 26 | ) 27 | 28 | suggested_tags = suggest_tags('I was once a student at the University of Kansas') 29 | 30 | self.assertTrue(ku_tag in suggested_tags) 31 | 32 | def test_bad_regex(self): 33 | ku_tag = Tag.objects.create(name='ku') 34 | TagKeyword.objects.create( 35 | tag=ku_tag, 36 | keyword='kansas university' 37 | ) 38 | new_regex = TagRegex( 39 | tag=ku_tag, 40 | name='Find University of Kansas', 41 | regex='University\s+of(\s+Kansas' 42 | ) 43 | self.assertRaises(ValidationError, new_regex.save) 44 | 45 | suggested_tags = suggest_tags('I was once a student at the University ' 46 | 'of Kansas. Also known as kansas university by the way.') 47 | 48 | self.assertTrue(ku_tag in suggested_tags) 49 | -------------------------------------------------------------------------------- /taggit_suggest/models.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from django.core.exceptions import ValidationError 4 | from django.db import models 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from taggit.models import Tag 8 | 9 | try: 10 | import Stemmer 11 | except ImportError: 12 | Stemmer = None 13 | 14 | 15 | class TagKeyword(models.Model): 16 | """ 17 | Model to associate simple keywords to a Tag 18 | """ 19 | tag = models.ForeignKey(Tag, related_name='tkeywords') 20 | keyword = models.CharField(max_length=30) 21 | stem = models.CharField(max_length=30) 22 | 23 | def __unicode__(self): 24 | return "Keyword '%s' for Tag '%s'" % (self.keyword, self.tag.name) 25 | 26 | def save(self, *args, **kwargs): 27 | """ 28 | Stem the keyword on save if they have PyStemmer 29 | """ 30 | language = kwargs.pop('stemmer-language', 'english') 31 | if not self.pk and not self.stem and Stemmer: 32 | stemmer = Stemmer.Stemmer(language) 33 | self.stem = stemmer.stemWord(self.keyword) 34 | super(TagKeyword, self).save(*args, **kwargs) 35 | 36 | 37 | def validate_regex(value): 38 | """ 39 | Make sure we have a valid regular expression 40 | """ 41 | try: 42 | re.compile(value) 43 | except Exception: 44 | # TODO: more restrictive in the exceptions 45 | raise ValidationError('Please enter a valid regular expression') 46 | 47 | 48 | class TagRegex(models.Model): 49 | """ 50 | Model to associate regular expressions with a Tag 51 | """ 52 | tag = models.ForeignKey(Tag, related_name='tregexes') 53 | name = models.CharField(max_length=30) 54 | regex = models.CharField( 55 | max_length=250, 56 | validators=[validate_regex], 57 | help_text=_('Enter a valid Regular Expression. To make it ' 58 | 'case-insensitive include "(?i)" in your expression.') 59 | ) 60 | 61 | def __unicode__(self): 62 | return self.name 63 | 64 | def save(self, *args, **kwargs): 65 | """ 66 | Make sure to validate 67 | """ 68 | self.full_clean() 69 | super(TagRegex, self).save(*args, **kwargs) 70 | --------------------------------------------------------------------------------