newsletter models change to emencia format

remotes/origin/1203
Nazar Kotjuk 10 years ago
parent 2b3e715d3d
commit 38025dccfc
  1. 6
      emencia/__init__.py
  2. 6
      emencia/django/__init__.py
  3. 8
      emencia/django/newsletter/__init__.py
  4. 35
      emencia/django/newsletter/admin/__init__.py
  5. 184
      emencia/django/newsletter/admin/contact.py
  6. 125
      emencia/django/newsletter/admin/mailinglist.py
  7. 185
      emencia/django/newsletter/admin/newsletter.py
  8. 57
      emencia/django/newsletter/admin/smtpserver.py
  9. 26
      emencia/django/newsletter/admin/workgroup.py
  10. 0
      emencia/django/newsletter/cmsplugin_newsletter/__init__.py
  11. 38
      emencia/django/newsletter/cmsplugin_newsletter/cms_plugins.py
  12. 19
      emencia/django/newsletter/cmsplugin_newsletter/models.py
  13. 4
      emencia/django/newsletter/cmsplugin_newsletter/settings.py
  14. 56
      emencia/django/newsletter/forms.py
  15. 0
      emencia/django/newsletter/locale/cs/LC_MESSAGES/django.mo
  16. 0
      emencia/django/newsletter/locale/cs/LC_MESSAGES/django.po
  17. 0
      emencia/django/newsletter/locale/de/LC_MESSAGES/django.mo
  18. 0
      emencia/django/newsletter/locale/de/LC_MESSAGES/django.po
  19. 0
      emencia/django/newsletter/locale/en/LC_MESSAGES/django.mo
  20. 0
      emencia/django/newsletter/locale/en/LC_MESSAGES/django.po
  21. 0
      emencia/django/newsletter/locale/es/LC_MESSAGES/django.mo
  22. 0
      emencia/django/newsletter/locale/es/LC_MESSAGES/django.po
  23. 0
      emencia/django/newsletter/locale/es_DO/LC_MESSAGES/django.mo
  24. 0
      emencia/django/newsletter/locale/es_DO/LC_MESSAGES/django.po
  25. 0
      emencia/django/newsletter/locale/fo/LC_MESSAGES/django.mo
  26. 0
      emencia/django/newsletter/locale/fo/LC_MESSAGES/django.po
  27. 0
      emencia/django/newsletter/locale/fr/LC_MESSAGES/django.mo
  28. 0
      emencia/django/newsletter/locale/fr/LC_MESSAGES/django.po
  29. 0
      emencia/django/newsletter/locale/it/LC_MESSAGES/django.mo
  30. 0
      emencia/django/newsletter/locale/it/LC_MESSAGES/django.po
  31. 0
      emencia/django/newsletter/locale/ja/LC_MESSAGES/django.mo
  32. 0
      emencia/django/newsletter/locale/ja/LC_MESSAGES/django.po
  33. 0
      emencia/django/newsletter/locale/nl/LC_MESSAGES/django.mo
  34. 0
      emencia/django/newsletter/locale/nl/LC_MESSAGES/django.po
  35. 0
      emencia/django/newsletter/locale/pt/LC_MESSAGES/django.mo
  36. 0
      emencia/django/newsletter/locale/pt/LC_MESSAGES/django.po
  37. 0
      emencia/django/newsletter/locale/pt_BR/LC_MESSAGES/django.mo
  38. 0
      emencia/django/newsletter/locale/pt_BR/LC_MESSAGES/django.po
  39. 0
      emencia/django/newsletter/locale/ro/LC_MESSAGES/django.mo
  40. 0
      emencia/django/newsletter/locale/ro/LC_MESSAGES/django.po
  41. 0
      emencia/django/newsletter/locale/ru/LC_MESSAGES/django.mo
  42. 0
      emencia/django/newsletter/locale/ru/LC_MESSAGES/django.po
  43. 0
      emencia/django/newsletter/locale/sk_SK/LC_MESSAGES/django.mo
  44. 0
      emencia/django/newsletter/locale/sk_SK/LC_MESSAGES/django.po
  45. 0
      emencia/django/newsletter/locale/sl/LC_MESSAGES/django.mo
  46. 0
      emencia/django/newsletter/locale/sl/LC_MESSAGES/django.po
  47. 0
      emencia/django/newsletter/locale/zh_CN/LC_MESSAGES/django.mo
  48. 0
      emencia/django/newsletter/locale/zh_CN/LC_MESSAGES/django.po
  49. 29
      emencia/django/newsletter/mailer.py
  50. 0
      emencia/django/newsletter/management/__init__.py
  51. 0
      emencia/django/newsletter/management/commands/__init__.py
  52. 4
      emencia/django/newsletter/management/commands/send_newsletter.py
  53. 4
      emencia/django/newsletter/management/commands/send_newsletter_continuous.py
  54. 1
      emencia/django/newsletter/managers.py
  55. 272
      emencia/django/newsletter/migrations/0001_initial.py
  56. 130
      emencia/django/newsletter/migrations/0002_auto__add_attachment.py
  57. 123
      emencia/django/newsletter/migrations/0003_auto__add_unique_newsletter_slug.py
  58. 0
      emencia/django/newsletter/migrations/__init__.py
  59. 14
      emencia/django/newsletter/models.py
  60. 4
      emencia/django/newsletter/settings.py
  61. 660
      emencia/django/newsletter/static/edn/js/jquery.form.js
  62. 7
      emencia/django/newsletter/static/edn/js/jquery.heatcolor.js
  63. 152
      emencia/django/newsletter/static/edn/js/jquery.js
  64. BIN
      emencia/django/newsletter/static/edn/js/swfobject/expressInstall.swf
  65. 28
      emencia/django/newsletter/static/edn/js/swfobject/index.html
  66. 17
      emencia/django/newsletter/static/edn/js/swfobject/index_dynamic.html
  67. 4
      emencia/django/newsletter/static/edn/js/swfobject/swfobject.js
  68. BIN
      emencia/django/newsletter/static/edn/js/swfobject/test.swf
  69. BIN
      emencia/django/newsletter/static/edn/swf/open-flash-chart.swf
  70. 0
      emencia/django/newsletter/templates/admin/newsletter/contact/change_list.html
  71. 0
      emencia/django/newsletter/templates/newsletter/base.html
  72. 0
      emencia/django/newsletter/templates/newsletter/cms/subscription_form.html
  73. 0
      emencia/django/newsletter/templates/newsletter/contact_import.html
  74. 0
      emencia/django/newsletter/templates/newsletter/mailing_list_subscribe.html
  75. 0
      emencia/django/newsletter/templates/newsletter/mailing_list_unsubscribe.html
  76. 0
      emencia/django/newsletter/templates/newsletter/newsletter_density.html
  77. 0
      emencia/django/newsletter/templates/newsletter/newsletter_detail.html
  78. 0
      emencia/django/newsletter/templates/newsletter/newsletter_historic.html
  79. 0
      emencia/django/newsletter/templates/newsletter/newsletter_image_tracking.html
  80. 0
      emencia/django/newsletter/templates/newsletter/newsletter_link_site.html
  81. 0
      emencia/django/newsletter/templates/newsletter/newsletter_link_unsubscribe.html
  82. 0
      emencia/django/newsletter/templates/newsletter/newsletter_statistics.html
  83. 708
      emencia/django/newsletter/tests.py
  84. 16
      emencia/django/newsletter/testsettings.py
  85. 11
      emencia/django/newsletter/urls/__init__.py
  86. 20
      emencia/django/newsletter/urls/mailing_list.py
  87. 12
      emencia/django/newsletter/urls/newsletter.py
  88. 18
      emencia/django/newsletter/urls/statistics.py
  89. 15
      emencia/django/newsletter/urls/tracking.py
  90. 0
      emencia/django/newsletter/utils/__init__.py
  91. 2
      emencia/django/newsletter/utils/excel.py
  92. 4
      emencia/django/newsletter/utils/importation.py
  93. 4
      emencia/django/newsletter/utils/newsletter.py
  94. 0
      emencia/django/newsletter/utils/ofc.py
  95. 2
      emencia/django/newsletter/utils/premailer.py
  96. 4
      emencia/django/newsletter/utils/statistics.py
  97. 4
      emencia/django/newsletter/utils/tokens.py
  98. 0
      emencia/django/newsletter/utils/vcard.py
  99. 2
      emencia/django/newsletter/utils/workgroups.py
  100. 0
      emencia/django/newsletter/views/__init__.py
  101. Some files were not shown because too many files have changed in this diff Show More

@ -0,0 +1,6 @@
# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

@ -0,0 +1,6 @@
# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages
try:
__import__('pkg_resources').declare_namespace(__name__)
except ImportError:
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

@ -0,0 +1,8 @@
"""emencia.django.newsletter"""
__version__ = '0.3.dev'
__license__ = 'BSD License'
__author__ = 'Fantomas42'
__email__ = 'fantomas42@gmail.com'
__url__ = 'http://emencia.fr/'

@ -0,0 +1,35 @@
"""Admin for emencia.django.newsletter"""
from django.contrib import admin
from django.conf import settings
from emencia.django.newsletter.models import Link
from emencia.django.newsletter.models import Contact
from emencia.django.newsletter.models import WorkGroup
from emencia.django.newsletter.models import SMTPServer
from emencia.django.newsletter.models import Newsletter
from emencia.django.newsletter.models import MailingList
from emencia.django.newsletter.models import ContactMailingStatus
from emencia.django.newsletter.settings import USE_WORKGROUPS
from emencia.django.newsletter.admin.contact import ContactAdmin
from emencia.django.newsletter.admin.workgroup import WorkGroupAdmin
from emencia.django.newsletter.admin.newsletter import NewsletterAdmin
from emencia.django.newsletter.admin.smtpserver import SMTPServerAdmin
from emencia.django.newsletter.admin.mailinglist import MailingListAdmin
admin.site.register(Contact, ContactAdmin)
admin.site.register(SMTPServer, SMTPServerAdmin)
admin.site.register(Newsletter, NewsletterAdmin)
admin.site.register(MailingList, MailingListAdmin)
if USE_WORKGROUPS:
admin.site.register(WorkGroup, WorkGroupAdmin)
class LinkAdmin(admin.ModelAdmin):
list_display = ('title', 'url', 'creation_date')
if settings.DEBUG:
admin.site.register(Link, LinkAdmin)
admin.site.register(ContactMailingStatus)

@ -0,0 +1,184 @@
"""ModelAdmin for Contact"""
import StringIO
from django.conf import settings
from datetime import datetime
from django.contrib import admin
from django.dispatch import Signal
from django.conf.urls.defaults import url
from django.conf.urls.defaults import patterns
from django.core.urlresolvers import reverse
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect
from django.contrib.admin.views.main import ChangeList
from django.db import DatabaseError
from emencia.django.newsletter.models import MailingList
from emencia.django.newsletter.settings import USE_WORKGROUPS
from emencia.django.newsletter.utils.importation import import_dispatcher
from emencia.django.newsletter.utils.workgroups import request_workgroups
from emencia.django.newsletter.utils.workgroups import request_workgroups_contacts_pk
from emencia.django.newsletter.utils.vcard import vcard_contacts_export_response
from emencia.django.newsletter.utils.excel import ExcelResponse
contacts_imported = Signal(providing_args=['source', 'type'])
class ContactAdmin(admin.ModelAdmin):
date_hierarchy = 'creation_date'
list_display = ('email', 'first_name', 'last_name', 'tags', 'tester', 'subscriber',
'valid', 'total_subscriptions', 'creation_date', 'related_object_admin')
list_filter = ('subscriber', 'valid', 'tester', 'creation_date', 'modification_date')
search_fields = ('email', 'first_name', 'last_name', 'tags')
fieldsets = ((None, {'fields': ('email', 'first_name', 'last_name')}),
(None, {'fields': ('tags',)}),
(_('Status'), {'fields': ('subscriber', 'valid', 'tester')}),
(_('Advanced'), {'fields': ('object_id', 'content_type'),
'classes': ('collapse',)}),
)
actions = ['create_mailinglist', 'export_vcard', 'export_excel']
actions_on_top = False
actions_on_bottom = True
def queryset(self, request):
queryset = super(ContactAdmin, self).queryset(request)
if not request.user.is_superuser and USE_WORKGROUPS:
contacts_pk = request_workgroups_contacts_pk(request)
queryset = queryset.filter(pk__in=contacts_pk)
return queryset
def save_model(self, request, contact, form, change):
workgroups = []
if not contact.pk and not request.user.is_superuser \
and USE_WORKGROUPS:
workgroups = request_workgroups(request)
contact.save()
for workgroup in workgroups:
workgroup.contacts.add(contact)
def related_object_admin(self, contact):
"""Display link to related object's admin"""
if contact.content_type and contact.object_id:
admin_url = reverse('admin:%s_%s_change' % (contact.content_type.app_label,
contact.content_type.model),
args=(contact.object_id,))
return '%s: <a href="%s">%s</a>' % (contact.content_type.model.capitalize(),
admin_url,
contact.content_object.__unicode__())
return _('No relative object')
related_object_admin.allow_tags = True
related_object_admin.short_description = _('Related object')
def total_subscriptions(self, contact):
"""Display user subscriptions to unsubscriptions"""
subscriptions = contact.subscriptions().count()
unsubscriptions = contact.unsubscriptions().count()
return '%s / %s' % (subscriptions - unsubscriptions, subscriptions)
total_subscriptions.short_description = _('Total subscriptions')
def export_vcard(self, request, queryset, export_name=''):
"""Export selected contact in VCard"""
return vcard_contacts_export_response(queryset)
export_vcard.short_description = _('Export contacts as VCard')
def export_excel(self, request, queryset, export_name=''):
"""Export selected contact in Excel"""
if not export_name:
export_name = 'contacts_edn_%s' % datetime.now().strftime('%d-%m-%Y')
return ExcelResponse(queryset, export_name)
export_excel.short_description = _('Export contacts in Excel')
def create_mailinglist(self, request, queryset):
"""Create a mailing list from selected contact"""
when = str(datetime.now()).split('.')[0]
new_mailing = MailingList(name=_('New mailinglist at %s') % when,
description=_('New mailing list created in admin at %s') % when)
new_mailing.save()
if 'lite' in settings.DATABASES['default']['ENGINE']:
self.message_user(request, _('SQLite3 or a SpatialLite database type detected, ' \
'please note you will be limited to 999 contacts ' \
'per mailing list.'))
try:
new_mailing.subscribers = queryset.all()
except DatabaseError:
new_mailing.subscribers = queryset.none()
if not request.user.is_superuser and USE_WORKGROUPS:
for workgroup in request_workgroups(request):
workgroup.mailinglists.add(new_mailing)
self.message_user(request, _('%s succesfully created.') % new_mailing)
return HttpResponseRedirect(reverse('admin:newsletter_mailinglist_change',
args=[new_mailing.pk]))
create_mailinglist.short_description = _('Create a mailinglist')
def importation(self, request):
"""Import contacts from a VCard"""
opts = self.model._meta
if request.POST:
source = request.FILES.get('source') or \
StringIO.StringIO(request.POST.get('source', ''))
if not request.user.is_superuser and USE_WORKGROUPS:
workgroups = request_workgroups(request)
else:
workgroups = []
inserted = import_dispatcher(source, request.POST['type'],
workgroups)
if inserted:
contacts_imported.send(sender=self, source=source,
type=request.POST['type'])
self.message_user(request, _('%s contacts succesfully imported.') % inserted)
context = {'title': _('Contact importation'),
'opts': opts,
'root_path': self.admin_site.root_path,
'app_label': opts.app_label}
return render_to_response('newsletter/contact_import.html',
context, RequestContext(request))
def filtered_request_queryset(self, request):
"""Return queryset filtered by the admin list view"""
cl = ChangeList(request, self.model, self.list_display,
self.list_display_links, self.list_filter,
self.date_hierarchy, self.search_fields,
self.list_select_related, self.list_per_page,
self.list_editable, self)
return cl.get_query_set()
def creation_mailinglist(self, request):
"""Create a mailing list form the filtered contacts"""
return self.create_mailinglist(request, self.filtered_request_queryset(request))
def exportation_vcard(self, request):
"""Export filtered contacts in VCard"""
return self.export_vcard(request, self.filtered_request_queryset(request),
'contacts_edn_%s' % datetime.now().strftime('%d-%m-%Y'))
def exportation_excel(self, request):
"""Export filtered contacts in Excel"""
return self.export_excel(request, self.filtered_request_queryset(request),
'contacts_edn_%s' % datetime.now().strftime('%d-%m-%Y'))
def get_urls(self):
urls = super(ContactAdmin, self).get_urls()
my_urls = patterns('',
url(r'^import/$',
self.admin_site.admin_view(self.importation),
name='newsletter_contact_import'),
url(r'^create_mailinglist/$',
self.admin_site.admin_view(self.creation_mailinglist),
name='newsletter_contact_create_mailinglist'),
url(r'^export/vcard/$',
self.admin_site.admin_view(self.exportation_vcard),
name='newsletter_contact_export_vcard'),
url(r'^export/excel/$',
self.admin_site.admin_view(self.exportation_excel),
name='newsletter_contact_export_excel'),)
return my_urls + urls

