Browse files

L10n support for emails, and a lot of refactoring to support it.

  • Loading branch information...
1 parent f29fe2e commit eccce5ad4c6ae45bd5b1384271a0e0093770d157 Alex Buchanan committed Aug 11, 2010
Showing with 3,376 additions and 379 deletions.
  1. +23 −14 README.rst
  2. +0 −3 admin.py
  3. +163 −0 apps/emailer/__init__.py
  4. +0 −26 apps/emailer/admin.py
  5. +0 −159 apps/emailer/base.py
  6. +0 −1 apps/emailer/fixtures/sync_emails.json
  7. +16 −27 apps/emailer/management/commands/sendmail.py
  8. +3 −100 apps/emailer/models.py
  9. +49 −1 apps/emailer/tests.py
  10. +8 −8 apps/nagios/views.py
  11. +42 −1 apps/subscriptions/fixtures/subscriptions.json
  12. +6 −1 apps/subscriptions/models.py
  13. +2 −2 apps/subscriptions/templates/admin/subscriptions/change_list.html
  14. +25 −14 apps/subscriptions/tests.py
  15. +5 −0 cron/prod
  16. 0 emails/__init__.py
  17. +17 −0 emails/home.py
  18. +6 −6 libs/custom_emailers/reminder.py
  19. +442 −0 libs/html2text.py
  20. BIN locale/ca/LC_MESSAGES/django.mo
  21. +122 −0 locale/ca/LC_MESSAGES/django.po
  22. BIN locale/cs/LC_MESSAGES/django.mo
  23. +155 −0 locale/cs/LC_MESSAGES/django.po
  24. BIN locale/de/LC_MESSAGES/django.mo
  25. +159 −0 locale/de/LC_MESSAGES/django.po
  26. BIN locale/en_US/LC_MESSAGES/django.mo
  27. +121 −0 locale/en_US/LC_MESSAGES/django.po
  28. BIN locale/es/LC_MESSAGES/django.mo
  29. +156 −0 locale/es/LC_MESSAGES/django.po
  30. BIN locale/fr/LC_MESSAGES/django.mo
  31. +159 −0 locale/fr/LC_MESSAGES/django.po
  32. BIN locale/it/LC_MESSAGES/django.mo
  33. +158 −0 locale/it/LC_MESSAGES/django.po
  34. BIN locale/ja/LC_MESSAGES/django.mo
  35. +153 −0 locale/ja/LC_MESSAGES/django.po
  36. BIN locale/ko/LC_MESSAGES/django.mo
  37. +142 −0 locale/ko/LC_MESSAGES/django.po
  38. BIN locale/nl/LC_MESSAGES/django.mo
  39. +158 −0 locale/nl/LC_MESSAGES/django.po
  40. BIN locale/pl/LC_MESSAGES/django.mo
  41. +156 −0 locale/pl/LC_MESSAGES/django.po
  42. BIN locale/pt_BR/LC_MESSAGES/django.mo
  43. +121 −0 locale/pt_BR/LC_MESSAGES/django.po
  44. BIN locale/ru/LC_MESSAGES/django.mo
  45. +160 −0 locale/ru/LC_MESSAGES/django.po
  46. BIN locale/si/LC_MESSAGES/django.mo
  47. +144 −0 locale/si/LC_MESSAGES/django.po
  48. BIN locale/tr/LC_MESSAGES/django.mo
  49. +154 −0 locale/tr/LC_MESSAGES/django.po
  50. BIN locale/zh_CN/LC_MESSAGES/django.mo
  51. +121 −0 locale/zh_CN/LC_MESSAGES/django.po
  52. BIN locale/zh_TW/LC_MESSAGES/django.mo
  53. +139 −0 locale/zh_TW/LC_MESSAGES/django.po
  54. +0 −5 migrations/03-mailchimp.sql
  55. +4 −0 migrations/05-migrate-recipients.sql
  56. +2 −0 requirements/dev.txt
  57. +3 −3 requirements/prod.txt
  58. +24 −5 settings.py
  59. +0 −3 settings_ex.py
  60. +50 −0 templates/emails/firefox-home-instructions-initial.html
  61. +7 −0 templates/emails/firefox-home-instructions-reminder.html
  62. +1 −0 templates/emails/test.html
