neue Verzeichnissstruktur
This commit is contained in:
31
utils/__init__.py
Normal file
31
utils/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
'''
|
||||
Created on 28.09.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from .countries import COUNTRIES
|
||||
from .html_cleaner import HtmlCleaner
|
||||
from .massmailer import MassMailer
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
STATUS_REJECTED, STATUS_WAITING, STATUS_PUBLISHED = -1, 0, 1
|
||||
STATUS_CHOICES = (
|
||||
(STATUS_REJECTED, _('Rejected')),
|
||||
(STATUS_WAITING, _('Waiting...')),
|
||||
(STATUS_PUBLISHED, _('Published')),
|
||||
)
|
||||
cleaner = HtmlCleaner()
|
||||
|
||||
|
||||
class OverwriteStorage(FileSystemStorage):
|
||||
"""
|
||||
Returns same name for existing file and deletes existing file on save.
|
||||
"""
|
||||
def _save(self, name, content):
|
||||
if self.exists(name):
|
||||
self.delete(name)
|
||||
return super(OverwriteStorage, self)._save(name, content)
|
||||
|
||||
def get_available_name(self, name):
|
||||
return name
|
||||
250
utils/countries.py
Normal file
250
utils/countries.py
Normal file
@@ -0,0 +1,250 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
COUNTRIES = (
|
||||
('GB', _('United Kingdom')),
|
||||
('AF', _('Afghanistan')),
|
||||
('AX', _('Aland Islands')),
|
||||
('AL', _('Albania')),
|
||||
('DZ', _('Algeria')),
|
||||
('AS', _('American Samoa')),
|
||||
('AD', _('Andorra')),
|
||||
('AO', _('Angola')),
|
||||
('AI', _('Anguilla')),
|
||||
('AQ', _('Antarctica')),
|
||||
('AG', _('Antigua and Barbuda')),
|
||||
('AR', _('Argentina')),
|
||||
('AM', _('Armenia')),
|
||||
('AW', _('Aruba')),
|
||||
('AU', _('Australia')),
|
||||
('AT', _('Austria')),
|
||||
('AZ', _('Azerbaijan')),
|
||||
('BS', _('Bahamas')),
|
||||
('BH', _('Bahrain')),
|
||||
('BD', _('Bangladesh')),
|
||||
('BB', _('Barbados')),
|
||||
('BY', _('Belarus')),
|
||||
('BE', _('Belgium')),
|
||||
('BZ', _('Belize')),
|
||||
('BJ', _('Benin')),
|
||||
('BM', _('Bermuda')),
|
||||
('BT', _('Bhutan')),
|
||||
('BO', _('Bolivia')),
|
||||
('BA', _('Bosnia and Herzegovina')),
|
||||
('BW', _('Botswana')),
|
||||
('BV', _('Bouvet Island')),
|
||||
('BR', _('Brazil')),
|
||||
('IO', _('British Indian Ocean Territory')),
|
||||
('BN', _('Brunei Darussalam')),
|
||||
('BG', _('Bulgaria')),
|
||||
('BF', _('Burkina Faso')),
|
||||
('BI', _('Burundi')),
|
||||
('KH', _('Cambodia')),
|
||||
('CM', _('Cameroon')),
|
||||
('CA', _('Canada')),
|
||||
('CV', _('Cape Verde')),
|
||||
('KY', _('Cayman Islands')),
|
||||
('CF', _('Central African Republic')),
|
||||
('TD', _('Chad')),
|
||||
('CL', _('Chile')),
|
||||
('CN', _('China')),
|
||||
('CX', _('Christmas Island')),
|
||||
('CC', _('Cocos (Keeling) Islands')),
|
||||
('CO', _('Colombia')),
|
||||
('KM', _('Comoros')),
|
||||
('CG', _('Congo')),
|
||||
('CD', _('Congo, The Democratic Republic of the')),
|
||||
('CK', _('Cook Islands')),
|
||||
('CR', _('Costa Rica')),
|
||||
('CI', _('Cote d\'Ivoire')),
|
||||
('HR', _('Croatia')),
|
||||
('CU', _('Cuba')),
|
||||
('CY', _('Cyprus')),
|
||||
('CZ', _('Czech Republic')),
|
||||
('DK', _('Denmark')),
|
||||
('DJ', _('Djibouti')),
|
||||
('DM', _('Dominica')),
|
||||
('DO', _('Dominican Republic')),
|
||||
('EC', _('Ecuador')),
|
||||
('EG', _('Egypt')),
|
||||
('SV', _('El Salvador')),
|
||||
('GQ', _('Equatorial Guinea')),
|
||||
('ER', _('Eritrea')),
|
||||
('EE', _('Estonia')),
|
||||
('ET', _('Ethiopia')),
|
||||
('FK', _('Falkland Islands (Malvinas)')),
|
||||
('FO', _('Faroe Islands')),
|
||||
('FJ', _('Fiji')),
|
||||
('FI', _('Finland')),
|
||||
('FR', _('France')),
|
||||
('GF', _('French Guiana')),
|
||||
('PF', _('French Polynesia')),
|
||||
('TF', _('French Southern Territories')),
|
||||
('GA', _('Gabon')),
|
||||
('GM', _('Gambia')),
|
||||
('GE', _('Georgia')),
|
||||
('DE', _('Germany')),
|
||||
('GH', _('Ghana')),
|
||||
('GI', _('Gibraltar')),
|
||||
('GR', _('Greece')),
|
||||
('GL', _('Greenland')),
|
||||
('GD', _('Grenada')),
|
||||
('GP', _('Guadeloupe')),
|
||||
('GU', _('Guam')),
|
||||
('GT', _('Guatemala')),
|
||||
('GG', _('Guernsey')),
|
||||
('GN', _('Guinea')),
|
||||
('GW', _('Guinea-Bissau')),
|
||||
('GY', _('Guyana')),
|
||||
('HT', _('Haiti')),
|
||||
('HM', _('Heard Island and McDonald Islands')),
|
||||
('VA', _('Holy See (Vatican City State)')),
|
||||
('HN', _('Honduras')),
|
||||
('HK', _('Hong Kong')),
|
||||
('HU', _('Hungary')),
|
||||
('IS', _('Iceland')),
|
||||
('IN', _('India')),
|
||||
('ID', _('Indonesia')),
|
||||
('IR', _('Iran, Islamic Republic of')),
|
||||
('IQ', _('Iraq')),
|
||||
('IE', _('Ireland')),
|
||||
('IM', _('Isle of Man')),
|
||||
('IL', _('Israel')),
|
||||
('IT', _('Italy')),
|
||||
('JM', _('Jamaica')),
|
||||
('JP', _('Japan')),
|
||||
('JE', _('Jersey')),
|
||||
('JO', _('Jordan')),
|
||||
('KZ', _('Kazakhstan')),
|
||||
('KE', _('Kenya')),
|
||||
('KI', _('Kiribati')),
|
||||
('KP', _('Korea, Democratic People\'s Republic of')),
|
||||
('KR', _('Korea, Republic of')),
|
||||
('KW', _('Kuwait')),
|
||||
('KG', _('Kyrgyzstan')),
|
||||
('LA', _('Lao People\'s Democratic Republic')),
|
||||
('LV', _('Latvia')),
|
||||
('LB', _('Lebanon')),
|
||||
('LS', _('Lesotho')),
|
||||
('LR', _('Liberia')),
|
||||
('LY', _('Libyan Arab Jamahiriya')),
|
||||
('LI', _('Liechtenstein')),
|
||||
('LT', _('Lithuania')),
|
||||
('LU', _('Luxembourg')),
|
||||
('MO', _('Macao')),
|
||||
('MK', _('Macedonia, The Former Yugoslav Republic of')),
|
||||
('MG', _('Madagascar')),
|
||||
('MW', _('Malawi')),
|
||||
('MY', _('Malaysia')),
|
||||
('MV', _('Maldives')),
|
||||
('ML', _('Mali')),
|
||||
('MT', _('Malta')),
|
||||
('MH', _('Marshall Islands')),
|
||||
('MQ', _('Martinique')),
|
||||
('MR', _('Mauritania')),
|
||||
('MU', _('Mauritius')),
|
||||
('YT', _('Mayotte')),
|
||||
('MX', _('Mexico')),
|
||||
('FM', _('Micronesia, Federated States of')),
|
||||
('MD', _('Moldova')),
|
||||
('MC', _('Monaco')),
|
||||
('MN', _('Mongolia')),
|
||||
('ME', _('Montenegro')),
|
||||
('MS', _('Montserrat')),
|
||||
('MA', _('Morocco')),
|
||||
('MZ', _('Mozambique')),
|
||||
('MM', _('Myanmar')),
|
||||
('NA', _('Namibia')),
|
||||
('NR', _('Nauru')),
|
||||
('NP', _('Nepal')),
|
||||
('NL', _('Netherlands')),
|
||||
('AN', _('Netherlands Antilles')),
|
||||
('NC', _('New Caledonia')),
|
||||
('NZ', _('New Zealand')),
|
||||
('NI', _('Nicaragua')),
|
||||
('NE', _('Niger')),
|
||||
('NG', _('Nigeria')),
|
||||
('NU', _('Niue')),
|
||||
('NF', _('Norfolk Island')),
|
||||
('MP', _('Northern Mariana Islands')),
|
||||
('NO', _('Norway')),
|
||||
('OM', _('Oman')),
|
||||
('PK', _('Pakistan')),
|
||||
('PW', _('Palau')),
|
||||
('PS', _('Palestinian Territory, Occupied')),
|
||||
('PA', _('Panama')),
|
||||
('PG', _('Papua New Guinea')),
|
||||
('PY', _('Paraguay')),
|
||||
('PE', _('Peru')),
|
||||
('PH', _('Philippines')),
|
||||
('PN', _('Pitcairn')),
|
||||
('PL', _('Poland')),
|
||||
('PT', _('Portugal')),
|
||||
('PR', _('Puerto Rico')),
|
||||
('QA', _('Qatar')),
|
||||
('RE', _('Reunion')),
|
||||
('RO', _('Romania')),
|
||||
('RU', _('Russian Federation')),
|
||||
('RW', _('Rwanda')),
|
||||
('BL', _('Saint Barthelemy')),
|
||||
('SH', _('Saint Helena')),
|
||||
('KN', _('Saint Kitts and Nevis')),
|
||||
('LC', _('Saint Lucia')),
|
||||
('MF', _('Saint Martin')),
|
||||
('PM', _('Saint Pierre and Miquelon')),
|
||||
('VC', _('Saint Vincent and the Grenadines')),
|
||||
('WS', _('Samoa')),
|
||||
('SM', _('San Marino')),
|
||||
('ST', _('Sao Tome and Principe')),
|
||||
('SA', _('Saudi Arabia')),
|
||||
('SN', _('Senegal')),
|
||||
('RS', _('Serbia')),
|
||||
('SC', _('Seychelles')),
|
||||
('SL', _('Sierra Leone')),
|
||||
('SG', _('Singapore')),
|
||||
('SK', _('Slovakia')),
|
||||
('SI', _('Slovenia')),
|
||||
('SB', _('Solomon Islands')),
|
||||
('SO', _('Somalia')),
|
||||
('ZA', _('South Africa')),
|
||||
('GS', _('South Georgia and the South Sandwich Islands')),
|
||||
('ES', _('Spain')),
|
||||
('LK', _('Sri Lanka')),
|
||||
('SD', _('Sudan')),
|
||||
('SR', _('Suriname')),
|
||||
('SJ', _('Svalbard and Jan Mayen')),
|
||||
('SZ', _('Swaziland')),
|
||||
('SE', _('Sweden')),
|
||||
('CH', _('Switzerland')),
|
||||
('SY', _('Syrian Arab Republic')),
|
||||
('TW', _('Taiwan, Province of China')),
|
||||
('TJ', _('Tajikistan')),
|
||||
('TZ', _('Tanzania, United Republic of')),
|
||||
('TH', _('Thailand')),
|
||||
('TL', _('Timor-Leste')),
|
||||
('TG', _('Togo')),
|
||||
('TK', _('Tokelau')),
|
||||
('TO', _('Tonga')),
|
||||
('TT', _('Trinidad and Tobago')),
|
||||
('TN', _('Tunisia')),
|
||||
('TR', _('Turkey')),
|
||||
('TM', _('Turkmenistan')),
|
||||
('TC', _('Turks and Caicos Islands')),
|
||||
('TV', _('Tuvalu')),
|
||||
('UG', _('Uganda')),
|
||||
('UA', _('Ukraine')),
|
||||
('AE', _('United Arab Emirates')),
|
||||
('US', _('United States')),
|
||||
('UM', _('United States Minor Outlying Islands')),
|
||||
('UY', _('Uruguay')),
|
||||
('UZ', _('Uzbekistan')),
|
||||
('VU', _('Vanuatu')),
|
||||
('VE', _('Venezuela')),
|
||||
('VN', _('Viet Nam')),
|
||||
('VG', _('Virgin Islands, British')),
|
||||
('VI', _('Virgin Islands, U.S.')),
|
||||
('WF', _('Wallis and Futuna')),
|
||||
('EH', _('Western Sahara')),
|
||||
('YE', _('Yemen')),
|
||||
('ZM', _('Zambia')),
|
||||
('ZW', _('Zimbabwe')),
|
||||
)
|
||||
58
utils/forms.py
Normal file
58
utils/forms.py
Normal file
@@ -0,0 +1,58 @@
|
||||
'''
|
||||
Created on 24.11.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django import forms
|
||||
import datetime
|
||||
|
||||
|
||||
class DateInput(forms.widgets.DateInput):
|
||||
input_type = 'date'
|
||||
attrs = {'class': 'dateinput'}
|
||||
|
||||
def __init__(self, attrs=None, **kwargs):
|
||||
forms.widgets.DateInput.__init__(self,
|
||||
attrs=attrs,
|
||||
format='%Y-%m-%d',
|
||||
**kwargs)
|
||||
|
||||
|
||||
class NumberInput(forms.widgets.Input):
|
||||
input_type = 'number'
|
||||
|
||||
|
||||
class TimeInput(forms.widgets.Select):
|
||||
|
||||
def __init__(self, attrs=None,):
|
||||
timeset = datetime.datetime(2000, 1, 1, 0, 0)
|
||||
choices = [('', '-------')]
|
||||
while timeset < datetime.datetime(2000, 1, 2, 0, 0):
|
||||
choices.append((
|
||||
timeset.strftime('%H:%M:%S'),
|
||||
timeset.strftime('%H:%M')
|
||||
))
|
||||
timeset += datetime.timedelta(minutes=30)
|
||||
forms.widgets.Select.__init__(self, attrs=attrs, choices=choices)
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
return forms.widgets.Select.render(self, name, value, attrs=attrs,
|
||||
choices=choices)
|
||||
|
||||
|
||||
class SplitDateTimeWidget(forms.widgets.MultiWidget):
|
||||
"""
|
||||
A Widget that splits datetime input into two <input type="text"> boxes.
|
||||
"""
|
||||
|
||||
def __init__(self, attrs=None, date_format='%Y-%m-%d'):
|
||||
widgets = (
|
||||
DateInput(attrs=attrs, format=date_format),
|
||||
TimeInput(attrs=attrs)
|
||||
)
|
||||
super(SplitDateTimeWidget, self).__init__(widgets, attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
if value:
|
||||
return [value.date(), value.time()]
|
||||
return [None, None]
|
||||
23
utils/html5/__init__.py
Normal file
23
utils/html5/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/python
|
||||
from . import base
|
||||
|
||||
registry = base.LookupRegistry()
|
||||
|
||||
|
||||
def url_autodiscover():
|
||||
import copy
|
||||
from django.conf import settings
|
||||
from django.utils.importlib import import_module
|
||||
from django.utils.module_loading import module_has_submodule
|
||||
|
||||
for app in settings.INSTALLED_APPS:
|
||||
mod = import_module(app)
|
||||
# Attempt to import the app's lookups module.
|
||||
try:
|
||||
before_import_registry = copy.copy(registry._registry)
|
||||
import_module('%s.lookups' % app)
|
||||
except:
|
||||
registry._registry = before_import_registry
|
||||
|
||||
if module_has_submodule(mod, 'lookups'):
|
||||
raise
|
||||
135
utils/html5/base.py
Normal file
135
utils/html5/base.py
Normal file
@@ -0,0 +1,135 @@
|
||||
import re
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.http import HttpResponse
|
||||
from django.utils.encoding import smart_unicode, force_unicode
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
from django.utils import simplejson as json
|
||||
|
||||
class LookupBase(object):
|
||||
|
||||
@classmethod
|
||||
def name(cls):
|
||||
app_name = cls.__module__.split('.')[-2].lower()
|
||||
class_name = cls.__name__.lower()
|
||||
name = u'%s-%s' % (app_name, class_name)
|
||||
return name
|
||||
|
||||
@classmethod
|
||||
def url(cls):
|
||||
return reverse('autocomplete-lookup', args=[cls.name()])
|
||||
|
||||
def get_query(self, request, term):
|
||||
return []
|
||||
|
||||
def get_item_label(self, item):
|
||||
return smart_unicode(item)
|
||||
|
||||
def get_item_id(self, item):
|
||||
return smart_unicode(item)
|
||||
|
||||
def get_item_value(self, item):
|
||||
return smart_unicode(item)
|
||||
|
||||
def get_item(self, value):
|
||||
return value
|
||||
|
||||
def create_item(self, value):
|
||||
raise NotImplemented()
|
||||
|
||||
def format_item(self, item):
|
||||
return {
|
||||
'id': self.get_item_id(item),
|
||||
'value': self.get_item_value(item),
|
||||
'label': self.get_item_label(item)
|
||||
}
|
||||
|
||||
def results(self, request):
|
||||
term = request.GET.get('term', '')
|
||||
raw_data = self.get_query(request, term)
|
||||
data = []
|
||||
for item in raw_data:
|
||||
data.append(self.format_item(item))
|
||||
content = json.dumps(data, cls=DjangoJSONEncoder, ensure_ascii=False)
|
||||
return HttpResponse(content, content_type='application/json')
|
||||
|
||||
|
||||
class LookupAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LookupNotRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LookupInvalid(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LookupRegistry(object):
|
||||
|
||||
def __init__(self):
|
||||
self._registry = {}
|
||||
|
||||
def validate(self, lookup):
|
||||
if not issubclass(lookup, LookupBase):
|
||||
raise LookupInvalid(u'Registered lookups must inherit from the \
|
||||
LookupBase class')
|
||||
|
||||
def register(self, lookup):
|
||||
self.validate(lookup)
|
||||
name = force_unicode(lookup.name())
|
||||
if name in self._registry:
|
||||
raise LookupAlreadyRegistered(u'The name %s is already registered'\
|
||||
% name)
|
||||
self._registry[name] = lookup
|
||||
|
||||
def unregister(self, lookup):
|
||||
self.validate(lookup)
|
||||
name = force_unicode(lookup.name())
|
||||
if name not in self._registry:
|
||||
raise LookupNotRegistered(u'The name %s is not registered' % name)
|
||||
del self._registry[name]
|
||||
|
||||
def get(self, key):
|
||||
return self._registry.get(key, None)
|
||||
|
||||
|
||||
class ModelLookup(LookupBase):
|
||||
model = None
|
||||
filters = {}
|
||||
search_field = ''
|
||||
|
||||
def get_query(self, request, term):
|
||||
qs = self.get_queryset()
|
||||
if term and self.search_field:
|
||||
qs = qs.filter(**{self.search_field: term})
|
||||
return qs
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.model._default_manager.get_query_set()
|
||||
if self.filters:
|
||||
qs = qs.filter(**self.filters)
|
||||
return qs
|
||||
|
||||
def get_item_id(self, item):
|
||||
return item.pk
|
||||
|
||||
def get_item(self, value):
|
||||
item = None
|
||||
if value:
|
||||
try:
|
||||
item = self.get_queryset().filter(pk=value)[0]
|
||||
except IndexError:
|
||||
pass
|
||||
return item
|
||||
|
||||
def create_item(self, value):
|
||||
data = {}
|
||||
if self.search_field:
|
||||
field_name = re.sub(r'__\w+$', '', self.search_field)
|
||||
if field_name:
|
||||
data = {field_name: value}
|
||||
return self.model(**data)
|
||||
270
utils/html5/forms.py
Normal file
270
utils/html5/forms.py
Normal file
@@ -0,0 +1,270 @@
|
||||
'''
|
||||
Created on 08.05.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.core import validators
|
||||
from django.core.validators import EMPTY_VALUES
|
||||
from django.forms import util, Form, ModelForm, ValidationError # @UnusedImport
|
||||
import django.forms.fields
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from . import widgets
|
||||
|
||||
|
||||
class Html5Mixin(object):
|
||||
def widget_attrs(self, widget):
|
||||
'''
|
||||
Overwrites the standard Widget Attributes to add some HTML5 Stuff
|
||||
:param widget: A Widget Object
|
||||
'''
|
||||
attrs = super(Html5Mixin, self).widget_attrs(widget)
|
||||
if self.required and not isinstance(widget, widgets.CheckboxInput):
|
||||
attrs['required'] = 'required'
|
||||
if self.help_text:
|
||||
attrs['title'] = self.help_text
|
||||
attrs['placeholder'] = self.help_text
|
||||
if self.accesskey:
|
||||
attrs['accesskey'] = self.accesskey
|
||||
return attrs
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.accesskey = kwargs.pop('accesskey', None)
|
||||
super(Html5Mixin, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class AutoCompleteSelectField(Html5Mixin, django.forms.Field):
|
||||
widget = widgets.AutoCompleteSelectWidget
|
||||
default_error_messages = {
|
||||
'invalid_choice': _(u'Select a valid choice. That choice is not one \
|
||||
of the available choices.'),
|
||||
}
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
self.allow_new = kwargs.pop('allow_new', False)
|
||||
if not kwargs['widget']:
|
||||
kwargs['widget'] = self.widget(lookup_class,
|
||||
allow_new=self.allow_new)
|
||||
super(AutoCompleteSelectField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return None
|
||||
if isinstance(value, list):
|
||||
# Input comes from an AutoComplete widget. It's two
|
||||
# components: text and id
|
||||
if len(value) != 2:
|
||||
raise django.forms.ValidationError(
|
||||
self.error_messages['invalid_choice'])
|
||||
lookup = self.lookup_class()
|
||||
if value[1] in EMPTY_VALUES:
|
||||
if not self.allow_new:
|
||||
if value[0]:
|
||||
raise django.forms.ValidationError(
|
||||
self.error_messages['invalid_choice'])
|
||||
else:
|
||||
return None
|
||||
value = lookup.create_item(value[0])
|
||||
else:
|
||||
value = lookup.get_item(value[1])
|
||||
if value is None:
|
||||
raise django.forms.ValidationError(
|
||||
self.error_messages['invalid_choice'])
|
||||
return value
|
||||
|
||||
|
||||
class AutoComboboxSelectField(AutoCompleteSelectField):
|
||||
widget = widgets.AutoComboboxSelectWidget
|
||||
|
||||
|
||||
class AutoCompleteSelectMultipleField(Html5Mixin, django.forms.Field):
|
||||
widget = widgets.AutoCompleteSelectMultipleWidget
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_choice': _(u'Select a valid choice. \
|
||||
That choice is not one of the available choices.'),
|
||||
}
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
kwargs['widget'] = self.widget(lookup_class)
|
||||
self.attrs['autofocus'] = 'autofocus'
|
||||
super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_python(self, value):
|
||||
if value in EMPTY_VALUES:
|
||||
return None
|
||||
lookup = self.lookup_class()
|
||||
items = []
|
||||
for v in value:
|
||||
if v not in EMPTY_VALUES:
|
||||
item = lookup.get_item(v)
|
||||
if item is None:
|
||||
raise django.forms.ValidationError(
|
||||
self.error_messages['invalid_choice'])
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
class AutoComboboxSelectMultipleField(AutoCompleteSelectMultipleField):
|
||||
widget = widgets.AutoComboboxSelectMultipleWidget
|
||||
|
||||
|
||||
class BooleanField(Html5Mixin, django.forms.BooleanField):
|
||||
widget = widgets.CheckboxInput
|
||||
|
||||
|
||||
class CharField(Html5Mixin, django.forms.CharField):
|
||||
pass
|
||||
|
||||
|
||||
class DateField(Html5Mixin, django.forms.fields.DateField):
|
||||
widget = widgets.DateInput
|
||||
|
||||
|
||||
class DateTimeField(Html5Mixin, django.forms.fields.DateTimeField):
|
||||
widget = widgets.DateTimeInput
|
||||
|
||||
|
||||
class EmailField(Html5Mixin, django.forms.fields.EmailField):
|
||||
widget = widgets.EmailInput
|
||||
|
||||
|
||||
class FileField(Html5Mixin, django.forms.fields.FileField):
|
||||
widget = widgets.NumberInput
|
||||
pass
|
||||
|
||||
|
||||
class FloatField(Html5Mixin, django.forms.fields.FloatField):
|
||||
pass
|
||||
|
||||
|
||||
class HiddenInput(Html5Mixin, django.forms.HiddenInput):
|
||||
pass
|
||||
|
||||
|
||||
class IntegerField(Html5Mixin, django.forms.fields.IntegerField):
|
||||
widget = widgets.NumberInput
|
||||
|
||||
def widget_attrs(self, widget):
|
||||
attrs = super(IntegerField, self).widget_attrs(widget)
|
||||
if isinstance(widget, widgets.NumberInput):
|
||||
if self.min_value is not None:
|
||||
attrs['min'] = self.min_value
|
||||
if self.max_value is not None:
|
||||
attrs['max'] = self.max_value
|
||||
return attrs
|
||||
|
||||
|
||||
class ModelChoiceField(Html5Mixin, django.forms.ModelChoiceField):
|
||||
pass
|
||||
|
||||
|
||||
class PasswordInput(Html5Mixin, django.forms.PasswordInput):
|
||||
pass
|
||||
|
||||
|
||||
class PhoneField(Html5Mixin, django.forms.CharField):
|
||||
widget = widgets.PhoneInput
|
||||
|
||||
def __init__(self, regex=None, max_length=None, min_length=None,
|
||||
error_message=None, *args, **kwargs):
|
||||
super(PhoneField, self).__init__(max_length, min_length,
|
||||
*args, **kwargs)
|
||||
self._set_regex(u'^[0-9+-/ ]+$')
|
||||
|
||||
def _get_regex(self):
|
||||
return self._regex
|
||||
|
||||
def _set_regex(self, regex=u'^[0-9+-/ ]+$'):
|
||||
regex = re.compile(regex, re.UNICODE)
|
||||
self._regex = regex
|
||||
if hasattr(self, '_regex_validator') \
|
||||
and self._regex_validator in self.validators:
|
||||
self.validators.remove(self._regex_validator)
|
||||
self._regex_validator = validators.RegexValidator(regex=regex)
|
||||
self.validators.append(self._regex_validator)
|
||||
|
||||
regex = property(_get_regex, _set_regex)
|
||||
|
||||
|
||||
class ReCaptchaField(django.forms.fields.CharField):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.widget = widgets.ReCaptchaInput
|
||||
self.required = True
|
||||
super(ReCaptchaField, self).__init__(*args, **kwargs)
|
||||
|
||||
def _check_recaptcha(self, challenge_value, response_value, \
|
||||
remote_ip={}):
|
||||
'''
|
||||
Submits a reCAPTCHA request for verification.
|
||||
Returns RecaptchaResponse for the request
|
||||
|
||||
@param challenge_value: value of recaptcha_challenge_field
|
||||
@param response_value: value of recaptcha_response_field
|
||||
@param remoteip -- the user's ip address
|
||||
|
||||
'''
|
||||
import urllib
|
||||
import urllib2
|
||||
private_key = settings.RECAPTCHA_PRIVATE_KEY
|
||||
challenge_value = challenge_value.encode('utf-8')
|
||||
response_value = response_value.encode('utf-8')
|
||||
params = urllib.urlencode({
|
||||
'privatekey': private_key,
|
||||
'remoteip': remote_ip,
|
||||
'challenge': challenge_value,
|
||||
'response': response_value,
|
||||
})
|
||||
|
||||
request = urllib2.Request(
|
||||
url="http://www.google.com/recaptcha/api/verify",
|
||||
data=params,
|
||||
headers={
|
||||
"Content-type": "application/x-www-form-urlencoded",
|
||||
"User-agent": "reCAPTCHA Python"
|
||||
}
|
||||
)
|
||||
httpresp = urllib2.urlopen(request)
|
||||
|
||||
return_values = httpresp.read().splitlines()
|
||||
httpresp.close()
|
||||
|
||||
return_code = return_values[0]
|
||||
if (return_code == "true"):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def clean(self, values):
|
||||
challenge_value = values[0]
|
||||
response_value = values[1]
|
||||
super(ReCaptchaField, self).clean(response_value)
|
||||
if not challenge_value:
|
||||
raise util.ValidationError(
|
||||
_(u'The CAPTCHA challenge is missing.'))
|
||||
elif not response_value:
|
||||
raise util.ValidationError(
|
||||
_(u'The CAPTCHA solution is missing.'))
|
||||
elif self._check_recaptcha(challenge_value, response_value):
|
||||
return challenge_value
|
||||
else:
|
||||
raise util.ValidationError(
|
||||
_(u'The CAPTCHA solution was incorrect.'))
|
||||
|
||||
|
||||
class RegexField(Html5Mixin, django.forms.RegexField):
|
||||
pass
|
||||
|
||||
|
||||
class SlugField(Html5Mixin, django.forms.SlugField):
|
||||
pass
|
||||
|
||||
|
||||
class URLField(Html5Mixin, django.forms.fields.URLField):
|
||||
widget = widgets.URLInput
|
||||
149
utils/html5/models.py
Normal file
149
utils/html5/models.py
Normal file
@@ -0,0 +1,149 @@
|
||||
'''
|
||||
Created on 08.05.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.db import models
|
||||
from django.db.models import ForeignKey, Model, SET_NULL # @UnusedImport
|
||||
from django.db.models import SET_DEFAULT, ManyToManyField # @UnusedImport
|
||||
from django.utils import six
|
||||
|
||||
from . import forms, widgets
|
||||
|
||||
|
||||
class BooleanField(models.BooleanField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.BooleanField}
|
||||
defaults.update(kwargs)
|
||||
return super(BooleanField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class CharField(models.CharField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.CharField}
|
||||
defaults.update(kwargs)
|
||||
return super(CharField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class DateField(models.DateField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'form_class': forms.DateField}
|
||||
defaults.update(kwargs)
|
||||
return super(DateField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class DateTimeField(models.DateTimeField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.DateTimeField}
|
||||
defaults.update(kwargs)
|
||||
return super(DateTimeField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class EmailField(models.EmailField):
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.EmailField}
|
||||
defaults.update(kwargs)
|
||||
return super(EmailField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class FileField(models.FileField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.FileField}
|
||||
defaults.update(kwargs)
|
||||
return super(FileField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class ForeignKey(models.ForeignKey):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
db = kwargs.pop('using', None)
|
||||
if isinstance(self.rel.to, six.string_types):
|
||||
raise ValueError("Cannot create form field for %r yet, because "
|
||||
"its related model %r has not been loaded yet" %
|
||||
(self.name, self.rel.to))
|
||||
queryset = self.rel.to._default_manager.using(db)
|
||||
queryset = queryset.complex_filter(self.rel.limit_choices_to)
|
||||
defaults = {
|
||||
'form_class': forms.ModelChoiceField,
|
||||
'queryset': queryset,
|
||||
'to_field_name': self.rel.field_name,
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super(ForeignKey, self).formfield(**defaults)
|
||||
|
||||
|
||||
class ImageField(models.ImageField):
|
||||
pass
|
||||
|
||||
|
||||
class IntegerField(models.IntegerField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.IntegerField}
|
||||
defaults.update(kwargs)
|
||||
return super(IntegerField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class NullBooleanField(models.NullBooleanField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.BooleanField}
|
||||
defaults.update(kwargs)
|
||||
return super(NullBooleanField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class PositiveSmallIntegerField(models.PositiveSmallIntegerField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'form_class': forms.IntegerField,
|
||||
'min_value': 0,
|
||||
'max_value': 32767
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super(PositiveSmallIntegerField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class PositiveIntegerField(models.IntegerField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {
|
||||
'form_class': forms.IntegerField,
|
||||
'min_value': 0
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super(PositiveIntegerField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class SlugField(models.SlugField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.SlugField}
|
||||
defaults.update(kwargs)
|
||||
return super(SlugField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class TextField(models.TextField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
|
||||
defaults = {
|
||||
'form_class': forms.CharField,
|
||||
'widget': widgets.Textarea
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return super(TextField, self).formfield(**defaults)
|
||||
|
||||
|
||||
class URLField(models.URLField):
|
||||
|
||||
def formfield(self, **kwargs):
|
||||
defaults = {'form_class': forms.URLField}
|
||||
defaults.update(kwargs)
|
||||
return super(URLField, self).formfield(**defaults)
|
||||
15
utils/html5/views.py
Normal file
15
utils/html5/views.py
Normal file
@@ -0,0 +1,15 @@
|
||||
'''
|
||||
Created on 05.08.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.http import Http404
|
||||
from . import registry
|
||||
|
||||
|
||||
def get_lookup(request, lookup_name):
|
||||
lookup_cls = registry.get(lookup_name)
|
||||
if lookup_cls is None:
|
||||
raise Http404(u'Lookup %s not found' % lookup_name)
|
||||
lookup = lookup_cls()
|
||||
return lookup.results(request)
|
||||
318
utils/html5/widgets.py
Normal file
318
utils/html5/widgets.py
Normal file
@@ -0,0 +1,318 @@
|
||||
'''
|
||||
Created on 08.05.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.conf import settings
|
||||
from django.forms import widgets
|
||||
from django.forms.util import flatatt
|
||||
from django.utils import formats
|
||||
from django.utils.encoding import force_unicode, force_text
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
||||
class AutoCompleteWidget(widgets.TextInput):
|
||||
|
||||
def __init__(self, lookup_class, attrs=None, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
self.allow_new = kwargs.pop('allow_new', False)
|
||||
self.qs = {}
|
||||
super(AutoCompleteWidget, self).__init__(*args, **kwargs)
|
||||
if attrs is not None:
|
||||
self.attrs = attrs.copy()
|
||||
else:
|
||||
self.attrs = {}
|
||||
|
||||
def update_query_parameters(self, qs_dict):
|
||||
self.qs.update(qs_dict)
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
attrs = super(AutoCompleteWidget, self).build_attrs(extra_attrs, **kwargs) # @IgnorePep8
|
||||
url = self.lookup_class.url()
|
||||
if self.qs:
|
||||
url = '%s?%s' % (url, urlencode(self.qs))
|
||||
attrs[u'data-selectable-url'] = url
|
||||
attrs[u'data-selectable-type'] = 'text'
|
||||
attrs[u'data-selectable-allow-new'] = str(self.allow_new).lower()
|
||||
return attrs
|
||||
|
||||
|
||||
class AutoComboboxWidget(AutoCompleteWidget):
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
attrs = super(AutoComboboxWidget, self).build_attrs(extra_attrs, **kwargs) # @IgnorePep8
|
||||
attrs[u'data-selectable-type'] = 'combobox'
|
||||
return attrs
|
||||
|
||||
|
||||
class DateInput(widgets.DateInput):
|
||||
input_type = 'date'
|
||||
attrs = {'class': 'dateinput'}
|
||||
|
||||
def __init__(self, attrs=None, date_format='%Y-%m-%d'):
|
||||
super(DateInput, self).__init__(attrs)
|
||||
if date_format:
|
||||
self.format = date_format
|
||||
self.manual_format = True
|
||||
else:
|
||||
self.format = formats.get_format('DATE_INPUT_FORMATS')[0]
|
||||
self.manual_format = False
|
||||
|
||||
|
||||
class DateTimeInput(widgets.MultiWidget):
|
||||
"""
|
||||
A Widget that splits datetime input into two <input type="text"> boxes.
|
||||
"""
|
||||
|
||||
def __init__(self, attrs=None, date_format='%Y-%m-%d', time_format='%H:%M'): # @IgnorePep8
|
||||
widgets = (
|
||||
DateInput(attrs=attrs, date_format=date_format),
|
||||
TimeInput(attrs=attrs, time_format=time_format)
|
||||
)
|
||||
super(DateTimeInput, self).__init__(widgets, attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
if value:
|
||||
return [value.date(), value.time()]
|
||||
return [None, None]
|
||||
|
||||
|
||||
class CheckboxInput(widgets.CheckboxInput):
|
||||
input_type = 'checkbox'
|
||||
is_checkbox = True
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
final_attrs = self.build_attrs(attrs, type='checkbox', name=name)
|
||||
title = force_text(self.attrs.get('title', ''))
|
||||
try:
|
||||
result = self.check_test(value)
|
||||
except: # Silently catch exceptions
|
||||
result = False
|
||||
if result:
|
||||
final_attrs['checked'] = 'checked'
|
||||
if not (value is True or value is False or value is None or value == ''): # @IgnorePep8
|
||||
# Only add the 'value' attribute if a value is non-empty.
|
||||
final_attrs['value'] = force_unicode(value)
|
||||
|
||||
return mark_safe(u'<input%s /> %s' % (flatatt(final_attrs), title))
|
||||
|
||||
|
||||
class EmailInput(widgets.TextInput):
|
||||
input_type = 'email'
|
||||
|
||||
|
||||
class NumberInput(widgets.TextInput):
|
||||
input_type = 'number'
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
widgets.Input.__init__(self, attrs=attrs)
|
||||
|
||||
|
||||
class PhoneInput(widgets.TextInput):
|
||||
input_type = 'tel'
|
||||
|
||||
|
||||
class RangeInput(widgets.TextInput):
|
||||
input_type = 'range'
|
||||
|
||||
|
||||
class ReCaptchaInput(widgets.Widget):
|
||||
'''
|
||||
Der HTML Code von Googles ReCaptcha als Form Widget
|
||||
'''
|
||||
recaptcha_challenge_name = 'recaptcha_challenge_field'
|
||||
recaptcha_response_name = 'recaptcha_response_field'
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
javascript = u"""
|
||||
<script type="text/javascript">var RecaptchaOptions = {theme :
|
||||
'%(theme)s'};</script><script type="text/javascript"
|
||||
src="https://www.google.com/recaptcha/api/challenge?k=%(public_key)s">
|
||||
</script><noscript><iframe
|
||||
src="https://www.google.com/recaptcha/api/noscript?k=%(public_key)s"
|
||||
height="300" width="500" frameborder="0"></iframe><br /><textarea
|
||||
name="recaptcha_challenge_field" rows="3" cols="40"></textarea><input
|
||||
type="hidden" name="recaptcha_response_field" value="manual_challenge">
|
||||
</noscript>"""
|
||||
return mark_safe(javascript % {'public_key':
|
||||
settings.RECAPTCHA_PUBLIC_KEY, 'theme': 'red'})
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return [data.get(self.recaptcha_challenge_name, None),
|
||||
data.get(self.recaptcha_response_name, None)]
|
||||
|
||||
|
||||
class SelectableMultiWidget(widgets.MultiWidget):
|
||||
|
||||
def update_query_parameters(self, qs_dict):
|
||||
self.widgets[0].update_query_parameters(qs_dict)
|
||||
|
||||
|
||||
class Textarea(widgets.Widget):
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
# The 'rows' and 'cols' attributes are required for HTML correctness.
|
||||
default_attrs = {'cols': '40', 'rows': '10'}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super(Textarea, self).__init__(default_attrs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
value = '' if value is None else value
|
||||
final_attrs = self.build_attrs(attrs, name=name)
|
||||
return mark_safe(u'<textarea%s>%s</textarea>' % (flatatt(final_attrs),
|
||||
conditional_escape(force_unicode(value))))
|
||||
|
||||
|
||||
class TextInput(widgets.TextInput):
|
||||
pass
|
||||
|
||||
|
||||
class TimeInput(widgets.TimeInput):
|
||||
input_type = 'time'
|
||||
|
||||
def __init__(self, attrs=None, time_format='%H:%M'):
|
||||
default_attrs = {'maxlength': 5, 'size': 5}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
super(TimeInput, self).__init__(default_attrs)
|
||||
|
||||
if time_format:
|
||||
self.format = time_format
|
||||
self.manual_format = True
|
||||
else:
|
||||
self.format = formats.get_format('TIME_INPUT_FORMATS')[0]
|
||||
self.manual_format = False
|
||||
|
||||
|
||||
class URLInput(widgets.Input):
|
||||
input_type = 'url'
|
||||
|
||||
|
||||
class LookupMultipleHiddenInput(widgets.MultipleHiddenInput):
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
super(LookupMultipleHiddenInput, self).__init__(*args, **kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None, choices=()):
|
||||
lookup = self.lookup_class()
|
||||
value = [] if value is None else value
|
||||
final_attrs = self.build_attrs(attrs, type=self.input_type, name=name)
|
||||
id_ = final_attrs.get('id', None)
|
||||
inputs = []
|
||||
model = getattr(self.lookup_class, 'model', None)
|
||||
for i, v in enumerate(value):
|
||||
item = None
|
||||
if model and isinstance(v, model):
|
||||
item = v
|
||||
v = lookup.get_item_id(item)
|
||||
input_attrs = dict(value=force_unicode(v), **final_attrs)
|
||||
if id_:
|
||||
# An ID attribute was given. Add a numeric index as a suffix
|
||||
# so that the inputs don't all have the same ID attribute.
|
||||
input_attrs['id'] = '%s_%s' % (id_, i)
|
||||
if v:
|
||||
item = item or lookup.get_item(v)
|
||||
input_attrs['title'] = lookup.get_item_value(item)
|
||||
inputs.append(u'<input%s />' % flatatt(input_attrs))
|
||||
return mark_safe(u'\n'.join(inputs))
|
||||
|
||||
def build_attrs(self, extra_attrs=None, **kwargs):
|
||||
attrs = super(LookupMultipleHiddenInput, self).build_attrs(extra_attrs, **kwargs) # @IgnorePep8
|
||||
attrs[u'data-selectable-type'] = 'hidden-multiple'
|
||||
return attrs
|
||||
|
||||
|
||||
class AutoCompleteSelectMultipleWidget(SelectableMultiWidget):
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
widgets = [
|
||||
AutoCompleteWidget(lookup_class, allow_new=False,
|
||||
attrs={u'data-selectable-multiple': 'true'}),
|
||||
LookupMultipleHiddenInput(lookup_class)
|
||||
]
|
||||
super(AutoCompleteSelectMultipleWidget, self).__init__(widgets, *args, **kwargs) # @IgnorePep8
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return self.widgets[1].value_from_datadict(data, files, name + '_1')
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if value and not hasattr(value, '__iter__'):
|
||||
value = [value]
|
||||
value = [u'', value]
|
||||
return super(AutoCompleteSelectMultipleWidget, self).render(name, value, attrs) # @IgnorePep8
|
||||
|
||||
|
||||
class AutoComboboxSelectMultipleWidget(SelectableMultiWidget):
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
widgets = [
|
||||
AutoComboboxWidget(lookup_class, allow_new=False,
|
||||
attrs={u'data-selectable-multiple': 'true'}),
|
||||
LookupMultipleHiddenInput(lookup_class)
|
||||
]
|
||||
super(AutoComboboxSelectMultipleWidget, self).__init__(widgets, *args, **kwargs) # @IgnorePep8
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
return self.widgets[1].value_from_datadict(data, files, name + '_1')
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
if value and not hasattr(value, '__iter__'):
|
||||
value = [value]
|
||||
value = [u'', value]
|
||||
return super(AutoComboboxSelectMultipleWidget, self).render(name, value, attrs) # @IgnorePep8
|
||||
|
||||
|
||||
class AutoCompleteSelectWidget(SelectableMultiWidget):
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
self.allow_new = kwargs.pop('allow_new', False)
|
||||
widget_set = [
|
||||
AutoCompleteWidget(lookup_class, allow_new=self.allow_new),
|
||||
widgets.HiddenInput(attrs={u'data-selectable-type': 'hidden'})
|
||||
]
|
||||
super(AutoCompleteSelectWidget, self).__init__(widget_set, *args, **kwargs) # @IgnorePep8
|
||||
|
||||
def decompress(self, value):
|
||||
if value:
|
||||
lookup = self.lookup_class()
|
||||
model = getattr(self.lookup_class, 'model', None)
|
||||
if model and isinstance(value, model):
|
||||
item = value
|
||||
value = lookup.get_item_id(item)
|
||||
else:
|
||||
item = lookup.get_item(value)
|
||||
item_value = lookup.get_item_value(item)
|
||||
return [item_value, value]
|
||||
return [None, None]
|
||||
|
||||
|
||||
class AutoComboboxSelectWidget(SelectableMultiWidget):
|
||||
|
||||
def __init__(self, lookup_class, *args, **kwargs):
|
||||
self.lookup_class = lookup_class
|
||||
self.allow_new = kwargs.pop('allow_new', False)
|
||||
widget_set = [
|
||||
AutoComboboxWidget(lookup_class, allow_new=self.allow_new),
|
||||
widgets.HiddenInput(attrs={u'data-selectable-type': 'hidden'})
|
||||
]
|
||||
super(AutoComboboxSelectWidget, self).__init__(widget_set, *args, **kwargs) # @IgnorePep8
|
||||
|
||||
def decompress(self, value):
|
||||
if value:
|
||||
lookup = self.lookup_class()
|
||||
model = getattr(self.lookup_class, 'model', None)
|
||||
if model and isinstance(value, model):
|
||||
item = value
|
||||
value = lookup.get_item_id(item)
|
||||
else:
|
||||
item = lookup.get_item(value)
|
||||
item_value = lookup.get_item_value(item)
|
||||
return [item_value, value]
|
||||
return [None, None]
|
||||
64
utils/html_cleaner.py
Normal file
64
utils/html_cleaner.py
Normal file
@@ -0,0 +1,64 @@
|
||||
'''
|
||||
Created on 19.10.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
|
||||
class HtmlCleaner(object):
|
||||
ACCEPTABLE_ELEMENTS = ['a', 'abbr', 'acronym', 'address', 'area', 'b',
|
||||
'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite',
|
||||
'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl',
|
||||
'dt', 'em', 'font', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i',
|
||||
'img', 'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol',
|
||||
'p', 'pre', 'q', 's', 'samp', 'small', 'span', 'strike',
|
||||
'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th',
|
||||
'thead', 'tr', 'tt', 'u', 'ul', 'var']
|
||||
|
||||
ACCEPTABELE_ATTRIBUTES = [
|
||||
'abbr', 'accept', 'accept-charset', 'accesskey',
|
||||
'action', 'align', 'class', 'alt', 'axis',
|
||||
'char', 'charoff', 'charset', 'checked', 'cite', 'clear', 'cols',
|
||||
'colspan', 'color', 'compact', 'coords', 'datetime', 'dir',
|
||||
'enctype', 'for', 'headers', 'height', 'href', 'hreflang', 'hspace',
|
||||
'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'method',
|
||||
'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt',
|
||||
'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'shape', 'size',
|
||||
'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title',
|
||||
'type', 'usemap', 'valign', 'value', 'vspace', 'width']
|
||||
|
||||
counter = 1
|
||||
tag_removed = False
|
||||
|
||||
def clean_attributes(self, tag):
|
||||
for attr in tag._getAttrMap().keys():
|
||||
if attr not in self.ACCEPTABELE_ATTRIBUTES:
|
||||
del tag[attr]
|
||||
elif tag[attr].count('script:'):
|
||||
del tag[attr]
|
||||
|
||||
def clean_tag(self, tag):
|
||||
if tag.name not in self.ACCEPTABLE_ELEMENTS:
|
||||
tag.extract() # remove the bad ones
|
||||
self.tag_removed = True
|
||||
else:
|
||||
self.clean_attributes(tag)
|
||||
|
||||
def clean_html(self, fragment=''):
|
||||
'''
|
||||
Reparses and cleans the html from XSS Attacks until it stops changing.
|
||||
@param fragment:
|
||||
'''
|
||||
while True:
|
||||
soup = BeautifulSoup(fragment)
|
||||
self.tag_removed = False
|
||||
self.counter += 1
|
||||
for tag in soup.findAll(True):
|
||||
self.clean_tag(tag)
|
||||
fragment = unicode(soup)
|
||||
if self.tag_removed:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
return fragment
|
||||
16
utils/icalendar/__init__.py
Normal file
16
utils/icalendar/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Components
|
||||
from .cal import Calendar, Event, Todo, Journal
|
||||
from .cal import FreeBusy, Timezone, Alarm, ComponentFactory
|
||||
|
||||
# Property Data Value Types
|
||||
from .prop import vBinary, vBoolean, vCalAddress, vDatetime, vDate, \
|
||||
vDDDTypes, vDuration, vFloat, vInt, vPeriod, \
|
||||
vWeekday, vFrequency, vRecur, vText, vTime, vUri, \
|
||||
vGeo, vUTCOffset, TypesFactory
|
||||
|
||||
# useful tzinfo subclasses
|
||||
from .prop import FixedOffset, UTC, LocalTimezone
|
||||
|
||||
# Parameters and helper methods for splitting and joining string with escaped
|
||||
# chars.
|
||||
from .parser import Parameters, q_split, q_join
|
||||
533
utils/icalendar/cal.py
Normal file
533
utils/icalendar/cal.py
Normal file
@@ -0,0 +1,533 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
|
||||
Calendar is a dictionary like Python object that can render itself as VCAL
|
||||
files according to rfc2445.
|
||||
|
||||
These are the defined components.
|
||||
|
||||
"""
|
||||
|
||||
# from python
|
||||
from types import ListType, TupleType
|
||||
SequenceTypes = (ListType, TupleType)
|
||||
|
||||
# from this package
|
||||
from .caselessdict import CaselessDict
|
||||
from .parser import Contentlines, Contentline, Parameters
|
||||
from .parser import q_split, q_join
|
||||
from .prop import TypesFactory, vText
|
||||
|
||||
|
||||
######################################
|
||||
# The component factory
|
||||
|
||||
class ComponentFactory(CaselessDict):
|
||||
"""
|
||||
All components defined in rfc 2445 are registered in this factory class. To
|
||||
get a component you can use it like this.
|
||||
|
||||
>>> factory = ComponentFactory()
|
||||
>>> component = factory['VEVENT']
|
||||
>>> event = component(dtstart='19700101')
|
||||
>>> event.as_string()
|
||||
'BEGIN:VEVENT\\r\\nDTSTART:19700101\\r\\nEND:VEVENT\\r\\n'
|
||||
|
||||
>>> factory.get('VCALENDAR', Component)
|
||||
<class 'icalendar.cal.Calendar'>
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
CaselessDict.__init__(self, *args, **kwargs)
|
||||
self['VEVENT'] = Event
|
||||
self['VTODO'] = Todo
|
||||
self['VJOURNAL'] = Journal
|
||||
self['VFREEBUSY'] = FreeBusy
|
||||
self['VTIMEZONE'] = Timezone
|
||||
self['VALARM'] = Alarm
|
||||
self['VCALENDAR'] = Calendar
|
||||
|
||||
|
||||
# These Properties have multiple property values inlined in one propertyline
|
||||
# seperated by comma. Use CaselessDict as simple caseless set.
|
||||
INLINE = CaselessDict(
|
||||
[(cat, 1) for cat in ('CATEGORIES', 'RESOURCES', 'FREEBUSY')]
|
||||
)
|
||||
|
||||
_marker = []
|
||||
|
||||
class Component(CaselessDict):
|
||||
"""
|
||||
Component is the base object for calendar, Event and the other components
|
||||
defined in RFC 2445. normally you will not use this class directy, but
|
||||
rather one of the subclasses.
|
||||
|
||||
A component is like a dictionary with extra methods and attributes.
|
||||
>>> c = Component()
|
||||
>>> c.name = 'VCALENDAR'
|
||||
|
||||
Every key defines a property. A property can consist of either a single
|
||||
item. This can be set with a single value
|
||||
>>> c['prodid'] = '-//max m//icalendar.mxm.dk/'
|
||||
>>> c
|
||||
VCALENDAR({'PRODID': '-//max m//icalendar.mxm.dk/'})
|
||||
|
||||
or with a list
|
||||
>>> c['ATTENDEE'] = ['Max M', 'Rasmussen']
|
||||
|
||||
if you use the add method you don't have to considder if a value is a list
|
||||
or not.
|
||||
>>> c = Component()
|
||||
>>> c.name = 'VEVENT'
|
||||
>>> c.add('attendee', 'maxm@mxm.dk')
|
||||
>>> c.add('attendee', 'test@example.dk')
|
||||
>>> c
|
||||
VEVENT({'ATTENDEE': [vCalAddress('maxm@mxm.dk'), vCalAddress('test@example.dk')]})
|
||||
|
||||
You can get the values back directly
|
||||
>>> c.add('prodid', '-//my product//')
|
||||
>>> c['prodid']
|
||||
vText(u'-//my product//')
|
||||
|
||||
or decoded to a python type
|
||||
>>> c.decoded('prodid')
|
||||
u'-//my product//'
|
||||
|
||||
With default values for non existing properties
|
||||
>>> c.decoded('version', 'No Version')
|
||||
'No Version'
|
||||
|
||||
The component can render itself in the RFC 2445 format.
|
||||
>>> c = Component()
|
||||
>>> c.name = 'VCALENDAR'
|
||||
>>> c.add('attendee', 'Max M')
|
||||
>>> c.as_string()
|
||||
'BEGIN:VCALENDAR\\r\\nATTENDEE:Max M\\r\\nEND:VCALENDAR\\r\\n'
|
||||
|
||||
>>> from icalendar.prop import vDatetime
|
||||
|
||||
Components can be nested, so You can add a subcompont. Eg a calendar holds events.
|
||||
>>> e = Component(summary='A brief history of time')
|
||||
>>> e.name = 'VEVENT'
|
||||
>>> e.add('dtend', '20000102T000000', encode=0)
|
||||
>>> e.add('dtstart', '20000101T000000', encode=0)
|
||||
>>> e.as_string()
|
||||
'BEGIN:VEVENT\\r\\nDTEND:20000102T000000\\r\\nDTSTART:20000101T000000\\r\\nSUMMARY:A brief history of time\\r\\nEND:VEVENT\\r\\n'
|
||||
|
||||
>>> c.add_component(e)
|
||||
>>> c.subcomponents
|
||||
[VEVENT({'DTEND': '20000102T000000', 'DTSTART': '20000101T000000', 'SUMMARY': 'A brief history of time'})]
|
||||
|
||||
We can walk over nested componentes with the walk method.
|
||||
>>> [i.name for i in c.walk()]
|
||||
['VCALENDAR', 'VEVENT']
|
||||
|
||||
We can also just walk over specific component types, by filtering them on
|
||||
their name.
|
||||
>>> [i.name for i in c.walk('VEVENT')]
|
||||
['VEVENT']
|
||||
|
||||
>>> [i['dtstart'] for i in c.walk('VEVENT')]
|
||||
['20000101T000000']
|
||||
|
||||
INLINE properties have their values on one property line. Note the double
|
||||
quoting of the value with a colon in it.
|
||||
>>> c = Calendar()
|
||||
>>> c['resources'] = 'Chair, Table, "Room: 42"'
|
||||
>>> c
|
||||
VCALENDAR({'RESOURCES': 'Chair, Table, "Room: 42"'})
|
||||
|
||||
>>> c.as_string()
|
||||
'BEGIN:VCALENDAR\\r\\nRESOURCES:Chair, Table, "Room: 42"\\r\\nEND:VCALENDAR\\r\\n'
|
||||
|
||||
The inline values must be handled by the get_inline() and set_inline()
|
||||
methods.
|
||||
|
||||
>>> c.get_inline('resources', decode=0)
|
||||
['Chair', 'Table', 'Room: 42']
|
||||
|
||||
These can also be decoded
|
||||
>>> c.get_inline('resources', decode=1)
|
||||
[u'Chair', u'Table', u'Room: 42']
|
||||
|
||||
You can set them directly
|
||||
>>> c.set_inline('resources', ['A', 'List', 'of', 'some, recources'], encode=1)
|
||||
>>> c['resources']
|
||||
'A,List,of,"some, recources"'
|
||||
|
||||
and back again
|
||||
>>> c.get_inline('resources', decode=0)
|
||||
['A', 'List', 'of', 'some, recources']
|
||||
|
||||
>>> c['freebusy'] = '19970308T160000Z/PT3H,19970308T200000Z/PT1H,19970308T230000Z/19970309T000000Z'
|
||||
>>> c.get_inline('freebusy', decode=0)
|
||||
['19970308T160000Z/PT3H', '19970308T200000Z/PT1H', '19970308T230000Z/19970309T000000Z']
|
||||
|
||||
>>> freebusy = c.get_inline('freebusy', decode=1)
|
||||
>>> type(freebusy[0][0]), type(freebusy[0][1])
|
||||
(<type 'datetime.datetime'>, <type 'datetime.timedelta'>)
|
||||
"""
|
||||
|
||||
name = '' # must be defined in each component
|
||||
required = () # These properties are required
|
||||
singletons = () # These properties must only appear once
|
||||
multiple = () # may occur more than once
|
||||
exclusive = () # These properties are mutually exclusive
|
||||
inclusive = () # if any occurs the other(s) MUST occur ('duration', 'repeat')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
CaselessDict.__init__(self, *args, **kwargs)
|
||||
# set parameters here for properties that use non-default values
|
||||
self.subcomponents = [] # Components can be nested.
|
||||
|
||||
|
||||
# def non_complience(self, warnings=0):
|
||||
# """
|
||||
# not implemented yet!
|
||||
# Returns a dict describing non compliant properties, if any.
|
||||
# If warnings is true it also returns warnings.
|
||||
#
|
||||
# If the parser is too strict it might prevent parsing erroneous but
|
||||
# otherwise compliant properties. So the parser is pretty lax, but it is
|
||||
# possible to test for non-complience by calling this method.
|
||||
# """
|
||||
# nc = {}
|
||||
# if not getattr(self, 'name', ''):
|
||||
# nc['name'] = {'type':'ERROR', 'description':'Name is not defined'}
|
||||
# return nc
|
||||
|
||||
|
||||
#############################
|
||||
# handling of property values
|
||||
|
||||
def _encode(self, name, value, cond=1):
|
||||
# internal, for conditional convertion of values.
|
||||
if cond:
|
||||
klass = types_factory.for_property(name)
|
||||
return klass(value)
|
||||
return value
|
||||
|
||||
|
||||
def set(self, name, value, encode=1): #@ReservedAssignment
|
||||
if type(value) == ListType:
|
||||
self[name] = [self._encode(name, v, encode) for v in value]
|
||||
else:
|
||||
self[name] = self._encode(name, value, encode)
|
||||
|
||||
|
||||
def add(self, name, value, encode=1):
|
||||
"If property exists append, else create and set it"
|
||||
if name in self:
|
||||
oldval = self[name]
|
||||
value = self._encode(name, value, encode)
|
||||
if type(oldval) == ListType:
|
||||
oldval.append(value)
|
||||
else:
|
||||
self.set(name, [oldval, value], encode=0)
|
||||
else:
|
||||
self.set(name, value, encode)
|
||||
|
||||
|
||||
def _decode(self, name, value):
|
||||
# internal for decoding property values
|
||||
decoded = types_factory.from_ical(name, value)
|
||||
return decoded
|
||||
|
||||
|
||||
def decoded(self, name, default=_marker):
|
||||
"Returns decoded value of property"
|
||||
if name in self:
|
||||
value = self[name]
|
||||
if type(value) == ListType:
|
||||
return [self._decode(name, v) for v in value]
|
||||
return self._decode(name, value)
|
||||
else:
|
||||
if default is _marker:
|
||||
raise KeyError, name
|
||||
else:
|
||||
return default
|
||||
|
||||
|
||||
########################################################################
|
||||
# Inline values. A few properties have multiple values inlined in in one
|
||||
# property line. These methods are used for splitting and joining these.
|
||||
|
||||
def get_inline(self, name, decode=1):
|
||||
"""
|
||||
Returns a list of values (split on comma).
|
||||
"""
|
||||
vals = [v.strip('" ').encode(vText.encoding)
|
||||
for v in q_split(self[name])]
|
||||
if decode:
|
||||
return [self._decode(name, val) for val in vals]
|
||||
return vals
|
||||
|
||||
|
||||
def set_inline(self, name, values, encode=1):
|
||||
"""
|
||||
Converts a list of values into comma seperated string and sets value to
|
||||
that.
|
||||
"""
|
||||
if encode:
|
||||
values = [self._encode(name, value, 1) for value in values]
|
||||
joined = q_join(values).encode(vText.encoding)
|
||||
self[name] = types_factory['inline'](joined)
|
||||
|
||||
|
||||
#########################
|
||||
# Handling of components
|
||||
|
||||
def add_component(self, component):
|
||||
"add a subcomponent to this component"
|
||||
self.subcomponents.append(component)
|
||||
|
||||
|
||||
def _walk(self, name):
|
||||
# private!
|
||||
result = []
|
||||
if name is None or self.name == name:
|
||||
result.append(self)
|
||||
for subcomponent in self.subcomponents:
|
||||
result += subcomponent._walk(name)
|
||||
return result
|
||||
|
||||
|
||||
def walk(self, name=None):
|
||||
"""
|
||||
Recursively traverses component and subcomponents. Returns sequence of
|
||||
same. If name is passed, only components with name will be returned.
|
||||
"""
|
||||
if not name is None:
|
||||
name = name.upper()
|
||||
return self._walk(name)
|
||||
|
||||
#####################
|
||||
# Generation
|
||||
|
||||
def property_items(self):
|
||||
"""
|
||||
Returns properties in this component and subcomponents as:
|
||||
[(name, value), ...]
|
||||
"""
|
||||
vText = types_factory['text']
|
||||
properties = [('BEGIN', vText(self.name).ical())]
|
||||
property_names = self.keys()
|
||||
property_names.sort()
|
||||
for name in property_names:
|
||||
values = self[name]
|
||||
if type(values) == ListType:
|
||||
# normally one property is one line
|
||||
for value in values:
|
||||
properties.append((name, value))
|
||||
else:
|
||||
properties.append((name, values))
|
||||
# recursion is fun!
|
||||
for subcomponent in self.subcomponents:
|
||||
properties += subcomponent.property_items()
|
||||
properties.append(('END', vText(self.name).ical()))
|
||||
return properties
|
||||
|
||||
|
||||
def from_string(st, multiple=False):
|
||||
"""
|
||||
Populates the component recursively from a string
|
||||
"""
|
||||
stack = [] # a stack of components
|
||||
comps = []
|
||||
for line in Contentlines.from_string(st): # raw parsing
|
||||
if not line:
|
||||
continue
|
||||
name, params, vals = line.parts()
|
||||
uname = name.upper()
|
||||
# check for start of component
|
||||
if uname == 'BEGIN':
|
||||
# try and create one of the components defined in the spec,
|
||||
# otherwise get a general Components for robustness.
|
||||
component_name = vals.upper()
|
||||
component_class = component_factory.get(component_name, Component)
|
||||
component = component_class()
|
||||
if not getattr(component, 'name', ''): # for undefined components
|
||||
component.name = component_name
|
||||
stack.append(component)
|
||||
# check for end of event
|
||||
elif uname == 'END':
|
||||
# we are done adding properties to this component
|
||||
# so pop it from the stack and add it to the new top.
|
||||
component = stack.pop()
|
||||
if not stack: # we are at the end
|
||||
comps.append(component)
|
||||
else:
|
||||
stack[-1].add_component(component)
|
||||
# we are adding properties to the current top of the stack
|
||||
else:
|
||||
factory = types_factory.for_property(name)
|
||||
vals = factory(factory.from_ical(vals))
|
||||
vals.params = params
|
||||
stack[-1].add(name, vals, encode=0)
|
||||
if multiple:
|
||||
return comps
|
||||
if not len(comps) == 1:
|
||||
raise ValueError('Found multiple components where '
|
||||
'only one is allowed')
|
||||
return comps[0]
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(' % self.name + dict.__repr__(self) + ')'
|
||||
|
||||
# def content_line(self, name):
|
||||
# "Returns property as content line"
|
||||
# value = self[name]
|
||||
# params = getattr(value, 'params', Parameters())
|
||||
# return Contentline.from_parts((name, params, value))
|
||||
|
||||
def content_lines(self):
|
||||
"Converts the Component and subcomponents into content lines"
|
||||
contentlines = Contentlines()
|
||||
for name, values in self.property_items():
|
||||
params = getattr(values, 'params', Parameters())
|
||||
contentlines.append(Contentline.from_parts((name, params, values)))
|
||||
contentlines.append('') # remember the empty string in the end
|
||||
return contentlines
|
||||
|
||||
|
||||
def as_string(self):
|
||||
return str(self.content_lines())
|
||||
|
||||
|
||||
def __str__(self):
|
||||
"Returns rendered iCalendar"
|
||||
return self.as_string()
|
||||
|
||||
|
||||
|
||||
#######################################
|
||||
# components defined in RFC 2445
|
||||
|
||||
|
||||
class Event(Component):
|
||||
|
||||
name = 'VEVENT'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'GEO',
|
||||
'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PRIORITY', 'DTSTAMP', 'SEQUENCE',
|
||||
'STATUS', 'SUMMARY', 'TRANSP', 'URL', 'RECURID', 'DTEND', 'DURATION',
|
||||
'DTSTART',
|
||||
)
|
||||
exclusive = ('DTEND', 'DURATION',)
|
||||
multiple = (
|
||||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
|
||||
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Todo(Component):
|
||||
|
||||
name = 'VTODO'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CLASS', 'COMPLETED', 'CREATED', 'DESCRIPTION', 'DTSTAMP', 'DTSTART',
|
||||
'GEO', 'LAST-MOD', 'LOCATION', 'ORGANIZER', 'PERCENT', 'PRIORITY',
|
||||
'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL', 'DUE', 'DURATION',
|
||||
)
|
||||
exclusive = ('DUE', 'DURATION',)
|
||||
multiple = (
|
||||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
|
||||
'EXRULE', 'RSTATUS', 'RELATED', 'RESOURCES', 'RDATE', 'RRULE'
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Journal(Component):
|
||||
|
||||
name = 'VJOURNAL'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CLASS', 'CREATED', 'DESCRIPTION', 'DTSTART', 'DTSTAMP', 'LAST-MOD',
|
||||
'ORGANIZER', 'RECURID', 'SEQUENCE', 'STATUS', 'SUMMARY', 'UID', 'URL',
|
||||
)
|
||||
multiple = (
|
||||
'ATTACH', 'ATTENDEE', 'CATEGORIES', 'COMMENT', 'CONTACT', 'EXDATE',
|
||||
'EXRULE', 'RELATED', 'RDATE', 'RRULE', 'RSTATUS',
|
||||
)
|
||||
|
||||
|
||||
class FreeBusy(Component):
|
||||
|
||||
name = 'VFREEBUSY'
|
||||
|
||||
required = ('UID',)
|
||||
singletons = (
|
||||
'CONTACT', 'DTSTART', 'DTEND', 'DURATION', 'DTSTAMP', 'ORGANIZER',
|
||||
'UID', 'URL',
|
||||
)
|
||||
multiple = ('ATTENDEE', 'COMMENT', 'FREEBUSY', 'RSTATUS',)
|
||||
|
||||
|
||||
class Timezone(Component):
|
||||
|
||||
name = 'VTIMEZONE'
|
||||
|
||||
required = (
|
||||
'TZID', 'STANDARDC', 'DAYLIGHTC', 'DTSTART', 'TZOFFSETTO',
|
||||
'TZOFFSETFROM'
|
||||
)
|
||||
singletons = ('LAST-MOD', 'TZURL', 'TZID',)
|
||||
multiple = ('COMMENT', 'RDATE', 'RRULE', 'TZNAME',)
|
||||
|
||||
|
||||
class Alarm(Component):
|
||||
|
||||
name = 'VALARM'
|
||||
# not quite sure about these ...
|
||||
required = ('ACTION', 'TRIGGER',)
|
||||
singletons = ('ATTACH', 'ACTION', 'TRIGGER', 'DURATION', 'REPEAT',)
|
||||
inclusive = (('DURATION', 'REPEAT',),)
|
||||
multiple = ('STANDARDC', 'DAYLIGHTC')
|
||||
|
||||
|
||||
class Calendar(Component):
|
||||
"""
|
||||
This is the base object for an iCalendar file.
|
||||
|
||||
Setting up a minimal calendar component looks like this
|
||||
>>> cal = Calendar()
|
||||
|
||||
Som properties are required to be compliant
|
||||
>>> cal['prodid'] = '-//My calendar product//mxm.dk//'
|
||||
>>> cal['version'] = '2.0'
|
||||
|
||||
We also need at least one subcomponent for a calendar to be compliant
|
||||
>>> from datetime import datetime
|
||||
>>> event = Event()
|
||||
>>> event['summary'] = 'Python meeting about calendaring'
|
||||
>>> event['uid'] = '42'
|
||||
>>> event.set('dtstart', datetime(2005,4,4,8,0,0))
|
||||
>>> cal.add_component(event)
|
||||
>>> cal.subcomponents[0].as_string()
|
||||
'BEGIN:VEVENT\\r\\nDTSTART:20050404T080000\\r\\nSUMMARY:Python meeting about calendaring\\r\\nUID:42\\r\\nEND:VEVENT\\r\\n'
|
||||
|
||||
Write to disc
|
||||
>>> import tempfile, os
|
||||
>>> directory = tempfile.mkdtemp()
|
||||
>>> open(os.path.join(directory, 'test.ics'), 'wb').write(cal.as_string())
|
||||
"""
|
||||
|
||||
name = 'VCALENDAR'
|
||||
required = ('prodid', 'version',)
|
||||
singletons = ('prodid', 'version',)
|
||||
multiple = ('calscale', 'method',)
|
||||
|
||||
|
||||
# These are read only singleton, so one instance is enough for the module
|
||||
types_factory = TypesFactory()
|
||||
component_factory = ComponentFactory()
|
||||
93
utils/icalendar/caselessdict.py
Normal file
93
utils/icalendar/caselessdict.py
Normal file
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
class CaselessDict(dict):
|
||||
"""
|
||||
A dictionary that isn't case sensitive, and only use string as keys.
|
||||
|
||||
>>> ncd = CaselessDict(key1='val1', key2='val2')
|
||||
>>> ncd
|
||||
CaselessDict({'KEY2': 'val2', 'KEY1': 'val1'})
|
||||
>>> ncd['key1']
|
||||
'val1'
|
||||
>>> ncd['KEY1']
|
||||
'val1'
|
||||
>>> ncd['KEY3'] = 'val3'
|
||||
>>> ncd['key3']
|
||||
'val3'
|
||||
>>> ncd.setdefault('key3', 'FOUND')
|
||||
'val3'
|
||||
>>> ncd.setdefault('key4', 'NOT FOUND')
|
||||
'NOT FOUND'
|
||||
>>> ncd['key4']
|
||||
'NOT FOUND'
|
||||
>>> ncd.get('key1')
|
||||
'val1'
|
||||
>>> ncd.get('key3', 'NOT FOUND')
|
||||
'val3'
|
||||
>>> ncd.get('key4', 'NOT FOUND')
|
||||
'NOT FOUND'
|
||||
>>> 'key4' in ncd
|
||||
True
|
||||
>>> del ncd['key4']
|
||||
>>> ncd.has_key('key4')
|
||||
False
|
||||
>>> ncd.update({'key5':'val5', 'KEY6':'val6', 'KEY5':'val7'})
|
||||
>>> ncd['key6']
|
||||
'val6'
|
||||
>>> keys = ncd.keys()
|
||||
>>> keys.sort()
|
||||
>>> keys
|
||||
['KEY1', 'KEY2', 'KEY3', 'KEY5', 'KEY6']
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Set keys to upper for initial dict"
|
||||
dict.__init__(self, *args, **kwargs)
|
||||
for k,v in self.items():
|
||||
k_upper = k.upper()
|
||||
if k != k_upper:
|
||||
dict.__delitem__(self, k)
|
||||
self[k_upper] = v
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, key.upper())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key.upper(), value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key.upper())
|
||||
|
||||
def __contains__(self, item):
|
||||
return dict.__contains__(self, item.upper())
|
||||
|
||||
def get(self, key, default=None):
|
||||
return dict.get(self, key.upper(), default)
|
||||
|
||||
def setdefault(self, key, value=None):
|
||||
return dict.setdefault(self, key.upper(), value)
|
||||
|
||||
def pop(self, key, default=None):
|
||||
return dict.pop(self, key.upper(), default)
|
||||
|
||||
def popitem(self):
|
||||
return dict.popitem(self)
|
||||
|
||||
def has_key(self, key):
|
||||
return dict.has_key(self, key.upper())
|
||||
|
||||
def update(self, indict):
|
||||
"""
|
||||
Multiple keys where key1.upper() == key2.upper() will be lost.
|
||||
"""
|
||||
for entry in indict:
|
||||
self[entry] = indict[entry]
|
||||
|
||||
def copy(self):
|
||||
return CaselessDict(dict.copy(self))
|
||||
|
||||
def clear(self):
|
||||
dict.clear(self)
|
||||
|
||||
def __repr__(self):
|
||||
return 'CaselessDict(' + dict.__repr__(self) + ')'
|
||||
256
utils/icalendar/interfaces.py
Normal file
256
utils/icalendar/interfaces.py
Normal file
@@ -0,0 +1,256 @@
|
||||
try:
|
||||
from zope.interface import Interface, Attribute
|
||||
except ImportError:
|
||||
class Interface:
|
||||
"""A dummy interface base class"""
|
||||
|
||||
class Attribute:
|
||||
"""A dummy attribute implementation"""
|
||||
def __init__(self, doc):
|
||||
self.doc = doc
|
||||
|
||||
_marker = object()
|
||||
|
||||
class IComponent(Interface):
|
||||
"""
|
||||
Component is the base object for calendar, Event and the other
|
||||
components defined in RFC 2445.
|
||||
|
||||
A component is like a dictionary with extra methods and attributes.
|
||||
"""
|
||||
|
||||
# MANIPULATORS
|
||||
def __setitem__(self, name, value):
|
||||
"""Set a property.
|
||||
|
||||
name - case insensitive name
|
||||
value - value of the property to set. This can be either a single
|
||||
item or a list.
|
||||
|
||||
Some iCalendar properties are set INLINE; these properties
|
||||
have multiple values on one property line in the iCalendar
|
||||
representation. The list can be supplied as a comma separated
|
||||
string to __setitem__. If special iCalendar characters exist in
|
||||
an entry, such as the colon (:) and (,), that comma-separated
|
||||
entry needs to be quoted with double quotes. For example:
|
||||
|
||||
'foo, bar, "baz:hoi"'
|
||||
|
||||
See also set_inline() for an easier way to deal with this case.
|
||||
"""
|
||||
def set_inline(self, name, values, encode=1):
|
||||
"""Set list of INLINE values for property.
|
||||
|
||||
Converts a list of values into valid iCalendar comma seperated
|
||||
string and sets value to that.
|
||||
|
||||
name - case insensitive name of property
|
||||
values - list of values to set
|
||||
encode - if True, encode Python values as iCalendar types first.
|
||||
"""
|
||||
def add(self, name, value):
|
||||
"""Add a property. Can be called multiple times to set a list.
|
||||
|
||||
name - case insensitive name
|
||||
value - value of property to set or add to list for this property.
|
||||
"""
|
||||
def add_component(self, component):
|
||||
"""Add a nested subcomponent to this component.
|
||||
"""
|
||||
|
||||
# static method, can be called on class directly
|
||||
def from_string(self, st, multiple=False):
|
||||
"""Populates the component recursively from a iCalendar string.
|
||||
|
||||
Reads the iCalendar string and constructs components and
|
||||
subcomponents out of it.
|
||||
"""
|
||||
|
||||
# ACCESSORS
|
||||
def __getitem__(self, name):
|
||||
"""Get a property
|
||||
|
||||
name - case insensitive name
|
||||
|
||||
Returns an iCalendar property object such as vText.
|
||||
"""
|
||||
def decoded(self, name, default=_marker):
|
||||
"""Get a property as a python object.
|
||||
|
||||
name - case insensitive name
|
||||
default - optional argument. If supplied, will use this if
|
||||
name cannot be found. If not supplied, decoded will raise a
|
||||
KeyError if name cannot be found.
|
||||
|
||||
Returns python object (such as unicode string, datetime, etc).
|
||||
"""
|
||||
def get_inline(self, name, decode=1):
|
||||
"""Get list of INLINE values from property.
|
||||
|
||||
name - case insensitive name
|
||||
decode - decode to Python objects.
|
||||
|
||||
Returns list of python objects.
|
||||
"""
|
||||
|
||||
def as_string(self):
|
||||
"""Render the component in the RFC 2445 (iCalendar) format.
|
||||
|
||||
Returns a string in RFC 2445 format.
|
||||
"""
|
||||
|
||||
subcomponents = Attribute("""
|
||||
A list of all subcomponents of this component,
|
||||
added using add_component()""")
|
||||
|
||||
name = Attribute("""
|
||||
Name of this component (VEVENT, etc)
|
||||
""")
|
||||
|
||||
def walk(self, name=None):
|
||||
"""Recursively traverses component and subcomponents.
|
||||
|
||||
name - optional, if given, only return components with that name
|
||||
|
||||
Returns sequence of components.
|
||||
"""
|
||||
|
||||
def property_items(self):
|
||||
"""Return properties as (name, value) tuples.
|
||||
|
||||
Returns all properties in this comopnent and subcomponents as
|
||||
name, value tuples.
|
||||
"""
|
||||
|
||||
class IEvent(IComponent):
|
||||
"""A component which conforms to an iCalendar VEVENT.
|
||||
"""
|
||||
|
||||
class ITodo(IComponent):
|
||||
"""A component which conforms to an iCalendar VTODO.
|
||||
"""
|
||||
|
||||
class IJournal(IComponent):
|
||||
"""A component which conforms to an iCalendar VJOURNAL.
|
||||
"""
|
||||
|
||||
class IFreeBusy(IComponent):
|
||||
"""A component which conforms to an iCalendar VFREEBUSY.
|
||||
"""
|
||||
|
||||
class ITimezone(IComponent):
|
||||
"""A component which conforms to an iCalendar VTIMEZONE.
|
||||
"""
|
||||
|
||||
class IAlarm(IComponent):
|
||||
"""A component which conforms to an iCalendar VALARM.
|
||||
"""
|
||||
|
||||
class ICalendar(IComponent):
|
||||
"""A component which conforms to an iCalendar VCALENDAR.
|
||||
"""
|
||||
|
||||
class IPropertyValue(Interface):
|
||||
"""An iCalendar property value.
|
||||
iCalendar properties have strongly typed values.
|
||||
|
||||
This invariance should always be true:
|
||||
|
||||
assert x == vDataType.from_ical(vDataType(x).ical())
|
||||
"""
|
||||
|
||||
def ical(self):
|
||||
"""Render property as string, as defined in iCalendar RFC 2445.
|
||||
"""
|
||||
|
||||
# this is a static method
|
||||
def from_ical(self, ical):
|
||||
"""Parse property from iCalendar RFC 2445 text.
|
||||
|
||||
Inverse of ical().
|
||||
"""
|
||||
|
||||
class IBinary(IPropertyValue):
|
||||
"""Binary property values are base 64 encoded
|
||||
"""
|
||||
|
||||
class IBoolean(IPropertyValue):
|
||||
"""Boolean property.
|
||||
|
||||
Also behaves like a python int.
|
||||
"""
|
||||
|
||||
class ICalAddress(IPropertyValue):
|
||||
"""Email address.
|
||||
|
||||
Also behaves like a python str.
|
||||
"""
|
||||
|
||||
class IDateTime(IPropertyValue):
|
||||
"""Render and generates iCalendar datetime format.
|
||||
|
||||
Important: if tzinfo is defined it renders itself as 'date with utc time'
|
||||
Meaning that it has a 'Z' appended, and is in absolute time.
|
||||
"""
|
||||
|
||||
class IDate(IPropertyValue):
|
||||
"""Render and generates iCalendar date format.
|
||||
"""
|
||||
|
||||
class IDuration(IPropertyValue):
|
||||
"""Render and generates timedelta in iCalendar DURATION format.
|
||||
"""
|
||||
|
||||
class IFloat(IPropertyValue):
|
||||
"""Render and generate floats in iCalendar format.
|
||||
|
||||
Also behaves like a python float.
|
||||
"""
|
||||
|
||||
class IInt(IPropertyValue):
|
||||
"""Render and generate ints in iCalendar format.
|
||||
|
||||
Also behaves like a python int.
|
||||
"""
|
||||
|
||||
class IPeriod(IPropertyValue):
|
||||
"""A precise period of time (datetime, datetime).
|
||||
"""
|
||||
|
||||
class IWeekDay(IPropertyValue):
|
||||
"""Render and generate weekday abbreviation.
|
||||
"""
|
||||
|
||||
class IFrequency(IPropertyValue):
|
||||
"""Frequency.
|
||||
"""
|
||||
|
||||
class IRecur(IPropertyValue):
|
||||
"""Render and generate data based on recurrent event representation.
|
||||
|
||||
This acts like a caseless dictionary.
|
||||
"""
|
||||
|
||||
class IText(IPropertyValue):
|
||||
"""Unicode text.
|
||||
"""
|
||||
|
||||
class ITime(IPropertyValue):
|
||||
"""Time.
|
||||
"""
|
||||
|
||||
class IUri(IPropertyValue):
|
||||
"""URI
|
||||
"""
|
||||
|
||||
class IGeo(IPropertyValue):
|
||||
"""Geographical location.
|
||||
"""
|
||||
|
||||
class IUTCOffset(IPropertyValue):
|
||||
"""Offset from UTC.
|
||||
"""
|
||||
|
||||
class IInline(IPropertyValue):
|
||||
"""Inline list.
|
||||
"""
|
||||
522
utils/icalendar/parser.py
Normal file
522
utils/icalendar/parser.py
Normal file
@@ -0,0 +1,522 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
This module parses and generates contentlines as defined in RFC 2445
|
||||
(iCalendar), but will probably work for other MIME types with similar syntax.
|
||||
Eg. RFC 2426 (vCard)
|
||||
|
||||
It is stupid in the sense that it treats the content purely as strings. No type
|
||||
conversion is attempted.
|
||||
|
||||
Copyright, 2005: Max M <maxm@mxm.dk>
|
||||
License: GPL (Just contact med if and why you would like it changed)
|
||||
"""
|
||||
|
||||
# from python
|
||||
from types import TupleType, ListType
|
||||
SequenceTypes = [TupleType, ListType]
|
||||
import re
|
||||
# from this package
|
||||
from .caselessdict import CaselessDict
|
||||
|
||||
|
||||
#################################################################
|
||||
# Property parameter stuff
|
||||
|
||||
def paramVal(val):
|
||||
"Returns a parameter value"
|
||||
if type(val) in SequenceTypes:
|
||||
return q_join(val)
|
||||
return dQuote(val)
|
||||
|
||||
# Could be improved
|
||||
NAME = re.compile('[\w-]+')
|
||||
UNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F",:;]')
|
||||
QUNSAFE_CHAR = re.compile('[\x00-\x08\x0a-\x1f\x7F"]')
|
||||
FOLD = re.compile('([\r]?\n)+[ \t]{1}')
|
||||
|
||||
def validate_token(name):
|
||||
match = NAME.findall(name)
|
||||
if len(match) == 1 and name == match[0]:
|
||||
return
|
||||
raise ValueError, name
|
||||
|
||||
def validate_param_value(value, quoted=True):
|
||||
validator = UNSAFE_CHAR
|
||||
if quoted:
|
||||
validator = QUNSAFE_CHAR
|
||||
if validator.findall(value):
|
||||
raise ValueError, value
|
||||
|
||||
QUOTABLE = re.compile('[,;:].')
|
||||
def dQuote(val):
|
||||
"""
|
||||
Parameter values containing [,;:] must be double quoted
|
||||
>>> dQuote('Max')
|
||||
'Max'
|
||||
>>> dQuote('Rasmussen, Max')
|
||||
'"Rasmussen, Max"'
|
||||
>>> dQuote('name:value')
|
||||
'"name:value"'
|
||||
"""
|
||||
if QUOTABLE.search(val):
|
||||
return '"%s"' % val
|
||||
return val
|
||||
|
||||
# parsing helper
|
||||
def q_split(st, sep=','):
|
||||
"""
|
||||
Splits a string on char, taking double (q)uotes into considderation
|
||||
>>> q_split('Max,Moller,"Rasmussen, Max"')
|
||||
['Max', 'Moller', '"Rasmussen, Max"']
|
||||
"""
|
||||
result = []
|
||||
cursor = 0
|
||||
length = len(st)
|
||||
inquote = 0
|
||||
for i in range(length):
|
||||
ch = st[i]
|
||||
if ch == '"':
|
||||
inquote = not inquote
|
||||
if not inquote and ch == sep:
|
||||
result.append(st[cursor:i])
|
||||
cursor = i + 1
|
||||
if i + 1 == length:
|
||||
result.append(st[cursor:])
|
||||
return result
|
||||
|
||||
def q_join(lst, sep=','):
|
||||
"""
|
||||
Joins a list on sep, quoting strings with QUOTABLE chars
|
||||
>>> s = ['Max', 'Moller', 'Rasmussen, Max']
|
||||
>>> q_join(s)
|
||||
'Max,Moller,"Rasmussen, Max"'
|
||||
"""
|
||||
return sep.join([dQuote(itm) for itm in lst])
|
||||
|
||||
class Parameters(CaselessDict):
|
||||
"""
|
||||
Parser and generator of Property parameter strings. It knows nothing of
|
||||
datatypes. It's main concern is textual structure.
|
||||
|
||||
|
||||
Simple parameter:value pair
|
||||
>>> p = Parameters(parameter1='Value1')
|
||||
>>> str(p)
|
||||
'PARAMETER1=Value1'
|
||||
|
||||
|
||||
keys are converted to upper
|
||||
>>> p.keys()
|
||||
['PARAMETER1']
|
||||
|
||||
|
||||
Parameters are case insensitive
|
||||
>>> p['parameter1']
|
||||
'Value1'
|
||||
>>> p['PARAMETER1']
|
||||
'Value1'
|
||||
|
||||
|
||||
Parameter with list of values must be seperated by comma
|
||||
>>> p = Parameters({'parameter1':['Value1', 'Value2']})
|
||||
>>> str(p)
|
||||
'PARAMETER1=Value1,Value2'
|
||||
|
||||
|
||||
Multiple parameters must be seperated by a semicolon
|
||||
>>> p = Parameters({'RSVP':'TRUE', 'ROLE':'REQ-PARTICIPANT'})
|
||||
>>> str(p)
|
||||
'ROLE=REQ-PARTICIPANT;RSVP=TRUE'
|
||||
|
||||
|
||||
Parameter values containing ',;:' must be double quoted
|
||||
>>> p = Parameters({'ALTREP':'http://www.wiz.org'})
|
||||
>>> str(p)
|
||||
'ALTREP="http://www.wiz.org"'
|
||||
|
||||
|
||||
list items must be quoted seperately
|
||||
>>> p = Parameters({'MEMBER':['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com', ]})
|
||||
>>> str(p)
|
||||
'MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"'
|
||||
|
||||
Now the whole sheebang
|
||||
>>> p = Parameters({'parameter1':'Value1', 'parameter2':['Value2', 'Value3'],\
|
||||
'ALTREP':['http://www.wiz.org', 'value4']})
|
||||
>>> str(p)
|
||||
'ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3'
|
||||
|
||||
We can also parse parameter strings
|
||||
>>> Parameters.from_string('PARAMETER1=Value 1;param2=Value 2')
|
||||
Parameters({'PARAMETER1': 'Value 1', 'PARAM2': 'Value 2'})
|
||||
|
||||
Including empty strings
|
||||
>>> Parameters.from_string('param=')
|
||||
Parameters({'PARAM': ''})
|
||||
|
||||
We can also parse parameter strings
|
||||
>>> Parameters.from_string('MEMBER="MAILTO:projectA@host.com","MAILTO:projectB@host.com"')
|
||||
Parameters({'MEMBER': ['MAILTO:projectA@host.com', 'MAILTO:projectB@host.com']})
|
||||
|
||||
We can also parse parameter strings
|
||||
>>> Parameters.from_string('ALTREP="http://www.wiz.org",value4;PARAMETER1=Value1;PARAMETER2=Value2,Value3')
|
||||
Parameters({'PARAMETER1': 'Value1', 'ALTREP': ['http://www.wiz.org', 'value4'], 'PARAMETER2': ['Value2', 'Value3']})
|
||||
"""
|
||||
|
||||
|
||||
def params(self):
|
||||
"""
|
||||
in rfc2445 keys are called parameters, so this is to be consitent with
|
||||
the naming conventions
|
||||
"""
|
||||
return self.keys()
|
||||
|
||||
### Later, when I get more time... need to finish this off now. The last majot thing missing.
|
||||
### def _encode(self, name, value, cond=1):
|
||||
### # internal, for conditional convertion of values.
|
||||
### if cond:
|
||||
### klass = types_factory.for_property(name)
|
||||
### return klass(value)
|
||||
### return value
|
||||
###
|
||||
### def add(self, name, value, encode=0):
|
||||
### "Add a parameter value and optionally encode it."
|
||||
### if encode:
|
||||
### value = self._encode(name, value, encode)
|
||||
### self[name] = value
|
||||
###
|
||||
### def decoded(self, name):
|
||||
### "returns a decoded value, or list of same"
|
||||
|
||||
def __repr__(self):
|
||||
return 'Parameters(' + dict.__repr__(self) + ')'
|
||||
|
||||
|
||||
def __str__(self):
|
||||
result = []
|
||||
items = self.items()
|
||||
items.sort() # To make doctests work
|
||||
for key, value in items:
|
||||
value = paramVal(value)
|
||||
result.append('%s=%s' % (key.upper(), value))
|
||||
return ';'.join(result)
|
||||
|
||||
|
||||
def from_string(st, strict=False):
|
||||
"Parses the parameter format from ical text format"
|
||||
try:
|
||||
# parse into strings
|
||||
result = Parameters()
|
||||
for param in q_split(st, ';'):
|
||||
key, val = q_split(param, '=')
|
||||
validate_token(key)
|
||||
param_values = [v for v in q_split(val, ',')]
|
||||
# Property parameter values that are not in quoted
|
||||
# strings are case insensitive.
|
||||
vals = []
|
||||
for v in param_values:
|
||||
if v.startswith('"') and v.endswith('"'):
|
||||
v = v.strip('"')
|
||||
validate_param_value(v, quoted=True)
|
||||
vals.append(v)
|
||||
else:
|
||||
validate_param_value(v, quoted=False)
|
||||
if strict:
|
||||
vals.append(v.upper())
|
||||
else:
|
||||
vals.append(v)
|
||||
if not vals:
|
||||
result[key] = val
|
||||
else:
|
||||
if len(vals) == 1:
|
||||
result[key] = vals[0]
|
||||
else:
|
||||
result[key] = vals
|
||||
return result
|
||||
except:
|
||||
raise ValueError, 'Not a valid parameter string'
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
|
||||
#########################################
|
||||
# parsing and generation of content lines
|
||||
|
||||
class Contentline(str):
|
||||
"""
|
||||
A content line is basically a string that can be folded and parsed into
|
||||
parts.
|
||||
|
||||
>>> c = Contentline('Si meliora dies, ut vina, poemata reddit')
|
||||
>>> str(c)
|
||||
'Si meliora dies, ut vina, poemata reddit'
|
||||
|
||||
A long line gets folded
|
||||
>>> c = Contentline(''.join(['123456789 ']*10))
|
||||
>>> str(c)
|
||||
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 '
|
||||
|
||||
A folded line gets unfolded
|
||||
>>> c = Contentline.from_string(str(c))
|
||||
>>> c
|
||||
'123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 '
|
||||
|
||||
We do not fold within a UTF-8 character:
|
||||
>>> c = Contentline('This line has a UTF-8 character where it should be folded. Make sure it g\xc3\xabts folded before that character.')
|
||||
>>> '\xc3\xab' in str(c)
|
||||
True
|
||||
|
||||
Don't fail if we fold a line that is exactly X times 74 characters long:
|
||||
>>> c = str(Contentline(''.join(['x']*148)))
|
||||
|
||||
It can parse itself into parts. Which is a tuple of (name, params, vals)
|
||||
|
||||
>>> c = Contentline('dtstart:20050101T120000')
|
||||
>>> c.parts()
|
||||
('dtstart', Parameters({}), '20050101T120000')
|
||||
|
||||
>>> c = Contentline('dtstart;value=datetime:20050101T120000')
|
||||
>>> c.parts()
|
||||
('dtstart', Parameters({'VALUE': 'datetime'}), '20050101T120000')
|
||||
|
||||
>>> c = Contentline('ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com')
|
||||
>>> c.parts()
|
||||
('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
|
||||
>>> str(c)
|
||||
'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
|
||||
|
||||
and back again
|
||||
>>> parts = ('ATTENDEE', Parameters({'ROLE': 'REQ-PARTICIPANT', 'CN': 'Max Rasmussen'}), 'MAILTO:maxm@example.com')
|
||||
>>> Contentline.from_parts(parts)
|
||||
'ATTENDEE;CN=Max Rasmussen;ROLE=REQ-PARTICIPANT:MAILTO:maxm@example.com'
|
||||
|
||||
and again
|
||||
>>> parts = ('ATTENDEE', Parameters(), 'MAILTO:maxm@example.com')
|
||||
>>> Contentline.from_parts(parts)
|
||||
'ATTENDEE:MAILTO:maxm@example.com'
|
||||
|
||||
A value can also be any of the types defined in PropertyValues
|
||||
>>> from icalendar.prop import vText
|
||||
>>> parts = ('ATTENDEE', Parameters(), vText('MAILTO:test@example.com'))
|
||||
>>> Contentline.from_parts(parts)
|
||||
'ATTENDEE:MAILTO:test@example.com'
|
||||
|
||||
A value can also be unicode
|
||||
>>> from icalendar.prop import vText
|
||||
>>> parts = ('SUMMARY', Parameters(), vText(u'INternational char <20> <20> <20>'))
|
||||
>>> Contentline.from_parts(parts)
|
||||
'SUMMARY:INternational char \\xc3\\xa6 \\xc3\\xb8 \\xc3\\xa5'
|
||||
|
||||
Traversing could look like this.
|
||||
>>> name, params, vals = c.parts()
|
||||
>>> name
|
||||
'ATTENDEE'
|
||||
>>> vals
|
||||
'MAILTO:maxm@example.com'
|
||||
>>> for key, val in params.items():
|
||||
... (key, val)
|
||||
('ROLE', 'REQ-PARTICIPANT')
|
||||
('CN', 'Max Rasmussen')
|
||||
|
||||
And the traditional failure
|
||||
>>> c = Contentline('ATTENDEE;maxm@example.com')
|
||||
>>> c.parts()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Content line could not be parsed into parts
|
||||
|
||||
Another failure:
|
||||
>>> c = Contentline(':maxm@example.com')
|
||||
>>> c.parts()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Content line could not be parsed into parts
|
||||
|
||||
>>> c = Contentline('key;param=:value')
|
||||
>>> c.parts()
|
||||
('key', Parameters({'PARAM': ''}), 'value')
|
||||
|
||||
>>> c = Contentline('key;param="pvalue":value')
|
||||
>>> c.parts()
|
||||
('key', Parameters({'PARAM': 'pvalue'}), 'value')
|
||||
|
||||
Should bomb on missing param:
|
||||
>>> c = Contentline.from_string("k;:no param")
|
||||
>>> c.parts()
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Content line could not be parsed into parts
|
||||
|
||||
>>> c = Contentline('key;param=pvalue:value', strict=False)
|
||||
>>> c.parts()
|
||||
('key', Parameters({'PARAM': 'pvalue'}), 'value')
|
||||
|
||||
If strict is set to True, uppercase param values that are not
|
||||
double-quoted, this is because the spec says non-quoted params are
|
||||
case-insensitive.
|
||||
|
||||
>>> c = Contentline('key;param=pvalue:value', strict=True)
|
||||
>>> c.parts()
|
||||
('key', Parameters({'PARAM': 'PVALUE'}), 'value')
|
||||
|
||||
>>> c = Contentline('key;param="pValue":value', strict=True)
|
||||
>>> c.parts()
|
||||
('key', Parameters({'PARAM': 'pValue'}), 'value')
|
||||
|
||||
"""
|
||||
|
||||
def __new__(cls, st, strict=False):
|
||||
self = str.__new__(cls, st)
|
||||
setattr(self, 'strict', strict)
|
||||
return self
|
||||
|
||||
def from_parts(parts):
|
||||
"Turns a tuple of parts into a content line"
|
||||
(name, params, values) = [str(p) for p in parts]
|
||||
try:
|
||||
if params:
|
||||
return Contentline('%s;%s:%s' % (name, params, values))
|
||||
return Contentline('%s:%s' % (name, values))
|
||||
except:
|
||||
raise ValueError(
|
||||
'Property: %s Wrong values "%s" or "%s"' % (repr(name),
|
||||
repr(params),
|
||||
repr(values)))
|
||||
from_parts = staticmethod(from_parts)
|
||||
|
||||
def parts(self):
|
||||
""" Splits the content line up into (name, parameters, values) parts
|
||||
"""
|
||||
try:
|
||||
name_split = None
|
||||
value_split = None
|
||||
inquotes = 0
|
||||
for i in range(len(self)):
|
||||
ch = self[i]
|
||||
if not inquotes:
|
||||
if ch in ':;' and not name_split:
|
||||
name_split = i
|
||||
if ch == ':' and not value_split:
|
||||
value_split = i
|
||||
if ch == '"':
|
||||
inquotes = not inquotes
|
||||
name = self[:name_split]
|
||||
if not name:
|
||||
raise ValueError, 'Key name is required'
|
||||
validate_token(name)
|
||||
if name_split + 1 == value_split:
|
||||
raise ValueError, 'Invalid content line'
|
||||
params = Parameters.from_string(self[name_split + 1:value_split],
|
||||
strict=self.strict)
|
||||
values = self[value_split + 1:]
|
||||
return (name, params, values)
|
||||
except:
|
||||
raise ValueError, 'Content line could not be parsed into parts'
|
||||
|
||||
def from_string(st, strict=False):
|
||||
"Unfolds the content lines in an iCalendar into long content lines"
|
||||
try:
|
||||
# a fold is carriage return followed by either a space or a tab
|
||||
return Contentline(FOLD.sub('', st), strict=strict)
|
||||
except:
|
||||
raise ValueError, 'Expected StringType with content line'
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
def __str__(self):
|
||||
"Long content lines are folded so they are less than 75 characters wide"
|
||||
l_line = len(self)
|
||||
new_lines = []
|
||||
start = 0
|
||||
end = 74
|
||||
while True:
|
||||
if end >= l_line:
|
||||
end = l_line
|
||||
else:
|
||||
# Check that we don't fold in the middle of a UTF-8 character:
|
||||
# http://lists.osafoundation.org/pipermail/ietf-calsify/2006-August/001126.html
|
||||
while True:
|
||||
char_value = ord(self[end])
|
||||
if char_value < 128 or char_value >= 192:
|
||||
# This is not in the middle of a UTF-8 character, so we
|
||||
# can fold here:
|
||||
break
|
||||
else:
|
||||
end -= 1
|
||||
|
||||
new_lines.append(self[start:end])
|
||||
if end == l_line:
|
||||
# Done
|
||||
break
|
||||
start = end
|
||||
end = start + 74
|
||||
return '\r\n '.join(new_lines)
|
||||
|
||||
|
||||
|
||||
class Contentlines(list):
|
||||
"""
|
||||
I assume that iCalendar files generally are a few kilobytes in size. Then
|
||||
this should be efficient. for Huge files, an iterator should probably be
|
||||
used instead.
|
||||
|
||||
>>> c = Contentlines([Contentline('BEGIN:VEVENT\\r\\n')])
|
||||
>>> str(c)
|
||||
'BEGIN:VEVENT\\r\\n'
|
||||
|
||||
Lets try appending it with a 100 charater wide string
|
||||
>>> c.append(Contentline(''.join(['123456789 ']*10)+'\\r\\n'))
|
||||
>>> str(c)
|
||||
'BEGIN:VEVENT\\r\\n\\r\\n123456789 123456789 123456789 123456789 123456789 123456789 123456789 1234\\r\\n 56789 123456789 123456789 \\r\\n'
|
||||
|
||||
Notice that there is an extra empty string in the end of the content lines.
|
||||
That is so they can be easily joined with: '\r\n'.join(contentlines)).
|
||||
>>> Contentlines.from_string('A short line\\r\\n')
|
||||
['A short line', '']
|
||||
>>> Contentlines.from_string('A faked\\r\\n long line\\r\\n')
|
||||
['A faked long line', '']
|
||||
>>> Contentlines.from_string('A faked\\r\\n long line\\r\\nAnd another lin\\r\\n\\te that is folded\\r\\n')
|
||||
['A faked long line', 'And another line that is folded', '']
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
"Simply join self."
|
||||
return '\r\n'.join(map(str, self))
|
||||
|
||||
def from_string(st):
|
||||
"Parses a string into content lines"
|
||||
try:
|
||||
# a fold is carriage return followed by either a space or a tab
|
||||
unfolded = FOLD.sub('', st)
|
||||
lines = [Contentline(line) for line in unfolded.splitlines() if line]
|
||||
lines.append('') # we need a '\r\n' in the end of every content line
|
||||
return Contentlines(lines)
|
||||
except:
|
||||
raise ValueError, 'Expected StringType with content lines'
|
||||
from_string = staticmethod(from_string)
|
||||
|
||||
|
||||
# ran this:
|
||||
# sample = open('./samples/test.ics', 'rb').read() # binary file in windows!
|
||||
# lines = Contentlines.from_string(sample)
|
||||
# for line in lines[:-1]:
|
||||
# print line.parts()
|
||||
|
||||
# got this:
|
||||
#('BEGIN', Parameters({}), 'VCALENDAR')
|
||||
#('METHOD', Parameters({}), 'Request')
|
||||
#('PRODID', Parameters({}), '-//My product//mxm.dk/')
|
||||
#('VERSION', Parameters({}), '2.0')
|
||||
#('BEGIN', Parameters({}), 'VEVENT')
|
||||
#('DESCRIPTION', Parameters({}), 'This is a very long description that ...')
|
||||
#('PARTICIPANT', Parameters({'CN': 'Max M'}), 'MAILTO:maxm@mxm.dk')
|
||||
#('DTEND', Parameters({}), '20050107T160000')
|
||||
#('DTSTART', Parameters({}), '20050107T120000')
|
||||
#('SUMMARY', Parameters({}), 'A second event')
|
||||
#('END', Parameters({}), 'VEVENT')
|
||||
#('BEGIN', Parameters({}), 'VEVENT')
|
||||
#('DTEND', Parameters({}), '20050108T235900')
|
||||
#('DTSTART', Parameters({}), '20050108T230000')
|
||||
#('SUMMARY', Parameters({}), 'A single event')
|
||||
#('UID', Parameters({}), '42')
|
||||
#('END', Parameters({}), 'VEVENT')
|
||||
#('END', Parameters({}), 'VCALENDAR')
|
||||
1511
utils/icalendar/prop.py
Normal file
1511
utils/icalendar/prop.py
Normal file
File diff suppressed because it is too large
Load Diff
1
utils/icalendar/tests/__init__.py
Normal file
1
utils/icalendar/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# this is a package
|
||||
16
utils/icalendar/tests/test_icalendar.py
Normal file
16
utils/icalendar/tests/test_icalendar.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import unittest, doctest, os
|
||||
from utils.icalendar import cal, caselessdict, parser, prop
|
||||
|
||||
def test_suite():
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
suite.addTest(doctest.DocTestSuite(caselessdict))
|
||||
suite.addTest(doctest.DocTestSuite(parser))
|
||||
suite.addTest(doctest.DocTestSuite(prop))
|
||||
suite.addTest(doctest.DocTestSuite(cal))
|
||||
doc_dir = '../../../doc'
|
||||
for docfile in ['example.txt', 'groupscheduled.txt',
|
||||
'small.txt', 'multiple.txt', 'recurrence.txt']:
|
||||
suite.addTest(doctest.DocFileSuite(os.path.join(doc_dir, docfile),
|
||||
optionflags=doctest.ELLIPSIS),)
|
||||
return suite
|
||||
1
utils/management/__init__.py
Normal file
1
utils/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
1
utils/management/commands/__init__.py
Normal file
1
utils/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
#!/usr/bin/python
|
||||
243
utils/management/commands/compresscss.py
Normal file
243
utils/management/commands/compresscss.py
Normal file
@@ -0,0 +1,243 @@
|
||||
'''
|
||||
Created on 06.06.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
import fnmatch
|
||||
from optparse import make_option
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
'''
|
||||
classdocs
|
||||
'''
|
||||
can_import_settings = True
|
||||
help = _("Reads raw CSS from stdin, and writes compressed CSS to stdout.")
|
||||
option_list = BaseCommand.option_list + (
|
||||
make_option('-w', '--wrap',
|
||||
type='int',
|
||||
default=None,
|
||||
metavar='N',
|
||||
help="Wrap output to approximately N chars per line."),
|
||||
)
|
||||
|
||||
def remove_comments(self, css):
|
||||
"""Remove all CSS comment blocks."""
|
||||
|
||||
iemac = False
|
||||
preserve = False
|
||||
comment_start = css.find("/*")
|
||||
while comment_start >= 0:
|
||||
# Preserve comments that look like `/*!...*/`.
|
||||
# Slicing is used to make sure we don"t get an IndexError.
|
||||
preserve = css[comment_start + 2:comment_start + 3] == "!"
|
||||
|
||||
comment_end = css.find("*/", comment_start + 2)
|
||||
if comment_end < 0:
|
||||
if not preserve:
|
||||
css = css[:comment_start]
|
||||
break
|
||||
elif comment_end >= (comment_start + 2):
|
||||
if css[comment_end - 1] == "\\":
|
||||
# This is an IE Mac-specific comment; leave this one
|
||||
# and the following one alone.
|
||||
comment_start = comment_end + 2
|
||||
iemac = True
|
||||
elif iemac:
|
||||
comment_start = comment_end + 2
|
||||
iemac = False
|
||||
elif not preserve:
|
||||
css = css[:comment_start] + css[comment_end + 2:]
|
||||
else:
|
||||
comment_start = comment_end + 2
|
||||
comment_start = css.find("/*", comment_start)
|
||||
|
||||
return css
|
||||
|
||||
def remove_unnecessary_whitespace(self, css):
|
||||
"""Remove unnecessary whitespace characters."""
|
||||
|
||||
def pseudoclasscolon(css):
|
||||
|
||||
"""
|
||||
Prevents 'p :link' from becoming 'p:link'.
|
||||
|
||||
Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is
|
||||
translated back again later.
|
||||
"""
|
||||
|
||||
regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)")
|
||||
match = regex.search(css)
|
||||
while match:
|
||||
css = ''.join([
|
||||
css[:match.start()],
|
||||
match.group().replace(":", "___PSEUDOCLASSCOLON___"),
|
||||
css[match.end():]])
|
||||
match = regex.search(css)
|
||||
return css
|
||||
|
||||
css = pseudoclasscolon(css)
|
||||
# Remove spaces from before things.
|
||||
css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css)
|
||||
|
||||
# If there is a `@charset`,
|
||||
# then only allow one, and move to the beginning.
|
||||
css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css)
|
||||
css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css)
|
||||
|
||||
# Put the space back in for a few cases, such as `@media screen` and
|
||||
# `(-webkit-min-device-pixel-ratio:0)`.
|
||||
css = re.sub(r"\band\(", "and (", css)
|
||||
|
||||
# Put the colons back.
|
||||
css = css.replace('___PSEUDOCLASSCOLON___', ':')
|
||||
|
||||
# Remove spaces from after things.
|
||||
css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css)
|
||||
|
||||
return css
|
||||
|
||||
|
||||
def remove_unnecessary_semicolons(self, css):
|
||||
"""Remove unnecessary semicolons."""
|
||||
|
||||
return re.sub(r";+\}", "}", css)
|
||||
|
||||
|
||||
def remove_empty_rules(self, css):
|
||||
"""Remove empty rules."""
|
||||
|
||||
return re.sub(r"[^\}\{]+\{\}", "", css)
|
||||
|
||||
|
||||
def normalize_rgb_colors_to_hex(self, css):
|
||||
"""Convert `rgb(51,102,153)` to `#336699`."""
|
||||
|
||||
regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)")
|
||||
match = regex.search(css)
|
||||
while match:
|
||||
colors = match.group(1).split(",")
|
||||
hexcolor = '#%.2x%.2x%.2x' % map(int, colors)
|
||||
css = css.replace(match.group(), hexcolor)
|
||||
match = regex.search(css)
|
||||
return css
|
||||
|
||||
def condense_zero_units(self, css):
|
||||
"""Replace `0(px, em, %, etc)` with `0`."""
|
||||
|
||||
return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css)
|
||||
|
||||
|
||||
def condense_multidimensional_zeros(self, css):
|
||||
"""Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
|
||||
|
||||
css = css.replace(":0 0 0 0;", ":0;")
|
||||
css = css.replace(":0 0 0;", ":0;")
|
||||
css = css.replace(":0 0;", ":0;")
|
||||
|
||||
# Revert `background-position:0;` to the valid `background-position:0 0;`.
|
||||
css = css.replace("background-position:0;", "background-position:0 0;")
|
||||
|
||||
return css
|
||||
|
||||
|
||||
def condense_floating_points(self, css):
|
||||
"""Replace `0.6` with `.6` where possible."""
|
||||
|
||||
return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
|
||||
|
||||
|
||||
def condense_hex_colors(self, css):
|
||||
"""Shorten colors from #AABBCC to #ABC where possible."""
|
||||
|
||||
regex = re.compile(r"([^\"'=\s])(\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])")
|
||||
match = regex.search(css)
|
||||
while match:
|
||||
first = match.group(3) + match.group(5) + match.group(7)
|
||||
second = match.group(4) + match.group(6) + match.group(8)
|
||||
if first.lower() == second.lower():
|
||||
css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first)
|
||||
match = regex.search(css, match.end() - 3)
|
||||
else:
|
||||
match = regex.search(css, match.end())
|
||||
return css
|
||||
|
||||
|
||||
def condense_whitespace(self, css):
|
||||
"""Condense multiple adjacent whitespace characters into one."""
|
||||
|
||||
return re.sub(r"\s+", " ", css)
|
||||
|
||||
def condense_semicolons(self, css):
|
||||
"""Condense multiple adjacent semicolon characters into one."""
|
||||
|
||||
return re.sub(r";;+", ";", css)
|
||||
|
||||
def wrap_css_lines(self, css, line_length):
|
||||
"""Wrap the lines of the given CSS to an approximate length."""
|
||||
|
||||
lines = []
|
||||
line_start = 0
|
||||
for i, char in enumerate(css):
|
||||
# It's safe to break after `}` characters.
|
||||
if char == '}' and (i - line_start >= line_length):
|
||||
lines.append(css[line_start:i + 1])
|
||||
line_start = i + 1
|
||||
|
||||
if line_start < len(css):
|
||||
lines.append(css[line_start:])
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def compress(self, css, wrap=None):
|
||||
css = self.remove_comments(css)
|
||||
css = self.condense_whitespace(css)
|
||||
# A pseudo class for the Box Model Hack
|
||||
# (see http://tantek.com/CSS/Examples/boxmodelhack.html)
|
||||
css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___")
|
||||
css = self.remove_unnecessary_whitespace(css)
|
||||
css = self.remove_unnecessary_semicolons(css)
|
||||
css = self.condense_zero_units(css)
|
||||
css = self.condense_multidimensional_zeros(css)
|
||||
css = self.condense_floating_points(css)
|
||||
css = self.normalize_rgb_colors_to_hex(css)
|
||||
css = self.condense_hex_colors(css)
|
||||
if wrap is not None:
|
||||
css = self.wrap_css_lines(css, wrap)
|
||||
css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""')
|
||||
css = self.condense_semicolons(css)
|
||||
# A Hack for IE compatiblity
|
||||
# These crappy browser has issues with AND Statments in @MEDIA Blocks
|
||||
css = css.replace('and(', 'and (')
|
||||
return css.strip()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
CSS_ROOT = os.path.join(settings.STATICFILES_DIRS[0], 'css')
|
||||
for site in Site.objects.all():
|
||||
css_input = os.path.join(CSS_ROOT, site.domain)
|
||||
css_output = '%s/%s.css' % (CSS_ROOT, site.domain.replace('.', '_'))
|
||||
|
||||
print _("Compressing CSS for %s") % site.name
|
||||
print "Input Dir: %s" % css_input
|
||||
print "Output File: %s" % css_output
|
||||
|
||||
try:
|
||||
os.makedirs(css_input)
|
||||
except OSError:
|
||||
pass
|
||||
css = ''
|
||||
"""Read each .css file in the css_input directory,
|
||||
and append their content"""
|
||||
for file_name in fnmatch.filter(sorted(os.listdir(css_input)), '*.css'):
|
||||
print ' Adding: %s' % file_name
|
||||
with open(os.path.join(css_input, file_name), 'r') as css_file:
|
||||
css += css_file.read()
|
||||
with open(css_output, 'w') as css_file:
|
||||
css_file.write(self.compress(css))
|
||||
47
utils/management/commands/compressjs.py
Normal file
47
utils/management/commands/compressjs.py
Normal file
@@ -0,0 +1,47 @@
|
||||
'''
|
||||
Created on 06.06.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
import fnmatch
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import ugettext as _
|
||||
import jsmin
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
'''
|
||||
classdocs
|
||||
'''
|
||||
can_import_settings = True
|
||||
help = _("Reads raw CSS from stdin, and writes compressed CSS to stdout.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
JS_ROOT = os.path.join(settings.STATICFILES_DIRS[0], 'js')
|
||||
for site in Site.objects.all():
|
||||
js_input = os.path.join(JS_ROOT, site.domain)
|
||||
js_output = '%s/%s.js' % (JS_ROOT, site.domain.replace('.', '_'))
|
||||
|
||||
print _("Compressing JavaScript for %s") % site.name
|
||||
print "Input Dir: %s" % js_input
|
||||
print "Output File: %s" % js_output
|
||||
|
||||
try:
|
||||
os.makedirs(js_input)
|
||||
except OSError:
|
||||
pass
|
||||
output = ''
|
||||
# Read each .js file in the js_input directory, and append their
|
||||
# content
|
||||
js_files = fnmatch.filter(sorted(os.listdir(js_input)), '*.js')
|
||||
for file_name in js_files:
|
||||
print " Adding: %s..." % file_name
|
||||
input_file_name = os.path.join(js_input, file_name)
|
||||
with open(input_file_name, 'r') as input_file:
|
||||
output += input_file.read()
|
||||
with open(js_output, 'w') as output_file:
|
||||
output_file.write(jsmin.jsmin(output))
|
||||
46
utils/management/commands/scss-compiler.py
Normal file
46
utils/management/commands/scss-compiler.py
Normal file
@@ -0,0 +1,46 @@
|
||||
'''
|
||||
Created on 06.06.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils.translation import ugettext as _
|
||||
from optparse import make_option
|
||||
import os, re, fnmatch
|
||||
from scss import parser
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
'''
|
||||
classdocs
|
||||
'''
|
||||
can_import_settings = True
|
||||
help = _("Compile SCSS rules.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
CSS_ROOT = os.path.join(settings.STATICFILES_DIRS[0], 'css')
|
||||
for site in Site.objects.all():
|
||||
css_input = os.path.join(CSS_ROOT, site.domain)
|
||||
css_output = '%s/%s.css' % (CSS_ROOT, site.domain.replace('.', '_'))
|
||||
|
||||
print _("Compressing CSS for %s") % site.name
|
||||
print "Input Dir: %s" % css_input
|
||||
print "Output File: %s" % css_output
|
||||
|
||||
try:
|
||||
os.makedirs(css_input)
|
||||
except OSError:
|
||||
pass
|
||||
css = ''
|
||||
"""Read each .css file in the css_input directory,
|
||||
and append their content"""
|
||||
for file_name in fnmatch.filter(sorted(os.listdir(css_input)), '*.css'):
|
||||
print ' Adding: %s' % file_name
|
||||
with open(os.path.join(css_input, file_name), 'r') as css_file:
|
||||
css += css_file.read()
|
||||
with open(css_output, 'w') as css_file:
|
||||
css_file.write(self.compress(css))
|
||||
#file.write(css)
|
||||
|
||||
106
utils/massmailer.py
Normal file
106
utils/massmailer.py
Normal file
@@ -0,0 +1,106 @@
|
||||
import logging
|
||||
from django.core import mail
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
from django.template import loader, Context
|
||||
|
||||
|
||||
class MassMailer(object):
|
||||
'''
|
||||
This Class will send E-Mails via an SMTP Connection to multiple recipients.
|
||||
Each E-Mail will be send individually and can be personalized.
|
||||
It will be send as HTML and Plain-Text Message.
|
||||
'''
|
||||
context = {}
|
||||
headers = {}
|
||||
subject = None
|
||||
txt_template = None
|
||||
html_template = None
|
||||
|
||||
def __init__(self, subject=None, template=None, context=None):
|
||||
self.mail_queue = set()
|
||||
self.recipients = set()
|
||||
self.subject = subject
|
||||
self.template = 'email'
|
||||
self.context = context
|
||||
self.log = logging.getLogger('maillog')
|
||||
|
||||
def open_smtp_connection(self):
|
||||
try:
|
||||
self.smtp_connection = mail.get_connection(fail_silently=False)
|
||||
self.smtp_connection.open()
|
||||
self.mail_counter = 0
|
||||
except:
|
||||
self.log.error('Connection to SMTP server failed. Giving up!')
|
||||
raise False
|
||||
else:
|
||||
self.log.debug('Connected to SMTP server.')
|
||||
return True
|
||||
|
||||
def close_smtp_connection(self):
|
||||
self.smtp_connection.close()
|
||||
self.log.debug("closed the SMTP connection. I'm done")
|
||||
|
||||
def add_recipient(self, recipient):
|
||||
if isinstance(recipient, User):
|
||||
self.recipients.add(recipient)
|
||||
else:
|
||||
self.log.warn('%s is not a User Object!', recipient)
|
||||
|
||||
def add_recipients(self, user_list):
|
||||
for user in user_list:
|
||||
if isinstance(user, User):
|
||||
self.log.debug('Adding %s as Recipient' % user)
|
||||
self.recipients.add(user)
|
||||
else:
|
||||
self.log.warn('%s is not a User Object!', user)
|
||||
|
||||
def process_mails(self):
|
||||
mail_context = Context(self.context)
|
||||
mail_context['site'] = Site.objects.get_current()
|
||||
|
||||
self.txt_template = loader.get_template(self.txt_template)
|
||||
if self.html_template:
|
||||
self.html_template = loader.get_template(self.html_template)
|
||||
|
||||
for recipient in self.recipients:
|
||||
mail_context['recipient'] = recipient
|
||||
mail_to = "%s %s <%s>" % (recipient.first_name,
|
||||
recipient.last_name, recipient.email)
|
||||
message = mail.EmailMultiAlternatives(
|
||||
subject=self.subject,
|
||||
body=self.txt_template.render(mail_context),
|
||||
to=(mail_to,),
|
||||
headers=self.headers
|
||||
)
|
||||
if self.html_template:
|
||||
message.attach_alternative(
|
||||
self.html_template.render(mail_context), "text/html"
|
||||
)
|
||||
self.mail_queue.add(message)
|
||||
|
||||
def set_header(self, name, value):
|
||||
self.headers[name] = value
|
||||
|
||||
def send(self):
|
||||
'''
|
||||
Process the E-Mails and send them
|
||||
'''
|
||||
self.process_mails()
|
||||
if len(self.mail_queue) == 0:
|
||||
self.log.info('No recipients for eMail "%s", bye!', self.subject)
|
||||
return True
|
||||
else:
|
||||
self.log.info('Sending eMail "%s" to %d people', self.subject,
|
||||
len(self.mail_queue))
|
||||
self.log.debug(self.recipients)
|
||||
|
||||
self.open_smtp_connection()
|
||||
for mail in self.mail_queue:
|
||||
try:
|
||||
mail.send()
|
||||
except:
|
||||
self.log.warn("Mail failed for: %s", mail.to)
|
||||
else:
|
||||
self.log.info("Mail sent successful to: %s" % mail.to)
|
||||
self.close_smtp_connection()
|
||||
20
utils/middleware.py
Normal file
20
utils/middleware.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
'''
|
||||
Created on 23.05.2011
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django.utils.html import strip_spaces_between_tags
|
||||
|
||||
|
||||
class CompressHtmlMiddleware(object):
|
||||
'''
|
||||
This Middleware compresses the HTML Output at the End. It strips the Spaces
|
||||
between Tags, an at the beginning and the end of the content.
|
||||
'''
|
||||
|
||||
def process_response(self, request, response):
|
||||
if 'text/html' in response['Content-Type']:
|
||||
response.content = strip_spaces_between_tags(response.content)
|
||||
response.content = response.content.strip()
|
||||
return response
|
||||
92
utils/mixins.py
Normal file
92
utils/mixins.py
Normal file
@@ -0,0 +1,92 @@
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django import http
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.http import urlquote
|
||||
|
||||
|
||||
class LoginRequiredMixin(object):
|
||||
"""
|
||||
View mixin which verifies that the user has authenticated.
|
||||
|
||||
NOTE:
|
||||
This should be the left-most mixin of a view.
|
||||
"""
|
||||
login_url = settings.LOGIN_URL
|
||||
raise_exception = False
|
||||
redirect_field_name = REDIRECT_FIELD_NAME
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated():
|
||||
return super(LoginRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
elif self.raise_exception: # if an exception was desired
|
||||
return http.HttpResponseForbidden() # return a forbidden response.
|
||||
else:
|
||||
messages.error(request, _("You need to be logged in"))
|
||||
path = urlquote(request.get_full_path())
|
||||
return http.HttpResponseRedirect("%s?%s=%s" % (self.login_url, self.redirect_field_name, path))
|
||||
|
||||
class PermissionRequiredMixin(object):
|
||||
"""
|
||||
View mixin which verifies that the loggedin user has the specified
|
||||
permission.
|
||||
|
||||
Class Settings
|
||||
`permission_required` - the permission to check for.
|
||||
`login_url` - the login url of site
|
||||
`redirect_field_name` - defaults to "next"
|
||||
`raise_exception` - defaults to False - raise 403 if set to True
|
||||
|
||||
Example Usage
|
||||
|
||||
class SomeView(PermissionRequiredMixin, ListView):
|
||||
...
|
||||
# required
|
||||
permission_required = "app.permission"
|
||||
|
||||
# optional
|
||||
login_url = "/signup/"
|
||||
redirect_field_name = "hollaback"
|
||||
raise_exception = True
|
||||
...
|
||||
"""
|
||||
login_url = settings.LOGIN_URL
|
||||
permission_required = None
|
||||
permission_failed_message = _("You don't have the permission to do this")
|
||||
raise_exception = False
|
||||
redirect_field_name = REDIRECT_FIELD_NAME
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Verify class settings
|
||||
if self.permission_required == None or len(self.permission_required.split(".")) != 2:
|
||||
raise ImproperlyConfigured("'PermissionRequiredMixin' requires 'permission_required' attribute to be set.")
|
||||
has_permission = request.user.has_perm(self.permission_required)
|
||||
|
||||
if has_permission:
|
||||
return super(PermissionRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
elif self.raise_exception:
|
||||
return http.HttpResponseForbidden()
|
||||
else:
|
||||
messages.warning(request, self.permission_failed_message)
|
||||
path = urlquote(request.get_full_path())
|
||||
return http.HttpResponseRedirect("%s?%s=%s" % (self.login_url, self.redirect_field_name, path))
|
||||
|
||||
class SuperuserRequiredMixin(object):
|
||||
"""
|
||||
Mixin allows you to require a user with `is_superuser` set to True.
|
||||
"""
|
||||
login_url = settings.LOGIN_URL # LOGIN_URL from project settings
|
||||
raise_exception = False # Default whether to raise an exception to none
|
||||
redirect_field_name = REDIRECT_FIELD_NAME # Set by django.contrib.auth
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_superuser: # If the user is a standard user,
|
||||
return super(SuperuserRequiredMixin, self).dispatch(request, *args, **kwargs)
|
||||
elif self.raise_exception: # *and* if an exception was desired
|
||||
return http.HttpResponseForbidden() # return a forbidden response.
|
||||
else:
|
||||
messages.error(request, _("You don't have the permissions for this"))
|
||||
path = urlquote(request.get_full_path())
|
||||
return http.HttpResponseRedirect("%s?%s=%s" % (self.login_url, self.redirect_field_name, path))
|
||||
0
utils/templatetags/__init__.py
Normal file
0
utils/templatetags/__init__.py
Normal file
47
utils/templatetags/markup.py
Normal file
47
utils/templatetags/markup.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
Created on 25.11.2013
|
||||
|
||||
@author: christian
|
||||
'''
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.encoding import force_text
|
||||
import markdown
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='markdown', is_safe=True)
|
||||
def markdown_filter(value, arg=''):
|
||||
"""
|
||||
Runs Markdown over a given value, optionally using various
|
||||
extensions python-markdown supports.
|
||||
|
||||
Syntax::
|
||||
|
||||
{{ value|markdown:"extension1_name,extension2_name..." }}
|
||||
|
||||
To enable safe mode, which strips raw HTML and only returns HTML
|
||||
generated by actual Markdown syntax, pass "safe" as the first
|
||||
extension in the list.
|
||||
|
||||
If the version of Markdown in use does not support extensions,
|
||||
they will be silently ignored.
|
||||
|
||||
"""
|
||||
markdown_vers = getattr(markdown, "version_info", 0)
|
||||
if markdown_vers < (2, 1):
|
||||
if settings.DEBUG:
|
||||
raise template.TemplateSyntaxError(
|
||||
"Error in 'markdown' filter: Django does not support versions of the Python markdown library < 2.1.")
|
||||
return force_text(value)
|
||||
else:
|
||||
extensions = [e for e in arg.split(",") if e]
|
||||
if extensions and extensions[0] == "safe":
|
||||
extensions = extensions[1:]
|
||||
return mark_safe(markdown.markdown(
|
||||
force_text(value), extensions, safe_mode=True, enable_attributes=False))
|
||||
else:
|
||||
return mark_safe(markdown.markdown(
|
||||
force_text(value), extensions, safe_mode=False))
|
||||
Reference in New Issue
Block a user