@ -0,0 +1,125 @@
"""ModelAdmin for MailingList"""
from datetime import datetime
from django.contrib import admin
from django.conf.urls.defaults import url
from django.conf.urls.defaults import patterns
from django.utils.encoding import smart_str
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.utils.translation import ugettext_lazy as _
from django.http import HttpResponseRedirect
from emencia.django.newsletter.models import Contact
from emencia.django.newsletter.models import MailingList
from emencia.django.newsletter.settings import USE_WORKGROUPS
from emencia.django.newsletter.utils.workgroups import request_workgroups
from emencia.django.newsletter.utils.workgroups import request_workgroups_contacts_pk
from emencia.django.newsletter.utils.workgroups import request_workgroups_mailinglists_pk
from emencia.django.newsletter.utils.vcard import vcard_contacts_export_response
from emencia.django.newsletter.utils.excel import ExcelResponse
class MailingListAdmin(admin.ModelAdmin):
date_hierarchy = 'creation_date'
list_display = ('creation_date', 'name', 'description',
'subscribers_count', 'unsubscribers_count',
'exportation_links')
list_editable = ('name', 'description')
list_filter = ('creation_date', 'modification_date')
search_fields = ('name', 'description',)
filter_horizontal = ['subscribers', 'unsubscribers']
fieldsets = ((None, {'fields': ('name', 'description',)}),
(None, {'fields': ('subscribers',)}),
(None, {'fields': ('unsubscribers',)}),
)
actions = ['merge_mailinglist']
actions_on_top = False
actions_on_bottom = True
def queryset(self, request):
queryset = super(MailingListAdmin, self).queryset(request)
if not request.user.is_superuser and USE_WORKGROUPS:
mailinglists_pk = request_workgroups_mailinglists_pk(request)
queryset = queryset.filter(pk__in=mailinglists_pk)
return queryset
def save_model(self, request, mailinglist, form, change):
workgroups = []
if not mailinglist.pk and not request.user.is_superuser \
and USE_WORKGROUPS:
workgroups = request_workgroups(request)
mailinglist.save()
for workgroup in workgroups:
workgroup.mailinglists.add(mailinglist)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if 'subscribers' in db_field.name and not request.user.is_superuser \
and USE_WORKGROUPS:
contacts_pk = request_workgroups_contacts_pk(request)
kwargs['queryset'] = Contact.objects.filter(pk__in=contacts_pk)
return super(MailingListAdmin, self).formfield_for_manytomany(
db_field, request, **kwargs)
def merge_mailinglist(self, request, queryset):
"""Merge multiple mailing list"""
if queryset.count() == 1:
self.message_user(request, _('Please select a least 2 mailing list.'))
return None
subscribers = {}
unsubscribers = {}
for ml in queryset:
for contact in ml.subscribers.all():
subscribers[contact] = ''
for contact in ml.unsubscribers.all():
unsubscribers[contact] = ''
when = str(datetime.now()).split('.')[0]
new_mailing = MailingList(name=_('Merging list at %s') % when,
description=_('Mailing list created by merging at %s') % when)
new_mailing.save()
new_mailing.subscribers = subscribers.keys()
new_mailing.unsubscribers = unsubscribers.keys()
if not request.user.is_superuser and USE_WORKGROUPS:
for workgroup in request_workgroups(request):
workgroup.mailinglists.add(new_mailing)
self.message_user(request, _('%s succesfully created by merging.') % new_mailing)
return HttpResponseRedirect(reverse('admin:newsletter_mailinglist_change',
args=[new_mailing.pk]))
merge_mailinglist.short_description = _('Merge selected mailinglists')
def exportation_links(self, mailinglist):
"""Display links for exportation"""
return u'<a href="%s">%s</a> / <a href="%s">%s</a>' % (
reverse('admin:newsletter_mailinglist_export_excel',
args=[mailinglist.pk]), _('Excel'),
reverse('admin:newsletter_mailinglist_export_vcard',
args=[mailinglist.pk]), _('VCard'))
exportation_links.allow_tags = True
exportation_links.short_description = _('Export')
def exportion_vcard(self, request, mailinglist_id):
"""Export subscribers in the mailing in VCard"""
mailinglist = get_object_or_404(MailingList, pk=mailinglist_id)
name = 'contacts_%s' % smart_str(mailinglist.name)
return vcard_contacts_export_response(mailinglist.subscribers.all(), name)
def exportion_excel(self, request, mailinglist_id):
"""Export subscribers in the mailing in Excel"""
mailinglist = get_object_or_404(MailingList, pk=mailinglist_id)
name = 'contacts_%s' % smart_str(mailinglist.name)
return ExcelResponse(mailinglist.subscribers.all(), name)
def get_urls(self):
urls = super(MailingListAdmin, self).get_urls()
my_urls = patterns('',
url(r'^export/vcard/(?P<mailinglist_id>\d+)/$',
self.admin_site.admin_view(self.exportion_vcard),
name='newsletter_mailinglist_export_vcard'),
url(r'^export/excel/(?P<mailinglist_id>\d+)/$',
self.admin_site.admin_view(self.exportion_excel),
name='newsletter_mailinglist_export_excel'))
return my_urls + urls

@ -0,0 +1,185 @@
"""ModelAdmin for Newsletter"""
from HTMLParser import HTMLParseError
from django import forms
from django.db.models import Q
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from emencia.django.newsletter.models import Contact
from emencia.django.newsletter.models import Newsletter
from emencia.django.newsletter.models import Attachment
from emencia.django.newsletter.models import MailingList
from emencia.django.newsletter.mailer import Mailer
from emencia.django.newsletter.settings import USE_TINYMCE
from emencia.django.newsletter.settings import USE_WORKGROUPS
try:
CAN_USE_PREMAILER = True
from emencia.django.newsletter.utils.premailer import Premailer
from emencia.django.newsletter.utils.premailer import PremailerError
except ImportError:
CAN_USE_PREMAILER = False
from emencia.django.newsletter.utils.workgroups import request_workgroups
from emencia.django.newsletter.utils.workgroups import request_workgroups_contacts_pk
from emencia.django.newsletter.utils.workgroups import request_workgroups_newsletters_pk
from emencia.django.newsletter.utils.workgroups import request_workgroups_mailinglists_pk
class AttachmentAdminInline(admin.TabularInline):
model = Attachment
extra = 1
fieldsets = ((None, {'fields': (('title', 'file_attachment'))}),)
class BaseNewsletterAdmin(admin.ModelAdmin):
date_hierarchy = 'creation_date'
list_display = ('title', 'mailing_list', 'server', 'status',
'sending_date', 'creation_date', 'modification_date',
'historic_link', 'statistics_link')
list_filter = ('status', 'sending_date', 'creation_date', 'modification_date')
search_fields = ('title', 'content', 'header_sender', 'header_reply')
filter_horizontal = ['test_contacts']
fieldsets = ((None, {'fields': ('title', 'content',)}),
(_('Receivers'), {'fields': ('mailing_list', 'test_contacts',)}),
(_('Sending'), {'fields': ('sending_date', 'status',)}),
(_('Miscellaneous'), {'fields': ('server', 'header_sender',
'header_reply', 'slug'),
'classes': ('collapse',)}),
)
prepopulated_fields = {'slug': ('title',)}
inlines = (AttachmentAdminInline,)
actions = ['send_mail_test', 'make_ready_to_send', 'make_cancel_sending']
actions_on_top = False
actions_on_bottom = True
def get_actions(self, request):
actions = super(BaseNewsletterAdmin, self).get_actions(request)
if not request.user.has_perm('newsletter.can_change_status'):
del actions['make_ready_to_send']
del actions['make_cancel_sending']
return actions
def queryset(self, request):
queryset = super(BaseNewsletterAdmin, self).queryset(request)
if not request.user.is_superuser and USE_WORKGROUPS:
newsletters_pk = request_workgroups_newsletters_pk(request)
queryset = queryset.filter(pk__in=newsletters_pk)
return queryset
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'mailing_list' and \
not request.user.is_superuser and USE_WORKGROUPS:
mailinglists_pk = request_workgroups_mailinglists_pk(request)
kwargs['queryset'] = MailingList.objects.filter(pk__in=mailinglists_pk)
return db_field.formfield(**kwargs)
return super(BaseNewsletterAdmin, self).formfield_for_foreignkey(
db_field, request, **kwargs)
def formfield_for_choice_field(self, db_field, request, **kwargs):
if db_field.name == 'status' and \
not request.user.has_perm('newsletter.can_change_status'):
kwargs['choices'] = ((Newsletter.DRAFT, _('Default')),)
return db_field.formfield(**kwargs)
return super(BaseNewsletterAdmin, self).formfield_for_choice_field(
db_field, request, **kwargs)
def formfield_for_manytomany(self, db_field, request, **kwargs):
if db_field.name == 'test_contacts':
queryset = Contact.objects.filter(tester=True)
if not request.user.is_superuser and USE_WORKGROUPS:
contacts_pk = request_workgroups_contacts_pk(request)
queryset = queryset.filter(pk__in=contacts_pk)
kwargs['queryset'] = queryset
return super(BaseNewsletterAdmin, self).formfield_for_manytomany(
db_field, request, **kwargs)
def save_model(self, request, newsletter, form, change):
workgroups = []
if not newsletter.pk and not request.user.is_superuser \
and USE_WORKGROUPS:
workgroups = request_workgroups(request)
if newsletter.content.startswith('http://'):
if CAN_USE_PREMAILER:
try:
premailer = Premailer(newsletter.content.strip())
newsletter.content = premailer.transform()
except PremailerError:
self.message_user(request, _('Unable to download HTML, due to errors within.'))
else:
self.message_user(request, _('Please install lxml for parsing an URL.'))
if not request.user.has_perm('newsletter.can_change_status'):
newsletter.status = form.initial.get('status', Newsletter.DRAFT)
newsletter.save()
for workgroup in workgroups:
workgroup.newsletters.add(newsletter)
def historic_link(self, newsletter):
"""Display link for historic"""
if newsletter.contactmailingstatus_set.count():
return u'<a href="%s">%s</a>' % (newsletter.get_historic_url(), _('View historic'))
return _('Not available')
historic_link.allow_tags = True
historic_link.short_description = _('Historic')
def statistics_link(self, newsletter):
"""Display link for statistics"""
if newsletter.status == Newsletter.SENDING or \
newsletter.status == Newsletter.SENT:
return u'<a href="%s">%s</a>' % (newsletter.get_statistics_url(), _('View statistics'))
return _('Not available')
statistics_link.allow_tags = True
statistics_link.short_description = _('Statistics')
def send_mail_test(self, request, queryset):
"""Send newsletter in test"""
for newsletter in queryset:
if newsletter.test_contacts.count():
mailer = Mailer(newsletter, test=True)
try:
mailer.run()
except HTMLParseError:
self.message_user(request, _('Unable send newsletter, due to errors within HTML.'))
continue
self.message_user(request, _('%s succesfully sent.') % newsletter)
else:
self.message_user(request, _('No test contacts assigned for %s.') % newsletter)
send_mail_test.short_description = _('Send test email')
def make_ready_to_send(self, request, queryset):
"""Make newsletter ready to send"""
queryset = queryset.filter(status=Newsletter.DRAFT)
for newsletter in queryset:
newsletter.status = Newsletter.WAITING
newsletter.save()
self.message_user(request, _('%s newletters are ready to send') % queryset.count())
make_ready_to_send.short_description = _('Make ready to send')
def make_cancel_sending(self, request, queryset):
"""Cancel the sending of newsletters"""
queryset = queryset.filter(Q(status=Newsletter.WAITING) |
Q(status=Newsletter.SENDING))
for newsletter in queryset:
newsletter.status = Newsletter.CANCELED
newsletter.save()
self.message_user(request, _('%s newletters are cancelled') % queryset.count())
make_cancel_sending.short_description = _('Cancel the sending')
if USE_TINYMCE:
from tinymce.widgets import TinyMCE
class NewsletterTinyMCEForm(forms.ModelForm):
content = forms.CharField(
widget=TinyMCE(attrs={'cols': 150, 'rows': 80}))
class Meta:
model = Newsletter
class NewsletterAdmin(BaseNewsletterAdmin):
form = NewsletterTinyMCEForm
else:
class NewsletterAdmin(BaseNewsletterAdmin):
pass

@ -0,0 +1,57 @@
"""ModelAdmin for SMTPServer"""
from django import forms
from django.contrib import admin
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext_lazy as _
from emencia.django.newsletter.models import SMTPServer
class SMTPServerAdminForm(forms.ModelForm):
"""Form ofr SMTPServer with custom validation"""
def clean_headers(self):
"""Check if the headers are well formated"""
for line in self.cleaned_data['headers'].splitlines():
elems = line.split(':')
if len(elems) < 2:
raise ValidationError(_('Invalid syntax, do not forget the ":".'))
if len(elems) > 2:
raise ValidationError(_('Invalid syntax, several assignments by line.'))
return self.cleaned_data['headers']
class Meta:
model = SMTPServer
class SMTPServerAdmin(admin.ModelAdmin):
form = SMTPServerAdminForm
list_display = ('name', 'host', 'port', 'user', 'tls', 'mails_hour',)
list_filter = ('tls',)
search_fields = ('name', 'host', 'user')
fieldsets = ((None, {'fields': ('name', )}),
(_('Configuration'), {'fields': ('host', 'port',
'user', 'password', 'tls')}),
(_('Miscellaneous'), {'fields': ('mails_hour', 'headers'),
'classes': ('collapse', )}),
)
actions = ['check_connections']
actions_on_top = False
actions_on_bottom = True
def check_connections(self, request, queryset):
"""Check the SMTP connection"""
message = '%s connection %s'
for server in queryset:
try:
smtp = server.connect()
if smtp:
status = 'OK'
smtp.quit()
else:
status = 'KO'
except:
status = 'KO'
self.message_user(request, message % (server.__unicode__(), status))
check_connections.short_description = _('Check connection')

@ -0,0 +1,26 @@
"""ModelAdmin for WorkGroup"""
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
class WorkGroupAdmin(admin.ModelAdmin):
list_display = ('name', 'group', 'contacts_length',
'mailinglists_length', 'newsletters_length')
fieldsets = ((None, {'fields': ('name', 'group')}),
(None, {'fields': ('contacts', 'mailinglists', 'newsletters')}),
)
filter_horizontal = ['contacts', 'mailinglists', 'newsletters']
actions_on_top = False
actions_on_bottom = True
def contacts_length(self, workgroup):
return workgroup.contacts.count()
contacts_length.short_description = _('Contacts length')
def mailinglists_length(self, workgroup):
return workgroup.mailinglists.count()
mailinglists_length.short_description = _('Mailing List length')
def newsletters_length(self, workgroup):
return workgroup.newsletters.count()
newsletters_length.short_description = _('Newsletter length')

@ -0,0 +1,38 @@
"""Plugins for CMS"""
from django.utils.translation import ugettext_lazy as _
from cms.plugin_base import CMSPluginBase
from cms.plugin_pool import plugin_pool
from emencia.django.newsletter.cmsplugin_newsletter import settings
from emencia.django.newsletter.cmsplugin_newsletter.models import SubscriptionFormPlugin
from emencia.django.newsletter.forms import MailingListSubscriptionForm
class CMSSubscriptionFormPlugin(CMSPluginBase):
module = _('newsletter')
model = SubscriptionFormPlugin
name = _('Subscription Form')
render_template = 'newsletter/cms/subscription_form.html'
text_enabled = False
admin_preview = False
def render(self, context, instance, placeholder):
request = context['request']
if request.method == "POST" and (settings.FORM_NAME in request.POST.keys()):
form = MailingListSubscriptionForm(data=request.POST)
if form.is_valid():
form.save(instance.mailing_list)
form.saved = True
else:
form = MailingListSubscriptionForm()
context.update({
'object': instance,
'form': form,
'form_name': settings.FORM_NAME,
'placeholder': placeholder,
})
return context
plugin_pool.register_plugin(CMSSubscriptionFormPlugin)

@ -0,0 +1,19 @@
"""Models of Emencia CMS Plugins"""
from django.db import models
from django.utils.translation import ugettext_lazy as _
from cms.models import CMSPlugin
from emencia.django.newsletter.models import MailingList
class SubscriptionFormPlugin(CMSPlugin):
"""CMS Plugin for susbcribing to a mailing list"""
title = models.CharField(_('title'), max_length=100, blank=True)
show_description = models.BooleanField(_('show description'), default=True,
help_text=_('Show the mailing list\'s description.'))
mailing_list = models.ForeignKey(MailingList, verbose_name=_('mailing list'),
help_text=_('Mailing List to subscribe to.'))
def __unicode__(self):
return self.mailing_list.name

@ -0,0 +1,4 @@
"""Settings for emencia.django.newsletter.cmsplugin_newsletter"""
from django.conf import settings
FORM_NAME = getattr(settings, 'SUBSCRIPTION_FORM_NAME', 'cms_subscription_form_plugin')

@ -0,0 +1,56 @@
"""Forms for emencia.django.newsletter"""
from django import forms
from django.utils.translation import ugettext_lazy as _
from emencia.django.newsletter.models import Contact
from emencia.django.newsletter.models import MailingList
class MailingListSubscriptionForm(forms.ModelForm):
"""Form for subscribing to a mailing list"""
# Notes : This form will not check the uniquess of
# the 'email' field, by defining it explictly and setting
# it the Meta.exclude list, for allowing registration
# to a mailing list even if the contact already exists.
# Then the contact is always added to the subscribers field
# of the mailing list because it will be cleaned with no
# double.
email = forms.EmailField(label=_('Email'), max_length=75)
def save(self, mailing_list):
data = self.cleaned_data
contact, created = Contact.objects.get_or_create(
email=data['email'],
defaults={'first_name': data['first_name'],
'last_name': data['last_name']})
mailing_list.subscribers.add(contact)
mailing_list.unsubscribers.remove(contact)
class Meta:
model = Contact
fields = ('first_name', 'last_name')
exclude = ('email',)
class AllMailingListSubscriptionForm(MailingListSubscriptionForm):
"""Form for subscribing to all mailing list"""
mailing_lists = forms.ModelMultipleChoiceField(
queryset=MailingList.objects.all(),
initial=[obj.id for obj in MailingList.objects.all()],
label=_('Mailing lists'),
widget=forms.CheckboxSelectMultiple())
def save(self, mailing_list):
data = self.cleaned_data
contact, created = Contact.objects.get_or_create(
email=data['email'],
defaults={'first_name': data['first_name'],
'last_name': data['last_name']})
for mailing_list in data['mailing_lists']:
mailing_list.subscribers.add(contact)
mailing_list.unsubscribers.remove(contact)

@ -1,4 +1,4 @@
"""Mailer for django.newsletter"""
"""Mailer for emencia.django.newsletter"""
import re
import sys
import time
@ -35,19 +35,20 @@ from django.utils.encoding import smart_str
from django.utils.encoding import smart_unicode
from django.core.urlresolvers import reverse
from newsletter.models import Newsletter
from newsletter.models import ContactMailingStatus
from newsletter.utils.tokens import tokenize
from newsletter.utils.newsletter import track_links
from newsletter.utils.newsletter import body_insertion
from newsletter.settings import TRACKING_LINKS
from newsletter.settings import TRACKING_IMAGE
from newsletter.settings import TRACKING_IMAGE_FORMAT
from newsletter.settings import UNIQUE_KEY_LENGTH
from newsletter.settings import UNIQUE_KEY_CHAR_SET
from newsletter.settings import INCLUDE_UNSUBSCRIPTION
from newsletter.settings import SLEEP_BETWEEN_SENDING
from newsletter.settings import RESTART_CONNECTION_BETWEEN_SENDING
from emencia.django.newsletter.models import Newsletter
from emencia.django.newsletter.models import ContactMailingStatus
from emencia.django.newsletter.utils.tokens import tokenize
from emencia.django.newsletter.utils.newsletter import track_links
from emencia.django.newsletter.utils.newsletter import body_insertion
from emencia.django.newsletter.settings import TRACKING_LINKS
from emencia.django.newsletter.settings import TRACKING_IMAGE
from emencia.django.newsletter.settings import TRACKING_IMAGE_FORMAT
from emencia.django.newsletter.settings import UNIQUE_KEY_LENGTH
from emencia.django.newsletter.settings import UNIQUE_KEY_CHAR_SET
from emencia.django.newsletter.settings import INCLUDE_UNSUBSCRIPTION
from emencia.django.newsletter.settings import SLEEP_BETWEEN_SENDING
from emencia.django.newsletter.settings import \
RESTART_CONNECTION_BETWEEN_SENDING
import HTMLParser
import chardet