View
37 README.rst
@@ -2,9 +2,7 @@
Basket
======
-A RESTful service for storing email addresses
-
-*(These docs are very much a work in progress)*
+Stores email list subscriptions, and can send emails to those lists.
Requirements
============
@@ -21,7 +19,6 @@ Get the code
::
git clone git@github.com:abuchanan/basket.git
- cd basket
Make a virtualenv
@@ -39,6 +36,10 @@ Install packages
pip install -r requirements/prod.txt -r requirements/compiled.txt
+For developers::
+
+ pip install -r requirements/dev.txt
+
Settings
--------
@@ -68,21 +69,29 @@ Production installs often have a few different requirements:
Collecting Emails
=================
-TBD
+Send a POST request to /subscriptions/subscribe/ with the following fields
+* email address
+* campaign ID
+* locale (optional, defaults to en-US)
+* active (optional, defaults to True)
+* source, i.e. source page URL (optional)
Sending Emails
==============
After collecting emails, you'll also want to send some. To do that, first set
your outgoing email settings appropriately in ``settings_local.py``.
-Then, create an email template through the admin interface. Define the
-plain-text email (required) as well as the HTML text (optional).
+Then, create an email. See ./emails/home.py for examples.
To send an email to a campaign, run::
- ./manage.py sendmail --template mytemplatename mycampaignname [other_campaignnames ...]
+ ./manage.py sendmail --email emails.package.email campaignname [other_campaignnames ...]
+
+For example, to send the Firefox Home instructions email, you'd run::
+
+ ./manage.py sendmail --email emails.home.Initial firefox-home-instructions
You can run this as a cron job, as no-one will receive the same email twice,
unless the ``--force`` option is set.
@@ -92,9 +101,9 @@ Advanced emailing
-----------------
If you require special logic for sending your email, you can subclass
-``emailer.base.BaseEmailer`` in a module of your choice (recommended:
-inside ``libs/custom_emailers``). Then, go to the admin panel and set the
-``emailer_class`` field accordingly for the applicable email template(s) (for
-example: ``custom_emailers.reminder.ReminderEmailer``). When you run the
-``sendmail`` command above, your Emailer will be used instead of the default
-one.
+``emailer.Emailer`` in a module of your choice (recommended:
+inside ``libs/custom_emailers``). Set the
+``emailer_class`` field accordingly for the applicable email (see emails.home.Reminder for an example).
+
+When you run the ``sendmail`` command above, your Emailer will be used instead
+of the default one.
View
3 admin.py
@@ -4,8 +4,6 @@
from piston.models import Consumer
from basketauth.admin import ConsumerAdmin
-from emailer.admin import EmailAdmin
-from emailer.models import Email
from subscriptions.admin import SubscriptionAdmin
from subscriptions.models import Subscription
@@ -18,4 +16,3 @@ class BasketAdmin(admin.sites.AdminSite):
site.register(User, UserAdmin)
site.register(Consumer, ConsumerAdmin)
site.register(Subscription, SubscriptionAdmin)
-site.register(Email, EmailAdmin)
View
163 apps/emailer/__init__.py
@@ -0,0 +1,163 @@
+from email import Charset
+
+from django.conf import settings
+from django.core import mail
+from django.core.exceptions import ValidationError
+from django.core.urlresolvers import get_callable
+from django.utils import translation
+from django.utils.encoding import force_unicode
+
+import commonware.log
+import jingo
+import tower
+
+from emailer.models import Recipient
+from html2text import html2text
+from subscriptions.models import Subscription
+
+
+log = commonware.log.getLogger('basket')
+
+charsets = {
+ 'ja': 'ISO-2022-JP',
+ 'it': 'ISO-8859-1',
+ 'de': 'ISO-8859-15',
+ 'fr': 'ISO-8859-15',
+ 'zh-CN': 'GB18030',
+ 'ko': 'EUC-KR',
+ 'cs': 'ISO-8859-2',
+ 'tr': 'ISO-8859-9',
+}
+for c in charsets.values():
+ Charset.add_charset(c)
+
+
+class Email(object):
+
+ id = 'email-id'
+ subject = 'subject'
+ lang = settings.LANGUAGE_CODE
+ encoding = settings.DEFAULT_CHARSET
+ from_email = settings.DEFAULT_FROM_EMAIL
+ from_name = settings.DEFAULT_FROM_NAME
+ reply_email = settings.DEFAULT_FROM_EMAIL
+ emailer_class = 'emailer.Emailer'
+ template = 'test'
+
+ @classmethod
+ def get(cls, name):
+ email = get_callable(name)
+ return email()
+
+ @property
+ def html(self):
+ path = 'emails/{0}.html'.format(self.template)
+ return jingo.env.get_template(path).render({'lang': self.lang})
+
+ @property
+ def text(self):
+ return html2text(self.html)
+
+ def _activate_lang(self):
+ tower.activate(self.lang)
+ lang = translation.get_language()
+ if lang in charsets:
+ self.encoding = charsets[lang]
+ elif lang[:2] in charsets:
+ self.encoding = charsets[lang[:2]]
+ else:
+ self.encoding = settings.DEFAULT_CHARSET
+
+ def emailer(self, campaign, email, force=False):
+ emailer_class = get_callable(self.emailer_class)
+ return emailer_class(campaign, email, force)
+
+ def message(self, address):
+ self._activate_lang()
+
+ d = {
+ 'subject': force_unicode(self.subject),
+ 'from_email': u'{0} <{1}>'.format(self.from_name, self.from_email),
+ 'body': self.text,
+ 'headers': {
+ 'Reply-To': self.reply_email,
+ 'X-Mailer': 'Basket Emailer %s' % (
+ '.'.join(map(str, settings.VERSION)))}
+ }
+
+ msg = mail.EmailMultiAlternatives(to=(address,), **d)
+ msg.encoding = self.encoding
+ msg.attach_alternative(self.html, 'text/html')
+ return msg
+
+
+class Emailer(object):
+ """
+ Base Emailer class.
+
+ Given a template and a campaign, emails all active subscribers to that
+ campaign that haven't received that email yet.
+
+ Subclass and override to change behavior, such as excluding subscriptions
+ based on complex criteria. For an example, check out
+ lib/custom_emailers/*.py.
+ """
+ def __init__(self, campaign, email, force=False):
+ """Initialize emailer with campaign name and email model instance."""
+ self.campaign = campaign
+ self.email = email
+ self.force = force
+
+ def get_subscriptions(self):
+ """
+ Return all subscribers to the chosen campaign that are active and have
+ not yet received this email.
+ """
+ subscriptions = Subscription.objects.filter(
+ campaign=self.campaign, active=True)
+ if not self.force:
+ subscriptions = subscriptions.exclude(
+ subscriber__received__email_id=self.email.id)
+ return subscriptions
+
+ def send_email(self):
+ """Send out the email and record the subscriptions."""
+
+ subscriptions = self.get_subscriptions()
+ if not subscriptions:
+ log.info('Nothing to do: List of subscriptions is empty.')
+ return
+
+ emails = dict((s.subscriber.email, s) for s in subscriptions)
+
+ messages = []
+ for (address, subscription) in emails.items():
+ self.email.lang = subscription.locale
+ msg = self.email.message(address)
+ messages.append(msg)
+
+ log.info('Establishing SMTP connection...')
+ connection = mail.get_connection()
+ connection.open()
+
+ # We don't want to silence connection errors, but now we want to see
+ # (success, failed) from send_messages).
+ connection.fail_silently = True
+ success, failed = connection.send_messages(messages)
+
+ log.info('%d failed messages' % len(failed))
+ log.debug([x.to for x in failed])
+ log.info('%d successful messages' % len(success))
+
+ for msg in success:
+ dest = msg.to[0]
+ sent = Recipient(subscriber_id=emails[dest].id, email_id=self.email.id)
+ try:
+ sent.validate_unique()
+ except ValidationError, e:
+ # Already exists? Sending was probably forced.
+ pass
+ else:
+ sent.save()
+
+ connection.close()
View
26 apps/emailer/admin.py
@@ -1,26 +0,0 @@
-from django.contrib import admin
-
-from .models import Email
-
-
-class EmailAdmin(admin.ModelAdmin):
- list_display = ('name', 'short_subject', 'short_text', 'recipient_count')
- exclude = ('mailchimp_campaign',)
- ordering = ('name',)
-
- def short_subject(self, obj):
- short = obj.subject[:80]
- if len(obj.subject) > 80:
- short += '...'
- return short
- short_subject.short_description = 'Subject'
-
- def short_text(self, obj):
- short = obj.text[:80]
- if len(obj.text) > 80:
- short += '...'
- return short
- short_text.short_description = 'Text'
-
- def recipient_count(self, obj):
- return obj.recipients.count()
View
159 apps/emailer/base.py
@@ -1,159 +0,0 @@
-import commonware.log
-
-from django.conf import settings
-from django.core.exceptions import ValidationError
-from django.core import mail
-
-from greatape import MailChimp
-
-from emailer.models import Recipient
-from subscriptions.models import Subscriber
-
-
-log = commonware.log.getLogger('basket')
-
-mailchimp = MailChimp(settings.MAILCHIMP_API_KEY)
-
-
-class BaseEmailer(object):
- """
- Base Emailer class.
-
- Given a template and a campaign, emails all active subscribers to that
- campaign that haven't received that email yet.
-
- Subclass and override to change behavior, such as excluding recipients
- based on complex criteria. For an example, check out
- lib/custom_emailers/*.py.
- """
- def __init__(self, campaign, email, force=False):
- """Initialize emailer with campaign name and email model instance."""
- self.email = email
- self.campaign = campaign
- self.force = force
-
- def get_subject(self):
- """Return the email subject."""
- return self.email.subject
-
- def get_text(self):
- """Return the plain text email message."""
- return self.email.text
-
- def get_html(self):
- """Return the HTML text of the email message."""
- return self.email.html
-
- def get_from(self):
- """Return "from" email address."""
- if self.email.from_email:
- return '{name} <{email}>'.format(name=self.email.from_name,
- email=self.email.from_email)
- else:
- return settings.DEFAULT_FROM_EMAIL
-
- def get_recipients(self):
- """
- Return all subscribers to the chosen campaign that are active and have
- not yet received this email.
- """
- recipients = Subscriber.objects.filter(
- subscriptions__campaign=self.campaign, subscriptions__active=True)
- if not self.force:
- recipients = recipients.exclude(received=self.email)
- return recipients
-
- def get_headers(self):
- """Return additional headers."""
- return {
- 'Reply-To': self.email.reply_to_email or settings.DEFAULT_FROM_EMAIL,
- 'X-Mailer': 'Basket Emailer %s' % (
- '.'.join(map(str, settings.VERSION)))}
-
- def send_email(self):
- """Send out the email and record the recipients."""
- recipients = self.get_recipients()
- if not recipients:
- log.info('Nothing to do: List of recipients is empty.')
- return
-
-
- emails = dict((r.email, r.id) for r in recipients)
-
- d = {
- 'subject': self.get_subject(),
- 'body': self.get_text(),
- 'from_email': self.get_from(),
- 'headers': self.get_headers(),
- }
- html = self.get_html()
-
- messages = []
- for address in emails:
- msg = mail.EmailMultiAlternatives(to=(address,), **d)
- msg.attach_alternative(html, 'text/html')
- messages.append(msg)
-
- log.info('Establishing SMTP connection...')
- connection = mail.get_connection()
- connection.open()
-
- # We don't want to silence connection errors, but now we want to see
- # (success, failed) from send_messages).
- connection.fail_silently = True
- success, failed = connection.send_messages(messages)
-
- log.info('%d failed messages' % len(failed))
- log.info('%d successful messages' % len(success))
-
- for msg in success:
- dest = msg.to[0]
- sent = Recipient(subscriber_id=emails[dest], email=self.email)
- try:
- sent.validate_unique()
- except ValidationError, e:
- # Already exists? Sending was probably forced.
- pass
- else:
- sent.save()
-
- connection.close()
-
-
-class MailChimpEmailer(BaseEmailer):
- """
- Send email using MailChimp lists and transactional campaigns
- """
-
- def send_email(self):
- """Send out the email and record the recipients."""
- recipients = self.get_recipients()
- if not recipients:
- log.info('Nothing to do: List of recipients is empty.')
- return
-
- # MailChimp recommends max batch size of 10K
- recipients = recipients[0:10000]
-
- batch = [dict(EMAIL=x.email, EMAIL_TYPE='html') for x in recipients]
-
- ret = mailchimp.listBatchSubscribe(id=self.email.mailchimp_list,
- batch=batch, double_optin=False)
-
- failed = [x['row']['EMAIL'] for x in ret['errors']]
-
- for recipient in recipients:
- if recipient.email in failed:
- log.error('Failed to subscribe %s' % recipient.email)
- else:
- log.info('Subscribed %s' % recipient.email)
- sent = Recipient(subscriber=recipient, email=self.email)
- try:
- sent.validate_unique()
- except ValidationError:
- # Already exists? Sending was probably forced.
- pass
- else:
- sent.save()
-
- mailchimp.campaignSendNow(cid=self.email.mailchimp_campaign)
View
1 apps/emailer/fixtures/sync_emails.json
@@ -1 +0,0 @@
-[{"pk": 1, "model": "emailer.email", "fields": {"mailchimp_campaign": "", "name": "iphone-reg", "text": "Greetings!\r\n\r\nFollow these four simple steps to set up Firefox Home and start sharing your Firefox history, bookmarks and even your open tabs between your computer and your iPhone or iPod Touch:\r\n\r\n1. Install the free Firefox Sync add-on on your desktop: <http://www.mozilla.com/en-US/firefox/sync/>\r\n (Firefox Home uses the sync capabilities from the Firefox Sync add-on to access your Firefox browser information and securely send it to your iPhone.)\r\n\r\n2. Restart Firefox and follow prompts to create an account with both a password and a Secret Phrase.\r\n\r\n3. Sign in, then go to your iPhone to complete the setup.\r\n\r\n4. On your iPhone, enter your new account log-in info in the fields provided and tap \"Done\" on the keypad.\r\n (Already using Firefox Sync on your desktop? Go to iTunes and install the Firefox Home app from the App Store on your iPhone.)\r\n\r\nYou're done! After these steps, you can take your favorite parts of the Web with you on your iPhone.\r\n\r\nIf you do not setup Firefox Home within one week of receiving this email, one reminder email will be sent to you.\r\n\r\nSee our Privacy Policy: <http://www.mozilla.com/en-US/privacy-policy.html>\r\nVisit us for more information about Firefox Home: <http://www.mozilla.com/en-US/mobile/home/>", "html": "<html lang=\"en-US\">\r\n<body>\r\n<p>Greetings!</p>\r\n\r\n<p>Follow these four simple steps to set up <strong>Firefox Home</strong> and start sharing your Firefox history, bookmarks and even your open tabs between your computer and your iPhone or iPod Touch:</p>\r\n\r\n<ol>\r\n<li>\r\n<p style=\"font-weight:bold\">Install the <a href=\"http://www.mozilla.com/en-US/firefox/sync/\">free Firefox Sync add-on</a> on your desktop.</p>\r\n<p style=\"font-size:90%;color:#807970\">Firefox Home uses the sync capabilities from the Firefox Sync add-on to access your Firefox browser information and securely send it to your iPhone.</p>\r\n</li>\r\n\r\n<li><p style=\"font-weight:bold\">Restart Firefox and follow prompts to create an account with both a password and a Secret Phrase.</p></li>\r\n\r\n<li><p style=\"font-weight:bold\">Sign in, then go to your iPhone to complete the setup.</p></li>\r\n\r\n<li>\r\n<p style=\"font-weight:bold\">On your iPhone, enter your new account log-in info in the fields provided and tap \"Done\" on the keypad.</p>\r\n<p style=\"font-size:90%;color:#807970\">Already using Firefox Sync on your desktop? Go to iTunes and install the Firefox Home app from the App Store on your iPhone.</p>\r\n</li>\r\n</ol>\r\n\r\n<p>You're done! After these steps, you can take your favorite parts of the Web with you on your iPhone.</p>\r\n\r\n<p>If you do not set up Firefox Home within one week of receiving this email, one reminder email will be sent to you.</p>\r\n\r\n<p>See our <a href=\"http://www.mozilla.com/en-US/privacy-policy.html\">Privacy Policy</a>. Visit us for more information about <a href=\"http://www.mozilla.com/en-US/mobile/home/\">Firefox Home</a>.</p>\r\n</body>\r\n</html>", "emailer_class": "", "mailchimp_list": "", "subject": "Set Up Firefox Home On Your iPhone"}}, {"pk": 2, "model": "emailer.email", "fields": {"mailchimp_campaign": "", "name": "iphone-reminder", "text": "Greetings!\r\n\r\nA few days ago you expressed interest in setting up Firefox Home on your iPhone.\r\n\r\nFollow these four simple steps to set up Firefox Home and start sharing your Firefox history, bookmarks and even your open tabs between your computer and your iPhone or iPod Touch:\r\n\r\n1. Install the free Firefox Sync add-on on your desktop: <http://www.mozilla.com/en-US/firefox/sync/>\r\n (Firefox Home uses the sync capabilities from the Firefox Sync add-on to access your Firefox browser information and securely send it to your iPhone.)\r\n\r\n2. Restart Firefox and follow prompts to create an account with both a password and a Secret Phrase.\r\n\r\n3. Sign in, then go to your iPhone to complete the setup.\r\n\r\n4. On your iPhone, enter your new account log-in info in the fields provided and tap \"Done\" on the keypad.\r\n (Already using Firefox Sync on your desktop? Go to iTunes and install the Firefox Home app from the App Store on your iPhone.)\r\n\r\nYou're done! After these steps, you can take your favorite parts of the Web with you on your iPhone.\r\n\r\nWe will not be sending you any further reminder emails.\r\n\r\nVisit us for more information about Firefox Home: <http://www.mozilla.com/en-US/mobile/home/>", "html": "<html lang=\"en-US\">\r\n<body>\r\n<p>Greetings!</p>\r\n\r\n<p>A few days ago you expressed interest in setting up Firefox Home on your iPhone.</p>\r\n\r\n<p>Follow these four simple steps to set up <strong>Firefox Home</strong> and start sharing your Firefox history, bookmarks and even your open tabs between your computer and your iPhone or iPod Touch:</p>\r\n\r\n<ol>\r\n<li>\r\n<p style=\"font-weight:bold\">Install the <a href=\"http://www.mozilla.com/en-US/firefox/sync/\">free Firefox Sync add-on</a> on your desktop.</p>\r\n<p style=\"font-size:90%;color:#807970\">Firefox Home uses the sync capabilities from the Firefox Sync add-on to access your Firefox browser information and securely send it to your iPhone.</p>\r\n</li>\r\n\r\n<li><p style=\"font-weight:bold\">Restart Firefox and follow prompts to create an account with both a password and a Secret Phrase.</p></li>\r\n\r\n<li><p style=\"font-weight:bold\">Sign in, then go to your iPhone to complete the setup.</p></li>\r\n\r\n<li>\r\n<p style=\"font-weight:bold\">On your iPhone, enter your new account log-in info in the fields provided and tap \"Done\" on the keypad.</p>\r\n<p style=\"font-size:90%;color:#807970\">Already using Firefox Sync on your desktop? Go to iTunes and install the Firefox Home app from the App Store on your iPhone.</p>\r\n</li>\r\n</ol>\r\n\r\n<p>You're done! After these steps, you can take your favorite parts of the Web with you on your iPhone.</p>\r\n\r\n<p>We will not be sending you any further reminder emails.</p>\r\n\r\n<p>Visit us for more information about <a href=\"http://www.mozilla.com/en-US/mobile/home/\">Firefox Home</a>.</p>\r\n</body>\r\n</html>", "emailer_class": "custom_emailers.reminder.ReminderEmailer", "mailchimp_list": "aa3479dc85", "subject": "Reminder to Set Up Firefox Home On Your iPhone"}}]
View
43 apps/emailer/management/commands/sendmail.py
@@ -2,7 +2,7 @@
from django.core.management.base import LabelCommand, CommandError
-from emailer.models import Email
+from emailer import Email
from utils import locked
@@ -11,8 +11,8 @@ class Command(LabelCommand):
make_option('--force', '-f', dest='force', action='store_true',
default=False,
help='Send email even to prior recipients.'),
- make_option('--template', '-t', dest='template',
- help='Template name of email to be sent (required).'),
+ make_option('--email', '-e', dest='email',
+ help='Name of email to be sent (required).'),
)
help = 'Send an email to the subscribers to a campaign.'
args = '<campaign campaign ...>'
@@ -24,30 +24,19 @@ def handle_label(self, label, **options):
Locked command handler to avoid running this command more than once
simultaneously.
"""
- template = getattr(self, 'template', None)
- if not template:
- template_name = options.get('template', None)
- if not template_name:
- raise CommandError('--template option is required.')
+ email = getattr(self, 'email', None)
+ if not email:
+ email_name = options.get('email', None)
+ if not email_name:
+ raise CommandError('--email option is required.')
try:
- template = Email.objects.get(name=template_name)
- self.template = template
- except Email.DoesNotExist:
- raise CommandError(
- 'No email template %s found.' % template_name)
-
- # Use custom emailer if defined, default otherwise
- emailer_class = getattr(self, 'emailer_class', None)
- if not emailer_class:
- try:
- emailer_class = template.get_emailer_callable()
- self.emailer_class = emailer_class
- except ImportError, e:
+ email = Email.get(email_name)
+ self.email = email
+ except (ImportError, AttributeError), e:
raise CommandError(e)
- emailer = emailer_class(campaign=label, email=template,
- force=options['force'])
- try:
- emailer.send_email()
- except Exception, e:
- raise CommandError(e)
+ force = options.get('force', False)
+ emailer = email.emailer(campaign=label, email=email,
+ force=force)
+
+ emailer.send_email()
View
103 apps/emailer/models.py
@@ -1,113 +1,16 @@
-from django.conf import settings
-from django.core.exceptions import ValidationError
-from django.core.urlresolvers import get_callable
from django.db import models
-from greatape import MailChimp
-
-import emailer
from subscriptions.models import Subscriber
-mailchimp = MailChimp(settings.MAILCHIMP_API_KEY)
-
-
-class Email(models.Model):
- """An email template, to be sent to subscribers."""
- name = models.CharField(max_length=255, db_index=True)
- subject = models.CharField(max_length=255)
- text = models.TextField()
- html = models.TextField(blank=True, help_text=(
- 'Keep empty for text-only mail. Otherwise, make this the HTML version '
- 'of the email. HTML-only emails are not allowed.'))
- recipients = models.ManyToManyField(Subscriber, through='Recipient',
- related_name='received')
- emailer_class = models.CharField(max_length=255, blank=True, help_text=(
- 'Python class name of custom Emailer to use. Example: '
- '<code>emailer.emailers.MyFancyEmailer</code><br/>Keep empty for '
- 'default Emailer.'))
- from_name = models.CharField(max_length=255, blank=True, help_text=(
- "The sender's name (not an email address)"))
- from_email = models.EmailField(blank=True, help_text=(
- "The sender's address e.g. campaign@mozilla.com"))
- reply_to_email = models.EmailField(blank=True, help_text=(
- "The reply-to address"))
- mailchimp_campaign = models.CharField(max_length=20, blank=True)
- mailchimp_list = models.CharField(max_length=20, blank=True, help_text=(
- "MailChimp list ID."
- "Only required if you're using the MailChimp emailer."
- "You can find this in the MailChimp list admin page."))
-
- def get_emailer_callable(self):
- return get_callable(self.emailer_class or 'emailer.base.BaseEmailer')
-
- def clean(self):
- if issubclass(self.get_emailer_callable(),
- emailer.base.MailChimpEmailer):
-
- if not self.mailchimp_list:
- raise ValidationError("A MailChimp list ID is required.")
-
- def save(self):
- if issubclass(self.get_emailer_callable(),
- emailer.base.MailChimpEmailer):
-
- if not self.mailchimp_campaign:
- self.create_mailchimp_campaign()
- else:
- self.update_mailchimp_campaign()
-
- super(Email, self).save()
-
- def update_mailchimp_campaign(self):
- """Update the MailChimp campaign"""
-
- mailchimp.campaignUpdate(cid=self.mailchimp_campaign, name='list_id',
- value=self.mailchimp_list)
- mailchimp.campaignUpdate(cid=self.mailchimp_campaign, name='subject',
- value=self.subject)
- updates = {
- 'list_id': self.mailchimp_list,
- 'subject': self.subject,
- 'from_email': self.from_email,
- 'from_name': self.from_name,
- 'auto_footer': False,
- 'content': {
- 'html': self.html,
- 'text': self.text,
- }
- }
- for name, update in updates.items():
- mailchimp.campaignUpdate(cid=self.mailchimp_campaign, name=name,
- value=update)
-
- def create_mailchimp_campaign(self):
- """Create a MailChimp campaign and store its ID."""
-
- type = 'trans'
- options = {
- 'list_id': self.mailchimp_list,
- 'subject': self.subject,
- 'from_email': self.from_email,
- 'from_name': self.from_name,
- 'auto_footer': False,
- }
- content = {
- 'html': self.html,
- 'text': self.text,
- }
- cid = mailchimp.campaignCreate(type=type, options=options,
- content=content)
- self.mailchimp_campaign = cid
-
class Recipient(models.Model):
"""
A mapping between templates and subscribers, keeping track of people who
have already received a specific template.
"""
- subscriber = models.ForeignKey(Subscriber)
- email = models.ForeignKey(Email)
+ subscriber = models.ForeignKey(Subscriber, related_name='received')
+ email_id = models.CharField(max_length=255)
created = models.DateTimeField(auto_now_add=True, editable=False)
class Meta:
- unique_together = (('subscriber', 'email'),)
+ unique_together = (('subscriber', 'email_id'),)
View
50 apps/emailer/tests.py
@@ -1,8 +1,18 @@
+from django import test
+
import mock
-import test_utils
from nose.tools import eq_
+from . import charsets, Email, Emailer
import mysmtp
+from test_client import TestClient
+
+
+class TestEmail(Email):
+ id = 'test'
+
+test_email = TestEmail()
+test_emailer = Emailer('test', test_email)
@mock.patch('mysmtp.EmailBackend.open')
@@ -27,3 +37,41 @@ def test_send_failure(send_mock, open_mock):
rv = backend.send_messages(['sent', 'failed', 'sent'])
eq_(send_mock.call_count, 3)
eq_(rv, expected)
+
+class EmailTest(test.TestCase):
+ fixtures = ['subscriptions']
+
+ def setUp(self):
+ self.email = test_email
+ self.emailer = test_emailer
+
+ def test_get_email(self):
+ email = Email.get('emailer.tests.TestEmail')
+ eq_(email.id, 'test')
+
+ def test_get_subscriptions(self):
+ subs = self.emailer.get_subscriptions()
+ eq_(subs.count(), 2)
+
+ @mock.patch('emailer.mail.get_connection')
+ def test_recipients_saved(self, conn_mock):
+ sent = [True, False]
+
+ backend = mysmtp.EmailBackend()
+ backend.connection = mock.Mock()
+ backend._send = lambda *a, **k: sent.pop()
+
+ conn_mock.return_value = backend
+ self.emailer.send_email()
+
+ subs = self.emailer.get_subscriptions()
+ eq_(subs.count(), 1)
+
+ def test_email_charset(self):
+ self.email.lang = 'it'
+ expected = charsets['it']
+
+ msg = self.email.message('test').message()
+
+ for x in msg.get_payload():
+ eq_(x.get_charset(), expected)
View
16 apps/nagios/views.py
@@ -1,17 +1,17 @@
from django.conf import settings
-from django.http import HttpResponse
+from django.http import HttpResponse, HttpResponseServerError
from subscriptions.models import Subscription
-from emailer.models import Email
+from emailer.models import Recipient
def index(request):
# check that the Firefox Home emails are sending
- subscriptions = Subscription.objects.filter(campaign='firefox-home-instructions')
- reg = Email.objects.filter(name='iphone-reg')
- if subscriptions.count() and reg.count():
- delta = subscriptions.count() - reg[0].recipients.count()
- if delta > settings.EMAIL_BACKLOG_TOLERANCE:
- return HttpResponse('ERROR: FxHome email backlog is %d' % delta)
+ s_count = Subscription.objects.filter(campaign='firefox-home-instructions').count()
+ r_count = Recipient.objects.filter(email_id='firefox-home-instructions-initial').count()
+ delta = s_count - r_count
+
+ if delta > settings.EMAIL_BACKLOG_TOLERANCE:
+ return HttpResponseServerError('ERROR: FxHome email backlog is %d' % delta)
return HttpResponse('SUCCESS')
View
43 apps/subscriptions/fixtures/subscriptions.json
@@ -1 +1,42 @@
-[{"pk": 1, "model": "subscriptions.subscriber", "fields": {"email": "foo@foo.com"}}, {"pk": 2, "model": "subscriptions.subscriber", "fields": {"email": "bar@bar.com"}}, {"pk": 1, "model": "subscriptions.subscription", "fields": {"campaign": "foo", "created": "2010-06-17 14:37:13", "locale": "en-US", "modified": "2010-06-17 14:37:13", "subscriber": 1, "source": "example.com/source", "active": 1}}, {"pk": 2, "model": "subscriptions.subscription", "fields": {"campaign": "foo", "created": "2010-06-17 14:37:40", "locale": "en-US", "modified": "2010-06-17 14:37:40", "subscriber": 2, "source": "example.com/source", "active": 1}}]
+[
+ {
+ "pk": 2,
+ "model": "subscriptions.subscriber",
+ "fields": {
+ "email": "test2@test.com"
+ }
+ },
+ {
+ "pk": 1,
+ "model": "subscriptions.subscriber",
+ "fields": {
+ "email": "test@test.com"
+ }
+ },
+ {
+ "pk": 2,
+ "model": "subscriptions.subscription",
+ "fields": {
+ "campaign": "test",
+ "created": "2010-06-17 14:37:40",
+ "locale": "en-us",
+ "modified": "2010-06-17 14:37:40",
+ "subscriber": 2,
+ "source": "example.com/source",
+ "active": true
+ }
+ },
+ {
+ "pk": 1,
+ "model": "subscriptions.subscription",
+ "fields": {
+ "campaign": "test",
+ "created": "2010-06-17 14:37:13",
+ "locale": "en-us",
+ "modified": "2010-06-17 14:37:13",
+ "subscriber": 1,
+ "source": "example.com/source",
+ "active": true
+ }
+ }
+]
View
7 apps/subscriptions/models.py
@@ -1,6 +1,7 @@
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
+from django.utils.translation.trans_real import to_language
class Subscriber(models.Model):
@@ -24,5 +25,9 @@ class Meta:
def clean(self):
if self.locale == '':
self.locale = 'en-US'
- if self.locale not in settings.LANGUAGES:
+
+ # convert locale codes (en_US) to lang code (en-us)
+ self.locale = to_language(self.locale)
+
+ if self.locale.lower() not in settings.LANGUAGES_LOWERED:
raise ValidationError("Not a valid language")
View
4 apps/subscriptions/templates/admin/subscriptions/change_list.html
@@ -1,9 +1,9 @@
{% extends "admin/change_list.html" %}
-{% load adminmedia admin_list i18n %}
+{% load adminmedia admin_list %}
{% block filters %}
<div id="changelist-filter">
- <h2>{% trans 'Filter' %}</h2>
+ <h2>Filter</h2>
<h3>Filter by date range</h3>
<form method="get">
<ul>
View
39 apps/subscriptions/tests.py
@@ -18,9 +18,11 @@ def tearDown(self):
self.c.tearDown()
def valid_subscriber(self):
- return Subscriber(email='foo@foo.com')
+ """Convenience function for generating a valid Subscriber object"""
+ return Subscriber(email='test@test.com')
def valid_subscription(self):
+ """Convenience function for generating a valid Subscription object"""
subscriber = self.valid_subscriber()
subscriber.save()
s = Subscription(campaign='test')
@@ -37,7 +39,7 @@ def test_subscriber_validation(self):
self.assertRaises(ValidationError, a.full_clean)
# fail on bad email format
- a.email = 'foo'
+ a.email = 'test'
self.assertRaises(ValidationError, a.full_clean)
def test_subscription_validation(self):
@@ -52,16 +54,16 @@ def test_subscription_validation(self):
# fail on bad locale
c = self.valid_subscription()
- c.locale = 'foo'
+ c.locale = 'test'
self.assertRaises(ValidationError, c.full_clean)
def test_locale_fallback(self):
- """A blank locale will fall back to en-US"""
+ """A blank locale will fall back to en-us"""
a = self.valid_subscription()
a.locale = ''
a.full_clean()
- eq_(a.locale, 'en-US')
+ eq_(a.locale, 'en-us')
def test_valid_locale(self):
"""A valid locale, other than en-US, works"""
@@ -70,10 +72,19 @@ def test_valid_locale(self):
a.locale = 'es-ES'
a.full_clean()
a.save()
+ eq_(Subscription.objects.filter(locale='es-es').count(), 1)
+
+ def test_underscore_locale(self):
+ """Replace underscores with hyphens in locale codes"""
+
+ a = self.valid_subscription()
+ a.locale = 'es_ES'
+ a.full_clean()
+ a.save()
eq_(Subscription.objects.filter(locale='es-ES').count(), 1)
def test_active_default(self):
- """A new record is active be default"""
+ """A new record is active by default"""
a = self.valid_subscription()
a.save()
@@ -86,24 +97,24 @@ def test_status_codes(self):
eq_(self.count(), 0)
# success returns 200
- resp = self.c.subscribe(email='foo@bar.com', campaign='foo')
+ resp = self.c.subscribe(email='test@test.com', campaign='test')
eq_(resp.status_code, 201, resp.content)
eq_(self.count(), 1)
- eq_(Subscription.objects.filter(subscriber__email='foo@bar.com').count(), 1)
+ eq_(Subscription.objects.filter(subscriber__email='test@test.com').count(), 1)
# duplicate returns 409
- resp = self.c.subscribe(email='foo@bar.com', campaign='foo')
+ resp = self.c.subscribe(email='test@test.com', campaign='test')
eq_(resp.status_code, 409, resp.content)
eq_(self.count(), 1)
- eq_(Subscription.objects.filter(subscriber__email='foo@bar.com').count(), 1)
+ eq_(Subscription.objects.filter(subscriber__email='test@test.com').count(), 1)
def test_read(self):
resp = self.c.read()
eq_(resp.status_code, 501)
def unsubscribe_conditional(subscription):
- return subscription.subscriber.email == 'foo@foo.com'
+ return subscription.subscriber.email == 'test@test.com'
class UnsubscribeManagementTest(test.TestCase):
fixtures = ['subscriptions']
@@ -113,11 +124,11 @@ def setUp(self):
self.run = self.command.handle_label
def test_unsubscribe_all(self):
- self.run('foo')
+ self.run('test')
eq_(Subscription.objects.filter(active=True).count(), 0)
def test_conditional_unsubscribe(self):
- self.run('foo', conditional='subscriptions.tests.unsubscribe_conditional')
+ self.run('test', conditional='subscriptions.tests.unsubscribe_conditional')
eq_(Subscription.objects.filter(active=True).count(), 1)
- rec = Subscription.objects.get(subscriber__email='foo@foo.com')
+ rec = Subscription.objects.get(subscriber__email='test@test.com')
eq_(rec.active, False)
View
5 cron/prod
@@ -0,0 +1,5 @@
+MAILTO=abuchanan@mozilla.com
+* * * * * apache cd /data/generic/www/django/basket.mozilla.com/basket; /usr/bin/python26 manage.py sendmail --email emails.home.Initial firefox-home-instructions > /dev/null
+* * * * * apache cd /data/generic/www/django/basket.mozilla.com/basket; /usr/bin/python26 manage.py sendmail --email emails.home.Reminder firefox-home-instructions > /dev/null
+*/3 * * * * apache cd /data/generic/www/django/basket.mozilla.com/basket; /usr/bin/python26 manage.py sync_unsubscribe > /dev/null
+MAILTO=root
View
0 emails/__init__.py
No changes.
View
17 emails/home.py
@@ -0,0 +1,17 @@
+from tower import ugettext_lazy as _
+
+from emailer import Email
+
+
+class Initial(Email):
+ id = 'firefox-home-instructions-initial'
+ subject = _('Set Up Firefox Home On Your iPhone')
+ from_name = _('Firefox Home Account Setup')
+ from_email = 'firefox-home-support@mozilla.com'
+ reply_email = 'firefox-home-support@mozilla.com'
+ template = 'firefox-home-instructions-initial'
+
+class Reminder(Initial):
+ id = 'firefox-home-instructions-reminder'
+ template = 'firefox-home-instructions-reminder'
+ emailer_class='custom_emailers.reminder.ReminderEmailer'
View
12 libs/custom_emailers/reminder.py
@@ -1,17 +1,17 @@
"""Custom emailer for sending a reminder email."""
import datetime
-from emailer.base import BaseEmailer
+from emailer import Emailer
-class ReminderEmailer(BaseEmailer):
+class ReminderEmailer(Emailer):
"""
Send email to subscribers, only after a week has passed since subscribing.
"""
delay = datetime.timedelta(weeks=1)
- def get_recipients(self):
- recipients = super(ReminderEmailer, self).get_recipients()
- recipients = recipients.exclude(
+ def get_subscriptions(self):
+ subscriptions = super(ReminderEmailer, self).get_subscriptions()
+ subscriptions = subscriptions.exclude(
subscriptions__created__gte=datetime.datetime.now()-self.delay)
- return recipients
+ return subscriptions
View
442 libs/html2text.py
@@ -0,0 +1,442 @@
+#!/usr/bin/env python
+"""html2text: Turn HTML into equivalent Markdown-structured text."""
+__version__ = "2.38"
+__author__ = "Aaron Swartz (me@aaronsw.com)"
+__copyright__ = "(C) 2004-2008 Aaron Swartz. GNU GPL 3."
+__contributors__ = ["Martin 'Joey' Schulze", "Ricardo Reyes", "Kevin Jay North"]
+
+# TODO:
+# Support decoded entities with unifiable.
+
+if not hasattr(__builtins__, 'True'): True, False = 1, 0
+import re, sys, urllib, htmlentitydefs, codecs, StringIO, types
+import sgmllib
+import urlparse
+sgmllib.charref = re.compile('&#([xX]?[0-9a-fA-F]+)[^0-9a-fA-F]')
+
+try: from textwrap import wrap
+except: pass
+
+# Use Unicode characters instead of their ascii psuedo-replacements
+UNICODE_SNOB = 0
+
+# Put the links after each paragraph instead of at the end.
+LINKS_EACH_PARAGRAPH = 0
+
+# Wrap long lines at position. 0 for no wrapping. (Requires Python 2.3.)
+BODY_WIDTH = 78
+
+# Don't show internal links (href="#local-anchor") -- corresponding link targets
+# won't be visible in the plain text file anyway.
+SKIP_INTERNAL_LINKS = False
+
+### Entity Nonsense ###
+
+def name2cp(k):
+ if k == 'apos': return ord("'")
+ if hasattr(htmlentitydefs, "name2codepoint"): # requires Python 2.3
+ return htmlentitydefs.name2codepoint[k]
+ else:
+ k = htmlentitydefs.entitydefs[k]
+ if k.startswith("&#") and k.endswith(";"): return int(k[2:-1]) # not in latin-1
+ return ord(codecs.latin_1_decode(k)[0])
+
+unifiable = {'rsquo':"'", 'lsquo':"'", 'rdquo':'"', 'ldquo':'"',
+'copy':'(C)', 'mdash':'--', 'nbsp':' ', 'rarr':'->', 'larr':'<-', 'middot':'*',
+'ndash':'-', 'oelig':'oe', 'aelig':'ae',
+'agrave':'a', 'aacute':'a', 'acirc':'a', 'atilde':'a', 'auml':'a', 'aring':'a',
+'egrave':'e', 'eacute':'e', 'ecirc':'e', 'euml':'e',
+'igrave':'i', 'iacute':'i', 'icirc':'i', 'iuml':'i',
+'ograve':'o', 'oacute':'o', 'ocirc':'o', 'otilde':'o', 'ouml':'o',
+'ugrave':'u', 'uacute':'u', 'ucirc':'u', 'uuml':'u'}
+
+unifiable_n = {}
+
+for k in unifiable.keys():
+ unifiable_n[name2cp(k)] = unifiable[k]
+
+def charref(name):
+ if name[0] in ['x','X']:
+ c = int(name[1:], 16)
+ else:
+ c = int(name)
+
+ if not UNICODE_SNOB and c in unifiable_n.keys():
+ return unifiable_n[c]
+ else:
+ return unichr(c)
+
+def entityref(c):
+ if not UNICODE_SNOB and c in unifiable.keys():
+ return unifiable[c]
+ else:
+ try: name2cp(c)
+ except KeyError: return "&" + c
+ else: return unichr(name2cp(c))
+
+def replaceEntities(s):
+ s = s.group(1)
+ if s[0] == "#":
+ return charref(s[1:])
+ else: return entityref(s)
+
+r_unescape = re.compile(r"&(#?[xX]?(?:[0-9a-fA-F]+|\w{1,8}));")
+def unescape(s):
+ return r_unescape.sub(replaceEntities, s)
+
+def fixattrs(attrs):
+ # Fix bug in sgmllib.py
+ if not attrs: return attrs
+ newattrs = []
+ for attr in attrs:
+ newattrs.append((attr[0], unescape(attr[1])))
+ return newattrs
+
+### End Entity Nonsense ###
+
+def onlywhite(line):
+ """Return true if the line does only consist of whitespace characters."""
+ for c in line:
+ if c is not ' ' and c is not ' ':
+ return c is ' '
+ return line
+
+def optwrap(text):
+ """Wrap all paragraphs in the provided text."""
+ if not BODY_WIDTH:
+ return text
+
+ assert wrap, "Requires Python 2.3."
+ result = ''
+ newlines = 0
+ for para in text.split("\n"):
+ if len(para) > 0:
+ if para[0] is not ' ' and para[0] is not '-' and para[0] is not '*':
+ for line in wrap(para, BODY_WIDTH):
+ result += line + "\n"
+ result += "\n"
+ newlines = 2
+ else:
+ if not onlywhite(para):
+ result += para + "\n"
+ newlines = 1
+ else:
+ if newlines < 2:
+ result += "\n"
+ newlines += 1
+ return result
+
+def hn(tag):
+ if tag[0] == 'h' and len(tag) == 2:
+ try:
+ n = int(tag[1])
+ if n in range(1, 10): return n
+ except ValueError: return 0
+
+class _html2text(sgmllib.SGMLParser):
+ def __init__(self, out=None, baseurl=''):
+ sgmllib.SGMLParser.__init__(self)
+
+ if out is None: self.out = self.outtextf
+ else: self.out = out
+ self.outtext = u''
+ self.quiet = 0
+ self.p_p = 0
+ self.outcount = 0
+ self.start = 1
+ self.space = 0
+ self.a = []
+ self.astack = []
+ self.acount = 0
+ self.list = []
+ self.blockquote = 0
+ self.pre = 0
+ self.startpre = 0
+ self.lastWasNL = 0
+ self.abbr_title = None # current abbreviation definition
+ self.abbr_data = None # last inner HTML (for abbr being defined)
+ self.abbr_list = {} # stack of abbreviations to write later
+ self.baseurl = baseurl
+
+ def outtextf(self, s):
+ self.outtext += s
+
+ def close(self):
+ sgmllib.SGMLParser.close(self)
+
+ self.pbr()
+ self.o('', 0, 'end')
+
+ return self.outtext
+
+ def handle_charref(self, c):
+ self.o(charref(c))
+
+ def handle_entityref(self, c):
+ self.o(entityref(c))
+
+ def unknown_starttag(self, tag, attrs):
+ self.handle_tag(tag, attrs, 1)
+
+ def unknown_endtag(self, tag):
+ self.handle_tag(tag, None, 0)
+
+ def previousIndex(self, attrs):
+ """ returns the index of certain set of attributes (of a link) in the
+ self.a list
+
+ If the set of attributes is not found, returns None
+ """
+ if not attrs.has_key('href'): return None
+
+ i = -1
+ for a in self.a:
+ i += 1
+ match = 0
+
+ if a.has_key('href') and a['href'] == attrs['href']:
+ if a.has_key('title') or attrs.has_key('title'):
+ if (a.has_key('title') and attrs.has_key('title') and
+ a['title'] == attrs['title']):
+ match = True
+ else:
+ match = True
+
+ if match: return i
+
+ def handle_tag(self, tag, attrs, start):
+ attrs = fixattrs(attrs)
+
+ if hn(tag):
+ self.p()
+ if start: self.o(hn(tag)*"#" + ' ')
+
+ if tag in ['p', 'div']: self.p()
+
+ if tag == "br" and start: self.o(" \n")
+
+ if tag == "hr" and start:
+ self.p()
+ self.o("* * *")
+ self.p()
+
+ if tag in ["head", "style", 'script']:
+ if start: self.quiet += 1
+ else: self.quiet -= 1
+
+ if tag in ["body"]:
+ self.quiet = 0 # sites like 9rules.com never close <head>
+
+ if tag == "blockquote":
+ if start:
+ self.p(); self.o('> ', 0, 1); self.start = 1
+ self.blockquote += 1
+ else:
+ self.blockquote -= 1
+ self.p()
+
+ if tag in ['em', 'i', 'u']: self.o("_")
+ if tag in ['strong', 'b']: self.o("**")
+ if tag == "code" and not self.pre: self.o('`') #TODO: `` `this` ``
+ if tag == "abbr":
+ if start:
+ attrsD = {}
+ for (x, y) in attrs: attrsD[x] = y
+ attrs = attrsD
+
+ self.abbr_title = None
+ self.abbr_data = ''
+ if attrs.has_key('title'):
+ self.abbr_title = attrs['title']
+ else:
+ if self.abbr_title != None:
+ self.abbr_list[self.abbr_data] = self.abbr_title
+ self.abbr_title = None
+ self.abbr_data = ''
+
+ if tag == "a":
+ if start:
+ attrsD = {}
+ for (x, y) in attrs: attrsD[x] = y
+ attrs = attrsD
+ if attrs.has_key('href') and not (SKIP_INTERNAL_LINKS and attrs['href'].startswith('#')):
+ self.astack.append(attrs)
+ else:
+ self.astack.append(None)
+ else:
+ if self.astack:
+ a = self.astack.pop()
+ if a:
+ self.o(" <" + a['href'] + ">")
+
+ if tag == "img" and start:
+ attrsD = {}
+ for (x, y) in attrs: attrsD[x] = y
+ attrs = attrsD
+ if attrs.has_key('src'):
+ attrs['href'] = attrs['src']
+ alt = attrs.get('alt', '')
+ i = self.previousIndex(attrs)
+ if i is not None:
+ attrs = self.a[i]
+ else:
+ self.acount += 1
+ attrs['count'] = self.acount
+ attrs['outcount'] = self.outcount
+ self.a.append(attrs)
+ self.o("![")
+ self.o(alt)
+ self.o("]["+`attrs['count']`+"]")
+
+ if tag == 'dl' and start: self.p()
+ if tag == 'dt' and not start: self.pbr()
+ if tag == 'dd' and start: self.o(' ')
+ if tag == 'dd' and not start: self.pbr()
+
+ if tag in ["ol", "ul"]:
+ if start:
+ self.list.append({'name':tag, 'num':0})
+ else:
+ if self.list: self.list.pop()
+
+ self.p()
+
+ if tag == 'li':
+ if start:
+ self.pbr()
+ if self.list: li = self.list[-1]
+ else: li = {'name':'ul', 'num':0}
+ self.o(" "*len(self.list)) #TODO: line up <ol><li>s > 9 correctly.
+ if li['name'] == "ul": self.o("* ")
+ elif li['name'] == "ol":
+ li['num'] += 1
+ self.o(`li['num']`+". ")
+ self.start = 1
+ else:
+ self.pbr()
+
+ if tag in ["table", "tr"] and start: self.p()
+ if tag == 'td': self.pbr()
+
+ if tag == "pre":
+ if start:
+ self.startpre = 1
+ self.pre = 1
+ else:
+ self.pre = 0
+ self.p()
+
+ def pbr(self):
+ if self.p_p == 0: self.p_p = 1
+
+ def p(self): self.p_p = 2
+
+ def o(self, data, puredata=0, force=0):
+ if self.abbr_data is not None: self.abbr_data += data
+
+ if not self.quiet:
+ if puredata and not self.pre:
+ data = re.sub('\s+', ' ', data)
+ if data and data[0] == ' ':
+ self.space = 1
+ data = data[1:]
+ if not data and not force: return
+
+ if self.startpre:
+ #self.out(" :") #TODO: not output when already one there
+ self.startpre = 0
+
+ bq = (">" * self.blockquote)
+ if not (force and data and data[0] == ">") and self.blockquote: bq += " "
+
+ if self.pre:
+ bq += " "
+ data = data.replace("\n", "\n"+bq)
+
+ if self.start:
+ self.space = 0
+ self.p_p = 0
+ self.start = 0
+
+ if force == 'end':
+ # It's the end.
+ self.p_p = 0
+ self.out("\n")
+ self.space = 0
+
+
+ if self.p_p:
+ self.out(('\n'+bq)*self.p_p)
+ self.space = 0
+
+ if self.space:
+ if not self.lastWasNL: self.out(' ')
+ self.space = 0
+
+ if self.a and ((self.p_p == 2 and LINKS_EACH_PARAGRAPH) or force == "end"):
+ if force == "end": self.out("\n")
+
+ newa = []
+ for link in self.a:
+ if self.outcount > link['outcount']:
+ self.out(" ["+`link['count']`+"]: " + urlparse.urljoin(self.baseurl, link['href']))
+ if link.has_key('title'): self.out(" ("+link['title']+")")
+ self.out("\n")
+ else:
+ newa.append(link)
+
+ if self.a != newa: self.out("\n") # Don't need an extra line when nothing was done.
+
+ self.a = newa
+
+ if self.abbr_list and force == "end":
+ for abbr, definition in self.abbr_list.items():
+ self.out(" *[" + abbr + "]: " + definition + "\n")
+
+ self.p_p = 0
+ self.out(data)
+ self.lastWasNL = data and data[-1] == '\n'
+ self.outcount += 1
+
+ def handle_data(self, data):
+ if r'\/script>' in data: self.quiet -= 1
+ self.o(data, 1)
+
+ def unknown_decl(self, data): pass
+
+def wrapwrite(text): sys.stdout.write(text.encode('utf8'))
+
+def html2text_file(html, out=wrapwrite, baseurl=''):
+ h = _html2text(out, baseurl)
+ h.feed(html)
+ h.feed("")
+ return h.close()
+
+def html2text(html, baseurl=''):
+ return optwrap(html2text_file(html, None, baseurl))
+
+if __name__ == "__main__":
+ baseurl = ''
+ if sys.argv[1:]:
+ arg = sys.argv[1]
+ if arg.startswith('http://'):
+ baseurl = arg
+ j = urllib.urlopen(baseurl)
+ try:
+ from feedparser import _getCharacterEncoding as enc
+ except ImportError:
+ enc = lambda x, y: ('utf-8', 1)
+ text = j.read()
+ encoding = enc(j.headers, text)[0]
+ if encoding == 'us-ascii': encoding = 'utf-8'
+ data = text.decode(encoding)
+
+ else:
+ encoding = 'utf8'
+ if len(sys.argv) > 2:
+ encoding = sys.argv[2]
+ data = open(arg, 'r').read().decode(encoding)
+ else:
+ data = sys.stdin.read().decode('utf8')
+ wrapwrite(html2text(data, baseurl))
+
View
BIN locale/ca/LC_MESSAGES/django.mo
Binary file not shown.
View
122 locale/ca/LC_MESSAGES/django.po
@@ -0,0 +1,122 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-08-19 18:01-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: emails/home.py:8
+msgid "Set Up Firefox Home On Your iPhone"
+msgstr ""
+
+#: emails/home.py:9
+msgid "Firefox Home Account Setup"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:4
+msgid "Greetings!"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:8
+msgid ""
+"Follow these five steps to set up Firefox Home and get access to your "
+"desktop Firefox history, bookmarks and even your open tabs on your iPhone:"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:12
+msgid "Install the free <a href='{0}'>Firefox Sync add-on</a> on your desktop."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:13
+msgid ""
+"Firefox Home uses the sync capabilities from the Firefox Sync add-on to "
+"access your Firefox browser information and securely send it to your iPhone."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:17
+msgid ""
+"Once Firefox restarts, follow the prompts to create an account with both a "
+"password and a Secret Phrase."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:18
+msgid ""
+"Firefox Home uses your Secret Phrase to locally encrypt and protect your "
+"information."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:22
+msgid ""
+"Firefox Sync will now begin securely synchronizing your data with our "
+"service. This initial synchronization may take up to 20 minutes."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:23
+msgid ""
+"Firefox Sync will only work if you are not in Private Browsing mode. Once "
+"your initial synchronization is complete, you'll see a Last Update time in "
+"the Tools > Sync menu."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:27
+msgid ""
+"To complete the setup, on your iPhone enter your account, password and "
+"Secret Phrase in the field provided and tap 'Done' on the keypad."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:28
+msgid ""
+"If you're having trouble logging in, <a href='{0}'>click here for help</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:33
+msgid ""
+"Once complete you will be able to access your desktop bookmarks, history, "
+"and open tabs on your iPhone."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:34
+msgid "If your data is not showing up, <a href='{0}'>click here for help</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:38
+msgid ""
+"If you're having any trouble at all, please <a href='{0}'>visit Firefox Home "
+"Support</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:40
+msgid ""
+"If you have ideas on how to make Firefox Home even better <a href='{0}'>let "
+"us know</a>!"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:44
+msgid ""
+"If you do not set up <a href='{0}'>Firefox Home</a> within one week of "
+"receiving this email, one reminder email will be sent to you."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:47
+msgid ""
+"See our <a href='{0}'>Privacy Policy</a>. Visit us for more information "
+"about <a href='{1}'>Firefox Home</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-reminder.html:4
+msgid ""
+"A few days ago you expressed interest in setting up Firefox Home on your "
+"iPhone."
+msgstr ""
View
BIN locale/cs/LC_MESSAGES/django.mo
Binary file not shown.
View
155 locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,155 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-08-19 18:01-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n>1 && n<5 ? 1 : 2;\n"
+
+#: emails/home.py:8
+msgid "Set Up Firefox Home On Your iPhone"
+msgstr "Nastavte si Firefox Home na svém iPhonu"
+
+#: emails/home.py:9
+msgid "Firefox Home Account Setup"
+msgstr "Nastavení účtu Firefox Home"
+
+#: templates/emails/firefox-home-instructions-initial.html:4
+msgid "Greetings!"
+msgstr "Vážený uživateli,"
+
+#: templates/emails/firefox-home-instructions-initial.html:8
+msgid ""
+"Follow these five steps to set up Firefox Home and get access to your "
+"desktop Firefox history, bookmarks and even your open tabs on your iPhone:"
+msgstr ""
+"v následujících pěti krocích vám pomůžeme nastavit Firefox Home a získat tak "
+"na svém iPhonu přístup k historii Firefoxu z vašeho počítače, k jeho "
+"záložkám a dokonce i k otevřeným panelům:"
+
+#: templates/emails/firefox-home-instructions-initial.html:12
+msgid "Install the free <a href='{0}'>Firefox Sync add-on</a> on your desktop."
+msgstr ""
+"Nainstalujte si na svůj počítač <a href='{0}'>doplněk Firefox Sync</a>, "
+"který je dostupný zdarma."
+
+#: templates/emails/firefox-home-instructions-initial.html:13
+msgid ""
+"Firefox Home uses the sync capabilities from the Firefox Sync add-on to "
+"access your Firefox browser information and securely send it to your iPhone."
+msgstr ""
+"Firefox Home využívá synchronizačních možností doplňku Firefox Sync pro "
+"přístup k informacím prohlížeče Firefox a zabezpečeně je přenáší na váš "
+"iPhone."
+
+#: templates/emails/firefox-home-instructions-initial.html:17
+msgid ""
+"Once Firefox restarts, follow the prompts to create an account with both a "
+"password and a Secret Phrase."
+msgstr ""
+"Po restartu Firefoxu postupujte podle zobrazených pokynů pro vytvoření účtu "
+"a zadejte heslo i tajnou frázi."
+
+#: templates/emails/firefox-home-instructions-initial.html:18
+msgid ""
+"Firefox Home uses your Secret Phrase to locally encrypt and protect your "
+"information."
+msgstr ""
+"Pomocí tajné fráze Firefox Home vaše data lokálně šifruje a zabezpečuje."
+
+#: templates/emails/firefox-home-instructions-initial.html:22
+msgid ""
+"Firefox Sync will now begin securely synchronizing your data with our "
+"service. This initial synchronization may take up to 20 minutes."
+msgstr ""
+"Firefox Sync nyní začne zabezpečeně synchronizovat vaše data s naší službou. "
+"Počáteční synchronizace může trvat až 20 minut."
+
+#: templates/emails/firefox-home-instructions-initial.html:23
+msgid ""
+"Firefox Sync will only work if you are not in Private Browsing mode. Once "
+"your initial synchronization is complete, you'll see a Last Update time in "
+"the Tools > Sync menu."
+msgstr ""
+"Firefox Sync bude fungovat pouze pokud nejste přepnuti do režimu anonymního "
+"prohlížení. Po dokončení počáteční synchronizace uvidíte čas poslední "
+"synchronizace pod nabídkou Nástroje > Sync."
+
+#: templates/emails/firefox-home-instructions-initial.html:27
+msgid ""
+"To complete the setup, on your iPhone enter your account, password and "
+"Secret Phrase in the field provided and tap 'Done' on the keypad."
+msgstr ""
+"Pro dokončení nastavení zadejte na vašem iPhonu svůj účet, heslo a tajnou "
+"frázi do připravených políček a na klávesnici klepněte na tlačítko \"Hotovo"
+"\"."
+
+#: templates/emails/firefox-home-instructions-initial.html:28
+msgid ""
+"If you're having trouble logging in, <a href='{0}'>click here for help</a>."
+msgstr ""
+"Pokud máte problémy s přihlášením, <a href='{0}'>klepněte pro pomoc sem</a>."
+
+#: templates/emails/firefox-home-instructions-initial.html:33
+msgid ""
+"Once complete you will be able to access your desktop bookmarks, history, "
+"and open tabs on your iPhone."
+msgstr ""
+"Po dokončení budete mít na svém iPhonu přístup k záložkám, historii, a "
+"otevřeným panelům Firefoxu z vašeho počítače."
+
+#: templates/emails/firefox-home-instructions-initial.html:34
+msgid "If your data is not showing up, <a href='{0}'>click here for help</a>."
+msgstr ""
+"Pokud se vaše data nezobrazují, <a href='{0}'>klepněte pro pomoc sem</a>."
+
+#: templates/emails/firefox-home-instructions-initial.html:38
+msgid ""
+"If you're having any trouble at all, please <a href='{0}'>visit Firefox Home "
+"Support</a>."
+msgstr ""
+"V případě jakýchkoliv potíží navštivte <a href='{0}'>podporu Firefox Home</"
+"a>."
+
+#: templates/emails/firefox-home-instructions-initial.html:40
+msgid ""
+"If you have ideas on how to make Firefox Home even better <a href='{0}'>let "
+"us know</a>!"
+msgstr ""
+"Pokud máte návrhy na vylepšení Firefox Home, <a href='{0}'>dejte nám vědět</"
+"a>!"
+
+#: templates/emails/firefox-home-instructions-initial.html:44
+msgid ""
+"If you do not set up <a href='{0}'>Firefox Home</a> within one week of "
+"receiving this email, one reminder email will be sent to you."
+msgstr ""
+"Nenastavíte-li si <a href='{0}'>Firefox Home</a> do jednoho týdne od "
+"obdržení tohoto e-mailu, pro připomenutí vám bude zaslána ještě jedna zpráva."
+
+#: templates/emails/firefox-home-instructions-initial.html:47
+msgid ""
+"See our <a href='{0}'>Privacy Policy</a>. Visit us for more information "
+"about <a href='{1}'>Firefox Home</a>."
+msgstr ""
+"Přečtěte si prosím naše <a href='{0}'>zásady ochrany soukromí</a>. Pro další "
+"informace o <a href='{1}'>Firefox Home</a> prosím navštivte naše stránky."
+
+#: templates/emails/firefox-home-instructions-reminder.html:4
+msgid ""
+"A few days ago you expressed interest in setting up Firefox Home on your "
+"iPhone."
+msgstr ""
+"Před několika dny jste projevil zájem o nastavení Firefox Home na svém "
+"iPhonu."
View
BIN locale/de/LC_MESSAGES/django.mo
Binary file not shown.
View
159 locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,159 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-08-19 18:01-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+
+#: emails/home.py:8
+msgid "Set Up Firefox Home On Your iPhone"
+msgstr "Einrichtung von Firefox Home auf Ihrem iPhone"
+
+#: emails/home.py:9
+msgid "Firefox Home Account Setup"
+msgstr "Firefox Home Benutzerkonten-Einrichtung"
+
+#: templates/emails/firefox-home-instructions-initial.html:4
+msgid "Greetings!"
+msgstr "Hallo!"
+
+#: templates/emails/firefox-home-instructions-initial.html:8
+msgid ""
+"Follow these five steps to set up Firefox Home and get access to your "
+"desktop Firefox history, bookmarks and even your open tabs on your iPhone:"
+msgstr ""
+"Folgen Sie diesen fünf Schritten, um Firefox Home einzurichten und auf Ihrem "
+"iPhone Zugang zur Chronik, den Lesezeichen und sogar den geöffneten Tabs von "
+"Firefox auf Ihrem Desktop-Rechner zu erhalten:"
+
+#: templates/emails/firefox-home-instructions-initial.html:12
+msgid "Install the free <a href='{0}'>Firefox Sync add-on</a> on your desktop."
+msgstr ""
+"Installieren Sie das <a href='{0}'>kostenlose Add-on Firefox Sync</a> auf "
+"Ihrem Desktop-Rechner."
+
+#: templates/emails/firefox-home-instructions-initial.html:13
+msgid ""
+"Firefox Home uses the sync capabilities from the Firefox Sync add-on to "
+"access your Firefox browser information and securely send it to your iPhone."
+msgstr ""
+"Firefox Home verwendet die Synchronisierungsfunktionen des Add-ons Firefox "
+"Sync, um auf die Daten Ihres Firefox-Browsers zuzugreifen und sie geschützt "
+"auf Ihr iPhone zu übertragen."
+
+#: templates/emails/firefox-home-instructions-initial.html:17
+msgid ""
+"Once Firefox restarts, follow the prompts to create an account with both a "
+"password and a Secret Phrase."
+msgstr ""
+"Folgen Sie nach einem Neustart von Firefox dem Assistenten, um ein "
+"Benutzerkonto mit einem Passwort und einer Passphrase anzulegen."
+
+#: templates/emails/firefox-home-instructions-initial.html:18
+msgid ""
+"Firefox Home uses your Secret Phrase to locally encrypt and protect your "
+"information."
+msgstr ""
+"Firefox Home verwendet Ihre Passphrase, um Ihre Daten lokal zu verschlüsseln "
+"und zu schützen."
+
+#: templates/emails/firefox-home-instructions-initial.html:22
+msgid ""
+"Firefox Sync will now begin securely synchronizing your data with our "
+"service. This initial synchronization may take up to 20 minutes."
+msgstr ""
+"Firefox Sync beginnt nun, Ihre Daten sicher mit unserem Dienst abzugleichen. "
+"Diese erste Synchronisation kann bis zu 20 Minuten dauern."
+
+#: templates/emails/firefox-home-instructions-initial.html:23
+msgid ""
+"Firefox Sync will only work if you are not in Private Browsing mode. Once "
+"your initial synchronization is complete, you'll see a Last Update time in "
+"the Tools > Sync menu."
+msgstr ""
+"Firefox Sync funktioniert nur, wenn Sie nicht im Privaten Modus surfen. "
+"Sobald die erste Synchronisation abgeschlossen ist, können Sie den Zeitpunkt "
+"der letzten Aktualisierung im Menü Extras > Sync sehen."
+
+#: templates/emails/firefox-home-instructions-initial.html:27
+msgid ""
+"To complete the setup, on your iPhone enter your account, password and "
+"Secret Phrase in the field provided and tap 'Done' on the keypad."
+msgstr ""
+"Um die Einrichtung abzuschließen, geben Sie auf Ihrem iPhone Ihren "
+"Benutzernamen, Ihr Passwort und Ihre Passphrase in die entsprechenden Felder "
+"ein und tippen Sie dann auf „Fertig“ am Tastenfeld."
+
+#: templates/emails/firefox-home-instructions-initial.html:28
+msgid ""
+"If you're having trouble logging in, <a href='{0}'>click here for help</a>."
+msgstr ""
+"Wenn Sie Probleme beim Anmelden haben, <a href='{0}'>klicken Sie hier, um "
+"Hilfe zu erhalten</a>."
+
+#: templates/emails/firefox-home-instructions-initial.html:33
+msgid ""
+"Once complete you will be able to access your desktop bookmarks, history, "
+"and open tabs on your iPhone."
+msgstr ""
+"Wenn Sie fertig sind, können Sie mit Ihrem iPhone auf die Lesezeichen, "
+"Chronik und geöffneten Tabs Ihres Desktop-Rechners zugreifen."
+
+#: templates/emails/firefox-home-instructions-initial.html:34
+msgid "If your data is not showing up, <a href='{0}'>click here for help</a>."
+msgstr ""
+"Wenn Ihre Daten nicht angezeigt werden, <a href='{0}'>klicken Sie hier, um "
+"Hilfe zu erhalten</a>."
+
+#: templates/emails/firefox-home-instructions-initial.html:38
+msgid ""
+"If you're having any trouble at all, please <a href='{0}'>visit Firefox Home "
+"Support</a>."
+msgstr ""
+"Wenn Sie Schwierigkeiten haben, besuchen Sie bitte die <a href='{0}'>Firefox-"
+"Home-Hilfe</a>."
+
+#: templates/emails/firefox-home-instructions-initial.html:40
+msgid ""
+"If you have ideas on how to make Firefox Home even better <a href='{0}'>let "
+"us know</a>!"
+msgstr ""
+"Wenn Sie Vorschläge haben, wie man Firefox Home verbessern könnte, <a "
+"href='{0}'>lassen Sie es uns wissen</a>!"
+
+#: templates/emails/firefox-home-instructions-initial.html:44
+msgid ""
+"If you do not set up <a href='{0}'>Firefox Home</a> within one week of "
+"receiving this email, one reminder email will be sent to you."
+msgstr ""
+"Wenn Sie <a href='{0}'>Firefox Home</a> nicht innerhalb einer Woche nach "
+"Erhalt dieser E-Mail einrichten, erhalten Sie eine Erinnerung per E-Mail."
+
+#: templates/emails/firefox-home-instructions-initial.html:47
+msgid ""
+"See our <a href='{0}'>Privacy Policy</a>. Visit us for more information "
+"about <a href='{1}'>Firefox Home</a>."
+msgstr ""
+"Lesen Sie unsere <a href='{0}'>Datenschutzerklärung</a>. Besuchen Sie unsere "
+"Webseiten, um weitere Informationen über <a href='{1}'>Firefox Home</a> zu "
+"erhalten."
+
+#: templates/emails/firefox-home-instructions-reminder.html:4
+msgid ""
+"A few days ago you expressed interest in setting up Firefox Home on your "
+"iPhone."
+msgstr ""
+"Vor einigen Tagen haben Sie Ihr Interesse am Einrichten von Firefox Home auf "
+"Ihrem iPhone bekundet."
View
BIN locale/en_US/LC_MESSAGES/django.mo
Binary file not shown.
View
121 locale/en_US/LC_MESSAGES/django.po
@@ -0,0 +1,121 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-08-19 18:01-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: emails/home.py:8
+msgid "Set Up Firefox Home On Your iPhone"
+msgstr ""
+
+#: emails/home.py:9
+msgid "Firefox Home Account Setup"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:4
+msgid "Greetings!"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:8
+msgid ""
+"Follow these five steps to set up Firefox Home and get access to your "
+"desktop Firefox history, bookmarks and even your open tabs on your iPhone:"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:12
+msgid "Install the free <a href='{0}'>Firefox Sync add-on</a> on your desktop."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:13
+msgid ""
+"Firefox Home uses the sync capabilities from the Firefox Sync add-on to "
+"access your Firefox browser information and securely send it to your iPhone."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:17
+msgid ""
+"Once Firefox restarts, follow the prompts to create an account with both a "
+"password and a Secret Phrase."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:18
+msgid ""
+"Firefox Home uses your Secret Phrase to locally encrypt and protect your "
+"information."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:22
+msgid ""
+"Firefox Sync will now begin securely synchronizing your data with our "
+"service. This initial synchronization may take up to 20 minutes."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:23
+msgid ""
+"Firefox Sync will only work if you are not in Private Browsing mode. Once "
+"your initial synchronization is complete, you'll see a Last Update time in "
+"the Tools > Sync menu."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:27
+msgid ""
+"To complete the setup, on your iPhone enter your account, password and "
+"Secret Phrase in the field provided and tap 'Done' on the keypad."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:28
+msgid ""
+"If you're having trouble logging in, <a href='{0}'>click here for help</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:33
+msgid ""
+"Once complete you will be able to access your desktop bookmarks, history, "
+"and open tabs on your iPhone."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:34
+msgid "If your data is not showing up, <a href='{0}'>click here for help</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:38
+msgid ""
+"If you're having any trouble at all, please <a href='{0}'>visit Firefox Home "
+"Support</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:40
+msgid ""
+"If you have ideas on how to make Firefox Home even better <a href='{0}'>let "
+"us know</a>!"
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:44
+msgid ""
+"If you do not set up <a href='{0}'>Firefox Home</a> within one week of "
+"receiving this email, one reminder email will be sent to you."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-initial.html:47
+msgid ""
+"See our <a href='{0}'>Privacy Policy</a>. Visit us for more information "
+"about <a href='{1}'>Firefox Home</a>."
+msgstr ""
+
+#: templates/emails/firefox-home-instructions-reminder.html:4
+msgid ""
+"A few days ago you expressed interest in setting up Firefox Home on your "
+"iPhone."
+msgstr ""
View
BIN locale/es/LC_MESSAGES/django.mo
Binary file not shown.
View
156 locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,156 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2010-08-19 18:01-0500\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: emails/home.py:8
+msgid "Set Up Firefox Home On Your iPhone"
+msgstr "Configura Firefox Home en tu iPhone"
+
+#: emails/home.py:9
+msgid "Firefox Home Account Setup"
+msgstr "Configuración de cuenta de Firefox Home"
+
+#: templates/emails/firefox-home-instructions-initial.html:4
+msgid "Greetings!"
+msgstr "¡Saludos!"
+
+#: templates/emails/firefox-home-instructions-initial.html:8
+msgid ""
+"Follow these five steps to set up Firefox Home and get access to your "
+"desktop Firefox history, bookmarks and even your open tabs on your iPhone:"
+msgstr ""
+"Sigue estos cinco pasos para configurar Firefox Home y obtener accesso en tu "
+"iPhone a tu historial, marcadores e incluso pestañas abiertas de tu Firefox "
+"de escritorio:"
+
+#: templates/emails/firefox-home-instructions-initial.html:12
+msgid "Install the free <a href='{0}'>Firefox Sync add-on</a> on your desktop."
+msgstr ""
+"Instala el <a href='{0}'>complemento gratuito Firefox Sync</a> en tu "
+"escritorio."
+
+#: templates/emails/firefox-home-instructions-initial.html:13
+msgid ""
+"Firefox Home uses the sync capabilities from the Firefox Sync add-on to "
+"access your Firefox browser information and securely send it to your iPhone."
+msgstr ""
+"Firefox Home usa las capacidades de sincronización del complemento Firefox "
+"Sync para acceder a la información de tu navegador Firefox y enviarla de "
+"manera segura a tu iPhone."
+
+#: templates/emails/firefox-home-instructions-initial.html:17
+msgid ""
+"Once Firefox restarts, follow the prompts to create an account with both a "
+"password and a Secret Phrase."
+msgstr ""
+"Una vez se reinicie Firefox, sigue las instrucciones para crear una cuenta, "
+"con una contraseña y una frase secreta."
+
+#: templates/emails/firefox-home-instructions-initial.html:18
+msgid ""
+"Firefox Home uses your Secret Phrase to locally encrypt and protect your "
+"information."
+msgstr ""
+"Firefox Home usa tu frase secreta para cifrar y proteger localmente tu "
+"informción."
+
+#: templates/emails/firefox-home-instructions-initial.html:22
+msgid ""
+"Firefox Sync will now begin securely synchronizing your data with our "
+"service. This initial synchronization may take up to 20 minutes."
+msgstr ""
+"Firefox Sync comenzará ahora a sincronizar de manera segura tus datos con "
+"nuestro servicio. Esta sincronización inicial puede llevar hasta 20 minutos."
+
+#: templates/emails/firefox-home-instructions-initial.html:23
+msgid ""
+"Firefox Sync will only work if you are not in Private Browsing mode. Once "
+"your initial synchronization is complete, you'll see a Last Update time in "
+"the Tools > Sync menu."
+msgstr ""
+"Firefox Sync sólo operará si no estás en el modo de navegación privada. Una "
+"vez que se complete tu sincronización inicial, verás una fecha de última "
+"actualización en el menú Herramientas -> Sync."
+
+#: templates/emails/firefox-home-instructions-initial.html:27
+msgid ""
+"To complete the setup, on your iPhone enter your account, password and "
+"Secret Phrase in the field provided and tap 'Done' on the keypad."
+msgstr ""
+"Para completar la configuración, introduce tu cuenta, contraseña y frase "
+"secreta en tu iPhone en los campos proporcionados y toca 'Hecho' en el "
+"teclado en pantalla."
+
+#: templates/emails/firefox-home-instructions-initial.html:28
+msgid ""
+"If you're having trouble logging in, <a href='{0}'>click here for help</a>."
+msgstr ""
+"Si tienes problemas para iniciar sesión, <a href='{0}'>pulsa aquí para "