Anfänglicher Commit: Producion Version Stand: Oktober 2014

This commit is contained in:
Christian Berg
2014-11-25 23:43:21 +01:00
commit daa35f5913
3381 changed files with 132346 additions and 0 deletions

31
src/utils/__init__.py Normal file
View 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
src/utils/countries.py Normal file
View 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
src/utils/forms.py Normal file
View 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]

View 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
src/utils/html5/base.py Normal file
View 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
src/utils/html5/forms.py Normal file
View 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
src/utils/html5/models.py Normal file
View 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
src/utils/html5/views.py Normal file
View 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
src/utils/html5/widgets.py Normal file
View 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
src/utils/html_cleaner.py Normal file
View 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

View 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
src/utils/icalendar/cal.py Normal file
View 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()

View 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) + ')'

View 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.
"""

View 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
src/utils/icalendar/prop.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
# this is a package

View 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

View File

@@ -0,0 +1 @@
#!/usr/bin/python

View File

@@ -0,0 +1 @@
#!/usr/bin/python

View 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))

View 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))

View 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
src/utils/massmailer.py Normal file
View 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
src/utils/middleware.py Normal file
View 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
src/utils/mixins.py Normal file
View 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))

View File

View 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))