@ -3,8 +3,8 @@ from django.conf import settings
from django.utils.translation import activate
from django.core.management.base import NoArgsCommand
from newsletter.mailer import Mailer
from newsletter.models import Newsletter
from emencia.django.newsletter.mailer import Mailer
from emencia.django.newsletter.models import Newsletter
class Command(NoArgsCommand):

@ -8,8 +8,8 @@ from django.utils.translation import activate
from django.core import signals
from django.core.management.base import NoArgsCommand
from newsletter.mailer import SMTPMailer
from newsletter.models import SMTPServer
from emencia.django.newsletter.mailer import SMTPMailer
from emencia.django.newsletter.models import SMTPServer
class Command(NoArgsCommand):

@ -1,3 +1,4 @@
"""Managers for emencia.django.newsletter"""
from django.db import models

@ -0,0 +1,272 @@
from south.db import db
from django.db import models
from emencia.django.newsletter.models import *
class Migration:
def forwards(self, orm):
# Adding model 'MailingList'
db.create_table('newsletter_mailinglist', (
('id', orm['newsletter.MailingList:id']),
('name', orm['newsletter.MailingList:name']),
('description', orm['newsletter.MailingList:description']),
('creation_date', orm['newsletter.MailingList:creation_date']),
('modification_date', orm['newsletter.MailingList:modification_date']),
))
db.send_create_signal('newsletter', ['MailingList'])
# Adding model 'ContactMailingStatus'
db.create_table('newsletter_contactmailingstatus', (
('id', orm['newsletter.ContactMailingStatus:id']),
('newsletter', orm['newsletter.ContactMailingStatus:newsletter']),
('contact', orm['newsletter.ContactMailingStatus:contact']),
('status', orm['newsletter.ContactMailingStatus:status']),
('link', orm['newsletter.ContactMailingStatus:link']),
('creation_date', orm['newsletter.ContactMailingStatus:creation_date']),
))
db.send_create_signal('newsletter', ['ContactMailingStatus'])
# Adding model 'WorkGroup'
db.create_table('newsletter_workgroup', (
('id', orm['newsletter.WorkGroup:id']),
('name', orm['newsletter.WorkGroup:name']),
('group', orm['newsletter.WorkGroup:group']),
))
db.send_create_signal('newsletter', ['WorkGroup'])
# Adding model 'Link'
db.create_table('newsletter_link', (
('id', orm['newsletter.Link:id']),
('title', orm['newsletter.Link:title']),
('url', orm['newsletter.Link:url']),
('creation_date', orm['newsletter.Link:creation_date']),
))
db.send_create_signal('newsletter', ['Link'])
# Adding model 'Newsletter'
db.create_table('newsletter_newsletter', (
('id', orm['newsletter.Newsletter:id']),
('title', orm['newsletter.Newsletter:title']),
('content', orm['newsletter.Newsletter:content']),
('mailing_list', orm['newsletter.Newsletter:mailing_list']),
('server', orm['newsletter.Newsletter:server']),
('header_sender', orm['newsletter.Newsletter:header_sender']),
('header_reply', orm['newsletter.Newsletter:header_reply']),
('status', orm['newsletter.Newsletter:status']),
('sending_date', orm['newsletter.Newsletter:sending_date']),
('slug', orm['newsletter.Newsletter:slug']),
('creation_date', orm['newsletter.Newsletter:creation_date']),
('modification_date', orm['newsletter.Newsletter:modification_date']),
))
db.send_create_signal('newsletter', ['Newsletter'])
# Adding model 'SMTPServer'
db.create_table('newsletter_smtpserver', (
('id', orm['newsletter.SMTPServer:id']),
('name', orm['newsletter.SMTPServer:name']),
('host', orm['newsletter.SMTPServer:host']),
('user', orm['newsletter.SMTPServer:user']),
('password', orm['newsletter.SMTPServer:password']),
('port', orm['newsletter.SMTPServer:port']),
('tls', orm['newsletter.SMTPServer:tls']),
('headers', orm['newsletter.SMTPServer:headers']),
('mails_hour', orm['newsletter.SMTPServer:mails_hour']),
))
db.send_create_signal('newsletter', ['SMTPServer'])
# Adding model 'Contact'
db.create_table('newsletter_contact', (
('id', orm['newsletter.Contact:id']),
('email', orm['newsletter.Contact:email']),
('first_name', orm['newsletter.Contact:first_name']),
('last_name', orm['newsletter.Contact:last_name']),
('subscriber', orm['newsletter.Contact:subscriber']),
('valid', orm['newsletter.Contact:valid']),
('tester', orm['newsletter.Contact:tester']),
('tags', orm['newsletter.Contact:tags']),
('content_type', orm['newsletter.Contact:content_type']),
('object_id', orm['newsletter.Contact:object_id']),
('creation_date', orm['newsletter.Contact:creation_date']),
('modification_date', orm['newsletter.Contact:modification_date']),
))
db.send_create_signal('newsletter', ['Contact'])
# Adding ManyToManyField 'WorkGroup.mailinglists'
db.create_table('newsletter_workgroup_mailinglists', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('workgroup', models.ForeignKey(orm.WorkGroup, null=False)),
('mailinglist', models.ForeignKey(orm.MailingList, null=False))
))
# Adding ManyToManyField 'MailingList.subscribers'
db.create_table('newsletter_mailinglist_subscribers', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('mailinglist', models.ForeignKey(orm.MailingList, null=False)),
('contact', models.ForeignKey(orm.Contact, null=False))
))
# Adding ManyToManyField 'WorkGroup.contacts'
db.create_table('newsletter_workgroup_contacts', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('workgroup', models.ForeignKey(orm.WorkGroup, null=False)),
('contact', models.ForeignKey(orm.Contact, null=False))
))
# Adding ManyToManyField 'WorkGroup.newsletters'
db.create_table('newsletter_workgroup_newsletters', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('workgroup', models.ForeignKey(orm.WorkGroup, null=False)),
('newsletter', models.ForeignKey(orm.Newsletter, null=False))
))
# Adding ManyToManyField 'MailingList.unsubscribers'
db.create_table('newsletter_mailinglist_unsubscribers', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('mailinglist', models.ForeignKey(orm.MailingList, null=False)),
('contact', models.ForeignKey(orm.Contact, null=False))
))
# Adding ManyToManyField 'Newsletter.test_contacts'
db.create_table('newsletter_newsletter_test_contacts', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('newsletter', models.ForeignKey(orm.Newsletter, null=False)),
('contact', models.ForeignKey(orm.Contact, null=False))
))
def backwards(self, orm):
# Deleting model 'MailingList'
db.delete_table('newsletter_mailinglist')
# Deleting model 'ContactMailingStatus'
db.delete_table('newsletter_contactmailingstatus')
# Deleting model 'WorkGroup'
db.delete_table('newsletter_workgroup')
# Deleting model 'Link'
db.delete_table('newsletter_link')
# Deleting model 'Newsletter'
db.delete_table('newsletter_newsletter')
# Deleting model 'SMTPServer'
db.delete_table('newsletter_smtpserver')
# Deleting model 'Contact'
db.delete_table('newsletter_contact')
# Dropping ManyToManyField 'WorkGroup.mailinglists'
db.delete_table('newsletter_workgroup_mailinglists')
# Dropping ManyToManyField 'MailingList.subscribers'
db.delete_table('newsletter_mailinglist_subscribers')
# Dropping ManyToManyField 'WorkGroup.contacts'
db.delete_table('newsletter_workgroup_contacts')
# Dropping ManyToManyField 'WorkGroup.newsletters'
db.delete_table('newsletter_workgroup_newsletters')
# Dropping ManyToManyField 'MailingList.unsubscribers'
db.delete_table('newsletter_mailinglist_unsubscribers')
# Dropping ManyToManyField 'Newsletter.test_contacts'
db.delete_table('newsletter_newsletter_test_contacts')
models = {
'auth.group': {
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'blank': 'True'})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)"},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'newsletter.contact': {
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'subscriber': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'tags': ('tagging.fields.TagField', [], {'default': "''"}),
'tester': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'valid': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'})
},
'newsletter.contactmailingstatus': {
'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Contact']"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'link': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Link']", 'null': 'True', 'blank': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Newsletter']"}),
'status': ('django.db.models.fields.IntegerField', [], {})
},
'newsletter.link': {
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.mailinglist': {
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['newsletter.Contact']"}),
'unsubscribers': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'})
},
'newsletter.newsletter': {
'content': ('django.db.models.fields.TextField', [], {'default': "u'<body>\\n<!-- Edit your newsletter here -->\\n</body>'"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'header_reply': ('django.db.models.fields.CharField', [], {'default': "'Emencia Newsletter<noreply@emencia.com>'", 'max_length': '255'}),
'header_sender': ('django.db.models.fields.CharField', [], {'default': "'Emencia Newsletter<noreply@emencia.com>'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailing_list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.MailingList']"}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'sending_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'server': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['newsletter.SMTPServer']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'test_contacts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.smtpserver': {
'headers': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mails_hour': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '25'}),
'tls': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'})
},
'newsletter.workgroup': {
'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailinglists': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['newsletter.MailingList']", 'null': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'newsletters': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['newsletter.Newsletter']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['newsletter']

@ -0,0 +1,130 @@
from south.db import db
from south.v2 import SchemaMigration
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Attachment'
db.create_table('newsletter_attachment', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('newsletter', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['newsletter.Newsletter'])),
('title', self.gf('django.db.models.fields.CharField')(max_length=255)),
('file_attachment', self.gf('django.db.models.fields.files.FileField')(max_length=100)),
))
db.send_create_signal('newsletter', ['Attachment'])
def backwards(self, orm):
# Deleting model 'Attachment'
db.delete_table('newsletter_attachment')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'newsletter.attachment': {
'Meta': {'object_name': 'Attachment'},
'file_attachment': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Newsletter']"}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.contact': {
'Meta': {'object_name': 'Contact'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'subscriber': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
'tags': ('tagging.fields.TagField', [], {'default': "''"}),
'tester': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'valid': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'})
},
'newsletter.contactmailingstatus': {
'Meta': {'object_name': 'ContactMailingStatus'},
'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Contact']"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'link': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Link']", 'null': 'True', 'blank': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Newsletter']"}),
'status': ('django.db.models.fields.IntegerField', [], {})
},
'newsletter.link': {
'Meta': {'object_name': 'Link'},
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.mailinglist': {
'Meta': {'object_name': 'MailingList'},
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mailinglist_subscriber'", 'symmetrical': 'False', 'to': "orm['newsletter.Contact']"}),
'unsubscribers': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'mailinglist_unsubscriber'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['newsletter.Contact']"})
},
'newsletter.newsletter': {
'Meta': {'object_name': 'Newsletter'},
'content': ('django.db.models.fields.TextField', [], {'default': "u'<body>\\n<!-- Edit your newsletter here -->\\n</body>'"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'header_reply': ('django.db.models.fields.CharField', [], {'default': "'Giorgio Barbarotta Newsletter<noreply@giorgiobarbarotta.it>'", 'max_length': '255'}),
'header_sender': ('django.db.models.fields.CharField', [], {'default': "'Giorgio Barbarotta Newsletter<noreply@giorgiobarbarotta.it>'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailing_list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.MailingList']"}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'sending_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'server': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['newsletter.SMTPServer']"}),
'slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'db_index': 'True'}),
'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'test_contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.smtpserver': {
'Meta': {'object_name': 'SMTPServer'},
'headers': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mails_hour': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '25'}),
'tls': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
'user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'})
},
'newsletter.workgroup': {
'Meta': {'object_name': 'WorkGroup'},
'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailinglists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.MailingList']", 'null': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'newsletters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.Newsletter']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['newsletter']

@ -0,0 +1,123 @@
from south.db import db
from south.v2 import SchemaMigration
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding unique constraint on 'Newsletter', fields ['slug']
db.create_unique('newsletter_newsletter', ['slug'])
def backwards(self, orm):
# Removing unique constraint on 'Newsletter', fields ['slug']
db.delete_unique('newsletter_newsletter', ['slug'])
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'newsletter.attachment': {
'Meta': {'object_name': 'Attachment'},
'file_attachment': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Newsletter']"}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.contact': {
'Meta': {'ordering': "('creation_date',)", 'object_name': 'Contact'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.EmailField', [], {'unique': 'True', 'max_length': '75'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'subscriber': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'tags': ('tagging.fields.TagField', [], {'default': "''"}),
'tester': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'valid': ('django.db.models.fields.BooleanField', [], {'default': 'True'})
},
'newsletter.contactmailingstatus': {
'Meta': {'ordering': "('creation_date',)", 'object_name': 'ContactMailingStatus'},
'contact': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Contact']"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'link': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Link']", 'null': 'True', 'blank': 'True'}),
'newsletter': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.Newsletter']"}),
'status': ('django.db.models.fields.IntegerField', [], {})
},
'newsletter.link': {
'Meta': {'ordering': "('creation_date',)", 'object_name': 'Link'},
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'url': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.mailinglist': {
'Meta': {'ordering': "('creation_date',)", 'object_name': 'MailingList'},
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'subscribers': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mailinglist_subscriber'", 'symmetrical': 'False', 'to': "orm['newsletter.Contact']"}),
'unsubscribers': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'mailinglist_unsubscriber'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['newsletter.Contact']"})
},
'newsletter.newsletter': {
'Meta': {'ordering': "('creation_date',)", 'object_name': 'Newsletter'},
'content': ('django.db.models.fields.TextField', [], {'default': "u'<body>\\n<!-- Edit your newsletter here -->\\n</body>'"}),
'creation_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'header_reply': ('django.db.models.fields.CharField', [], {'default': "'Emencia Newsletter<noreply@emencia.com>'", 'max_length': '255'}),
'header_sender': ('django.db.models.fields.CharField', [], {'default': "'Emencia Newsletter<noreply@emencia.com>'", 'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailing_list': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['newsletter.MailingList']"}),
'modification_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'sending_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'server': ('django.db.models.fields.related.ForeignKey', [], {'default': '1', 'to': "orm['newsletter.SMTPServer']"}),
'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
'status': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'test_contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'title': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'newsletter.smtpserver': {
'Meta': {'object_name': 'SMTPServer'},
'headers': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'host': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mails_hour': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
'port': ('django.db.models.fields.IntegerField', [], {'default': '25'}),
'tls': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'})
},
'newsletter.workgroup': {
'Meta': {'object_name': 'WorkGroup'},
'contacts': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.Contact']", 'null': 'True', 'blank': 'True'}),
'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'mailinglists': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.MailingList']", 'null': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'newsletters': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['newsletter.Newsletter']", 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['newsletter']

@ -1,4 +1,6 @@
"""Models for emencia.django.newsletter"""
from smtplib import SMTP
from smtplib import SMTPHeloError
from datetime import datetime
from datetime import timedelta
@ -11,13 +13,13 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Group
from django.utils.encoding import force_unicode
from newsletter.managers import ContactManager
from newsletter.settings import BASE_PATH
from newsletter.settings import MAILER_HARD_LIMIT
from newsletter.settings import DEFAULT_HEADER_REPLY
from newsletter.settings import DEFAULT_HEADER_SENDER
from newsletter.utils.vcard import vcard_contact_export
from emencia.django.newsletter.managers import ContactManager
from emencia.django.newsletter.settings import BASE_PATH
from emencia.django.newsletter.settings import MAILER_HARD_LIMIT
from emencia.django.newsletter.settings import DEFAULT_HEADER_REPLY
from emencia.django.newsletter.settings import DEFAULT_HEADER_SENDER
from emencia.django.newsletter.utils.vcard import vcard_contact_export
class SMTPServer(models.Model):
"""Configuration of a SMTP server"""

@ -1,3 +1,4 @@
"""Settings for emencia.django.newsletter"""
import string
from django.conf import settings
@ -37,6 +38,3 @@ RESTART_CONNECTION_BETWEEN_SENDING = getattr(
settings, 'NEWSLETTER_RESTART_CONNECTION_BETWEEN_SENDING', False)
BASE_PATH = getattr(settings, 'NEWSLETTER_BASE_PATH', 'uploads/newsletter')
#
PERIODIC_FOR_ANNOUNCES = [(1, 'week'), (2, '2 weeks'), (3, 'month')]

@ -0,0 +1,660 @@
/*
* jQuery Form Plugin
* version: 2.36 (07-NOV-2009)
* @requires jQuery v1.2.6 or later
*
* Examples and documentation at: http://malsup.com/jquery/form/
* Dual licensed under the MIT and GPL licenses:
* http://www.opensource.org/licenses/mit-license.php
* http://www.gnu.org/licenses/gpl.html
*/
;(function($) {
/*
Usage Note:
-----------
Do not use both ajaxSubmit and ajaxForm on the same form. These
functions are intended to be exclusive. Use ajaxSubmit if you want
to bind your own submit handler to the form. For example,
$(document).ready(function() {
$('#myForm').bind('submit', function() {
$(this).ajaxSubmit({
target: '#output'
});
return false; // <-- important!
});
});
Use ajaxForm when you want the plugin to manage all the event binding
for you. For example,
$(document).ready(function() {
$('#myForm').ajaxForm({
target: '#output'
});
});
When using ajaxForm, the ajaxSubmit function will be invoked for you
at the appropriate time.
*/
/**
* ajaxSubmit() provides a mechanism for immediately submitting
* an HTML form using AJAX.
*/
$.fn.ajaxSubmit = function(options) {
// fast fail if nothing selected (http://dev.jquery.com/ticket/2752)
if (!this.length) {
log('ajaxSubmit: skipping submit process - no element selected');
return this;
}
if (typeof options == 'function')
options = { success: options };
var url = $.trim(this.attr('action'));
if (url) {
// clean url (don't include hash vaue)
url = (url.match(/^([^#]+)/)||[])[1];
}
url = url || window.location.href || '';
options = $.extend({
url: url,
type: this.attr('method') || 'GET',
iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank'
}, options || {});
// hook for manipulating the form data before it is extracted;
// convenient for use with rich editors like tinyMCE or FCKEditor
var veto = {};
this.trigger('form-pre-serialize', [this, options, veto]);
if (veto.veto) {
log('ajaxSubmit: submit vetoed via form-pre-serialize trigger');
return this;
}
// provide opportunity to alter form data before it is serialized
if (options.beforeSerialize && options.beforeSerialize(this, options) === false) {
log('ajaxSubmit: submit aborted via beforeSerialize callback');
return this;
}
var a = this.formToArray(options.semantic);
if (options.data) {
options.extraData = options.data;
for (var n in options.data) {
if(options.data[n] instanceof Array) {
for (var k in options.data[n])
a.push( { name: n, value: options.data[n][k] } );
}
else
a.push( { name: n, value: options.data[n] } );
}
}
// give pre-submit callback an opportunity to abort the submit
if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) {
log('ajaxSubmit: submit aborted via beforeSubmit callback');
return this;
}
// fire vetoable 'validate' event
this.trigger('form-submit-validate', [a, this, options, veto]);
if (veto.veto) {
log('ajaxSubmit: submit vetoed via form-submit-validate trigger');
return this;
}
var q = $.param(a);
if (options.type.toUpperCase() == 'GET') {
options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q;
options.data = null; // data is null for 'get'
}
else
options.data = q; // data is the query string for 'post'
var $form = this, callbacks = [];
if (options.resetForm) callbacks.push(function() { $form.resetForm(); });
if (options.clearForm) callbacks.push(function() { $form.clearForm(); });
// perform a load on the target only if dataType is not provided
if (!options.dataType && options.target) {
var oldSuccess = options.success || function(){};
callbacks.push(function(data) {
$(options.target).html(data).each(oldSuccess, arguments);
});
}
else if (options.success)
callbacks.push(options.success);
options.success = function(data, status) {
for (var i=0, max=callbacks.length; i < max; i++)
callbacks[i].apply(options, [data, status, $form]);
};
// are there files to upload?
var files = $('input:file', this).fieldValue();
var found = false;
for (var j=0; j < files.length; j++)
if (files[j])
found = true;
var multipart = false;
// var mp = 'multipart/form-data';
// multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp);
// options.iframe allows user to force iframe mode
// 06-NOV-09: now defaulting to iframe mode if file input is detected
if ((files.length && options.iframe !== false) || options.iframe || found || multipart) {
// hack to fix Safari hang (thanks to Tim Molendijk for this)
// see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d
if (options.closeKeepAlive)
$.get(options.closeKeepAlive, fileUpload);
else
fileUpload();
}
else
$.ajax(options);
// fire 'notify' event
this.trigger('form-submit-notify', [this, options]);
return this;
// private function for handling file uploads (hat tip to YAHOO!)
function fileUpload() {
var form = $form[0];
if ($(':input[name=submit]', form).length) {
alert('Error: Form elements must not be named "submit".');
return;
}
var opts = $.extend({}, $.ajaxSettings, options);
var s = $.extend(true, {}, $.extend(true, {}, $.ajaxSettings), opts);
var id = 'jqFormIO' + (new Date().getTime());
var $io = $('<iframe id="' + id + '" name="' + id + '" src="'+ opts.iframeSrc +'" />');
var io = $io[0];
$io.css({ position: 'absolute', top: '-1000px', left: '-1000px' });
var xhr = { // mock object
aborted: 0,
responseText: null,
responseXML: null,
status: 0,
statusText: 'n/a',
getAllResponseHeaders: function() {},
getResponseHeader: function() {},
setRequestHeader: function() {},
abort: function() {
this.aborted = 1;
$io.attr('src', opts.iframeSrc); // abort op in progress
}
};
var g = opts.global;
// trigger ajax global events so that activity/block indicators work like normal
if (g && ! $.active++) $.event.trigger("ajaxStart");
if (g) $.event.trigger("ajaxSend", [xhr, opts]);
if (s.beforeSend && s.beforeSend(xhr, s) === false) {
s.global && $.active--;
return;
}
if (xhr.aborted)
return;
var cbInvoked = 0;
var timedOut = 0;
// add submitting element to data if we know it
var sub = form.clk;
if (sub) {
var n = sub.name;
if (n && !sub.disabled) {
options.extraData = options.extraData || {};
options.extraData[n] = sub.value;
if (sub.type == "image") {
options.extraData[name+'.x'] = form.clk_x;
options.extraData[name+'.y'] = form.clk_y;
}
}
}
// take a breath so that pending repaints get some cpu time before the upload starts
setTimeout(function() {
// make sure form attrs are set
var t = $form.attr('target'), a = $form.attr('action');
// update form attrs in IE friendly way
form.setAttribute('target',id);
if (form.getAttribute('method') != 'POST')
form.setAttribute('method', 'POST');
if (form.getAttribute('action') != opts.url)
form.setAttribute('action', opts.url);
// ie borks in some cases when setting encoding
if (! options.skipEncodingOverride) {
$form.attr({
encoding: 'multipart/form-data',
enctype: 'multipart/form-data'
});
}
// support timout
if (opts.timeout)
setTimeout(function() { timedOut = true; cb(); }, opts.timeout);
// add "extra" data to form if provided in options
var extraInputs = [];
try {
if (options.extraData)
for (var n in options.extraData)
extraInputs.push(
$('<input type="hidden" name="'+n+'" value="'+options.extraData[n]+'" />')
.appendTo(form)[0]);
// add iframe to doc and submit the form
$io.appendTo('body');
io.attachEvent ? io.attachEvent('onload', cb) : io.addEventListener('load', cb, false);
form.submit();
}
finally {
// reset attrs and remove "extra" input elements
form.setAttribute('action',a);
t ? form.setAttribute('target', t) : $form.removeAttr('target');
$(extraInputs).remove();
}
}, 10);
var domCheckCount = 50;
function cb() {
if (cbInvoked++) return;
io.detachEvent ? io.detachEvent('onload', cb) : io.removeEventListener('load', cb, false);
var ok = true;
try {
if (timedOut) throw 'timeout';
// extract the server response from the iframe
var data, doc;
doc = io.contentWindow ? io.contentWindow.document : io.contentDocument ? io.contentDocument : io.document;
var isXml = opts.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc);
log('isXml='+isXml);
if (!isXml && (doc.body == null || doc.body.innerHTML == '')) {
if (--domCheckCount) {
// in some browsers (Opera) the iframe DOM is not always traversable when
// the onload callback fires, so we loop a bit to accommodate
cbInvoked = 0;
setTimeout(cb, 100);
return;
}
log('Could not access iframe DOM after 50 tries.');
return;
}
xhr.responseText = doc.body ? doc.body.innerHTML : null;
xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc;
xhr.getResponseHeader = function(header){
var headers = {'content-type': opts.dataType};
return headers[header];
};
if (opts.dataType == 'json' || opts.dataType == 'script') {
// see if user embedded response in textarea
var ta = doc.getElementsByTagName('textarea')[0];
if (ta)
xhr.responseText = ta.value;
else {
// account for browsers injecting pre around json response
var pre = doc.getElementsByTagName('pre')[0];
if (pre)
xhr.responseText = pre.innerHTML;
}
}
else if (opts.dataType == 'xml' && !xhr.responseXML && xhr.responseText != null) {
xhr.responseXML = toXml(xhr.responseText);
}
data = $.httpData(xhr, opts.dataType);
}
catch(e){
ok = false;
$.handleError(opts, xhr, 'error', e);
}
// ordering of these callbacks/triggers is odd, but that's how $.ajax does it
if (ok) {
opts.success(data, 'success');
if (g) $.event.trigger("ajaxSuccess", [xhr, opts]);
}
if (g) $.event.trigger("ajaxComplete", [xhr, opts]);
if (g && ! --$.active) $.event.trigger("ajaxStop");
if (opts.complete) opts.complete(xhr, ok ? 'success' : 'error');
// clean up
setTimeout(function() {
$io.remove();
xhr.responseXML = null;
}, 100);
};
function toXml(s, doc) {
if (window.ActiveXObject) {
doc = new ActiveXObject('Microsoft.XMLDOM');
doc.async = 'false';
doc.loadXML(s);
}
else
doc = (new DOMParser()).parseFromString(s, 'text/xml');
return (doc && doc.documentElement && doc.documentElement.tagName != 'parsererror') ? doc : null;
};
};
};
/**
* ajaxForm() provides a mechanism for fully automating form submission.
*
* The advantages of using this method instead of ajaxSubmit() are:
*
* 1: This method will include coordinates for <input type="image" /> elements (if the element
* is used to submit the form).
* 2. This method will include the submit element's name/value data (for the element that was
* used to submit the form).
* 3. This method binds the submit() method to the form for you.
*
* The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely
* passes the options argument along after properly binding events for submit elements and
* the form itself.
*/
$.fn.ajaxForm = function(options) {
return this.ajaxFormUnbind().bind('submit.form-plugin', function() {
$(this).ajaxSubmit(options);
return false;
}).bind('click.form-plugin', function(e) {
var target = e.target;
var $el = $(target);
if (!($el.is(":submit,input:image"))) {
// is this a child element of the submit el? (ex: a span within a button)
var t = $el.closest(':submit');
if (t.length == 0)
return;
target = t[0];
}
var form = this;
form.clk = target;
if (target.type == 'image') {
if (e.offsetX != undefined) {
form.clk_x = e.offsetX;
form.clk_y = e.offsetY;
} else if (typeof $.fn.offset == 'function') { // try to use dimensions plugin
var offset = $el.offset();
form.clk_x = e.pageX - offset.left;
form.clk_y = e.pageY - offset.top;
} else {
form.clk_x = e.pageX - target.offsetLeft;
form.clk_y = e.pageY - target.offsetTop;
}
}
// clear form vars
setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100);
});
};
// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm
$.fn.ajaxFormUnbind = function() {
return this.unbind('submit.form-plugin click.form-plugin');
};
/**
* formToArray() gathers form element data into an array of objects that can
* be passed to any of the following ajax functions: $.get, $.post, or load.
* Each object in the array has both a 'name' and 'value' property. An example of
* an array for a simple login form might be:
*
* [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ]
*
* It is this array that is passed to pre-submit callback functions provided to the
* ajaxSubmit() and ajaxForm() methods.
*/
$.fn.formToArray = function(semantic) {
var a = [];
if (this.length == 0) return a;
var form = this[0];
var els = semantic ? form.getElementsByTagName('*') : form.elements;
if (!els) return a;
for(var i=0, max=els.length; i < max; i++) {
var el = els[i];
var n = el.name;
if (!n) continue;
if (semantic && form.clk && el.type == "image") {
// handle image inputs on the fly when semantic == true
if(!el.disabled && form.clk == el) {
a.push({name: n, value: $(el).val()});
a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
}
continue;
}
var v = $.fieldValue(el, true);
if (v && v.constructor == Array) {
for(var j=0, jmax=v.length; j < jmax; j++)
a.push({name: n, value: v[j]});
}
else if (v !== null && typeof v != 'undefined')
a.push({name: n, value: v});
}
if (!semantic && form.clk) {
// input type=='image' are not found in elements array! handle it here
var $input = $(form.clk), input = $input[0], n = input.name;
if (n && !input.disabled && input.type == 'image') {
a.push({name: n, value: $input.val()});
a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y});
}
}
return a;
};
/**
* Serializes form data into a 'submittable' string. This method will return a string
* in the format: name1=value1&amp;name2=value2
*/
$.fn.formSerialize = function(semantic) {
//hand off to jQuery.param for proper encoding
return $.param(this.formToArray(semantic));
};
/**
* Serializes all field elements in the jQuery object into a query string.
* This method will return a string in the format: name1=value1&amp;name2=value2
*/
$.fn.fieldSerialize = function(successful) {
var a = [];
this.each(function() {
var n = this.name;
if (!n) return;
var v = $.fieldValue(this, successful);
if (v && v.constructor == Array) {
for (var i=0,max=v.length; i < max; i++)
a.push({name: n, value: v[i]});
}
else if (v !== null && typeof v != 'undefined')
a.push({name: this.name, value: v});
});
//hand off to jQuery.param for proper encoding
return $.param(a);
};
/**
* Returns the value(s) of the element in the matched set. For example, consider the following form:
*
* <form><fieldset>
* <input name="A" type="text" />
* <input name="A" type="text" />
* <input name="B" type="checkbox" value="B1" />
* <input name="B" type="checkbox" value="B2"/>
* <input name="C" type="radio" value="C1" />
* <input name="C" type="radio" value="C2" />
* </fieldset></form>
*
* var v = $(':text').fieldValue();
* // if no values are entered into the text inputs
* v == ['','']
* // if values entered into the text inputs are 'foo' and 'bar'
* v == ['foo','bar']
*
* var v = $(':checkbox').fieldValue();
* // if neither checkbox is checked
* v === undefined
* // if both checkboxes are checked
* v == ['B1', 'B2']
*
* var v = $(':radio').fieldValue();
* // if neither radio is checked
* v === undefined
* // if first radio is checked
* v == ['C1']
*
* The successful argument controls whether or not the field element must be 'successful'
* (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls).
* The default value of the successful argument is true. If this value is false the value(s)
* for each element is returned.
*
* Note: This method *always* returns an array. If no valid value can be determined the
* array will be empty, otherwise it will contain one or more values.
*/
$.fn.fieldValue = function(successful) {
for (var val=[], i=0, max=this.length; i < max; i++) {
var el = this[i];
var v = $.fieldValue(el, successful);
if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length))
continue;
v.constructor == Array ? $.merge(val, v) : val.push(v);
}
return val;
};
/**
* Returns the value of the field element.
*/
$.fieldValue = function(el, successful) {
var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
if (typeof successful == 'undefined') successful = true;
if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
(t == 'checkbox' || t == 'radio') && !el.checked ||
(t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
tag == 'select' && el.selectedIndex == -1))
return null;
if (tag == 'select') {
var index = el.selectedIndex;
if (index < 0) return null;
var a = [], ops = el.options;
var one = (t == 'select-one');
var max = (one ? index+1 : ops.length);
for(var i=(one ? index : 0); i < max; i++) {
var op = ops[i];
if (op.selected) {
var v = op.value;
if (!v) // extra pain for IE...
v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value;
if (one) return v;
a.push(v);
}
}
return a;
}
return el.value;
};
/**
* Clears the form data. Takes the following actions on the form's input fields:
* - input text fields will have their 'value' property set to the empty string
* - select elements will have their 'selectedIndex' property set to -1
* - checkbox and radio inputs will have their 'checked' property set to false
* - inputs of type submit, button, reset, and hidden will *not* be effected
* - button elements will *not* be effected
*/
$.fn.clearForm = function() {
return this.each(function() {
$('input,select,textarea', this).clearFields();
});
};
/**
* Clears the selected form elements.
*/
$.fn.clearFields = $.fn.clearInputs = function() {
return this.each(function() {
var t = this.type, tag = this.tagName.toLowerCase();
if (t == 'text' || t == 'password' || tag == 'textarea')
this.value = '';
else if (t == 'checkbox' || t == 'radio')
this.checked = false;
else if (tag == 'select')
this.selectedIndex = -1;
});
};
/**
* Resets the form data. Causes all form elements to be reset to their original value.
*/
$.fn.resetForm = function() {
return this.each(function() {
// guard against an input with the name of 'reset'
// note that IE reports the reset function as an 'object'
if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType))
this.reset();
});
};
/**
* Enables or disables any matching elements.
*/
$.fn.enable = function(b) {
if (b == undefined) b = true;
return this.each(function() {
this.disabled = !b;
});
};
/**
* Checks/unchecks any matching checkboxes or radio buttons and
* selects/deselects and matching option elements.
*/
$.fn.selected = function(select) {
if (select == undefined) select = true;
return this.each(function() {
var t = this.type;
if (t == 'checkbox' || t == 'radio')
this.checked = select;
else if (this.tagName.toLowerCase() == 'option') {
var $sel = $(this).parent('select');
if (select && $sel[0] && $sel[0].type == 'select-one') {
// deselect all other options
$sel.find('option').selected(false);
}
this.selected = select;
}
});
};
// helper fn for console logging
// set $.fn.ajaxSubmit.debug to true to enable debug logging
function log() {
if ($.fn.ajaxSubmit.debug && window.console && window.console.log)
window.console.log('[jquery.form] ' + Array.prototype.join.call(arguments,''));
};
})(jQuery);

@ -0,0 +1,7 @@
/*
HeatColor, by Josh Nathanson
A plugin for jQuery
Complete documentation at http://www.jnathanson.com/blog/client/jquery/heatcolor/index.cfm
*/
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);return p}('p.V.10=9(j,k){4 l={M:9(){v p(8)},q:0,t:0,N:0.Z,F:\'E\',y:X};u(k){p.W(l,k)};4 m={K:9(a,c,d){4 e=(a-c)/(d-c);4 f=l.F==\'E\'?0.5*e+1.7*(1-e):e+0.2+5.5*(1-e);4 h=Y;4 i=2*6.A;4 x=f+e*i;x=l.F!=\'E\'?-x:x;4 r=8.w(6.z((6.D(x)+1)*h));4 g=8.w(6.z((6.D(x+6.A/2)+1)*h));4 b=8.w(6.z((6.D(x+6.A)+1)*h));v\'#\'+r+g+b},w:9(a){4 n=6.z(a+l.N*(U-a));4 s=n.T(S);s=s.C==1?\'0\'+s:s;v s},J:9(c){4 d=[];c.I(9(){d.R(j.B(p(8)))});d=d.Q(9(a,b){v a-b});l.t=!l.y?d[d.C-1]:d[0];l.q=!l.y?d[0]:d[d.C-1]}};u(!l.q&&!l.t)m.J(p(8));H u(l.y){4 o=l.q;l.q=l.t;l.t=o}p(8).I(9(){4 a=p(8);4 b=j.B(a);4 c=m.K(b,l.q,l.t);4 d=l.M.B(a);u(d[0].L==1)d.O("P-G",c);H u(d[0].L==3)d.O("G",c)});v(8)}',62,63,'||||var||Math||this|function||||||||||||||||jQuery|minval|||maxval|if|return|process||reverseOrder|floor|PI|apply|length|cos|roygbiv|colorStyle|color|else|each|setMaxAndMin|findcolor|nodeType|elementFunction|lightness|css|background|sort|push|16|toString|256|fn|extend|false|128|75|heatcolor'.split('|'),0,{}))

