--- /dev/null
+# -*- coding: utf-8 -*-
+from django.contrib import admin
+
+from newtagging.admin import TaggableModelAdmin
+from catalogue.models import Tag, Book, Fragment
+
+
+class TagAdmin(admin.ModelAdmin):
+ list_display = ('name', 'slug', 'sort_key', 'category', 'has_description',)
+ list_filter = ('category',)
+ search_fields = ('name',)
+ ordering = ('name',)
+
+ prepopulated_fields = {'slug': ('name',), 'sort_key': ('name',),}
+ radio_fields = {'category': admin.HORIZONTAL}
+
+
+class BookAdmin(TaggableModelAdmin):
+ tag_model = Tag
+
+ list_display = ('title', 'slug', 'has_pdf_file', 'has_odt_file', 'has_html_file', 'has_description',)
+ search_fields = ('title',)
+ ordering = ('title',)
+
+ prepopulated_fields = {'slug': ('title',)}
+
+
+class FragmentAdmin(TaggableModelAdmin):
+ tag_model = Tag
+
+ list_display = ('book', 'anchor',)
+ ordering = ('book', 'anchor',)
+
+
+admin.site.register(Tag, TagAdmin)
+admin.site.register(Book, BookAdmin)
+admin.site.register(Fragment, FragmentAdmin)
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from django import forms
+from django.forms.widgets import flatatt
+from django.forms.util import smart_unicode
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.simplejson import dumps
+
+
+class JQueryAutoCompleteWidget(forms.TextInput):
+ def __init__(self, source, options=None, *args, **kwargs):
+ self.source = source
+ self.options = None
+ if options:
+ self.options = dumps(options)
+ super(JQueryAutoCompleteWidget, self).__init__(*args, **kwargs)
+
+ def render_js(self, field_id):
+ source = "'%s'" % escape(self.source)
+ options = ''
+ if self.options:
+ options += ', %s' % self.options
+
+ return u'$(\'#%s\').autocomplete(%s%s);' % (field_id, source, options)
+
+ def render(self, name, value=None, attrs=None):
+ final_attrs = self.build_attrs(attrs, name=name)
+ if value:
+ final_attrs['value'] = smart_unicode(value)
+
+ if not self.attrs.has_key('id'):
+ final_attrs['id'] = 'id_%s' % name
+
+ html = u'''<input type="text" %(attrs)s/>
+ <script type="text/javascript"><!--//
+ %(js)s//--></script>
+ ''' % {
+ 'attrs' : flatatt(final_attrs),
+ 'js' : self.render_js(final_attrs['id']),
+ }
+
+ return mark_safe(html)
+
+
+class JQueryAutoCompleteField(forms.CharField):
+ def __init__(self, source, options=None, *args, **kwargs):
+ if 'widget' not in kwargs:
+ kwargs['widget'] = JQueryAutoCompleteWidget(source, options)
+
+ super(JQueryAutoCompleteField, self).__init__(*args, **kwargs)
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from django import forms
+from slughifi import slughifi
+
+from catalogue.models import Tag
+from catalogue.fields import JQueryAutoCompleteField
+
+
+class SearchForm(forms.Form):
+ q = JQueryAutoCompleteField('/katalog/tags/', {'minChars': 2, 'selectFirst': True, 'cacheLength': 50})
+ tags = forms.CharField(widget=forms.HiddenInput, required=False)
+
+ def __init__(self, *args, **kwargs):
+ tags = kwargs.pop('tags', [])
+ super(SearchForm, self).__init__(*args, **kwargs)
+ self.fields['q'].widget.attrs['title'] = u'tytuł utworu, motyw lub kategoria'
+ self.fields['tags'].initial = '/'.join(tag.slug for tag in Tag.get_tag_list(tags))
+
+
+class UserSetsForm(forms.Form):
+ def __init__(self, book, user, *args, **kwargs):
+ super(UserSetsForm, self).__init__(*args, **kwargs)
+ self.fields['set_ids'] = forms.ChoiceField(
+ choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user)],
+ )
+
+
+class ObjectSetsForm(forms.Form):
+ def __init__(self, obj, user, *args, **kwargs):
+ super(ObjectSetsForm, self).__init__(*args, **kwargs)
+ self.fields['set_ids'] = forms.MultipleChoiceField(
+ label=u'Półki',
+ required=False,
+ choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user)],
+ initial=[tag.id for tag in obj.tags.filter(category='set', user=user)],
+ widget=forms.CheckboxSelectMultiple
+ )
+
+
+class NewSetForm(forms.Form):
+ name = forms.CharField(max_length=50, required=True)
+
+ def save(self, user, commit=True):
+ name = self.cleaned_data['name']
+ new_set = Tag(name=name, slug=slughifi(name), sort_key=slughifi(name),
+ category='set', user=user)
+
+ new_set.save()
+ return new_set
+
--- /dev/null
+import os
+
+from django.core.management.base import BaseCommand
+from django.core.management.color import color_style
+from optparse import make_option
+
+from catalogue.models import Book
+
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('--verbosity', action='store', dest='verbosity', default='1',
+ type='choice', choices=['0', '1', '2'],
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
+ )
+ help = 'Imports books from the specified directories.'
+ args = 'directory [directory ...]'
+
+ def handle(self, *directories, **options):
+ from django.db import transaction
+
+ self.style = color_style()
+
+ verbosity = int(options.get('verbosity', 1))
+ show_traceback = options.get('traceback', False)
+
+ # Start transaction management.
+ transaction.commit_unless_managed()
+ transaction.enter_transaction_management()
+ transaction.managed(True)
+
+ for dir_name in directories:
+ if not os.path.isdir(dir_name):
+ print self.style.ERROR("Skipping '%s': not a directory." % dir_name)
+ else:
+ for file_name in os.listdir(dir_name):
+ file_path = os.path.join(dir_name, file_name)
+ if not os.path.splitext(file_name)[1] == '.xml':
+ print self.style.NOTICE("Skipping '%s': not an XML file." % file_path)
+ continue
+ if verbosity > 0:
+ print "Parsing '%s'" % file_path
+
+ Book.from_xml_file(file_path)
+
+ transaction.commit()
+ transaction.leave_transaction_management()
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.db import models
+from django.db.models import permalink, Q
+from django.utils.translation import ugettext_lazy as _
+from django.contrib.auth.models import User
+from django.core.files import File
+from django.template.loader import render_to_string
+from django.utils.safestring import mark_safe
+
+from newtagging.models import TagBase
+from newtagging import managers
+
+from librarian import html, dcparser
+
+
+TAG_CATEGORIES = (
+ ('author', _('author')),
+ ('epoch', _('epoch')),
+ ('kind', _('kind')),
+ ('genre', _('genre')),
+ ('theme', _('theme')),
+ ('set', _('set')),
+)
+
+
+class TagSubcategoryManager(models.Manager):
+ def __init__(self, subcategory):
+ super(TagSubcategoryManager, self).__init__()
+ self.subcategory = subcategory
+
+ def get_query_set(self):
+ return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
+
+
+class Tag(TagBase):
+ name = models.CharField(_('name'), max_length=50, unique=True, db_index=True)
+ slug = models.SlugField(_('slug'), unique=True, db_index=True)
+ sort_key = models.SlugField(_('sort key'), db_index=True)
+ category = models.CharField(_('category'), max_length=50, blank=False, null=False,
+ db_index=True, choices=TAG_CATEGORIES)
+ description = models.TextField(blank=True)
+
+ user = models.ForeignKey(User, blank=True, null=True)
+
+ def has_description(self):
+ return len(self.description) > 0
+ has_description.short_description = _('description')
+ has_description.boolean = True
+
+ @permalink
+ def get_absolute_url(self):
+ return ('catalogue.views.tagged_object_list', [self.slug])
+
+ class Meta:
+ ordering = ('sort_key',)
+ verbose_name = _('tag')
+ verbose_name_plural = _('tags')
+
+ def __unicode__(self):
+ return self.name
+
+ @staticmethod
+ def get_tag_list(tags):
+ if isinstance(tags, basestring):
+ tag_slugs = tags.split('/')
+ return [Tag.objects.get(slug=slug) for slug in tag_slugs]
+ else:
+ return TagBase.get_tag_list(tags)
+
+
+class Book(models.Model):
+ title = models.CharField(_('title'), max_length=120)
+ slug = models.SlugField(_('slug'), unique=True, db_index=True)
+ description = models.TextField(_('description'), blank=True)
+ created_at = models.DateTimeField(_('creation date'), auto_now=True)
+ _short_html = models.TextField(_('short HTML'), editable=False)
+
+ # Formats
+ xml_file = models.FileField(_('XML file'), upload_to='books/xml', blank=True)
+ pdf_file = models.FileField(_('PDF file'), upload_to='books/pdf', blank=True)
+ odt_file = models.FileField(_('ODT file'), upload_to='books/odt', blank=True)
+ html_file = models.FileField(_('HTML file'), upload_to='books/html', blank=True)
+
+ parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
+
+ objects = models.Manager()
+ tagged = managers.ModelTaggedItemManager(Tag)
+ tags = managers.TagDescriptor(Tag)
+
+ def short_html(self):
+ if len(self._short_html):
+ return mark_safe(self._short_html)
+ else:
+ tags = self.tags.filter(~Q(category__in=('set', 'theme')))
+ tags = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in tags]
+
+ formats = []
+ if self.html_file:
+ formats.append(u'<a href="%s">Czytaj online</a>' % self.html_file.url)
+ if self.pdf_file:
+ formats.append(u'<a href="%s">Plik PDF</a>' % self.pdf_file.url)
+ if self.odt_file:
+ formats.append(u'<a href="%s">Plik ODT</a>' % self.odt_file.url)
+
+ self._short_html = unicode(render_to_string('catalogue/book_short.html',
+ {'book': self, 'tags': tags, 'formats': formats}))
+ self.save()
+ return mark_safe(self._short_html)
+
+ def has_description(self):
+ return len(self.description) > 0
+ has_description.short_description = _('description')
+ has_description.boolean = True
+
+ def has_pdf_file(self):
+ return bool(self.pdf_file)
+ has_pdf_file.short_description = 'PDF'
+ has_pdf_file.boolean = True
+
+ def has_odt_file(self):
+ return bool(self.odt_file)
+ has_odt_file.short_description = 'ODT'
+ has_odt_file.boolean = True
+
+ def has_html_file(self):
+ return bool(self.html_file)
+ has_html_file.short_description = 'HTML'
+ has_html_file.boolean = True
+
+ @staticmethod
+ def from_xml_file(xml_file):
+ from tempfile import NamedTemporaryFile
+ from slughifi import slughifi
+ from markupstring import MarkupString
+
+ # Read book metadata
+ book_info = dcparser.parse(xml_file)
+ book = Book(title=book_info.title, slug=slughifi(book_info.title))
+ book.save()
+
+ book_tags = []
+ for category in ('kind', 'genre', 'author', 'epoch'):
+ tag_name = getattr(book_info, category)
+ tag_sort_key = tag_name
+ if category == 'author':
+ tag_sort_key = tag_name.last_name
+ tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
+ tag, created = Tag.objects.get_or_create(name=tag_name,
+ slug=slughifi(tag_name), sort_key=slughifi(tag_sort_key), category=category)
+ tag.save()
+ book_tags.append(tag)
+ book.tags = book_tags
+
+ if hasattr(book_info, 'parts'):
+ for part_url in book_info.parts:
+ base, slug = part_url.rsplit('/', 1)
+ child_book = Book.objects.get(slug=slug)
+ child_book.parent = book
+ child_book.save()
+
+ # Save XML and HTML files
+ book.xml_file.save('%s.xml' % book.slug, File(file(xml_file)), save=False)
+
+ html_file = NamedTemporaryFile()
+ html.transform(book.xml_file.path, html_file)
+ book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
+
+ # Extract fragments
+ closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
+ book_themes = []
+ for fragment in closed_fragments.values():
+ text = fragment.to_string()
+ short_text = ''
+ if (len(MarkupString(text)) > 240):
+ short_text = unicode(MarkupString(text)[:160])
+ new_fragment = Fragment(text=text, short_text=short_text, anchor=fragment.id, book=book)
+
+ theme_names = [s.strip() for s in fragment.themes.split(',')]
+ themes = []
+ for theme_name in theme_names:
+ tag, created = Tag.objects.get_or_create(name=theme_name,
+ slug=slughifi(theme_name), sort_key=slughifi(theme_name), category='theme')
+ tag.save()
+ themes.append(tag)
+ new_fragment.save()
+ new_fragment.tags = list(book.tags) + themes
+ book_themes += themes
+
+ book_themes = set(book_themes)
+ book.tags = list(book.tags) + list(book_themes)
+ return book.save()
+
+ @permalink
+ def get_absolute_url(self):
+ return ('catalogue.views.book_detail', [self.slug])
+
+ class Meta:
+ ordering = ('title',)
+ verbose_name = _('book')
+ verbose_name_plural = _('books')
+
+ def __unicode__(self):
+ return self.title
+
+
+class Fragment(models.Model):
+ text = models.TextField()
+ short_text = models.TextField(editable=False)
+ _short_html = models.TextField(editable=False)
+ anchor = models.IntegerField()
+ book = models.ForeignKey(Book, related_name='fragments')
+
+ objects = models.Manager()
+ tagged = managers.ModelTaggedItemManager(Tag)
+ tags = managers.TagDescriptor(Tag)
+
+ def short_html(self):
+ if len(self._short_html):
+ return mark_safe(self._short_html)
+ else:
+ book_authors = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)
+ for tag in self.book.tags if tag.category == 'author']
+
+ self._short_html = unicode(render_to_string('catalogue/fragment_short.html',
+ {'fragment': self, 'book': self.book, 'book_authors': book_authors}))
+ self.save()
+ return mark_safe(self._short_html)
+
+ class Meta:
+ ordering = ('book', 'anchor',)
+ verbose_name = _('fragment')
+ verbose_name_plural = _('fragments')
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from django import template
+from django.template import Node, Variable
+from django.utils.encoding import smart_str
+from django.core.urlresolvers import reverse
+from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
+from django.db.models import Q
+
+
+register = template.Library()
+
+
+class RegistrationForm(UserCreationForm):
+ def as_ul(self):
+ "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
+ return self._html_output(u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>', '</li>', u' %s', False)
+
+
+class LoginForm(AuthenticationForm):
+ def as_ul(self):
+ "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
+ return self._html_output(u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>', '</li>', u' %s', False)
+
+
+def iterable(obj):
+ try:
+ iter(obj)
+ return True
+ except TypeError:
+ return False
+
+
+def capfirst(text):
+ try:
+ return '%s%s' % (text[0].upper(), text[1:])
+ except IndexError:
+ return ''
+
+
+@register.simple_tag
+def title_from_tags(tags):
+ def split_tags(tags):
+ result = {}
+ for tag in tags:
+ result[tag.category] = tag
+ return result
+
+ class Flection(object):
+ def get_case(self, name, flection):
+ return name
+ flection = Flection()
+
+ self = split_tags(tags)
+
+ title = u''
+
+ # Specjalny przypadek oglądania wszystkich lektur na danej półce
+ if len(self) == 1 and 'set' in self:
+ return u'Półka %s' % self['set']
+
+ # Specjalny przypadek "Twórczość w pozytywizmie", wtedy gdy tylko epoka
+ # jest wybrana przez użytkownika
+ if 'epoch' in self and len(self) == 1:
+ text = u'Twórczość w %s' % flection.get_case(unicode(self['epoch']), u'miejscownik')
+ return capfirst(text)
+
+ # Specjalny przypadek "Dramat w twórczości Sofoklesa", wtedy gdy podane
+ # są tylko rodzaj literacki i autor
+ if 'kind' in self and 'author' in self and len(self) == 2:
+ text = u'%s w twórczości %s' % (unicode(self['kind']),
+ flection.get_case(unicode(self['author']), u'dopełniacz'))
+ return capfirst(text)
+
+ # Przypadki ogólniejsze
+ if 'theme' in self:
+ title += u'Motyw %s' % unicode(self['theme'])
+
+ if 'genre' in self:
+ if 'theme' in self:
+ title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
+ else:
+ title += unicode(self['genre'])
+
+ if 'kind' in self or 'author' in self or 'epoch' in self:
+ if 'genre' in self or 'theme' in self:
+ if 'kind' in self:
+ title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
+ else:
+ title += u' w twórczości '
+ else:
+ title += u'%s ' % unicode(self.get('kind', u'twórczość'))
+
+ if 'author' in self:
+ title += flection.get_case(unicode(self['author']), u'dopełniacz')
+ elif 'epoch' in self:
+ title += flection.get_case(unicode(self['epoch']), u'dopełniacz')
+
+ return capfirst(title)
+
+
+@register.simple_tag
+def user_creation_form():
+ return RegistrationForm(prefix='registration').as_ul()
+
+
+@register.simple_tag
+def authentication_form():
+ return LoginForm(prefix='login').as_ul()
+
+
+@register.inclusion_tag('catalogue/breadcrumbs.html')
+def breadcrumbs(tags, search_form=True):
+ from catalogue.forms import SearchForm
+ context = {'tag_list': tags}
+ if search_form:
+ context['search_form'] = SearchForm(tags=tags)
+ return context
+
+
+@register.tag
+def catalogue_url(parser, token):
+ bits = token.split_contents()
+ tag_name = bits[0]
+
+ tags_to_add = []
+ tags_to_remove = []
+ for bit in bits[1:]:
+ if bit[0] == '-':
+ tags_to_remove.append(bit[1:])
+ else:
+ tags_to_add.append(bit)
+
+ return CatalogueURLNode(tags_to_add, tags_to_remove)
+
+
+class CatalogueURLNode(Node):
+ def __init__(self, tags_to_add, tags_to_remove):
+ self.tags_to_add = [Variable(tag) for tag in tags_to_add]
+ self.tags_to_remove = [Variable(tag) for tag in tags_to_remove]
+
+ def render(self, context):
+ tags_to_add = []
+ tags_to_remove = []
+
+ for tag_variable in self.tags_to_add:
+ tag = tag_variable.resolve(context)
+ if isinstance(tag, (list, dict)):
+ tags_to_add += [t for t in tag]
+ else:
+ tags_to_add.append(tag)
+
+ for tag_variable in self.tags_to_remove:
+ tag = tag_variable.resolve(context)
+ if iterable(tag):
+ tags_to_remove += [t for t in tag]
+ else:
+ tags_to_remove.append(tag)
+
+ tag_slugs = [tag.slug for tag in tags_to_add]
+ for tag in tags_to_remove:
+ try:
+ tag_slugs.remove(tag.slug)
+ except KeyError:
+ pass
+
+ if len(tag_slugs) > 0:
+ return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
+ else:
+ return reverse('main_page')
+
+
+@register.inclusion_tag('catalogue/latest_blog_posts.html')
+def latest_blog_posts(feed_url, posts_to_show=5):
+ import feedparser
+ import datetime
+
+ feed = feedparser.parse(feed_url)
+ posts = []
+ for i in range(posts_to_show):
+ pub_date = feed['entries'][i].updated_parsed
+ published = datetime.date(pub_date[0], pub_date[1], pub_date[2] )
+ posts.append({
+ 'title': feed['entries'][i].title,
+ 'summary': feed['entries'][i].summary,
+ 'link': feed['entries'][i].link,
+ 'date': published,
+ })
+ return {'posts': posts}
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.conf.urls.defaults import *
+
+
+urlpatterns = patterns('catalogue.views',
+ url(r'^$', 'main_page', name='main_page'),
+ url(r'^polki/$', 'user_shelves', name='user_shelves'),
+ url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'),
+ url(r'^lektury/', 'book_list', name='book_list'),
+ url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'),
+ url(r'^fragment/(?P<id>[0-9]+)/polki/', 'fragment_sets', name='fragment_shelves'),
+ url(r'^polki/nowa/$', 'new_set', name='new_set'),
+ url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'),
+ url(r'^tags/$', 'tags_starting_with', name='hint'),
+ url(r'^szukaj/$', 'search', name='search'),
+ url(r'^(?P<tags>[a-zA-Z0-9-/]+)/$', 'tagged_object_list', name='tagged_object_list'),
+)
+
--- /dev/null
+# -*- coding: utf-8 -*-
+
+
+def split_tags(tags):
+ result = {}
+ for tag in tags:
+ result.setdefault(tag.category, []).append(tag)
+ return result
+
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.template import RequestContext
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponse, HttpResponseRedirect, Http404
+from django.core.urlresolvers import reverse
+from django.db.models import Q
+from django.contrib.auth.decorators import login_required
+from django.utils.datastructures import SortedDict
+from django.views.decorators.http import require_POST
+from django.contrib import auth
+from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
+from django.utils import simplejson
+from django.utils.functional import Promise
+from django.utils.encoding import force_unicode
+
+from catalogue import models
+from catalogue import forms
+from catalogue.utils import split_tags
+from newtagging import views as newtagging_views
+
+
+class LazyEncoder(simplejson.JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, Promise):
+ return force_unicode(obj)
+ return obj
+
+
+def search(request):
+ query = request.GET.get('q', '')
+ tags = request.GET.get('tags', '')
+ if tags == '':
+ tags = []
+
+ try:
+ tag_list = models.Tag.get_tag_list(tags)
+ tag = models.Tag.objects.get(name=query)
+ except models.Tag.DoesNotExist:
+ try:
+ book = models.Book.objects.get(title=query)
+ return HttpResponseRedirect(book.get_absolute_url())
+ except models.Book.DoesNotExist:
+ return HttpResponseRedirect(reverse('catalogue.views.main_page'))
+ else:
+ tag_list.append(tag)
+ return HttpResponseRedirect(reverse('catalogue.views.tagged_object_list',
+ kwargs={'tags': '/'.join(tag.slug for tag in tag_list)}
+ ))
+
+
+def tags_starting_with(request):
+ try:
+ prefix = request.GET['q']
+ if len(prefix) < 2:
+ raise KeyError
+
+ books = models.Book.objects.filter(title__icontains=prefix)
+ tags = models.Tag.objects.filter(name__icontains=prefix)
+ if request.user.is_authenticated():
+ tags = tags.filter(~Q(category='set') | Q(user=request.user))
+ else:
+ tags = tags.filter(~Q(category='set'))
+
+ completions = [book.title for book in books] + [tag.name for tag in tags]
+
+ return HttpResponse('\n'.join(completions))
+
+ except KeyError:
+ return HttpResponse('')
+
+
+def main_page(request):
+ if request.user.is_authenticated():
+ extra_where = '(NOT catalogue_tag.category = "set" OR catalogue_tag.user_id = %d)' % request.user.id
+ else:
+ extra_where = 'NOT catalogue_tag.category = "set"'
+ tags = models.Tag.objects.usage_for_model(models.Book, counts=True, extra={'where': [extra_where]})
+ fragment_tags = models.Tag.objects.usage_for_model(models.Fragment, counts=True,
+ extra={'where': ['catalogue_tag.category = "theme"']})
+ categories = split_tags(tags)
+
+ form = forms.SearchForm()
+ return render_to_response('catalogue/main_page.html', locals(),
+ context_instance=RequestContext(request))
+
+
+def book_list(request):
+ books = models.Book.objects.all()
+ form = forms.SearchForm()
+
+ books_by_first_letter = SortedDict()
+ for book in books:
+ books_by_first_letter.setdefault(book.title[0], []).append(book)
+
+ return render_to_response('catalogue/book_list.html', locals(),
+ context_instance=RequestContext(request))
+
+
+def tagged_object_list(request, tags=''):
+ try:
+ tags = models.Tag.get_tag_list(tags)
+ except models.Tag.DoesNotExist:
+ raise Http404
+
+ model = models.Book
+ theme_is_set = any(tag.category == 'theme' for tag in tags)
+ if theme_is_set:
+ model = models.Fragment
+
+ if request.user.is_authenticated():
+ extra_where = '(NOT catalogue_tag.category = "set" OR catalogue_tag.user_id = %d)' % request.user.id
+ else:
+ extra_where = 'NOT catalogue_tag.category = "set"'
+ related_tags = models.Tag.objects.related_for_model(tags, model, counts=True, extra={'where': [extra_where]})
+ categories = split_tags(related_tags)
+
+ return newtagging_views.tagged_object_list(
+ request,
+ tag_model=models.Tag,
+ queryset_or_model=model,
+ tags=tags,
+ template_name='catalogue/tagged_object_list.html',
+ extra_context = {'categories': categories },
+ )
+
+
+def book_detail(request, slug):
+ book = get_object_or_404(models.Book, slug=slug)
+ tags = list(book.tags.filter(~Q(category='set')))
+ categories = split_tags(tags)
+
+ return render_to_response('catalogue/book_detail.html', locals(),
+ context_instance=RequestContext(request))
+
+
+def logout_then_redirect(request):
+ auth.logout(request)
+ return HttpResponseRedirect(request.GET.get('next', '/'))
+
+
+@require_POST
+def register(request):
+ registration_form = UserCreationForm(request.POST, prefix='registration')
+ if registration_form.is_valid():
+ user = registration_form.save()
+ user = auth.authenticate(
+ username=registration_form.cleaned_data['username'],
+ password=registration_form.cleaned_data['password1']
+ )
+ auth.login(request, user)
+ response_data = {'success': True, 'errors': {}}
+ else:
+ response_data = {'success': False, 'errors': registration_form.errors}
+ return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
+
+
+@require_POST
+def login(request):
+ form = AuthenticationForm(data=request.POST, prefix='login')
+ if form.is_valid():
+ auth.login(request, form.get_user())
+ response_data = {'success': True, 'errors': {}}
+ else:
+ response_data = {'success': False, 'errors': form.errors}
+ return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
+
+
+def book_sets(request, slug):
+ book = get_object_or_404(models.Book, slug=slug)
+ user_sets = models.Tag.objects.filter(category='set', user=request.user)
+ book_sets = book.tags.filter(category='set', user=request.user)
+
+ if not request.user.is_authenticated():
+ return HttpResponse('<p>Aby zarządzać swoimi półkami, musisz się zalogować.</p>')
+
+ if request.method == 'POST':
+ form = forms.ObjectSetsForm(book, request.user, request.POST)
+ if form.is_valid():
+ book.tags = ([models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] +
+ list(book.tags.filter(~Q(category='set') | ~Q(user=request.user))))
+ if request.is_ajax():
+ return HttpResponse('<p>Półki zostały zapisane.</p>')
+ else:
+ return HttpResponseRedirect('/')
+ else:
+ form = forms.ObjectSetsForm(book, request.user)
+ new_set_form = forms.NewSetForm()
+
+ return render_to_response('catalogue/book_sets.html', locals(),
+ context_instance=RequestContext(request))
+
+
+def fragment_sets(request, id):
+ fragment = get_object_or_404(models.Fragment, pk=id)
+ user_sets = models.Tag.objects.filter(category='set', user=request.user)
+ fragment_sets = fragment.tags.filter(category='set', user=request.user)
+
+ if not request.user.is_authenticated():
+ return HttpResponse('<p>Aby zarządzać swoimi półkami, musisz się zalogować.</p>')
+
+ if request.method == 'POST':
+ form = forms.ObjectSetsForm(fragment, request.user, request.POST)
+ if form.is_valid():
+ fragment.tags = ([models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] +
+ list(fragment.tags.filter(~Q(category='set') | ~Q(user=request.user))))
+ if request.is_ajax():
+ return HttpResponse('<p>Półki zostały zapisane.</p>')
+ else:
+ return HttpResponseRedirect('/')
+ else:
+ form = forms.ObjectSetsForm(fragment, request.user)
+ new_set_form = forms.NewSetForm()
+
+ return render_to_response('catalogue/fragment_sets.html', locals(),
+ context_instance=RequestContext(request))
+
+
+@login_required
+@require_POST
+def new_set(request):
+ new_set_form = forms.NewSetForm(request.POST)
+ if new_set_form.is_valid():
+ new_set = new_set_form.save(request.user)
+ return HttpResponse(u'<p>Półka <strong>%s</strong> została utworzona</p>' % new_set)
+
+ return render_to_response('catalogue/book_sets.html', locals(),
+ context_instance=RequestContext(request))
+
+
+@login_required
+@require_POST
+def delete_shelf(request, slug):
+ user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
+ user_set.delete()
+ return HttpResponse(u'<p>Półka <strong>%s</strong> została usunięta</p>' % user_set.name)
+
+
+@login_required
+def user_shelves(request):
+ shelves = models.Tag.objects.filter(category='set', user=request.user)
+ new_set_form = forms.NewSetForm()
+ return render_to_response('catalogue/user_shelves.html', locals(),
+ context_instance=RequestContext(request))
+
--- /dev/null
+from django.contrib import admin
+
+from chunks.models import Chunk
+
+
+class ChunkAdmin(admin.ModelAdmin):
+ list_display = ('key', 'description',)
+ search_fields = ('key', 'content',)
+
+admin.site.register(Chunk, ChunkAdmin)
+
--- /dev/null
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+
+class Chunk(models.Model):
+ """
+ A Chunk is a piece of content associated with a unique key that can be inserted into
+ any template with the use of a special template tag.
+ """
+ key = models.CharField(_('key'), help_text=_('A unique name for this chunk of content'), primary_key=True, max_length=255)
+ description = models.CharField(_('description'), blank=True, max_length=255)
+ content = models.TextField(_('content'), blank=True)
+
+ class Meta:
+ ordering = ('key',)
+ verbose_name = _('chunk')
+ verbose_name_plural = _('chunks')
+
+ def __unicode__(self):
+ return u'%s' % (self.key,)
+
--- /dev/null
+from django import template
+from django.db import models
+from django.core.cache import cache
+
+register = template.Library()
+
+Chunk = models.get_model('chunks', 'chunk')
+CACHE_PREFIX = "chunk_"
+
+def do_get_chunk(parser, token):
+ # split_contents() knows not to split quoted strings.
+ tokens = token.split_contents()
+ if len(tokens) < 2 or len(tokens) > 3:
+ raise template.TemplateSyntaxError, "%r tag should have either 2 or 3 arguments" % (tokens[0],)
+ if len(tokens) == 2:
+ tag_name, key = tokens
+ cache_time = 0
+ if len(tokens) == 3:
+ tag_name, key, cache_time = tokens
+ # Check to see if the key is properly double/single quoted
+ if not (key[0] == key[-1] and key[0] in ('"', "'")):
+ raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name
+ # Send key without quotes and caching time
+ return ChunkNode(key[1:-1], cache_time)
+
+class ChunkNode(template.Node):
+ def __init__(self, key, cache_time=0):
+ self.key = key
+ self.cache_time = cache_time
+
+ def render(self, context):
+ try:
+ cache_key = CACHE_PREFIX + self.key
+ c = cache.get(cache_key)
+ if c is None:
+ c = Chunk.objects.get(key=self.key)
+ cache.set(cache_key, c, int(self.cache_time))
+ content = c.content
+ except Chunk.DoesNotExist:
+ n = Chunk(key=self.key)
+ n.save()
+ return ''
+ return content
+
+register.tag('chunk', do_get_chunk)
--- /dev/null
+from django.core.exceptions import ImproperlyConfigured
+from django.conf import settings
+
+COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG)
+COMPRESS_AUTO = getattr(settings, 'COMPRESS_AUTO', True)
+COMPRESS_VERSION = getattr(settings, 'COMPRESS_VERSION', False)
+COMPRESS_VERSION_PLACEHOLDER = getattr(settings, 'COMPRESS_VERSION_PLACEHOLDER', '?')
+COMPRESS_VERSION_DEFAULT = getattr(settings, 'COMPRESS_VERSION_DEFAULT', '0')
+
+COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compress.filters.csstidy.CSSTidyFilter'])
+COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compress.filters.jsmin.JSMinFilter'])
+COMPRESS_CSS = getattr(settings, 'COMPRESS_CSS', {})
+COMPRESS_JS = getattr(settings, 'COMPRESS_JS', {})
+
+if COMPRESS_CSS_FILTERS is None:
+ COMPRESS_CSS_FILTERS = []
+
+if COMPRESS_JS_FILTERS is None:
+ COMPRESS_JS_FILTERS = []
+
+if COMPRESS_VERSION and not COMPRESS_AUTO:
+ raise ImproperlyConfigured('COMPRESS_AUTO needs to be True when using COMPRESS_VERSION.')
--- /dev/null
+class FilterBase:
+ def __init__(self, verbose):
+ self.verbose = verbose
+
+ def filter_css(self, css):
+ raise NotImplementedError
+ def filter_js(self, js):
+ raise NotImplementedError
+
+class FilterError(Exception):
+ """
+ This exception is raised when a filter fails
+ """
+ pass
\ No newline at end of file
--- /dev/null
+import os
+import warnings
+import tempfile
+
+from django.conf import settings
+
+from compress.filter_base import FilterBase
+
+BINARY = getattr(settings, 'CSSTIDY_BINARY', 'csstidy')
+ARGUMENTS = getattr(settings, 'CSSTIDY_ARGUMENTS', '--template=highest')
+
+warnings.simplefilter('ignore', RuntimeWarning)
+
+class CSSTidyFilter(FilterBase):
+ def filter_css(self, css):
+ tmp_file = tempfile.NamedTemporaryFile(mode='w+b')
+ tmp_file.write(css)
+ tmp_file.flush()
+
+ output_file = tempfile.NamedTemporaryFile(mode='w+b')
+
+ command = '%s %s %s %s' % (BINARY, tmp_file.name, ARGUMENTS, output_file.name)
+
+ command_output = os.popen(command).read()
+
+ filtered_css = output_file.read()
+ output_file.close()
+ tmp_file.close()
+
+ if self.verbose:
+ print command_output
+
+ return filtered_css
--- /dev/null
+from django.conf import settings
+
+from compress.filter_base import FilterBase
+from compress.filters.csstidy_python.csstidy import CSSTidy
+
+COMPRESS_CSSTIDY_SETTINGS = getattr(settings, 'COMPRESS_CSSTIDY_SETTINGS', {})
+
+class CSSTidyFilter(FilterBase):
+ def filter_css(self, css):
+ tidy = CSSTidy()
+
+ for k, v in COMPRESS_CSSTIDY_SETTINGS.items():
+ tidy.setSetting(k, v)
+
+ tidy.parse(css)
+
+ r = tidy.Output('string')
+
+ return r
--- /dev/null
+# CSSTidy - CSS Parse
+#
+# CSS Parser class
+#
+# This file is part of CSSTidy.
+#
+# CSSTidy is free software you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation either version 2 of the License, or
+# (at your option) any later version.
+#
+# CSSTidy is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with CSSTidy if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# @license http://opensource.org/licenses/gpl-license.php GNU Public License
+# @package csstidy
+# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
+
+import re
+
+from optimizer import CSSOptimizer
+from output import CSSPrinter
+import data
+from tools import SortedDict
+
+class CSSTidy(object):
+ #Saves the parsed CSS
+ _css = ""
+ _raw_css = SortedDict()
+ _optimized_css = SortedDict()
+
+ #List of Tokens
+ _tokens = []
+
+ #Printer class
+ _output = None
+
+ #Optimiser class
+ _optimizer = None
+
+ #Saves the CSS charset (@charset)
+ _charset = ''
+
+ #Saves all @import URLs
+ _import = []
+
+ #Saves the namespace
+ _namespace = ''
+
+ #Contains the version of csstidy
+ _version = '1.3'
+
+ #Stores the settings
+ _settings = {}
+
+ # Saves the parser-status.
+ #
+ # Possible values:
+ # - is = in selector
+ # - ip = in property
+ # - iv = in value
+ # - instr = in string (started at " or ' or ( )
+ # - ic = in comment (ignore everything)
+ # - at = in @-block
+ _status = 'is'
+
+ #Saves the current at rule (@media)
+ _at = ''
+
+ #Saves the current selector
+ _selector = ''
+
+ #Saves the current property
+ _property = ''
+
+ #Saves the position of , in selectors
+ _sel_separate = []
+
+ #Saves the current value
+ _value = ''
+
+ #Saves the current sub-value
+ _sub_value = ''
+
+ #Saves all subvalues for a property.
+ _sub_value_arr = []
+
+ #Saves the char which opened the last string
+ _str_char = ''
+ _cur_string = ''
+
+ #Status from which the parser switched to ic or instr
+ _from = ''
+
+ #Variable needed to manage string-in-strings, for example url("foo.png")
+ _str_in_str = False
+
+ #=True if in invalid at-rule
+ _invalid_at = False
+
+ #=True if something has been added to the current selector
+ _added = False
+
+ #Saves the message log
+ _log = SortedDict()
+
+ #Saves the line number
+ _line = 1
+
+ def __init__(self):
+ self._settings['remove_bslash'] = True
+ self._settings['compress_colors'] = True
+ self._settings['compress_font-weight'] = True
+ self._settings['lowercase_s'] = False
+ self._settings['optimise_shorthands'] = 2
+ self._settings['remove_last_'] = False
+ self._settings['case_properties'] = 1
+ self._settings['sort_properties'] = False
+ self._settings['sort_selectors'] = False
+ self._settings['merge_selectors'] = 2
+ self._settings['discard_invalid_properties'] = False
+ self._settings['css_level'] = 'CSS2.1'
+ self._settings['preserve_css'] = False
+ self._settings['timestamp'] = False
+ self._settings['template'] = 'highest_compression'
+
+ #Maps self._status to methods
+ self.__statusMethod = {'is':self.__parseStatus_is, 'ip': self.__parseStatus_ip, 'iv':self.__parseStatus_iv, 'instr':self.__parseStatus_instr, 'ic':self.__parseStatus_ic, 'at':self.__parseStatus_at}
+
+ self._output = CSSPrinter(self)
+ self._optimizer = CSSOptimizer(self)
+
+ #Public Methods
+ def getSetting(self, setting):
+ return self._settings.get(setting, False)
+
+ #Set the value of a setting.
+ def setSetting(self, setting, value):
+ self._settings[setting] = value
+ return True
+
+ def log(self, message, ttype, line = -1):
+ if line == -1:
+ line = self._line
+
+ line = int(line)
+
+ add = {'m': message, 't': ttype}
+
+ if not self._log.has_key(line):
+ self._log[line] = []
+ self._log[line].append(add)
+ elif add not in self._log[line]:
+ self._log[line].append(add)
+
+
+ #Checks if a character is escaped (and returns True if it is)
+ def escaped(self, string, pos):
+ return not (string[pos-1] != '\\' or self.escaped(string, pos-1))
+
+ #Adds CSS to an existing media/selector
+ def merge_css_blocks(self, media, selector, css_add):
+ for prop, value in css_add.iteritems():
+ self.__css_add_property(media, selector, prop, value, False)
+
+ #Checks if $value is !important.
+ def is_important(self, value):
+ return '!important' in value.lower()
+
+ #Returns a value without !important
+ def gvw_important(self, value):
+ if self.is_important(value):
+ ret = value.strip()
+ ret = ret[0:-9]
+ ret = ret.strip()
+ ret = ret[0:-1]
+ ret = ret.strip()
+ return ret
+
+ return value
+
+ def parse(self, cssString):
+ #Switch from \r\n to \n
+ self._css = cssString.replace("\r\n", "\n") + ' '
+ self._raw_css = {}
+ self._optimized_css = {}
+ self._curComment = ''
+
+ #Start Parsing
+ i = 0
+ while i < len(cssString):
+ if self._css[i] == "\n" or self._css[i] == "\r":
+ self._line += 1
+
+ i += self.__statusMethod[self._status](i)
+
+ i += 1;
+
+ self._optimized_css = self._optimizer.optimize(self._raw_css)
+
+ def parseFile(self, filename):
+ try:
+ f = open(filename, "r")
+ self.parse(f.read())
+ finally:
+ f.close()
+
+ #Private Methods
+ def __parseStatus_is(self, idx):
+ """
+ Parse in Selector
+ """
+ ret = 0
+
+ if self.__is_token(self._css, idx):
+ if self._css[idx] == '/' and self._css[idx+1] == '*' and self._selector.strip() == '':
+ self._status = 'ic'
+ self._from = 'is'
+ return 1
+
+ elif self._css[idx] == '@' and self._selector.strip() == '':
+ #Check for at-rule
+ self._invalid_at = True
+
+ for name, ttype in data.at_rules.iteritems():
+ if self._css[idx+1:len(name)].lower() == name.lower():
+ if ttype == 'at':
+ self._at = '@' + name
+ else:
+ self._selector = '@' + name
+
+ self._status = ttype
+ self._invalid_at = False
+ ret += len(name)
+
+ if self._invalid_at:
+ self._selector = '@'
+ invalid_at_name = ''
+ for j in xrange(idx+1, len(self._css)):
+ if not self._css[j].isalpha():
+ break;
+
+ invalid_at_name += self._css[j]
+
+ self.log('Invalid @-rule: ' + invalid_at_name + ' (removed)', 'Warning')
+
+ elif self._css[idx] == '"' or self._css[idx] == "'":
+ self._cur_string = self._css[idx]
+ self._status = 'instr'
+ self._str_char = self._css[idx]
+ self._from = 'is'
+
+ elif self._invalid_at and self._css[idx] == ';':
+ self._invalid_at = False
+ self._status = 'is'
+
+ elif self._css[idx] == '{':
+ self._status = 'ip'
+ self.__add_token(data.SEL_START, self._selector)
+ self._added = False;
+
+ elif self._css[idx] == '}':
+ self.__add_token(data.AT_END, self._at)
+ self._at = ''
+ self._selector = ''
+ self._sel_separate = []
+
+ elif self._css[idx] == ',':
+ self._selector = self._selector.strip() + ','
+ self._sel_separate.append(len(self._selector))
+
+ elif self._css[idx] == '\\':
+ self._selector += self.__unicode(idx)
+
+ #remove unnecessary universal selector, FS#147
+ elif not (self._css[idx] == '*' and self._css[idx+1] in ('.', '#', '[', ':')):
+ self._selector += self._css[idx]
+
+ else:
+ lastpos = len(self._selector)-1
+
+ if lastpos == -1 or not ((self._selector[lastpos].isspace() or self.__is_token(self._selector, lastpos) and self._selector[lastpos] == ',') and self._css[idx].isspace()):
+ self._selector += self._css[idx]
+
+ return ret
+
+ def __parseStatus_ip(self, idx):
+ """
+ Parse in property
+ """
+ if self.__is_token(self._css, idx):
+ if (self._css[idx] == ':' or self._css[idx] == '=') and self._property != '':
+ self._status = 'iv'
+
+ if not self.getSetting('discard_invalid_properties') or self.__property_is_valid(self._property):
+ self.__add_token(data.PROPERTY, self._property)
+
+ elif self._css[idx] == '/' and self._css[idx+1] == '*' and self._property == '':
+ self._status = 'ic'
+ self._from = 'ip'
+ return 1
+
+ elif self._css[idx] == '}':
+ self.__explode_selectors()
+ self._status = 'is'
+ self._invalid_at = False
+ self.__add_token(data.SEL_END, self._selector)
+ self._selector = ''
+ self._property = ''
+
+ elif self._css[idx] == ';':
+ self._property = ''
+
+ elif self._css[idx] == '\\':
+ self._property += self.__unicode(idx)
+
+ elif not self._css[idx].isspace():
+ self._property += self._css[idx]
+
+ return 0
+
+ def __parseStatus_iv(self, idx):
+ """
+ Parse in value
+ """
+ pn = (( self._css[idx] == "\n" or self._css[idx] == "\r") and self.__property_is_next(idx+1) or idx == len(self._css)) #CHECK#
+ if self.__is_token(self._css, idx) or pn:
+ if self._css[idx] == '/' and self._css[idx+1] == '*':
+ self._status = 'ic'
+ self._from = 'iv'
+ return 1
+
+ elif self._css[idx] == '"' or self._css[idx] == "'" or self._css[idx] == '(':
+ self._cur_string = self._css[idx]
+ self._str_char = ')' if self._css[idx] == '(' else self._css[idx]
+ self._status = 'instr'
+ self._from = 'iv'
+
+ elif self._css[idx] == ',':
+ self._sub_value = self._sub_value.strip() + ','
+
+ elif self._css[idx] == '\\':
+ self._sub_value += self.__unicode(idx)
+
+ elif self._css[idx] == ';' or pn:
+ if len(self._selector) > 0 and self._selector[0] == '@' and data.at_rules.has_key(self._selector[1:]) and data.at_rules[self._selector[1:]] == 'iv':
+ self._sub_value_arr.append(self._sub_value.strip())
+
+ self._status = 'is'
+
+ if '@charset' in self._selector:
+ self._charset = self._sub_value_arr[0]
+
+ elif '@namespace' in self._selector:
+ self._namespace = ' '.join(self._sub_value_arr)
+
+ elif '@import' in self._selector:
+ self._import.append(' '.join(self._sub_value_arr))
+
+
+ self._sub_value_arr = []
+ self._sub_value = ''
+ self._selector = ''
+ self._sel_separate = []
+
+ else:
+ self._status = 'ip'
+
+ elif self._css[idx] != '}':
+ self._sub_value += self._css[idx]
+
+ if (self._css[idx] == '}' or self._css[idx] == ';' or pn) and self._selector != '':
+ if self._at == '':
+ self._at = data.DEFAULT_AT
+
+ #case settings
+ if self.getSetting('lowercase_s'):
+ self._selector = self._selector.lower()
+
+ self._property = self._property.lower()
+
+ if self._sub_value != '':
+ self._sub_value_arr.append(self._sub_value)
+ self._sub_value = ''
+
+ self._value = ' '.join(self._sub_value_arr)
+
+
+ self._selector = self._selector.strip()
+
+ valid = self.__property_is_valid(self._property)
+
+ if (not self._invalid_at or self.getSetting('preserve_css')) and (not self.getSetting('discard_invalid_properties') or valid):
+ self.__css_add_property(self._at, self._selector, self._property, self._value)
+ self.__add_token(data.VALUE, self._value)
+
+ if not valid:
+ if self.getSetting('discard_invalid_properties'):
+ self.log('Removed invalid property: ' + self._property, 'Warning')
+
+ else:
+ self.log('Invalid property in ' + self.getSetting('css_level').upper() + ': ' + self._property, 'Warning')
+
+ self._property = '';
+ self._sub_value_arr = []
+ self._value = ''
+
+ if self._css[idx] == '}':
+ self.__explode_selectors()
+ self.__add_token(data.SEL_END, self._selector)
+ self._status = 'is'
+ self._invalid_at = False
+ self._selector = ''
+
+ elif not pn:
+ self._sub_value += self._css[idx]
+
+ if self._css[idx].isspace():
+ if self._sub_value != '':
+ self._sub_value_arr.append(self._sub_value)
+ self._sub_value = ''
+
+ return 0
+
+ def __parseStatus_instr(self, idx):
+ """
+ Parse in String
+ """
+ if self._str_char == ')' and (self._css[idx] == '"' or self._css[idx] == "'") and not self.escaped(self._css, idx):
+ self._str_in_str = not self._str_in_str
+
+ temp_add = self._css[idx] # ...and no not-escaped backslash at the previous position
+ if (self._css[idx] == "\n" or self._css[idx] == "\r") and not (self._css[idx-1] == '\\' and not self.escaped(self._css, idx-1)):
+ temp_add = "\\A "
+ self.log('Fixed incorrect newline in string', 'Warning')
+
+ if not (self._str_char == ')' and self._css[idx].isspace() and not self._str_in_str):
+ self._cur_string += temp_add
+
+ if self._css[idx] == self._str_char and not self.escaped(self._css, idx) and not self._str_in_str:
+ self._status = self._from
+ regex = re.compile(r'([\s]+)', re.I | re.U | re.S)
+ if regex.match(self._cur_string) is None and self._property != 'content':
+ if self._str_char == '"' or self._str_char == "'":
+ self._cur_string = self._cur_string[1:-1]
+
+ elif len(self._cur_string) > 3 and (self._cur_string[1] == '"' or self._cur_string[1] == "'"):
+ self._cur_string = self._cur_string[0] + self._cur_string[2:-2] + self._cur_string[-1]
+
+ if self._from == 'iv':
+ self._sub_value += self._cur_string
+
+ elif self._from == 'is':
+ self._selector += self._cur_string
+
+ return 0
+
+ def __parseStatus_ic(self, idx):
+ """
+ Parse css In Comment
+ """
+ if self._css[idx] == '*' and self._css[idx+1] == '/':
+ self._status = self._from
+ self.__add_token(data.COMMENT, self._curComment)
+ self._curComment = ''
+ return 1
+
+ else:
+ self._curComment += self._css[idx]
+
+ return 0
+
+ def __parseStatus_at(self, idx):
+ """
+ Parse in at-block
+ """
+ if self.__is_token(string, idx):
+ if self._css[idx] == '/' and self._css[idx+1] == '*':
+ self._status = 'ic'
+ self._from = 'at'
+ return 1
+
+ elif self._css[i] == '{':
+ self._status = 'is'
+ self.__add_token(data.AT_START, self._at)
+
+ elif self._css[i] == ',':
+ self._at = self._at.strip() + ','
+
+ elif self._css[i] == '\\':
+ self._at += self.__unicode(i)
+ else:
+ lastpos = len(self._at)-1
+ if not (self._at[lastpos].isspace() or self.__is_token(self._at, lastpos) and self._at[lastpos] == ',') and self._css[i].isspace():
+ self._at += self._css[i]
+
+ return 0
+
+ def __explode_selectors(self):
+ #Explode multiple selectors
+ if self.getSetting('merge_selectors') == 1:
+ new_sels = []
+ lastpos = 0;
+ self._sel_separate.append(len(self._selector))
+
+ for num in xrange(len(self._sel_separate)):
+ pos = self._sel_separate[num]
+ if num == (len(self._sel_separate)): #CHECK#
+ pos += 1
+
+ new_sels.append(self._selector[lastpos:(pos-lastpos-1)])
+ lastpos = pos
+
+ if len(new_sels) > 1:
+ for selector in new_sels:
+ self.merge_css_blocks(self._at, selector, self._raw_css[self._at][self._selector])
+
+ del self._raw_css[self._at][self._selector]
+
+ self._sel_separate = []
+
+ #Adds a property with value to the existing CSS code
+ def __css_add_property(self, media, selector, prop, new_val):
+ if self.getSetting('preserve_css') or new_val.strip() == '':
+ return
+
+ if not self._raw_css.has_key(media):
+ self._raw_css[media] = SortedDict()
+
+ if not self._raw_css[media].has_key(selector):
+ self._raw_css[media][selector] = SortedDict()
+
+ self._added = True
+ if self._raw_css[media][selector].has_key(prop):
+ if (self.is_important(self._raw_css[media][selector][prop]) and self.is_important(new_val)) or not self.is_important(self._raw_css[media][selector][prop]):
+ del self._raw_css[media][selector][prop]
+ self._raw_css[media][selector][prop] = new_val.strip()
+
+ else:
+ self._raw_css[media][selector][prop] = new_val.strip()
+
+ #Checks if the next word in a string from pos is a CSS property
+ def __property_is_next(self, pos):
+ istring = self._css[pos: len(self._css)]
+ pos = istring.find(':')
+ if pos == -1:
+ return False;
+
+ istring = istring[:pos].strip().lower()
+ if data.all_properties.has_key(istring):
+ self.log('Added semicolon to the end of declaration', 'Warning')
+ return True
+
+ return False;
+
+ #Checks if a property is valid
+ def __property_is_valid(self, prop):
+ return (data.all_properties.has_key(prop) and data.all_properties[prop].find(self.getSetting('css_level').upper()) != -1)
+
+ #Adds a token to self._tokens
+ def __add_token(self, ttype, cssdata, do=False):
+ if self.getSetting('preserve_css') or do:
+ if ttype == data.COMMENT:
+ token = [ttype, cssdata]
+ else:
+ token = [ttype, cssdata.strip()]
+
+ self._tokens.append(token)
+
+ #Parse unicode notations and find a replacement character
+ def __unicode(self, idx):
+ ##FIX##
+ return ''
+
+ #Starts parsing from URL
+ ##USED?
+ def __parse_from_url(self, url):
+ try:
+ if "http" in url.lower() or "https" in url.lower():
+ f = urllib.urlopen(url)
+ else:
+ f = open(url)
+
+ data = f.read()
+ return self.parse(data)
+ finally:
+ f.close()
+
+ #Checks if there is a token at the current position
+ def __is_token(self, string, idx):
+ return (string[idx] in data.tokens and not self.escaped(string, idx))
+
+
+ #Property Methods
+ def _getOutput(self):
+ self._output.prepare(self._optimized_css)
+ return self._output.render
+
+ def _getLog(self):
+ ret = ""
+ ks = self._log.keys()
+ ks.sort()
+ for line in ks:
+ for msg in self._log[line]:
+ ret += "Type: " + msg['t'] + "\n"
+ ret += "Message: " + msg['m'] + "\n"
+ ret += "\n"
+
+ return ret
+
+ def _getCSS(self):
+ return self._css
+
+
+ #Properties
+ Output = property(_getOutput, None)
+ Log = property(_getLog, None)
+ CSS = property(_getCSS, None)
+
+
+if __name__ == '__main__':
+ import sys
+ tidy = CSSTidy()
+ f = open(sys.argv[1], "r")
+ css = f.read()
+ f.close()
+ tidy.parse(css)
+ tidy.Output('file', filename="Stylesheet.min.css")
+ print tidy.Output()
+ #print tidy._import
\ No newline at end of file
--- /dev/null
+# Various CSS Data for CSSTidy
+#
+# This file is part of CSSTidy.
+#
+# CSSTidy is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# CSSTidy is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with CSSTidy; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# @license http://opensource.org/licenses/gpl-license.php GNU Public License
+# @package csstidy
+# @author Florian Schmitz (floele at gmail dot com) 2005
+
+AT_START = 1
+AT_END = 2
+SEL_START = 3
+SEL_END = 4
+PROPERTY = 5
+VALUE = 6
+COMMENT = 7
+DEFAULT_AT = 41
+
+# All whitespace allowed in CSS
+#
+# @global array whitespace
+# @version 1.0
+whitespace = frozenset([' ',"\n","\t","\r","\x0B"])
+
+# All CSS tokens used by csstidy
+#
+# @global string tokens
+# @version 1.0
+tokens = '/@}{;:=\'"(,\\!$%&)#+.<>?[]^`|~'
+
+# All CSS units (CSS 3 units included)
+#
+# @see compress_numbers()
+# @global array units
+# @version 1.0
+units = frozenset(['in','cm','mm','pt','pc','px','rem','em','%','ex','gd','vw','vh','vm','deg','grad','rad','ms','s','khz','hz'])
+
+# Available at-rules
+#
+# @global array at_rules
+# @version 1.0
+at_rules = {'page':'is', 'font-face':'is', 'charset':'iv', 'import':'iv', 'namespace':'iv', 'media':'at'}
+
+# Properties that need a value with unit
+#
+# @todo CSS3 properties
+# @see compress_numbers()
+# @global array unit_values
+# @version 1.2
+unit_values = frozenset(['background', 'background-position', 'border', 'border-top', 'border-right', 'border-bottom',
+ 'border-left', 'border-width', 'border-top-width', 'border-right-width', 'border-left-width',
+ 'border-bottom-width', 'bottom', 'border-spacing', 'font-size','height', 'left', 'margin', 'margin-top',
+ 'margin-right', 'margin-bottom', 'margin-left', 'max-height', 'max-width', 'min-height', 'min-width',
+ 'outline-width', 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left','position',
+ 'right', 'top', 'text-indent', 'letter-spacing', 'word-spacing', 'width'
+ ])
+
+
+# Properties that allow <color> as value
+#
+# @todo CSS3 properties
+# @see compress_numbers()
+# @global array color_values
+# @version 1.0
+color_values = frozenset(['background-color', 'border-color', 'border-top-color', 'border-right-color',
+ 'border-bottom-color', 'border-left-color', 'color', 'outline-color'])
+
+
+# Default values for the background properties
+#
+# @todo Possibly property names will change during CSS3 development
+# @global array background_prop_default
+# @see dissolve_short_bg()
+# @see merge_bg()
+# @version 1.0
+background_prop_default = {}
+background_prop_default['background-image'] = 'none'
+background_prop_default['background-size'] = 'auto'
+background_prop_default['background-repeat'] = 'repeat'
+background_prop_default['background-position'] = '0 0'
+background_prop_default['background-attachment'] = 'scroll'
+background_prop_default['background-clip'] = 'border'
+background_prop_default['background-origin'] = 'padding'
+background_prop_default['background-color'] = 'transparent'
+
+# A list of non-W3C color names which get replaced by their hex-codes
+#
+# @global array replace_colors
+# @see cut_color()
+# @version 1.0
+replace_colors = {}
+replace_colors['aliceblue'] = '#F0F8FF'
+replace_colors['antiquewhite'] = '#FAEBD7'
+replace_colors['aquamarine'] = '#7FFFD4'
+replace_colors['azure'] = '#F0FFFF'
+replace_colors['beige'] = '#F5F5DC'
+replace_colors['bisque'] = '#FFE4C4'
+replace_colors['blanchedalmond'] = '#FFEBCD'
+replace_colors['blueviolet'] = '#8A2BE2'
+replace_colors['brown'] = '#A52A2A'
+replace_colors['burlywood'] = '#DEB887'
+replace_colors['cadetblue'] = '#5F9EA0'
+replace_colors['chartreuse'] = '#7FFF00'
+replace_colors['chocolate'] = '#D2691E'
+replace_colors['coral'] = '#FF7F50'
+replace_colors['cornflowerblue'] = '#6495ED'
+replace_colors['cornsilk'] = '#FFF8DC'
+replace_colors['crimson'] = '#DC143C'
+replace_colors['cyan'] = '#00FFFF'
+replace_colors['darkblue'] = '#00008B'
+replace_colors['darkcyan'] = '#008B8B'
+replace_colors['darkgoldenrod'] = '#B8860B'
+replace_colors['darkgray'] = '#A9A9A9'
+replace_colors['darkgreen'] = '#006400'
+replace_colors['darkkhaki'] = '#BDB76B'
+replace_colors['darkmagenta'] = '#8B008B'
+replace_colors['darkolivegreen'] = '#556B2F'
+replace_colors['darkorange'] = '#FF8C00'
+replace_colors['darkorchid'] = '#9932CC'
+replace_colors['darkred'] = '#8B0000'
+replace_colors['darksalmon'] = '#E9967A'
+replace_colors['darkseagreen'] = '#8FBC8F'
+replace_colors['darkslateblue'] = '#483D8B'
+replace_colors['darkslategray'] = '#2F4F4F'
+replace_colors['darkturquoise'] = '#00CED1'
+replace_colors['darkviolet'] = '#9400D3'
+replace_colors['deeppink'] = '#FF1493'
+replace_colors['deepskyblue'] = '#00BFFF'
+replace_colors['dimgray'] = '#696969'
+replace_colors['dodgerblue'] = '#1E90FF'
+replace_colors['feldspar'] = '#D19275'
+replace_colors['firebrick'] = '#B22222'
+replace_colors['floralwhite'] = '#FFFAF0'
+replace_colors['forestgreen'] = '#228B22'
+replace_colors['gainsboro'] = '#DCDCDC'
+replace_colors['ghostwhite'] = '#F8F8FF'
+replace_colors['gold'] = '#FFD700'
+replace_colors['goldenrod'] = '#DAA520'
+replace_colors['greenyellow'] = '#ADFF2F'
+replace_colors['honeydew'] = '#F0FFF0'
+replace_colors['hotpink'] = '#FF69B4'
+replace_colors['indianred'] = '#CD5C5C'
+replace_colors['indigo'] = '#4B0082'
+replace_colors['ivory'] = '#FFFFF0'
+replace_colors['khaki'] = '#F0E68C'
+replace_colors['lavender'] = '#E6E6FA'
+replace_colors['lavenderblush'] = '#FFF0F5'
+replace_colors['lawngreen'] = '#7CFC00'
+replace_colors['lemonchiffon'] = '#FFFACD'
+replace_colors['lightblue'] = '#ADD8E6'
+replace_colors['lightcoral'] = '#F08080'
+replace_colors['lightcyan'] = '#E0FFFF'
+replace_colors['lightgoldenrodyellow'] = '#FAFAD2'
+replace_colors['lightgrey'] = '#D3D3D3'
+replace_colors['lightgreen'] = '#90EE90'
+replace_colors['lightpink'] = '#FFB6C1'
+replace_colors['lightsalmon'] = '#FFA07A'
+replace_colors['lightseagreen'] = '#20B2AA'
+replace_colors['lightskyblue'] = '#87CEFA'
+replace_colors['lightslateblue'] = '#8470FF'
+replace_colors['lightslategray'] = '#778899'
+replace_colors['lightsteelblue'] = '#B0C4DE'
+replace_colors['lightyellow'] = '#FFFFE0'
+replace_colors['limegreen'] = '#32CD32'
+replace_colors['linen'] = '#FAF0E6'
+replace_colors['magenta'] = '#FF00FF'
+replace_colors['mediumaquamarine'] = '#66CDAA'
+replace_colors['mediumblue'] = '#0000CD'
+replace_colors['mediumorchid'] = '#BA55D3'
+replace_colors['mediumpurple'] = '#9370D8'
+replace_colors['mediumseagreen'] = '#3CB371'
+replace_colors['mediumslateblue'] = '#7B68EE'
+replace_colors['mediumspringgreen'] = '#00FA9A'
+replace_colors['mediumturquoise'] = '#48D1CC'
+replace_colors['mediumvioletred'] = '#C71585'
+replace_colors['midnightblue'] = '#191970'
+replace_colors['mintcream'] = '#F5FFFA'
+replace_colors['mistyrose'] = '#FFE4E1'
+replace_colors['moccasin'] = '#FFE4B5'
+replace_colors['navajowhite'] = '#FFDEAD'
+replace_colors['oldlace'] = '#FDF5E6'
+replace_colors['olivedrab'] = '#6B8E23'
+replace_colors['orangered'] = '#FF4500'
+replace_colors['orchid'] = '#DA70D6'
+replace_colors['palegoldenrod'] = '#EEE8AA'
+replace_colors['palegreen'] = '#98FB98'
+replace_colors['paleturquoise'] = '#AFEEEE'
+replace_colors['palevioletred'] = '#D87093'
+replace_colors['papayawhip'] = '#FFEFD5'
+replace_colors['peachpuff'] = '#FFDAB9'
+replace_colors['peru'] = '#CD853F'
+replace_colors['pink'] = '#FFC0CB'
+replace_colors['plum'] = '#DDA0DD'
+replace_colors['powderblue'] = '#B0E0E6'
+replace_colors['rosybrown'] = '#BC8F8F'
+replace_colors['royalblue'] = '#4169E1'
+replace_colors['saddlebrown'] = '#8B4513'
+replace_colors['salmon'] = '#FA8072'
+replace_colors['sandybrown'] = '#F4A460'
+replace_colors['seagreen'] = '#2E8B57'
+replace_colors['seashell'] = '#FFF5EE'
+replace_colors['sienna'] = '#A0522D'
+replace_colors['skyblue'] = '#87CEEB'
+replace_colors['slateblue'] = '#6A5ACD'
+replace_colors['slategray'] = '#708090'
+replace_colors['snow'] = '#FFFAFA'
+replace_colors['springgreen'] = '#00FF7F'
+replace_colors['steelblue'] = '#4682B4'
+replace_colors['tan'] = '#D2B48C'
+replace_colors['thistle'] = '#D8BFD8'
+replace_colors['tomato'] = '#FF6347'
+replace_colors['turquoise'] = '#40E0D0'
+replace_colors['violet'] = '#EE82EE'
+replace_colors['violetred'] = '#D02090'
+replace_colors['wheat'] = '#F5DEB3'
+replace_colors['whitesmoke'] = '#F5F5F5'
+replace_colors['yellowgreen'] = '#9ACD32'
+
+#A list of optimized colors
+optimize_colors = {}
+optimize_colors['black'] = '#000'
+optimize_colors['fuchsia'] = '#F0F'
+optimize_colors['white'] = '#FFF'
+optimize_colors['yellow'] = '#FF0'
+optimize_colors['cyan'] = '#0FF'
+optimize_colors['magenta'] = '#F0F'
+optimize_colors['lightslategray'] = '#789'
+
+optimize_colors['#800000'] = 'maroon'
+optimize_colors['#FFA500'] = 'orange'
+optimize_colors['#808000'] = 'olive'
+optimize_colors['#800080'] = 'purple'
+optimize_colors['#008000'] = 'green'
+optimize_colors['#000080'] = 'navy'
+optimize_colors['#008080'] = 'teal'
+optimize_colors['#C0C0C0'] = 'silver'
+optimize_colors['#808080'] = 'gray'
+optimize_colors['#4B0082'] = 'indigo'
+optimize_colors['#FFD700'] = 'gold'
+optimize_colors['#A52A2A'] = 'brown'
+optimize_colors['#00FFFF'] = 'cyan'
+optimize_colors['#EE82EE'] = 'violet'
+optimize_colors['#DA70D6'] = 'orchid'
+optimize_colors['#FFE4C4'] = 'bisque'
+optimize_colors['#F0E68C'] = 'khaki'
+optimize_colors['#F5DEB3'] = 'wheat'
+optimize_colors['#FF7F50'] = 'coral'
+optimize_colors['#F5F5DC'] = 'beige'
+optimize_colors['#F0FFFF'] = 'azure'
+optimize_colors['#A0522D'] = 'sienna'
+optimize_colors['#CD853F'] = 'peru'
+optimize_colors['#FFFFF0'] = 'ivory'
+optimize_colors['#DDA0DD'] = 'plum'
+optimize_colors['#D2B48C'] = 'tan'
+optimize_colors['#FFC0CB'] = 'pink'
+optimize_colors['#FFFAFA'] = 'snow'
+optimize_colors['#FA8072'] = 'salmon'
+optimize_colors['#FF6347'] = 'tomato'
+optimize_colors['#FAF0E6'] = 'linen'
+optimize_colors['#F00'] = 'red'
+
+
+# A list of all shorthand properties that are devided into four properties and/or have four subvalues
+#
+# @global array shorthands
+# @todo Are there new ones in CSS3?
+# @see dissolve_4value_shorthands()
+# @see merge_4value_shorthands()
+# @version 1.0
+shorthands = {}
+shorthands['border-color'] = ['border-top-color','border-right-color','border-bottom-color','border-left-color']
+shorthands['border-style'] = ['border-top-style','border-right-style','border-bottom-style','border-left-style']
+shorthands['border-width'] = ['border-top-width','border-right-width','border-bottom-width','border-left-width']
+shorthands['margin'] = ['margin-top','margin-right','margin-bottom','margin-left']
+shorthands['padding'] = ['padding-top','padding-right','padding-bottom','padding-left']
+shorthands['-moz-border-radius'] = 0
+
+# All CSS Properties. Needed for csstidy::property_is_next()
+#
+# @global array all_properties
+# @todo Add CSS3 properties
+# @version 1.0
+# @see csstidy::property_is_next()
+all_properties = {}
+all_properties['background'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['background-color'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['background-image'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['background-repeat'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['background-attachment'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['background-position'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-top'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-right'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-left'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-color'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-top-color'] = 'CSS2.0,CSS2.1'
+all_properties['border-bottom-color'] = 'CSS2.0,CSS2.1'
+all_properties['border-left-color'] = 'CSS2.0,CSS2.1'
+all_properties['border-right-color'] = 'CSS2.0,CSS2.1'
+all_properties['border-style'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-top-style'] = 'CSS2.0,CSS2.1'
+all_properties['border-right-style'] = 'CSS2.0,CSS2.1'
+all_properties['border-left-style'] = 'CSS2.0,CSS2.1'
+all_properties['border-bottom-style'] = 'CSS2.0,CSS2.1'
+all_properties['border-width'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-top-width'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-right-width'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-left-width'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-bottom-width'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['border-collapse'] = 'CSS2.0,CSS2.1'
+all_properties['border-spacing'] = 'CSS2.0,CSS2.1'
+all_properties['bottom'] = 'CSS2.0,CSS2.1'
+all_properties['caption-side'] = 'CSS2.0,CSS2.1'
+all_properties['content'] = 'CSS2.0,CSS2.1'
+all_properties['clear'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['clip'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['color'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['counter-reset'] = 'CSS2.0,CSS2.1'
+all_properties['counter-increment'] = 'CSS2.0,CSS2.1'
+all_properties['cursor'] = 'CSS2.0,CSS2.1'
+all_properties['empty-cells'] = 'CSS2.0,CSS2.1'
+all_properties['display'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['direction'] = 'CSS2.0,CSS2.1'
+all_properties['float'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['font'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['font-family'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['font-style'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['font-variant'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['font-weight'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['font-stretch'] = 'CSS2.0'
+all_properties['font-size-adjust'] = 'CSS2.0'
+all_properties['font-size'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['height'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['left'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['line-height'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['list-style'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['list-style-type'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['list-style-image'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['list-style-position'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['margin'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['margin-top'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['margin-right'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['margin-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['margin-left'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['marks'] = 'CSS1.0,CSS2.0'
+all_properties['marker-offset'] = 'CSS2.0'
+all_properties['max-height'] = 'CSS2.0,CSS2.1'
+all_properties['max-width'] = 'CSS2.0,CSS2.1'
+all_properties['min-height'] = 'CSS2.0,CSS2.1'
+all_properties['min-width'] = 'CSS2.0,CSS2.1'
+all_properties['overflow'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['orphans'] = 'CSS2.0,CSS2.1'
+all_properties['outline'] = 'CSS2.0,CSS2.1'
+all_properties['outline-width'] = 'CSS2.0,CSS2.1'
+all_properties['outline-style'] = 'CSS2.0,CSS2.1'
+all_properties['outline-color'] = 'CSS2.0,CSS2.1'
+all_properties['padding'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['padding-top'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['padding-right'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['padding-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['padding-left'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['page-break-before'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['page-break-after'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['page-break-inside'] = 'CSS2.0,CSS2.1'
+all_properties['page'] = 'CSS2.0'
+all_properties['position'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['quotes'] = 'CSS2.0,CSS2.1'
+all_properties['right'] = 'CSS2.0,CSS2.1'
+all_properties['size'] = 'CSS1.0,CSS2.0'
+all_properties['speak-header'] = 'CSS2.0,CSS2.1'
+all_properties['table-layout'] = 'CSS2.0,CSS2.1'
+all_properties['top'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['text-indent'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['text-align'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['text-decoration'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['text-shadow'] = 'CSS2.0'
+all_properties['letter-spacing'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['word-spacing'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['text-transform'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['white-space'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['unicode-bidi'] = 'CSS2.0,CSS2.1'
+all_properties['vertical-align'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['visibility'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['width'] = 'CSS1.0,CSS2.0,CSS2.1'
+all_properties['widows'] = 'CSS2.0,CSS2.1'
+all_properties['z-index'] = 'CSS1.0,CSS2.0,CSS2.1'
+
+# Speech #
+all_properties['volume'] = 'CSS2.0,CSS2.1'
+all_properties['speak'] = 'CSS2.0,CSS2.1'
+all_properties['pause'] = 'CSS2.0,CSS2.1'
+all_properties['pause-before'] = 'CSS2.0,CSS2.1'
+all_properties['pause-after'] = 'CSS2.0,CSS2.1'
+all_properties['cue'] = 'CSS2.0,CSS2.1'
+all_properties['cue-before'] = 'CSS2.0,CSS2.1'
+all_properties['cue-after'] = 'CSS2.0,CSS2.1'
+all_properties['play-during'] = 'CSS2.0,CSS2.1'
+all_properties['azimuth'] = 'CSS2.0,CSS2.1'
+all_properties['elevation'] = 'CSS2.0,CSS2.1'
+all_properties['speech-rate'] = 'CSS2.0,CSS2.1'
+all_properties['voice-family'] = 'CSS2.0,CSS2.1'
+all_properties['pitch'] = 'CSS2.0,CSS2.1'
+all_properties['pitch-range'] = 'CSS2.0,CSS2.1'
+all_properties['stress'] = 'CSS2.0,CSS2.1'
+all_properties['richness'] = 'CSS2.0,CSS2.1'
+all_properties['speak-punctuation'] = 'CSS2.0,CSS2.1'
+all_properties['speak-numeral'] = 'CSS2.0,CSS2.1'
\ No newline at end of file
--- /dev/null
+# CSSTidy - CSS Optimizer
+#
+# CSS Optimizer class
+#
+# This file is part of CSSTidy.
+#
+# CSSTidy is free software you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation either version 2 of the License, or
+# (at your option) any later version.
+#
+# CSSTidy is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with CSSTidy if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# @license http://opensource.org/licenses/gpl-license.php GNU Public License
+# @package csstidy
+# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
+
+import data
+from tools import SortedDict
+
+
+class CSSOptimizer(object):
+ def __init__(self, parser):
+ #raw_css is a dict
+ self.parser = parser
+ self._optimized_css = SortedDict
+
+
+#PUBLIC METHODS
+ def optimize(self, raw_css):
+ if self.parser.getSetting('preserve_css'):
+ return raw_css
+
+ self._optimized_css = raw_css
+
+ if self.parser.getSetting('merge_selectors') == 2:
+ self.__merge_selectors()
+
+ ##OPTIMIZE##
+ for media, css in self._optimized_css.iteritems():
+ for selector, cssdata in css.iteritems():
+ if self.parser.getSetting('optimise_shorthands') >= 1:
+ cssdata = self.__merge_4value_shorthands(cssdata)
+
+ if self.parser.getSetting('optimise_shorthands') >= 2:
+ cssdata = self.__merge_bg(cssdata)
+
+ for item, value in cssdata.iteritems():
+ value = self.__compress_numbers(item, value)
+ value = self.__compress_important(value)
+
+ if item in data.color_values and self.parser.getSetting('compress_colors'):
+ old = value[:]
+ value = self.__compress_color(value)
+ if old != value:
+ self.parser.log('In "' + selector + '" Optimised ' + item + ': Changed ' + old + ' to ' + value, 'Information')
+
+ if item == 'font-weight' and self.parser.getSetting('compress_font-weight'):
+ if value == 'bold':
+ value = '700'
+ self.parser.log('In "' + selector + '" Optimised font-weight: Changed "bold" to "700"', 'Information')
+
+ elif value == 'normal':
+ value = '400'
+ self.parser.log('In "' + selector + '" Optimised font-weight: Changed "normal" to "400"', 'Information')
+
+ self._optimized_css[media][selector][item] = value
+
+
+ return self._optimized_css
+
+
+#PRIVATE METHODS
+ def __merge_bg(self, cssdata):
+ """
+ Merges all background properties
+ @cssdata (dict) is a dictionary of the selector properties
+ """
+ #Max number of background images. CSS3 not yet fully implemented
+ img = 1
+ clr = 1
+ bg_img_list = []
+ if cssdata.has_key('background-image'):
+ img = len(cssdata['background-image'].split(','))
+ bg_img_list = self.parser.gvw_important(cssdata['background-image']).split(',')
+
+ elif cssdata.has_key('background-color'):
+ clr = len(cssdata['background-color'].split(','))
+
+
+ number_of_values = max(img, clr, 1)
+
+ new_bg_value = ''
+ important = ''
+
+ for i in xrange(number_of_values):
+ for bg_property, default_value in data.background_prop_default.iteritems():
+ #Skip if property does not exist
+ if not cssdata.has_key(bg_property):
+ continue
+
+ cur_value = cssdata[bg_property]
+
+ #Skip some properties if there is no background image
+ if (len(bg_img_list) > i and bg_img_list[i] == 'none') and bg_property in frozenset(['background-size', 'background-position', 'background-attachment', 'background-repeat']):
+ continue
+
+ #Remove !important
+ if self.parser.is_important(cur_value):
+ important = ' !important'
+ cur_value = self.parser.gvw_important(cur_value)
+
+ #Do not add default values
+ if cur_value == default_value:
+ continue
+
+ temp = cur_value.split(',')
+
+ if len(temp) > i:
+ if bg_property == 'background-size':
+ new_bg_value += '(' + temp[i] + ') '
+
+ else:
+ new_bg_value += temp[i] + ' '
+
+ new_bg_value = new_bg_value.strip()
+ if i != (number_of_values-1):
+ new_bg_value += ','
+
+ #Delete all background-properties
+ for bg_property, default_value in data.background_prop_default.iteritems():
+ try:
+ del cssdata[bg_property]
+ except:
+ pass
+
+ #Add new background property
+ if new_bg_value != '':
+ cssdata['background'] = new_bg_value + important
+
+ return cssdata
+
+ def __merge_4value_shorthands(self, cssdata):
+ """
+ Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
+ @cssdata (dict) is a dictionary of the selector properties
+ """
+ for key, value in data.shorthands.iteritems():
+ important = ''
+ if value != 0 and cssdata.has_key(value[0]) and cssdata.has_key(value[1]) and cssdata.has_key(value[2]) and cssdata.has_key(value[3]):
+ cssdata[key] = ''
+
+ for i in xrange(4):
+ val = cssdata[value[i]]
+ if self.parser.is_important(val):
+ important = '!important'
+ cssdata[key] += self.parser.gvw_important(val) + ' '
+
+ else:
+ cssdata[key] += val + ' '
+
+ del cssdata[value[i]]
+ if cssdata.has_key(key):
+ cssdata[key] = self.__shorthand(cssdata[key] + important.strip())
+
+ return cssdata
+
+
+ def __merge_selectors(self):
+ """
+ Merges selectors with same properties. Example: a{color:red} b{color:red} . a,b{color:red}
+ Very basic and has at least one bug. Hopefully there is a replacement soon.
+ @selector_one (string) is the current selector
+ @value_one (dict) is a dictionary of the selector properties
+ Note: Currently is the elements of a selector are identical, but in a different order, they are not merged
+ """
+
+ ##OPTIMIZE##
+ ##FIX##
+
+ raw_css = self._optimized_css.copy()
+ delete = []
+ add = SortedDict()
+ for media, css in raw_css.iteritems():
+ for selector_one, value_one in css.iteritems():
+ newsel = selector_one
+
+ for selector_two, value_two in css.iteritems():
+ if selector_one == selector_two:
+ #We need to skip self
+ continue
+
+ if value_one == value_two:
+ #Ok, we need to merge these two selectors
+ newsel += ', ' + selector_two
+ delete.append((media, selector_two))
+
+
+ if not add.has_key(media):
+ add[media] = SortedDict()
+
+ add[media][newsel] = value_one
+ delete.append((media, selector_one))
+
+ for item in delete:
+ try:
+ del self._optimized_css[item[0]][item[1]]
+ except:
+ #Must have already been deleted
+ continue
+
+ for media, css in add.iteritems():
+ self._optimized_css[media].update(css)
+
+
+
+ def __shorthand(self, value):
+ """
+ Compresses shorthand values. Example: margin:1px 1px 1px 1px . margin:1px
+ @value (string)
+ """
+
+ ##FIX##
+
+ important = '';
+ if self.parser.is_important(value):
+ value_list = self.parser.gvw_important(value)
+ important = '!important'
+ else:
+ value_list = value
+
+ ret = value
+ value_list = value_list.split(' ')
+
+ if len(value_list) == 4:
+ if value_list[0] == value_list[1] and value_list[0] == value_list[2] and value_list[0] == value_list[3]:
+ ret = value_list[0] + important
+
+ elif value_list[1] == value_list[3] and value_list[0] == value_list[2]:
+ ret = value_list[0] + ' ' + value_list[1] + important
+
+ elif value_list[1] == value_list[3]:
+ ret = value_list[0] + ' ' + value_list[1] + ' ' + value_list[2] + important
+
+ elif len(value_list) == 3:
+ if value_list[0] == value_list[1] and value_list[0] == value_list[2]:
+ ret = value_list[0] + important
+
+ elif value_list[0] == value_list[2]:
+ return value_list[0] + ' ' + value_list[1] + important
+
+ elif len(value_list) == 2:
+ if value_list[0] == value_list[1]:
+ ret = value_list[0] + important
+
+ if ret != value:
+ self.parser.log('Optimised shorthand notation: Changed "' + value + '" to "' + ret + '"', 'Information')
+
+ return ret
+
+ def __compress_important(self, value):
+ """
+ Removes unnecessary whitespace in ! important
+ @value (string)
+ """
+ if self.parser.is_important(value):
+ value = self.parser.gvw_important(value) + '!important'
+
+ return value
+
+ def __compress_numbers(self, prop, value):
+ """
+ Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
+ @value (string) is the posible number to be compressed
+ """
+
+ ##FIX##
+
+ value = value.split('/')
+
+ for l in xrange(len(value)):
+ #continue if no numeric value
+ if not (len(value[l]) > 0 and (value[l][0].isdigit() or value[l][0] in ('+', '-') )):
+ continue
+
+ #Fix bad colors
+ if prop in data.color_values:
+ value[l] = '#' + value[l]
+
+ is_floatable = False
+ try:
+ float(value[l])
+ is_floatable = True
+ except:
+ pass
+
+ if is_floatable and float(value[l]) == 0:
+ value[l] = '0'
+
+ elif value[l][0] != '#':
+ unit_found = False
+ for unit in data.units:
+ pos = value[l].lower().find(unit)
+ if pos != -1 and prop not in data.shorthands:
+ value[l] = self.__remove_leading_zeros(float(value[l][:pos])) + unit
+ unit_found = True
+ break;
+
+ if not unit_found and prop in data.unit_values and prop not in data.shorthands:
+ value[l] = self.__remove_leading_zeros(float(value[l])) + 'px'
+
+ elif not unit_found and prop not in data.shorthands:
+ value[l] = self.__remove_leading_zeros(float(value[l]))
+
+
+ if len(value) > 1:
+ return '/'.join(value)
+
+ return value[0]
+
+ def __remove_leading_zeros(self, float_val):
+ """
+ Removes the leading zeros from a float value
+ @float_val (float)
+ @returns (string)
+ """
+ #Remove leading zero
+ if abs(float_val) < 1:
+ if float_val < 0:
+ float_val = '-' . str(float_val)[2:]
+ else:
+ float_val = str(float_val)[1:]
+
+ return str(float_val)
+
+ def __compress_color(self, color):
+ """
+ Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
+ @color (string) the {posible} color to change
+ """
+
+ #rgb(0,0,0) . #000000 (or #000 in this case later)
+ if color[:4].lower() == 'rgb(':
+ color_tmp = color[4:(len(color)-5)]
+ color_tmp = color_tmp.split(',')
+
+ for c in color_tmp:
+ c = c.strip()
+ if c[:-1] == '%':
+ c = round((255*color_tmp[i])/100)
+
+ if color_tmp[i] > 255:
+ color_tmp[i] = 255
+
+ color = '#'
+
+ for i in xrange(3):
+ if color_tmp[i] < 16:
+ color += '0' + str(hex(color_tmp[i])).replace('0x', '')
+ else:
+ color += str(hex(color_tmp[i])).replace('0x', '')
+
+ #Fix bad color names
+ if data.replace_colors.has_key(color.lower()):
+ color = data.replace_colors[color.lower()]
+
+ #aabbcc . #abc
+ if len(color) == 7:
+ color_temp = color.lower()
+ if color_temp[0] == '#' and color_temp[1] == color_temp[2] and color_temp[3] == color_temp[4] and color_temp[5] == color_temp[6]:
+ color = '#' + color[1] + color[3] + color[5]
+
+ if data.optimize_colors.has_key(color.lower()):
+ color = data.optimize_colors[color.lower()]
+
+ return color
\ No newline at end of file
--- /dev/null
+# CSSTidy - CSS Printer
+#
+# CSS Printer class
+#
+# This file is part of CSSTidy.
+#
+# CSSTidy is free software you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation either version 2 of the License, or
+# (at your option) any later version.
+#
+# CSSTidy is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with CSSTidy if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# @license http://opensource.org/licenses/gpl-license.php GNU Public License
+# @package csstidy
+# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
+
+import data
+
+class CSSPrinter(object):
+ def __init__(self, parser):
+ self.parser = parser
+ self._css = {}
+ self.__renderMethods = {'string': self.__renderString, 'file': self.__renderFile}
+
+#PUBLIC METHODS
+ def prepare(self, css):
+ self._css = css
+
+ def render(self, output="string", *args, **kwargs):
+ return self.__renderMethods[output](*args, **kwargs)
+
+#PRIVATE METHODS
+ def __renderString(self, *args, **kwargs):
+ ##OPTIMIZE##
+ template = self.parser.getSetting('template')
+ ret = ""
+
+ if template == 'highest_compression':
+ top_line_end = ""
+ iner_line_end = ""
+ bottom_line_end = ""
+ indent = ""
+
+ elif template == 'high_compression':
+ top_line_end = "\n"
+ iner_line_end = ""
+ bottom_line_end = "\n"
+ indent = ""
+
+ elif template == 'default':
+ top_line_end = "\n"
+ iner_line_end = "\n"
+ bottom_line_end = "\n\n"
+ indent = ""
+
+ elif template == 'low_compression':
+ top_line_end = "\n"
+ iner_line_end = "\n"
+ bottom_line_end = "\n\n"
+ indent = " "
+
+ if self.parser.getSetting('timestamp'):
+ ret += '/# CSSTidy ' + self.parser.version + ': ' + datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000") + ' #/' + top_line_end
+
+ for item in self.parser._import:
+ ret += '@import(' + item + ');' + top_line_end
+
+ for item in self.parser._charset:
+ ret += '@charset(' + item + ');' + top_line_end
+
+ for item in self.parser._namespace:
+ ret += '@namespace(' + item + ');' + top_line_end
+
+ for media, css in self._css.iteritems():
+ for selector, cssdata in css.iteritems():
+ ret += selector + '{' + top_line_end
+
+ for item, value in cssdata.iteritems():
+ ret += indent + item + ':' + value + ';' + iner_line_end
+
+ ret += '}' + bottom_line_end
+
+ return ret
+
+ def __renderFile(self, filename=None, *args, **kwargs):
+ if filename is None:
+ return self.__renderString()
+
+ try:
+ f = open(filename, "w")
+ f.write(self.__renderString())
+ finally:
+ f.close()
\ No newline at end of file
--- /dev/null
+
+class SortedDict(dict):
+ """
+ A dictionary that keeps its keys in the order in which they're inserted.
+ """
+ def __init__(self, data=None):
+ if data is None:
+ data = {}
+ super(SortedDict, self).__init__(data)
+ if isinstance(data, dict):
+ self.keyOrder = data.keys()
+ else:
+ self.keyOrder = []
+ for key, value in data:
+ if key not in self.keyOrder:
+ self.keyOrder.append(key)
+
+ def __deepcopy__(self, memo):
+ from copy import deepcopy
+ return self.__class__([(key, deepcopy(value, memo))
+ for key, value in self.iteritems()])
+
+ def __setitem__(self, key, value):
+ super(SortedDict, self).__setitem__(key, value)
+ if key not in self.keyOrder:
+ self.keyOrder.append(key)
+
+ def __delitem__(self, key):
+ super(SortedDict, self).__delitem__(key)
+ self.keyOrder.remove(key)
+
+ def __iter__(self):
+ for k in self.keyOrder:
+ yield k
+
+ def pop(self, k, *args):
+ result = super(SortedDict, self).pop(k, *args)
+ try:
+ self.keyOrder.remove(k)
+ except ValueError:
+ # Key wasn't in the dictionary in the first place. No problem.
+ pass
+ return result
+
+ def popitem(self):
+ result = super(SortedDict, self).popitem()
+ self.keyOrder.remove(result[0])
+ return result
+
+ def items(self):
+ return zip(self.keyOrder, self.values())
+
+ def iteritems(self):
+ for key in self.keyOrder:
+ yield key, super(SortedDict, self).__getitem__(key)
+
+ def keys(self):
+ return self.keyOrder[:]
+
+ def iterkeys(self):
+ return iter(self.keyOrder)
+
+ def values(self):
+ return [super(SortedDict, self).__getitem__(k) for k in self.keyOrder]
+
+ def itervalues(self):
+ for key in self.keyOrder:
+ yield super(SortedDict, self).__getitem__(key)
+
+ def update(self, dict_):
+ for k, v in dict_.items():
+ self.__setitem__(k, v)
+
+ def setdefault(self, key, default):
+ if key not in self.keyOrder:
+ self.keyOrder.append(key)
+ return super(SortedDict, self).setdefault(key, default)
+
+ def value_for_index(self, index):
+ """Returns the value of the item at the given zero-based index."""
+ return self[self.keyOrder[index]]
+
+ def insert(self, index, key, value):
+ """Inserts the key, value pair before the item with the given index."""
+ if key in self.keyOrder:
+ n = self.keyOrder.index(key)
+ del self.keyOrder[n]
+ if n < index:
+ index -= 1
+ self.keyOrder.insert(index, key)
+ super(SortedDict, self).__setitem__(key, value)
+
+ def copy(self):
+ """Returns a copy of this object."""
+ # This way of initializing the copy means it works for subclasses, too.
+ obj = self.__class__(self)
+ obj.keyOrder = self.keyOrder[:]
+ return obj
+
+ def __repr__(self):
+ """
+ Replaces the normal dict.__repr__ with a version that returns the keys
+ in their sorted order.
+ """
+ return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
+
+ def clear(self):
+ super(SortedDict, self).clear()
+ self.keyOrder = []
\ No newline at end of file
--- /dev/null
+from compress.filters.jsmin.jsmin import jsmin
+from compress.filter_base import FilterBase
+
+class JSMinFilter(FilterBase):
+ def filter_js(self, js):
+ return jsmin(js)
\ No newline at end of file
--- /dev/null
+#!/usr/bin/python
+
+# This code is original from jsmin by Douglas Crockford, it was translated to
+# Python by Baruch Even. The original code had the following copyright and
+# license.
+#
+# /* jsmin.c
+# 2007-05-22
+#
+# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy of
+# this software and associated documentation files (the "Software"), to deal in
+# the Software without restriction, including without limitation the rights to
+# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+# of the Software, and to permit persons to whom the Software is furnished to do
+# so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# The Software shall be used for Good, not Evil.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+# */
+
+from StringIO import StringIO
+
+def jsmin(js):
+ ins = StringIO(js)
+ outs = StringIO()
+ JavascriptMinify().minify(ins, outs)
+ str = outs.getvalue()
+ if len(str) > 0 and str[0] == '\n':
+ str = str[1:]
+ return str
+
+def isAlphanum(c):
+ """return true if the character is a letter, digit, underscore,
+ dollar sign, or non-ASCII character.
+ """
+ return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
+ (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
+
+class UnterminatedComment(Exception):
+ pass
+
+class UnterminatedStringLiteral(Exception):
+ pass
+
+class UnterminatedRegularExpression(Exception):
+ pass
+
+class JavascriptMinify(object):
+
+ def _outA(self):
+ self.outstream.write(self.theA)
+ def _outB(self):
+ self.outstream.write(self.theB)
+
+ def _get(self):
+ """return the next character from stdin. Watch out for lookahead. If
+ the character is a control character, translate it to a space or
+ linefeed.
+ """
+ c = self.theLookahead
+ self.theLookahead = None
+ if c == None:
+ c = self.instream.read(1)
+ if c >= ' ' or c == '\n':
+ return c
+ if c == '': # EOF
+ return '\000'
+ if c == '\r':
+ return '\n'
+ return ' '
+
+ def _peek(self):
+ self.theLookahead = self._get()
+ return self.theLookahead
+
+ def _next(self):
+ """get the next character, excluding comments. peek() is used to see
+ if a '/' is followed by a '/' or '*'.
+ """
+ c = self._get()
+ if c == '/':
+ p = self._peek()
+ if p == '/':
+ c = self._get()
+ while c > '\n':
+ c = self._get()
+ return c
+ if p == '*':
+ c = self._get()
+ while 1:
+ c = self._get()
+ if c == '*':
+ if self._peek() == '/':
+ self._get()
+ return ' '
+ if c == '\000':
+ raise UnterminatedComment()
+
+ return c
+
+ def _action(self, action):
+ """do something! What you do is determined by the argument:
+ 1 Output A. Copy B to A. Get the next B.
+ 2 Copy B to A. Get the next B. (Delete A).
+ 3 Get the next B. (Delete B).
+ action treats a string as a single character. Wow!
+ action recognizes a regular expression if it is preceded by ( or , or =.
+ """
+ if action <= 1:
+ self._outA()
+
+ if action <= 2:
+ self.theA = self.theB
+ if self.theA == "'" or self.theA == '"':
+ while 1:
+ self._outA()
+ self.theA = self._get()
+ if self.theA == self.theB:
+ break
+ if self.theA <= '\n':
+ raise UnterminatedStringLiteral()
+ if self.theA == '\\':
+ self._outA()
+ self.theA = self._get()
+
+
+ if action <= 3:
+ self.theB = self._next()
+ if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
+ self.theA == '=' or self.theA == ':' or
+ self.theA == '[' or self.theA == '?' or
+ self.theA == '!' or self.theA == '&' or
+ self.theA == '|' or self.theA == ';' or
+ self.theA == '{' or self.theA == '}' or
+ self.theA == '\n'):
+ self._outA()
+ self._outB()
+ while 1:
+ self.theA = self._get()
+ if self.theA == '/':
+ break
+ elif self.theA == '\\':
+ self._outA()
+ self.theA = self._get()
+ elif self.theA <= '\n':
+ raise UnterminatedRegularExpression()
+ self._outA()
+ self.theB = self._next()
+
+
+ def _jsmin(self):
+ """Copy the input to the output, deleting the characters which are
+ insignificant to JavaScript. Comments will be removed. Tabs will be
+ replaced with spaces. Carriage returns will be replaced with linefeeds.
+ Most spaces and linefeeds will be removed.
+ """
+ self.theA = '\n'
+ self._action(3)
+
+ while self.theA != '\000':
+ if self.theA == ' ':
+ if isAlphanum(self.theB):
+ self._action(1)
+ else:
+ self._action(2)
+ elif self.theA == '\n':
+ if self.theB in ['{', '[', '(', '+', '-']:
+ self._action(1)
+ elif self.theB == ' ':
+ self._action(3)
+ else:
+ if isAlphanum(self.theB):
+ self._action(1)
+ else:
+ self._action(2)
+ else:
+ if self.theB == ' ':
+ if isAlphanum(self.theA):
+ self._action(1)
+ else:
+ self._action(3)
+ elif self.theB == '\n':
+ if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
+ self._action(1)
+ else:
+ if isAlphanum(self.theA):
+ self._action(1)
+ else:
+ self._action(3)
+ else:
+ self._action(1)
+
+ def minify(self, instream, outstream):
+ self.instream = instream
+ self.outstream = outstream
+ self.theA = '\n'
+ self.theB = None
+ self.theLookahead = None
+
+ self._jsmin()
+ self.instream.close()
+
+if __name__ == '__main__':
+ import sys
+ jsm = JavascriptMinify()
+ jsm.minify(sys.stdin, sys.stdout)
\ No newline at end of file
--- /dev/null
+import subprocess
+
+from django.conf import settings
+
+from compress.filter_base import FilterBase, FilterError
+
+BINARY = getattr(settings, 'COMPRESS_YUI_BINARY', 'java -jar yuicompressor.jar')
+CSS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_CSS_ARGUMENTS', '')
+JS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_JS_ARGUMENTS', '')
+
+class YUICompressorFilter(FilterBase):
+
+ def filter_common(self, content, type_, arguments):
+ command = '%s --type=%s %s' % (BINARY, type_, arguments)
+
+ if self.verbose:
+ command += ' --verbose'
+
+ p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
+ p.stdin.write(content)
+ p.stdin.close()
+
+ filtered_css = p.stdout.read()
+ p.stdout.close()
+
+ err = p.stderr.read()
+ p.stderr.close()
+
+ if p.wait() != 0:
+ if not err:
+ err = 'Unable to apply YUI Compressor filter'
+
+ raise FilterError(err)
+
+ if self.verbose:
+ print err
+
+ return filtered_css
+
+ def filter_js(self, js):
+ return self.filter_common(js, 'js', JS_ARGUMENTS)
+
+ def filter_css(self, css):
+ return self.filter_common(css, 'css', CSS_ARGUMENTS)
\ No newline at end of file
--- /dev/null
+from django.core.management.base import NoArgsCommand
+from optparse import make_option
+
+from django.conf import settings
+
+class Command(NoArgsCommand):
+ option_list = NoArgsCommand.option_list + (
+ make_option('--force', action='store_true', default=False, help='Force update of all files, even if the source files are older than the current compressed file.'),
+ make_option('--verbosity', action='store', dest='verbosity', default='1',
+ type='choice', choices=['0', '1', '2'],
+ help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
+ )
+ help = 'Updates and compresses CSS and JavsScript on-demand, without restarting Django'
+ args = ''
+
+ def handle_noargs(self, **options):
+
+ force = options.get('force', False)
+ verbosity = int(options.get('verbosity', 1))
+
+ from compress.utils import needs_update, filter_css, filter_js
+
+ for name, css in settings.COMPRESS_CSS.items():
+ u, version = needs_update(css['output_filename'], css['source_filenames'])
+
+ if (force or u) or verbosity >= 2:
+ msg = 'CSS Group \'%s\'' % name
+ print msg
+ print len(msg) * '-'
+ print "Version: %s" % version
+
+ if force or u:
+ filter_css(css, verbosity)
+
+ if (force or u) or verbosity >= 2:
+ print
+
+ for name, js in settings.COMPRESS_JS.items():
+ u, version = needs_update(js['output_filename'], js['source_filenames'])
+
+ if (force or u) or verbosity >= 2:
+ msg = 'JavaScript Group \'%s\'' % name
+ print msg
+ print len(msg) * '-'
+ print "Version: %s" % version
+
+ if force or u:
+ filter_js(js, verbosity)
+
+ if (force or u) or verbosity >= 2:
+ print
\ No newline at end of file
--- /dev/null
+from django.dispatch import Signal
+
+css_filtered = Signal()
+js_filtered = Signal()
--- /dev/null
+<link href="{{ url }}" rel="stylesheet" type="text/css" media="{{ media|default:"all" }}" />
\ No newline at end of file
--- /dev/null
+<!--[if {{ condition|default:"IE" }}]><link href="{{ url }}" rel="stylesheet" type="text/css" media="{{ media|default:"all" }}" /><![endif]-->
\ No newline at end of file
--- /dev/null
+<script type="text/javascript" src="{{ url }}"></script>
\ No newline at end of file
--- /dev/null
+<!--[if {{ condition|default:"IE" }}]><script type="text/javascript" src="{{ url }}"></script><![endif]-->
\ No newline at end of file
--- /dev/null
+import os
+
+from django import template
+
+from django.conf import settings as django_settings
+
+from compress.conf import settings
+from compress.utils import media_root, media_url, needs_update, filter_css, filter_js, get_output_filename, get_version
+
+register = template.Library()
+
+def render_common(template_name, obj, filename, version):
+ if settings.COMPRESS:
+ filename = get_output_filename(filename, version)
+
+ context = obj.get('extra_context', {})
+ context['url'] = media_url(filename)
+
+ return template.loader.render_to_string(template_name, context)
+
+def render_css(css, filename, version=None):
+ return render_common(css.get('template_name', 'compress/css.html'), css, filename, version)
+
+def render_js(js, filename, version=None):
+ return render_common(js.get('template_name', 'compress/js.html'), js, filename, version)
+
+class CompressedCSSNode(template.Node):
+ def __init__(self, name):
+ self.name = name
+
+ def render(self, context):
+ css_name = template.Variable(self.name).resolve(context)
+
+ try:
+ css = settings.COMPRESS_CSS[css_name]
+ except KeyError:
+ return '' # fail silently, do not return anything if an invalid group is specified
+
+ if settings.COMPRESS:
+
+ version = None
+
+ if settings.COMPRESS_AUTO:
+ u, version = needs_update(css['output_filename'], css['source_filenames'])
+ if u:
+ filter_css(css)
+
+ return render_css(css, css['output_filename'], version)
+ else:
+ # output source files
+ r = ''
+ for source_file in css['source_filenames']:
+ r += render_css(css, source_file)
+
+ return r
+
+class CompressedJSNode(template.Node):
+ def __init__(self, name):
+ self.name = name
+
+ def render(self, context):
+ js_name = template.Variable(self.name).resolve(context)
+
+ try:
+ js = settings.COMPRESS_JS[js_name]
+ except KeyError:
+ return '' # fail silently, do not return anything if an invalid group is specified
+
+ if settings.COMPRESS:
+
+ version = None
+
+ if settings.COMPRESS_AUTO:
+ u, version = needs_update(js['output_filename'], js['source_filenames'])
+ if u:
+ filter_js(js)
+
+ return render_js(js, js['output_filename'], version)
+ else:
+ # output source files
+ r = ''
+ for source_file in js['source_filenames']:
+ r += render_js(js, source_file)
+ return r
+
+#@register.tag
+def compressed_css(parser, token):
+ try:
+ tag_name, name = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError, '%r requires exactly one argument: the name of a group in the COMPRESS_CSS setting' % token.split_contents()[0]
+
+ return CompressedCSSNode(name)
+compressed_css = register.tag(compressed_css)
+
+#@register.tag
+def compressed_js(parser, token):
+ try:
+ tag_name, name = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError, '%r requires exactly one argument: the name of a group in the COMPRESS_JS setting' % token.split_contents()[0]
+
+ return CompressedJSNode(name)
+compressed_js = register.tag(compressed_js)
--- /dev/null
+import os
+import re
+import tempfile
+
+from django.conf import settings as django_settings
+from django.utils.http import urlquote
+from django.dispatch import dispatcher
+
+from compress.conf import settings
+from compress.signals import css_filtered, js_filtered
+
+def get_filter(compressor_class):
+ """
+ Convert a string version of a function name to the callable object.
+ """
+
+ if not hasattr(compressor_class, '__bases__'):
+
+ try:
+ compressor_class = compressor_class.encode('ascii')
+ mod_name, class_name = get_mod_func(compressor_class)
+ if class_name != '':
+ compressor_class = getattr(__import__(mod_name, {}, {}, ['']), class_name)
+ except (ImportError, AttributeError):
+ raise Exception('Failed to import filter %s' % compressor_class)
+
+ return compressor_class
+
+def get_mod_func(callback):
+ """
+ Converts 'django.views.news.stories.story_detail' to
+ ('django.views.news.stories', 'story_detail')
+ """
+
+ try:
+ dot = callback.rindex('.')
+ except ValueError:
+ return callback, ''
+ return callback[:dot], callback[dot+1:]
+
+def needs_update(output_file, source_files):
+ """
+ Scan the source files for changes and returns True if the output_file needs to be updated.
+ """
+
+ mtime = max_mtime(source_files)
+ version = get_version(mtime)
+
+ compressed_file_full = media_root(get_output_filename(output_file, version))
+
+ if not os.path.exists(compressed_file_full):
+ return True, version
+
+ # Check if the output file is outdated
+ return (os.stat(compressed_file_full).st_mtime < mtime), mtime
+
+def media_root(filename):
+ """
+ Return the full path to ``filename``. ``filename`` is a relative path name in MEDIA_ROOT
+ """
+ return os.path.join(django_settings.MEDIA_ROOT, filename)
+
+def media_url(url):
+ return django_settings.MEDIA_URL + urlquote(url)
+
+def concat(filenames, separator=''):
+ """
+ Concatenate the files from the list of the ``filenames``, ouput separated with ``separator``.
+ """
+ r = ''
+
+ for filename in filenames:
+ fd = open(media_root(filename), 'rb')
+ r += fd.read()
+ r += separator
+ fd.close()
+
+ return r
+
+def max_mtime(files):
+ return int(max([os.stat(media_root(f)).st_mtime for f in files]))
+
+def save_file(filename, contents):
+ fd = open(media_root(filename), 'wb+')
+ fd.write(contents)
+ fd.close()
+
+def get_output_filename(filename, version):
+ if settings.COMPRESS_VERSION and version is not None:
+ return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, get_version(version))
+ else:
+ return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, settings.COMPRESS_VERSION_DEFAULT)
+
+def get_version(version):
+ try:
+ return str(int(version))
+ except ValueError:
+ return str(version)
+
+def remove_files(path, filename, verbosity=0):
+ regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'\d+'))))
+
+ for f in os.listdir(path):
+ if regex.match(f):
+ if verbosity >= 1:
+ print "Removing outdated file %s" % f
+
+ os.unlink(os.path.join(path, f))
+
+def filter_common(obj, verbosity, filters, attr, separator, signal):
+ output = concat(obj['source_filenames'], separator)
+ filename = get_output_filename(obj['output_filename'], get_version(max_mtime(obj['source_filenames'])))
+
+ if settings.COMPRESS_VERSION:
+ remove_files(os.path.dirname(media_root(filename)), obj['output_filename'], verbosity)
+
+ if verbosity >= 1:
+ print "Saving %s" % filename
+
+ for f in filters:
+ output = getattr(get_filter(f)(verbose=(verbosity >= 2)), attr)(output)
+
+ save_file(filename, output)
+ signal.send(None)
+
+def filter_css(css, verbosity=0):
+ return filter_common(css, verbosity, filters=settings.COMPRESS_CSS_FILTERS, attr='filter_css', separator='', signal=css_filtered)
+
+def filter_js(js, verbosity=0):
+ return filter_common(js, verbosity, filters=settings.COMPRESS_JS_FILTERS, attr='filter_js', separator=';', signal=js_filtered)
--- /dev/null
+# -*- coding: utf-8 -*-
+from django.contrib import admin
+from django import forms
+from django.utils.safestring import mark_safe
+from django.utils.translation import ugettext_lazy as _
+
+
+class FilteredSelectMultiple(forms.SelectMultiple):
+ """
+ A SelectMultiple with a JavaScript filter interface.
+
+ Note that the resulting JavaScript assumes that the SelectFilter2.js
+ library and its dependencies have been loaded in the HTML page.
+ """
+ def _media(self):
+ from django.conf import settings
+ js = ['js/SelectBox.js' , 'js/SelectFilter2.js']
+ return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
+ media = property(_media)
+
+ def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
+ self.verbose_name = verbose_name
+ self.is_stacked = is_stacked
+ super(FilteredSelectMultiple, self).__init__(attrs, choices)
+
+ def render(self, name, value, attrs=None, choices=()):
+ from django.conf import settings
+ output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
+ output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
+ # TODO: "id_" is hard-coded here. This should instead use the correct
+ # API to determine the ID dynamically.
+ output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % \
+ (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX))
+ return mark_safe(u''.join(output))
+
+
+class TaggableModelForm(forms.ModelForm):
+ tags = forms.MultipleChoiceField(label=_('tags').capitalize(), required=True, widget=FilteredSelectMultiple(_('tags'), False))
+
+ def __init__(self, *args, **kwargs):
+ if 'instance' in kwargs:
+ if 'initial' not in kwargs:
+ kwargs['initial'] = {}
+ kwargs['initial']['tags'] = [tag.id for tag in self.tag_model.objects.get_for_object(kwargs['instance'])]
+ super(TaggableModelForm, self).__init__(*args, **kwargs)
+ self.fields['tags'].choices = [(tag.id, tag.name) for tag in self.tag_model.objects.all()]
+
+ def save(self, commit):
+ obj = super(TaggableModelForm, self).save()
+ tag_ids = self.cleaned_data['tags']
+ tags = self.tag_model.objects.filter(pk__in=tag_ids)
+ self.tag_model.objects.update_tags(obj, tags)
+ return obj
+
+ def save_m2m(self):
+ # TODO: Shouldn't be needed
+ pass
+
+
+class TaggableModelAdmin(admin.ModelAdmin):
+ form = TaggableModelForm
+
+ def get_form(self, request, obj=None):
+ form = super(TaggableModelAdmin, self).get_form(request, obj)
+ form.tag_model = self.tag_model
+ return form
+
--- /dev/null
+"""
+Custom managers for Django models registered with the tagging
+application.
+"""
+from django.contrib.contenttypes.models import ContentType
+from django.db import models
+
+
+class ModelTagManager(models.Manager):
+ """
+ A manager for retrieving tags for a particular model.
+ """
+ def __init__(self, tag_model):
+ super(ModelTagManager, self).__init__()
+ self.tag_model = tag_model
+
+ def get_query_set(self):
+ content_type = ContentType.objects.get_for_model(self.model)
+ return self.tag_model.objects.filter(
+ items__content_type__pk=content_type.pk).distinct()
+
+ def related(self, tags, *args, **kwargs):
+ return self.tag_model.objects.related_for_model(tags, self.model, *args, **kwargs)
+
+ def usage(self, *args, **kwargs):
+ return self.tag_model.objects.usage_for_model(self.model, *args, **kwargs)
+
+
+class ModelTaggedItemManager(models.Manager):
+ """
+ A manager for retrieving model instances based on their tags.
+ """
+ def __init__(self, tag_model):
+ super(ModelTaggedItemManager, self).__init__()
+ self.intermediary_table_model = tag_model.objects.intermediary_table_model
+
+ def related_to(self, obj, queryset=None, num=None):
+ if queryset is None:
+ return self.intermediary_table_model.objects.get_related(obj, self.model, num=num)
+ else:
+ return self.intermediary_table_model.objects.get_related(obj, queryset, num=num)
+
+ def with_all(self, tags, queryset=None):
+ if queryset is None:
+ return self.intermediary_table_model.objects.get_by_model(self.model, tags)
+ else:
+ return self.intermediary_table_model.objects.get_by_model(queryset, tags)
+
+ def with_any(self, tags, queryset=None):
+ if queryset is None:
+ return self.intermediary_table_model.objects.get_union_by_model(self.model, tags)
+ else:
+ return self.intermediary_table_model.objects.get_union_by_model(queryset, tags)
+
+
+class TagDescriptor(object):
+ """
+ A descriptor which provides access to a ``ModelTagManager`` for
+ model classes and simple retrieval, updating and deletion of tags
+ for model instances.
+ """
+ def __init__(self, tag_model):
+ self.tag_model = tag_model
+
+ def __get__(self, instance, owner):
+ if not instance:
+ tag_manager = ModelTagManager(self.tag_model)
+ tag_manager.model = owner
+ return tag_manager
+ else:
+ return self.tag_model.objects.get_for_object(instance)
+
+ def __set__(self, instance, value):
+ self.tag_model.objects.update_tags(instance, value)
+
+ def __del__(self, instance):
+ self.tag_model.objects.update_tags(instance, [])
+
--- /dev/null
+"""
+Models and managers for generic tagging.
+"""
+# Python 2.3 compatibility
+if not hasattr(__builtins__, 'set'):
+ from sets import Set as set
+
+from django.contrib.contenttypes import generic
+from django.contrib.contenttypes.models import ContentType
+from django.db import connection, models
+from django.utils.translation import ugettext_lazy as _
+from django.db.models.base import ModelBase
+
+qn = connection.ops.quote_name
+
+try:
+ from django.db.models.query import parse_lookup
+except ImportError:
+ parse_lookup = None
+
+
+def get_queryset_and_model(queryset_or_model):
+ """
+ Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
+ (queryset, model).
+
+ If a ``Model`` is given, the ``QuerySet`` returned will be created
+ using its default manager.
+ """
+ try:
+ return queryset_or_model, queryset_or_model.model
+ except AttributeError:
+ return queryset_or_model._default_manager.all(), queryset_or_model
+
+
+############
+# Managers #
+############
+class TagManager(models.Manager):
+ def __init__(self, intermediary_table_model):
+ super(TagManager, self).__init__()
+ self.intermediary_table_model = intermediary_table_model
+
+ def update_tags(self, obj, tags):
+ """
+ Update tags associated with an object.
+ """
+ content_type = ContentType.objects.get_for_model(obj)
+ current_tags = list(self.filter(items__content_type__pk=content_type.pk,
+ items__object_id=obj.pk))
+ updated_tags = self.model.get_tag_list(tags)
+
+ # Remove tags which no longer apply
+ tags_for_removal = [tag for tag in current_tags \
+ if tag not in updated_tags]
+ if len(tags_for_removal):
+ self.intermediary_table_model._default_manager.filter(content_type__pk=content_type.pk,
+ object_id=obj.pk,
+ tag__in=tags_for_removal).delete()
+ # Add new tags
+ for tag in updated_tags:
+ if tag not in current_tags:
+ self.intermediary_table_model._default_manager.create(tag=tag, content_object=obj)
+
+ def get_for_object(self, obj):
+ """
+ Create a queryset matching all tags associated with the given
+ object.
+ """
+ ctype = ContentType.objects.get_for_model(obj)
+ return self.filter(items__content_type__pk=ctype.pk,
+ items__object_id=obj.pk)
+
+ def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None, extra=None):
+ """
+ Perform the custom SQL query for ``usage_for_model`` and
+ ``usage_for_queryset``.
+ """
+ if min_count is not None: counts = True
+
+ model_table = qn(model._meta.db_table)
+ model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
+ tag_columns = self._get_tag_columns()
+
+ if extra is None: extra = {}
+ extra_where = ''
+ if 'where' in extra:
+ extra_where = 'AND ' + ' AND '.join(extra['where'])
+
+ query = """
+ SELECT DISTINCT %(tag_columns)s%(count_sql)s
+ FROM
+ %(tag)s
+ INNER JOIN %(tagged_item)s
+ ON %(tag)s.id = %(tagged_item)s.tag_id
+ INNER JOIN %(model)s
+ ON %(tagged_item)s.object_id = %(model_pk)s
+ %%s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ %%s
+ %(extra_where)s
+ GROUP BY %(tag)s.id, %(tag)s.name
+ %%s
+ ORDER BY %(tag)s.%(ordering)s ASC""" % {
+ 'tag': qn(self.model._meta.db_table),
+ 'ordering': ', '.join(qn(field) for field in self.model._meta.ordering),
+ 'tag_columns': tag_columns,
+ 'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
+ 'tagged_item': qn(self.intermediary_table_model._meta.db_table),
+ 'model': model_table,
+ 'model_pk': model_pk,
+ 'extra_where': extra_where,
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ }
+
+ min_count_sql = ''
+ if min_count is not None:
+ min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
+ params.append(min_count)
+
+ cursor = connection.cursor()
+ cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
+ tags = []
+ for row in cursor.fetchall():
+ t = self.model(*row[:len(self.model._meta.fields)])
+ if counts:
+ t.count = row[len(self.model._meta.fields)]
+ tags.append(t)
+ return tags
+
+ def usage_for_model(self, model, counts=False, min_count=None, filters=None, extra=None):
+ """
+ Obtain a list of tags associated with instances of the given
+ Model class.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating how many times it has been used against
+ the Model class in question.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+
+ To limit the tags (and counts, if specified) returned to those
+ used by a subset of the Model's instances, pass a dictionary
+ of field lookups to be applied to the given Model as the
+ ``filters`` argument.
+ """
+ if extra is None: extra = {}
+ if filters is None: filters = {}
+
+ if not parse_lookup:
+ # post-queryset-refactor (hand off to usage_for_queryset)
+ queryset = model._default_manager.filter()
+ for f in filters.items():
+ queryset.query.add_filter(f)
+ usage = self.usage_for_queryset(queryset, counts, min_count, extra)
+ else:
+ # pre-queryset-refactor
+ extra_joins = ''
+ extra_criteria = ''
+ params = []
+ if len(filters) > 0:
+ joins, where, params = parse_lookup(filters.items(), model._meta)
+ extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
+ for (alias, (table, join_type, condition)) in joins.items()])
+ extra_criteria = 'AND %s' % (' AND '.join(where))
+ usage = self._get_usage(model, counts, min_count, extra_joins, extra_criteria, params, extra)
+
+ return usage
+
+ def usage_for_queryset(self, queryset, counts=False, min_count=None, extra=None):
+ """
+ Obtain a list of tags associated with instances of a model
+ contained in the given queryset.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating how many times it has been used against
+ the Model class in question.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+ """
+ if parse_lookup:
+ raise AttributeError("'TagManager.usage_for_queryset' is not compatible with pre-queryset-refactor versions of Django.")
+
+ extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:])
+ where, params = queryset.query.where.as_sql()
+ if where:
+ extra_criteria = 'AND %s' % where
+ else:
+ extra_criteria = ''
+ return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params, extra)
+
+ def related_for_model(self, tags, model, counts=False, min_count=None, extra=None):
+ """
+ Obtain a list of tags related to a given list of tags - that
+ is, other tags used by items which have all the given tags.
+
+ If ``counts`` is True, a ``count`` attribute will be added to
+ each tag, indicating the number of items which have it in
+ addition to the given list of tags.
+
+ If ``min_count`` is given, only tags which have a ``count``
+ greater than or equal to ``min_count`` will be returned.
+ Passing a value for ``min_count`` implies ``counts=True``.
+ """
+ if min_count is not None: counts = True
+ tags = self.model.get_tag_list(tags)
+ tag_count = len(tags)
+ tagged_item_table = qn(self.intermediary_table_model._meta.db_table)
+ tag_columns = self._get_tag_columns()
+
+ if extra is None: extra = {}
+ extra_where = ''
+ if 'where' in extra:
+ extra_where = 'AND ' + ' AND '.join(extra['where'])
+
+ query = """
+ SELECT %(tag_columns)s%(count_sql)s
+ FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.object_id IN
+ (
+ SELECT %(tagged_item)s.object_id
+ FROM %(tagged_item)s, %(tag)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tag)s.id = %(tagged_item)s.tag_id
+ AND %(tag)s.id IN (%(tag_id_placeholders)s)
+ GROUP BY %(tagged_item)s.object_id
+ HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
+ )
+ AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
+ %(extra_where)s
+ GROUP BY %(tag)s.id, %(tag)s.name
+ %(min_count_sql)s
+ ORDER BY %(tag)s.%(ordering)s ASC""" % {
+ 'tag': qn(self.model._meta.db_table),
+ 'ordering': ', '.join(qn(field) for field in self.model._meta.ordering),
+ 'tag_columns': tag_columns,
+ 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
+ 'tagged_item': tagged_item_table,
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ 'extra_where': extra_where,
+ 'tag_count': tag_count,
+ 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
+ }
+
+ params = [tag.pk for tag in tags] * 2
+ if min_count is not None:
+ params.append(min_count)
+
+ cursor = connection.cursor()
+ cursor.execute(query, params)
+ related = []
+ for row in cursor.fetchall():
+ tag = self.model(*row[:len(self.model._meta.fields)])
+ if counts is True:
+ tag.count = row[len(self.model._meta.fields)]
+ related.append(tag)
+ return related
+
+ def _get_tag_columns(self):
+ tag_table = qn(self.model._meta.db_table)
+ return ', '.join('%s.%s' % (tag_table, qn(field.column)) for field in self.model._meta.fields)
+
+
+class TaggedItemManager(models.Manager):
+ """
+ FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING``
+ SQL clauses required by many of this manager's methods into
+ Django's ORM.
+
+ For now, we manually execute a query to retrieve the PKs of
+ objects we're interested in, then use the ORM's ``__in``
+ lookup to return a ``QuerySet``.
+
+ Once the queryset-refactor branch lands in trunk, this can be
+ tidied up significantly.
+ """
+ def __init__(self, tag_model):
+ super(TaggedItemManager, self).__init__()
+ self.tag_model = tag_model
+
+ def get_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with a given tag or list of tags.
+ """
+ tags = self.tag_model.get_tag_list(tags)
+ tag_count = len(tags)
+ if tag_count == 0:
+ # No existing tags were given
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ return model._default_manager.none()
+ elif tag_count == 1:
+ # Optimisation for single tag - fall through to the simpler
+ # query below.
+ tag = tags[0]
+ else:
+ return self.get_intersection_by_model(queryset_or_model, tags)
+
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ content_type = ContentType.objects.get_for_model(model)
+ opts = self.model._meta
+ tagged_item_table = qn(opts.db_table)
+ return queryset.extra(
+ tables=[opts.db_table],
+ where=[
+ '%s.content_type_id = %%s' % tagged_item_table,
+ '%s.tag_id = %%s' % tagged_item_table,
+ '%s.%s = %s.object_id' % (qn(model._meta.db_table),
+ qn(model._meta.pk.column),
+ tagged_item_table)
+ ],
+ params=[content_type.pk, tag.pk],
+ )
+
+ def get_intersection_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with *all* of the given list of tags.
+ """
+ tags = self.tag_model.get_tag_list(tags)
+ tag_count = len(tags)
+ queryset, model = get_queryset_and_model(queryset_or_model)
+
+ if not tag_count:
+ return model._default_manager.none()
+
+ model_table = qn(model._meta.db_table)
+ # This query selects the ids of all objects which have all the
+ # given tags.
+ query = """
+ SELECT %(model_pk)s
+ FROM %(model)s, %(tagged_item)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
+ AND %(model_pk)s = %(tagged_item)s.object_id
+ GROUP BY %(model_pk)s
+ HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ 'tag_count': tag_count,
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [tag.pk for tag in tags])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ return queryset.filter(pk__in=object_ids)
+ else:
+ return model._default_manager.none()
+
+ def get_union_by_model(self, queryset_or_model, tags):
+ """
+ Create a ``QuerySet`` containing instances of the specified
+ model associated with *any* of the given list of tags.
+ """
+ tags = self.tag_model.get_tag_list(tags)
+ tag_count = len(tags)
+ queryset, model = get_queryset_and_model(queryset_or_model)
+
+ if not tag_count:
+ return model._default_manager.none()
+
+ model_table = qn(model._meta.db_table)
+ # This query selects the ids of all objects which have any of
+ # the given tags.
+ query = """
+ SELECT %(model_pk)s
+ FROM %(model)s, %(tagged_item)s
+ WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
+ AND %(model_pk)s = %(tagged_item)s.object_id
+ GROUP BY %(model_pk)s""" % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'content_type_id': ContentType.objects.get_for_model(model).pk,
+ 'tag_id_placeholders': ','.join(['%s'] * tag_count),
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [tag.pk for tag in tags])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ return queryset.filter(pk__in=object_ids)
+ else:
+ return model._default_manager.none()
+
+ def get_related(self, obj, queryset_or_model, num=None):
+ """
+ Retrieve a list of instances of the specified model which share
+ tags with the model instance ``obj``, ordered by the number of
+ shared tags in descending order.
+
+ If ``num`` is given, a maximum of ``num`` instances will be
+ returned.
+ """
+ queryset, model = get_queryset_and_model(queryset_or_model)
+ model_table = qn(model._meta.db_table)
+ content_type = ContentType.objects.get_for_model(obj)
+ related_content_type = ContentType.objects.get_for_model(model)
+ query = """
+ SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
+ FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
+ WHERE %(tagged_item)s.object_id = %%s
+ AND %(tagged_item)s.content_type_id = %(content_type_id)s
+ AND %(tag)s.id = %(tagged_item)s.tag_id
+ AND related_tagged_item.content_type_id = %(related_content_type_id)s
+ AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
+ AND %(model_pk)s = related_tagged_item.object_id"""
+ if content_type.pk == related_content_type.pk:
+ # Exclude the given instance itself if determining related
+ # instances for the same model.
+ query += """
+ AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
+ query += """
+ GROUP BY %(model_pk)s
+ ORDER BY %(count)s DESC
+ %(limit_offset)s"""
+ query = query % {
+ 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
+ 'count': qn('count'),
+ 'model': model_table,
+ 'tagged_item': qn(self.model._meta.db_table),
+ 'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
+ 'content_type_id': content_type.pk,
+ 'related_content_type_id': related_content_type.pk,
+ 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
+ }
+
+ cursor = connection.cursor()
+ cursor.execute(query, [obj.pk])
+ object_ids = [row[0] for row in cursor.fetchall()]
+ if len(object_ids) > 0:
+ # Use in_bulk here instead of an id__in lookup, because id__in would
+ # clobber the ordering.
+ object_dict = queryset.in_bulk(object_ids)
+ return [object_dict[object_id] for object_id in object_ids \
+ if object_id in object_dict]
+ else:
+ return []
+
+
+##########
+# Models #
+##########
+def create_intermediary_table_model(model):
+ """Create an intermediary table model for the specific tag model"""
+ name = model.__name__ + 'Relation'
+
+ class Meta:
+ db_table = '%s_relation' % model._meta.db_table
+ unique_together = (('tag', 'content_type', 'object_id'),)
+
+ def obj_unicode(self):
+ return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
+
+ # Set up a dictionary to simulate declarations within a class
+ attrs = {
+ '__module__': model.__module__,
+ 'Meta': Meta,
+ 'tag': models.ForeignKey(model, verbose_name=_('tag'), related_name='items'),
+ 'content_type': models.ForeignKey(ContentType, verbose_name=_('content type')),
+ 'object_id': models.PositiveIntegerField(_('object id'), db_index=True),
+ 'content_object': generic.GenericForeignKey('content_type', 'object_id'),
+ '__unicode__': obj_unicode,
+ }
+
+ return type(name, (models.Model,), attrs)
+
+
+class TagMeta(ModelBase):
+ "Metaclass for tag models (models inheriting from TagBase)."
+ def __new__(cls, name, bases, attrs):
+ model = super(TagMeta, cls).__new__(cls, name, bases, attrs)
+ if not model._meta.abstract:
+ # Create an intermediary table and register custom managers for concrete models
+ model.intermediary_table_model = create_intermediary_table_model(model)
+ TagManager(model.intermediary_table_model).contribute_to_class(model, 'objects')
+ TaggedItemManager(model).contribute_to_class(model.intermediary_table_model, 'objects')
+ return model
+
+
+class TagBase(models.Model):
+ """Abstract class to be inherited by model classes."""
+ __metaclass__ = TagMeta
+
+ class Meta:
+ abstract = True
+
+ @staticmethod
+ def get_tag_list(tag_list):
+ """
+ Utility function for accepting tag input in a flexible manner.
+
+ You should probably override this method in your subclass.
+ """
+ if isinstance(tag_list, TagBase):
+ return [tag_list]
+ else:
+ return tag_list
+
--- /dev/null
+"""
+Tagging related views.
+"""
+from django.http import Http404
+from django.utils.translation import ugettext as _
+from django.views.generic.list_detail import object_list
+
+
+def tagged_object_list(request, queryset_or_model=None, tag_model=None, tags=None,
+ related_tags=False, related_tag_counts=True, **kwargs):
+ """
+ A thin wrapper around
+ ``django.views.generic.list_detail.object_list`` which creates a
+ ``QuerySet`` containing instances of the given queryset or model
+ tagged with the given tag.
+
+ In addition to the context variables set up by ``object_list``, a
+ ``tag`` context variable will contain the ``Tag`` instance for the
+ tag.
+
+ If ``related_tags`` is ``True``, a ``related_tags`` context variable
+ will contain tags related to the given tag for the given model.
+ Additionally, if ``related_tag_counts`` is ``True``, each related
+ tag will have a ``count`` attribute indicating the number of items
+ which have it in addition to the given tag.
+ """
+ # Check attributes
+ if queryset_or_model is None:
+ raise AttributeError(_('tagged_object_list must be called with a queryset or a model.'))
+ if tag_model is None:
+ raise AttributeError(_('tagged_object_list must be called with a tag model.'))
+ if tags is None:
+ raise AttributeError(_('tagged_object_list must be called with a tag.'))
+
+ tag_instances = tag_model.get_tag_list(tags)
+ if tag_instances is None:
+ raise Http404(_('No tags found matching "%s".') % tags)
+ queryset = tag_model.intermediary_table_model.objects.get_intersection_by_model(queryset_or_model, tag_instances)
+ if not kwargs.has_key('extra_context'):
+ kwargs['extra_context'] = {}
+ kwargs['extra_context']['tags'] = tag_instances
+ if related_tags:
+ kwargs['extra_context']['related_tags'] = \
+ tag_model.objects.related_for_model(tag_instances, queryset_or_model,
+ counts=related_tag_counts)
+ return object_list(request, queryset, **kwargs)
+
--- /dev/null
+class PaginationMiddleware(object):
+ """
+ Inserts a variable representing the current page onto the request object if
+ it exists in either **GET** or **POST** portions of the request.
+ """
+ def process_request(self, request):
+ try:
+ request.page = int(request.REQUEST['page'])
+ except (KeyError, ValueError):
+ request.page = 1
\ No newline at end of file
--- /dev/null
+{% if is_paginated %}
+<div class="pagination">
+ {% if page_obj.has_previous %}
+ <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}" class="prev">‹‹ previous</a>
+ {% else %}
+ <span class="disabled prev">‹‹ previous</span>
+ {% endif %}
+ {% for page in pages %}
+ {% if page %}
+ {% ifequal page page_obj.number %}
+ <span class="current page">{{ page }}</span>
+ {% else %}
+ <a href="?page={{ page }}{{ getvars }}" class="page">{{ page }}</a>
+ {% endifequal %}
+ {% else %}
+ ...
+ {% endif %}
+ {% endfor %}
+ {% if page_obj.has_next %}
+ <a href="?page={{ page_obj.next_page_number }}{{ getvars }}" class="next">next ››</a>
+ {% else %}
+ <span class="disabled next">next ››</span>
+ {% endif %}
+</div>
+{% endif %}
--- /dev/null
+try:
+ set
+except NameError:
+ from sets import Set as set
+from django import template
+from django.db.models.query import QuerySet
+from django.core.paginator import Paginator, QuerySetPaginator, InvalidPage
+
+register = template.Library()
+
+DEFAULT_PAGINATION = 20
+DEFAULT_WINDOW = 4
+DEFAULT_ORPHANS = 0
+
+def do_autopaginate(parser, token):
+ """
+ Splits the arguments to the autopaginate tag and formats them correctly.
+ """
+ split = token.split_contents()
+ if len(split) == 2:
+ return AutoPaginateNode(split[1])
+ elif len(split) == 3:
+ try:
+ paginate_by = int(split[2])
+ except ValueError:
+ raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
+ return AutoPaginateNode(split[1], paginate_by=paginate_by)
+ elif len(split) == 4:
+ try:
+ paginate_by = int(split[2])
+ except ValueError:
+ raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
+ try:
+ orphans = int(split[3])
+ except ValueError:
+ raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[3])
+ return AutoPaginateNode(split[1], paginate_by=paginate_by, orphans=orphans)
+ else:
+ raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])
+
+class AutoPaginateNode(template.Node):
+ """
+ Emits the required objects to allow for Digg-style pagination.
+
+ First, it looks in the current context for the variable specified. This
+ should be either a QuerySet or a list.
+
+ 1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a
+ ``QuerySetPaginator`` and the current page object into the context names
+ ``paginator`` and ``page_obj``, respectively.
+
+ 2. If it is a list, this ``AutoPaginateNode`` will emit a simple
+ ``Paginator`` and the current page object into the context names
+ ``paginator`` and ``page_obj``, respectively.
+
+ It will then replace the variable specified with only the objects for the
+ current page.
+
+ .. note::
+
+ It is recommended to use *{% paginate %}* after using the autopaginate
+ tag. If you choose not to use *{% paginate %}*, make sure to display the
+ list of availabale pages, or else the application may seem to be buggy.
+ """
+ def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION, orphans=DEFAULT_ORPHANS):
+ self.queryset_var = template.Variable(queryset_var)
+ self.paginate_by = paginate_by
+ self.orphans = orphans
+
+ def render(self, context):
+ key = self.queryset_var.var
+ value = self.queryset_var.resolve(context)
+ if issubclass(value.__class__, QuerySet):
+ model = value.model
+ paginator_class = QuerySetPaginator
+ else:
+ value = list(value)
+ try:
+ model = value[0].__class__
+ except IndexError:
+ return u''
+ paginator_class = Paginator
+ paginator = paginator_class(value, self.paginate_by, self.orphans)
+ try:
+ page_obj = paginator.page(context['request'].page)
+ except InvalidPage:
+ context[key] = []
+ context['invalid_page'] = True
+ return u''
+ context[key] = page_obj.object_list
+ context['paginator'] = paginator
+ context['page_obj'] = page_obj
+ return u''
+
+def paginate(context, window=DEFAULT_WINDOW):
+ """
+ Renders the ``pagination/pagination.html`` template, resulting in a
+ Digg-like display of the available pages, given the current page. If there
+ are too many pages to be displayed before and after the current page, then
+ elipses will be used to indicate the undisplayed gap between page numbers.
+
+ Requires one argument, ``context``, which should be a dictionary-like data
+ structure and must contain the following keys:
+
+ ``paginator``
+ A ``Paginator`` or ``QuerySetPaginator`` object.
+
+ ``page_obj``
+ This should be the result of calling the page method on the
+ aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
+ the current page.
+
+ This same ``context`` dictionary-like data structure may also include:
+
+ ``getvars``
+ A dictionary of all of the **GET** parameters in the current request.
+ This is useful to maintain certain types of state, even when requesting
+ a different page.
+ """
+ try:
+ paginator = context['paginator']
+ page_obj = context['page_obj']
+ page_range = paginator.page_range
+ # First and last are simply the first *n* pages and the last *n* pages,
+ # where *n* is the current window size.
+ first = set(page_range[:window])
+ last = set(page_range[-window:])
+ # Now we look around our current page, making sure that we don't wrap
+ # around.
+ current_start = page_obj.number-1-window
+ if current_start < 0:
+ current_start = 0
+ current_end = page_obj.number-1+window
+ if current_end < 0:
+ current_end = 0
+ current = set(page_range[current_start:current_end])
+ pages = []
+ # If there's no overlap between the first set of pages and the current
+ # set of pages, then there's a possible need for elusion.
+ if len(first.intersection(current)) == 0:
+ first_list = sorted(list(first))
+ second_list = sorted(list(current))
+ pages.extend(first_list)
+ diff = second_list[0] - first_list[-1]
+ # If there is a gap of two, between the last page of the first
+ # set and the first page of the current set, then we're missing a
+ # page.
+ if diff == 2:
+ pages.append(second_list[0] - 1)
+ # If the difference is just one, then there's nothing to be done,
+ # as the pages need no elusion and are correct.
+ elif diff == 1:
+ pass
+ # Otherwise, there's a bigger gap which needs to be signaled for
+ # elusion, by pushing a None value to the page list.
+ else:
+ pages.append(None)
+ pages.extend(second_list)
+ else:
+ pages.extend(sorted(list(first.union(current))))
+ # If there's no overlap between the current set of pages and the last
+ # set of pages, then there's a possible need for elusion.
+ if len(current.intersection(last)) == 0:
+ second_list = sorted(list(last))
+ diff = second_list[0] - pages[-1]
+ # If there is a gap of two, between the last page of the current
+ # set and the first page of the last set, then we're missing a
+ # page.
+ if diff == 2:
+ pages.append(second_list[0] - 1)
+ # If the difference is just one, then there's nothing to be done,
+ # as the pages need no elusion and are correct.
+ elif diff == 1:
+ pass
+ # Otherwise, there's a bigger gap which needs to be signaled for
+ # elusion, by pushing a None value to the page list.
+ else:
+ pages.append(None)
+ pages.extend(second_list)
+ else:
+ pages.extend(sorted(list(last.difference(current))))
+ to_return = {
+ 'pages': pages,
+ 'page_obj': page_obj,
+ 'paginator': paginator,
+ 'is_paginated': paginator.count > paginator.per_page,
+ }
+ if 'request' in context:
+ getvars = context['request'].GET.copy()
+ if 'page' in getvars:
+ del getvars['page']
+ if len(getvars.keys()) > 0:
+ to_return['getvars'] = "&%s" % getvars.urlencode()
+ else:
+ to_return['getvars'] = ''
+ return to_return
+ except KeyError:
+ return {}
+register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
+register.tag('autopaginate', do_autopaginate)
\ No newline at end of file
--- /dev/null
+"""
+>>> from django.core.paginator import Paginator
+>>> from pagination.templatetags.pagination_tags import paginate
+>>> from django.template import Template, Context
+
+>>> p = Paginator(range(15), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, 5, 6, 7, 8]
+
+>>> p = Paginator(range(17), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+
+>>> p = Paginator(range(19), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, None, 7, 8, 9, 10]
+
+>>> p = Paginator(range(21), 2)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, None, 8, 9, 10, 11]
+
+# Testing orphans
+>>> p = Paginator(range(5), 2, 1)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2]
+
+>>> p = Paginator(range(21), 2, 1)
+>>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
+[1, 2, 3, 4, None, 7, 8, 9, 10]
+
+>>> t = Template("{% load pagination_tags %}{% autopaginate var 2 %}{% paginate %}")
+
+# WARNING: Please, please nobody read this portion of the code!
+>>> class GetProxy(object):
+... def __iter__(self): yield self.__dict__.__iter__
+... def copy(self): return self
+... def urlencode(self): return u''
+... def keys(self): return []
+>>> class RequestProxy(object):
+... page = 1
+... GET = GetProxy()
+>>>
+# ENDWARNING
+
+>>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
+u'\\n<div class="pagination">...
+>>>
+>>> t = Template("{% load pagination_tags %}{% autopaginate var %}{% paginate %}")
+>>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
+u'\\n<div class="pagination">...
+>>>
+"""
\ No newline at end of file
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django.contrib import admin
-
-from newtagging.admin import TaggableModelAdmin
-from catalogue.models import Tag, Book, Fragment
-
-
-class TagAdmin(admin.ModelAdmin):
- list_display = ('name', 'slug', 'sort_key', 'category', 'has_description',)
- list_filter = ('category',)
- search_fields = ('name',)
- ordering = ('name',)
-
- prepopulated_fields = {'slug': ('name',), 'sort_key': ('name',),}
- radio_fields = {'category': admin.HORIZONTAL}
-
-
-class BookAdmin(TaggableModelAdmin):
- tag_model = Tag
-
- list_display = ('title', 'slug', 'has_pdf_file', 'has_odt_file', 'has_html_file', 'has_description',)
- search_fields = ('title',)
- ordering = ('title',)
-
- prepopulated_fields = {'slug': ('title',)}
-
-
-class FragmentAdmin(TaggableModelAdmin):
- tag_model = Tag
-
- list_display = ('book', 'anchor',)
- ordering = ('book', 'anchor',)
-
-
-admin.site.register(Tag, TagAdmin)
-admin.site.register(Book, BookAdmin)
-admin.site.register(Fragment, FragmentAdmin)
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django import forms
-from django.forms.widgets import flatatt
-from django.forms.util import smart_unicode
-from django.utils.html import escape
-from django.utils.safestring import mark_safe
-from django.utils.simplejson import dumps
-
-
-class JQueryAutoCompleteWidget(forms.TextInput):
- def __init__(self, source, options=None, *args, **kwargs):
- self.source = source
- self.options = None
- if options:
- self.options = dumps(options)
- super(JQueryAutoCompleteWidget, self).__init__(*args, **kwargs)
-
- def render_js(self, field_id):
- source = "'%s'" % escape(self.source)
- options = ''
- if self.options:
- options += ', %s' % self.options
-
- return u'$(\'#%s\').autocomplete(%s%s);' % (field_id, source, options)
-
- def render(self, name, value=None, attrs=None):
- final_attrs = self.build_attrs(attrs, name=name)
- if value:
- final_attrs['value'] = smart_unicode(value)
-
- if not self.attrs.has_key('id'):
- final_attrs['id'] = 'id_%s' % name
-
- html = u'''<input type="text" %(attrs)s/>
- <script type="text/javascript"><!--//
- %(js)s//--></script>
- ''' % {
- 'attrs' : flatatt(final_attrs),
- 'js' : self.render_js(final_attrs['id']),
- }
-
- return mark_safe(html)
-
-
-class JQueryAutoCompleteField(forms.CharField):
- def __init__(self, source, options=None, *args, **kwargs):
- if 'widget' not in kwargs:
- kwargs['widget'] = JQueryAutoCompleteWidget(source, options)
-
- super(JQueryAutoCompleteField, self).__init__(*args, **kwargs)
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django import forms
-from slughifi import slughifi
-
-from catalogue.models import Tag
-from catalogue.fields import JQueryAutoCompleteField
-
-
-class SearchForm(forms.Form):
- q = JQueryAutoCompleteField('/katalog/tags/', {'minChars': 2, 'selectFirst': True, 'cacheLength': 50})
- tags = forms.CharField(widget=forms.HiddenInput, required=False)
-
- def __init__(self, *args, **kwargs):
- tags = kwargs.pop('tags', [])
- super(SearchForm, self).__init__(*args, **kwargs)
- self.fields['q'].widget.attrs['title'] = u'tytuł utworu, motyw lub kategoria'
- self.fields['tags'].initial = '/'.join(tag.slug for tag in Tag.get_tag_list(tags))
-
-
-class UserSetsForm(forms.Form):
- def __init__(self, book, user, *args, **kwargs):
- super(UserSetsForm, self).__init__(*args, **kwargs)
- self.fields['set_ids'] = forms.ChoiceField(
- choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user)],
- )
-
-
-class ObjectSetsForm(forms.Form):
- def __init__(self, obj, user, *args, **kwargs):
- super(ObjectSetsForm, self).__init__(*args, **kwargs)
- self.fields['set_ids'] = forms.MultipleChoiceField(
- label=u'Półki',
- required=False,
- choices=[(tag.id, tag.name) for tag in Tag.objects.filter(category='set', user=user)],
- initial=[tag.id for tag in obj.tags.filter(category='set', user=user)],
- widget=forms.CheckboxSelectMultiple
- )
-
-
-class NewSetForm(forms.Form):
- name = forms.CharField(max_length=50, required=True)
-
- def save(self, user, commit=True):
- name = self.cleaned_data['name']
- new_set = Tag(name=name, slug=slughifi(name), sort_key=slughifi(name),
- category='set', user=user)
-
- new_set.save()
- return new_set
-
+++ /dev/null
-import os
-
-from django.core.management.base import BaseCommand
-from django.core.management.color import color_style
-from optparse import make_option
-
-from catalogue.models import Book
-
-
-class Command(BaseCommand):
- option_list = BaseCommand.option_list + (
- make_option('--verbosity', action='store', dest='verbosity', default='1',
- type='choice', choices=['0', '1', '2'],
- help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
- )
- help = 'Imports books from the specified directories.'
- args = 'directory [directory ...]'
-
- def handle(self, *directories, **options):
- from django.db import transaction
-
- self.style = color_style()
-
- verbosity = int(options.get('verbosity', 1))
- show_traceback = options.get('traceback', False)
-
- # Start transaction management.
- transaction.commit_unless_managed()
- transaction.enter_transaction_management()
- transaction.managed(True)
-
- for dir_name in directories:
- if not os.path.isdir(dir_name):
- print self.style.ERROR("Skipping '%s': not a directory." % dir_name)
- else:
- for file_name in os.listdir(dir_name):
- file_path = os.path.join(dir_name, file_name)
- if not os.path.splitext(file_name)[1] == '.xml':
- print self.style.NOTICE("Skipping '%s': not an XML file." % file_path)
- continue
- if verbosity > 0:
- print "Parsing '%s'" % file_path
-
- Book.from_xml_file(file_path)
-
- transaction.commit()
- transaction.leave_transaction_management()
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django.db import models
-from django.db.models import permalink, Q
-from django.utils.translation import ugettext_lazy as _
-from django.contrib.auth.models import User
-from django.core.files import File
-from django.template.loader import render_to_string
-from django.utils.safestring import mark_safe
-
-from newtagging.models import TagBase
-from newtagging import managers
-
-from librarian import html, dcparser
-
-
-TAG_CATEGORIES = (
- ('author', _('author')),
- ('epoch', _('epoch')),
- ('kind', _('kind')),
- ('genre', _('genre')),
- ('theme', _('theme')),
- ('set', _('set')),
-)
-
-
-class TagSubcategoryManager(models.Manager):
- def __init__(self, subcategory):
- super(TagSubcategoryManager, self).__init__()
- self.subcategory = subcategory
-
- def get_query_set(self):
- return super(TagSubcategoryManager, self).get_query_set().filter(category=self.subcategory)
-
-
-class Tag(TagBase):
- name = models.CharField(_('name'), max_length=50, unique=True, db_index=True)
- slug = models.SlugField(_('slug'), unique=True, db_index=True)
- sort_key = models.SlugField(_('sort key'), db_index=True)
- category = models.CharField(_('category'), max_length=50, blank=False, null=False,
- db_index=True, choices=TAG_CATEGORIES)
- description = models.TextField(blank=True)
-
- user = models.ForeignKey(User, blank=True, null=True)
-
- def has_description(self):
- return len(self.description) > 0
- has_description.short_description = _('description')
- has_description.boolean = True
-
- @permalink
- def get_absolute_url(self):
- return ('catalogue.views.tagged_object_list', [self.slug])
-
- class Meta:
- ordering = ('sort_key',)
- verbose_name = _('tag')
- verbose_name_plural = _('tags')
-
- def __unicode__(self):
- return self.name
-
- @staticmethod
- def get_tag_list(tags):
- if isinstance(tags, basestring):
- tag_slugs = tags.split('/')
- return [Tag.objects.get(slug=slug) for slug in tag_slugs]
- else:
- return TagBase.get_tag_list(tags)
-
-
-class Book(models.Model):
- title = models.CharField(_('title'), max_length=120)
- slug = models.SlugField(_('slug'), unique=True, db_index=True)
- description = models.TextField(_('description'), blank=True)
- created_at = models.DateTimeField(_('creation date'), auto_now=True)
- _short_html = models.TextField(_('short HTML'), editable=False)
-
- # Formats
- xml_file = models.FileField(_('XML file'), upload_to='books/xml', blank=True)
- pdf_file = models.FileField(_('PDF file'), upload_to='books/pdf', blank=True)
- odt_file = models.FileField(_('ODT file'), upload_to='books/odt', blank=True)
- html_file = models.FileField(_('HTML file'), upload_to='books/html', blank=True)
-
- parent = models.ForeignKey('self', blank=True, null=True, related_name='children')
-
- objects = models.Manager()
- tagged = managers.ModelTaggedItemManager(Tag)
- tags = managers.TagDescriptor(Tag)
-
- def short_html(self):
- if len(self._short_html):
- return mark_safe(self._short_html)
- else:
- tags = self.tags.filter(~Q(category__in=('set', 'theme')))
- tags = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name) for tag in tags]
-
- formats = []
- if self.html_file:
- formats.append(u'<a href="%s">Czytaj online</a>' % self.html_file.url)
- if self.pdf_file:
- formats.append(u'<a href="%s">Plik PDF</a>' % self.pdf_file.url)
- if self.odt_file:
- formats.append(u'<a href="%s">Plik ODT</a>' % self.odt_file.url)
-
- self._short_html = render_to_string('catalogue/book_short.html',
- {'book': self, 'tags': tags, 'formats': formats})
- self.save()
- return mark_safe(self._short_html)
-
- def has_description(self):
- return len(self.description) > 0
- has_description.short_description = _('description')
- has_description.boolean = True
-
- def has_pdf_file(self):
- return bool(self.pdf_file)
- has_pdf_file.short_description = 'PDF'
- has_pdf_file.boolean = True
-
- def has_odt_file(self):
- return bool(self.odt_file)
- has_odt_file.short_description = 'ODT'
- has_odt_file.boolean = True
-
- def has_html_file(self):
- return bool(self.html_file)
- has_html_file.short_description = 'HTML'
- has_html_file.boolean = True
-
- @staticmethod
- def from_xml_file(xml_file):
- from tempfile import NamedTemporaryFile
- from slughifi import slughifi
- from markupstring import MarkupString
-
- # Read book metadata
- book_info = dcparser.parse(xml_file)
- book = Book(title=book_info.title, slug=slughifi(book_info.title))
- book.save()
-
- book_tags = []
- for category in ('kind', 'genre', 'author', 'epoch'):
- tag_name = getattr(book_info, category)
- tag_sort_key = tag_name
- if category == 'author':
- tag_sort_key = tag_name.last_name
- tag_name = ' '.join(tag_name.first_names) + ' ' + tag_name.last_name
- tag, created = Tag.objects.get_or_create(name=tag_name,
- slug=slughifi(tag_name), sort_key=slughifi(tag_sort_key), category=category)
- tag.save()
- book_tags.append(tag)
- book.tags = book_tags
-
- if hasattr(book_info, 'parts'):
- for part_url in book_info.parts:
- base, slug = part_url.rsplit('/', 1)
- child_book = Book.objects.get(slug=slug)
- child_book.parent = book
- child_book.save()
-
- # Save XML and HTML files
- book.xml_file.save('%s.xml' % book.slug, File(file(xml_file)), save=False)
-
- html_file = NamedTemporaryFile()
- html.transform(book.xml_file.path, html_file)
- book.html_file.save('%s.html' % book.slug, File(html_file), save=False)
-
- # Extract fragments
- closed_fragments, open_fragments = html.extract_fragments(book.html_file.path)
- book_themes = []
- for fragment in closed_fragments.values():
- text = fragment.to_string()
- short_text = ''
- if (len(MarkupString(text)) > 240):
- short_text = MarkupString(text)[:160]
- new_fragment = Fragment(text=text, short_text=short_text, anchor=fragment.id, book=book)
-
- theme_names = [s.strip() for s in fragment.themes.split(',')]
- themes = []
- for theme_name in theme_names:
- tag, created = Tag.objects.get_or_create(name=theme_name,
- slug=slughifi(theme_name), sort_key=slughifi(theme_name), category='theme')
- tag.save()
- themes.append(tag)
- new_fragment.save()
- new_fragment.tags = list(book.tags) + themes
- book_themes += themes
-
- book_themes = set(book_themes)
- book.tags = list(book.tags) + list(book_themes)
- return book.save()
-
- @permalink
- def get_absolute_url(self):
- return ('catalogue.views.book_detail', [self.slug])
-
- class Meta:
- ordering = ('title',)
- verbose_name = _('book')
- verbose_name_plural = _('books')
-
- def __unicode__(self):
- return self.title
-
-
-class Fragment(models.Model):
- text = models.TextField()
- short_text = models.TextField(editable=False)
- _short_html = models.TextField(editable=False)
- anchor = models.IntegerField()
- book = models.ForeignKey(Book, related_name='fragments')
-
- objects = models.Manager()
- tagged = managers.ModelTaggedItemManager(Tag)
- tags = managers.TagDescriptor(Tag)
-
- def short_html(self):
- if len(self._short_html):
- return mark_safe(self._short_html)
- else:
- book_authors = [u'<a href="%s">%s</a>' % (tag.get_absolute_url(), tag.name)
- for tag in self.book.tags if tag.category == 'author']
-
- self._short_html = render_to_string('catalogue/fragment_short.html',
- {'fragment': self, 'book': self.book, 'book_authors': book_authors})
- self.save()
- return mark_safe(self._short_html)
-
- class Meta:
- ordering = ('book', 'anchor',)
- verbose_name = _('fragment')
- verbose_name_plural = _('fragments')
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django import template
-from django.template import Node, Variable
-from django.utils.encoding import smart_str
-from django.core.urlresolvers import reverse
-from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
-from django.db.models import Q
-
-
-register = template.Library()
-
-
-class RegistrationForm(UserCreationForm):
- def as_ul(self):
- "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
- return self._html_output(u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>', '</li>', u' %s', False)
-
-
-class LoginForm(AuthenticationForm):
- def as_ul(self):
- "Returns this form rendered as HTML <li>s -- excluding the <ul></ul>."
- return self._html_output(u'<li>%(errors)s%(label)s %(field)s<span class="help-text">%(help_text)s</span></li>', u'<li>%s</li>', '</li>', u' %s', False)
-
-
-def iterable(obj):
- try:
- iter(obj)
- return True
- except TypeError:
- return False
-
-
-def capfirst(text):
- try:
- return '%s%s' % (text[0].upper(), text[1:])
- except IndexError:
- return ''
-
-
-@register.simple_tag
-def title_from_tags(tags):
- def split_tags(tags):
- result = {}
- for tag in tags:
- result[tag.category] = tag
- return result
-
- class Flection(object):
- def get_case(self, name, flection):
- return name
- flection = Flection()
-
- self = split_tags(tags)
-
- title = u''
-
- # Specjalny przypadek oglądania wszystkich lektur na danej półce
- if len(self) == 1 and 'set' in self:
- return u'Półka %s' % self['set']
-
- # Specjalny przypadek "Twórczość w pozytywizmie", wtedy gdy tylko epoka
- # jest wybrana przez użytkownika
- if 'epoch' in self and len(self) == 1:
- text = u'Twórczość w %s' % flection.get_case(unicode(self['epoch']), u'miejscownik')
- return capfirst(text)
-
- # Specjalny przypadek "Dramat w twórczości Sofoklesa", wtedy gdy podane
- # są tylko rodzaj literacki i autor
- if 'kind' in self and 'author' in self and len(self) == 2:
- text = u'%s w twórczości %s' % (unicode(self['kind']),
- flection.get_case(unicode(self['author']), u'dopełniacz'))
- return capfirst(text)
-
- # Przypadki ogólniejsze
- if 'theme' in self:
- title += u'Motyw %s' % unicode(self['theme'])
-
- if 'genre' in self:
- if 'theme' in self:
- title += u' w %s' % flection.get_case(unicode(self['genre']), u'miejscownik')
- else:
- title += unicode(self['genre'])
-
- if 'kind' in self or 'author' in self or 'epoch' in self:
- if 'genre' in self or 'theme' in self:
- if 'kind' in self:
- title += u' w %s ' % flection.get_case(unicode(self['kind']), u'miejscownik')
- else:
- title += u' w twórczości '
- else:
- title += u'%s ' % unicode(self.get('kind', u'twórczość'))
-
- if 'author' in self:
- title += flection.get_case(unicode(self['author']), u'dopełniacz')
- elif 'epoch' in self:
- title += flection.get_case(unicode(self['epoch']), u'dopełniacz')
-
- return capfirst(title)
-
-
-@register.simple_tag
-def user_creation_form():
- return RegistrationForm(prefix='registration').as_ul()
-
-
-@register.simple_tag
-def authentication_form():
- return LoginForm(prefix='login').as_ul()
-
-
-@register.inclusion_tag('catalogue/breadcrumbs.html')
-def breadcrumbs(tags, search_form=True):
- from wolnelektury.catalogue.forms import SearchForm
- context = {'tag_list': tags}
- if search_form:
- context['search_form'] = SearchForm(tags=tags)
- return context
-
-
-@register.tag
-def catalogue_url(parser, token):
- bits = token.split_contents()
- tag_name = bits[0]
-
- tags_to_add = []
- tags_to_remove = []
- for bit in bits[1:]:
- if bit[0] == '-':
- tags_to_remove.append(bit[1:])
- else:
- tags_to_add.append(bit)
-
- return CatalogueURLNode(tags_to_add, tags_to_remove)
-
-
-class CatalogueURLNode(Node):
- def __init__(self, tags_to_add, tags_to_remove):
- self.tags_to_add = [Variable(tag) for tag in tags_to_add]
- self.tags_to_remove = [Variable(tag) for tag in tags_to_remove]
-
- def render(self, context):
- tags_to_add = []
- tags_to_remove = []
-
- for tag_variable in self.tags_to_add:
- tag = tag_variable.resolve(context)
- if isinstance(tag, (list, dict)):
- tags_to_add += [t for t in tag]
- else:
- tags_to_add.append(tag)
-
- for tag_variable in self.tags_to_remove:
- tag = tag_variable.resolve(context)
- if iterable(tag):
- tags_to_remove += [t for t in tag]
- else:
- tags_to_remove.append(tag)
-
- tag_slugs = [tag.slug for tag in tags_to_add]
- for tag in tags_to_remove:
- try:
- tag_slugs.remove(tag.slug)
- except KeyError:
- pass
-
- if len(tag_slugs) > 0:
- return reverse('tagged_object_list', kwargs={'tags': '/'.join(tag_slugs)})
- else:
- return reverse('main_page')
-
-
-@register.inclusion_tag('catalogue/latest_blog_posts.html')
-def latest_blog_posts(feed_url, posts_to_show=5):
- import feedparser
- import datetime
-
- feed = feedparser.parse(feed_url)
- posts = []
- for i in range(posts_to_show):
- pub_date = feed['entries'][i].updated_parsed
- published = datetime.date(pub_date[0], pub_date[1], pub_date[2] )
- posts.append({
- 'title': feed['entries'][i].title,
- 'summary': feed['entries'][i].summary,
- 'link': feed['entries'][i].link,
- 'date': published,
- })
- return {'posts': posts}
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django.conf.urls.defaults import *
-
-
-urlpatterns = patterns('catalogue.views',
- url(r'^$', 'main_page', name='main_page'),
- url(r'^polki/$', 'user_shelves', name='user_shelves'),
- url(r'^polki/(?P<slug>[a-zA-Z0-9-]+)/usun/$', 'delete_shelf', name='delete_shelf'),
- url(r'^lektury/', 'book_list', name='book_list'),
- url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/polki/', 'book_sets', name='book_shelves'),
- url(r'^fragment/(?P<id>[0-9]+)/polki/', 'fragment_sets', name='fragment_shelves'),
- url(r'^polki/nowa/$', 'new_set', name='new_set'),
- url(r'^lektura/(?P<slug>[a-zA-Z0-9-]+)/$', 'book_detail', name='book_detail'),
- url(r'^tags/$', 'tags_starting_with', name='hint'),
- url(r'^szukaj/$', 'search', name='search'),
- url(r'^(?P<tags>[a-zA-Z0-9-/]+)/$', 'tagged_object_list', name='tagged_object_list'),
-)
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-
-
-def split_tags(tags):
- result = {}
- for tag in tags:
- result.setdefault(tag.category, []).append(tag)
- return result
-
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django.template import RequestContext
-from django.shortcuts import render_to_response, get_object_or_404
-from django.http import HttpResponse, HttpResponseRedirect, Http404
-from django.core.urlresolvers import reverse
-from django.db.models import Q
-from django.contrib.auth.decorators import login_required
-from django.utils.datastructures import SortedDict
-from django.views.decorators.http import require_POST
-from django.contrib import auth
-from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
-from django.utils import simplejson
-from django.utils.functional import Promise
-from django.utils.encoding import force_unicode
-
-from catalogue import models
-from catalogue import forms
-from catalogue.utils import split_tags
-from newtagging import views as newtagging_views
-
-
-class LazyEncoder(simplejson.JSONEncoder):
- def default(self, obj):
- if isinstance(obj, Promise):
- return force_unicode(obj)
- return obj
-
-
-def search(request):
- query = request.GET.get('q', '')
- tags = request.GET.get('tags', '')
- if tags == '':
- tags = []
-
- try:
- tag_list = models.Tag.get_tag_list(tags)
- tag = models.Tag.objects.get(name=query)
- except models.Tag.DoesNotExist:
- try:
- book = models.Book.objects.get(title=query)
- return HttpResponseRedirect(book.get_absolute_url())
- except models.Book.DoesNotExist:
- return HttpResponseRedirect(reverse('catalogue.views.main_page'))
- else:
- tag_list.append(tag)
- return HttpResponseRedirect(reverse('catalogue.views.tagged_object_list',
- kwargs={'tags': '/'.join(tag.slug for tag in tag_list)}
- ))
-
-
-def tags_starting_with(request):
- try:
- prefix = request.GET['q']
- if len(prefix) < 2:
- raise KeyError
-
- books = models.Book.objects.filter(title__icontains=prefix)
- tags = models.Tag.objects.filter(name__icontains=prefix)
- if request.user.is_authenticated():
- tags = tags.filter(~Q(category='set') | Q(user=request.user))
- else:
- tags = tags.filter(~Q(category='set'))
-
- completions = [book.title for book in books] + [tag.name for tag in tags]
-
- return HttpResponse('\n'.join(completions))
-
- except KeyError:
- return HttpResponse('')
-
-
-def main_page(request):
- if request.user.is_authenticated():
- extra_where = '(NOT catalogue_tag.category = "set" OR catalogue_tag.user_id = %d)' % request.user.id
- else:
- extra_where = 'NOT catalogue_tag.category = "set"'
- tags = models.Tag.objects.usage_for_model(models.Book, counts=True, extra={'where': [extra_where]})
- fragment_tags = models.Tag.objects.usage_for_model(models.Fragment, counts=True,
- extra={'where': ['catalogue_tag.category = "theme"']})
- categories = split_tags(tags)
-
- form = forms.SearchForm()
- return render_to_response('catalogue/main_page.html', locals(),
- context_instance=RequestContext(request))
-
-
-def book_list(request):
- books = models.Book.objects.all()
- form = forms.SearchForm()
-
- books_by_first_letter = SortedDict()
- for book in books:
- books_by_first_letter.setdefault(book.title[0], []).append(book)
-
- return render_to_response('catalogue/book_list.html', locals(),
- context_instance=RequestContext(request))
-
-
-def tagged_object_list(request, tags=''):
- try:
- tags = models.Tag.get_tag_list(tags)
- except models.Tag.DoesNotExist:
- raise Http404
-
- model = models.Book
- theme_is_set = any(tag.category == 'theme' for tag in tags)
- if theme_is_set:
- model = models.Fragment
-
- if request.user.is_authenticated():
- extra_where = '(NOT catalogue_tag.category = "set" OR catalogue_tag.user_id = %d)' % request.user.id
- else:
- extra_where = 'NOT catalogue_tag.category = "set"'
- related_tags = models.Tag.objects.related_for_model(tags, model, counts=True, extra={'where': [extra_where]})
- categories = split_tags(related_tags)
-
- return newtagging_views.tagged_object_list(
- request,
- tag_model=models.Tag,
- queryset_or_model=model,
- tags=tags,
- template_name='catalogue/tagged_object_list.html',
- extra_context = {'categories': categories },
- )
-
-
-def book_detail(request, slug):
- book = get_object_or_404(models.Book, slug=slug)
- tags = list(book.tags.filter(~Q(category='set')))
- categories = split_tags(tags)
-
- return render_to_response('catalogue/book_detail.html', locals(),
- context_instance=RequestContext(request))
-
-
-def logout_then_redirect(request):
- auth.logout(request)
- return HttpResponseRedirect(request.GET.get('next', '/'))
-
-
-@require_POST
-def register(request):
- registration_form = UserCreationForm(request.POST, prefix='registration')
- if registration_form.is_valid():
- user = registration_form.save()
- user = auth.authenticate(
- username=registration_form.cleaned_data['username'],
- password=registration_form.cleaned_data['password1']
- )
- auth.login(request, user)
- response_data = {'success': True, 'errors': {}}
- else:
- response_data = {'success': False, 'errors': registration_form.errors}
- return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
-
-
-@require_POST
-def login(request):
- form = AuthenticationForm(data=request.POST, prefix='login')
- if form.is_valid():
- auth.login(request, form.get_user())
- response_data = {'success': True, 'errors': {}}
- else:
- response_data = {'success': False, 'errors': form.errors}
- return HttpResponse(LazyEncoder(ensure_ascii=False).encode(response_data))
-
-
-def book_sets(request, slug):
- book = get_object_or_404(models.Book, slug=slug)
- user_sets = models.Tag.objects.filter(category='set', user=request.user)
- book_sets = book.tags.filter(category='set', user=request.user)
-
- if not request.user.is_authenticated():
- return HttpResponse('<p>Aby zarządzać swoimi półkami, musisz się zalogować.</p>')
-
- if request.method == 'POST':
- form = forms.ObjectSetsForm(book, request.user, request.POST)
- if form.is_valid():
- book.tags = ([models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] +
- list(book.tags.filter(~Q(category='set') | ~Q(user=request.user))))
- if request.is_ajax():
- return HttpResponse('<p>Półki zostały zapisane.</p>')
- else:
- return HttpResponseRedirect('/')
- else:
- form = forms.ObjectSetsForm(book, request.user)
- new_set_form = forms.NewSetForm()
-
- return render_to_response('catalogue/book_sets.html', locals(),
- context_instance=RequestContext(request))
-
-
-def fragment_sets(request, id):
- fragment = get_object_or_404(models.Fragment, pk=id)
- user_sets = models.Tag.objects.filter(category='set', user=request.user)
- fragment_sets = fragment.tags.filter(category='set', user=request.user)
-
- if not request.user.is_authenticated():
- return HttpResponse('<p>Aby zarządzać swoimi półkami, musisz się zalogować.</p>')
-
- if request.method == 'POST':
- form = forms.ObjectSetsForm(fragment, request.user, request.POST)
- if form.is_valid():
- fragment.tags = ([models.Tag.objects.get(pk=id) for id in form.cleaned_data['set_ids']] +
- list(fragment.tags.filter(~Q(category='set') | ~Q(user=request.user))))
- if request.is_ajax():
- return HttpResponse('<p>Półki zostały zapisane.</p>')
- else:
- return HttpResponseRedirect('/')
- else:
- form = forms.ObjectSetsForm(fragment, request.user)
- new_set_form = forms.NewSetForm()
-
- return render_to_response('catalogue/fragment_sets.html', locals(),
- context_instance=RequestContext(request))
-
-
-@login_required
-@require_POST
-def new_set(request):
- new_set_form = forms.NewSetForm(request.POST)
- if new_set_form.is_valid():
- new_set = new_set_form.save(request.user)
- return HttpResponse(u'<p>Półka <strong>%s</strong> została utworzona</p>' % new_set)
-
- return render_to_response('catalogue/book_sets.html', locals(),
- context_instance=RequestContext(request))
-
-
-@login_required
-@require_POST
-def delete_shelf(request, slug):
- user_set = get_object_or_404(models.Tag, slug=slug, category='set', user=request.user)
- user_set.delete()
- return HttpResponse(u'<p>Półka <strong>%s</strong> została usunięta</p>' % user_set.name)
-
-
-@login_required
-def user_shelves(request):
- shelves = models.Tag.objects.filter(category='set', user=request.user)
- new_set_form = forms.NewSetForm()
- return render_to_response('catalogue/user_shelves.html', locals(),
- context_instance=RequestContext(request))
-
+++ /dev/null
-from django.contrib import admin
-
-from chunks.models import Chunk
-
-
-class ChunkAdmin(admin.ModelAdmin):
- list_display = ('key', 'description',)
- search_fields = ('key', 'content',)
-
-admin.site.register(Chunk, ChunkAdmin)
-
+++ /dev/null
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-
-
-class Chunk(models.Model):
- """
- A Chunk is a piece of content associated with a unique key that can be inserted into
- any template with the use of a special template tag.
- """
- key = models.CharField(_('key'), help_text=_('A unique name for this chunk of content'), primary_key=True, max_length=255)
- description = models.CharField(_('description'), blank=True, max_length=255)
- content = models.TextField(_('content'), blank=True)
-
- class Meta:
- ordering = ('key',)
- verbose_name = _('chunk')
- verbose_name_plural = _('chunks')
-
- def __unicode__(self):
- return u'%s' % (self.key,)
-
+++ /dev/null
-from django import template
-from django.db import models
-from django.core.cache import cache
-
-register = template.Library()
-
-Chunk = models.get_model('chunks', 'chunk')
-CACHE_PREFIX = "chunk_"
-
-def do_get_chunk(parser, token):
- # split_contents() knows not to split quoted strings.
- tokens = token.split_contents()
- if len(tokens) < 2 or len(tokens) > 3:
- raise template.TemplateSyntaxError, "%r tag should have either 2 or 3 arguments" % (tokens[0],)
- if len(tokens) == 2:
- tag_name, key = tokens
- cache_time = 0
- if len(tokens) == 3:
- tag_name, key, cache_time = tokens
- # Check to see if the key is properly double/single quoted
- if not (key[0] == key[-1] and key[0] in ('"', "'")):
- raise template.TemplateSyntaxError, "%r tag's argument should be in quotes" % tag_name
- # Send key without quotes and caching time
- return ChunkNode(key[1:-1], cache_time)
-
-class ChunkNode(template.Node):
- def __init__(self, key, cache_time=0):
- self.key = key
- self.cache_time = cache_time
-
- def render(self, context):
- try:
- cache_key = CACHE_PREFIX + self.key
- c = cache.get(cache_key)
- if c is None:
- c = Chunk.objects.get(key=self.key)
- cache.set(cache_key, c, int(self.cache_time))
- content = c.content
- except Chunk.DoesNotExist:
- n = Chunk(key=self.key)
- n.save()
- return ''
- return content
-
-register.tag('chunk', do_get_chunk)
+++ /dev/null
-from django.core.exceptions import ImproperlyConfigured
-from django.conf import settings
-
-COMPRESS = getattr(settings, 'COMPRESS', not settings.DEBUG)
-COMPRESS_AUTO = getattr(settings, 'COMPRESS_AUTO', True)
-COMPRESS_VERSION = getattr(settings, 'COMPRESS_VERSION', False)
-COMPRESS_VERSION_PLACEHOLDER = getattr(settings, 'COMPRESS_VERSION_PLACEHOLDER', '?')
-COMPRESS_VERSION_DEFAULT = getattr(settings, 'COMPRESS_VERSION_DEFAULT', '0')
-
-COMPRESS_CSS_FILTERS = getattr(settings, 'COMPRESS_CSS_FILTERS', ['compress.filters.csstidy.CSSTidyFilter'])
-COMPRESS_JS_FILTERS = getattr(settings, 'COMPRESS_JS_FILTERS', ['compress.filters.jsmin.JSMinFilter'])
-COMPRESS_CSS = getattr(settings, 'COMPRESS_CSS', {})
-COMPRESS_JS = getattr(settings, 'COMPRESS_JS', {})
-
-if COMPRESS_CSS_FILTERS is None:
- COMPRESS_CSS_FILTERS = []
-
-if COMPRESS_JS_FILTERS is None:
- COMPRESS_JS_FILTERS = []
-
-if COMPRESS_VERSION and not COMPRESS_AUTO:
- raise ImproperlyConfigured('COMPRESS_AUTO needs to be True when using COMPRESS_VERSION.')
+++ /dev/null
-class FilterBase:
- def __init__(self, verbose):
- self.verbose = verbose
-
- def filter_css(self, css):
- raise NotImplementedError
- def filter_js(self, js):
- raise NotImplementedError
-
-class FilterError(Exception):
- """
- This exception is raised when a filter fails
- """
- pass
\ No newline at end of file
+++ /dev/null
-import os
-import warnings
-import tempfile
-
-from django.conf import settings
-
-from compress.filter_base import FilterBase
-
-BINARY = getattr(settings, 'CSSTIDY_BINARY', 'csstidy')
-ARGUMENTS = getattr(settings, 'CSSTIDY_ARGUMENTS', '--template=highest')
-
-warnings.simplefilter('ignore', RuntimeWarning)
-
-class CSSTidyFilter(FilterBase):
- def filter_css(self, css):
- tmp_file = tempfile.NamedTemporaryFile(mode='w+b')
- tmp_file.write(css)
- tmp_file.flush()
-
- output_file = tempfile.NamedTemporaryFile(mode='w+b')
-
- command = '%s %s %s %s' % (BINARY, tmp_file.name, ARGUMENTS, output_file.name)
-
- command_output = os.popen(command).read()
-
- filtered_css = output_file.read()
- output_file.close()
- tmp_file.close()
-
- if self.verbose:
- print command_output
-
- return filtered_css
+++ /dev/null
-from django.conf import settings
-
-from compress.filter_base import FilterBase
-from compress.filters.csstidy_python.csstidy import CSSTidy
-
-COMPRESS_CSSTIDY_SETTINGS = getattr(settings, 'COMPRESS_CSSTIDY_SETTINGS', {})
-
-class CSSTidyFilter(FilterBase):
- def filter_css(self, css):
- tidy = CSSTidy()
-
- for k, v in COMPRESS_CSSTIDY_SETTINGS.items():
- tidy.setSetting(k, v)
-
- tidy.parse(css)
-
- r = tidy.Output('string')
-
- return r
+++ /dev/null
-# CSSTidy - CSS Parse
-#
-# CSS Parser class
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
-
-import re
-
-from optimizer import CSSOptimizer
-from output import CSSPrinter
-import data
-from tools import SortedDict
-
-class CSSTidy(object):
- #Saves the parsed CSS
- _css = ""
- _raw_css = SortedDict()
- _optimized_css = SortedDict()
-
- #List of Tokens
- _tokens = []
-
- #Printer class
- _output = None
-
- #Optimiser class
- _optimizer = None
-
- #Saves the CSS charset (@charset)
- _charset = ''
-
- #Saves all @import URLs
- _import = []
-
- #Saves the namespace
- _namespace = ''
-
- #Contains the version of csstidy
- _version = '1.3'
-
- #Stores the settings
- _settings = {}
-
- # Saves the parser-status.
- #
- # Possible values:
- # - is = in selector
- # - ip = in property
- # - iv = in value
- # - instr = in string (started at " or ' or ( )
- # - ic = in comment (ignore everything)
- # - at = in @-block
- _status = 'is'
-
- #Saves the current at rule (@media)
- _at = ''
-
- #Saves the current selector
- _selector = ''
-
- #Saves the current property
- _property = ''
-
- #Saves the position of , in selectors
- _sel_separate = []
-
- #Saves the current value
- _value = ''
-
- #Saves the current sub-value
- _sub_value = ''
-
- #Saves all subvalues for a property.
- _sub_value_arr = []
-
- #Saves the char which opened the last string
- _str_char = ''
- _cur_string = ''
-
- #Status from which the parser switched to ic or instr
- _from = ''
-
- #Variable needed to manage string-in-strings, for example url("foo.png")
- _str_in_str = False
-
- #=True if in invalid at-rule
- _invalid_at = False
-
- #=True if something has been added to the current selector
- _added = False
-
- #Saves the message log
- _log = SortedDict()
-
- #Saves the line number
- _line = 1
-
- def __init__(self):
- self._settings['remove_bslash'] = True
- self._settings['compress_colors'] = True
- self._settings['compress_font-weight'] = True
- self._settings['lowercase_s'] = False
- self._settings['optimise_shorthands'] = 2
- self._settings['remove_last_'] = False
- self._settings['case_properties'] = 1
- self._settings['sort_properties'] = False
- self._settings['sort_selectors'] = False
- self._settings['merge_selectors'] = 2
- self._settings['discard_invalid_properties'] = False
- self._settings['css_level'] = 'CSS2.1'
- self._settings['preserve_css'] = False
- self._settings['timestamp'] = False
- self._settings['template'] = 'highest_compression'
-
- #Maps self._status to methods
- self.__statusMethod = {'is':self.__parseStatus_is, 'ip': self.__parseStatus_ip, 'iv':self.__parseStatus_iv, 'instr':self.__parseStatus_instr, 'ic':self.__parseStatus_ic, 'at':self.__parseStatus_at}
-
- self._output = CSSPrinter(self)
- self._optimizer = CSSOptimizer(self)
-
- #Public Methods
- def getSetting(self, setting):
- return self._settings.get(setting, False)
-
- #Set the value of a setting.
- def setSetting(self, setting, value):
- self._settings[setting] = value
- return True
-
- def log(self, message, ttype, line = -1):
- if line == -1:
- line = self._line
-
- line = int(line)
-
- add = {'m': message, 't': ttype}
-
- if not self._log.has_key(line):
- self._log[line] = []
- self._log[line].append(add)
- elif add not in self._log[line]:
- self._log[line].append(add)
-
-
- #Checks if a character is escaped (and returns True if it is)
- def escaped(self, string, pos):
- return not (string[pos-1] != '\\' or self.escaped(string, pos-1))
-
- #Adds CSS to an existing media/selector
- def merge_css_blocks(self, media, selector, css_add):
- for prop, value in css_add.iteritems():
- self.__css_add_property(media, selector, prop, value, False)
-
- #Checks if $value is !important.
- def is_important(self, value):
- return '!important' in value.lower()
-
- #Returns a value without !important
- def gvw_important(self, value):
- if self.is_important(value):
- ret = value.strip()
- ret = ret[0:-9]
- ret = ret.strip()
- ret = ret[0:-1]
- ret = ret.strip()
- return ret
-
- return value
-
- def parse(self, cssString):
- #Switch from \r\n to \n
- self._css = cssString.replace("\r\n", "\n") + ' '
- self._raw_css = {}
- self._optimized_css = {}
- self._curComment = ''
-
- #Start Parsing
- i = 0
- while i < len(cssString):
- if self._css[i] == "\n" or self._css[i] == "\r":
- self._line += 1
-
- i += self.__statusMethod[self._status](i)
-
- i += 1;
-
- self._optimized_css = self._optimizer.optimize(self._raw_css)
-
- def parseFile(self, filename):
- try:
- f = open(filename, "r")
- self.parse(f.read())
- finally:
- f.close()
-
- #Private Methods
- def __parseStatus_is(self, idx):
- """
- Parse in Selector
- """
- ret = 0
-
- if self.__is_token(self._css, idx):
- if self._css[idx] == '/' and self._css[idx+1] == '*' and self._selector.strip() == '':
- self._status = 'ic'
- self._from = 'is'
- return 1
-
- elif self._css[idx] == '@' and self._selector.strip() == '':
- #Check for at-rule
- self._invalid_at = True
-
- for name, ttype in data.at_rules.iteritems():
- if self._css[idx+1:len(name)].lower() == name.lower():
- if ttype == 'at':
- self._at = '@' + name
- else:
- self._selector = '@' + name
-
- self._status = ttype
- self._invalid_at = False
- ret += len(name)
-
- if self._invalid_at:
- self._selector = '@'
- invalid_at_name = ''
- for j in xrange(idx+1, len(self._css)):
- if not self._css[j].isalpha():
- break;
-
- invalid_at_name += self._css[j]
-
- self.log('Invalid @-rule: ' + invalid_at_name + ' (removed)', 'Warning')
-
- elif self._css[idx] == '"' or self._css[idx] == "'":
- self._cur_string = self._css[idx]
- self._status = 'instr'
- self._str_char = self._css[idx]
- self._from = 'is'
-
- elif self._invalid_at and self._css[idx] == ';':
- self._invalid_at = False
- self._status = 'is'
-
- elif self._css[idx] == '{':
- self._status = 'ip'
- self.__add_token(data.SEL_START, self._selector)
- self._added = False;
-
- elif self._css[idx] == '}':
- self.__add_token(data.AT_END, self._at)
- self._at = ''
- self._selector = ''
- self._sel_separate = []
-
- elif self._css[idx] == ',':
- self._selector = self._selector.strip() + ','
- self._sel_separate.append(len(self._selector))
-
- elif self._css[idx] == '\\':
- self._selector += self.__unicode(idx)
-
- #remove unnecessary universal selector, FS#147
- elif not (self._css[idx] == '*' and self._css[idx+1] in ('.', '#', '[', ':')):
- self._selector += self._css[idx]
-
- else:
- lastpos = len(self._selector)-1
-
- if lastpos == -1 or not ((self._selector[lastpos].isspace() or self.__is_token(self._selector, lastpos) and self._selector[lastpos] == ',') and self._css[idx].isspace()):
- self._selector += self._css[idx]
-
- return ret
-
- def __parseStatus_ip(self, idx):
- """
- Parse in property
- """
- if self.__is_token(self._css, idx):
- if (self._css[idx] == ':' or self._css[idx] == '=') and self._property != '':
- self._status = 'iv'
-
- if not self.getSetting('discard_invalid_properties') or self.__property_is_valid(self._property):
- self.__add_token(data.PROPERTY, self._property)
-
- elif self._css[idx] == '/' and self._css[idx+1] == '*' and self._property == '':
- self._status = 'ic'
- self._from = 'ip'
- return 1
-
- elif self._css[idx] == '}':
- self.__explode_selectors()
- self._status = 'is'
- self._invalid_at = False
- self.__add_token(data.SEL_END, self._selector)
- self._selector = ''
- self._property = ''
-
- elif self._css[idx] == ';':
- self._property = ''
-
- elif self._css[idx] == '\\':
- self._property += self.__unicode(idx)
-
- elif not self._css[idx].isspace():
- self._property += self._css[idx]
-
- return 0
-
- def __parseStatus_iv(self, idx):
- """
- Parse in value
- """
- pn = (( self._css[idx] == "\n" or self._css[idx] == "\r") and self.__property_is_next(idx+1) or idx == len(self._css)) #CHECK#
- if self.__is_token(self._css, idx) or pn:
- if self._css[idx] == '/' and self._css[idx+1] == '*':
- self._status = 'ic'
- self._from = 'iv'
- return 1
-
- elif self._css[idx] == '"' or self._css[idx] == "'" or self._css[idx] == '(':
- self._cur_string = self._css[idx]
- self._str_char = ')' if self._css[idx] == '(' else self._css[idx]
- self._status = 'instr'
- self._from = 'iv'
-
- elif self._css[idx] == ',':
- self._sub_value = self._sub_value.strip() + ','
-
- elif self._css[idx] == '\\':
- self._sub_value += self.__unicode(idx)
-
- elif self._css[idx] == ';' or pn:
- if len(self._selector) > 0 and self._selector[0] == '@' and data.at_rules.has_key(self._selector[1:]) and data.at_rules[self._selector[1:]] == 'iv':
- self._sub_value_arr.append(self._sub_value.strip())
-
- self._status = 'is'
-
- if '@charset' in self._selector:
- self._charset = self._sub_value_arr[0]
-
- elif '@namespace' in self._selector:
- self._namespace = ' '.join(self._sub_value_arr)
-
- elif '@import' in self._selector:
- self._import.append(' '.join(self._sub_value_arr))
-
-
- self._sub_value_arr = []
- self._sub_value = ''
- self._selector = ''
- self._sel_separate = []
-
- else:
- self._status = 'ip'
-
- elif self._css[idx] != '}':
- self._sub_value += self._css[idx]
-
- if (self._css[idx] == '}' or self._css[idx] == ';' or pn) and self._selector != '':
- if self._at == '':
- self._at = data.DEFAULT_AT
-
- #case settings
- if self.getSetting('lowercase_s'):
- self._selector = self._selector.lower()
-
- self._property = self._property.lower()
-
- if self._sub_value != '':
- self._sub_value_arr.append(self._sub_value)
- self._sub_value = ''
-
- self._value = ' '.join(self._sub_value_arr)
-
-
- self._selector = self._selector.strip()
-
- valid = self.__property_is_valid(self._property)
-
- if (not self._invalid_at or self.getSetting('preserve_css')) and (not self.getSetting('discard_invalid_properties') or valid):
- self.__css_add_property(self._at, self._selector, self._property, self._value)
- self.__add_token(data.VALUE, self._value)
-
- if not valid:
- if self.getSetting('discard_invalid_properties'):
- self.log('Removed invalid property: ' + self._property, 'Warning')
-
- else:
- self.log('Invalid property in ' + self.getSetting('css_level').upper() + ': ' + self._property, 'Warning')
-
- self._property = '';
- self._sub_value_arr = []
- self._value = ''
-
- if self._css[idx] == '}':
- self.__explode_selectors()
- self.__add_token(data.SEL_END, self._selector)
- self._status = 'is'
- self._invalid_at = False
- self._selector = ''
-
- elif not pn:
- self._sub_value += self._css[idx]
-
- if self._css[idx].isspace():
- if self._sub_value != '':
- self._sub_value_arr.append(self._sub_value)
- self._sub_value = ''
-
- return 0
-
- def __parseStatus_instr(self, idx):
- """
- Parse in String
- """
- if self._str_char == ')' and (self._css[idx] == '"' or self._css[idx] == "'") and not self.escaped(self._css, idx):
- self._str_in_str = not self._str_in_str
-
- temp_add = self._css[idx] # ...and no not-escaped backslash at the previous position
- if (self._css[idx] == "\n" or self._css[idx] == "\r") and not (self._css[idx-1] == '\\' and not self.escaped(self._css, idx-1)):
- temp_add = "\\A "
- self.log('Fixed incorrect newline in string', 'Warning')
-
- if not (self._str_char == ')' and self._css[idx].isspace() and not self._str_in_str):
- self._cur_string += temp_add
-
- if self._css[idx] == self._str_char and not self.escaped(self._css, idx) and not self._str_in_str:
- self._status = self._from
- regex = re.compile(r'([\s]+)', re.I | re.U | re.S)
- if regex.match(self._cur_string) is None and self._property != 'content':
- if self._str_char == '"' or self._str_char == "'":
- self._cur_string = self._cur_string[1:-1]
-
- elif len(self._cur_string) > 3 and (self._cur_string[1] == '"' or self._cur_string[1] == "'"):
- self._cur_string = self._cur_string[0] + self._cur_string[2:-2] + self._cur_string[-1]
-
- if self._from == 'iv':
- self._sub_value += self._cur_string
-
- elif self._from == 'is':
- self._selector += self._cur_string
-
- return 0
-
- def __parseStatus_ic(self, idx):
- """
- Parse css In Comment
- """
- if self._css[idx] == '*' and self._css[idx+1] == '/':
- self._status = self._from
- self.__add_token(data.COMMENT, self._curComment)
- self._curComment = ''
- return 1
-
- else:
- self._curComment += self._css[idx]
-
- return 0
-
- def __parseStatus_at(self, idx):
- """
- Parse in at-block
- """
- if self.__is_token(string, idx):
- if self._css[idx] == '/' and self._css[idx+1] == '*':
- self._status = 'ic'
- self._from = 'at'
- return 1
-
- elif self._css[i] == '{':
- self._status = 'is'
- self.__add_token(data.AT_START, self._at)
-
- elif self._css[i] == ',':
- self._at = self._at.strip() + ','
-
- elif self._css[i] == '\\':
- self._at += self.__unicode(i)
- else:
- lastpos = len(self._at)-1
- if not (self._at[lastpos].isspace() or self.__is_token(self._at, lastpos) and self._at[lastpos] == ',') and self._css[i].isspace():
- self._at += self._css[i]
-
- return 0
-
- def __explode_selectors(self):
- #Explode multiple selectors
- if self.getSetting('merge_selectors') == 1:
- new_sels = []
- lastpos = 0;
- self._sel_separate.append(len(self._selector))
-
- for num in xrange(len(self._sel_separate)):
- pos = self._sel_separate[num]
- if num == (len(self._sel_separate)): #CHECK#
- pos += 1
-
- new_sels.append(self._selector[lastpos:(pos-lastpos-1)])
- lastpos = pos
-
- if len(new_sels) > 1:
- for selector in new_sels:
- self.merge_css_blocks(self._at, selector, self._raw_css[self._at][self._selector])
-
- del self._raw_css[self._at][self._selector]
-
- self._sel_separate = []
-
- #Adds a property with value to the existing CSS code
- def __css_add_property(self, media, selector, prop, new_val):
- if self.getSetting('preserve_css') or new_val.strip() == '':
- return
-
- if not self._raw_css.has_key(media):
- self._raw_css[media] = SortedDict()
-
- if not self._raw_css[media].has_key(selector):
- self._raw_css[media][selector] = SortedDict()
-
- self._added = True
- if self._raw_css[media][selector].has_key(prop):
- if (self.is_important(self._raw_css[media][selector][prop]) and self.is_important(new_val)) or not self.is_important(self._raw_css[media][selector][prop]):
- del self._raw_css[media][selector][prop]
- self._raw_css[media][selector][prop] = new_val.strip()
-
- else:
- self._raw_css[media][selector][prop] = new_val.strip()
-
- #Checks if the next word in a string from pos is a CSS property
- def __property_is_next(self, pos):
- istring = self._css[pos: len(self._css)]
- pos = istring.find(':')
- if pos == -1:
- return False;
-
- istring = istring[:pos].strip().lower()
- if data.all_properties.has_key(istring):
- self.log('Added semicolon to the end of declaration', 'Warning')
- return True
-
- return False;
-
- #Checks if a property is valid
- def __property_is_valid(self, prop):
- return (data.all_properties.has_key(prop) and data.all_properties[prop].find(self.getSetting('css_level').upper()) != -1)
-
- #Adds a token to self._tokens
- def __add_token(self, ttype, cssdata, do=False):
- if self.getSetting('preserve_css') or do:
- if ttype == data.COMMENT:
- token = [ttype, cssdata]
- else:
- token = [ttype, cssdata.strip()]
-
- self._tokens.append(token)
-
- #Parse unicode notations and find a replacement character
- def __unicode(self, idx):
- ##FIX##
- return ''
-
- #Starts parsing from URL
- ##USED?
- def __parse_from_url(self, url):
- try:
- if "http" in url.lower() or "https" in url.lower():
- f = urllib.urlopen(url)
- else:
- f = open(url)
-
- data = f.read()
- return self.parse(data)
- finally:
- f.close()
-
- #Checks if there is a token at the current position
- def __is_token(self, string, idx):
- return (string[idx] in data.tokens and not self.escaped(string, idx))
-
-
- #Property Methods
- def _getOutput(self):
- self._output.prepare(self._optimized_css)
- return self._output.render
-
- def _getLog(self):
- ret = ""
- ks = self._log.keys()
- ks.sort()
- for line in ks:
- for msg in self._log[line]:
- ret += "Type: " + msg['t'] + "\n"
- ret += "Message: " + msg['m'] + "\n"
- ret += "\n"
-
- return ret
-
- def _getCSS(self):
- return self._css
-
-
- #Properties
- Output = property(_getOutput, None)
- Log = property(_getLog, None)
- CSS = property(_getCSS, None)
-
-
-if __name__ == '__main__':
- import sys
- tidy = CSSTidy()
- f = open(sys.argv[1], "r")
- css = f.read()
- f.close()
- tidy.parse(css)
- tidy.Output('file', filename="Stylesheet.min.css")
- print tidy.Output()
- #print tidy._import
\ No newline at end of file
+++ /dev/null
-# Various CSS Data for CSSTidy
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation; either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Florian Schmitz (floele at gmail dot com) 2005
-
-AT_START = 1
-AT_END = 2
-SEL_START = 3
-SEL_END = 4
-PROPERTY = 5
-VALUE = 6
-COMMENT = 7
-DEFAULT_AT = 41
-
-# All whitespace allowed in CSS
-#
-# @global array whitespace
-# @version 1.0
-whitespace = frozenset([' ',"\n","\t","\r","\x0B"])
-
-# All CSS tokens used by csstidy
-#
-# @global string tokens
-# @version 1.0
-tokens = '/@}{;:=\'"(,\\!$%&)#+.<>?[]^`|~'
-
-# All CSS units (CSS 3 units included)
-#
-# @see compress_numbers()
-# @global array units
-# @version 1.0
-units = frozenset(['in','cm','mm','pt','pc','px','rem','em','%','ex','gd','vw','vh','vm','deg','grad','rad','ms','s','khz','hz'])
-
-# Available at-rules
-#
-# @global array at_rules
-# @version 1.0
-at_rules = {'page':'is', 'font-face':'is', 'charset':'iv', 'import':'iv', 'namespace':'iv', 'media':'at'}
-
-# Properties that need a value with unit
-#
-# @todo CSS3 properties
-# @see compress_numbers()
-# @global array unit_values
-# @version 1.2
-unit_values = frozenset(['background', 'background-position', 'border', 'border-top', 'border-right', 'border-bottom',
- 'border-left', 'border-width', 'border-top-width', 'border-right-width', 'border-left-width',
- 'border-bottom-width', 'bottom', 'border-spacing', 'font-size','height', 'left', 'margin', 'margin-top',
- 'margin-right', 'margin-bottom', 'margin-left', 'max-height', 'max-width', 'min-height', 'min-width',
- 'outline-width', 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left','position',
- 'right', 'top', 'text-indent', 'letter-spacing', 'word-spacing', 'width'
- ])
-
-
-# Properties that allow <color> as value
-#
-# @todo CSS3 properties
-# @see compress_numbers()
-# @global array color_values
-# @version 1.0
-color_values = frozenset(['background-color', 'border-color', 'border-top-color', 'border-right-color',
- 'border-bottom-color', 'border-left-color', 'color', 'outline-color'])
-
-
-# Default values for the background properties
-#
-# @todo Possibly property names will change during CSS3 development
-# @global array background_prop_default
-# @see dissolve_short_bg()
-# @see merge_bg()
-# @version 1.0
-background_prop_default = {}
-background_prop_default['background-image'] = 'none'
-background_prop_default['background-size'] = 'auto'
-background_prop_default['background-repeat'] = 'repeat'
-background_prop_default['background-position'] = '0 0'
-background_prop_default['background-attachment'] = 'scroll'
-background_prop_default['background-clip'] = 'border'
-background_prop_default['background-origin'] = 'padding'
-background_prop_default['background-color'] = 'transparent'
-
-# A list of non-W3C color names which get replaced by their hex-codes
-#
-# @global array replace_colors
-# @see cut_color()
-# @version 1.0
-replace_colors = {}
-replace_colors['aliceblue'] = '#F0F8FF'
-replace_colors['antiquewhite'] = '#FAEBD7'
-replace_colors['aquamarine'] = '#7FFFD4'
-replace_colors['azure'] = '#F0FFFF'
-replace_colors['beige'] = '#F5F5DC'
-replace_colors['bisque'] = '#FFE4C4'
-replace_colors['blanchedalmond'] = '#FFEBCD'
-replace_colors['blueviolet'] = '#8A2BE2'
-replace_colors['brown'] = '#A52A2A'
-replace_colors['burlywood'] = '#DEB887'
-replace_colors['cadetblue'] = '#5F9EA0'
-replace_colors['chartreuse'] = '#7FFF00'
-replace_colors['chocolate'] = '#D2691E'
-replace_colors['coral'] = '#FF7F50'
-replace_colors['cornflowerblue'] = '#6495ED'
-replace_colors['cornsilk'] = '#FFF8DC'
-replace_colors['crimson'] = '#DC143C'
-replace_colors['cyan'] = '#00FFFF'
-replace_colors['darkblue'] = '#00008B'
-replace_colors['darkcyan'] = '#008B8B'
-replace_colors['darkgoldenrod'] = '#B8860B'
-replace_colors['darkgray'] = '#A9A9A9'
-replace_colors['darkgreen'] = '#006400'
-replace_colors['darkkhaki'] = '#BDB76B'
-replace_colors['darkmagenta'] = '#8B008B'
-replace_colors['darkolivegreen'] = '#556B2F'
-replace_colors['darkorange'] = '#FF8C00'
-replace_colors['darkorchid'] = '#9932CC'
-replace_colors['darkred'] = '#8B0000'
-replace_colors['darksalmon'] = '#E9967A'
-replace_colors['darkseagreen'] = '#8FBC8F'
-replace_colors['darkslateblue'] = '#483D8B'
-replace_colors['darkslategray'] = '#2F4F4F'
-replace_colors['darkturquoise'] = '#00CED1'
-replace_colors['darkviolet'] = '#9400D3'
-replace_colors['deeppink'] = '#FF1493'
-replace_colors['deepskyblue'] = '#00BFFF'
-replace_colors['dimgray'] = '#696969'
-replace_colors['dodgerblue'] = '#1E90FF'
-replace_colors['feldspar'] = '#D19275'
-replace_colors['firebrick'] = '#B22222'
-replace_colors['floralwhite'] = '#FFFAF0'
-replace_colors['forestgreen'] = '#228B22'
-replace_colors['gainsboro'] = '#DCDCDC'
-replace_colors['ghostwhite'] = '#F8F8FF'
-replace_colors['gold'] = '#FFD700'
-replace_colors['goldenrod'] = '#DAA520'
-replace_colors['greenyellow'] = '#ADFF2F'
-replace_colors['honeydew'] = '#F0FFF0'
-replace_colors['hotpink'] = '#FF69B4'
-replace_colors['indianred'] = '#CD5C5C'
-replace_colors['indigo'] = '#4B0082'
-replace_colors['ivory'] = '#FFFFF0'
-replace_colors['khaki'] = '#F0E68C'
-replace_colors['lavender'] = '#E6E6FA'
-replace_colors['lavenderblush'] = '#FFF0F5'
-replace_colors['lawngreen'] = '#7CFC00'
-replace_colors['lemonchiffon'] = '#FFFACD'
-replace_colors['lightblue'] = '#ADD8E6'
-replace_colors['lightcoral'] = '#F08080'
-replace_colors['lightcyan'] = '#E0FFFF'
-replace_colors['lightgoldenrodyellow'] = '#FAFAD2'
-replace_colors['lightgrey'] = '#D3D3D3'
-replace_colors['lightgreen'] = '#90EE90'
-replace_colors['lightpink'] = '#FFB6C1'
-replace_colors['lightsalmon'] = '#FFA07A'
-replace_colors['lightseagreen'] = '#20B2AA'
-replace_colors['lightskyblue'] = '#87CEFA'
-replace_colors['lightslateblue'] = '#8470FF'
-replace_colors['lightslategray'] = '#778899'
-replace_colors['lightsteelblue'] = '#B0C4DE'
-replace_colors['lightyellow'] = '#FFFFE0'
-replace_colors['limegreen'] = '#32CD32'
-replace_colors['linen'] = '#FAF0E6'
-replace_colors['magenta'] = '#FF00FF'
-replace_colors['mediumaquamarine'] = '#66CDAA'
-replace_colors['mediumblue'] = '#0000CD'
-replace_colors['mediumorchid'] = '#BA55D3'
-replace_colors['mediumpurple'] = '#9370D8'
-replace_colors['mediumseagreen'] = '#3CB371'
-replace_colors['mediumslateblue'] = '#7B68EE'
-replace_colors['mediumspringgreen'] = '#00FA9A'
-replace_colors['mediumturquoise'] = '#48D1CC'
-replace_colors['mediumvioletred'] = '#C71585'
-replace_colors['midnightblue'] = '#191970'
-replace_colors['mintcream'] = '#F5FFFA'
-replace_colors['mistyrose'] = '#FFE4E1'
-replace_colors['moccasin'] = '#FFE4B5'
-replace_colors['navajowhite'] = '#FFDEAD'
-replace_colors['oldlace'] = '#FDF5E6'
-replace_colors['olivedrab'] = '#6B8E23'
-replace_colors['orangered'] = '#FF4500'
-replace_colors['orchid'] = '#DA70D6'
-replace_colors['palegoldenrod'] = '#EEE8AA'
-replace_colors['palegreen'] = '#98FB98'
-replace_colors['paleturquoise'] = '#AFEEEE'
-replace_colors['palevioletred'] = '#D87093'
-replace_colors['papayawhip'] = '#FFEFD5'
-replace_colors['peachpuff'] = '#FFDAB9'
-replace_colors['peru'] = '#CD853F'
-replace_colors['pink'] = '#FFC0CB'
-replace_colors['plum'] = '#DDA0DD'
-replace_colors['powderblue'] = '#B0E0E6'
-replace_colors['rosybrown'] = '#BC8F8F'
-replace_colors['royalblue'] = '#4169E1'
-replace_colors['saddlebrown'] = '#8B4513'
-replace_colors['salmon'] = '#FA8072'
-replace_colors['sandybrown'] = '#F4A460'
-replace_colors['seagreen'] = '#2E8B57'
-replace_colors['seashell'] = '#FFF5EE'
-replace_colors['sienna'] = '#A0522D'
-replace_colors['skyblue'] = '#87CEEB'
-replace_colors['slateblue'] = '#6A5ACD'
-replace_colors['slategray'] = '#708090'
-replace_colors['snow'] = '#FFFAFA'
-replace_colors['springgreen'] = '#00FF7F'
-replace_colors['steelblue'] = '#4682B4'
-replace_colors['tan'] = '#D2B48C'
-replace_colors['thistle'] = '#D8BFD8'
-replace_colors['tomato'] = '#FF6347'
-replace_colors['turquoise'] = '#40E0D0'
-replace_colors['violet'] = '#EE82EE'
-replace_colors['violetred'] = '#D02090'
-replace_colors['wheat'] = '#F5DEB3'
-replace_colors['whitesmoke'] = '#F5F5F5'
-replace_colors['yellowgreen'] = '#9ACD32'
-
-#A list of optimized colors
-optimize_colors = {}
-optimize_colors['black'] = '#000'
-optimize_colors['fuchsia'] = '#F0F'
-optimize_colors['white'] = '#FFF'
-optimize_colors['yellow'] = '#FF0'
-optimize_colors['cyan'] = '#0FF'
-optimize_colors['magenta'] = '#F0F'
-optimize_colors['lightslategray'] = '#789'
-
-optimize_colors['#800000'] = 'maroon'
-optimize_colors['#FFA500'] = 'orange'
-optimize_colors['#808000'] = 'olive'
-optimize_colors['#800080'] = 'purple'
-optimize_colors['#008000'] = 'green'
-optimize_colors['#000080'] = 'navy'
-optimize_colors['#008080'] = 'teal'
-optimize_colors['#C0C0C0'] = 'silver'
-optimize_colors['#808080'] = 'gray'
-optimize_colors['#4B0082'] = 'indigo'
-optimize_colors['#FFD700'] = 'gold'
-optimize_colors['#A52A2A'] = 'brown'
-optimize_colors['#00FFFF'] = 'cyan'
-optimize_colors['#EE82EE'] = 'violet'
-optimize_colors['#DA70D6'] = 'orchid'
-optimize_colors['#FFE4C4'] = 'bisque'
-optimize_colors['#F0E68C'] = 'khaki'
-optimize_colors['#F5DEB3'] = 'wheat'
-optimize_colors['#FF7F50'] = 'coral'
-optimize_colors['#F5F5DC'] = 'beige'
-optimize_colors['#F0FFFF'] = 'azure'
-optimize_colors['#A0522D'] = 'sienna'
-optimize_colors['#CD853F'] = 'peru'
-optimize_colors['#FFFFF0'] = 'ivory'
-optimize_colors['#DDA0DD'] = 'plum'
-optimize_colors['#D2B48C'] = 'tan'
-optimize_colors['#FFC0CB'] = 'pink'
-optimize_colors['#FFFAFA'] = 'snow'
-optimize_colors['#FA8072'] = 'salmon'
-optimize_colors['#FF6347'] = 'tomato'
-optimize_colors['#FAF0E6'] = 'linen'
-optimize_colors['#F00'] = 'red'
-
-
-# A list of all shorthand properties that are devided into four properties and/or have four subvalues
-#
-# @global array shorthands
-# @todo Are there new ones in CSS3?
-# @see dissolve_4value_shorthands()
-# @see merge_4value_shorthands()
-# @version 1.0
-shorthands = {}
-shorthands['border-color'] = ['border-top-color','border-right-color','border-bottom-color','border-left-color']
-shorthands['border-style'] = ['border-top-style','border-right-style','border-bottom-style','border-left-style']
-shorthands['border-width'] = ['border-top-width','border-right-width','border-bottom-width','border-left-width']
-shorthands['margin'] = ['margin-top','margin-right','margin-bottom','margin-left']
-shorthands['padding'] = ['padding-top','padding-right','padding-bottom','padding-left']
-shorthands['-moz-border-radius'] = 0
-
-# All CSS Properties. Needed for csstidy::property_is_next()
-#
-# @global array all_properties
-# @todo Add CSS3 properties
-# @version 1.0
-# @see csstidy::property_is_next()
-all_properties = {}
-all_properties['background'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-color'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-image'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-repeat'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-attachment'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['background-position'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-right'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-color'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-bottom-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-left-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-right-color'] = 'CSS2.0,CSS2.1'
-all_properties['border-style'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-right-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-left-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-bottom-style'] = 'CSS2.0,CSS2.1'
-all_properties['border-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-top-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-right-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-left-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-bottom-width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['border-collapse'] = 'CSS2.0,CSS2.1'
-all_properties['border-spacing'] = 'CSS2.0,CSS2.1'
-all_properties['bottom'] = 'CSS2.0,CSS2.1'
-all_properties['caption-side'] = 'CSS2.0,CSS2.1'
-all_properties['content'] = 'CSS2.0,CSS2.1'
-all_properties['clear'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['clip'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['color'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['counter-reset'] = 'CSS2.0,CSS2.1'
-all_properties['counter-increment'] = 'CSS2.0,CSS2.1'
-all_properties['cursor'] = 'CSS2.0,CSS2.1'
-all_properties['empty-cells'] = 'CSS2.0,CSS2.1'
-all_properties['display'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['direction'] = 'CSS2.0,CSS2.1'
-all_properties['float'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-family'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-style'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-variant'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-weight'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['font-stretch'] = 'CSS2.0'
-all_properties['font-size-adjust'] = 'CSS2.0'
-all_properties['font-size'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['height'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['line-height'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style-type'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style-image'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['list-style-position'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-right'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['margin-left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['marks'] = 'CSS1.0,CSS2.0'
-all_properties['marker-offset'] = 'CSS2.0'
-all_properties['max-height'] = 'CSS2.0,CSS2.1'
-all_properties['max-width'] = 'CSS2.0,CSS2.1'
-all_properties['min-height'] = 'CSS2.0,CSS2.1'
-all_properties['min-width'] = 'CSS2.0,CSS2.1'
-all_properties['overflow'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['orphans'] = 'CSS2.0,CSS2.1'
-all_properties['outline'] = 'CSS2.0,CSS2.1'
-all_properties['outline-width'] = 'CSS2.0,CSS2.1'
-all_properties['outline-style'] = 'CSS2.0,CSS2.1'
-all_properties['outline-color'] = 'CSS2.0,CSS2.1'
-all_properties['padding'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-right'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-bottom'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['padding-left'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['page-break-before'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['page-break-after'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['page-break-inside'] = 'CSS2.0,CSS2.1'
-all_properties['page'] = 'CSS2.0'
-all_properties['position'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['quotes'] = 'CSS2.0,CSS2.1'
-all_properties['right'] = 'CSS2.0,CSS2.1'
-all_properties['size'] = 'CSS1.0,CSS2.0'
-all_properties['speak-header'] = 'CSS2.0,CSS2.1'
-all_properties['table-layout'] = 'CSS2.0,CSS2.1'
-all_properties['top'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-indent'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-align'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-decoration'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-shadow'] = 'CSS2.0'
-all_properties['letter-spacing'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['word-spacing'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['text-transform'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['white-space'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['unicode-bidi'] = 'CSS2.0,CSS2.1'
-all_properties['vertical-align'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['visibility'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['width'] = 'CSS1.0,CSS2.0,CSS2.1'
-all_properties['widows'] = 'CSS2.0,CSS2.1'
-all_properties['z-index'] = 'CSS1.0,CSS2.0,CSS2.1'
-
-# Speech #
-all_properties['volume'] = 'CSS2.0,CSS2.1'
-all_properties['speak'] = 'CSS2.0,CSS2.1'
-all_properties['pause'] = 'CSS2.0,CSS2.1'
-all_properties['pause-before'] = 'CSS2.0,CSS2.1'
-all_properties['pause-after'] = 'CSS2.0,CSS2.1'
-all_properties['cue'] = 'CSS2.0,CSS2.1'
-all_properties['cue-before'] = 'CSS2.0,CSS2.1'
-all_properties['cue-after'] = 'CSS2.0,CSS2.1'
-all_properties['play-during'] = 'CSS2.0,CSS2.1'
-all_properties['azimuth'] = 'CSS2.0,CSS2.1'
-all_properties['elevation'] = 'CSS2.0,CSS2.1'
-all_properties['speech-rate'] = 'CSS2.0,CSS2.1'
-all_properties['voice-family'] = 'CSS2.0,CSS2.1'
-all_properties['pitch'] = 'CSS2.0,CSS2.1'
-all_properties['pitch-range'] = 'CSS2.0,CSS2.1'
-all_properties['stress'] = 'CSS2.0,CSS2.1'
-all_properties['richness'] = 'CSS2.0,CSS2.1'
-all_properties['speak-punctuation'] = 'CSS2.0,CSS2.1'
-all_properties['speak-numeral'] = 'CSS2.0,CSS2.1'
\ No newline at end of file
+++ /dev/null
-# CSSTidy - CSS Optimizer
-#
-# CSS Optimizer class
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
-
-import data
-from tools import SortedDict
-
-
-class CSSOptimizer(object):
- def __init__(self, parser):
- #raw_css is a dict
- self.parser = parser
- self._optimized_css = SortedDict
-
-
-#PUBLIC METHODS
- def optimize(self, raw_css):
- if self.parser.getSetting('preserve_css'):
- return raw_css
-
- self._optimized_css = raw_css
-
- if self.parser.getSetting('merge_selectors') == 2:
- self.__merge_selectors()
-
- ##OPTIMIZE##
- for media, css in self._optimized_css.iteritems():
- for selector, cssdata in css.iteritems():
- if self.parser.getSetting('optimise_shorthands') >= 1:
- cssdata = self.__merge_4value_shorthands(cssdata)
-
- if self.parser.getSetting('optimise_shorthands') >= 2:
- cssdata = self.__merge_bg(cssdata)
-
- for item, value in cssdata.iteritems():
- value = self.__compress_numbers(item, value)
- value = self.__compress_important(value)
-
- if item in data.color_values and self.parser.getSetting('compress_colors'):
- old = value[:]
- value = self.__compress_color(value)
- if old != value:
- self.parser.log('In "' + selector + '" Optimised ' + item + ': Changed ' + old + ' to ' + value, 'Information')
-
- if item == 'font-weight' and self.parser.getSetting('compress_font-weight'):
- if value == 'bold':
- value = '700'
- self.parser.log('In "' + selector + '" Optimised font-weight: Changed "bold" to "700"', 'Information')
-
- elif value == 'normal':
- value = '400'
- self.parser.log('In "' + selector + '" Optimised font-weight: Changed "normal" to "400"', 'Information')
-
- self._optimized_css[media][selector][item] = value
-
-
- return self._optimized_css
-
-
-#PRIVATE METHODS
- def __merge_bg(self, cssdata):
- """
- Merges all background properties
- @cssdata (dict) is a dictionary of the selector properties
- """
- #Max number of background images. CSS3 not yet fully implemented
- img = 1
- clr = 1
- bg_img_list = []
- if cssdata.has_key('background-image'):
- img = len(cssdata['background-image'].split(','))
- bg_img_list = self.parser.gvw_important(cssdata['background-image']).split(',')
-
- elif cssdata.has_key('background-color'):
- clr = len(cssdata['background-color'].split(','))
-
-
- number_of_values = max(img, clr, 1)
-
- new_bg_value = ''
- important = ''
-
- for i in xrange(number_of_values):
- for bg_property, default_value in data.background_prop_default.iteritems():
- #Skip if property does not exist
- if not cssdata.has_key(bg_property):
- continue
-
- cur_value = cssdata[bg_property]
-
- #Skip some properties if there is no background image
- if (len(bg_img_list) > i and bg_img_list[i] == 'none') and bg_property in frozenset(['background-size', 'background-position', 'background-attachment', 'background-repeat']):
- continue
-
- #Remove !important
- if self.parser.is_important(cur_value):
- important = ' !important'
- cur_value = self.parser.gvw_important(cur_value)
-
- #Do not add default values
- if cur_value == default_value:
- continue
-
- temp = cur_value.split(',')
-
- if len(temp) > i:
- if bg_property == 'background-size':
- new_bg_value += '(' + temp[i] + ') '
-
- else:
- new_bg_value += temp[i] + ' '
-
- new_bg_value = new_bg_value.strip()
- if i != (number_of_values-1):
- new_bg_value += ','
-
- #Delete all background-properties
- for bg_property, default_value in data.background_prop_default.iteritems():
- try:
- del cssdata[bg_property]
- except:
- pass
-
- #Add new background property
- if new_bg_value != '':
- cssdata['background'] = new_bg_value + important
-
- return cssdata
-
- def __merge_4value_shorthands(self, cssdata):
- """
- Merges Shorthand properties again, the opposite of dissolve_4value_shorthands()
- @cssdata (dict) is a dictionary of the selector properties
- """
- for key, value in data.shorthands.iteritems():
- important = ''
- if value != 0 and cssdata.has_key(value[0]) and cssdata.has_key(value[1]) and cssdata.has_key(value[2]) and cssdata.has_key(value[3]):
- cssdata[key] = ''
-
- for i in xrange(4):
- val = cssdata[value[i]]
- if self.parser.is_important(val):
- important = '!important'
- cssdata[key] += self.parser.gvw_important(val) + ' '
-
- else:
- cssdata[key] += val + ' '
-
- del cssdata[value[i]]
- if cssdata.has_key(key):
- cssdata[key] = self.__shorthand(cssdata[key] + important.strip())
-
- return cssdata
-
-
- def __merge_selectors(self):
- """
- Merges selectors with same properties. Example: a{color:red} b{color:red} . a,b{color:red}
- Very basic and has at least one bug. Hopefully there is a replacement soon.
- @selector_one (string) is the current selector
- @value_one (dict) is a dictionary of the selector properties
- Note: Currently is the elements of a selector are identical, but in a different order, they are not merged
- """
-
- ##OPTIMIZE##
- ##FIX##
-
- raw_css = self._optimized_css.copy()
- delete = []
- add = SortedDict()
- for media, css in raw_css.iteritems():
- for selector_one, value_one in css.iteritems():
- newsel = selector_one
-
- for selector_two, value_two in css.iteritems():
- if selector_one == selector_two:
- #We need to skip self
- continue
-
- if value_one == value_two:
- #Ok, we need to merge these two selectors
- newsel += ', ' + selector_two
- delete.append((media, selector_two))
-
-
- if not add.has_key(media):
- add[media] = SortedDict()
-
- add[media][newsel] = value_one
- delete.append((media, selector_one))
-
- for item in delete:
- try:
- del self._optimized_css[item[0]][item[1]]
- except:
- #Must have already been deleted
- continue
-
- for media, css in add.iteritems():
- self._optimized_css[media].update(css)
-
-
-
- def __shorthand(self, value):
- """
- Compresses shorthand values. Example: margin:1px 1px 1px 1px . margin:1px
- @value (string)
- """
-
- ##FIX##
-
- important = '';
- if self.parser.is_important(value):
- value_list = self.parser.gvw_important(value)
- important = '!important'
- else:
- value_list = value
-
- ret = value
- value_list = value_list.split(' ')
-
- if len(value_list) == 4:
- if value_list[0] == value_list[1] and value_list[0] == value_list[2] and value_list[0] == value_list[3]:
- ret = value_list[0] + important
-
- elif value_list[1] == value_list[3] and value_list[0] == value_list[2]:
- ret = value_list[0] + ' ' + value_list[1] + important
-
- elif value_list[1] == value_list[3]:
- ret = value_list[0] + ' ' + value_list[1] + ' ' + value_list[2] + important
-
- elif len(value_list) == 3:
- if value_list[0] == value_list[1] and value_list[0] == value_list[2]:
- ret = value_list[0] + important
-
- elif value_list[0] == value_list[2]:
- return value_list[0] + ' ' + value_list[1] + important
-
- elif len(value_list) == 2:
- if value_list[0] == value_list[1]:
- ret = value_list[0] + important
-
- if ret != value:
- self.parser.log('Optimised shorthand notation: Changed "' + value + '" to "' + ret + '"', 'Information')
-
- return ret
-
- def __compress_important(self, value):
- """
- Removes unnecessary whitespace in ! important
- @value (string)
- """
- if self.parser.is_important(value):
- value = self.parser.gvw_important(value) + '!important'
-
- return value
-
- def __compress_numbers(self, prop, value):
- """
- Compresses numbers (ie. 1.0 becomes 1 or 1.100 becomes 1.1 )
- @value (string) is the posible number to be compressed
- """
-
- ##FIX##
-
- value = value.split('/')
-
- for l in xrange(len(value)):
- #continue if no numeric value
- if not (len(value[l]) > 0 and (value[l][0].isdigit() or value[l][0] in ('+', '-') )):
- continue
-
- #Fix bad colors
- if prop in data.color_values:
- value[l] = '#' + value[l]
-
- is_floatable = False
- try:
- float(value[l])
- is_floatable = True
- except:
- pass
-
- if is_floatable and float(value[l]) == 0:
- value[l] = '0'
-
- elif value[l][0] != '#':
- unit_found = False
- for unit in data.units:
- pos = value[l].lower().find(unit)
- if pos != -1 and prop not in data.shorthands:
- value[l] = self.__remove_leading_zeros(float(value[l][:pos])) + unit
- unit_found = True
- break;
-
- if not unit_found and prop in data.unit_values and prop not in data.shorthands:
- value[l] = self.__remove_leading_zeros(float(value[l])) + 'px'
-
- elif not unit_found and prop not in data.shorthands:
- value[l] = self.__remove_leading_zeros(float(value[l]))
-
-
- if len(value) > 1:
- return '/'.join(value)
-
- return value[0]
-
- def __remove_leading_zeros(self, float_val):
- """
- Removes the leading zeros from a float value
- @float_val (float)
- @returns (string)
- """
- #Remove leading zero
- if abs(float_val) < 1:
- if float_val < 0:
- float_val = '-' . str(float_val)[2:]
- else:
- float_val = str(float_val)[1:]
-
- return str(float_val)
-
- def __compress_color(self, color):
- """
- Color compression function. Converts all rgb() values to #-values and uses the short-form if possible. Also replaces 4 color names by #-values.
- @color (string) the {posible} color to change
- """
-
- #rgb(0,0,0) . #000000 (or #000 in this case later)
- if color[:4].lower() == 'rgb(':
- color_tmp = color[4:(len(color)-5)]
- color_tmp = color_tmp.split(',')
-
- for c in color_tmp:
- c = c.strip()
- if c[:-1] == '%':
- c = round((255*color_tmp[i])/100)
-
- if color_tmp[i] > 255:
- color_tmp[i] = 255
-
- color = '#'
-
- for i in xrange(3):
- if color_tmp[i] < 16:
- color += '0' + str(hex(color_tmp[i])).replace('0x', '')
- else:
- color += str(hex(color_tmp[i])).replace('0x', '')
-
- #Fix bad color names
- if data.replace_colors.has_key(color.lower()):
- color = data.replace_colors[color.lower()]
-
- #aabbcc . #abc
- if len(color) == 7:
- color_temp = color.lower()
- if color_temp[0] == '#' and color_temp[1] == color_temp[2] and color_temp[3] == color_temp[4] and color_temp[5] == color_temp[6]:
- color = '#' + color[1] + color[3] + color[5]
-
- if data.optimize_colors.has_key(color.lower()):
- color = data.optimize_colors[color.lower()]
-
- return color
\ No newline at end of file
+++ /dev/null
-# CSSTidy - CSS Printer
-#
-# CSS Printer class
-#
-# This file is part of CSSTidy.
-#
-# CSSTidy is free software you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation either version 2 of the License, or
-# (at your option) any later version.
-#
-# CSSTidy is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with CSSTidy if not, write to the Free Software
-# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-#
-# @license http://opensource.org/licenses/gpl-license.php GNU Public License
-# @package csstidy
-# @author Dj Gilcrease (digitalxero at gmail dot com) 2005-2006
-
-import data
-
-class CSSPrinter(object):
- def __init__(self, parser):
- self.parser = parser
- self._css = {}
- self.__renderMethods = {'string': self.__renderString, 'file': self.__renderFile}
-
-#PUBLIC METHODS
- def prepare(self, css):
- self._css = css
-
- def render(self, output="string", *args, **kwargs):
- return self.__renderMethods[output](*args, **kwargs)
-
-#PRIVATE METHODS
- def __renderString(self, *args, **kwargs):
- ##OPTIMIZE##
- template = self.parser.getSetting('template')
- ret = ""
-
- if template == 'highest_compression':
- top_line_end = ""
- iner_line_end = ""
- bottom_line_end = ""
- indent = ""
-
- elif template == 'high_compression':
- top_line_end = "\n"
- iner_line_end = ""
- bottom_line_end = "\n"
- indent = ""
-
- elif template == 'default':
- top_line_end = "\n"
- iner_line_end = "\n"
- bottom_line_end = "\n\n"
- indent = ""
-
- elif template == 'low_compression':
- top_line_end = "\n"
- iner_line_end = "\n"
- bottom_line_end = "\n\n"
- indent = " "
-
- if self.parser.getSetting('timestamp'):
- ret += '/# CSSTidy ' + self.parser.version + ': ' + datetime.now().strftime("%a, %d %b %Y %H:%M:%S +0000") + ' #/' + top_line_end
-
- for item in self.parser._import:
- ret += '@import(' + item + ');' + top_line_end
-
- for item in self.parser._charset:
- ret += '@charset(' + item + ');' + top_line_end
-
- for item in self.parser._namespace:
- ret += '@namespace(' + item + ');' + top_line_end
-
- for media, css in self._css.iteritems():
- for selector, cssdata in css.iteritems():
- ret += selector + '{' + top_line_end
-
- for item, value in cssdata.iteritems():
- ret += indent + item + ':' + value + ';' + iner_line_end
-
- ret += '}' + bottom_line_end
-
- return ret
-
- def __renderFile(self, filename=None, *args, **kwargs):
- if filename is None:
- return self.__renderString()
-
- try:
- f = open(filename, "w")
- f.write(self.__renderString())
- finally:
- f.close()
\ No newline at end of file
+++ /dev/null
-
-class SortedDict(dict):
- """
- A dictionary that keeps its keys in the order in which they're inserted.
- """
- def __init__(self, data=None):
- if data is None:
- data = {}
- super(SortedDict, self).__init__(data)
- if isinstance(data, dict):
- self.keyOrder = data.keys()
- else:
- self.keyOrder = []
- for key, value in data:
- if key not in self.keyOrder:
- self.keyOrder.append(key)
-
- def __deepcopy__(self, memo):
- from copy import deepcopy
- return self.__class__([(key, deepcopy(value, memo))
- for key, value in self.iteritems()])
-
- def __setitem__(self, key, value):
- super(SortedDict, self).__setitem__(key, value)
- if key not in self.keyOrder:
- self.keyOrder.append(key)
-
- def __delitem__(self, key):
- super(SortedDict, self).__delitem__(key)
- self.keyOrder.remove(key)
-
- def __iter__(self):
- for k in self.keyOrder:
- yield k
-
- def pop(self, k, *args):
- result = super(SortedDict, self).pop(k, *args)
- try:
- self.keyOrder.remove(k)
- except ValueError:
- # Key wasn't in the dictionary in the first place. No problem.
- pass
- return result
-
- def popitem(self):
- result = super(SortedDict, self).popitem()
- self.keyOrder.remove(result[0])
- return result
-
- def items(self):
- return zip(self.keyOrder, self.values())
-
- def iteritems(self):
- for key in self.keyOrder:
- yield key, super(SortedDict, self).__getitem__(key)
-
- def keys(self):
- return self.keyOrder[:]
-
- def iterkeys(self):
- return iter(self.keyOrder)
-
- def values(self):
- return [super(SortedDict, self).__getitem__(k) for k in self.keyOrder]
-
- def itervalues(self):
- for key in self.keyOrder:
- yield super(SortedDict, self).__getitem__(key)
-
- def update(self, dict_):
- for k, v in dict_.items():
- self.__setitem__(k, v)
-
- def setdefault(self, key, default):
- if key not in self.keyOrder:
- self.keyOrder.append(key)
- return super(SortedDict, self).setdefault(key, default)
-
- def value_for_index(self, index):
- """Returns the value of the item at the given zero-based index."""
- return self[self.keyOrder[index]]
-
- def insert(self, index, key, value):
- """Inserts the key, value pair before the item with the given index."""
- if key in self.keyOrder:
- n = self.keyOrder.index(key)
- del self.keyOrder[n]
- if n < index:
- index -= 1
- self.keyOrder.insert(index, key)
- super(SortedDict, self).__setitem__(key, value)
-
- def copy(self):
- """Returns a copy of this object."""
- # This way of initializing the copy means it works for subclasses, too.
- obj = self.__class__(self)
- obj.keyOrder = self.keyOrder[:]
- return obj
-
- def __repr__(self):
- """
- Replaces the normal dict.__repr__ with a version that returns the keys
- in their sorted order.
- """
- return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()])
-
- def clear(self):
- super(SortedDict, self).clear()
- self.keyOrder = []
\ No newline at end of file
+++ /dev/null
-from compress.filters.jsmin.jsmin import jsmin
-from compress.filter_base import FilterBase
-
-class JSMinFilter(FilterBase):
- def filter_js(self, js):
- return jsmin(js)
\ No newline at end of file
+++ /dev/null
-#!/usr/bin/python
-
-# This code is original from jsmin by Douglas Crockford, it was translated to
-# Python by Baruch Even. The original code had the following copyright and
-# license.
-#
-# /* jsmin.c
-# 2007-05-22
-#
-# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy of
-# this software and associated documentation files (the "Software"), to deal in
-# the Software without restriction, including without limitation the rights to
-# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
-# of the Software, and to permit persons to whom the Software is furnished to do
-# so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in all
-# copies or substantial portions of the Software.
-#
-# The Software shall be used for Good, not Evil.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-# */
-
-from StringIO import StringIO
-
-def jsmin(js):
- ins = StringIO(js)
- outs = StringIO()
- JavascriptMinify().minify(ins, outs)
- str = outs.getvalue()
- if len(str) > 0 and str[0] == '\n':
- str = str[1:]
- return str
-
-def isAlphanum(c):
- """return true if the character is a letter, digit, underscore,
- dollar sign, or non-ASCII character.
- """
- return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
- (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
-
-class UnterminatedComment(Exception):
- pass
-
-class UnterminatedStringLiteral(Exception):
- pass
-
-class UnterminatedRegularExpression(Exception):
- pass
-
-class JavascriptMinify(object):
-
- def _outA(self):
- self.outstream.write(self.theA)
- def _outB(self):
- self.outstream.write(self.theB)
-
- def _get(self):
- """return the next character from stdin. Watch out for lookahead. If
- the character is a control character, translate it to a space or
- linefeed.
- """
- c = self.theLookahead
- self.theLookahead = None
- if c == None:
- c = self.instream.read(1)
- if c >= ' ' or c == '\n':
- return c
- if c == '': # EOF
- return '\000'
- if c == '\r':
- return '\n'
- return ' '
-
- def _peek(self):
- self.theLookahead = self._get()
- return self.theLookahead
-
- def _next(self):
- """get the next character, excluding comments. peek() is used to see
- if a '/' is followed by a '/' or '*'.
- """
- c = self._get()
- if c == '/':
- p = self._peek()
- if p == '/':
- c = self._get()
- while c > '\n':
- c = self._get()
- return c
- if p == '*':
- c = self._get()
- while 1:
- c = self._get()
- if c == '*':
- if self._peek() == '/':
- self._get()
- return ' '
- if c == '\000':
- raise UnterminatedComment()
-
- return c
-
- def _action(self, action):
- """do something! What you do is determined by the argument:
- 1 Output A. Copy B to A. Get the next B.
- 2 Copy B to A. Get the next B. (Delete A).
- 3 Get the next B. (Delete B).
- action treats a string as a single character. Wow!
- action recognizes a regular expression if it is preceded by ( or , or =.
- """
- if action <= 1:
- self._outA()
-
- if action <= 2:
- self.theA = self.theB
- if self.theA == "'" or self.theA == '"':
- while 1:
- self._outA()
- self.theA = self._get()
- if self.theA == self.theB:
- break
- if self.theA <= '\n':
- raise UnterminatedStringLiteral()
- if self.theA == '\\':
- self._outA()
- self.theA = self._get()
-
-
- if action <= 3:
- self.theB = self._next()
- if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
- self.theA == '=' or self.theA == ':' or
- self.theA == '[' or self.theA == '?' or
- self.theA == '!' or self.theA == '&' or
- self.theA == '|' or self.theA == ';' or
- self.theA == '{' or self.theA == '}' or
- self.theA == '\n'):
- self._outA()
- self._outB()
- while 1:
- self.theA = self._get()
- if self.theA == '/':
- break
- elif self.theA == '\\':
- self._outA()
- self.theA = self._get()
- elif self.theA <= '\n':
- raise UnterminatedRegularExpression()
- self._outA()
- self.theB = self._next()
-
-
- def _jsmin(self):
- """Copy the input to the output, deleting the characters which are
- insignificant to JavaScript. Comments will be removed. Tabs will be
- replaced with spaces. Carriage returns will be replaced with linefeeds.
- Most spaces and linefeeds will be removed.
- """
- self.theA = '\n'
- self._action(3)
-
- while self.theA != '\000':
- if self.theA == ' ':
- if isAlphanum(self.theB):
- self._action(1)
- else:
- self._action(2)
- elif self.theA == '\n':
- if self.theB in ['{', '[', '(', '+', '-']:
- self._action(1)
- elif self.theB == ' ':
- self._action(3)
- else:
- if isAlphanum(self.theB):
- self._action(1)
- else:
- self._action(2)
- else:
- if self.theB == ' ':
- if isAlphanum(self.theA):
- self._action(1)
- else:
- self._action(3)
- elif self.theB == '\n':
- if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
- self._action(1)
- else:
- if isAlphanum(self.theA):
- self._action(1)
- else:
- self._action(3)
- else:
- self._action(1)
-
- def minify(self, instream, outstream):
- self.instream = instream
- self.outstream = outstream
- self.theA = '\n'
- self.theB = None
- self.theLookahead = None
-
- self._jsmin()
- self.instream.close()
-
-if __name__ == '__main__':
- import sys
- jsm = JavascriptMinify()
- jsm.minify(sys.stdin, sys.stdout)
\ No newline at end of file
+++ /dev/null
-import subprocess
-
-from django.conf import settings
-
-from compress.filter_base import FilterBase, FilterError
-
-BINARY = getattr(settings, 'COMPRESS_YUI_BINARY', 'java -jar yuicompressor.jar')
-CSS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_CSS_ARGUMENTS', '')
-JS_ARGUMENTS = getattr(settings, 'COMPRESS_YUI_JS_ARGUMENTS', '')
-
-class YUICompressorFilter(FilterBase):
-
- def filter_common(self, content, type_, arguments):
- command = '%s --type=%s %s' % (BINARY, type_, arguments)
-
- if self.verbose:
- command += ' --verbose'
-
- p = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
- p.stdin.write(content)
- p.stdin.close()
-
- filtered_css = p.stdout.read()
- p.stdout.close()
-
- err = p.stderr.read()
- p.stderr.close()
-
- if p.wait() != 0:
- if not err:
- err = 'Unable to apply YUI Compressor filter'
-
- raise FilterError(err)
-
- if self.verbose:
- print err
-
- return filtered_css
-
- def filter_js(self, js):
- return self.filter_common(js, 'js', JS_ARGUMENTS)
-
- def filter_css(self, css):
- return self.filter_common(css, 'css', CSS_ARGUMENTS)
\ No newline at end of file
+++ /dev/null
-from django.core.management.base import NoArgsCommand
-from optparse import make_option
-
-from django.conf import settings
-
-class Command(NoArgsCommand):
- option_list = NoArgsCommand.option_list + (
- make_option('--force', action='store_true', default=False, help='Force update of all files, even if the source files are older than the current compressed file.'),
- make_option('--verbosity', action='store', dest='verbosity', default='1',
- type='choice', choices=['0', '1', '2'],
- help='Verbosity level; 0=minimal output, 1=normal output, 2=all output'),
- )
- help = 'Updates and compresses CSS and JavsScript on-demand, without restarting Django'
- args = ''
-
- def handle_noargs(self, **options):
-
- force = options.get('force', False)
- verbosity = int(options.get('verbosity', 1))
-
- from compress.utils import needs_update, filter_css, filter_js
-
- for name, css in settings.COMPRESS_CSS.items():
- u, version = needs_update(css['output_filename'], css['source_filenames'])
-
- if (force or u) or verbosity >= 2:
- msg = 'CSS Group \'%s\'' % name
- print msg
- print len(msg) * '-'
- print "Version: %s" % version
-
- if force or u:
- filter_css(css, verbosity)
-
- if (force or u) or verbosity >= 2:
- print
-
- for name, js in settings.COMPRESS_JS.items():
- u, version = needs_update(js['output_filename'], js['source_filenames'])
-
- if (force or u) or verbosity >= 2:
- msg = 'JavaScript Group \'%s\'' % name
- print msg
- print len(msg) * '-'
- print "Version: %s" % version
-
- if force or u:
- filter_js(js, verbosity)
-
- if (force or u) or verbosity >= 2:
- print
\ No newline at end of file
+++ /dev/null
-from django.dispatch import Signal
-
-css_filtered = Signal()
-js_filtered = Signal()
+++ /dev/null
-<link href="{{ url }}" rel="stylesheet" type="text/css" media="{{ media|default:"all" }}" />
\ No newline at end of file
+++ /dev/null
-<!--[if {{ condition|default:"IE" }}]><link href="{{ url }}" rel="stylesheet" type="text/css" media="{{ media|default:"all" }}" /><![endif]-->
\ No newline at end of file
+++ /dev/null
-<script type="text/javascript" src="{{ url }}"></script>
\ No newline at end of file
+++ /dev/null
-<!--[if {{ condition|default:"IE" }}]><script type="text/javascript" src="{{ url }}"></script><![endif]-->
\ No newline at end of file
+++ /dev/null
-import os
-
-from django import template
-
-from django.conf import settings as django_settings
-
-from compress.conf import settings
-from compress.utils import media_root, media_url, needs_update, filter_css, filter_js, get_output_filename, get_version
-
-register = template.Library()
-
-def render_common(template_name, obj, filename, version):
- if settings.COMPRESS:
- filename = get_output_filename(filename, version)
-
- context = obj.get('extra_context', {})
- context['url'] = media_url(filename)
-
- return template.loader.render_to_string(template_name, context)
-
-def render_css(css, filename, version=None):
- return render_common(css.get('template_name', 'compress/css.html'), css, filename, version)
-
-def render_js(js, filename, version=None):
- return render_common(js.get('template_name', 'compress/js.html'), js, filename, version)
-
-class CompressedCSSNode(template.Node):
- def __init__(self, name):
- self.name = name
-
- def render(self, context):
- css_name = template.Variable(self.name).resolve(context)
-
- try:
- css = settings.COMPRESS_CSS[css_name]
- except KeyError:
- return '' # fail silently, do not return anything if an invalid group is specified
-
- if settings.COMPRESS:
-
- version = None
-
- if settings.COMPRESS_AUTO:
- u, version = needs_update(css['output_filename'], css['source_filenames'])
- if u:
- filter_css(css)
-
- return render_css(css, css['output_filename'], version)
- else:
- # output source files
- r = ''
- for source_file in css['source_filenames']:
- r += render_css(css, source_file)
-
- return r
-
-class CompressedJSNode(template.Node):
- def __init__(self, name):
- self.name = name
-
- def render(self, context):
- js_name = template.Variable(self.name).resolve(context)
-
- try:
- js = settings.COMPRESS_JS[js_name]
- except KeyError:
- return '' # fail silently, do not return anything if an invalid group is specified
-
- if settings.COMPRESS:
-
- version = None
-
- if settings.COMPRESS_AUTO:
- u, version = needs_update(js['output_filename'], js['source_filenames'])
- if u:
- filter_js(js)
-
- return render_js(js, js['output_filename'], version)
- else:
- # output source files
- r = ''
- for source_file in js['source_filenames']:
- r += render_js(js, source_file)
- return r
-
-#@register.tag
-def compressed_css(parser, token):
- try:
- tag_name, name = token.split_contents()
- except ValueError:
- raise template.TemplateSyntaxError, '%r requires exactly one argument: the name of a group in the COMPRESS_CSS setting' % token.split_contents()[0]
-
- return CompressedCSSNode(name)
-compressed_css = register.tag(compressed_css)
-
-#@register.tag
-def compressed_js(parser, token):
- try:
- tag_name, name = token.split_contents()
- except ValueError:
- raise template.TemplateSyntaxError, '%r requires exactly one argument: the name of a group in the COMPRESS_JS setting' % token.split_contents()[0]
-
- return CompressedJSNode(name)
-compressed_js = register.tag(compressed_js)
+++ /dev/null
-import os
-import re
-import tempfile
-
-from django.conf import settings as django_settings
-from django.utils.http import urlquote
-from django.dispatch import dispatcher
-
-from compress.conf import settings
-from compress.signals import css_filtered, js_filtered
-
-def get_filter(compressor_class):
- """
- Convert a string version of a function name to the callable object.
- """
-
- if not hasattr(compressor_class, '__bases__'):
-
- try:
- compressor_class = compressor_class.encode('ascii')
- mod_name, class_name = get_mod_func(compressor_class)
- if class_name != '':
- compressor_class = getattr(__import__(mod_name, {}, {}, ['']), class_name)
- except (ImportError, AttributeError):
- raise Exception('Failed to import filter %s' % compressor_class)
-
- return compressor_class
-
-def get_mod_func(callback):
- """
- Converts 'django.views.news.stories.story_detail' to
- ('django.views.news.stories', 'story_detail')
- """
-
- try:
- dot = callback.rindex('.')
- except ValueError:
- return callback, ''
- return callback[:dot], callback[dot+1:]
-
-def needs_update(output_file, source_files):
- """
- Scan the source files for changes and returns True if the output_file needs to be updated.
- """
-
- mtime = max_mtime(source_files)
- version = get_version(mtime)
-
- compressed_file_full = media_root(get_output_filename(output_file, version))
-
- if not os.path.exists(compressed_file_full):
- return True, version
-
- # Check if the output file is outdated
- return (os.stat(compressed_file_full).st_mtime < mtime), mtime
-
-def media_root(filename):
- """
- Return the full path to ``filename``. ``filename`` is a relative path name in MEDIA_ROOT
- """
- return os.path.join(django_settings.MEDIA_ROOT, filename)
-
-def media_url(url):
- return django_settings.MEDIA_URL + urlquote(url)
-
-def concat(filenames, separator=''):
- """
- Concatenate the files from the list of the ``filenames``, ouput separated with ``separator``.
- """
- r = ''
-
- for filename in filenames:
- fd = open(media_root(filename), 'rb')
- r += fd.read()
- r += separator
- fd.close()
-
- return r
-
-def max_mtime(files):
- return int(max([os.stat(media_root(f)).st_mtime for f in files]))
-
-def save_file(filename, contents):
- fd = open(media_root(filename), 'wb+')
- fd.write(contents)
- fd.close()
-
-def get_output_filename(filename, version):
- if settings.COMPRESS_VERSION and version is not None:
- return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, get_version(version))
- else:
- return filename.replace(settings.COMPRESS_VERSION_PLACEHOLDER, settings.COMPRESS_VERSION_DEFAULT)
-
-def get_version(version):
- try:
- return str(int(version))
- except ValueError:
- return str(version)
-
-def remove_files(path, filename, verbosity=0):
- regex = re.compile(r'^%s$' % (os.path.basename(get_output_filename(settings.COMPRESS_VERSION_PLACEHOLDER.join([re.escape(part) for part in filename.split(settings.COMPRESS_VERSION_PLACEHOLDER)]), r'\d+'))))
-
- for f in os.listdir(path):
- if regex.match(f):
- if verbosity >= 1:
- print "Removing outdated file %s" % f
-
- os.unlink(os.path.join(path, f))
-
-def filter_common(obj, verbosity, filters, attr, separator, signal):
- output = concat(obj['source_filenames'], separator)
- filename = get_output_filename(obj['output_filename'], get_version(max_mtime(obj['source_filenames'])))
-
- if settings.COMPRESS_VERSION:
- remove_files(os.path.dirname(media_root(filename)), obj['output_filename'], verbosity)
-
- if verbosity >= 1:
- print "Saving %s" % filename
-
- for f in filters:
- output = getattr(get_filter(f)(verbose=(verbosity >= 2)), attr)(output)
-
- save_file(filename, output)
- signal.send(None)
-
-def filter_css(css, verbosity=0):
- return filter_common(css, verbosity, filters=settings.COMPRESS_CSS_FILTERS, attr='filter_css', separator='', signal=css_filtered)
-
-def filter_js(js, verbosity=0):
- return filter_common(js, verbosity, filters=settings.COMPRESS_JS_FILTERS, attr='filter_js', separator=';', signal=js_filtered)
+++ /dev/null
-# -*- coding: utf-8 -*-
-from django.contrib import admin
-from django import forms
-from django.utils.safestring import mark_safe
-from django.utils.translation import ugettext_lazy as _
-
-
-class FilteredSelectMultiple(forms.SelectMultiple):
- """
- A SelectMultiple with a JavaScript filter interface.
-
- Note that the resulting JavaScript assumes that the SelectFilter2.js
- library and its dependencies have been loaded in the HTML page.
- """
- def _media(self):
- from django.conf import settings
- js = ['js/SelectBox.js' , 'js/SelectFilter2.js']
- return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
- media = property(_media)
-
- def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
- self.verbose_name = verbose_name
- self.is_stacked = is_stacked
- super(FilteredSelectMultiple, self).__init__(attrs, choices)
-
- def render(self, name, value, attrs=None, choices=()):
- from django.conf import settings
- output = [super(FilteredSelectMultiple, self).render(name, value, attrs, choices)]
- output.append(u'<script type="text/javascript">addEvent(window, "load", function(e) {')
- # TODO: "id_" is hard-coded here. This should instead use the correct
- # API to determine the ID dynamically.
- output.append(u'SelectFilter.init("id_%s", "%s", %s, "%s"); });</script>\n' % \
- (name, self.verbose_name.replace('"', '\\"'), int(self.is_stacked), settings.ADMIN_MEDIA_PREFIX))
- return mark_safe(u''.join(output))
-
-
-class TaggableModelForm(forms.ModelForm):
- tags = forms.MultipleChoiceField(label=_('tags').capitalize(), required=True, widget=FilteredSelectMultiple(_('tags'), False))
-
- def __init__(self, *args, **kwargs):
- if 'instance' in kwargs:
- if 'initial' not in kwargs:
- kwargs['initial'] = {}
- kwargs['initial']['tags'] = [tag.id for tag in self.tag_model.objects.get_for_object(kwargs['instance'])]
- super(TaggableModelForm, self).__init__(*args, **kwargs)
- self.fields['tags'].choices = [(tag.id, tag.name) for tag in self.tag_model.objects.all()]
-
- def save(self, commit):
- obj = super(TaggableModelForm, self).save()
- tag_ids = self.cleaned_data['tags']
- tags = self.tag_model.objects.filter(pk__in=tag_ids)
- self.tag_model.objects.update_tags(obj, tags)
- return obj
-
- def save_m2m(self):
- # TODO: Shouldn't be needed
- pass
-
-
-class TaggableModelAdmin(admin.ModelAdmin):
- form = TaggableModelForm
-
- def get_form(self, request, obj=None):
- form = super(TaggableModelAdmin, self).get_form(request, obj)
- form.tag_model = self.tag_model
- return form
-
+++ /dev/null
-"""
-Custom managers for Django models registered with the tagging
-application.
-"""
-from django.contrib.contenttypes.models import ContentType
-from django.db import models
-
-
-class ModelTagManager(models.Manager):
- """
- A manager for retrieving tags for a particular model.
- """
- def __init__(self, tag_model):
- super(ModelTagManager, self).__init__()
- self.tag_model = tag_model
-
- def get_query_set(self):
- content_type = ContentType.objects.get_for_model(self.model)
- return self.tag_model.objects.filter(
- items__content_type__pk=content_type.pk).distinct()
-
- def related(self, tags, *args, **kwargs):
- return self.tag_model.objects.related_for_model(tags, self.model, *args, **kwargs)
-
- def usage(self, *args, **kwargs):
- return self.tag_model.objects.usage_for_model(self.model, *args, **kwargs)
-
-
-class ModelTaggedItemManager(models.Manager):
- """
- A manager for retrieving model instances based on their tags.
- """
- def __init__(self, tag_model):
- super(ModelTaggedItemManager, self).__init__()
- self.intermediary_table_model = tag_model.objects.intermediary_table_model
-
- def related_to(self, obj, queryset=None, num=None):
- if queryset is None:
- return self.intermediary_table_model.objects.get_related(obj, self.model, num=num)
- else:
- return self.intermediary_table_model.objects.get_related(obj, queryset, num=num)
-
- def with_all(self, tags, queryset=None):
- if queryset is None:
- return self.intermediary_table_model.objects.get_by_model(self.model, tags)
- else:
- return self.intermediary_table_model.objects.get_by_model(queryset, tags)
-
- def with_any(self, tags, queryset=None):
- if queryset is None:
- return self.intermediary_table_model.objects.get_union_by_model(self.model, tags)
- else:
- return self.intermediary_table_model.objects.get_union_by_model(queryset, tags)
-
-
-class TagDescriptor(object):
- """
- A descriptor which provides access to a ``ModelTagManager`` for
- model classes and simple retrieval, updating and deletion of tags
- for model instances.
- """
- def __init__(self, tag_model):
- self.tag_model = tag_model
-
- def __get__(self, instance, owner):
- if not instance:
- tag_manager = ModelTagManager(self.tag_model)
- tag_manager.model = owner
- return tag_manager
- else:
- return self.tag_model.objects.get_for_object(instance)
-
- def __set__(self, instance, value):
- self.tag_model.objects.update_tags(instance, value)
-
- def __del__(self, instance):
- self.tag_model.objects.update_tags(instance, [])
-
+++ /dev/null
-"""
-Models and managers for generic tagging.
-"""
-# Python 2.3 compatibility
-if not hasattr(__builtins__, 'set'):
- from sets import Set as set
-
-from django.contrib.contenttypes import generic
-from django.contrib.contenttypes.models import ContentType
-from django.db import connection, models
-from django.utils.translation import ugettext_lazy as _
-from django.db.models.base import ModelBase
-
-qn = connection.ops.quote_name
-
-try:
- from django.db.models.query import parse_lookup
-except ImportError:
- parse_lookup = None
-
-
-def get_queryset_and_model(queryset_or_model):
- """
- Given a ``QuerySet`` or a ``Model``, returns a two-tuple of
- (queryset, model).
-
- If a ``Model`` is given, the ``QuerySet`` returned will be created
- using its default manager.
- """
- try:
- return queryset_or_model, queryset_or_model.model
- except AttributeError:
- return queryset_or_model._default_manager.all(), queryset_or_model
-
-
-############
-# Managers #
-############
-class TagManager(models.Manager):
- def __init__(self, intermediary_table_model):
- super(TagManager, self).__init__()
- self.intermediary_table_model = intermediary_table_model
-
- def update_tags(self, obj, tags):
- """
- Update tags associated with an object.
- """
- content_type = ContentType.objects.get_for_model(obj)
- current_tags = list(self.filter(items__content_type__pk=content_type.pk,
- items__object_id=obj.pk))
- updated_tags = self.model.get_tag_list(tags)
-
- # Remove tags which no longer apply
- tags_for_removal = [tag for tag in current_tags \
- if tag not in updated_tags]
- if len(tags_for_removal):
- self.intermediary_table_model._default_manager.filter(content_type__pk=content_type.pk,
- object_id=obj.pk,
- tag__in=tags_for_removal).delete()
- # Add new tags
- for tag in updated_tags:
- if tag not in current_tags:
- self.intermediary_table_model._default_manager.create(tag=tag, content_object=obj)
-
- def get_for_object(self, obj):
- """
- Create a queryset matching all tags associated with the given
- object.
- """
- ctype = ContentType.objects.get_for_model(obj)
- return self.filter(items__content_type__pk=ctype.pk,
- items__object_id=obj.pk)
-
- def _get_usage(self, model, counts=False, min_count=None, extra_joins=None, extra_criteria=None, params=None, extra=None):
- """
- Perform the custom SQL query for ``usage_for_model`` and
- ``usage_for_queryset``.
- """
- if min_count is not None: counts = True
-
- model_table = qn(model._meta.db_table)
- model_pk = '%s.%s' % (model_table, qn(model._meta.pk.column))
- tag_columns = self._get_tag_columns()
-
- if extra is None: extra = {}
- extra_where = ''
- if 'where' in extra:
- extra_where = 'AND ' + ' AND '.join(extra['where'])
-
- query = """
- SELECT DISTINCT %(tag_columns)s%(count_sql)s
- FROM
- %(tag)s
- INNER JOIN %(tagged_item)s
- ON %(tag)s.id = %(tagged_item)s.tag_id
- INNER JOIN %(model)s
- ON %(tagged_item)s.object_id = %(model_pk)s
- %%s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- %%s
- %(extra_where)s
- GROUP BY %(tag)s.id, %(tag)s.name
- %%s
- ORDER BY %(tag)s.%(ordering)s ASC""" % {
- 'tag': qn(self.model._meta.db_table),
- 'ordering': ', '.join(qn(field) for field in self.model._meta.ordering),
- 'tag_columns': tag_columns,
- 'count_sql': counts and (', COUNT(%s)' % model_pk) or '',
- 'tagged_item': qn(self.intermediary_table_model._meta.db_table),
- 'model': model_table,
- 'model_pk': model_pk,
- 'extra_where': extra_where,
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- }
-
- min_count_sql = ''
- if min_count is not None:
- min_count_sql = 'HAVING COUNT(%s) >= %%s' % model_pk
- params.append(min_count)
-
- cursor = connection.cursor()
- cursor.execute(query % (extra_joins, extra_criteria, min_count_sql), params)
- tags = []
- for row in cursor.fetchall():
- t = self.model(*row[:len(self.model._meta.fields)])
- if counts:
- t.count = row[len(self.model._meta.fields)]
- tags.append(t)
- return tags
-
- def usage_for_model(self, model, counts=False, min_count=None, filters=None, extra=None):
- """
- Obtain a list of tags associated with instances of the given
- Model class.
-
- If ``counts`` is True, a ``count`` attribute will be added to
- each tag, indicating how many times it has been used against
- the Model class in question.
-
- If ``min_count`` is given, only tags which have a ``count``
- greater than or equal to ``min_count`` will be returned.
- Passing a value for ``min_count`` implies ``counts=True``.
-
- To limit the tags (and counts, if specified) returned to those
- used by a subset of the Model's instances, pass a dictionary
- of field lookups to be applied to the given Model as the
- ``filters`` argument.
- """
- if extra is None: extra = {}
- if filters is None: filters = {}
-
- if not parse_lookup:
- # post-queryset-refactor (hand off to usage_for_queryset)
- queryset = model._default_manager.filter()
- for f in filters.items():
- queryset.query.add_filter(f)
- usage = self.usage_for_queryset(queryset, counts, min_count, extra)
- else:
- # pre-queryset-refactor
- extra_joins = ''
- extra_criteria = ''
- params = []
- if len(filters) > 0:
- joins, where, params = parse_lookup(filters.items(), model._meta)
- extra_joins = ' '.join(['%s %s AS %s ON %s' % (join_type, table, alias, condition)
- for (alias, (table, join_type, condition)) in joins.items()])
- extra_criteria = 'AND %s' % (' AND '.join(where))
- usage = self._get_usage(model, counts, min_count, extra_joins, extra_criteria, params, extra)
-
- return usage
-
- def usage_for_queryset(self, queryset, counts=False, min_count=None, extra=None):
- """
- Obtain a list of tags associated with instances of a model
- contained in the given queryset.
-
- If ``counts`` is True, a ``count`` attribute will be added to
- each tag, indicating how many times it has been used against
- the Model class in question.
-
- If ``min_count`` is given, only tags which have a ``count``
- greater than or equal to ``min_count`` will be returned.
- Passing a value for ``min_count`` implies ``counts=True``.
- """
- if parse_lookup:
- raise AttributeError("'TagManager.usage_for_queryset' is not compatible with pre-queryset-refactor versions of Django.")
-
- extra_joins = ' '.join(queryset.query.get_from_clause()[0][1:])
- where, params = queryset.query.where.as_sql()
- if where:
- extra_criteria = 'AND %s' % where
- else:
- extra_criteria = ''
- return self._get_usage(queryset.model, counts, min_count, extra_joins, extra_criteria, params, extra)
-
- def related_for_model(self, tags, model, counts=False, min_count=None, extra=None):
- """
- Obtain a list of tags related to a given list of tags - that
- is, other tags used by items which have all the given tags.
-
- If ``counts`` is True, a ``count`` attribute will be added to
- each tag, indicating the number of items which have it in
- addition to the given list of tags.
-
- If ``min_count`` is given, only tags which have a ``count``
- greater than or equal to ``min_count`` will be returned.
- Passing a value for ``min_count`` implies ``counts=True``.
- """
- if min_count is not None: counts = True
- tags = self.model.get_tag_list(tags)
- tag_count = len(tags)
- tagged_item_table = qn(self.intermediary_table_model._meta.db_table)
- tag_columns = self._get_tag_columns()
-
- if extra is None: extra = {}
- extra_where = ''
- if 'where' in extra:
- extra_where = 'AND ' + ' AND '.join(extra['where'])
-
- query = """
- SELECT %(tag_columns)s%(count_sql)s
- FROM %(tagged_item)s INNER JOIN %(tag)s ON %(tagged_item)s.tag_id = %(tag)s.id
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tagged_item)s.object_id IN
- (
- SELECT %(tagged_item)s.object_id
- FROM %(tagged_item)s, %(tag)s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tag)s.id = %(tagged_item)s.tag_id
- AND %(tag)s.id IN (%(tag_id_placeholders)s)
- GROUP BY %(tagged_item)s.object_id
- HAVING COUNT(%(tagged_item)s.object_id) = %(tag_count)s
- )
- AND %(tag)s.id NOT IN (%(tag_id_placeholders)s)
- %(extra_where)s
- GROUP BY %(tag)s.id, %(tag)s.name
- %(min_count_sql)s
- ORDER BY %(tag)s.%(ordering)s ASC""" % {
- 'tag': qn(self.model._meta.db_table),
- 'ordering': ', '.join(qn(field) for field in self.model._meta.ordering),
- 'tag_columns': tag_columns,
- 'count_sql': counts and ', COUNT(%s.object_id)' % tagged_item_table or '',
- 'tagged_item': tagged_item_table,
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- 'tag_id_placeholders': ','.join(['%s'] * tag_count),
- 'extra_where': extra_where,
- 'tag_count': tag_count,
- 'min_count_sql': min_count is not None and ('HAVING COUNT(%s.object_id) >= %%s' % tagged_item_table) or '',
- }
-
- params = [tag.pk for tag in tags] * 2
- if min_count is not None:
- params.append(min_count)
-
- cursor = connection.cursor()
- cursor.execute(query, params)
- related = []
- for row in cursor.fetchall():
- tag = self.model(*row[:len(self.model._meta.fields)])
- if counts is True:
- tag.count = row[len(self.model._meta.fields)]
- related.append(tag)
- return related
-
- def _get_tag_columns(self):
- tag_table = qn(self.model._meta.db_table)
- return ', '.join('%s.%s' % (tag_table, qn(field.column)) for field in self.model._meta.fields)
-
-
-class TaggedItemManager(models.Manager):
- """
- FIXME There's currently no way to get the ``GROUP BY`` and ``HAVING``
- SQL clauses required by many of this manager's methods into
- Django's ORM.
-
- For now, we manually execute a query to retrieve the PKs of
- objects we're interested in, then use the ORM's ``__in``
- lookup to return a ``QuerySet``.
-
- Once the queryset-refactor branch lands in trunk, this can be
- tidied up significantly.
- """
- def __init__(self, tag_model):
- super(TaggedItemManager, self).__init__()
- self.tag_model = tag_model
-
- def get_by_model(self, queryset_or_model, tags):
- """
- Create a ``QuerySet`` containing instances of the specified
- model associated with a given tag or list of tags.
- """
- tags = self.tag_model.get_tag_list(tags)
- tag_count = len(tags)
- if tag_count == 0:
- # No existing tags were given
- queryset, model = get_queryset_and_model(queryset_or_model)
- return model._default_manager.none()
- elif tag_count == 1:
- # Optimisation for single tag - fall through to the simpler
- # query below.
- tag = tags[0]
- else:
- return self.get_intersection_by_model(queryset_or_model, tags)
-
- queryset, model = get_queryset_and_model(queryset_or_model)
- content_type = ContentType.objects.get_for_model(model)
- opts = self.model._meta
- tagged_item_table = qn(opts.db_table)
- return queryset.extra(
- tables=[opts.db_table],
- where=[
- '%s.content_type_id = %%s' % tagged_item_table,
- '%s.tag_id = %%s' % tagged_item_table,
- '%s.%s = %s.object_id' % (qn(model._meta.db_table),
- qn(model._meta.pk.column),
- tagged_item_table)
- ],
- params=[content_type.pk, tag.pk],
- )
-
- def get_intersection_by_model(self, queryset_or_model, tags):
- """
- Create a ``QuerySet`` containing instances of the specified
- model associated with *all* of the given list of tags.
- """
- tags = self.tag_model.get_tag_list(tags)
- tag_count = len(tags)
- queryset, model = get_queryset_and_model(queryset_or_model)
-
- if not tag_count:
- return model._default_manager.none()
-
- model_table = qn(model._meta.db_table)
- # This query selects the ids of all objects which have all the
- # given tags.
- query = """
- SELECT %(model_pk)s
- FROM %(model)s, %(tagged_item)s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
- AND %(model_pk)s = %(tagged_item)s.object_id
- GROUP BY %(model_pk)s
- HAVING COUNT(%(model_pk)s) = %(tag_count)s""" % {
- 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
- 'model': model_table,
- 'tagged_item': qn(self.model._meta.db_table),
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- 'tag_id_placeholders': ','.join(['%s'] * tag_count),
- 'tag_count': tag_count,
- }
-
- cursor = connection.cursor()
- cursor.execute(query, [tag.pk for tag in tags])
- object_ids = [row[0] for row in cursor.fetchall()]
- if len(object_ids) > 0:
- return queryset.filter(pk__in=object_ids)
- else:
- return model._default_manager.none()
-
- def get_union_by_model(self, queryset_or_model, tags):
- """
- Create a ``QuerySet`` containing instances of the specified
- model associated with *any* of the given list of tags.
- """
- tags = self.tag_model.get_tag_list(tags)
- tag_count = len(tags)
- queryset, model = get_queryset_and_model(queryset_or_model)
-
- if not tag_count:
- return model._default_manager.none()
-
- model_table = qn(model._meta.db_table)
- # This query selects the ids of all objects which have any of
- # the given tags.
- query = """
- SELECT %(model_pk)s
- FROM %(model)s, %(tagged_item)s
- WHERE %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tagged_item)s.tag_id IN (%(tag_id_placeholders)s)
- AND %(model_pk)s = %(tagged_item)s.object_id
- GROUP BY %(model_pk)s""" % {
- 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
- 'model': model_table,
- 'tagged_item': qn(self.model._meta.db_table),
- 'content_type_id': ContentType.objects.get_for_model(model).pk,
- 'tag_id_placeholders': ','.join(['%s'] * tag_count),
- }
-
- cursor = connection.cursor()
- cursor.execute(query, [tag.pk for tag in tags])
- object_ids = [row[0] for row in cursor.fetchall()]
- if len(object_ids) > 0:
- return queryset.filter(pk__in=object_ids)
- else:
- return model._default_manager.none()
-
- def get_related(self, obj, queryset_or_model, num=None):
- """
- Retrieve a list of instances of the specified model which share
- tags with the model instance ``obj``, ordered by the number of
- shared tags in descending order.
-
- If ``num`` is given, a maximum of ``num`` instances will be
- returned.
- """
- queryset, model = get_queryset_and_model(queryset_or_model)
- model_table = qn(model._meta.db_table)
- content_type = ContentType.objects.get_for_model(obj)
- related_content_type = ContentType.objects.get_for_model(model)
- query = """
- SELECT %(model_pk)s, COUNT(related_tagged_item.object_id) AS %(count)s
- FROM %(model)s, %(tagged_item)s, %(tag)s, %(tagged_item)s related_tagged_item
- WHERE %(tagged_item)s.object_id = %%s
- AND %(tagged_item)s.content_type_id = %(content_type_id)s
- AND %(tag)s.id = %(tagged_item)s.tag_id
- AND related_tagged_item.content_type_id = %(related_content_type_id)s
- AND related_tagged_item.tag_id = %(tagged_item)s.tag_id
- AND %(model_pk)s = related_tagged_item.object_id"""
- if content_type.pk == related_content_type.pk:
- # Exclude the given instance itself if determining related
- # instances for the same model.
- query += """
- AND related_tagged_item.object_id != %(tagged_item)s.object_id"""
- query += """
- GROUP BY %(model_pk)s
- ORDER BY %(count)s DESC
- %(limit_offset)s"""
- query = query % {
- 'model_pk': '%s.%s' % (model_table, qn(model._meta.pk.column)),
- 'count': qn('count'),
- 'model': model_table,
- 'tagged_item': qn(self.model._meta.db_table),
- 'tag': qn(self.model._meta.get_field('tag').rel.to._meta.db_table),
- 'content_type_id': content_type.pk,
- 'related_content_type_id': related_content_type.pk,
- 'limit_offset': num is not None and connection.ops.limit_offset_sql(num) or '',
- }
-
- cursor = connection.cursor()
- cursor.execute(query, [obj.pk])
- object_ids = [row[0] for row in cursor.fetchall()]
- if len(object_ids) > 0:
- # Use in_bulk here instead of an id__in lookup, because id__in would
- # clobber the ordering.
- object_dict = queryset.in_bulk(object_ids)
- return [object_dict[object_id] for object_id in object_ids \
- if object_id in object_dict]
- else:
- return []
-
-
-##########
-# Models #
-##########
-def create_intermediary_table_model(model):
- """Create an intermediary table model for the specific tag model"""
- name = model.__name__ + 'Relation'
-
- class Meta:
- db_table = '%s_relation' % model._meta.db_table
- unique_together = (('tag', 'content_type', 'object_id'),)
-
- def obj_unicode(self):
- return u'%s [%s]' % (self.content_type.get_object_for_this_type(pk=self.object_id), self.tag)
-
- # Set up a dictionary to simulate declarations within a class
- attrs = {
- '__module__': model.__module__,
- 'Meta': Meta,
- 'tag': models.ForeignKey(model, verbose_name=_('tag'), related_name='items'),
- 'content_type': models.ForeignKey(ContentType, verbose_name=_('content type')),
- 'object_id': models.PositiveIntegerField(_('object id'), db_index=True),
- 'content_object': generic.GenericForeignKey('content_type', 'object_id'),
- '__unicode__': obj_unicode,
- }
-
- return type(name, (models.Model,), attrs)
-
-
-class TagMeta(ModelBase):
- "Metaclass for tag models (models inheriting from TagBase)."
- def __new__(cls, name, bases, attrs):
- model = super(TagMeta, cls).__new__(cls, name, bases, attrs)
- if not model._meta.abstract:
- # Create an intermediary table and register custom managers for concrete models
- model.intermediary_table_model = create_intermediary_table_model(model)
- TagManager(model.intermediary_table_model).contribute_to_class(model, 'objects')
- TaggedItemManager(model).contribute_to_class(model.intermediary_table_model, 'objects')
- return model
-
-
-class TagBase(models.Model):
- """Abstract class to be inherited by model classes."""
- __metaclass__ = TagMeta
-
- class Meta:
- abstract = True
-
- @staticmethod
- def get_tag_list(tag_list):
- """
- Utility function for accepting tag input in a flexible manner.
-
- You should probably override this method in your subclass.
- """
- if isinstance(tag_list, TagBase):
- return [tag_list]
- else:
- return tag_list
-
+++ /dev/null
-"""
-Tagging related views.
-"""
-from django.http import Http404
-from django.utils.translation import ugettext as _
-from django.views.generic.list_detail import object_list
-
-
-def tagged_object_list(request, queryset_or_model=None, tag_model=None, tags=None,
- related_tags=False, related_tag_counts=True, **kwargs):
- """
- A thin wrapper around
- ``django.views.generic.list_detail.object_list`` which creates a
- ``QuerySet`` containing instances of the given queryset or model
- tagged with the given tag.
-
- In addition to the context variables set up by ``object_list``, a
- ``tag`` context variable will contain the ``Tag`` instance for the
- tag.
-
- If ``related_tags`` is ``True``, a ``related_tags`` context variable
- will contain tags related to the given tag for the given model.
- Additionally, if ``related_tag_counts`` is ``True``, each related
- tag will have a ``count`` attribute indicating the number of items
- which have it in addition to the given tag.
- """
- # Check attributes
- if queryset_or_model is None:
- raise AttributeError(_('tagged_object_list must be called with a queryset or a model.'))
- if tag_model is None:
- raise AttributeError(_('tagged_object_list must be called with a tag model.'))
- if tags is None:
- raise AttributeError(_('tagged_object_list must be called with a tag.'))
-
- tag_instances = tag_model.get_tag_list(tags)
- if tag_instances is None:
- raise Http404(_('No tags found matching "%s".') % tags)
- queryset = tag_model.intermediary_table_model.objects.get_intersection_by_model(queryset_or_model, tag_instances)
- if not kwargs.has_key('extra_context'):
- kwargs['extra_context'] = {}
- kwargs['extra_context']['tags'] = tag_instances
- if related_tags:
- kwargs['extra_context']['related_tags'] = \
- tag_model.objects.related_for_model(tag_instances, queryset_or_model,
- counts=related_tag_counts)
- return object_list(request, queryset, **kwargs)
-
+++ /dev/null
-class PaginationMiddleware(object):
- """
- Inserts a variable representing the current page onto the request object if
- it exists in either **GET** or **POST** portions of the request.
- """
- def process_request(self, request):
- try:
- request.page = int(request.REQUEST['page'])
- except (KeyError, ValueError):
- request.page = 1
\ No newline at end of file
+++ /dev/null
-{% if is_paginated %}
-<div class="pagination">
- {% if page_obj.has_previous %}
- <a href="?page={{ page_obj.previous_page_number }}{{ getvars }}" class="prev">‹‹ previous</a>
- {% else %}
- <span class="disabled prev">‹‹ previous</span>
- {% endif %}
- {% for page in pages %}
- {% if page %}
- {% ifequal page page_obj.number %}
- <span class="current page">{{ page }}</span>
- {% else %}
- <a href="?page={{ page }}{{ getvars }}" class="page">{{ page }}</a>
- {% endifequal %}
- {% else %}
- ...
- {% endif %}
- {% endfor %}
- {% if page_obj.has_next %}
- <a href="?page={{ page_obj.next_page_number }}{{ getvars }}" class="next">next ››</a>
- {% else %}
- <span class="disabled next">next ››</span>
- {% endif %}
-</div>
-{% endif %}
+++ /dev/null
-try:
- set
-except NameError:
- from sets import Set as set
-from django import template
-from django.db.models.query import QuerySet
-from django.core.paginator import Paginator, QuerySetPaginator, InvalidPage
-
-register = template.Library()
-
-DEFAULT_PAGINATION = 20
-DEFAULT_WINDOW = 4
-DEFAULT_ORPHANS = 0
-
-def do_autopaginate(parser, token):
- """
- Splits the arguments to the autopaginate tag and formats them correctly.
- """
- split = token.split_contents()
- if len(split) == 2:
- return AutoPaginateNode(split[1])
- elif len(split) == 3:
- try:
- paginate_by = int(split[2])
- except ValueError:
- raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
- return AutoPaginateNode(split[1], paginate_by=paginate_by)
- elif len(split) == 4:
- try:
- paginate_by = int(split[2])
- except ValueError:
- raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[2])
- try:
- orphans = int(split[3])
- except ValueError:
- raise template.TemplateSyntaxError(u'Got %s, but expected integer.' % split[3])
- return AutoPaginateNode(split[1], paginate_by=paginate_by, orphans=orphans)
- else:
- raise template.TemplateSyntaxError('%r tag takes one required argument and one optional argument' % split[0])
-
-class AutoPaginateNode(template.Node):
- """
- Emits the required objects to allow for Digg-style pagination.
-
- First, it looks in the current context for the variable specified. This
- should be either a QuerySet or a list.
-
- 1. If it is a QuerySet, this ``AutoPaginateNode`` will emit a
- ``QuerySetPaginator`` and the current page object into the context names
- ``paginator`` and ``page_obj``, respectively.
-
- 2. If it is a list, this ``AutoPaginateNode`` will emit a simple
- ``Paginator`` and the current page object into the context names
- ``paginator`` and ``page_obj``, respectively.
-
- It will then replace the variable specified with only the objects for the
- current page.
-
- .. note::
-
- It is recommended to use *{% paginate %}* after using the autopaginate
- tag. If you choose not to use *{% paginate %}*, make sure to display the
- list of availabale pages, or else the application may seem to be buggy.
- """
- def __init__(self, queryset_var, paginate_by=DEFAULT_PAGINATION, orphans=DEFAULT_ORPHANS):
- self.queryset_var = template.Variable(queryset_var)
- self.paginate_by = paginate_by
- self.orphans = orphans
-
- def render(self, context):
- key = self.queryset_var.var
- value = self.queryset_var.resolve(context)
- if issubclass(value.__class__, QuerySet):
- model = value.model
- paginator_class = QuerySetPaginator
- else:
- value = list(value)
- try:
- model = value[0].__class__
- except IndexError:
- return u''
- paginator_class = Paginator
- paginator = paginator_class(value, self.paginate_by, self.orphans)
- try:
- page_obj = paginator.page(context['request'].page)
- except InvalidPage:
- context[key] = []
- context['invalid_page'] = True
- return u''
- context[key] = page_obj.object_list
- context['paginator'] = paginator
- context['page_obj'] = page_obj
- return u''
-
-def paginate(context, window=DEFAULT_WINDOW):
- """
- Renders the ``pagination/pagination.html`` template, resulting in a
- Digg-like display of the available pages, given the current page. If there
- are too many pages to be displayed before and after the current page, then
- elipses will be used to indicate the undisplayed gap between page numbers.
-
- Requires one argument, ``context``, which should be a dictionary-like data
- structure and must contain the following keys:
-
- ``paginator``
- A ``Paginator`` or ``QuerySetPaginator`` object.
-
- ``page_obj``
- This should be the result of calling the page method on the
- aforementioned ``Paginator`` or ``QuerySetPaginator`` object, given
- the current page.
-
- This same ``context`` dictionary-like data structure may also include:
-
- ``getvars``
- A dictionary of all of the **GET** parameters in the current request.
- This is useful to maintain certain types of state, even when requesting
- a different page.
- """
- try:
- paginator = context['paginator']
- page_obj = context['page_obj']
- page_range = paginator.page_range
- # First and last are simply the first *n* pages and the last *n* pages,
- # where *n* is the current window size.
- first = set(page_range[:window])
- last = set(page_range[-window:])
- # Now we look around our current page, making sure that we don't wrap
- # around.
- current_start = page_obj.number-1-window
- if current_start < 0:
- current_start = 0
- current_end = page_obj.number-1+window
- if current_end < 0:
- current_end = 0
- current = set(page_range[current_start:current_end])
- pages = []
- # If there's no overlap between the first set of pages and the current
- # set of pages, then there's a possible need for elusion.
- if len(first.intersection(current)) == 0:
- first_list = sorted(list(first))
- second_list = sorted(list(current))
- pages.extend(first_list)
- diff = second_list[0] - first_list[-1]
- # If there is a gap of two, between the last page of the first
- # set and the first page of the current set, then we're missing a
- # page.
- if diff == 2:
- pages.append(second_list[0] - 1)
- # If the difference is just one, then there's nothing to be done,
- # as the pages need no elusion and are correct.
- elif diff == 1:
- pass
- # Otherwise, there's a bigger gap which needs to be signaled for
- # elusion, by pushing a None value to the page list.
- else:
- pages.append(None)
- pages.extend(second_list)
- else:
- pages.extend(sorted(list(first.union(current))))
- # If there's no overlap between the current set of pages and the last
- # set of pages, then there's a possible need for elusion.
- if len(current.intersection(last)) == 0:
- second_list = sorted(list(last))
- diff = second_list[0] - pages[-1]
- # If there is a gap of two, between the last page of the current
- # set and the first page of the last set, then we're missing a
- # page.
- if diff == 2:
- pages.append(second_list[0] - 1)
- # If the difference is just one, then there's nothing to be done,
- # as the pages need no elusion and are correct.
- elif diff == 1:
- pass
- # Otherwise, there's a bigger gap which needs to be signaled for
- # elusion, by pushing a None value to the page list.
- else:
- pages.append(None)
- pages.extend(second_list)
- else:
- pages.extend(sorted(list(last.difference(current))))
- to_return = {
- 'pages': pages,
- 'page_obj': page_obj,
- 'paginator': paginator,
- 'is_paginated': paginator.count > paginator.per_page,
- }
- if 'request' in context:
- getvars = context['request'].GET.copy()
- if 'page' in getvars:
- del getvars['page']
- if len(getvars.keys()) > 0:
- to_return['getvars'] = "&%s" % getvars.urlencode()
- else:
- to_return['getvars'] = ''
- return to_return
- except KeyError:
- return {}
-register.inclusion_tag('pagination/pagination.html', takes_context=True)(paginate)
-register.tag('autopaginate', do_autopaginate)
\ No newline at end of file
+++ /dev/null
-"""
->>> from django.core.paginator import Paginator
->>> from pagination.templatetags.pagination_tags import paginate
->>> from django.template import Template, Context
-
->>> p = Paginator(range(15), 2)
->>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
-[1, 2, 3, 4, 5, 6, 7, 8]
-
->>> p = Paginator(range(17), 2)
->>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
-[1, 2, 3, 4, 5, 6, 7, 8, 9]
-
->>> p = Paginator(range(19), 2)
->>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
-[1, 2, 3, 4, None, 7, 8, 9, 10]
-
->>> p = Paginator(range(21), 2)
->>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
-[1, 2, 3, 4, None, 8, 9, 10, 11]
-
-# Testing orphans
->>> p = Paginator(range(5), 2, 1)
->>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
-[1, 2]
-
->>> p = Paginator(range(21), 2, 1)
->>> paginate({'paginator': p, 'page_obj': p.page(1)})['pages']
-[1, 2, 3, 4, None, 7, 8, 9, 10]
-
->>> t = Template("{% load pagination_tags %}{% autopaginate var 2 %}{% paginate %}")
-
-# WARNING: Please, please nobody read this portion of the code!
->>> class GetProxy(object):
-... def __iter__(self): yield self.__dict__.__iter__
-... def copy(self): return self
-... def urlencode(self): return u''
-... def keys(self): return []
->>> class RequestProxy(object):
-... page = 1
-... GET = GetProxy()
->>>
-# ENDWARNING
-
->>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
-u'\\n<div class="pagination">...
->>>
->>> t = Template("{% load pagination_tags %}{% autopaginate var %}{% paginate %}")
->>> t.render(Context({'var': range(21), 'request': RequestProxy()}))
-u'\\n<div class="pagination">...
->>>
-"""
\ No newline at end of file