@ -0,0 +1,152 @@
/*!
* jQuery JavaScript Library v1.4.1
* http://jquery.com/
*
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* Includes Sizzle.js
* http://sizzlejs.com/
* Copyright 2010, The Dojo Foundation
* Released under the MIT, BSD, and GPL Licenses.
*
* Date: Mon Jan 25 19:43:33 2010 -0500
*/
(function(z,v){function la(){if(!c.isReady){try{r.documentElement.doScroll("left")}catch(a){setTimeout(la,1);return}c.ready()}}function Ma(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,i){var j=a.length;if(typeof b==="object"){for(var n in b)X(a,n,b[n],f,e,d);return a}if(d!==v){f=!i&&f&&c.isFunction(d);for(n=0;n<j;n++)e(a[n],b,f?d.call(a[n],n,e(a[n],b)):d,i);return a}return j?
e(a[0],b):null}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function ma(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function na(a){var b,d=[],f=[],e=arguments,i,j,n,o,m,s,x=c.extend({},c.data(this,"events").live);if(!(a.button&&a.type==="click")){for(o in x){j=x[o];if(j.live===a.type||j.altLive&&c.inArray(a.type,j.altLive)>-1){i=j.data;i.beforeFilter&&i.beforeFilter[a.type]&&!i.beforeFilter[a.type](a)||f.push(j.selector)}else delete x[o]}i=c(a.target).closest(f,
a.currentTarget);m=0;for(s=i.length;m<s;m++)for(o in x){j=x[o];n=i[m].elem;f=null;if(i[m].selector===j.selector){if(j.live==="mouseenter"||j.live==="mouseleave")f=c(a.relatedTarget).closest(j.selector)[0];if(!f||f!==n)d.push({elem:n,fn:j})}}m=0;for(s=d.length;m<s;m++){i=d[m];a.currentTarget=i.elem;a.data=i.fn.data;if(i.fn.apply(i.elem,e)===false){b=false;break}}return b}}function oa(a,b){return"live."+(a?a+".":"")+b.replace(/\./g,"`").replace(/ /g,"&")}function pa(a){return!a||!a.parentNode||a.parentNode.nodeType===
11}function qa(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var i in f)for(var j in f[i])c.event.add(this,i,f[i][j],f[i][j].data)}}})}function ra(a,b,d){var f,e,i;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&a[0].indexOf("<option")<0&&(c.support.checkClone||!sa.test(a[0]))){e=true;if(i=c.fragments[a[0]])if(i!==1)f=i}if(!f){b=b&&b[0]?b[0].ownerDocument||b[0]:r;f=b.createDocumentFragment();
c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=i?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(ta.concat.apply([],ta.slice(0,b)),function(){d[this]=a});return d}function ua(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Na=z.jQuery,Oa=z.$,r=z.document,S,Pa=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Qa=/^.[^:#\[\.,]*$/,Ra=/\S/,Sa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Ta=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,O=navigator.userAgent,
va=false,P=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,Q=Array.prototype.slice,wa=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(typeof a==="string")if((d=Pa.exec(a))&&(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:r;if(a=Ta.exec(a))if(c.isPlainObject(b)){a=[r.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=ra([d[1]],
[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}}else{if(b=r.getElementById(d[2])){if(b.id!==d[2])return S.find(a);this.length=1;this[0]=b}this.context=r;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=r;a=r.getElementsByTagName(a)}else return!b||b.jquery?(b||S).find(a):c(b).find(a);else if(c.isFunction(a))return S.ready(a);if(a.selector!==v){this.selector=a.selector;this.context=a.context}return c.isArray(a)?this.setArray(a):c.makeArray(a,
this)},selector:"",jquery:"1.4.1",length:0,size:function(){return this.length},toArray:function(){return Q.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){a=c(a||null);a.prevObject=this;a.context=this.context;if(b==="find")a.selector=this.selector+(this.selector?" ":"")+d;else if(b)a.selector=this.selector+"."+b+"("+d+")";return a},setArray:function(a){this.length=0;ba.apply(this,a);return this},each:function(a,b){return c.each(this,
a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(r,c);else P&&P.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(Q.apply(this,arguments),"slice",Q.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};
c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,i,j,n;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(i in e){j=a[i];n=e[i];if(a!==n)if(f&&n&&(c.isPlainObject(n)||c.isArray(n))){j=j&&(c.isPlainObject(j)||c.isArray(j))?j:c.isArray(n)?[]:{};a[i]=c.extend(f,j,n)}else if(n!==v)a[i]=n}return a};c.extend({noConflict:function(a){z.$=
Oa;if(a)z.jQuery=Na;return c},isReady:false,ready:function(){if(!c.isReady){if(!r.body)return setTimeout(c.ready,13);c.isReady=true;if(P){for(var a,b=0;a=P[b++];)a.call(r,c);P=null}c.fn.triggerHandler&&c(r).triggerHandler("ready")}},bindReady:function(){if(!va){va=true;if(r.readyState==="complete")return c.ready();if(r.addEventListener){r.addEventListener("DOMContentLoaded",L,false);z.addEventListener("load",c.ready,false)}else if(r.attachEvent){r.attachEvent("onreadystatechange",L);z.attachEvent("onload",
c.ready);var a=false;try{a=z.frameElement==null}catch(b){}r.documentElement.doScroll&&a&&la()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,"isPrototypeOf"))return false;var b;for(b in a);return b===v||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;
return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return z.JSON&&z.JSON.parse?z.JSON.parse(a):(new Function("return "+a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Ra.test(a)){var b=r.getElementsByTagName("head")[0]||
r.documentElement,d=r.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(r.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,i=a.length,j=i===v||c.isFunction(a);if(d)if(j)for(f in a){if(b.apply(a[f],d)===false)break}else for(;e<i;){if(b.apply(a[e++],d)===false)break}else if(j)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=
a[0];e<i&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Sa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==
v;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,i=a.length;e<i;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,i=0,j=a.length;i<j;i++){e=b(a[i],i,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=v}else if(b&&!c.isFunction(b)){d=b;b=v}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},
uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});O=c.uaMatch(O);if(O.browser){c.browser[O.browser]=true;c.browser.version=O.version}if(c.browser.webkit)c.browser.safari=true;if(wa)c.inArray=function(a,b){return wa.call(b,a)};S=c(r);if(r.addEventListener)L=function(){r.removeEventListener("DOMContentLoaded",
L,false);c.ready()};else if(r.attachEvent)L=function(){if(r.readyState==="complete"){r.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=r.documentElement,b=r.createElement("script"),d=r.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";var e=d.getElementsByTagName("*"),i=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!i)){c.support=
{leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(i.getAttribute("style")),hrefNormalized:i.getAttribute("href")==="/a",opacity:/^0.55$/.test(i.style.opacity),cssFloat:!!i.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:r.createElement("select").appendChild(r.createElement("option")).selected,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};
b.type="text/javascript";try{b.appendChild(r.createTextNode("window."+f+"=1;"))}catch(j){}a.insertBefore(b,a.firstChild);if(z[f]){c.support.scriptEval=true;delete z[f]}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function n(){c.support.noCloneEvent=false;d.detachEvent("onclick",n)});d.cloneNode(true).fireEvent("onclick")}d=r.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=r.createDocumentFragment();a.appendChild(d.firstChild);
c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var n=r.createElement("div");n.style.width=n.style.paddingLeft="1px";r.body.appendChild(n);c.boxModel=c.support.boxModel=n.offsetWidth===2;r.body.removeChild(n).style.display="none"});a=function(n){var o=r.createElement("div");n="on"+n;var m=n in o;if(!m){o.setAttribute(n,"return;");m=typeof o[n]==="function"}return m};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=i=null}})();c.props=
{"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ua=0,xa={},Va={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var f=a[G],e=c.cache;if(!b&&!f)return null;f||(f=++Ua);if(typeof b==="object"){a[G]=f;e=e[f]=c.extend(true,
{},b)}else e=e[f]?e[f]:typeof d==="undefined"?Va:(e[f]={});if(d!==v){a[G]=f;e[b]=d}return typeof b==="string"?e[b]:e}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==z?xa:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{try{delete a[G]}catch(i){a.removeAttribute&&a.removeAttribute(G)}delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,
a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===v){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===v&&this.length)f=c.data(this[0],a);return f===v&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);
return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===v)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||
a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var ya=/[\n\t]/g,ca=/\s+/,Wa=/\r/g,Xa=/href|src|style/,Ya=/(button|input)/i,Za=/(button|input|object|select|textarea)/i,$a=/^(a|area)$/i,za=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(o){var m=
c(this);m.addClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className)for(var i=" "+e.className+" ",j=0,n=b.length;j<n;j++){if(i.indexOf(" "+b[j]+" ")<0)e.className+=" "+b[j]}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(o){var m=c(this);m.removeClass(a.call(this,o,m.attr("class")))});if(a&&typeof a==="string"||a===v)for(var b=(a||"").split(ca),
d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var i=(" "+e.className+" ").replace(ya," "),j=0,n=b.length;j<n;j++)i=i.replace(" "+b[j]+" "," ");e.className=i.substring(1,i.length-1)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var i=c(this);i.toggleClass(a.call(this,e,i.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,i=0,j=c(this),n=b,o=
a.split(ca);e=o[i++];){n=f?n:!j.hasClass(e);j[n?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(ya," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===v){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||
{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var i=b?d:0;for(d=b?d+1:e.length;i<d;i++){var j=e[i];if(j.selected){a=c(j).val();if(b)return a;f.push(a)}}return f}if(za.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Wa,"")}return v}var n=c.isFunction(a);return this.each(function(o){var m=c(this),s=a;if(this.nodeType===1){if(n)s=a.call(this,o,m.val());
if(typeof s==="number")s+="";if(c.isArray(s)&&za.test(this.type))this.checked=c.inArray(m.val(),s)>=0;else if(c.nodeName(this,"select")){var x=c.makeArray(s);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),x)>=0});if(!x.length)this.selectedIndex=-1}else this.value=s}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return v;if(f&&b in c.attrFn)return c(a)[b](d);
f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==v;b=f&&c.props[b]||b;if(a.nodeType===1){var i=Xa.test(b);if(b in a&&f&&!i){if(e){b==="type"&&Ya.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:Za.test(a.nodeName)||$a.test(a.nodeName)&&a.href?0:v;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=
""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&i?a.getAttribute(b,2):a.getAttribute(b);return a===null?v:a}return c.style(a,b,d)}});var ab=function(a){return a.replace(/[^\w\s\.\|`]/g,function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==z&&!a.frameElement)a=z;if(!d.guid)d.guid=c.guid++;if(f!==v){d=c.proxy(d);d.data=f}var e=c.data(a,"events")||c.data(a,"events",{}),i=c.data(a,"handle"),j;if(!i){j=
function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(j.elem,arguments):v};i=c.data(a,"handle",j)}if(i){i.elem=a;b=b.split(/\s+/);for(var n,o=0;n=b[o++];){var m=n.split(".");n=m.shift();if(o>1){d=c.proxy(d);if(f!==v)d.data=f}d.type=m.slice(0).sort().join(".");var s=e[n],x=this.special[n]||{};if(!s){s=e[n]={};if(!x.setup||x.setup.call(a,f,m,d)===false)if(a.addEventListener)a.addEventListener(n,i,false);else a.attachEvent&&a.attachEvent("on"+n,i)}if(x.add)if((m=x.add.call(a,
d,f,m,s))&&c.isFunction(m)){m.guid=m.guid||d.guid;m.data=m.data||d.data;m.type=m.type||d.type;d=m}s[d.guid]=d;this.global[n]=true}a=null}}},global:{},remove:function(a,b,d){if(!(a.nodeType===3||a.nodeType===8)){var f=c.data(a,"events"),e,i,j;if(f){if(b===v||typeof b==="string"&&b.charAt(0)===".")for(i in f)this.remove(a,i+(b||""));else{if(b.type){d=b.handler;b=b.type}b=b.split(/\s+/);for(var n=0;i=b[n++];){var o=i.split(".");i=o.shift();var m=!o.length,s=c.map(o.slice(0).sort(),ab);s=new RegExp("(^|\\.)"+
s.join("\\.(?:.*\\.)?")+"(\\.|$)");var x=this.special[i]||{};if(f[i]){if(d){j=f[i][d.guid];delete f[i][d.guid]}else for(var A in f[i])if(m||s.test(f[i][A].type))delete f[i][A];x.remove&&x.remove.call(a,o,j);for(e in f[i])break;if(!e){if(!x.teardown||x.teardown.call(a,o)===false)if(a.removeEventListener)a.removeEventListener(i,c.data(a,"handle"),false);else a.detachEvent&&a.detachEvent("on"+i,c.data(a,"handle"));e=null;delete f[i]}}}}for(e in f)break;if(!e){if(A=c.data(a,"handle"))A.elem=null;c.removeData(a,
"events");c.removeData(a,"handle")}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();this.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return v;a.result=v;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,
b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(i){}if(!a.isPropagationStopped()&&f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){d=a.target;var j;if(!(c.nodeName(d,"a")&&e==="click")&&!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()])){try{if(d[e]){if(j=d["on"+e])d["on"+e]=null;this.triggered=true;d[e]()}}catch(n){}if(j)d["on"+e]=j;this.triggered=false}}},handle:function(a){var b,
d;a=arguments[0]=c.event.fix(a||z.event);a.currentTarget=this;d=a.type.split(".");a.type=d.shift();b=!d.length&&!a.exclusive;var f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)");d=(c.data(this,"events")||{})[a.type];for(var e in d){var i=d[e];if(b||f.test(i.type)){a.handler=i;a.data=i.data;i=i.apply(this,arguments);if(i!==v){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||r;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=r.documentElement;d=r.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==v)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a,b){c.extend(a,b||{});a.guid+=b.selector+b.live;b.liveProxy=a;c.event.add(this,b.live,na,b)},remove:function(a){if(a.length){var b=
0,d=new RegExp("(^|\\.)"+a[0]+"(\\.|$)");c.each(c.data(this,"events").live||{},function(){d.test(this.type)&&b++});b<1&&c.event.remove(this,a[0],na)}},special:{}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};
c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,isImmediatePropagationStopped:Y};var Aa=function(a){for(var b=
a.relatedTarget;b&&b!==this;)try{b=b.parentNode}catch(d){break}if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}},Ba=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ba:Aa,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ba:Aa)}}});if(!c.support.submitBubbles)c.event.special.submit={setup:function(a,b,d){if(this.nodeName.toLowerCase()!==
"form"){c.event.add(this,"click.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="submit"||i==="image")&&c(e).closest("form").length)return ma("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit."+d.guid,function(f){var e=f.target,i=e.type;if((i==="text"||i==="password")&&c(e).closest("form").length&&f.keyCode===13)return ma("submit",this,arguments)})}else return false},remove:function(a,b){c.event.remove(this,"click.specialSubmit"+(b?"."+b.guid:""));c.event.remove(this,
"keypress.specialSubmit"+(b?"."+b.guid:""))}};if(!c.support.changeBubbles){var da=/textarea|input|select/i;function Ca(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d}function ea(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Ca(d);if(a.type!=="focusout"||
d.type!=="radio")c.data(d,"_change_data",e);if(!(f===v||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}}c.event.special.change={filters:{focusout:ea,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return ea.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return ea.call(this,a)},beforeactivate:function(a){a=
a.target;a.nodeName.toLowerCase()==="input"&&a.type==="radio"&&c.data(a,"_change_data",Ca(a))}},setup:function(a,b,d){for(var f in T)c.event.add(this,f+".specialChange."+d.guid,T[f]);return da.test(this.nodeName)},remove:function(a,b){for(var d in T)c.event.remove(this,d+".specialChange"+(b?"."+b.guid:""),T[d]);return da.test(this.nodeName)}};var T=c.event.special.change.filters}r.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,
f)}c.event.special[b]={setup:function(){this.addEventListener(a,d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var i in d)this[b](i,f,d[i],e);return this}if(c.isFunction(f)){e=f;f=v}var j=b==="one"?c.proxy(e,function(n){c(this).unbind(n,j);return e.apply(this,arguments)}):e;return d==="unload"&&b!=="one"?this.one(d,f,e):this.each(function(){c.event.add(this,d,j,f)})}});c.fn.extend({unbind:function(a,
b){if(typeof a==="object"&&!a.preventDefault){for(var d in a)this.unbind(d,a[d]);return this}return this.each(function(){c.event.remove(this,a,b)})},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+
a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e){var i,j=0;if(c.isFunction(f)){e=f;f=v}for(d=(d||"").split(/\s+/);(i=d[j++])!=null;){i=i==="focus"?"focusin":i==="blur"?"focusout":i==="hover"?d.push("mouseleave")&&"mouseenter":i;b==="live"?c(this.context).bind(oa(i,this.selector),{data:f,selector:this.selector,
live:i},e):c(this.context).unbind(oa(i,this.selector),e?{guid:e.guid+this.selector+i}:null)}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});z.attachEvent&&!z.addEventListener&&z.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});
(function(){function a(g){for(var h="",k,l=0;g[l];l++){k=g[l];if(k.nodeType===3||k.nodeType===4)h+=k.nodeValue;else if(k.nodeType!==8)h+=a(k.childNodes)}return h}function b(g,h,k,l,q,p){q=0;for(var u=l.length;q<u;q++){var t=l[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===k){y=l[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=k;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}l[q]=y}}}function d(g,h,k,l,q,p){q=0;for(var u=l.length;q<u;q++){var t=l[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===
k){y=l[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=k;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(o.filter(h,[t]).length>0){y=t;break}}t=t[g]}l[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,i=Object.prototype.toString,j=false,n=true;[0,0].sort(function(){n=false;return 0});var o=function(g,h,k,l){k=k||[];var q=h=h||r;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||
typeof g!=="string")return k;for(var p=[],u,t,y,R,H=true,M=w(h),I=g;(f.exec(""),u=f.exec(I))!==null;){I=u[3];p.push(u[1]);if(u[2]){R=u[3];break}}if(p.length>1&&s.exec(g))if(p.length===2&&m.relative[p[0]])t=fa(p[0]+p[1],h);else for(t=m.relative[p[0]]?[h]:o(p.shift(),h);p.length;){g=p.shift();if(m.relative[g])g+=p.shift();t=fa(g,t)}else{if(!l&&p.length>1&&h.nodeType===9&&!M&&m.match.ID.test(p[0])&&!m.match.ID.test(p[p.length-1])){u=o.find(p.shift(),h,M);h=u.expr?o.filter(u.expr,u.set)[0]:u.set[0]}if(h){u=
l?{expr:p.pop(),set:A(l)}:o.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=u.expr?o.filter(u.expr,u.set):u.set;if(p.length>0)y=A(t);else H=false;for(;p.length;){var D=p.pop();u=D;if(m.relative[D])u=p.pop();else D="";if(u==null)u=h;m.relative[D](y,u,M)}}else y=[]}y||(y=t);y||o.error(D||g);if(i.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))k.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&
y[g].nodeType===1&&k.push(t[g]);else k.push.apply(k,y);else A(y,k);if(R){o(R,q,k,l);o.uniqueSort(k)}return k};o.uniqueSort=function(g){if(C){j=n;g.sort(C);if(j)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};o.matches=function(g,h){return o(g,null,null,h)};o.find=function(g,h,k){var l,q;if(!g)return[];for(var p=0,u=m.order.length;p<u;p++){var t=m.order[p];if(q=m.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");l=m.find[t](q,
h,k);if(l!=null){g=g.replace(m.match[t],"");break}}}}l||(l=h.getElementsByTagName("*"));return{set:l,expr:g}};o.filter=function(g,h,k,l){for(var q=g,p=[],u=h,t,y,R=h&&h[0]&&w(h[0]);g&&h.length;){for(var H in m.filter)if((t=m.leftMatch[H].exec(g))!=null&&t[2]){var M=m.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-1)!=="\\"){if(u===p)p=[];if(m.preFilter[H])if(t=m.preFilter[H](t,u,k,p,l,R)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=u[U])!=null;U++)if(D){I=M(D,t,U,u);var Da=
l^!!I;if(k&&I!=null)if(Da)y=true;else u[U]=false;else if(Da){p.push(D);y=true}}if(I!==v){k||(u=p);g=g.replace(m.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)o.error(g);else break;q=g}return u};o.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var m=o.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},relative:{"+":function(g,h){var k=typeof h==="string",l=k&&!/\W/.test(h);k=k&&!l;if(l)h=h.toLowerCase();l=0;for(var q=g.length,
p;l<q;l++)if(p=g[l]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[l]=k||p&&p.nodeName.toLowerCase()===h?p||false:p===h}k&&o.filter(h,g,true)},">":function(g,h){var k=typeof h==="string";if(k&&!/\W/.test(h)){h=h.toLowerCase();for(var l=0,q=g.length;l<q;l++){var p=g[l];if(p){k=p.parentNode;g[l]=k.nodeName.toLowerCase()===h?k:false}}}else{l=0;for(q=g.length;l<q;l++)if(p=g[l])g[l]=k?p.parentNode:p.parentNode===h;k&&o.filter(h,g,true)}},"":function(g,h,k){var l=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=
h=h.toLowerCase();q=b}q("parentNode",h,l,g,p,k)},"~":function(g,h,k){var l=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,l,g,p,k)}},find:{ID:function(g,h,k){if(typeof h.getElementById!=="undefined"&&!k)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var k=[];h=h.getElementsByName(g[1]);for(var l=0,q=h.length;l<q;l++)h[l].getAttribute("name")===g[1]&&k.push(h[l]);return k.length===0?null:k}},
TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,k,l,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var u;(u=h[p])!=null;p++)if(u)if(q^(u.className&&(" "+u.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))k||l.push(u);else if(k)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&
"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,k,l,q,p){h=g[1].replace(/\\/g,"");if(!p&&m.attrMap[h])g[1]=m.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,k,l,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=o(g[3],null,null,h);else{g=o.filter(g[3],h,k,true^q);k||l.push.apply(l,g);return false}else if(m.match.POS.test(g[0])||m.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);
return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,k){return!!o(k[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===
g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,h){return h===0},last:function(g,h,k,l){return h===l.length-1},even:function(g,h){return h%2===
0},odd:function(g,h){return h%2===1},lt:function(g,h,k){return h<k[3]-0},gt:function(g,h,k){return h>k[3]-0},nth:function(g,h,k){return k[3]-0===h},eq:function(g,h,k){return k[3]-0===h}},filter:{PSEUDO:function(g,h,k,l){var q=h[1],p=m.filters[q];if(p)return p(g,k,h,l);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=h[3];k=0;for(l=h.length;k<l;k++)if(h[k]===g)return false;return true}else o.error("Syntax error, unrecognized expression: "+
q)},CHILD:function(g,h){var k=h[1],l=g;switch(k){case "only":case "first":for(;l=l.previousSibling;)if(l.nodeType===1)return false;if(k==="first")return true;l=g;case "last":for(;l=l.nextSibling;)if(l.nodeType===1)return false;return true;case "nth":k=h[2];var q=h[3];if(k===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var u=0;for(l=p.firstChild;l;l=l.nextSibling)if(l.nodeType===1)l.nodeIndex=++u;p.sizcache=h}g=g.nodeIndex-q;return k===0?g===0:g%k===0&&g/k>=
0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var k=h[1];g=m.attrHandle[k]?m.attrHandle[k](g):g[k]!=null?g[k]:g.getAttribute(k);k=g+"";var l=h[2];h=h[4];return g==null?l==="!=":l==="="?k===h:l==="*="?k.indexOf(h)>=0:l==="~="?(" "+k+" ").indexOf(h)>=0:!h?k&&g!==false:l==="!="?k!==h:l==="^="?
k.indexOf(h)===0:l==="$="?k.substr(k.length-h.length)===h:l==="|="?k===h||k.substr(0,h.length+1)===h+"-":false},POS:function(g,h,k,l){var q=m.setFilters[h[2]];if(q)return q(g,k,h,l)}}},s=m.match.POS;for(var x in m.match){m.match[x]=new RegExp(m.match[x].source+/(?![^\[]*\])(?![^\(]*\))/.source);m.leftMatch[x]=new RegExp(/(^(?:.|\r|\n)*?)/.source+m.match[x].source.replace(/\\(\d+)/g,function(g,h){return"\\"+(h-0+1)}))}var A=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};
try{Array.prototype.slice.call(r.documentElement.childNodes,0)}catch(B){A=function(g,h){h=h||[];if(i.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var k=0,l=g.length;k<l;k++)h.push(g[k]);else for(k=0;g[k];k++)h.push(g[k]);return h}}var C;if(r.documentElement.compareDocumentPosition)C=function(g,h){if(!g.compareDocumentPosition||!h.compareDocumentPosition){if(g==h)j=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===
h?0:1;if(g===0)j=true;return g};else if("sourceIndex"in r.documentElement)C=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)j=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)j=true;return g};else if(r.createRange)C=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)j=true;return g.ownerDocument?-1:1}var k=g.ownerDocument.createRange(),l=h.ownerDocument.createRange();k.setStart(g,0);k.setEnd(g,0);l.setStart(h,0);l.setEnd(h,0);g=k.compareBoundaryPoints(Range.START_TO_END,
l);if(g===0)j=true;return g};(function(){var g=r.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var k=r.documentElement;k.insertBefore(g,k.firstChild);if(r.getElementById(h)){m.find.ID=function(l,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(l[1]))?q.id===l[1]||typeof q.getAttributeNode!=="undefined"&&q.getAttributeNode("id").nodeValue===l[1]?[q]:v:[]};m.filter.ID=function(l,q){var p=typeof l.getAttributeNode!=="undefined"&&l.getAttributeNode("id");
return l.nodeType===1&&p&&p.nodeValue===q}}k.removeChild(g);k=g=null})();(function(){var g=r.createElement("div");g.appendChild(r.createComment(""));if(g.getElementsByTagName("*").length>0)m.find.TAG=function(h,k){k=k.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var l=0;k[l];l++)k[l].nodeType===1&&h.push(k[l]);k=h}return k};g.innerHTML="<a href='#'></a>";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")m.attrHandle.href=function(h){return h.getAttribute("href",
2)};g=null})();r.querySelectorAll&&function(){var g=o,h=r.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){o=function(l,q,p,u){q=q||r;if(!u&&q.nodeType===9&&!w(q))try{return A(q.querySelectorAll(l),p)}catch(t){}return g(l,q,p,u)};for(var k in g)o[k]=g[k];h=null}}();(function(){var g=r.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===
0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){m.order.splice(1,0,"CLASS");m.find.CLASS=function(h,k,l){if(typeof k.getElementsByClassName!=="undefined"&&!l)return k.getElementsByClassName(h[1])};g=null}}})();var E=r.compareDocumentPosition?function(g,h){return g.compareDocumentPosition(h)&16}:function(g,h){return g!==h&&(g.contains?g.contains(h):true)},w=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},fa=function(g,h){var k=[],
l="",q;for(h=h.nodeType?[h]:h;q=m.match.PSEUDO.exec(g);){l+=q[0];g=g.replace(m.match.PSEUDO,"")}g=m.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)o(g,h[q],k);return o.filter(l,k)};c.find=o;c.expr=o.selectors;c.expr[":"]=c.expr.filters;c.unique=o.uniqueSort;c.getText=a;c.isXMLDoc=w;c.contains=E})();var bb=/Until$/,cb=/^(?:parents|prevUntil|prevAll)/,db=/,/;Q=Array.prototype.slice;var Ea=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,i){return!!b.call(e,i,e)===d});else if(b.nodeType)return c.grep(a,
function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Qa.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;c.find(a,this[f],b);if(f>0)for(var i=d;i<b.length;i++)for(var j=0;j<d;j++)if(b[j]===b[i]){b.splice(i--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=
0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ea(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ea(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,i={},j;if(f&&a.length){e=0;for(var n=a.length;e<n;e++){j=a[e];i[j]||(i[j]=c.expr.match.POS.test(j)?c(j,b||this.context):j)}for(;f&&f.ownerDocument&&f!==b;){for(j in i){e=i[j];if(e.jquery?e.index(f)>
-1:c(f).is(e)){d.push({selector:j,elem:f});delete i[j]}}f=f.parentNode}}return d}var o=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(m,s){for(;s&&s.ownerDocument&&s!==b;){if(o?o.index(s)>-1:c(s).is(a))return s;s=s.parentNode}return null})},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),
a);return this.pushStack(pa(a[0])||pa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},
nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);bb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):
e;if((this.length>1||db.test(f))&&cb.test(a))e=e.reverse();return this.pushStack(e,a,Q.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===v||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==
b&&d.push(a);return d}});var Fa=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ga=/(<([\w:]+)[^>]*?)\/>/g,eb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,Ha=/<([\w:]+)/,fb=/<tbody/i,gb=/<|&\w+;/,sa=/checked\s*(?:[^=]|=\s*.checked.)/i,Ia=function(a,b,d){return eb.test(d)?a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],
col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==v)return this.empty().append((this[0]&&this[0].ownerDocument||r).createTextNode(a));return c.getText(this)},
wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?
d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,
false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&
!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Fa,"").replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){qa(this,b);qa(this.find("*"),b.find("*"))}return b},html:function(a){if(a===v)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Fa,""):null;else if(typeof a==="string"&&!/<script/i.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(Ha.exec(a)||
["",""])[1].toLowerCase()]){a=a.replace(Ga,Ia);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var i=c(this),j=i.html();i.empty().append(function(){return a.call(this,e,j)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,
b,f))});else a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(s){return c.nodeName(s,"table")?s.getElementsByTagName("tbody")[0]||s.appendChild(s.ownerDocument.createElement("tbody")):s}var e,i,j=a[0],n=[];if(!c.support.checkClone&&arguments.length===3&&typeof j===
"string"&&sa.test(j))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(j))return this.each(function(s){var x=c(this);a[0]=j.call(this,s,b?x.html():v);x.domManip(a,b,d)});if(this[0]){e=a[0]&&a[0].parentNode&&a[0].parentNode.nodeType===11?{fragment:a[0].parentNode}:ra(a,this,n);if(i=e.fragment.firstChild){b=b&&c.nodeName(i,"tr");for(var o=0,m=this.length;o<m;o++)d.call(b?f(this[o],i):this[o],e.cacheable||this.length>1||o>0?e.fragment.cloneNode(true):e.fragment)}n&&c.each(n,
Ma)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);for(var e=0,i=d.length;e<i;e++){var j=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),j);f=f.concat(j)}return this.pushStack(f,a,d.selector)}});c.each({remove:function(a,b){if(!a||c.filter(a,[this]).length){if(!b&&this.nodeType===1){c.cleanData(this.getElementsByTagName("*"));c.cleanData([this])}this.parentNode&&
this.parentNode.removeChild(this)}},empty:function(){for(this.nodeType===1&&c.cleanData(this.getElementsByTagName("*"));this.firstChild;)this.removeChild(this.firstChild)}},function(a,b){c.fn[a]=function(){return this.each(b,arguments)}});c.extend({clean:function(a,b,d,f){b=b||r;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||r;var e=[];c.each(a,function(i,j){if(typeof j==="number")j+="";if(j){if(typeof j==="string"&&!gb.test(j))j=b.createTextNode(j);else if(typeof j===
"string"){j=j.replace(Ga,Ia);var n=(Ha.exec(j)||["",""])[1].toLowerCase(),o=F[n]||F._default,m=o[0];i=b.createElement("div");for(i.innerHTML=o[1]+j+o[2];m--;)i=i.lastChild;if(!c.support.tbody){m=fb.test(j);n=n==="table"&&!m?i.firstChild&&i.firstChild.childNodes:o[1]==="<table>"&&!m?i.childNodes:[];for(o=n.length-1;o>=0;--o)c.nodeName(n[o],"tbody")&&!n[o].childNodes.length&&n[o].parentNode.removeChild(n[o])}!c.support.leadingWhitespace&&V.test(j)&&i.insertBefore(b.createTextNode(V.exec(j)[0]),i.firstChild);
j=c.makeArray(i.childNodes)}if(j.nodeType)e.push(j);else e=c.merge(e,j)}});if(d)for(a=0;e[a];a++)if(f&&c.nodeName(e[a],"script")&&(!e[a].type||e[a].type.toLowerCase()==="text/javascript"))f.push(e[a].parentNode?e[a].parentNode.removeChild(e[a]):e[a]);else{e[a].nodeType===1&&e.splice.apply(e,[a+1,0].concat(c.makeArray(e[a].getElementsByTagName("script"))));d.appendChild(e[a])}return e},cleanData:function(a){for(var b=0,d;(d=a[b])!=null;b++){c.event.remove(d);c.removeData(d)}}});var hb=/z-?index|font-?weight|opacity|zoom|line-?height/i,
Ja=/alpha\([^)]*\)/,Ka=/opacity=([^)]*)/,ga=/float/i,ha=/-([a-z])/ig,ib=/([A-Z])/g,jb=/^-?\d+(?:px)?$/i,kb=/^-?\d/,lb={position:"absolute",visibility:"hidden",display:"block"},mb=["Left","Right"],nb=["Top","Bottom"],ob=r.defaultView&&r.defaultView.getComputedStyle,La=c.support.cssFloat?"cssFloat":"styleFloat",ia=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===v)return c.curCSS(d,f);if(typeof e==="number"&&!hb.test(f))e+="px";c.style(d,f,e)})};
c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return v;if((b==="width"||b==="height")&&parseFloat(d)<0)d=v;var f=a.style||a,e=d!==v;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=Ja.test(a)?a.replace(Ja,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Ka.exec(f.filter)[1])/100+"":""}if(ga.test(b))b=La;b=b.replace(ha,ia);if(e)f[b]=d;return f[b]},css:function(a,
b,d,f){if(b==="width"||b==="height"){var e,i=b==="width"?mb:nb;function j(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(i,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,"border"+this+"Width",true))||0})}a.offsetWidth!==0?j():c.swap(a,lb,j);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&
a.currentStyle){f=Ka.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ga.test(b))b=La;if(!d&&e&&e[b])f=e[b];else if(ob){if(ga.test(b))b="float";b=b.replace(ib,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ha,ia);f=a.currentStyle[b]||a.currentStyle[d];if(!jb.test(f)&&kb.test(f)){b=e.left;var i=a.runtimeStyle.left;a.runtimeStyle.left=
a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=i}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var pb=
J(),qb=/<script(.|\s)*?\/script>/gi,rb=/select|textarea/i,sb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ja=/\?/,tb=/(\?|&)_=.*?(&|$)/,ub=/^(\w+:)?\/\/([^\/?#]+)/,vb=/%20/g;c.fn.extend({_load:c.fn.load,load:function(a,b,d){if(typeof a!=="string")return this._load(a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=
c.param(b,c.ajaxSettings.traditional);f="POST"}var i=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(j,n){if(n==="success"||n==="notmodified")i.html(e?c("<div />").append(j.responseText.replace(qb,"")).find(e):j.responseText);d&&i.each(d,[j.responseText,n,j])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&
(this.checked||rb.test(this.nodeName)||sb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,
b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:z.XMLHttpRequest&&(z.location.protocol!=="file:"||!z.ActiveXObject)?function(){return new z.XMLHttpRequest}:
function(){try{return new z.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&e.success.call(o,n,j,w);e.global&&f("ajaxSuccess",[w,e])}function d(){e.complete&&e.complete.call(o,w,j);e.global&&f("ajaxComplete",[w,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}
function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),i,j,n,o=a&&a.context||e,m=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(m==="GET")N.test(e.url)||(e.url+=(ja.test(e.url)?"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||
N.test(e.url))){i=e.jsonpCallback||"jsonp"+pb++;if(e.data)e.data=(e.data+"").replace(N,"="+i+"$1");e.url=e.url.replace(N,"="+i+"$1");e.dataType="script";z[i]=z[i]||function(q){n=q;b();d();z[i]=v;try{delete z[i]}catch(p){}A&&A.removeChild(B)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===false&&m==="GET"){var s=J(),x=e.url.replace(tb,"$1_="+s+"$2");e.url=x+(x===e.url?(ja.test(e.url)?"&":"?")+"_="+s:"")}if(e.data&&m==="GET")e.url+=(ja.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&
c.event.trigger("ajaxStart");s=(s=ub.exec(e.url))&&(s[1]&&s[1]!==location.protocol||s[2]!==location.host);if(e.dataType==="script"&&m==="GET"&&s){var A=r.getElementsByTagName("head")[0]||r.documentElement,B=r.createElement("script");B.src=e.url;if(e.scriptCharset)B.charset=e.scriptCharset;if(!i){var C=false;B.onload=B.onreadystatechange=function(){if(!C&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){C=true;b();d();B.onload=B.onreadystatechange=null;A&&B.parentNode&&
A.removeChild(B)}}}A.insertBefore(B,A.firstChild);return v}var E=false,w=e.xhr();if(w){e.username?w.open(m,e.url,e.async,e.username,e.password):w.open(m,e.url,e.async);try{if(e.data||a&&a.contentType)w.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[e.url]);c.etag[e.url]&&w.setRequestHeader("If-None-Match",c.etag[e.url])}s||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",
e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(fa){}if(e.beforeSend&&e.beforeSend.call(o,w,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");w.abort();return false}e.global&&f("ajaxSend",[w,e]);var g=w.onreadystatechange=function(q){if(!w||w.readyState===0||q==="abort"){E||d();E=true;if(w)w.onreadystatechange=c.noop}else if(!E&&w&&(w.readyState===4||q==="timeout")){E=true;w.onreadystatechange=c.noop;j=q==="timeout"?"timeout":!c.httpSuccess(w)?
"error":e.ifModified&&c.httpNotModified(w,e.url)?"notmodified":"success";var p;if(j==="success")try{n=c.httpData(w,e.dataType,e)}catch(u){j="parsererror";p=u}if(j==="success"||j==="notmodified")i||b();else c.handleError(e,w,j,p);d();q==="timeout"&&w.abort();if(e.async)w=null}};try{var h=w.abort;w.abort=function(){w&&h.call(w);g("abort")}}catch(k){}e.async&&e.timeout>0&&setTimeout(function(){w&&!E&&g("timeout")},e.timeout);try{w.send(m==="POST"||m==="PUT"||m==="DELETE"?e.data:null)}catch(l){c.handleError(e,
w,null,l);d()}e.async||g();return w}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=
f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(j,n){if(c.isArray(n))c.each(n,
function(o,m){b?f(j,m):d(j+"["+(typeof m==="object"||c.isArray(m)?o:"")+"]",m)});else!b&&n!=null&&typeof n==="object"?c.each(n,function(o,m){d(j+"["+o+"]",m)}):f(j,n)}function f(j,n){n=c.isFunction(n)?n():n;e[e.length]=encodeURIComponent(j)+"="+encodeURIComponent(n)}var e=[];if(b===v)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var i in a)d(i,a[i]);return e.join("&").replace(vb,"+")}});var ka={},wb=/toggle|show|hide/,xb=/^([+-]=)?([\d+-.]+)(.*)$/,
W,ta=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(ka[d])f=ka[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();
ka[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&
c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var i=c.extend({},e),j,n=this.nodeType===1&&c(this).is(":hidden"),
o=this;for(j in a){var m=j.replace(ha,ia);if(j!==m){a[m]=a[j];delete a[j];j=m}if(a[j]==="hide"&&n||a[j]==="show"&&!n)return i.complete.call(this);if((j==="height"||j==="width")&&this.style){i.display=c.css(this,"display");i.overflow=this.style.overflow}if(c.isArray(a[j])){(i.specialEasing=i.specialEasing||{})[j]=a[j][1];a[j]=a[j][0]}}if(i.overflow!=null)this.style.overflow="hidden";i.curAnim=c.extend({},a);c.each(a,function(s,x){var A=new c.fx(o,i,s);if(wb.test(x))A[x==="toggle"?n?"show":"hide":x](a);
else{var B=xb.exec(x),C=A.cur(true)||0;if(B){x=parseFloat(B[2]);var E=B[3]||"px";if(E!=="px"){o.style[s]=(x||1)+E;C=(x||1)/A.cur(true)*C;o.style[s]=C+E}if(B[1])x=(B[1]==="-="?-1:1)*x+C;A.custom(C,x,E)}else A.custom(C,x,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",
1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration==="number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,
b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==
null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(i){return e.step(i)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop===
"width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=
this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=
c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=
null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in r.documentElement?function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),
f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=this[0];if(a)return this.each(function(s){c.offset.setOffset(this,a,s)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=
b,e=b.ownerDocument,i,j=e.documentElement,n=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var o=b.offsetTop,m=b.offsetLeft;(b=b.parentNode)&&b!==n&&b!==j;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;i=e?e.getComputedStyle(b,null):b.currentStyle;o-=b.scrollTop;m-=b.scrollLeft;if(b===d){o+=b.offsetTop;m+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){o+=parseFloat(i.borderTopWidth)||
0;m+=parseFloat(i.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&i.overflow!=="visible"){o+=parseFloat(i.borderTopWidth)||0;m+=parseFloat(i.borderLeftWidth)||0}f=i}if(f.position==="relative"||f.position==="static"){o+=n.offsetTop;m+=n.offsetLeft}if(c.offset.supportsFixedPosition&&f.position==="fixed"){o+=Math.max(j.scrollTop,n.scrollTop);m+=Math.max(j.scrollLeft,n.scrollLeft)}return{top:o,left:m}};c.offset={initialize:function(){var a=r.body,b=r.createElement("div"),
d,f,e,i=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";a.insertBefore(b,a.firstChild);
d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==i;a.removeChild(b);c.offset.initialize=c.noop},
bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),i=parseInt(c.curCSS(a,"top",true),10)||0,j=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,d,e);d={top:b.top-e.top+i,left:b.left-
e.left+j};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=
this.offsetParent||r.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],i;if(!e)return null;if(f!==v)return this.each(function(){if(i=ua(this))i.scrollTo(!a?f:c(i).scrollLeft(),a?f:c(i).scrollTop());else this[d]=f});else return(i=ua(e))?"pageXOffset"in i?i[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&i.document.documentElement[d]||i.document.body[d]:e[d]}});
c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(i){var j=c(this);j[d](f.call(this,i,j[d]()))});return"scrollTo"in e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||
e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===v?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});z.jQuery=z.$=c})(window);

@ -0,0 +1,28 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>SWFObject 2 static publishing example page</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
swfobject.registerObject("myId", "9.0.0", "expressInstall.swf");
</script>
</head>
<body>
<div>
<object id="myId" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="300" height="120">
<param name="movie" value="test.swf" />
<!--[if !IE]>-->
<object type="application/x-shockwave-flash" data="test.swf" width="300" height="120">
<!--<![endif]-->
<div>
<h1>Alternative content</h1>
<p><a href="http://www.adobe.com/go/getflashplayer"><img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" /></a></p>
</div>
<!--[if !IE]>-->
</object>
<!--<![endif]-->
</object>
</div>
</body>
</html>

@ -0,0 +1,17 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>SWFObject 2 dynamic publishing example page</title>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<script type="text/javascript" src="swfobject.js"></script>
<script type="text/javascript">
swfobject.embedSWF("test.swf", "myContent", "300", "120", "9.0.0", "expressInstall.swf");
</script>
</head>
<body>
<div id="myContent">
<h1>Alternative content</h1>
<p><a href="http://www.adobe.com/go/getflashplayer"><img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" /></a></p>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

@ -0,0 +1,708 @@
"""Unit tests for emencia.django.newsletter"""
from datetime import datetime
from datetime import timedelta
from tempfile import NamedTemporaryFile
from django.test import TestCase
from django.http import Http404
from django.db import IntegrityError
from django.core.files import File
from emencia.django.newsletter.mailer import Mailer
from emencia.django.newsletter.models import Link
from emencia.django.newsletter.models import Contact
from emencia.django.newsletter.models import MailingList
from emencia.django.newsletter.models import SMTPServer
from emencia.django.newsletter.models import Newsletter
from emencia.django.newsletter.models import Attachment
from emencia.django.newsletter.models import ContactMailingStatus
from emencia.django.newsletter.utils.tokens import tokenize
from emencia.django.newsletter.utils.tokens import untokenize
from emencia.django.newsletter.utils.statistics import get_newsletter_opening_statistics
from emencia.django.newsletter.utils.statistics import get_newsletter_on_site_opening_statistics
from emencia.django.newsletter.utils.statistics import get_newsletter_unsubscription_statistics
from emencia.django.newsletter.utils.statistics import get_newsletter_clicked_link_statistics
from emencia.django.newsletter.utils.statistics import get_newsletter_top_links
from emencia.django.newsletter.utils.statistics import get_newsletter_statistics
class FakeSMTP(object):
mails_sent = 0
def sendmail(self, *ka, **kw):
self.mails_sent += 1
return {}
def quit(*ka, **kw):
pass
class SMTPServerTestCase(TestCase):
"""Tests for the SMTPServer model"""
def setUp(self):
self.server = SMTPServer.objects.create(name='Test SMTP',
host='smtp.domain.com')
self.server_2 = SMTPServer.objects.create(name='Test SMTP 2',
host='smtp.domain2.com')
self.contact = Contact.objects.create(email='test@domain.com')
self.mailinglist = MailingList.objects.create(name='Test MailingList')
self.mailinglist.subscribers.add(self.contact)
self.newsletter = Newsletter.objects.create(title='Test Newsletter',
content='Test Newsletter Content',
mailing_list=self.mailinglist,
server=self.server, slug='test-nl')
self.newsletter_2 = Newsletter.objects.create(title='Test Newsletter 2',
content='Test Newsletter 2 Content',
mailing_list=self.mailinglist,
server=self.server, slug='test-nl-2')
self.newsletter_3 = Newsletter.objects.create(title='Test Newsletter 2',
content='Test Newsletter 2 Content',
mailing_list=self.mailinglist,
server=self.server_2, slug='test-nl-3')
def test_credits(self):
# Testing unlimited account
self.assertEquals(self.server.credits(), 10000)
# Testing default limit
self.server.mails_hour = 42
self.assertEquals(self.server.credits(), 42)
# Testing credits status, with multiple server case
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contact,
status=ContactMailingStatus.SENT)
self.assertEquals(self.server.credits(), 41)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contact,
status=ContactMailingStatus.SENT_TEST)
self.assertEquals(self.server.credits(), 40)
# Testing with a fake status
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contact,
status=ContactMailingStatus.ERROR)
self.assertEquals(self.server.credits(), 40)
# Testing with a second newsletter sharing the server
ContactMailingStatus.objects.create(newsletter=self.newsletter_2,
contact=self.contact,
status=ContactMailingStatus.SENT)
self.assertEquals(self.server.credits(), 39)
# Testing with a third newsletter with another server
ContactMailingStatus.objects.create(newsletter=self.newsletter_3,
contact=self.contact,
status=ContactMailingStatus.SENT)
self.assertEquals(self.server.credits(), 39)
def test_custom_headers(self):
self.assertEquals(self.server.custom_headers, {})
self.server.headers = 'key_1: val_1\r\nkey_2 : val_2'
self.assertEquals(len(self.server.custom_headers), 2)
class ContactTestCase(TestCase):
"""Tests for the Contact model"""
def setUp(self):
self.mailinglist_1 = MailingList.objects.create(name='Test MailingList')
self.mailinglist_2 = MailingList.objects.create(name='Test MailingList 2')
def test_unique(self):
Contact(email='test@domain.com').save()
self.assertRaises(IntegrityError, Contact(email='test@domain.com').save)
def test_mail_format(self):
contact = Contact(email='test@domain.com')
self.assertEquals(contact.mail_format(), 'test@domain.com')
contact = Contact(email='test@domain.com', first_name='Toto')
self.assertEquals(contact.mail_format(), 'test@domain.com')
contact = Contact(email='test@domain.com', first_name='Toto', last_name='Titi')
self.assertEquals(contact.mail_format(), 'Titi Toto <test@domain.com>')
def test_vcard_format(self):
contact = Contact(email='test@domain.com', first_name='Toto', last_name='Titi')
self.assertEquals(contact.vcard_format(), 'BEGIN:VCARD\r\nVERSION:3.0\r\n'\
'EMAIL;TYPE=INTERNET:test@domain.com\r\nFN:Toto Titi\r\n'\
'N:Titi;Toto;;;\r\nEND:VCARD\r\n')
def test_subscriptions(self):
contact = Contact.objects.create(email='test@domain.com')
self.assertEquals(len(contact.subscriptions()), 0)
self.mailinglist_1.subscribers.add(contact)
self.assertEquals(len(contact.subscriptions()), 1)
self.mailinglist_2.subscribers.add(contact)
self.assertEquals(len(contact.subscriptions()), 2)
def test_unsubscriptions(self):
contact = Contact.objects.create(email='test@domain.com')
self.assertEquals(len(contact.unsubscriptions()), 0)
self.mailinglist_1.unsubscribers.add(contact)
self.assertEquals(len(contact.unsubscriptions()), 1)
self.mailinglist_2.unsubscribers.add(contact)
self.assertEquals(len(contact.unsubscriptions()), 2)
class MailingListTestCase(TestCase):
"""Tests for the MailingList model"""
def setUp(self):
self.contact_1 = Contact.objects.create(email='test1@domain.com')
self.contact_2 = Contact.objects.create(email='test2@domain.com', valid=False)
self.contact_3 = Contact.objects.create(email='test3@domain.com', subscriber=False)
self.contact_4 = Contact.objects.create(email='test4@domain.com')
def test_subscribers_count(self):
mailinglist = MailingList(name='Test MailingList')
mailinglist.save()
self.assertEquals(mailinglist.subscribers_count(), 0)
mailinglist.subscribers.add(self.contact_1, self.contact_2, self.contact_3)
self.assertEquals(mailinglist.subscribers_count(), 3)
def test_unsubscribers_count(self):
mailinglist = MailingList.objects.create(name='Test MailingList')
self.assertEquals(mailinglist.unsubscribers_count(), 0)
mailinglist.unsubscribers.add(self.contact_1, self.contact_2, self.contact_3)
self.assertEquals(mailinglist.unsubscribers_count(), 3)
def test_expedition_set(self):
mailinglist = MailingList.objects.create(name='Test MailingList')
self.assertEquals(len(mailinglist.expedition_set()), 0)
mailinglist.subscribers.add(self.contact_1, self.contact_2, self.contact_3)
self.assertEquals(len(mailinglist.expedition_set()), 1)
mailinglist.subscribers.add(self.contact_4)
self.assertEquals(len(mailinglist.expedition_set()), 2)
mailinglist.unsubscribers.add(self.contact_4)
self.assertEquals(len(mailinglist.expedition_set()), 1)
class NewsletterTestCase(TestCase):
"""Tests for the Newsletter model"""
def setUp(self):
self.server = SMTPServer.objects.create(name='Test SMTP',
host='smtp.domain.com')
self.contact = Contact.objects.create(email='test@domain.com')
self.mailinglist = MailingList.objects.create(name='Test MailingList')
self.newsletter = Newsletter.objects.create(title='Test Newsletter',
content='Test Newsletter Content',
mailing_list=self.mailinglist,
server=self.server)
def test_mails_sent(self):
self.assertEquals(self.newsletter.mails_sent(), 0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contact,
status=ContactMailingStatus.SENT)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contact,
status=ContactMailingStatus.SENT_TEST)
self.assertEquals(self.newsletter.mails_sent(), 1)
class TokenizationTestCase(TestCase):
"""Tests for the tokenization process"""
def setUp(self):
self.contact = Contact.objects.create(email='test@domain.com')
def test_tokenize_untokenize(self):
uidb36, token = tokenize(self.contact)
self.assertEquals(untokenize(uidb36, token), self.contact)
self.assertRaises(Http404, untokenize, 'toto', token)
self.assertRaises(Http404, untokenize, uidb36, 'toto')
class MailerTestCase(TestCase):
"""Tests for the Mailer object"""
def setUp(self):
self.server = SMTPServer.objects.create(name='Test SMTP',
host='smtp.domain.com',
mails_hour=100)
self.contacts = [Contact.objects.create(email='test1@domain.com'),
Contact.objects.create(email='test2@domain.com'),
Contact.objects.create(email='test3@domain.com'),
Contact.objects.create(email='test4@domain.com')]
self.mailinglist = MailingList.objects.create(name='Test MailingList')
self.mailinglist.subscribers.add(*self.contacts)
self.newsletter = Newsletter.objects.create(title='Test Newsletter',
content='Test Newsletter Content',
slug='test-newsletter',
mailing_list=self.mailinglist,
server=self.server,
status=Newsletter.WAITING)
self.newsletter.test_contacts.add(*self.contacts[:2])
self.attachment = Attachment.objects.create(newsletter=self.newsletter,
title='Test attachment',
file_attachment=File(NamedTemporaryFile()))
def test_expedition_list(self):
mailer = Mailer(self.newsletter, test=True)
self.assertEquals(len(mailer.expedition_list), 2)
self.server.mails_hour = 1
self.assertEquals(len(mailer.expedition_list), 1)
self.server.mails_hour = 100
mailer = Mailer(self.newsletter)
self.assertEquals(len(mailer.expedition_list), 4)
self.server.mails_hour = 3
self.assertEquals(len(mailer.expedition_list), 3)
self.server.mails_hour = 100
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.SENT)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.SENT)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.SENT)
self.assertEquals(len(mailer.expedition_list), 2)
self.assertFalse(self.contacts[0] in mailer.expedition_list)
def test_can_send(self):
mailer = Mailer(self.newsletter)
self.assertTrue(mailer.can_send)
# Checks credits
self.server.mails_hour = 1
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.SENT)
mailer = Mailer(self.newsletter)
self.assertFalse(mailer.can_send)
self.server.mails_hour = 10
mailer = Mailer(self.newsletter)
self.assertTrue(mailer.can_send)
# Checks statut
self.newsletter.status = Newsletter.DRAFT
mailer = Mailer(self.newsletter)
self.assertFalse(mailer.can_send)
mailer = Mailer(self.newsletter, test=True)
self.assertTrue(mailer.can_send)
# Checks expedition time
self.newsletter.status = Newsletter.WAITING
self.newsletter.sending_date = datetime.now() + timedelta(hours=1)
mailer = Mailer(self.newsletter)
self.assertFalse(mailer.can_send)
self.newsletter.sending_date = datetime.now()
mailer = Mailer(self.newsletter)
self.assertTrue(mailer.can_send)
def test_run(self):
mailer = Mailer(self.newsletter)
mailer.smtp = FakeSMTP()
mailer.run()
self.assertEquals(mailer.smtp.mails_sent, 4)
self.assertEquals(ContactMailingStatus.objects.filter(
status=ContactMailingStatus.SENT, newsletter=self.newsletter).count(), 4)
mailer = Mailer(self.newsletter, test=True)
mailer.smtp = FakeSMTP()
mailer.run()
self.assertEquals(mailer.smtp.mails_sent, 2)
self.assertEquals(ContactMailingStatus.objects.filter(
status=ContactMailingStatus.SENT_TEST, newsletter=self.newsletter).count(), 2)
mailer.smtp = None
def test_update_newsletter_status(self):
mailer = Mailer(self.newsletter, test=True)
self.assertEquals(self.newsletter.status, Newsletter.WAITING)
mailer.update_newsletter_status()
self.assertEquals(self.newsletter.status, Newsletter.WAITING)
mailer = Mailer(self.newsletter)
self.assertEquals(self.newsletter.status, Newsletter.WAITING)
mailer.update_newsletter_status()
self.assertEquals(self.newsletter.status, Newsletter.SENDING)
for contact in self.contacts:
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=contact,
status=ContactMailingStatus.SENT)
mailer.update_newsletter_status()
self.assertEquals(self.newsletter.status, Newsletter.SENT)
def test_update_newsletter_status_advanced(self):
self.server.mails_hour = 2
self.server.save()
mailer = Mailer(self.newsletter)
mailer.smtp = FakeSMTP()
mailer.run()
self.assertEquals(mailer.smtp.mails_sent, 2)
self.assertEquals(ContactMailingStatus.objects.filter(
status=ContactMailingStatus.SENT, newsletter=self.newsletter).count(), 2)
self.assertEquals(self.newsletter.status, Newsletter.SENDING)
self.server.mails_hour = 0
self.server.save()
mailer = Mailer(self.newsletter)
mailer.smtp = FakeSMTP()
mailer.run()
self.assertEquals(mailer.smtp.mails_sent, 2)
self.assertEquals(ContactMailingStatus.objects.filter(
status=ContactMailingStatus.SENT, newsletter=self.newsletter).count(), 4)
self.assertEquals(self.newsletter.status, Newsletter.SENT)
def test_recipients_refused(self):
server = SMTPServer.objects.create(name='Local SMTP',
host='localhost')
contact = Contact.objects.create(email='thisisaninvalidemail')
self.newsletter.test_contacts.clear()
self.newsletter.test_contacts.add(contact)
self.newsletter.server = server
self.newsletter.save()
self.assertEquals(contact.valid, True)
self.assertEquals(ContactMailingStatus.objects.filter(
status=ContactMailingStatus.INVALID, newsletter=self.newsletter).count(), 0)
mailer = Mailer(self.newsletter, test=True)
mailer.run()
self.assertEquals(Contact.objects.get(email='thisisaninvalidemail').valid, False)
self.assertEquals(ContactMailingStatus.objects.filter(
status=ContactMailingStatus.INVALID, newsletter=self.newsletter).count(), 1)
class StatisticsTestCase(TestCase):
"""Tests for the statistics functions"""
def setUp(self):
self.server = SMTPServer.objects.create(name='Test SMTP',
host='smtp.domain.com')
self.contacts = [Contact.objects.create(email='test1@domain.com'),
Contact.objects.create(email='test2@domain.com'),
Contact.objects.create(email='test3@domain.com'),
Contact.objects.create(email='test4@domain.com')]
self.mailinglist = MailingList.objects.create(name='Test MailingList')
self.mailinglist.subscribers.add(*self.contacts)
self.newsletter = Newsletter.objects.create(title='Test Newsletter',
content='Test Newsletter Content',
mailing_list=self.mailinglist,
server=self.server,
status=Newsletter.SENT)
self.links = [Link.objects.create(title='link 1', url='htt://link.1'),
Link.objects.create(title='link 2', url='htt://link.2')]
for contact in self.contacts:
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=contact,
status=ContactMailingStatus.SENT)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.SENT_TEST)
self.recipients = len(self.contacts)
self.status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
def test_get_newsletter_opening_statistics(self):
stats = get_newsletter_opening_statistics(self.status, self.recipients)
self.assertEquals(stats['total_openings'], 0)
self.assertEquals(stats['unique_openings'], 0)
self.assertEquals(stats['double_openings'], 0)
self.assertEquals(stats['unique_openings_percent'], 0)
self.assertEquals(stats['unknow_openings'], 0)
self.assertEquals(stats['unknow_openings_percent'], 0)
self.assertEquals(stats['opening_average'], 0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
status=ContactMailingStatus.OPENED_ON_SITE)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
status=ContactMailingStatus.LINK_OPENED)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_opening_statistics(status, self.recipients)
self.assertEquals(stats['total_openings'], 4)
self.assertEquals(stats['unique_openings'], 3)
self.assertEquals(stats['double_openings'], 1)
self.assertEquals(stats['unique_openings_percent'], 75.0)
self.assertEquals(stats['unknow_openings'], 1)
self.assertEquals(stats['unknow_openings_percent'], 25.0)
self.assertEquals(stats['opening_average'], 1.3333333333333333)
self.assertEquals(stats['opening_deducted'], 0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[3],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[3],
status=ContactMailingStatus.LINK_OPENED)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_opening_statistics(status, self.recipients)
self.assertEquals(stats['total_openings'], 5)
self.assertEquals(stats['unique_openings'], 4)
self.assertEquals(stats['double_openings'], 1)
self.assertEquals(stats['unique_openings_percent'], 100.0)
self.assertEquals(stats['unknow_openings'], 0)
self.assertEquals(stats['unknow_openings_percent'], 0.0)
self.assertEquals(stats['opening_average'], 1.25)
self.assertEquals(stats['opening_deducted'], 1)
def test_get_newsletter_on_site_opening_statistics(self):
stats = get_newsletter_on_site_opening_statistics(self.status)
self.assertEquals(stats['total_on_site_openings'], 0)
self.assertEquals(stats['unique_on_site_openings'], 0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED_ON_SITE)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED_ON_SITE)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.OPENED_ON_SITE)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
status=ContactMailingStatus.OPENED_ON_SITE)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_on_site_opening_statistics(status)
self.assertEquals(stats['total_on_site_openings'], 4)
self.assertEquals(stats['unique_on_site_openings'], 3)
def test_get_newsletter_clicked_link_statistics(self):
stats = get_newsletter_clicked_link_statistics(self.status, self.recipients, 0)
self.assertEquals(stats['total_clicked_links'], 0)
self.assertEquals(stats['total_clicked_links_percent'], 0)
self.assertEquals(stats['double_clicked_links'], 0)
self.assertEquals(stats['double_clicked_links_percent'], 0.0)
self.assertEquals(stats['unique_clicked_links'], 0)
self.assertEquals(stats['unique_clicked_links_percent'], 0)
self.assertEquals(stats['clicked_links_by_openings'], 0.0)
self.assertEquals(stats['clicked_links_average'], 0.0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[1],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[1],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_clicked_link_statistics(status, self.recipients, 3)
self.assertEquals(stats['total_clicked_links'], 5)
self.assertEquals(stats['total_clicked_links_percent'], 125.0)
self.assertEquals(stats['double_clicked_links'], 2)
self.assertEquals(stats['double_clicked_links_percent'], 50.0)
self.assertEquals(stats['unique_clicked_links'], 3)
self.assertEquals(stats['unique_clicked_links_percent'], 75.0)
self.assertEquals(stats['clicked_links_by_openings'], 166.66666666666669)
self.assertEquals(stats['clicked_links_average'], 1.6666666666666667)
def test_get_newsletter_unsubscription_statistics(self):
stats = get_newsletter_unsubscription_statistics(self.status, self.recipients)
self.assertEquals(stats['total_unsubscriptions'], 0)
self.assertEquals(stats['total_unsubscriptions_percent'], 0.0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.UNSUBSCRIPTION)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.UNSUBSCRIPTION)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_unsubscription_statistics(status, self.recipients)
self.assertEquals(stats['total_unsubscriptions'], 2)
self.assertEquals(stats['total_unsubscriptions_percent'], 50.0)
def test_get_newsletter_unsubscription_statistics_fix_doublon(self):
stats = get_newsletter_unsubscription_statistics(self.status, self.recipients)
self.assertEquals(stats['total_unsubscriptions'], 0)
self.assertEquals(stats['total_unsubscriptions_percent'], 0.0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.UNSUBSCRIPTION)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.UNSUBSCRIPTION)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.UNSUBSCRIPTION)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_unsubscription_statistics(status, self.recipients)
self.assertEquals(stats['total_unsubscriptions'], 2)
self.assertEquals(stats['total_unsubscriptions_percent'], 50.0)
def test_get_newsletter_top_links(self):
stats = get_newsletter_top_links(self.status)
self.assertEquals(stats['top_links'], [])
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[1],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
status = ContactMailingStatus.objects.filter(newsletter=self.newsletter)
stats = get_newsletter_top_links(status)
self.assertEquals(len(stats['top_links']), 2)
self.assertEquals(stats['top_links'][0]['link'], self.links[0])
self.assertEquals(stats['top_links'][0]['total_clicks'], 4)
self.assertEquals(stats['top_links'][0]['unique_clicks'], 3)
self.assertEquals(stats['top_links'][1]['link'], self.links[1])
self.assertEquals(stats['top_links'][1]['total_clicks'], 1)
self.assertEquals(stats['top_links'][1]['unique_clicks'], 1)
def test_get_newsletter_statistics(self):
stats = get_newsletter_statistics(self.newsletter)
self.assertEquals(stats['clicked_links_average'], 0.0)
self.assertEquals(stats['clicked_links_by_openings'], 0.0)
self.assertEquals(stats['double_clicked_links'], 0)
self.assertEquals(stats['double_clicked_links_percent'], 00.0)
self.assertEquals(stats['double_openings'], 0)
self.assertEquals(stats['mails_sent'], 4)
self.assertEquals(stats['mails_to_send'], 4)
self.assertEquals(stats['opening_average'], 0)
self.assertEquals(stats['remaining_mails'], 0)
self.assertEquals(stats['tests_sent'], 1)
self.assertEquals(stats['top_links'], [])
self.assertEquals(stats['total_clicked_links'], 0)
self.assertEquals(stats['total_clicked_links_percent'], 0.0)
self.assertEquals(stats['total_on_site_openings'], 0)
self.assertEquals(stats['total_openings'], 0)
self.assertEquals(stats['total_unsubscriptions'], 0)
self.assertEquals(stats['total_unsubscriptions_percent'], 0.0)
self.assertEquals(stats['unique_clicked_links'], 0)
self.assertEquals(stats['unique_clicked_links_percent'], 0.0)
self.assertEquals(stats['unique_on_site_openings'], 0)
self.assertEquals(stats['unique_openings'], 0)
self.assertEquals(stats['unique_openings_percent'], 0)
self.assertEquals(stats['unknow_openings'], 0)
self.assertEquals(stats['unknow_openings_percent'], 0.0)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
status=ContactMailingStatus.OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED_ON_SITE)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
status=ContactMailingStatus.OPENED_ON_SITE)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[1],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
link=self.links[1],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[1],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[2],
link=self.links[0],
status=ContactMailingStatus.LINK_OPENED)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.UNSUBSCRIPTION)
stats = get_newsletter_statistics(self.newsletter)
self.assertEquals(stats['clicked_links_average'], 1.6666666666666667)
self.assertEquals(stats['clicked_links_by_openings'], 100.0)
self.assertEquals(stats['double_clicked_links'], 2)
self.assertEquals(stats['double_clicked_links_percent'], 50.0)
self.assertEquals(stats['double_openings'], 2)
self.assertEquals(stats['mails_sent'], 4)
self.assertEquals(stats['mails_to_send'], 4)
self.assertEquals(stats['opening_average'], 1.6666666666666667)
self.assertEquals(stats['remaining_mails'], 0)
self.assertEquals(stats['tests_sent'], 1)
self.assertEquals(stats['total_clicked_links'], 5)
self.assertEquals(stats['total_clicked_links_percent'], 125.0)
self.assertEquals(stats['total_on_site_openings'], 2)
self.assertEquals(stats['total_openings'], 5)
self.assertEquals(stats['total_unsubscriptions'], 1)
self.assertEquals(stats['total_unsubscriptions_percent'], 25.0)
self.assertEquals(stats['unique_clicked_links'], 3)
self.assertEquals(stats['unique_clicked_links_percent'], 75.0)
self.assertEquals(stats['unique_on_site_openings'], 2)
self.assertEquals(stats['unique_openings'], 3)
self.assertEquals(stats['unique_openings_percent'], 75)
self.assertEquals(stats['unknow_openings'], 1)
self.assertEquals(stats['unknow_openings_percent'], 25.0)
def test_get_newsletter_statistics_division_by_zero(self):
"""Try to have a ZeroDivisionError by unsubscribing all contacts,
and creating a ContactMailingStatus for more code coverage.
Bug : http://github.com/Fantomas42/emencia-django-newsletter/issues#issue/9"""
get_newsletter_statistics(self.newsletter)
self.mailinglist.unsubscribers.add(*self.contacts)
ContactMailingStatus.objects.create(newsletter=self.newsletter,
contact=self.contacts[0],
status=ContactMailingStatus.OPENED)
get_newsletter_statistics(self.newsletter)

@ -0,0 +1,16 @@
"""Settings for testing emencia.django.newsletter"""
SITE_ID = 1
USE_I18N = False
ROOT_URLCONF = 'emencia.django.newsletter.urls'
DATABASES = {'default': {'NAME': 'newsletter_tests.db',
'ENGINE': 'django.db.backends.sqlite3'}}
INSTALLED_APPS = ['django.contrib.contenttypes',
'django.contrib.sites',
'django.contrib.auth',
'tagging',
'emencia.django.newsletter']

@ -0,0 +1,11 @@
"""Default urls for the emencia.django.newsletter"""
from django.conf.urls.defaults import url
from django.conf.urls.defaults import include
from django.conf.urls.defaults import patterns
urlpatterns = patterns('',
url(r'^mailing/', include('emencia.django.newsletter.urls.mailing_list')),
url(r'^tracking/', include('emencia.django.newsletter.urls.tracking')),
url(r'^statistics/', include('emencia.django.newsletter.urls.statistics')),
url(r'^', include('emencia.django.newsletter.urls.newsletter')),
)

@ -0,0 +1,20 @@
"""Urls for the emencia.django.newsletter Mailing List"""
from django.conf.urls.defaults import url
from django.conf.urls.defaults import patterns
from emencia.django.newsletter.forms import MailingListSubscriptionForm
from emencia.django.newsletter.forms import AllMailingListSubscriptionForm
urlpatterns = patterns('emencia.django.newsletter.views.mailing_list',
url(r'^unsubscribe/(?P<slug>[-\w]+)/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
'view_mailinglist_unsubscribe',
name='newsletter_mailinglist_unsubscribe'),
url(r'^subscribe/(?P<mailing_list_id>\d+)/',
'view_mailinglist_subscribe',
{'form_class': MailingListSubscriptionForm},
name='newsletter_mailinglist_subscribe'),
url(r'^subscribe/',
'view_mailinglist_subscribe',
{'form_class': AllMailingListSubscriptionForm},
name='newsletter_mailinglist_subscribe_all'),
)

@ -0,0 +1,12 @@
"""Urls for the emencia.django.newsletter Newsletter"""
from django.conf.urls.defaults import url
from django.conf.urls.defaults import patterns
urlpatterns = patterns('emencia.django.newsletter.views.newsletter',
url(r'^preview/(?P<slug>[-\w]+)/$',
'view_newsletter_preview',
name='newsletter_newsletter_preview'),
url(r'^(?P<slug>[-\w]+)/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/$',
'view_newsletter_contact',
name='newsletter_newsletter_contact'),
)

@ -0,0 +1,18 @@
"""Urls for the emencia.django.newsletter statistics"""
from django.conf.urls.defaults import url
from django.conf.urls.defaults import patterns
urlpatterns = patterns('emencia.django.newsletter.views.statistics',
url(r'^(?P<slug>[-\w]+)/$',
'view_newsletter_statistics',
name='newsletter_newsletter_statistics'),
url(r'^report/(?P<slug>[-\w]+)/$',
'view_newsletter_report',
name='newsletter_newsletter_report'),
url(r'^charts/(?P<slug>[-\w]+)/$',
'view_newsletter_charts',
name='newsletter_newsletter_charts'),
url(r'^density/(?P<slug>[-\w]+)/$',
'view_newsletter_density',
name='newsletter_newsletter_density'),
)

@ -0,0 +1,15 @@
"""Urls for the emencia.django.newsletter Tracking"""
from django.conf.urls.defaults import url
from django.conf.urls.defaults import patterns
urlpatterns = patterns('emencia.django.newsletter.views.tracking',
url(r'^newsletter/(?P<slug>[-\w]+)/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)\.(?P<format>png|gif|jpg)$',
'view_newsletter_tracking',
name='newsletter_newsletter_tracking'),
url(r'^link/(?P<slug>[-\w]+)/(?P<uidb36>[0-9A-Za-z]+)-(?P<token>.+)/(?P<link_id>\d+)/$',
'view_newsletter_tracking_link',
name='newsletter_newsletter_tracking_link'),
url(r'^historic/(?P<slug>[-\w]+)/$',
'view_newsletter_historic',
name='newsletter_newsletter_historic'),
)

@ -1,4 +1,4 @@
"""ExcelResponse for newsletter"""
"""ExcelResponse for emencia.django.newsletter"""
# Based on http://www.djangosnippets.org/snippets/1151/
import datetime

@ -11,8 +11,8 @@ from django.utils.translation import ugettext_lazy as _
from tagging.models import Tag
from newsletter.models import Contact
from newsletter.models import MailingList
from emencia.django.newsletter.models import Contact
from emencia.django.newsletter.models import MailingList
COLUMNS = ['email', 'first_name', 'last_name', 'tags']

@ -2,8 +2,8 @@
from BeautifulSoup import BeautifulSoup
from django.core.urlresolvers import reverse
from newsletter.models import Link
from newsletter.settings import USE_PRETTIFY
from emencia.django.newsletter.models import Link
from emencia.django.newsletter.settings import USE_PRETTIFY
def body_insertion(content, insertion, end=False):

@ -1,4 +1,4 @@
"""Premailer for newsletter
"""Premailer for emencia.django.newsletter
Used for converting a page with CSS inline and links corrected.
Based on http://www.peterbe.com/plog/premailer.py"""
import re

@ -1,7 +1,7 @@
"""Statistics for newsletter"""
"""Statistics for emencia.django.newsletter"""
from django.db.models import Q
from newsletter.models import ContactMailingStatus as Status
from emencia.django.newsletter.models import ContactMailingStatus as Status
def smart_division(a, b):

@ -1,9 +1,9 @@
"""Tokens system for newsletter"""
"""Tokens system for emencia.django.newsletter"""
from django.conf import settings
from django.http import Http404
from django.utils.http import int_to_base36, base36_to_int
from newsletter.models import Contact
from emencia.django.newsletter.models import Contact
class ContactTokenGenerator(object):

@ -1,5 +1,5 @@
"""Utils for workgroups"""
from newsletter.models import WorkGroup
from emencia.django.newsletter.models import WorkGroup
def request_workgroups(request):

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save