summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-x.gitignore2
-rw-r--r--askbot/__init__.py6
-rw-r--r--askbot/conf/__init__.py1
-rw-r--r--askbot/conf/access_control.py22
-rw-r--r--askbot/conf/email.py10
-rw-r--r--askbot/conf/external_keys.py2
-rw-r--r--askbot/conf/forum_data_rules.py12
-rw-r--r--askbot/conf/group_settings.py4
-rw-r--r--askbot/conf/login_providers.py60
-rw-r--r--askbot/conf/minimum_reputation.py30
-rw-r--r--askbot/conf/settings_wrapper.py36
-rw-r--r--askbot/conf/site_modes.py5
-rw-r--r--askbot/conf/site_settings.py30
-rw-r--r--askbot/conf/user_settings.py32
-rw-r--r--askbot/conf/words.py912
-rw-r--r--askbot/const/__init__.py31
-rw-r--r--askbot/const/message_keys.py32
-rw-r--r--askbot/deps/django_authopenid/backends.py34
-rw-r--r--askbot/deps/django_authopenid/forms.py78
-rw-r--r--askbot/deps/django_authopenid/urls.py10
-rw-r--r--askbot/deps/django_authopenid/util.py66
-rw-r--r--askbot/deps/django_authopenid/views.py91
-rw-r--r--askbot/deps/group_messaging/urls.py7
-rw-r--r--askbot/deps/group_messaging/views.py4
-rw-r--r--askbot/deps/livesettings/urls.py5
-rw-r--r--askbot/doc/source/askbot/layout.html6
-rw-r--r--askbot/doc/source/changelog.rst18
-rw-r--r--askbot/doc/source/contributors.rst9
-rw-r--r--askbot/doc/source/index.rst5
-rw-r--r--askbot/doc/source/live-settings.rst17
-rw-r--r--askbot/doc/source/localization.rst81
-rw-r--r--askbot/doc/source/management-commands.rst39
-rw-r--r--askbot/doc/source/multilingual.rst46
-rw-r--r--askbot/doc/source/mysql-to-postgres.rst32
-rw-r--r--askbot/doc/source/solr.rst80
-rw-r--r--askbot/doc/source/text-search.rst48
-rw-r--r--askbot/feed.py11
-rw-r--r--askbot/forms.py205
-rw-r--r--askbot/importers/stackexchange/management/commands/load_stackexchange.py2
-rw-r--r--askbot/locale/en/LC_MESSAGES/django.mobin1180 -> 626 bytes
-rw-r--r--askbot/locale/en/LC_MESSAGES/django.po1895
-rw-r--r--askbot/locale/en/LC_MESSAGES/djangojs.mobin1943 -> 1943 bytes
-rw-r--r--askbot/locale/en/LC_MESSAGES/djangojs.po109
-rw-r--r--askbot/locale/fr/LC_MESSAGES/django.mobin178805 -> 180252 bytes
-rw-r--r--askbot/locale/fr/LC_MESSAGES/django.po93
-rw-r--r--askbot/locale/ru/LC_MESSAGES/django.mobin208457 -> 208376 bytes
-rw-r--r--askbot/locale/ru/LC_MESSAGES/django.po6
-rw-r--r--askbot/mail/__init__.py84
-rw-r--r--askbot/mail/parsing.py8
-rw-r--r--askbot/management/commands/add_admin.py3
-rw-r--r--askbot/management/commands/apply_hinted_tags.py3
-rw-r--r--askbot/management/commands/askbot_add_osqa_content.py482
-rw-r--r--askbot/management/commands/askbot_add_test_content.py38
-rw-r--r--askbot/management/commands/askbot_add_xml_content.py558
-rw-r--r--askbot/management/commands/askbot_import_jive.py315
-rw-r--r--askbot/management/commands/base.py240
-rw-r--r--askbot/management/commands/create_tag_synonyms.py54
-rw-r--r--askbot/management/commands/create_thousand_tags.py8
-rw-r--r--askbot/management/commands/createsuperuser.py3
-rw-r--r--askbot/management/commands/dump_forum.py36
-rw-r--r--askbot/management/commands/export_osqa.py215
-rw-r--r--askbot/management/commands/find_bodyless_questions.py19
-rw-r--r--askbot/management/commands/fix_comment_counts.py34
-rw-r--r--askbot/management/commands/fix_question_tags.py169
-rw-r--r--askbot/management/commands/generate_post_snippets.py21
-rw-r--r--askbot/management/commands/load_forum.py16
-rw-r--r--askbot/management/commands/remove_admin.py3
-rw-r--r--askbot/management/commands/rename_tags.py30
-rw-r--r--askbot/management/commands/rename_tags_id.py22
-rw-r--r--askbot/management/commands/send_accept_answer_reminders.py11
-rw-r--r--askbot/management/commands/send_email_alerts.py27
-rw-r--r--askbot/management/commands/send_unanswered_question_reminders.py6
-rw-r--r--askbot/media/bootstrap/css/bootstrap.css1
-rw-r--r--askbot/media/jquery-openid/images/mozilla-persona.gifbin0 -> 2197 bytes
-rw-r--r--askbot/media/jquery-openid/jquery.openid.js127
-rw-r--r--askbot/media/jquery-openid/openid.css25
-rw-r--r--askbot/media/js/group_messaging.js4
-rw-r--r--askbot/media/js/less.min.js32
-rw-r--r--askbot/media/js/live_search.js4
-rw-r--r--askbot/media/js/post.js118
-rw-r--r--askbot/media/js/tag_selector.js8
-rw-r--r--askbot/media/js/user.js54
-rw-r--r--askbot/media/js/utils.js438
-rw-r--r--askbot/media/js/wmd/wmd.js17
-rw-r--r--askbot/media/style/style.css444
-rw-r--r--askbot/media/style/style.less543
-rw-r--r--askbot/middleware/cancel.py2
-rw-r--r--askbot/middleware/forum_mode.py9
-rw-r--r--askbot/middleware/remote_ip.py21
-rw-r--r--askbot/migrations/0006_add_subscription_setting_for_comments_and_mentions.py2
-rw-r--r--askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py3
-rw-r--r--askbot/migrations/0055_auto__chg_field_question_thread.py2
-rw-r--r--askbot/migrations/0080_transplant_favquestions_2.py2
-rw-r--r--askbot/migrations/0093_auto__del_field_vote_content_type__del_field_vote_object_id__chg_field.py2
-rw-r--r--askbot/migrations/0099_auto__del_field_thread_accepted_answer__del_field_anonymousanswer_ques.py4
-rw-r--r--askbot/migrations/0171_auto__add_importedobjectinfo__add_importrun.py439
-rw-r--r--askbot/migrations/0172_sync_thread_deleted.py419
-rw-r--r--askbot/migrations/0173_auto__del_unique_tag_name__chg_field_thread_added_at.py426
-rw-r--r--askbot/migrations/0174_auto__add_field_tag_language_code.py423
-rw-r--r--askbot/migrations/0175_auto__add_unique_tag_name_language_code.py421
-rw-r--r--askbot/migrations/0176_populate_tag_language_code.py464
-rw-r--r--askbot/migrations/0177_auto__add_field_tagsynonym_language_code.py424
-rw-r--r--askbot/migrations_api/__init__.py2
-rw-r--r--askbot/models/__init__.py806
-rw-r--r--askbot/models/badges.py165
-rw-r--r--askbot/models/meta.py32
-rw-r--r--askbot/models/post.py135
-rw-r--r--askbot/models/question.py199
-rw-r--r--askbot/models/reply_by_email.py13
-rw-r--r--askbot/models/repute.py16
-rw-r--r--askbot/models/signals.py1
-rw-r--r--askbot/models/tag.py50
-rw-r--r--askbot/models/user.py33
-rw-r--r--askbot/patches/__init__.py8
-rw-r--r--askbot/patches/django_patches.py30
-rw-r--r--askbot/schedules.py19
-rw-r--r--askbot/search/haystack/__init__.py2
-rw-r--r--askbot/search/state_manager.py11
-rw-r--r--askbot/setup_templates/urls.py11
-rw-r--r--askbot/skins/loaders.py3
-rw-r--r--askbot/startup_procedures.py11
-rw-r--r--askbot/tasks.py5
-rw-r--r--askbot/templates/404.html7
-rw-r--r--askbot/templates/500.html14
-rw-r--r--askbot/templates/answer_edit.html12
-rw-r--r--askbot/templates/ask.html24
-rw-r--r--askbot/templates/authopenid/changeemail.html29
-rw-r--r--askbot/templates/authopenid/complete.html9
-rw-r--r--askbot/templates/authopenid/providers_javascript.html29
-rw-r--r--askbot/templates/authopenid/signin.html42
-rw-r--r--askbot/templates/authopenid/signup_with_password.html5
-rw-r--r--askbot/templates/authopenid/verify_email.html2
-rw-r--r--askbot/templates/authopenid/widget_signin.html18
-rw-r--r--askbot/templates/badges.html3
-rw-r--r--askbot/templates/close.html10
-rw-r--r--askbot/templates/email/change_settings_info.html3
-rw-r--r--askbot/templates/email/delayed_email_alert.html2
-rw-r--r--askbot/templates/email/insufficient_rep_to_post_by_email.html4
-rw-r--r--askbot/templates/email/macros.html87
-rw-r--r--askbot/templates/email/post_as_subthread.html4
-rw-r--r--askbot/templates/email/quoted_post.html6
-rw-r--r--askbot/templates/email/re_welcome_lamson_on.html2
-rw-r--r--askbot/templates/email/welcome_lamson_on.html2
-rw-r--r--askbot/templates/embed/ask_by_widget.html4
-rw-r--r--askbot/templates/embed/widget_form.html2
-rw-r--r--askbot/templates/embed/widgets.html4
-rw-r--r--askbot/templates/faq_static.html9
-rw-r--r--askbot/templates/feedback.html11
-rw-r--r--askbot/templates/flatpages/default.html7
-rw-r--r--askbot/templates/list_suggested_tags.html9
-rw-r--r--askbot/templates/macros.html125
-rw-r--r--askbot/templates/main_page.html4
-rw-r--r--askbot/templates/main_page/headline.html2
-rw-r--r--askbot/templates/main_page/javascript.html5
-rw-r--r--askbot/templates/main_page/nothing_found.html8
-rw-r--r--askbot/templates/main_page/questions_loop.html6
-rw-r--r--askbot/templates/main_page/tab_bar.html2
-rw-r--r--askbot/templates/meta/bottom_scripts.html2
-rw-r--r--askbot/templates/meta/fonts.html4
-rw-r--r--askbot/templates/meta/html_head_javascript.html2
-rw-r--r--askbot/templates/question.html40
-rw-r--r--askbot/templates/question/answer_card.html2
-rw-r--r--askbot/templates/question/answer_comments.html3
-rw-r--r--askbot/templates/question/answer_controls.html6
-rw-r--r--askbot/templates/question/answer_tab_bar.html15
-rw-r--r--askbot/templates/question/answer_vote_buttons.html10
-rw-r--r--askbot/templates/question/closed_question_info.html8
-rw-r--r--askbot/templates/question/content.html6
-rw-r--r--askbot/templates/question/javascript.html12
-rw-r--r--askbot/templates/question/new_answer_form.html29
-rw-r--r--askbot/templates/question/question_card.html5
-rw-r--r--askbot/templates/question/question_comments.html3
-rw-r--r--askbot/templates/question/sidebar.html15
-rw-r--r--askbot/templates/question_edit.html20
-rw-r--r--askbot/templates/question_retag.html8
-rw-r--r--askbot/templates/reopen.html14
-rw-r--r--askbot/templates/revisions.html2
-rw-r--r--askbot/templates/search/indexes/auth/user_text.txt2
-rw-r--r--askbot/templates/tags/form_bulk_tag_subscription.html2
-rw-r--r--askbot/templates/user_profile/user.html3
-rw-r--r--askbot/templates/user_profile/user_answers_list.html14
-rw-r--r--askbot/templates/user_profile/user_edit.html4
-rw-r--r--askbot/templates/user_profile/user_email_subscriptions.html27
-rw-r--r--askbot/templates/user_profile/user_favorites.html4
-rw-r--r--askbot/templates/user_profile/user_info.html10
-rw-r--r--askbot/templates/user_profile/user_moderate.html8
-rw-r--r--askbot/templates/user_profile/user_stats.html13
-rw-r--r--askbot/templates/user_profile/user_tabs.html11
-rw-r--r--askbot/templates/user_profile/users_answers.html29
-rw-r--r--askbot/templates/user_profile/users_questions.html20
-rw-r--r--askbot/templates/widgets/answer_edit_tips.html20
-rw-r--r--askbot/templates/widgets/ask_button.html8
-rw-r--r--askbot/templates/widgets/ask_form.html10
-rw-r--r--askbot/templates/widgets/group_info.html6
-rw-r--r--askbot/templates/widgets/header.html5
-rw-r--r--askbot/templates/widgets/question_edit_tips.html33
-rw-r--r--askbot/templates/widgets/question_summary.html11
-rw-r--r--askbot/templates/widgets/scope_nav.html8
-rw-r--r--askbot/templates/widgets/secondary_header.html43
-rw-r--r--askbot/templates/widgets/system_messages.html14
-rw-r--r--askbot/templates/widgets/tag_selector.html2
-rw-r--r--askbot/templates/widgets/three_column_category_selector.html2
-rw-r--r--askbot/templates/widgets/user_perms.html2
-rw-r--r--askbot/templatetags/extra_filters_jinja.py22
-rw-r--r--askbot/templatetags/extra_tags.py3
-rw-r--r--askbot/tests/__init__.py1
-rw-r--r--askbot/tests/db_api_tests.py8
-rw-r--r--askbot/tests/email_alert_tests.py2
-rw-r--r--askbot/tests/email_parsing_tests.py28
-rw-r--r--askbot/tests/form_tests.py3
-rw-r--r--askbot/tests/jive_tests.py280
-rw-r--r--askbot/tests/page_load_tests.py2
-rw-r--r--askbot/tests/permission_assertion_tests.py26
-rw-r--r--askbot/tests/post_model_tests.py2
-rw-r--r--askbot/tests/user_model_tests.py3
-rw-r--r--askbot/tests/utils.py13
-rw-r--r--askbot/tests/utils_tests.py7
-rw-r--r--askbot/tests/widget_tests.py5
-rw-r--r--askbot/urls.py57
-rw-r--r--askbot/user_messages/context_processors.py25
-rw-r--r--askbot/utils/console.py2
-rw-r--r--askbot/utils/decorators.py6
-rw-r--r--askbot/utils/file_utils.py19
-rw-r--r--askbot/utils/forms.py2
-rw-r--r--askbot/utils/get_plurals.py22
-rw-r--r--askbot/utils/html.py53
-rwxr-xr-xaskbot/utils/jive.py462
-rw-r--r--askbot/utils/markup.py4
-rw-r--r--askbot/utils/pluralization.py144
-rw-r--r--askbot/utils/slug.py8
-rw-r--r--askbot/utils/transaction.py20
-rw-r--r--askbot/utils/translation.py3
-rw-r--r--askbot/utils/url_utils.py5
-rw-r--r--askbot/views/api_v1.py10
-rw-r--r--askbot/views/commands.py67
-rw-r--r--askbot/views/context.py4
-rw-r--r--askbot/views/meta.py60
-rw-r--r--askbot/views/readers.py84
-rw-r--r--askbot/views/users.py110
-rw-r--r--askbot/views/widgets.py8
-rw-r--r--askbot/views/writers.py147
-rw-r--r--askbot_requirements.txt4
-rw-r--r--askbot_requirements_dev.txt4
243 files changed, 12194 insertions, 3629 deletions
diff --git a/.gitignore b/.gitignore
index 011d7bde..296c3c4f 100755
--- a/.gitignore
+++ b/.gitignore
@@ -17,6 +17,8 @@ settings.py
*.iml
lint
env
+.vagrant
+/Vagrantfile
/custom_settings
/static
django
diff --git a/askbot/__init__.py b/askbot/__init__.py
index 19c3953f..0e4ba506 100644
--- a/askbot/__init__.py
+++ b/askbot/__init__.py
@@ -13,7 +13,7 @@ VERSION = (0, 7, 49)
#values - the package qualifier to use for pip
REQUIREMENTS = {
'akismet': 'akismet',
- 'django': 'django>=1.3.1,<=1.5',
+ 'django': 'django>=1.3.1,<1.6',
'compressor': 'django-compressor==1.2',
'jinja2': 'Jinja2',
'coffin': 'Coffin>=0.3',
@@ -27,13 +27,13 @@ REQUIREMENTS = {
'sanction': 'sanction==0.3.1',
'unidecode': 'unidecode',
'django_countries': 'django-countries==1.0.5',
- 'djcelery': 'django-celery==3.0.11',
+ 'djcelery': 'django-celery>=3.0.11',
'djkombu': 'django-kombu==0.9.4',
'followit': 'django-followit',
'recaptcha_works': 'django-recaptcha-works',
'openid': 'python-openid',
'pystache': 'pystache==0.3.1',
- 'pytz': 'pytz',
+ 'pytz': 'pytz==2013b',
'tinymce': 'django-tinymce==1.5.1b2',
'longerusername': 'longerusername',
'bs4': 'beautifulsoup4',
diff --git a/askbot/conf/__init__.py b/askbot/conf/__init__.py
index 0f92bf99..c1ed6ef0 100644
--- a/askbot/conf/__init__.py
+++ b/askbot/conf/__init__.py
@@ -28,6 +28,7 @@ import askbot.conf.badges
import askbot.conf.login_providers
import askbot.conf.access_control
import askbot.conf.site_modes
+import askbot.conf.words
#import main settings object
from askbot.conf.settings_wrapper import settings
diff --git a/askbot/conf/access_control.py b/askbot/conf/access_control.py
index 0e3dcb54..c339da6d 100644
--- a/askbot/conf/access_control.py
+++ b/askbot/conf/access_control.py
@@ -1,6 +1,8 @@
from askbot.conf.settings_wrapper import settings
from askbot.conf.super_groups import LOGIN_USERS_COMMUNICATION
from askbot.deps import livesettings
+from askbot.deps.livesettings import BooleanValue
+from askbot.deps.livesettings import StringValue
from django.utils.translation import ugettext_lazy as _
ACCESS_CONTROL = livesettings.ConfigurationGroup(
@@ -10,6 +12,26 @@ ACCESS_CONTROL = livesettings.ConfigurationGroup(
)
settings.register(
+ BooleanValue(
+ ACCESS_CONTROL,
+ 'READ_ONLY_MODE_ENABLED',
+ default=False,
+ description=_('Make site read-only'),
+ )
+)
+
+settings.register(
+ StringValue(
+ ACCESS_CONTROL,
+ 'READ_ONLY_MESSAGE',
+ default=_(
+ 'The site is temporarily read-only. '
+ 'Only viewing of the content is possible at the moment.'
+ )
+ )
+)
+
+settings.register(
livesettings.BooleanValue(
ACCESS_CONTROL,
'ASKBOT_CLOSED_FORUM_MODE',
diff --git a/askbot/conf/email.py b/askbot/conf/email.py
index 41b6d00b..088e0590 100644
--- a/askbot/conf/email.py
+++ b/askbot/conf/email.py
@@ -55,6 +55,16 @@ settings.register(
)
settings.register(
+ livesettings.BooleanValue(
+ EMAIL,
+ 'HTML_EMAIL_ENABLED',
+ default=True,
+ description=_('Enable HTML-formatted email'),
+ help_text=_('May not be supported by some email clients')
+ )
+)
+
+settings.register(
livesettings.IntegerValue(
EMAIL,
'MAX_ALERTS_PER_EMAIL',
diff --git a/askbot/conf/external_keys.py b/askbot/conf/external_keys.py
index 3837b7ae..989cbf46 100644
--- a/askbot/conf/external_keys.py
+++ b/askbot/conf/external_keys.py
@@ -53,8 +53,6 @@ settings.register(
)
)
-
-
settings.register(
livesettings.StringValue(
EXTERNAL_KEYS,
diff --git a/askbot/conf/forum_data_rules.py b/askbot/conf/forum_data_rules.py
index caa93563..cd138a88 100644
--- a/askbot/conf/forum_data_rules.py
+++ b/askbot/conf/forum_data_rules.py
@@ -84,7 +84,7 @@ settings.register(
FORUM_DATA_RULES,
'ALLOW_ASK_ANONYMOUSLY',
default=True,
- description=_('Allow asking questions anonymously'),
+ description=_('Allow logged in users ask anonymously'),
help_text=_(
'Users do not accrue reputation for anonymous questions '
'and their identity is not revealed until they change their '
@@ -96,6 +96,16 @@ settings.register(
settings.register(
livesettings.BooleanValue(
FORUM_DATA_RULES,
+ 'ALLOW_ASK_UNREGISTERED',
+ default=False,
+ description=_('Allow asking without registration'),
+ help_text=_('Enabling ReCaptcha is recommended with this feature')
+ )
+)
+
+settings.register(
+ livesettings.BooleanValue(
+ FORUM_DATA_RULES,
'ALLOW_POSTING_BEFORE_LOGGING_IN',
default = True,
description = _('Allow posting before logging in'),
diff --git a/askbot/conf/group_settings.py b/askbot/conf/group_settings.py
index 804b5502..cacbdacb 100644
--- a/askbot/conf/group_settings.py
+++ b/askbot/conf/group_settings.py
@@ -19,6 +19,7 @@ settings.register(
)
)
+"""
def group_name_update_callback(old_name, new_name):
from askbot.models.tag import clean_group_name
from askbot.models import Group
@@ -32,6 +33,7 @@ def group_name_update_callback(old_name, new_name):
group.name = cleaned_new_name
group.save()
return new_name
+"""
settings.register(
@@ -41,7 +43,7 @@ settings.register(
default = _('everyone'),
description = _('Global user group name'),
help_text = _('All users belong to this group automatically'),
- update_callback=group_name_update_callback
+ #update_callback=group_name_update_callback
)
)
diff --git a/askbot/conf/login_providers.py b/askbot/conf/login_providers.py
index 36f71502..beb56a72 100644
--- a/askbot/conf/login_providers.py
+++ b/askbot/conf/login_providers.py
@@ -23,6 +23,8 @@ settings.register(
)
)
+#todo: remove this - we don't want the local login button
+#but instead always show the login/password field when used
settings.register(
livesettings.BooleanValue(
LOGIN_PROVIDERS,
@@ -62,6 +64,59 @@ settings.register(
)
)
+settings.register(
+ livesettings.BooleanValue(
+ LOGIN_PROVIDERS,
+ 'SIGNIN_CUSTOM_OPENID_ENABLED',
+ default=False,
+ description=_('Enable custom OpenID login')
+ )
+)
+
+settings.register(
+ livesettings.StringValue(
+ LOGIN_PROVIDERS,
+ 'SIGNIN_CUSTOM_OPENID_NAME',
+ default=_('Custom OpenID'),
+ description=_('Short name for the custom OpenID provider')
+ )
+)
+
+CUSTOM_OPENID_MODE_CHOICES = (
+ ('openid-direct', _('Direct button login')),
+ ('openid-username', _('Requires username'))
+)
+
+settings.register(
+ livesettings.StringValue(
+ LOGIN_PROVIDERS,
+ 'SIGNIN_CUSTOM_OPENID_MODE',
+ default='openid-direct',
+ description=_('Type of OpenID login'),
+ choices=CUSTOM_OPENID_MODE_CHOICES
+ )
+)
+
+settings.register(
+ livesettings.ImageValue(
+ LOGIN_PROVIDERS,
+ 'SIGNIN_CUSTOM_OPENID_LOGIN_BUTTON',
+ default='/images/logo.gif',
+ description=_('Upload custom OpenID icon'),
+ url_resolver=skin_utils.get_media_url
+ )
+)
+
+settings.register(
+ livesettings.StringValue(
+ LOGIN_PROVIDERS,
+ 'SIGNIN_CUSTOM_OPENID_ENDPOINT',
+ default='http://example.com',
+ description=_('Custom OpenID endpoint'),
+ help_text=_('Important: with the "username" mode must have a %%(username)s placeholder e.g. http://example.com/%%(username)s/'),
+ )
+)
+
providers = (
'local',
'AOL',
@@ -70,6 +125,7 @@ providers = (
'Facebook',
'Flickr',
'Google',
+ 'Mozilla Persona',
'Twitter',
'LinkedIn',
'LiveJournal',
@@ -81,7 +137,7 @@ providers = (
'Verisign',
'Yahoo',
'identi.ca',
- 'LaunchPad'
+ 'LaunchPad',
)
DISABLED_BY_DEFAULT = ('LaunchPad',)
@@ -105,7 +161,7 @@ for provider in providers:
'in the "External keys" section'
) % {'provider': provider}
- setting_name = 'SIGNIN_%s_ENABLED' % provider.upper()
+ setting_name = 'SIGNIN_%s_ENABLED' % provider.upper().replace(' ', '_')
settings.register(
livesettings.BooleanValue(
LOGIN_PROVIDERS,
diff --git a/askbot/conf/minimum_reputation.py b/askbot/conf/minimum_reputation.py
index fd4df4b3..d0db5891 100644
--- a/askbot/conf/minimum_reputation.py
+++ b/askbot/conf/minimum_reputation.py
@@ -17,6 +17,16 @@ MIN_REP = livesettings.ConfigurationGroup(
settings.register(
livesettings.IntegerValue(
MIN_REP,
+ 'MIN_REP_TO_AUTOAPPROVE_USER',
+ default=10,
+ description=_('Become approved'),
+ help_text=_('Approved users bypass moderation and skip recaptcha')
+ )
+)
+
+settings.register(
+ livesettings.IntegerValue(
+ MIN_REP,
'MIN_REP_TO_VOTE_UP',
default=5,
description=_('Upvote')
@@ -132,15 +142,6 @@ settings.register(
settings.register(
livesettings.IntegerValue(
MIN_REP,
- 'MIN_REP_TO_CLOSE_OWN_QUESTIONS',
- default=25,
- description=_('Close own questions'),
- )
-)
-
-settings.register(
- livesettings.IntegerValue(
- MIN_REP,
'MIN_REP_TO_RETAG_OTHERS_QUESTIONS',
default=50,
description=_('Retag questions posted by other people')
@@ -148,15 +149,6 @@ settings.register(
)
settings.register(
- livesettings.IntegerValue(
- MIN_REP,
- 'MIN_REP_TO_REOPEN_OWN_QUESTIONS',
- default=50,
- description=_('Reopen own questions')
- )
-)
-
-settings.register(
livesettings.IntegerValue(
MIN_REP,
'MIN_REP_TO_EDIT_WIKI',
@@ -188,7 +180,7 @@ settings.register(
MIN_REP,
'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS',
default=200,
- description=_('Close questions asked by others')
+ description=_('Close and reopen questions')
)
)
diff --git a/askbot/conf/settings_wrapper.py b/askbot/conf/settings_wrapper.py
index 0a4ba45f..7fc9540d 100644
--- a/askbot/conf/settings_wrapper.py
+++ b/askbot/conf/settings_wrapper.py
@@ -20,6 +20,7 @@ at run time
askbot.deps.livesettings is a module developed for satchmo project
"""
+from django.conf import settings as django_settings
from django.core.cache import cache
from askbot.deps.livesettings import SortedDotDict, config_register
from askbot.deps.livesettings.functions import config_get
@@ -47,7 +48,11 @@ class ConfigSettings(object):
will be required in code to convert an app
depending on django.conf.settings to askbot.deps.livesettings
"""
- return getattr(self.__instance, key).value
+ hardcoded_setting = getattr(django_settings, 'ASKBOT_' + key, None)
+ if hardcoded_setting is None:
+ return getattr(self.__instance, key).value
+ else:
+ return hardcoded_setting
def get_default(self, key):
"""return the defalut value for the setting"""
@@ -93,24 +98,39 @@ class ConfigSettings(object):
self.__group_map[key] = group_key
def as_dict(self):
- settings = cache.get('askbot-livesettings')
+ cache_key = get_bulk_cache_key()
+ settings = cache.get(cache_key)
if settings:
return settings
else:
- self.prime_cache()
- return cache.get('askbot-livesettings')
+ self.prime_cache(cache_key)
+ return cache.get(cache_key)
@classmethod
- def prime_cache(cls, **kwargs):
+ def prime_cache(cls, cache_key, **kwargs):
"""reload all settings into cache as dictionary
"""
out = dict()
for key in cls.__instance.keys():
#todo: this is odd that I could not use self.__instance.items() mapping here
- out[key] = cls.__instance[key].value
- cache.set('askbot-livesettings', out)
+ hardcoded_setting = getattr(django_settings, 'ASKBOT_' + key, None)
+ if hardcoded_setting is None:
+ out[key] = cls.__instance[key].value
+ else:
+ out[key] = hardcoded_setting
+ cache.set(cache_key, out)
+
+
+def get_bulk_cache_key():
+ from askbot.utils.translation import get_language
+ return 'askbot-settings-' + get_language()
+
+
+def prime_cache_handler(*args, **kwargs):
+ cache_key = get_bulk_cache_key()
+ ConfigSettings.prime_cache(cache_key)
-signals.configuration_value_changed.connect(ConfigSettings.prime_cache)
+signals.configuration_value_changed.connect(prime_cache_handler)
#settings instance to be used elsewhere in the project
settings = ConfigSettings()
diff --git a/askbot/conf/site_modes.py b/askbot/conf/site_modes.py
index feadd32b..29e537db 100644
--- a/askbot/conf/site_modes.py
+++ b/askbot/conf/site_modes.py
@@ -6,7 +6,8 @@ Site modes settings:
"""
from askbot.conf.settings_wrapper import settings
from askbot.conf.super_groups import REP_AND_BADGES
-from askbot.deps.livesettings import ConfigurationGroup, BooleanValue
+from askbot.deps.livesettings import ConfigurationGroup
+from askbot.deps.livesettings import BooleanValue
from django.utils.translation import ugettext_lazy as _
LARGE_SITE_MODE_SETTINGS = {
@@ -20,9 +21,7 @@ LARGE_SITE_MODE_SETTINGS = {
'MIN_REP_TO_DELETE_OTHERS_COMMENTS': 2000,
'MIN_REP_TO_DELETE_OTHERS_POSTS': 5000,
'MIN_REP_TO_UPLOAD_FILES': 60,
- 'MIN_REP_TO_CLOSE_OWN_QUESTIONS': 250,
'MIN_REP_TO_RETAG_OTHERS_QUESTIONS': 500,
- 'MIN_REP_TO_REOPEN_OWN_QUESTIONS': 500,
'MIN_REP_TO_EDIT_WIKI': 750,
'MIN_REP_TO_EDIT_OTHERS_POSTS': 2000,
'MIN_REP_TO_VIEW_OFFENSIVE_FLAGS': 2000,
diff --git a/askbot/conf/site_settings.py b/askbot/conf/site_settings.py
index 805cc5dc..0ac5b081 100644
--- a/askbot/conf/site_settings.py
+++ b/askbot/conf/site_settings.py
@@ -7,8 +7,11 @@ from askbot.conf.super_groups import CONTENT_AND_UI
from askbot.deps import livesettings
from django.utils.translation import ugettext_lazy as _
from django.conf import settings as django_settings
+from django.core.validators import ValidationError, validate_email
+import re
from urlparse import urlparse
+
QA_SITE_SETTINGS = livesettings.ConfigurationGroup(
'QA_SITE_SETTINGS',
_('URLS, keywords & greetings'),
@@ -132,3 +135,30 @@ settings.register(
)
)
)
+
+def feedback_emails_callback(old_value, new_value):
+ """validates the fedback emails list"""
+ emails = []
+ for value in re.split('\s*,\s*', new_value):
+ if not value:
+ continue
+ try:
+ validate_email(value)
+ emails.append(value)
+ except ValidationError:
+ raise ValueError(
+ _("'%(value)s' is not a valid email") % {'value': value})
+ return ", ".join(emails)
+
+settings.register(
+ livesettings.StringValue(
+ QA_SITE_SETTINGS,
+ 'FEEDBACK_EMAILS',
+ description=_('Internal feedback form email recipients'),
+ help_text=_(
+ 'Comma separated list. If left empty, feedback mails are sent '
+ 'to admins and moderators'
+ ),
+ update_callback=feedback_emails_callback
+ )
+)
diff --git a/askbot/conf/user_settings.py b/askbot/conf/user_settings.py
index a2d8d386..c8e879b5 100644
--- a/askbot/conf/user_settings.py
+++ b/askbot/conf/user_settings.py
@@ -8,6 +8,7 @@ from django.conf import settings as django_settings
from askbot.skins import utils as skin_utils
from django.utils.translation import ugettext_lazy as _
from askbot import const
+import re
USER_SETTINGS = livesettings.ConfigurationGroup(
'USER_SETTINGS',
@@ -37,8 +38,17 @@ settings.register(
livesettings.BooleanValue(
USER_SETTINGS,
'EDITABLE_SCREEN_NAME',
- default = True,
- description = _('Allow editing user screen name')
+ default=True,
+ description=_('Allow editing user screen name')
+ )
+)
+
+settings.register(
+ livesettings.BooleanValue(
+ USER_SETTINGS,
+ 'SHOW_ADMINS_PRIVATE_USER_DATA',
+ default=False,
+ description=_('Show email addresses to moderators')
)
)
@@ -112,6 +122,23 @@ settings.register(
)
)
+def gravatar_url_callback(old, new):
+ """strips trailing slash"""
+ url_re = re.compile(r'([^/]*)/+$')
+ return url_re.sub(r'\1', new)
+
+settings.register(
+ livesettings.StringValue(
+ USER_SETTINGS,
+ 'GRAVATAR_BASE_URL',
+ description=_(
+ 'Base URL for the gravatar service'
+ ),
+ default='//www.gravatar.com/avatar',
+ update_callback=gravatar_url_callback
+ )
+)
+
settings.register(
livesettings.BooleanValue(
USER_SETTINGS,
@@ -124,7 +151,6 @@ settings.register(
)
)
-
settings.register(
livesettings.StringValue(
USER_SETTINGS,
diff --git a/askbot/conf/words.py b/askbot/conf/words.py
new file mode 100644
index 00000000..be4eb191
--- /dev/null
+++ b/askbot/conf/words.py
@@ -0,0 +1,912 @@
+"""
+General skin settings
+"""
+from askbot.conf.settings_wrapper import settings
+from askbot.deps.livesettings import ConfigurationGroup
+from askbot.deps.livesettings import values
+from django.utils.translation import ugettext_lazy as _
+from askbot.skins import utils as skin_utils
+from askbot import const
+from askbot.conf.super_groups import CONTENT_AND_UI
+
+WORDS = ConfigurationGroup(
+ 'WORDS',
+ _('Site terms vocabulary'),
+ super_group = CONTENT_AND_UI
+ )
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASK_YOUR_QUESTION',
+ default=_('Ask Your Question'),
+ description=_('Ask Your Question'),
+ help_text=_('Used on a button')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_PLEASE_ENTER_YOUR_QUESTION',
+ default=_('Please enter your question'),
+ description=_('Please enter your question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASK_THE_GROUP',
+ default=_('Ask the Group'),
+ description=_('Ask the Group'),
+ help_text=_('Used on a button')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_POST_YOUR_ANSWER',
+ default=_('Post Your Answer'),
+ description=_('Post Your Answer'),
+ help_text=_('Used on a button')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWER_YOUR_OWN_QUESTION',
+ default=_('Answer Your Own Question'),
+ description=_('Answer Your Own Question'),
+ help_text=_('Used on a button')
+ )
+)
+
+settings.register(
+ values.LongStringValue(
+ WORDS,
+ 'WORDS_INSTRUCTION_TO_ANSWER_OWN_QUESTION',
+ default=_(
+ '<span class="big strong">You are welcome to answer your own question</span>, '
+ 'but please make sure to give an <strong>answer</strong>. '
+ 'Remember that you can always <strong>revise your original question</strong>.'
+ ),
+ description=_('Instruction to answer own questions'),
+ help_text=_('HTML is allowed')
+ )
+)
+
+settings.register(
+ values.LongStringValue(
+ WORDS,
+ 'WORDS_INSTRUCTION_TO_POST_ANONYMOUSLY',
+ default=_(
+ '<span class="strong big">Please start posting anonymously</span> - '
+ 'your entry will be published after you log in or create a new account.'
+ ),
+ description=_('Instruction to post anonymously'),
+ help_text=_('HTML is allowed')
+ )
+)
+
+settings.register(
+ values.LongStringValue(
+ WORDS,
+ 'WORDS_INSTRUCTION_TO_GIVE_ANSWERS',
+ default=_(
+ 'Please try to <strong>give a substantial answer</strong>, '
+ 'for discussions, <strong>please use comments</strong> and '
+ '<strong>do remember to vote</strong>.'
+ ),
+ description=_('Instruction to give answers'),
+ help_text=_('HTML is allowed')
+ )
+)
+
+settings.register(
+ values.LongStringValue(
+ WORDS,
+ 'WORDS_INSTRUCTION_FOR_THE_CATEGORY_SELECTOR',
+ default=_(
+ 'Categorize your question using this tag selector or '
+ 'entering text in tag box.'
+ ),
+ description=_('Instruction for the catogory selector'),
+ help_text=_('Plain text only')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_EDIT_YOUR_PREVIOUS_ANSWER',
+ default=_('Edit Your Previous Answer'),
+ description=_('Edit Your Previous Answer'),
+ help_text=_('Used on a button')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASK_QUESTIONS',
+ default=_('ask questions'),
+ description=_('ask questions')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASKED',
+ default=_('asked'),
+ description=_('asked'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASKED_FIRST_QUESTION',
+ default=_('Asked first question'),
+ description=_('Asked first question')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASKED_BY_ME',
+ default=_('Asked by me'),
+ description=_('Asked by me')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASKED_A_QUESTION',
+ default=_('Asked a question'),
+ description=_('Asked a question')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWERED_A_QUESTION',
+ default=_('Answered a question'),
+ description=_('Answered a question')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWERED_BY_ME',
+ default=_('Answered by me'),
+ description=_('Answered by me')
+ )
+)
+
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ACCEPTED_AN_ANSWER',
+ default=_('accepted an answer'),
+ description=_('accepted an answer')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GAVE_ACCEPTED_ANSWER',
+ default=_('Gave accepted answer'),
+ description=_('Gave accepted answer')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWERED',
+ default=_('answered'),
+ description=_('answered'),
+ )
+)
+
+settings.register(
+ values.LongStringValue(
+ WORDS,
+ 'WORDS_QUESTIONS_COUNTABLE_FORMS',
+ default='question\nquestions',
+ description=_('Countable plural forms for "queston"'),
+ help_text=_('Enter one form per line, pay attention')
+ )
+)
+
+settings.register(
+ values.LongStringValue(
+ WORDS,
+ 'WORDS_ANSWERS_COUNTABLE_FORMS',
+ default='answer\nanswers',
+ description=_('Countable plural forms for "answer"'),
+ help_text=_('Enter one form per line, pay attention')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_QUESTION_SINGULAR',
+ default=_('question'),
+ description=_('question (noun, singular)'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_QUESTION_PLURAL',
+ default=_('questions'),
+ description=_('questions (noun, plural)'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_UNANSWERED_QUESTION_SINGULAR',
+ default=_('unanswered question'),
+ description=_('unanswered question (singular)'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_UNANSWERED_QUESTION_PLURAL',
+ default=_('unanswered questions'),
+ description=_('unanswered questions (plural)'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWER_SINGULAR',
+ default=_('answer'),
+ description=_('answer (noun, sungular)'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_QUESTION_VOTED_UP',
+ default=_('Question voted up'),
+ description=_('Question voted up'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWER_VOTED_UP',
+ default=_('Answer voted up'),
+ description=_('Answer voted up'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_UPVOTED_ANSWER',
+ default=_('upvoted answer'),
+ description=_('upvoted answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_NICE_ANSWER',
+ default=_('Nice Answer'),
+ description=_('Nice Answer'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_NICE_QUESTION',
+ default=_('Nice Question'),
+ description=_('Nice Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GOOD_ANSWER',
+ default=_('Good Answer'),
+ description=_('Good Answer'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GOOD_QUESTION',
+ default=_('Good Question'),
+ description=_('Good Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GREAT_ANSWER',
+ default=_('Great Answer'),
+ description=_('Great Answer'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GREAT_QUESTION',
+ default=_('Great Question'),
+ description=_('Great Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_POPULAR_QUESTION',
+ default=_('Popular Question'),
+ description=_('Popular Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_NOTABLE_QUESTION',
+ default=_('Notable Question'),
+ description=_('Notable Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_FAMOUS_QUESTION',
+ default=_('Famous Question'),
+ description=_('Famous Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_STELLAR_QUESTION',
+ default=_('Stellar Question'),
+ description=_('Stellar Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_FAVORITE_QUESTION',
+ default=_('Favorite Question'),
+ description=_('Favorite Question'),
+ help_text='Badge name'
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_UPVOTED_ANSWERS',
+ default=_('upvoted answers'),
+ description=_('upvoted answers'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_SHOW_ONLY_QUESTIONS_FROM',
+ default=_('Show only questions from'),
+ description=_('Show only questions from'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_PLEASE_ASK_YOUR_QUESTION_HERE',
+ default=_('Please ask your question here'),
+ description=_('Please ask your question here'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_THIS_QUESTION_HAS_BEEN_DELETED',
+ default=_(
+ 'Sorry, this question has been '
+ 'deleted and is no longer accessible'
+ ),
+ description=_('This question has been deleted')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_PLEASE_ENTER_YOUR_QUESTION',
+ default=_('Please enter your question'),
+ description=_('Please enter your question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_DELETE_YOUR_QUESTION',
+ default=_('delete your question'),
+ description=_('delete your question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ASK_A_QUESTION_INTERESTING_TO_THIS_COMMUNITY',
+ default=_('ask a question interesting to this community'),
+ description=_('ask a question interesting to this community'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_NO_QUESTIONS_HERE',
+ default=_('No questions here.'),
+ description=_('No questions here.'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_PLEASE_FOLLOW_QUESTIONS',
+ default=_('Please follow some questions or follow some users.'),
+ description=_('Please follow some questions or follow some users.'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_PLEASE_FEEL_FREE_TO_ASK_YOUR_QUESTION',
+ default=_('Please feel free to ask your question!'),
+ description=_('Please feel free to ask your question!'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_SWAP_WITH_QUESTION',
+ default=_('swap with question'),
+ description=_('swap with question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_REPOST_AS_A_QUESTION_COMMENT',
+ default=_('repost as a question comment'),
+ description=_('repost as a question comment'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ONLY_ONE_ANSWER_PER_USER_IS_ALLOWED',
+ default=_('(only one answer per user is allowed)'),
+ description=_('Only one answer per user is allowed'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ACCEPT_BEST_ANSWERS_FOR_YOUR_QUESTIONS',
+ default=_('Accept the best answers for your questions'),
+ description=_('Accept the best answers for your questions')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_AUTHOR_OF_THE_QUESTION',
+ default=_('author of the question'),
+ description=_('author of the question')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ACCEPT_OR_UNACCEPT_THE_BEST_ANSWER',
+ default=_('accept or unaccept the best answer'),
+ description=_('accept or unaccept the best answer')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ACCEPT_OR_UNACCEPT_OWN_ANSWER',
+ default=_('accept or unaccept your own answer'),
+ description=_('accept or unaccept your own answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_YOU_ALREADY_GAVE_AN_ANSWER',
+ default=_('you already gave an answer'),
+ description=_('you already gave an answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GAVE_AN_ANSWER',
+ default=_('gave an answer'),
+ description=_('gave an answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWER_OWN_QUESTIONS',
+ default=_('answer own questions'),
+ description=_('answer own questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWERED_OWN_QUESTION',
+ default=_('Answered own question'),
+ description=_('Answered own question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_REPOST_AS_A_COMMENT_UNDER_THE_OLDER_ANSWER',
+ default=_('repost as a comment under older answer'),
+ description=_('repost as a comment under older answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_INVITE_OTHERS_TO_HELP_ANSWER_THIS_QUESTION',
+ default=_('invite other to help answer this question'),
+ description=_('invite other to help answer this question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_RELATED_QUESTIONS',
+ default=_('Related questions'),
+ description=_('Related questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_QUESTION_TOOLS',
+ default=_('Question Tools'),
+ description=_('Question Tools'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_THIS_QUESTION_IS_CURRENTLY_SHARED_ONLY_WITH',
+ default=_('Phrase: this question is currently shared only with:'),
+ description=_('Phrase: this question is currently shared only with:'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_BE_THE_FIRST_TO_ANSWER_THIS_QUESTION',
+ default=_('Be the first one to answer this question!'),
+ description=_('Be the first one to answer this question!'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_FOLLOWED_QUESTIONS',
+ default=_('followed questions'),
+ description=_('followed questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_FOLLOW_QUESTIONS',
+ default=_('follow questions'),
+ description=_('follow questions'),
+ help_text=_('Indefinite form')
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_COMMENTS_AND_ANSWERS_TO_OTHERS_QUESTIONS',
+ default = '',
+ description = _('Phrase: comments and answers to others questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_YOU_CAN_POST_QUESTIONS_BY_EMAILING_THEM_AT',
+ default=_('You can post questions by emailing them at'),
+ description=_('You can post questions by emailing them at'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_LIST_OF_QUESTIONS',
+ default=_('List of questions'),
+ description=_('List of questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_COMMUNITY_GIVES_YOU_AWARDS',
+ default=_('Community gives you awards for your questions, answers and votes'),
+ description=_('Community gives you awards for your questions, answers and votes'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_CLOSE_QUESTION',
+ default=_('Close question'),
+ description=_('Close question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_CLOSE_QUESTIONS',
+ default=_('close questions'),
+ description=_('close questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_EDIT_QUESTION',
+ default=_('Edit question'),
+ description=_('Edit question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_QUESTION_IN_ONE_SENTENCE',
+ default=_('Question - in one sentence'),
+ description=_('Question - in one sentence'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_RETAG_QUESTION',
+ default=_('Retag question'),
+ description=_('Retag question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_RETAG_QUESTIONS',
+ default=_('retag questions'),
+ description=_('retag questions'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_REOPEN_QUESTION',
+ default=_('Reopen question'),
+ description=_('Reopen question'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_THERE_ARE_NO_UNANSWERED_QUESTIONS_HERE',
+ default=_('There are no unanswered questions here'),
+ description=_('There are no unanswered questions here'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_THIS_ANSWER_HAS_BEEN_SELECTED_AS_CORRECT',
+ default=_('this answer has been selected as correct'),
+ description=_('this answer has been selected as correct'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_MARK_THIS_ANSWER_AS_CORRECT',
+ default=_('mark this answer as correct'),
+ description=_('mark this answer as correct'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_LOGIN_SIGNUP_TO_ANSWER',
+ default=_('Login/Signup to Answer'),
+ description=_('Login/Signup to Answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_YOUR_ANSWER',
+ default=_('Your Answer'),
+ description=_('Your Answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ADD_ANSWER',
+ default=_('Add Answer'),
+ description=_('Add Answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GIVE_AN_ANSWER_INTERESTING_TO_THIS_COMMUNITY',
+ default=_('give an answer interesting to this community'),
+ description=_('give an answer interesting to this community'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GIVE_AN_ANSWER_INTERESTING_TO_THIS_COMMUNITY',
+ default=_('give an answer interesting to this community'),
+ description=_('give an answer interesting to this community'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_GIVE_A_GOOD_ANSWER',
+ default=_('give a substantial answer'),
+ description=_('give a substantial answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_TRY_TO_GIVE_AN_ANSWER',
+ default=_('try to give an answer, rather than engage into a discussion'),
+ description=_('try to give an answer, rather than engage into a discussion'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_SHOW_ONLY_SELECTED_ANSWERS_TO_ENQUIRERS',
+ default=_('show only selected answers to enquirers'),
+ description=_('show only selected answers to enquirers'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_UNANSWERED',
+ default = _('UNANSWERED'),
+ description = _('UNANSWERED'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_EDIT_ANSWER',
+ default=_('Edit Answer'),
+ description=_('Edit Answer'),
+ )
+)
+
+settings.register(
+ values.StringValue(
+ WORDS,
+ 'WORDS_ANSWERED',
+ default=_('Answered'),
+ description=_('Answered'),
+ )
+)
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py
index 31a631d9..56da1c95 100644
--- a/askbot/const/__init__.py
+++ b/askbot/const/__init__.py
@@ -7,6 +7,7 @@ text in this project, all unicode text go here.
from django.utils.translation import ugettext_lazy as _
import re
+#todo: customize words
CLOSE_REASONS = (
(1, _('duplicate question')),
(2, _('question is off-topic or not relevant')),
@@ -122,6 +123,7 @@ DEFAULT_ANSWER_SORT_METHOD = 'votes'
#of Q.run_advanced_search
DEFAULT_POST_SORT_METHOD = 'activity-desc'
+#todo: customize words
POST_SCOPE_LIST = (
('all', _('all')),
('unanswered', _('unanswered')),
@@ -136,6 +138,7 @@ TAG_LIST_FORMAT_CHOICES = (
PAGE_SIZE_CHOICES = (('10', '10',), ('30', '30',), ('50', '50',),)
ANSWERS_PAGE_SIZE = 10
+USER_POSTS_PAGE_SIZE = 10
QUESTIONS_PER_PAGE_USER_CHOICES = ((10, u'10'), (30, u'30'), (50, u'50'),)
UNANSWERED_QUESTION_MEANING_CHOICES = (
@@ -154,11 +157,16 @@ UNANSWERED_QUESTION_MEANING_CHOICES = (
#to do full string match
#IMPRTANT: tag related regexes must be portable between js and python
TAG_CHARS = r'\w+.#-'
-TAG_REGEX_BARE = r'[%s]+' % TAG_CHARS
+TAG_FIRST_CHARS = r'\w'
+TAG_FORBIDDEN_FIRST_CHARS = r'#'
+TAG_REGEX_BARE = r'%s[%s]+' % (TAG_FIRST_CHARS, TAG_CHARS)
TAG_REGEX = r'^%s$' % TAG_REGEX_BARE
-TAG_SPLIT_REGEX = r'[ ,]+'
+
+TAG_STRIP_CHARS = ', '
+TAG_SPLIT_REGEX = r'[%s]+' % TAG_STRIP_CHARS
TAG_SEP = ',' # has to be valid TAG_SPLIT_REGEX char and MUST NOT be in const.TAG_CHARS
#!!! see const.message_keys.TAG_WRONG_CHARS_MESSAGE
+
EMAIL_REGEX = re.compile(r'\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}\b', re.I)
TYPE_ACTIVITY_ASK_QUESTION = 1
@@ -300,13 +308,6 @@ assert(
== set(RESPONSE_ACTIVITY_TYPE_MAP_FOR_TEMPLATES.keys())
)
-TYPE_RESPONSE = {
- 'QUESTION_ANSWERED' : _('answered question'),
- 'QUESTION_COMMENTED': _('commented question'),
- 'ANSWER_COMMENTED' : _('commented answer'),
- 'ANSWER_ACCEPTED' : _('accepted answer'),
-}
-
POST_STATUS = {
'closed': _('[closed]'),
'deleted': _('[deleted]'),
@@ -432,12 +433,12 @@ AVATAR_STATUS_CHOICE = (
SEARCH_ORDER_BY = (
('-added_at', _('date descendant')),
('added_at', _('date ascendant')),
- ('-last_activity_at', _('activity descendant')),
- ('last_activity_at', _('activity ascendant')),
- ('-answer_count', _('answers descendant')),
- ('answer_count', _('answers ascendant')),
- ('-points', _('votes descendant')),
- ('points', _('votes ascendant')),
+ ('-last_activity_at', _('most recently active')),
+ ('last_activity_at', _('least recently active')),
+ ('-answer_count', _('more responses')),
+ ('answer_count', _('fewer responses')),
+ ('-points', _('more votes')),
+ ('points', _('less votes')),
)
DEFAULT_QUESTION_WIDGET_STYLE = """
diff --git a/askbot/const/message_keys.py b/askbot/const/message_keys.py
index 291381cb..bb990d5e 100644
--- a/askbot/const/message_keys.py
+++ b/askbot/const/message_keys.py
@@ -4,12 +4,6 @@ that are used as variables
it is important that a dummy _() function is used here
this way message key will be pulled into django.po
and can still be used as a variable in python files.
-
-In addition, some messages are repeated too many times
-in the code, so we need to be able to retreive them
-by a key. Therefore we have a function here, called
-get_i18n_message(). Possibly all messages included in
-this file could be implemented this way.
'''
_ = lambda v:v
@@ -40,18 +34,14 @@ TAGS_ARE_REQUIRED_MESSAGE = _('tags are required')
TAG_WRONG_CHARS_MESSAGE = _(
'please use letters, numbers and characters "-+.#"'
)
-
-def get_i18n_message(key):
- messages = {
- 'BLOCKED_USERS_CANNOT_POST': _(
- 'Sorry, your account appears to be blocked and you cannot make new posts '
- 'until this issue is resolved. Please contact the forum administrator to '
- 'reach a resolution.'
- ),
- 'SUSPENDED_USERS_CANNOT_POST': _(
- 'Sorry, your account appears to be suspended and you cannot make new posts '
- 'until this issue is resolved. You can, however edit your existing posts. '
- 'Please contact the forum administrator to reach a resolution.'
- )
- }
- return messages[key]
+TAG_WRONG_FIRST_CHAR_MESSAGE = _(
+ '# is not a valid character at the beginning of tags, use only letters and numbers'
+)
+ACCOUNT_CANNOT_PERFORM_ACTION = _(
+ 'Sorry, you cannot %(perform_action)s because %(your_account_is)s'
+)
+MIN_REP_REQUIRED_TO_PERFORM_ACTION = _('>%(min_rep)s points required to %(perform_action)s')
+CANNOT_PERFORM_ACTION_UNTIL = _('Sorry, you will be able to %(perform_action)s after %(until)s')
+MODERATORS_OR_AUTHOR_CAN_PEFROM_ACTION = _(
+ 'Sorry, only moderators or the %(post_author)s %(perform_action)s'
+)
diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py
index f719e811..20533040 100644
--- a/askbot/deps/django_authopenid/backends.py
+++ b/askbot/deps/django_authopenid/backends.py
@@ -37,6 +37,7 @@ class AuthBackend(object):
provider_name = None,#required with all except email_key
openid_url = None,
email_key = None,
+ email = None, # used with mozilla-persona method
oauth_user_id = None,#used with oauth
facebook_user_id = None,#user with facebook
wordpress_url = None, # required for self hosted wordpress
@@ -129,7 +130,20 @@ class AuthBackend(object):
'duplicate openid url in the database!!! %s' % openid_url
)
return None
-
+
+ elif method == 'mozilla-persona':
+ try:
+ assoc = UserAssociation.objects.get(
+ openid_url=email,
+ provider_name='mozilla-persona'
+ )
+ return assoc.user
+ except UserAssociation.DoesNotExist:
+ return None
+ except UserAssociation.MultipleObjectsReturned:
+ logging.critical(
+ 'duplicate user with mozilla persona %s!!!' % email
+ )
elif method == 'email':
#with this method we do no use user association
@@ -144,6 +158,24 @@ class AuthBackend(object):
except User.DoesNotExist:
return None
+ elif method == 'valid_email':
+ try:
+ user = User.objects.get(email=email)
+ except User.DoesNotExist:
+ return None
+ except User.MultipleObjectsReturned:
+ LOG.critical(
+ ('have more than one user with email %s ' +
+ 'he/she will not be able to authenticate with ' +
+ 'the email address in the place of user name') % email_address
+ )
+ return None
+
+ if user.email_isvalid == False:
+ return None
+
+ return user
+
elif method == 'oauth':
if login_providers[provider_name]['type'] in ('oauth', 'oauth2'):
try:
diff --git a/askbot/deps/django_authopenid/forms.py b/askbot/deps/django_authopenid/forms.py
index 8328da4d..9e069ed8 100644
--- a/askbot/deps/django_authopenid/forms.py
+++ b/askbot/deps/django_authopenid/forms.py
@@ -30,6 +30,7 @@
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import logging
+import cgi
from django import forms
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
@@ -38,7 +39,7 @@ from django.conf import settings as django_settings
from askbot.conf import settings as askbot_settings
from askbot import const as askbot_const
from django.utils.safestring import mark_safe
-from recaptcha_works.fields import RecaptchaField
+from askbot.forms import AskbotRecaptchaField
from askbot.utils.forms import NextUrlField, UserNameField, UserEmailField, SetPasswordForm
from askbot.utils.loading import load_module
@@ -54,7 +55,6 @@ __all__ = [
'OpenidSigninForm','OpenidRegisterForm',
'ClassicRegisterForm', 'ChangePasswordForm',
'ChangeEmailForm', 'EmailPasswordForm', 'DeleteForm',
- 'ChangeOpenidForm'
]
class LoginProviderField(forms.CharField):
@@ -75,7 +75,7 @@ class LoginProviderField(forms.CharField):
if value in providers:
return value
else:
- error_message = 'unknown provider name %s' % value
+ error_message = u'unknown provider name %s' % value
logging.critical(error_message)
raise forms.ValidationError(error_message)
@@ -125,6 +125,10 @@ class LoginForm(forms.Form):
"""
next = NextUrlField()
login_provider_name = LoginProviderField()
+ persona_assertion = forms.CharField(
+ required=False,
+ widget=forms.widgets.HiddenInput()
+ )
openid_login_token = forms.CharField(
max_length=256,
required = False,
@@ -220,6 +224,8 @@ class LoginForm(forms.Form):
#self.do_clean_oauth_fields()
elif provider_type == 'wordpress_site':
self.cleaned_data['login_type'] = 'wordpress_site'
+ elif provider_type == 'mozilla-persona':
+ self.cleaned_data['login_type'] = 'mozilla-persona'
return self.cleaned_data
@@ -297,7 +303,7 @@ class LoginForm(forms.Form):
del self.cleaned_data['new_password']
del self.cleaned_data['new_password_retyped']
error_message = _(
- 'Please choose password > %(len)s characters'
+ 'choose password > %(len)s characters'
) % {'len': askbot_const.PASSWORD_MIN_LENGTH}
error = self.error_class([error_message])
self._errors['new_password'] = error
@@ -314,6 +320,14 @@ class OpenidRegisterForm(forms.Form):
username = UserNameField(widget_attrs={'tabindex': 0})
email = UserEmailField()
+class SafeOpenidRegisterForm(OpenidRegisterForm):
+ """this form uses recaptcha in addition
+ to the base register form
+ """
+ def __init__(self, *args, **kwargs):
+ super(SafeOpenidRegisterForm, self).__init__(*args, **kwargs)
+ self.fields['recaptcha'] = AskbotRecaptchaField()
+
class ClassicRegisterForm(SetPasswordForm):
""" legacy registration form """
@@ -327,28 +341,44 @@ class SafeClassicRegisterForm(ClassicRegisterForm):
"""this form uses recaptcha in addition
to the base register form
"""
- recaptcha = RecaptchaField(
- private_key = askbot_settings.RECAPTCHA_SECRET,
- public_key = askbot_settings.RECAPTCHA_KEY
- )
+ def __init__(self, *args, **kwargs):
+ super(SafeClassicRegisterForm, self).__init__(*args, **kwargs)
+ self.fields['recaptcha'] = AskbotRecaptchaField()
-class ChangePasswordForm(SetPasswordForm):
+class ChangePasswordForm(forms.Form):
""" change password form """
- oldpw = forms.CharField(widget=forms.PasswordInput(attrs={'class':'required'}),
- label=mark_safe(ugettext_lazy('Current password')))
-
- def __init__(self, data=None, user=None, *args, **kwargs):
- if user is None:
- raise TypeError("Keyword argument 'user' must be supplied")
- super(ChangePasswordForm, self).__init__(data, *args, **kwargs)
- self.user = user
+ new_password = forms.CharField(
+ widget=forms.PasswordInput(),
+ error_messages = {
+ 'required': _('password is required'),
+ }
+ )
+ new_password_retyped = forms.CharField(
+ widget=forms.PasswordInput(),
+ error_messages = {
+ 'required': _('retype your password'),
+ }
+ )
+
+ def clean_new_password(self):
+ if 'new_password' in self.cleaned_data:
+ password = self.cleaned_data['new_password']
+ min_len = askbot_const.PASSWORD_MIN_LENGTH
+ if len(password) < min_len:
+ error = _('choose password > %(len)s characters') % \
+ {'len': min_len}
+ raise forms.ValidationError(error)
+ return password
- def clean_oldpw(self):
- """ test old password """
- if not self.user.check_password(self.cleaned_data['oldpw']):
- raise forms.ValidationError(_("Old password is incorrect. \
- Please enter the correct password."))
- return self.cleaned_data['oldpw']
+ def clean(self):
+ expected_keys = set(['new_password', 'new_password_retyped'])
+ if set(self.cleaned_data.keys()) == expected_keys:
+ pw1 = self.cleaned_data['new_password']
+ pw2 = self.cleaned_data['new_password_retyped']
+ if pw1 != pw2:
+ error = _('entered passwords did not match, please try again')
+ raise forms.ValidationError(error)
+ return self.cleaned_data
class ChangeEmailForm(forms.Form):
""" change email form """
@@ -462,5 +492,7 @@ def get_registration_form_class():
custom_class = getattr(django_settings, 'REGISTRATION_FORM', None)
if custom_class:
return load_module(custom_class)
+ elif askbot_settings.USE_RECAPTCHA:
+ return SafeOpenidRegisterForm
else:
return OpenidRegisterForm
diff --git a/askbot/deps/django_authopenid/urls.py b/askbot/deps/django_authopenid/urls.py
index f9098995..caab2037 100644
--- a/askbot/deps/django_authopenid/urls.py
+++ b/askbot/deps/django_authopenid/urls.py
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
from django.conf import settings as django_settings
-from django.conf.urls.defaults import patterns, url
+try:
+ from django.conf.urls import patterns, url
+except ImportError:
+ from django.conf.urls.defaults import patterns, url
if django_settings.ASKBOT_TRANSLATE_URL == True:
from django.utils.translation import ugettext as _
@@ -35,6 +38,11 @@ urlpatterns = patterns('askbot.deps.django_authopenid.views',
'signup_with_password',
name='user_signup_with_password'
),
+ url(
+ r'change-password/',
+ 'change_password',
+ name='change_password'
+ ),
url(r'^%s$' % _('logout/'), 'logout_page', name='logout'),
#these two commeted out urls should work only with EMAIL_VALIDATION=True
#but the setting is disabled right now
diff --git a/askbot/deps/django_authopenid/util.py b/askbot/deps/django_authopenid/util.py
index 06379e1d..586136d6 100644
--- a/askbot/deps/django_authopenid/util.py
+++ b/askbot/deps/django_authopenid/util.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import cgi
+import httplib
import urllib
import urlparse
import functools
@@ -35,9 +36,9 @@ except:
import time, base64, hmac, hashlib, operator, logging
from models import Association, Nonce
-__all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next']
+__all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response']
-ALLOWED_LOGIN_TYPES = ('password', 'oauth', 'openid-direct', 'openid-username', 'wordpress')
+ALLOWED_LOGIN_TYPES = ('password', 'oauth', 'oauth2', 'openid-direct', 'openid-username', 'wordpress')
class OpenID:
def __init__(self, openid_, issued, attrs=None, sreg_=None):
@@ -182,7 +183,8 @@ def filter_enabled_providers(data):
delete_list = list()
for provider_key, provider_settings in data.items():
name = provider_settings['name']
- is_enabled = getattr(askbot_settings, 'SIGNIN_' + name.upper() + '_ENABLED')
+ name_key = name.upper().replace('-', '_')
+ is_enabled = getattr(askbot_settings, 'SIGNIN_' + name_key + '_ENABLED')
if is_enabled == False:
delete_list.append(provider_key)
@@ -273,6 +275,15 @@ class LoginMethod(object):
self.oauth_authorize_url = self.get_required_attr('OAUTH_AUTHORIZE_URL', for_what)
self.oauth_get_user_id_function = self.get_required_attr('oauth_get_user_id_function', for_what)
+ if self.login_type == 'oauth2':
+ for_what = 'custom OAuth2 login'
+ self.auth_endpoint = self.get_required_attr('OAUTH_ENDPOINT', for_what)
+ self.token_endpoint = self.get_required_attr('OAUTH_TOKEN_ENDPOINT', for_what)
+ self.resource_endpoint = self.get_required_attr('OAUTH_RESOURCE_ENDPOINT', for_what)
+ self.oauth_get_user_id_function = self.get_required_attr('oauth_get_user_id_function', for_what)
+ self.response_parser = getattr(self.mod, 'response_parser', None)
+ self.token_transport = getattr(self.mod, 'token_transport', None)
+
if self.login_type.startswith('openid'):
self.openid_endpoint = self.get_required_attr('OPENID_ENDPOINT', 'custom OpenID login')
if self.login_type == 'openid-username':
@@ -294,7 +305,8 @@ class LoginMethod(object):
'change_password_prompt', 'consumer_key', 'consumer_secret',
'request_token_url', 'access_token_url', 'authorize_url',
'get_user_id_function', 'openid_endpoint', 'tooltip_text',
- 'check_password',
+ 'check_password', 'auth_endpoint', 'token_endpoint',
+ 'resource_endpoint', 'response_parser', 'token_transport'
)
#some parameters in the class have different names from those
#in the dictionary
@@ -388,6 +400,18 @@ def get_enabled_major_login_providers():
'password_changeable': True
}
+ if askbot_settings.SIGNIN_CUSTOM_OPENID_ENABLED:
+ context_dict = {'login_name': askbot_settings.SIGNIN_CUSTOM_OPENID_NAME}
+ data['custom_openid'] = {
+ 'name': 'custom_openid',
+ 'display_name': askbot_settings.SIGNIN_CUSTOM_OPENID_NAME,
+ 'type': askbot_settings.SIGNIN_CUSTOM_OPENID_MODE,
+ 'icon_media_path': askbot_settings.SIGNIN_CUSTOM_OPENID_LOGIN_BUTTON,
+ 'tooltip_text': _('Login with %(login_name)s') % context_dict,
+ 'openid_endpoint': askbot_settings.SIGNIN_CUSTOM_OPENID_ENDPOINT,
+ 'extra_token_name': _('%(login_name)s username') % context_dict
+ }
+
def get_facebook_user_id(client):
"""returns facebook user id given the access token"""
profile = client.request('me')
@@ -403,7 +427,8 @@ def get_enabled_major_login_providers():
'resource_endpoint': 'https://graph.facebook.com/',
'icon_media_path': '/jquery-openid/images/facebook.gif',
'get_user_id_function': get_facebook_user_id,
- 'response_parser': lambda data: dict(urlparse.parse_qsl(data))
+ 'response_parser': lambda data: dict(urlparse.parse_qsl(data)),
+ 'scope': ['email',],
}
if askbot_settings.TWITTER_KEY and askbot_settings.TWITTER_SECRET:
@@ -478,6 +503,12 @@ def get_enabled_major_login_providers():
'icon_media_path': '/jquery-openid/images/google.gif',
'openid_endpoint': 'https://www.google.com/accounts/o8/id',
}
+ data['mozilla-persona'] = {
+ 'name': 'mozilla-persona',
+ 'display_name': 'Mozilla Persona',
+ 'type': 'mozilla-persona',
+ 'icon_media_path': '/jquery-openid/images/mozilla-persona.gif',
+ }
data['yahoo'] = {
'name': 'yahoo',
'display_name': 'Yahoo',
@@ -825,7 +856,7 @@ def get_oauth2_starter_url(provider_name, csrf_token):
client_id=client_id,
redirect_uri=redirect_uri
)
- return client.auth_uri(state=csrf_token)
+ return client.auth_uri(state=csrf_token, scope=params['scope'])
def ldap_check_password(username, password):
@@ -838,3 +869,26 @@ def ldap_check_password(username, password):
except ldap.LDAPError, e:
logging.critical(unicode(e))
return False
+
+
+def mozilla_persona_get_email_from_assertion(assertion):
+ conn = httplib.HTTPSConnection('verifier.login.persona.org')
+ parsed_url = urlparse.urlparse(askbot_settings.APP_URL)
+ params = urllib.urlencode({
+ 'assertion': assertion,
+ 'audience': parsed_url.scheme + '://' + parsed_url.netloc
+ })
+ headers = {'Content-type': 'application/x-www-form-urlencoded', 'Accept': 'text/plain'}
+ conn.request('POST', '/verify', params, headers)
+ response = conn.getresponse()
+ if response.status == 200:
+ data = simplejson.loads(response.read())
+ email = data.get('email')
+ if email:
+ return email
+ else:
+ message = unicode(data)
+ message += '\nMost likely base url in /settings/QA_SITE_SETTINGS/ is incorrect'
+ raise ImproperlyConfigured(message)
+ #todo: nead more feedback to help debug fail cases
+ return None
diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py
index 4f321538..e1a3b981 100644
--- a/askbot/deps/django_authopenid/views.py
+++ b/askbot/deps/django_authopenid/views.py
@@ -30,6 +30,7 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import cgi
import datetime
from django.http import HttpResponseRedirect, Http404
from django.http import HttpResponse
@@ -50,7 +51,9 @@ from askbot.utils.functions import generate_random_key
from django.utils.html import escape
from django.utils.translation import ugettext as _
from django.utils.safestring import mark_safe
+from django.utils import simplejson
from askbot.mail import send_mail
+from askbot.utils import decorators as askbot_decorators
from askbot.utils.html import site_url
from recaptcha_works.decorators import fix_recaptcha_remote_ip
from askbot.deps.django_authopenid.ldap_auth import ldap_create_user
@@ -210,6 +213,7 @@ def ask_openid(
try:
auth_request = consumer.begin(openid_url)
except DiscoveryFailure:
+ openid_url = cgi.escape(openid_url)
msg = _(u"OpenID %(openid_url)s is invalid" % {'openid_url':openid_url})
logging.debug(msg)
return on_failure(request, msg)
@@ -290,12 +294,12 @@ def complete_oauth2_signin(request):
client_id = getattr(
askbot_settings,
- provider_name.upper() + '_KEY'
+ provider_name.upper() + '_KEY',
)
client_secret = getattr(
askbot_settings,
- provider_name.upper() + '_SECRET'
+ provider_name.upper() + '_SECRET',
)
client = OAuth2Client(
@@ -303,12 +307,13 @@ def complete_oauth2_signin(request):
resource_endpoint=params['resource_endpoint'],
redirect_uri=site_url(reverse('user_complete_oauth2_signin')),
client_id=client_id,
- client_secret=client_secret
+ client_secret=client_secret,
+ token_transport=params.get('token_transport', None)
)
client.request_token(
code=request.GET['code'],
- parser=params['response_parser']
+ parser=params.get('response_parser', None)
)
#todo: possibly set additional parameters here
@@ -325,6 +330,11 @@ def complete_oauth2_signin(request):
request.session['email'] = ''#todo: pull from profile
request.session['username'] = ''#todo: pull from profile
+ if (provider_name == 'facebook'):
+ profile = client.request("me")
+ request.session['email'] = profile.get('email', '')
+ request.session['username'] = profile.get('username', '')
+
return finalize_generic_signin(
request = request,
user = user,
@@ -518,7 +528,7 @@ def signin(request, template_name='authopenid/signin.html'):
provider_name=provider_name
)
request.user.message_set.create(
- message = _('Your new password saved')
+ message = _('Your new password is saved')
)
return HttpResponseRedirect(next_url)
else:
@@ -527,6 +537,38 @@ def signin(request, template_name='authopenid/signin.html'):
)
raise Http404
+ elif login_form.cleaned_data['login_type'] == 'mozilla-persona':
+ assertion = login_form.cleaned_data['persona_assertion']
+ email = util.mozilla_persona_get_email_from_assertion(assertion)
+ if email:
+ user = authenticate(email=email, method='mozilla-persona')
+ if user is None:
+ user = authenticate(email=email, method='valid_email')
+ if user:
+ #create mozilla persona user association
+ #because we trust the given email address belongs
+ #to the same user
+ UserAssociation(
+ openid_url=email,
+ user=user,
+ provider_name='mozilla-persona',
+ last_used_timestamp=datetime.datetime.now()
+ ).save()
+
+ if user:
+ login(request, user)
+ return HttpResponseRedirect(next_url)
+
+ #else - create new user account
+ #pre-fill email address with persona registration
+ request.session['email'] = email
+ return finalize_generic_signin(
+ request,
+ login_provider_name = 'mozilla-persona',
+ user_identifier = email,
+ redirect_url = next_url
+ )
+
elif login_form.cleaned_data['login_type'] == 'openid':
#initiate communication process
logging.debug('processing signin with openid submission')
@@ -733,8 +775,8 @@ def show_signin_view(
'page_class': 'openid-signin',
'view_subtype': view_subtype, #add_openid|default
'page_title': page_title,
- 'question':question,
- 'answer':answer,
+ 'question': question,
+ 'answer': answer,
'login_form': login_form,
'use_password_login': util.use_password_login(),
'account_recovery_form': account_recovery_form,
@@ -784,6 +826,20 @@ def show_signin_view(
return render(request, template_name, data)
+@csrf.csrf_exempt
+@askbot_decorators.post_only
+@askbot_decorators.ajax_login_required
+def change_password(request):
+ form = forms.ChangePasswordForm(request.POST)
+ data = dict()
+ if form.is_valid():
+ request.user.set_password(form.cleaned_data['new_password'])
+ request.user.save()
+ data['message'] = _('Your new password is saved')
+ else:
+ data['errors'] = form.errors
+ return HttpResponse(simplejson.dumps(data), content_type='application/json')
+
@login_required
def delete_login_method(request):
if askbot_settings.ALLOW_ADD_REMOVE_LOGIN_METHODS == False:
@@ -868,6 +924,12 @@ def finalize_generic_signin(
have been resolved
"""
+ if 'in_recovery' in request.session:
+ del request.session['in_recovery']
+ redirect_url = getattr(django_settings, 'LOGIN_REDIRECT_URL', None)
+ if redirect_url is None:
+ redirect_url = reverse('questions')
+
if request.user.is_authenticated():
#this branch is for adding a new association
if user is None:
@@ -934,6 +996,7 @@ def finalize_generic_signin(
@not_authenticated
@csrf.csrf_protect
+@fix_recaptcha_remote_ip
def register(request, login_provider_name=None, user_identifier=None):
"""
this function is used via it's own url with request.method=POST
@@ -1320,6 +1383,7 @@ def account_recover(request):
greet_new_user(user)
#need to show "sticky" signin view here
+ request.session['in_recovery'] = True
return show_signin_view(
request,
view_subtype = 'add_openid',
@@ -1329,16 +1393,3 @@ def account_recover(request):
return show_signin_view(request, view_subtype = 'bad_key')
return HttpResponseRedirect(get_next_url(request))
-
-#internal server view used as return value by other views
-def validation_email_sent(request):
- """this function is called only if EMAIL_VALIDATION setting is
- set to True bolean value"""
- assert(askbot_settings.EMAIL_VALIDATION == True)
- logging.debug('')
- data = {
- 'email': request.user.email,
- 'change_email_url': reverse('user_changeemail'),
- 'action_type': 'validate'
- }
- return render(request, 'authopenid/changeemail.html', data)
diff --git a/askbot/deps/group_messaging/urls.py b/askbot/deps/group_messaging/urls.py
index 19ee35bb..79536770 100644
--- a/askbot/deps/group_messaging/urls.py
+++ b/askbot/deps/group_messaging/urls.py
@@ -1,6 +1,9 @@
"""url configuration for the group_messaging application"""
-from django.conf.urls.defaults import patterns
-from django.conf.urls.defaults import url
+try:
+ from django.conf.urls import patterns, url
+except ImportError:
+ from django.conf.urls.defaults import patterns, url
+
from group_messaging import views
urlpatterns = patterns('',
diff --git a/askbot/deps/group_messaging/views.py b/askbot/deps/group_messaging/views.py
index 244762d1..f2ddaef3 100644
--- a/askbot/deps/group_messaging/views.py
+++ b/askbot/deps/group_messaging/views.py
@@ -45,7 +45,7 @@ class InboxView(object):
template = get_template(template_name)
html = template.render(context)
json = simplejson.dumps({'html': html, 'success': True})
- return HttpResponse(json, mimetype='application/json')
+ return HttpResponse(json, content_type='application/json')
def get(self, request, *args, **kwargs):
@@ -120,7 +120,7 @@ class NewThread(InboxView):
)
result['success'] = True
result['message_id'] = message.id
- return HttpResponse(simplejson.dumps(result), mimetype='application/json')
+ return HttpResponse(simplejson.dumps(result), content_type='application/json')
class PostReply(InboxView):
diff --git a/askbot/deps/livesettings/urls.py b/askbot/deps/livesettings/urls.py
index b608bf1f..f628f830 100644
--- a/askbot/deps/livesettings/urls.py
+++ b/askbot/deps/livesettings/urls.py
@@ -1,4 +1,7 @@
-from django.conf.urls.defaults import *
+try:
+ from django.conf.urls import *
+except ImportError:
+ from django.conf.urls.defaults import *
urlpatterns = patterns('askbot.deps.livesettings.views',
url(r'^$', 'site_settings', {}, name='site_settings'),
diff --git a/askbot/doc/source/askbot/layout.html b/askbot/doc/source/askbot/layout.html
index f1c8b509..6fd6b0d1 100644
--- a/askbot/doc/source/askbot/layout.html
+++ b/askbot/doc/source/askbot/layout.html
@@ -10,10 +10,10 @@
<div class="ab-proj-header">
<a href="/">Home</a> |
<a href="/en/questions/" title="Ask Questions">Ask Questions</a> |
- <a href="/hire-us" alt='Hire Us'>Hire Us</a> |
+ <a href="https://askbot.com/hire-us/" alt='Hire Us'>Consulting Services</a> |
<a href="/doc/index.html" alt="Documentation">Documentation</a> |
- <a href="/contribute" alt='Contribute'>Contribute</a> |
- <a href="/feedback/" alt='contact'>Contact</a>
+ <a href="https://askbot.com/contribute" alt='Contribute'>Contribute</a> |
+ <a href="https://askbot.com/feedback/" alt='contact'>Contact</a>
</div>
{% endblock %}
{% block relbar2 %}
diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst
index 00accbe0..e554e324 100644
--- a/askbot/doc/source/changelog.rst
+++ b/askbot/doc/source/changelog.rst
@@ -1,8 +1,22 @@
Changes in Askbot
=================
-Development version
--------------------
+Development master branch (only on github)
+------------------------------------------
+* Option to allow asking without registration (Egil Moeller)
+* Implemented Mozilla Persona authentication
+* Allowed custom providers of gravatar service (michas2)
+* Allowed configurable custom OpenID login button
+* Allowed custom list of feedback recipients (Keto)
+* Added option to show user's emails to the moderators
+* Added Read-Only mode for the site in the "access control" section.
+* Added `askbot_add_osqa_content` management command.
+* Management command to add data from other Askbot site.
+* Allowed simple overrides of livesettings with `ASKBOT_...` prefixed
+ variables in the `settings.py` file.
+
+0.7.49 (Sep 19, 2013)
+---------------------
* Support for Solr search backend (Adolfo)
* Allowed read-only access user groups (Adolfo)
* Added simple read-only API (Adolfo)
diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst
index 15c3cb5b..2ebcaebb 100644
--- a/askbot/doc/source/contributors.rst
+++ b/askbot/doc/source/contributors.rst
@@ -49,6 +49,15 @@ Programming, bug fixes and documentation
* `Kevin Porterfield <http://www.shotgunsoftware.com>_`
* `Robert Martin <https://github.com/bobbydavid>_`
* `Director <http://codeflow.co.kr>`_
+* `Stéphane Klein <http://stephane-klein.info>`_
+* `Andrew Chen <https://github.com/yongjhih>`_
+* `Benjamin Abel <https://github.com/BenjaminABEL>`_
+* `Pami Ketolainen <https://github.com/keto>`_
+* `Hamdi <https://github.com/Hamdy>`_
+* `michas2 <https://github.com/michas2>`_
+* `Francis Devereux <https://github.com/frankoid>`_
+* `Andrew Chen <https://github.com/yongjhih>`_
+* `Egil Moeller <https://github.com/redhog>`_
Translations
------------
diff --git a/askbot/doc/source/index.rst b/askbot/doc/source/index.rst
index 64ab1b20..d6703775 100644
--- a/askbot/doc/source/index.rst
+++ b/askbot/doc/source/index.rst
@@ -28,8 +28,9 @@ at the forum_ or by email at admin@askbot.org
Appendix E: Askbot as reusable Django application <askbot-as-reusable-django-application>
Appendix F: Customizing skin in askbot <customizing-skin-in-askbot>
Appendix G: Intranet setup <intranet-setup>
- Appendix H: Haystack with Solr and Apache Tomcat <solr>
- Appendix I: Migration from MySQL to PostgreSQL <mysql-to-postgres>
+ Appendix H: Language support in Askbot <localization>
+ Appendix I: Configuration of text search <text-search>
+ Appendix J: Migration from MySQL to PostgreSQL <mysql-to-postgres>
Footnotes <footnotes>
Contributors <contributors>
Changelog <changelog>
diff --git a/askbot/doc/source/live-settings.rst b/askbot/doc/source/live-settings.rst
index 12546e6c..bc0d3d72 100644
--- a/askbot/doc/source/live-settings.rst
+++ b/askbot/doc/source/live-settings.rst
@@ -24,7 +24,22 @@ Entering live settings in settings.py file
==========================================
You might want to bypass live settings and enter them directly
-in the ``settings.py`` file in the ``LIVESETTINGS_OPTIONS`` dictionary.
+in the ``settings.py`` file.
+
+Currently there are two ways to do this:
+
+1. Simply add variable with the same name as defined in `askbot/conf` files,
+ but prefixed with `ASKBOT_` and the corresponding value.
+ For example, add `ASKBOT_RSS_ENABLED = False` to disable the rss.
+ In `askbot/conf` this value is defined simply as `RSS_ENABLED`.
+
+2. Put settings into the ``LIVESETTINGS_OPTIONS`` dictionary,
+ this way you can assign livesettings values to specific site by ID,
+ which may or may not be useful for the multi-portal (multi-site) askbot setup.
+
+The first method above overrides the second.
+
+Here is a more detailed description on how to use the `LIVESETTINGS_OPTIONS` method:
Having live settings overridden from the ``settings.py`` file may
somewhat speed up your site
diff --git a/askbot/doc/source/localization.rst b/askbot/doc/source/localization.rst
new file mode 100644
index 00000000..fbf51da6
--- /dev/null
+++ b/askbot/doc/source/localization.rst
@@ -0,0 +1,81 @@
+.. _localization:
+======================================
+Configuring language support in Askbot
+======================================
+
+There are several things to consider when localizing askbot:
+
+* :ref:`setting the site language <default-lang>`
+* :ref:`translation and display of the urls <translate-urls>`
+* :ref:`translation of the strings in the user interface <strings>`
+* :ref:`enabling the multilingual setup <multilingual>`
+* :ref:`configuring the language-specific text search <text-search>`
+
+.. _default-lang:
+
+Setting the site language
+=========================
+
+Specify the language code with the value of `LANGUAGE_CODE` parameter
+in the `settings.py` file::
+
+ LANGUAGE_CODE='es'
+
+.. note::
+ In the :ref:`multi-lingual configuration <multilingual>`
+ this language will be the default and the complete list of
+ language codes and their verbose names
+ is specified with the `LANGUAGES` parameter.
+
+.. _translate-urls:
+
+Translation of the URLs
+=======================
+
+There are also `settings.py` options to translate the urls:
+`ASKBOT_TRANSLATE_URL` and `ALLOW_UNICODE_SLUGS`.
+
+When the `ASKBOT_TRANSLATE_URL` is `True`, most urls will be translated,
+otherwise urls will be in English.
+When the `ALLOW_UNICODE_SLUGS` is `True` the question titles and user names
+will be presented as Unicode, e.g. with the Cyrillic, Chinese
+or Arabic characters, otherwise they will be transliterated into ASCII.
+
+If you are translating URLs (in the transifex you will probably
+find them as strings containing forward slashes) -
+take the following, in order to prevent broken links:
+
+* translation of multiple urls cannot be the same
+ (e.g. /question/ and /questions/ must have different translations)
+* if the same url is present in more than one translation file
+ those translation must be exactly the same
+
+.. _strings:
+
+Translation of strings in Askbot
+================================
+
+Translation of Askbot strings is performed at the `Transifex service <transifex>`_.
+Please `register there <transifex>`_ and work on the localization that interests you.
+We periodically update the source language strings on Transifex and pull
+the translations back into the project. Thanks!
+
+If you intend to translate urls - please :ref:`look here <translate-urls>`.
+
+Please *do not* translate via github (if you know what it means),
+as it's better to have just one source of strings.
+
+The remaining part will will most likely interest developers,
+therefore here we tell what is specific to Askbot and
+refer the developer to the documentation of tools
+used in Askbot.
+
+Firstly - Askbot uses `Jinja2 <http://jinja.pocoo.org/docs/>`_ templates,
+not the Django templates and an Jinja2 adapter module for Django, called
+`Coffin <https://github.com/coffin/coffin/>`_. Please look at how translation
+tags are added to the templates processed by the `coffin` module.
+
+Secondly - instead of the django `makemessages` command - use `jinja2_makemessages`.
+
+Finally - to pull strings from the transifex use the `tx` program from
+`transifex-client pypi package <https://pypi.python.org/pypi/transifex-client>`_.
diff --git a/askbot/doc/source/management-commands.rst b/askbot/doc/source/management-commands.rst
index a56aa47a..1f5c3fa1 100644
--- a/askbot/doc/source/management-commands.rst
+++ b/askbot/doc/source/management-commands.rst
@@ -46,9 +46,6 @@ The bulk of the management commands fall into this group and will probably be th
| `merge_users <from_id> | Merges user accounts and all related data from one user |
| <to_id>` | to another, the "from user" account is deleted. |
+---------------------------------+-------------------------------------------------------------+
-| `dump_forum [--dump-name | Save forum contents into a file. `--dump-name` parameter is |
-| some_name]` | optional |
-+---------------------------------+-------------------------------------------------------------+
| `get_tag_stats [-u|-t] [-e]` | Print tag subscription statistics, per tag (option -t) |
| | or per user (option -u), if option -e is given, empty |
| | records will be shown too (longer versions of the options |
@@ -56,16 +53,6 @@ The bulk of the management commands fall into this group and will probably be th
| | --per-user-tag-subscription-counts for -u, and --print-empty|
| | for -e). |
+---------------------------------+-------------------------------------------------------------+
-| `load_forum <file_name>` | Load forum data from a file saved by the `dump_forum` |
-| | command |
-+---------------------------------+-------------------------------------------------------------+
-| `load_stackexchange <file.zip>` | Load SackExchange dump into Askbot. It is best to run this |
-| | command on empty database. Also - before running, make sure |
-| | that `askbot.importers.stackexchange` is in the list of |
-| | installed apps within your settings.py file (it might also |
-| | be necessary to run `syncdb` command to initiate the |
-| | SE importer tables). |
-+---------------------------------+-------------------------------------------------------------+
| `rename_tags --from <from_tags> | Rename, merge or split tags. User ID is the id of the user |
| --to <to_tags> --user-id | who will be assigned as the performer of the retag action. |
| <user_id>` | If more than is in the `--from` or the `--to` parameters |
@@ -101,6 +88,32 @@ The bulk of the management commands fall into this group and will probably be th
| | foreign key to that object is still present. |
+---------------------------------+-------------------------------------------------------------+
+.. _data-import-commands:
+
+Data import commands
+====================
+
+These commands import or add data to the Askbot forum.
+
++---------------------------------+-------------------------------------------------------------+
+| command | purpose |
++=================================+=============================================================+
+| `load_stackexchange <file.zip>` | Load SackExchange dump into Askbot. It is best to run this |
+| | command on empty database. Also - before running, make sure |
+| | that `askbot.importers.stackexchange` is in the list of |
+| | installed apps within your settings.py file (it might also |
+| | be necessary to run `syncdb` command to initiate the |
+| | SE importer tables). |
++---------------------------------+-------------------------------------------------------------+
+| `askbot_add_xml_content | Add xml Askbot data dumped with the Django command |
+| <file.xml>` | `dumpdata` |
++---------------------------------+-------------------------------------------------------------+
+| `askbot_add_osqa_content | Add xml OSQA data dumped with the Django command |
+| <file.xml>` | `export_osqa` |
++---------------------------------+-------------------------------------------------------------+
+| `askbot_import_jive <file.xml> | Import xml Jive data |
++---------------------------------+-------------------------------------------------------------+
+
.. _email-related-commands:
Email-related commands
diff --git a/askbot/doc/source/multilingual.rst b/askbot/doc/source/multilingual.rst
new file mode 100644
index 00000000..75f93f0a
--- /dev/null
+++ b/askbot/doc/source/multilingual.rst
@@ -0,0 +1,46 @@
+.. _multilingual:
+====================================
+Setting up multilingual Askbot sites
+====================================
+
+Askbot can support multiple languages on a single site, in which case
+urls are modified by a prefix made of a language code, e.g.
+base url /questions/ becomes /de/questions/ for the German localization.
+
+.. note::
+ If you want to learn about configuration of individual languages
+ please look :ref:`here <localization>`
+
+In order to enable the multilingual setup add the following to the
+`settings.py` file::
+
+ ASKBOT_MULTILINGUAL=True
+
+Also, activate the django's locale middleware by adding to the
+`MIDDLEWARE_CLASSES` the following entry::
+
+ 'django.middleware.locale.LocaleMiddleware',
+
+There is a standard Django setting `LANGUAGES`, which enables specific languages.
+By default this setting contains very many languages.
+You will likely want to narrow in the `settings.py` file
+the choice of the available languages::
+
+ #it's important to use ugettext_lazy or ugettext_noop
+ #in the settings.py file
+ from django.utils.translation import ugettext_lazy as _
+ LANGUAGES = (
+ ('de', _('German')),
+ ('en', _('English'))
+ )
+
+More on the usage of this setting can be read in the
+`Django documentation <https://docs.djangoproject.com/en/dev/ref/settings/#languages>`_.
+
+The default language should be specified with the setting `LANGUAGE_CODE`.
+Users will be automatically redirected to the corresponding default language
+page from the non-prefixed urls.
+
+There are a number of `settings.py` options that control the various
+aspects of the site localization - the behaviour of the software depending on the
+currently active language.. Please read more about the :ref:`Localization of Askbot <localization>`.
diff --git a/askbot/doc/source/mysql-to-postgres.rst b/askbot/doc/source/mysql-to-postgres.rst
index 73bdaddf..c9c0e477 100644
--- a/askbot/doc/source/mysql-to-postgres.rst
+++ b/askbot/doc/source/mysql-to-postgres.rst
@@ -9,7 +9,7 @@ In this document we explain how to migrate from MySQL to Postgresql with differe
Askbot is optimized for Postgresql as search functionality works better with this database engine.
.. note::
- As a general advice and to reduce the database size run the **clearsessions** management command before starting the migration.
+ As a general advice, to reduce the database size - run the **cleanup** management command before starting the migration.
Simple Migration of small database
@@ -23,19 +23,17 @@ With MySQL as your database engine in your settings.py file run the following co
After that change your database engine to Postgresql in settings.py and do::
+ python manage.py syncdb --migrate --noinput #create the database structure
python manage.py loaddata data.json
.. note::
- This wont work with large datasets because django will load all your data into memory and you might run out of memory if the site data is too large.
-
+ This won't work with large datasets because django will load all your
+ data into memory and you might run out of memory if the site data is too large.
This process can produce warnings that can be ignored.
- Please make sure that your postgres database is empty but with the askbot structure inside as after syncdb and migrate.
-
-
Data migration with py-mysql2pgsql
==================================
@@ -43,7 +41,7 @@ If the database is large this tool will come handy, to install it run::
pip install py-mysql2pgsql
-After it is installed create a configuration file called config.yml with the following contents::
+Create a configuration file called config.yml with the following contents::
mysql:
hostname: localhost
@@ -65,13 +63,12 @@ Then run::
py-mysql2pgsql -v -f config.yml
-It will start to migrate data, it might take a while acording to the database size.
-
-After that process is finished there are a couple of things left to do.
+The script will start migrating the data and might take a while, depending on the database size.
+After the process is finished there are a couple of things left to do.
-Enabling Postgresql full text search
-------------------------------------
+Enable Postgresql full text search
+----------------------------------
Askbot relies on special postgresql features for better search, in this case the py-mysql2pgsql tool will not
add these features, so it requires to be added manually.
@@ -80,13 +77,14 @@ To fix it run the command::
python manage.py init_postgresql_full_text_search
-It may take a while according to your database size. Test this by running a search query on the askbot site.
+This may also take some time, depending on the database size.
+Test this by running a search query on the askbot site.
..
- Download:
- 1. `thread_and_post_models_10032013.plsql <https://raw.github.com/ASKBOT/askbot-devel/master/askbot/search/postgresql/thread_and_post_models_10032013.plsql>`_
- 2. `user_profile_search_08312012.plsql <https://raw.github.com/ASKBOT/askbot-devel/master/askbot/search/postgresql/user_profile_search_08312012.plsql>`_
- And apply them to your postgres database like this::
+ If you have an issue with the above command, it is possible to run the search setup sql script manually:
+ 1. Download `thread_and_post_models_10032013.plsql <https://raw.github.com/ASKBOT/askbot-devel/master/askbot/search/postgresql/thread_and_post_models_10032013.plsql>`_
+ 2. Download `user_profile_search_08312012.plsql <https://raw.github.com/ASKBOT/askbot-devel/master/askbot/search/postgresql/user_profile_search_08312012.plsql>`_
+ 3. Apply the scripts to your postgres database::
psql your_database < thread_and_post_models_10032013.plsql
psql your_database < user_profile_search_08312012.plsql
diff --git a/askbot/doc/source/solr.rst b/askbot/doc/source/solr.rst
index 9db6ba2f..8a1de0c2 100644
--- a/askbot/doc/source/solr.rst
+++ b/askbot/doc/source/solr.rst
@@ -8,22 +8,26 @@ Installing Apache Solr with Apache Tomcat 7 in Ubuntu 12.04
This document describes the process of instalation of Apache Solr search engine in Ubuntu Server 12.04
for askbot use. To follow this steps you must have already askbot installed and running.
-Getting the requirements
-========================
+Installation of the required packages
+=====================================
-We need the following packages installed::
+Install packages `tomcat7` and `tomcat7-admin`::
sudo apt-get install tomcat7 tomcat7-admin
-We need to download Apache Solr from the `official site <http://lucene.apache.org/solr/downloads.html>`_::
+Download Apache Solr from the `official site <http://lucene.apache.org/solr/downloads.html>`_::
wget http://www.bizdirusa.com/mirrors/apache/lucene/solr/3.6.2/apache-solr-3.6.2.tgz
+Install `django-haystack` module in your Python environment::
+
+ pip install django-haystack
+
Setting up Tomcat
=================
-After installing tomcat there are some configuration required to make it work. First we are going to add
-Tomcat users. Edit /etc/tomcat7/tomcat-users.xml and add the following::
+After installing Tomcat, add users to the Tomcat server.
+Edit `/etc/tomcat7/tomcat-users.xml` and add the following::
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
@@ -31,15 +35,17 @@ Tomcat users. Edit /etc/tomcat7/tomcat-users.xml and add the following::
<role rolename="admin"/>
<role rolename="admin-gui"/>
<role rolename="manager-gui"/>
- <user username="tomcat" password="tomcat" roles="manager,admin,manager-gui,admin-gui"/>
+ <user username="tomcat" password="tomcat"
+ roles="manager,admin,manager-gui,admin-gui"/>
</tomcat-users>
-This will allow you to connect to the web management interface. After doing it restart the service:
+Then restart the service::
service tomcat7 restart
-To make see if it works go to: http://youripaddress:8080/manager it will ask for your tomcat user password
-described in the tomcat-users.xml
+Now you should be able to connect to the web management interface
+via http://youripaddress:8080/manager
+and entering there user name and password.
Installing Solr under Tomcat
============================
@@ -48,28 +54,30 @@ Extract the solr tar archive from the previous download::
tar -xzf apache-solr-3.6.2.tgz
-Copy the example/ directory from the source to /opt/solr/. Open the file /opt/solr/example/solr/conf/solrconfig.xml
+Copy the `example/` directory from the source to `/opt/solr/`.
+Open the file `/opt/solr/example/solr/conf/solrconfig.xml`
and Modify the dataDir parameter as::
<dataDir>${solr.data.dir:/opt/solr/example/solr/data}</dataDir>
-Copy the .war file in dist directory to /opt/solr::
+Copy the `.war` file in dist directory to `/opt/solr`::
cp dist/apache-solr-3.6.2.war /opt/solr
-Create solr.xml inside of /etc/tomcat/Catalina/localhost/ with the following contents::
+Create `solr.xml` inside of `/etc/tomcat/Catalina/localhost/` with the following contents::
<?xml version="1.0" encoding="utf-8"?>
<Context docBase="/opt/solr/apache-solr-3.6.2.war" debug="0" crossContext="true">
- <Environment name="solr/home" type="java.lang.String" value="/opt/solr/example/solr" override="true"/>
+ <Environment name="solr/home" type="java.lang.String"
+ value="/opt/solr/example/solr" override="true"/>
</Context>
-Restart tomcat server::
+Restart the tomcat server::
service tomcat7 restart
-By now you should be able to see the "solr" application in the tomcat manager and also access it in /solr/admin.
-
+Now you should be able to access the "solr" application
+in the Tomcat manager at `/solr/admin`.
Configuring Askbot with Solr
============================
@@ -103,14 +111,18 @@ The output should be something like::
Indexing 101 posts.
Indexing 101 threads.
-You must be good to go after this, just restart the askbot application and test the search with haystack and solr
+Now all should be ready,
+just restart the askbot application
+and test the search with haystack and solr.
+.. _solr-multilingual:
Multilingual Setup
==================
.. note::
- This is experimental feature, currently xml generation works for: English, Spanish, Chinese, Japanese, Korean and French.
+ This is experimental feature, currently xml generation works for:
+ English, Spanish, Chinese, Japanese, Korean and French.
Add the following to settings.py::
@@ -129,7 +141,6 @@ Configure the HAYSTACK_CONNECTIONS settings with the following format for each l
},
}
-
Generate xml files according to language::
python manage.py askbot_build_solr_schema -l <language_code> > /opt/solr/example/solr/conf/schema-<language_code>.xml
@@ -141,12 +152,13 @@ For each language that you want to support you will need to add a solr core like
http://127.0.0.1:8080/solr/admin/cores?action=CREATE&name=core-<language_code>&instanceDir=.&config=solrconfig.xml&schema=schema-<language_code>.xml&dataDir=data
-For more information on how to handle Solr cores visit `the oficial Solr documetation wiki. <http://wiki.apache.org/solr/CoreAdmin>`_
+For more information on how to handle Solr cores visit the
+`Solr documetation <http://wiki.apache.org/solr/CoreAdmin>`_.
Build the index according to language
-------------------------------------
-For every language supported you'll need to rebuild the index the following way::
+For every active language rebuild the index::
python manage.py askbot_rebuild_index -l <language_code>
@@ -159,24 +171,32 @@ There are several ways to keep the index fresh in askbot with haystack.
Cronjob
-------
-Create a cronjob that executes *askbot_update_index* command for each language installed (in case of multilingual setup).
+Create a cronjob that executes *askbot_update_index* command
+for each of the activated languages.
Real Time Signal
----------------
-The real time signal method updates the index synchronously after each object it's saved or deleted, to enable it add this to settings.py::
+The *real time* signal method updates the index synchronously
+after each object it's saved or deleted,
+to enable it add this to settings.py::
HAYSTACK_SIGNAL_PROCESSOR = 'askbot.search.haystack.signals.AskbotRealtimeSignalProcessor'
-this can delay the requests time of your page, if you have a high traffic site this is not recommended.
+Use of synchronous index updates may slow down your site
+which may not be acceptable for the high traffic sites.
+
+Updating the Index asyncronously with Celery
+--------------------------------------------
-Updating the Index with Celery
-------------------------------
+The *asynchronous signal* method updates the index by adding delayed job to the queue
+after each object is saved or deleted.
-The real time signal method updates the index asynchronously after each object it's saved or deleted using Celery as queue to enable it add this to settings.py::
+To make this work,
+`django-celery <http://celery.readthedocs.org/en/latest/django/first-steps-with-django.html>`_
+must be installed, enabled and configured and the Haystack signal processor configured
+in the `settings.py` file::
HAYSTACK_SIGNAL_PROCESSOR = 'askbot.search.haystack.signals.AskbotCelerySignalProcessor'
#modify CELERY_ALWAYS_EAGER to:
CELERY_ALWAYS_EAGER = False
-
-You will need to enable Celery to make this work.
diff --git a/askbot/doc/source/text-search.rst b/askbot/doc/source/text-search.rst
new file mode 100644
index 00000000..b18e3f33
--- /dev/null
+++ b/askbot/doc/source/text-search.rst
@@ -0,0 +1,48 @@
+.. _text-search:
+======================================
+Configuring full text search in Askbot
+======================================
+
+Currently there are two supported language-aware mechanisms for full text search:
+
+* :ref:`postgresql full text search <postgresql-text-search>`
+* :ref:`Solr search engine <solr-text-search>`
+
+MySQL supports text search only for English and only for the MyISAM storage engine.
+MyISAM engine lacks support of the database transactions,
+therefore it is strongly recommended to use Postgresql.
+
+.. _postgresql-text-search:
+
+Postgresql full text search
+===========================
+
+Postgresql supports full text search in the following languages:
+
+Danish, Dutch, English, Finnish, French, German, Hungarian,
+Italian, Japanese (requires postgresql package `textsearch_ja`), Norwegian,
+Portugese, Romanian, Russian, Spanish, Swedish, Turkish.
+
+To enable this option - just use the postgresql database and
+add in the `settings.py` file
+the corresponding entry in the
+`LANGUAGES setting <https://docs.djangoproject.com/en/dev/ref/settings/#languages>`_.
+
+.. note::
+ Japanese language search in Postgresql requires installation
+ of a "contrib" package called `textsearch_ja`
+
+.. _solr-text-search:
+
+Solr full text search
+=====================
+
+Apache Solr search supports more languages and Askbot supports Solr via the
+module called Haystack.
+
+:ref:`Here <solr>` are detailed instructions on how to enable Solr on
+Ubuntu system version 12.04, which may be helpful for users of other
+distributions of Linux.
+
+In addition to the basic set up of Solr, it will be necessary to configure
+:ref:`multilingual search <solr-multilingual>` under solr.
diff --git a/askbot/feed.py b/askbot/feed.py
index 926447cb..b8f7efb7 100644
--- a/askbot/feed.py
+++ b/askbot/feed.py
@@ -32,7 +32,7 @@ class RssIndividualQuestionFeed(Feed):
def title(self):
return askbot_settings.APP_TITLE + _(' - ') + \
- _('Individual question feed')
+ _('Individual %(question)s feed') % {'question': askbot_settings.WORDS_QUESTION_SINGULAR}
def feed_copyright(self):
return askbot_settings.APP_COPYRIGHT
@@ -85,13 +85,12 @@ class RssIndividualQuestionFeed(Feed):
def item_title(self, item):
"""returns the title for the item
"""
- title = item
if item.post_type == "question":
- self.title = item
+ title = item.thread.title
elif item.post_type == "answer":
- title = "Answer by %s for %s " % (item.author, self.title)
+ title = u'Answer by %s for %s ' % (item.author, item.thread._question_post().summary)
elif item.post_type == "comment":
- title = "Comment by %s for %s" % (item.author, self.title)
+ title = u'Comment by %s for %s' % (item.author, item.parent.summary)
return title
def item_description(self, item):
@@ -106,7 +105,7 @@ class RssLastestQuestionsFeed(Feed):
def title(self):
return askbot_settings.APP_TITLE + _(' - ') + \
- _('Latest question feed')
+ _('Latest %(question)s feed') % {'question': askbot_settings.WORDS_QUESTION_SINGULAR}
def feed_copyright(self):
return askbot_settings.APP_COPYRIGHT
diff --git a/askbot/forms.py b/askbot/forms.py
index ce27ebc8..e3c6c253 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -9,8 +9,10 @@ from django.conf import settings as django_settings
from django.core.exceptions import PermissionDenied
from django.forms.util import ErrorList
from django.utils.html import strip_tags
+from django.utils.datastructures import SortedDict
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy, string_concat
+from django.utils.translation import get_language
from django.utils.text import get_text_list
from django.contrib.auth.models import User
from django_countries import countries
@@ -22,6 +24,10 @@ from askbot.conf import get_tag_display_filter_strategy_choices
from tinymce.widgets import TinyMCE
import logging
+def should_use_recaptcha(user):
+ """True if user must use recaptcha"""
+ return askbot_settings.USE_RECAPTCHA and (user.is_anonymous() or user.is_watched())
+
def cleanup_dict(dictionary, key, empty_value):
"""deletes key from dictionary if it exists
@@ -68,12 +74,14 @@ def clean_marked_tagnames(tagnames):
if tagname == '':
continue
if tagname.endswith('*'):
- if tagname.count('*') > 1:
+ if tagname.count('*') > 1 or len(tagname) == 1:
continue
else:
- wildcards.append(tagname)
+ base_tag = tagname[:-1]
+ cleaned_base_tag = clean_tag(base_tag, look_in_db=False)
+ wildcards.append(cleaned_base_tag + '*')
else:
- pure_tags.append(tagname)
+ pure_tags.append(clean_tag(tagname))
return pure_tags, wildcards
@@ -206,6 +214,14 @@ class CountedWordsField(forms.CharField):
return value
+class AskbotRecaptchaField(RecaptchaField):
+ """A recaptcha field with preset keys from the livesettings"""
+ def __init__(self, *args, **kwargs):
+ kwargs.setdefault('private_key', askbot_settings.RECAPTCHA_SECRET)
+ kwargs.setdefault('public_key', askbot_settings.RECAPTCHA_KEY)
+ super(AskbotRecaptchaField, self).__init__(*args, **kwargs)
+
+
class LanguageField(forms.ChoiceField):
def __init__(self, *args, **kwargs):
@@ -245,9 +261,7 @@ class TitleField(forms.CharField):
)
self.max_length = 255
self.label = _('title')
- self.help_text = _(
- 'Please enter your question'
- )
+ self.help_text = askbot_settings.WORDS_PLEASE_ENTER_YOUR_QUESTION
self.initial = ''
def clean(self, value):
@@ -263,20 +277,21 @@ class TitleField(forms.CharField):
) % askbot_settings.MIN_TITLE_LENGTH
raise forms.ValidationError(msg)
encoded_value = value.encode('utf-8')
+ question_term = askbot_settings.WORDS_QUESTION_SINGULAR
if len(value) == len(encoded_value):
if len(value) > self.max_length:
raise forms.ValidationError(
_(
- 'The question is too long, maximum allowed size is '
- '%d characters'
- ) % self.max_length
+ 'The %(question)s is too long, maximum allowed size is '
+ '%(length)d characters'
+ ) % {'question': question_term, 'length': self.max_length}
)
elif len(encoded_value) > self.max_length:
raise forms.ValidationError(
_(
- 'The question is too long, maximum allowed size is '
- '%d bytes'
- ) % self.max_length
+ 'The %(question)s is too long, maximum allowed size is '
+ '%(length)d bytes'
+ ) % {'question': question_term, 'length': self.max_length}
)
return value.strip() # TODO: test me
@@ -286,9 +301,6 @@ class EditorField(forms.CharField):
"""EditorField is subclassed by the
:class:`QuestionEditorField` and :class:`AnswerEditorField`
"""
- length_error_template_singular = 'post content must be > %d character',
- length_error_template_plural = 'post content must be > %d characters',
- min_length = 10 # sentinel default value
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
@@ -309,16 +321,18 @@ class EditorField(forms.CharField):
self.label = _('content')
self.help_text = u''
self.initial = ''
+ self.min_length = 10
+ self.post_term_name = _('post')
def clean(self, value):
if value is None:
value = ''
if len(value) < self.min_length:
msg = ungettext_lazy(
- self.length_error_template_singular,
- self.length_error_template_plural,
+ '%(post)s content must be > %(count)d character',
+ '%(post)s content must be > %(count)d characters',
self.min_length
- ) % self.min_length
+ ) % {'post': unicode(self.post_term_name), 'count': self.min_length}
raise forms.ValidationError(msg)
if self.user.is_anonymous():
@@ -342,11 +356,8 @@ class QuestionEditorField(EditorField):
super(QuestionEditorField, self).__init__(
user=user, *args, **kwargs
)
- self.length_error_template_singular = \
- 'question body must be > %d character'
- self.length_error_template_plural = \
- 'question body must be > %d characters'
self.min_length = askbot_settings.MIN_QUESTION_BODY_LENGTH
+ self.post_term_name = askbot_settings.WORDS_QUESTION_SINGULAR
class AnswerEditorField(EditorField):
@@ -354,12 +365,11 @@ class AnswerEditorField(EditorField):
def __init__(self, *args, **kwargs):
super(AnswerEditorField, self).__init__(*args, **kwargs)
- self.length_error_template_singular = 'answer must be > %d character'
- self.length_error_template_plural = 'answer must be > %d characters'
+ self.post_term_name = askbot_settings.WORDS_ANSWER_SINGULAR
self.min_length = askbot_settings.MIN_ANSWER_BODY_LENGTH
-def clean_tag(tag_name):
+def clean_tag(tag_name, look_in_db=True):
"""a function that cleans a single tag name"""
tag_length = len(tag_name)
if tag_length > askbot_settings.MAX_TAG_LENGTH:
@@ -376,16 +386,26 @@ def clean_tag(tag_name):
#todo - this needs to come from settings
tagname_re = re.compile(const.TAG_REGEX, re.UNICODE)
if not tagname_re.search(tag_name):
- raise forms.ValidationError(
- _(message_keys.TAG_WRONG_CHARS_MESSAGE)
- )
+ if tag_name[0] in const.TAG_FORBIDDEN_FIRST_CHARS:
+ raise forms.ValidationError(
+ _(message_keys.TAG_WRONG_FIRST_CHAR_MESSAGE)
+ )
+ else:
+ raise forms.ValidationError(
+ _(message_keys.TAG_WRONG_CHARS_MESSAGE)
+ )
if askbot_settings.FORCE_LOWERCASE_TAGS:
#a simpler way to handle tags - just lowercase thew all
return tag_name.lower()
+ elif look_in_db == False:
+ return tag_name
else:
from askbot import models
- matching_tags = models.Tag.objects.filter(name__iexact=tag_name)
+ matching_tags = models.Tag.objects.filter(
+ name__iexact=tag_name,
+ language_code=get_language()
+ )
if len(matching_tags) > 0:
return matching_tags[0].name
else:
@@ -420,7 +440,7 @@ class TagNamesField(forms.CharField):
def clean(self, value):
from askbot import models
value = super(TagNamesField, self).clean(value)
- data = value.strip()
+ data = value.strip(const.TAG_STRIP_CHARS)
if len(data) < 1:
if askbot_settings.TAGS_ARE_REQUIRED:
raise forms.ValidationError(
@@ -475,11 +495,6 @@ class WikiField(forms.BooleanField):
'community wiki (karma is not awarded & '
'many others can edit wiki post)'
)
- self.help_text = _(
- 'if you choose community wiki option, the question '
- 'and answer do not generate points and name of '
- 'author will not be shown'
- )
def clean(self, value):
return value and askbot_settings.WIKI_ON
@@ -718,19 +733,11 @@ class SendMessageForm(forms.Form):
)
-class NotARobotForm(forms.Form):
- recaptcha = RecaptchaField(
- private_key=askbot_settings.RECAPTCHA_SECRET,
- public_key=askbot_settings.RECAPTCHA_KEY
- )
-
-
class FeedbackForm(forms.Form):
name = forms.CharField(label=_('Your name (optional):'), required=False)
email = forms.EmailField(label=_('Email:'), required=False)
message = forms.CharField(
label=_('Your message:'),
- max_length=800,
widget=forms.Textarea(attrs={'cols': 60})
)
no_email = forms.BooleanField(
@@ -739,22 +746,21 @@ class FeedbackForm(forms.Form):
)
next = NextUrlField()
- def __init__(self, is_auth=False, *args, **kwargs):
+ def __init__(self, user=None, *args, **kwargs):
super(FeedbackForm, self).__init__(*args, **kwargs)
- self.is_auth = is_auth
- if not is_auth:
- if askbot_settings.USE_RECAPTCHA:
- self._add_recaptcha_field()
-
- def _add_recaptcha_field(self):
- self.fields['recaptcha'] = RecaptchaField(
- private_key=askbot_settings.RECAPTCHA_SECRET,
- public_key=askbot_settings.RECAPTCHA_KEY
- )
+ self.user = user
+ if should_use_recaptcha(user):
+ self.fields['recaptcha'] = AskbotRecaptchaField()
+
+ def clean_message(self):
+ message = self.cleaned_data.get('message', '').strip()
+ if not message:
+ raise forms.ValidationError(_('Message is required'))
+ return message
def clean(self):
super(FeedbackForm, self).clean()
- if not self.is_auth:
+ if self.user and self.user.is_anonymous():
if not self.cleaned_data['no_email'] \
and not self.cleaned_data['email']:
msg = _('Please mark "I dont want to give my mail" field.')
@@ -908,18 +914,9 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
in the cleaned data, and will evaluate to False if the
settings forbids anonymous asking
"""
- title = TitleField()
tags = TagNamesField()
wiki = WikiField()
group_id = forms.IntegerField(required = False, widget = forms.HiddenInput)
- ask_anonymously = forms.BooleanField(
- label=_('ask anonymously'),
- help_text=_(
- 'Check if you do not want to reveal your name '
- 'when asking this question'
- ),
- required=False,
- )
openid = forms.CharField(
required=False, max_length=255,
widget=forms.TextInput(attrs={'size': 40, 'class': 'openid-input'})
@@ -929,13 +926,22 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
user = kwargs.pop('user', None)
super(AskForm, self).__init__(*args, **kwargs)
#it's important that this field is set up dynamically
+ self.fields['title'] = TitleField()
self.fields['text'] = QuestionEditorField(user=user)
- #hide ask_anonymously field
+
+ self.fields['ask_anonymously'] = forms.BooleanField(
+ label=_('post anonymously'),
+ required=False
+ )
+
+ if user.is_anonymous() or not askbot_settings.ALLOW_ASK_ANONYMOUSLY:
+ self.hide_field('ask_anonymously')
+
if getattr(django_settings, 'ASKBOT_MULTILINGUAL', False):
self.fields['language'] = LanguageField()
- if askbot_settings.ALLOW_ASK_ANONYMOUSLY is False:
- self.hide_field('ask_anonymously')
+ if should_use_recaptcha(user):
+ self.fields['recaptcha'] = AskbotRecaptchaField()
def clean_ask_anonymously(self):
"""returns false if anonymous asking is not allowed
@@ -944,7 +950,6 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
self.cleaned_data['ask_anonymously'] = False
return self.cleaned_data['ask_anonymously']
-
ASK_BY_EMAIL_SUBJECT_HELP = _(
'Subject line is expected in the format: '
'[tag1, tag2, tag3,...] question title'
@@ -954,21 +959,17 @@ ASK_BY_EMAIL_SUBJECT_HELP = _(
class AskWidgetForm(forms.Form, FormWithHideableFields):
'''Simple form with just the title to ask a question'''
- title = TitleField()
ask_anonymously = forms.BooleanField(
label=_('ask anonymously'),
- help_text=_(
- 'Check if you do not want to reveal your name '
- 'when asking this question'
- ),
required=False,
)
def __init__(self, include_text=True, *args, **kwargs):
user = kwargs.pop('user', None)
super(AskWidgetForm, self).__init__(*args, **kwargs)
+ self.fields['title'] = TitleField()
#hide ask_anonymously field
- if not askbot_settings.ALLOW_ASK_ANONYMOUSLY:
+ if user.is_anonymous() or not askbot_settings.ALLOW_ASK_ANONYMOUSLY:
self.hide_field('ask_anonymously')
self.fields['text'] = QuestionEditorField(user=user)
if not include_text:
@@ -977,6 +978,9 @@ class AskWidgetForm(forms.Form, FormWithHideableFields):
self.fields['text'].required = False
self.fields['text'].min_length = 0
+ if should_use_recaptcha(user):
+ self.fields['recaptcha'] = AskbotRecaptchaField()
+
class CreateAskWidgetForm(forms.Form, FormWithHideableFields):
title = forms.CharField(max_length=100)
include_text_field = forms.BooleanField(required=False)
@@ -1120,7 +1124,11 @@ class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, *args, **kwargs):
super(AnswerForm, self).__init__(*args, **kwargs)
- self.fields['text'] = AnswerEditorField(user=kwargs['user'])
+ user = kwargs['user']
+ self.fields['text'] = AnswerEditorField(user=user)
+
+ if should_use_recaptcha(user):
+ self.fields['recaptcha'] = AskbotRecaptchaField()
def has_data(self):
"""True if form is bound or has inital data"""
@@ -1206,16 +1214,10 @@ class RevisionForm(forms.Form):
self.fields['revision'].initial = latest_revision.revision
class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
- title = TitleField()
tags = TagNamesField()
summary = SummaryField()
wiki = WikiField()
reveal_identity = forms.BooleanField(
- help_text=_(
- 'You have asked this question anonymously, '
- 'if you decide to reveal your identity, please check '
- 'this box.'
- ),
label=_('reveal identity'),
required=False,
)
@@ -1230,6 +1232,7 @@ class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
super(EditQuestionForm, self).__init__(*args, **kwargs)
#it is important to add this field dynamically
self.fields['text'] = QuestionEditorField(user=self.user)
+ self.fields['title'] = TitleField()
self.fields['title'].initial = revision.title
self.fields['text'].initial = revision.text
self.fields['tags'].initial = revision.tagnames
@@ -1241,6 +1244,9 @@ class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
if getattr(django_settings, 'ASKBOT_MULTILINGUAL', False):
self.fields['language'] = LanguageField()
+ if should_use_recaptcha(self.user):
+ self.fields['recaptcha'] = AskbotRecaptchaField()
+
def has_changed(self):
if super(EditQuestionForm, self).has_changed():
return True
@@ -1348,6 +1354,9 @@ class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
self.fields['text'].initial = revision.text
self.fields['wiki'].initial = answer.wiki
+ if should_use_recaptcha(user):
+ self.fields['recaptcha'] = AskbotRecaptchaField()
+
def has_changed(self):
#todo: this function is almost copy/paste of EditQuestionForm.has_changed()
if super(EditAnswerForm, self).has_changed():
@@ -1520,22 +1529,15 @@ class EditUserEmailFeedsForm(forms.Form):
'mentions_and_comments': 'i',
}
- asked_by_me = EmailFeedSettingField(
- label=_('Asked by me')
- )
- answered_by_me = EmailFeedSettingField(
- label=_('Answered by me')
- )
- individually_selected = EmailFeedSettingField(
- label=_('Individually selected')
- )
- all_questions = EmailFeedSettingField(
- label=_('Entire forum (tag filtered)'),
- )
-
- mentions_and_comments = EmailFeedSettingField(
- label=_('Comments and posts mentioning me'),
- )
+ def __init__(self, *args, **kwargs):
+ super(EditUserEmailFeedsForm, self).__init__(*args, **kwargs)
+ self.fields = SortedDict((
+ ('asked_by_me', EmailFeedSettingField(label=askbot_settings.WORDS_ASKED_BY_ME)),
+ ('answered_by_me', EmailFeedSettingField(label=askbot_settings.WORDS_ANSWERED_BY_ME)),
+ ('individually_selected', EmailFeedSettingField(label=_('Individually selected'))),
+ ('all_questions', EmailFeedSettingField(label=_('Entire forum (tag filtered)'))),
+ ('mentions_and_comments', EmailFeedSettingField(label=_('Comments and posts mentioning me')))
+ ))
def set_initial_values(self, user=None):
from askbot import models
@@ -1703,9 +1705,14 @@ class BulkTagSubscriptionForm(forms.Form):
if askbot_settings.GROUPS_ENABLED:
self.fields['groups'] = forms.ModelMultipleChoiceField(queryset=Group.objects.exclude_personal())
-class GetCommentsForPostForm(forms.Form):
+class GetDataForPostForm(forms.Form):
post_id = forms.IntegerField()
+class GetUserItemsForm(forms.Form):
+ page_size = forms.IntegerField(required=False)
+ page_number = forms.IntegerField(min_value=1)
+ user_id = forms.IntegerField()
+
class NewCommentForm(forms.Form):
comment = forms.CharField()
post_id = forms.IntegerField()
@@ -1716,5 +1723,5 @@ class EditCommentForm(forms.Form):
suppress_email = SuppressEmailField()
-class DeleteCommentForm(forms.Form):
+class ProcessCommentForm(forms.Form):
comment_id = forms.IntegerField()
diff --git a/askbot/importers/stackexchange/management/commands/load_stackexchange.py b/askbot/importers/stackexchange/management/commands/load_stackexchange.py
index ff6ddb15..902bc988 100644
--- a/askbot/importers/stackexchange/management/commands/load_stackexchange.py
+++ b/askbot/importers/stackexchange/management/commands/load_stackexchange.py
@@ -915,7 +915,7 @@ it may be helpful to split this procedure in two:\n
u = askbot.User()
u_type = se_u.user_type.name
if u_type == 'Administrator':
- u.set_admin_status()
+ u.set_status('d')
elif u_type == 'Moderator':
u.set_status('m')
elif u_type not in ('Unregistered', 'Registered'):
diff --git a/askbot/locale/en/LC_MESSAGES/django.mo b/askbot/locale/en/LC_MESSAGES/django.mo
index 9af75244..92662132 100644
--- a/askbot/locale/en/LC_MESSAGES/django.mo
+++ b/askbot/locale/en/LC_MESSAGES/django.mo
Binary files differ
diff --git a/askbot/locale/en/LC_MESSAGES/django.po b/askbot/locale/en/LC_MESSAGES/django.po
index bc0c0ccb..bbab876b 100644
--- a/askbot/locale/en/LC_MESSAGES/django.po
+++ b/askbot/locale/en/LC_MESSAGES/django.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.7\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-07-13 14:06-0500\n"
+"POT-Creation-Date: 2013-10-16 16:34-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Evgeny Fadeev <evgeny.fadeev@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -25,8 +25,14 @@ msgstr ""
msgid " - "
msgstr ""
-#: feed.py:35 feed.py:109
-msgid "Individual question feed"
+#: feed.py:35
+#, python-format
+msgid "Individual %(question)s feed"
+msgstr ""
+
+#: feed.py:109
+#, python-format
+msgid "Latest %(question)s feed"
msgstr ""
#: forms.py:140
@@ -64,8 +70,9 @@ msgstr ""
msgid "title"
msgstr ""
-#: forms.py:249 templates/embed/ask_by_widget.html:170
-msgid "Please enter your question"
+#: forms.py:249
+#, python-format
+msgid "Please enter your %(question)s"
msgstr ""
#: forms.py:260
@@ -75,39 +82,51 @@ msgid_plural "must have > %d characters"
msgstr[0] ""
msgstr[1] ""
-#: forms.py:270
+#: forms.py:271
#, python-format
-msgid "The question is too long, maximum allowed size is %d characters"
+msgid ""
+"The %(question)s is too long, maximum allowed size is %(length)d characters"
msgstr ""
-#: forms.py:277
+#: forms.py:278
#, python-format
-msgid "The question is too long, maximum allowed size is %d bytes"
+msgid "The %(question)s is too long, maximum allowed size is %(length)d bytes"
msgstr ""
-#: forms.py:309
+#: forms.py:307
msgid "content"
msgstr ""
-#: forms.py:370
+#: forms.py:311
+msgid "post"
+msgstr ""
+
+#: forms.py:318
+#, python-format
+msgid "%(post)s content must be > %(count)d character"
+msgid_plural "%(post)s content must be > %(count)d characters"
+msgstr[0] ""
+msgstr[1] ""
+
+#: forms.py:366
#, python-format
msgid "each tag must be shorter than %(max_chars)d character"
msgid_plural "each tag must be shorter than %(max_chars)d characters"
msgstr[0] ""
msgstr[1] ""
-#: forms.py:407
+#: forms.py:403
msgid ""
"We ran out of space for recording the tags. Please shorten or delete some of "
"them."
msgstr ""
-#: forms.py:410 forms.py:1006 models/widgets.py:27
+#: forms.py:406 forms.py:1006 models/widgets.py:27
#: templates/widgets/edit_post.html:32 templates/widgets/meta_nav.html:6
msgid "tags"
msgstr ""
-#: forms.py:412
+#: forms.py:408
#, python-format
msgid ""
"Tags are short keywords, with no spaces within. Up to %(max_tags)d tag can "
@@ -118,156 +137,163 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: forms.py:439
+#: forms.py:435
#, python-format
msgid "please use %(tag_count)d tag or less"
msgid_plural "please use %(tag_count)d tags or less"
msgstr[0] ""
msgstr[1] ""
-#: forms.py:447
+#: forms.py:443
#, python-format
msgid "At least one of the following tags is required : %(tags)s"
msgstr ""
-#: forms.py:475
+#: forms.py:471
msgid "community wiki (karma is not awarded & many others can edit wiki post)"
msgstr ""
-#: forms.py:479
+#: forms.py:475
+#, python-format
msgid ""
-"if you choose community wiki option, the question and answer do not generate "
-"points and name of author will not be shown"
+"if you choose community wiki option, the %(question)s and answer do not "
+"generate points and name of author will not be shown"
+msgstr ""
+
+#: forms.py:478 conf/words.py:90
+msgid "question"
msgstr ""
-#: forms.py:496
+#: forms.py:492
msgid "update summary:"
msgstr ""
-#: forms.py:498
+#: forms.py:494
msgid ""
"enter a brief summary of your revision (e.g. fixed spelling, grammar, "
"improved style, this field is optional)"
msgstr ""
-#: forms.py:585
+#: forms.py:583
msgid "Enter number of points to add or subtract"
msgstr ""
-#: forms.py:600 const/__init__.py:375
+#: forms.py:598 const/__init__.py:380
msgid "approved"
msgstr ""
-#: forms.py:601 const/__init__.py:376
+#: forms.py:599 const/__init__.py:381
msgid "watched"
msgstr ""
-#: forms.py:602 const/__init__.py:377
+#: forms.py:600 const/__init__.py:382
msgid "suspended"
msgstr ""
-#: forms.py:603 const/__init__.py:378
+#: forms.py:601 const/__init__.py:383
msgid "blocked"
msgstr ""
-#: forms.py:605
+#: forms.py:603
msgid "administrator"
msgstr ""
-#: forms.py:606 const/__init__.py:374
+#: forms.py:604 const/__init__.py:379
msgid "moderator"
msgstr ""
-#: forms.py:625
+#: forms.py:623
msgid "Change status to"
msgstr ""
-#: forms.py:652
+#: forms.py:650
msgid "which one?"
msgstr ""
-#: forms.py:673
+#: forms.py:671
msgid "Cannot change own status"
msgstr ""
-#: forms.py:679
+#: forms.py:677
msgid "Cannot turn other user to moderator"
msgstr ""
-#: forms.py:686
+#: forms.py:684
msgid "Cannot change status of another moderator"
msgstr ""
-#: forms.py:692
+#: forms.py:690
msgid "Cannot change status to admin"
msgstr ""
-#: forms.py:698
+#: forms.py:696
#, python-format
msgid ""
"If you wish to change %(username)s's status, please make a meaningful "
"selection."
msgstr ""
-#: forms.py:708
+#: forms.py:706
msgid "Subject line"
msgstr ""
-#: forms.py:713
+#: forms.py:711
msgid "Message text"
msgstr ""
-#: forms.py:727
+#: forms.py:725
msgid "Your name (optional):"
msgstr ""
-#: forms.py:728
+#: forms.py:726
msgid "Email:"
msgstr ""
-#: forms.py:730
+#: forms.py:728
msgid "Your message:"
msgstr ""
-#: forms.py:735
+#: forms.py:733
msgid "I don't want to give my email or receive a response:"
msgstr ""
-#: forms.py:758
+#: forms.py:756
msgid "Please mark \"I dont want to give my mail\" field."
msgstr ""
-#: forms.py:791
+#: forms.py:789
msgid "keep private within your groups"
msgstr ""
-#: forms.py:830
+#: forms.py:828 templates/ask.html:58 templates/question.html:338
#, fuzzy
msgid "User name:"
msgstr "User login"
-#: forms.py:832
+#: forms.py:830
msgid "Enter name to post on behalf of someone else. Can create new accounts."
msgstr ""
-#: forms.py:839
+#: forms.py:837 templates/question.html:339
msgid "Email address:"
msgstr ""
-#: forms.py:889
+#: forms.py:887
msgid "User name is required with the email"
msgstr ""
-#: forms.py:894
+#: forms.py:892
msgid "Email is required if user name is added"
msgstr ""
-#: forms.py:914 forms.py:957
-msgid "ask anonymously"
+#: forms.py:923
+msgid "post anonymously"
msgstr ""
-#: forms.py:916 forms.py:959
-msgid "Check if you do not want to reveal your name when asking this question"
+#: forms.py:925
+#, python-format
+msgid ""
+"Check if you do not want to reveal your name when posting this %(question)s"
msgstr ""
#: forms.py:947
@@ -275,6 +301,14 @@ msgid ""
"Subject line is expected in the format: [tag1, tag2, tag3,...] question title"
msgstr ""
+#: forms.py:957
+msgid "ask anonymously"
+msgstr ""
+
+#: forms.py:959
+msgid "Check if you do not want to reveal your name when asking this question"
+msgstr ""
+
#: forms.py:1213
msgid ""
"You have asked this question anonymously, if you decide to reveal your "
@@ -403,154 +437,154 @@ msgstr ""
msgid "Your post at %(site_name)s is now published"
msgstr ""
-#: urls.py:44
+#: urls.py:44 conf/words.py:99
msgid "questions"
msgstr ""
-#: urls.py:56
+#: urls.py:57
msgid "question/"
msgstr ""
-#: urls.py:61
+#: urls.py:62
msgid "tags/"
msgstr ""
-#: urls.py:66 urls.py:71 urls.py:78 urls.py:84 urls.py:93 urls.py:100
+#: urls.py:67 urls.py:72 urls.py:79 urls.py:85 urls.py:94 urls.py:101
msgid "users/"
msgstr ""
-#: urls.py:71
+#: urls.py:72
msgid "by-group/"
msgstr ""
-#: urls.py:78 urls.py:159 urls.py:226 urls.py:520
+#: urls.py:79 urls.py:160 urls.py:237 urls.py:531
msgid "edit/"
msgstr ""
-#: urls.py:85
+#: urls.py:86
msgid "subscriptions/"
msgstr ""
-#: urls.py:94
+#: urls.py:95
msgid "select_languages/"
msgstr ""
-#: urls.py:105
+#: urls.py:106
msgid "groups/"
msgstr ""
-#: urls.py:110
+#: urls.py:111
msgid "users/update_has_custom_avatar/"
msgstr ""
-#: urls.py:115 urls.py:120
+#: urls.py:116 urls.py:121
msgid "badges/"
msgstr ""
-#: urls.py:133
+#: urls.py:134
msgid "feedback/"
msgstr ""
-#: urls.py:154
+#: urls.py:155
msgid "about/"
msgstr ""
-#: urls.py:155
+#: urls.py:156
msgid "faq/"
msgstr ""
-#: urls.py:156
+#: urls.py:157
msgid "privacy/"
msgstr ""
-#: urls.py:157
+#: urls.py:158
msgid "help/"
msgstr ""
-#: urls.py:159 urls.py:164
+#: urls.py:160 urls.py:165
msgid "answers/"
msgstr ""
-#: urls.py:164 urls.py:256
+#: urls.py:165 urls.py:267
msgid "revisions/"
msgstr ""
-#: urls.py:221 urls.py:226 urls.py:231 urls.py:236 urls.py:241 urls.py:246
-#: urls.py:256
+#: urls.py:232 urls.py:237 urls.py:242 urls.py:247 urls.py:252 urls.py:257
+#: urls.py:267
msgid "questions/"
msgstr ""
-#: urls.py:221 urls.py:495 urls.py:500 urls.py:505 urls.py:510
+#: urls.py:232 urls.py:506 urls.py:511 urls.py:516 urls.py:521
msgid "ask/"
msgstr ""
-#: urls.py:231
+#: urls.py:242
msgid "retag/"
msgstr ""
-#: urls.py:236
+#: urls.py:247
msgid "close/"
msgstr ""
-#: urls.py:241
+#: urls.py:252
msgid "reopen/"
msgstr ""
-#: urls.py:246
+#: urls.py:257
msgid "answer/"
msgstr ""
-#: urls.py:314
+#: urls.py:325
msgid "tags/subscriptions/"
msgstr ""
-#: urls.py:319
+#: urls.py:330
msgid "tags/subscriptions/delete/"
msgstr ""
-#: urls.py:324
+#: urls.py:335
msgid "tags/subscriptions/create/"
msgstr ""
-#: urls.py:329
+#: urls.py:340
msgid "tags/subscriptions/edit/"
msgstr ""
-#: urls.py:334
+#: urls.py:345
msgid "suggested-tags/"
msgstr ""
-#: urls.py:459
+#: urls.py:470
msgid "messages/"
msgstr ""
-#: urls.py:459
+#: urls.py:470
msgid "markread/"
msgstr ""
-#: urls.py:490 urls.py:495 urls.py:500 urls.py:505 urls.py:510 urls.py:515
-#: urls.py:520 urls.py:525 urls.py:530
+#: urls.py:501 urls.py:506 urls.py:511 urls.py:516 urls.py:521 urls.py:526
+#: urls.py:531 urls.py:536 urls.py:541
msgid "widgets/"
msgstr ""
-#: urls.py:510 deps/django_authopenid/urls.py:20
+#: urls.py:521 deps/django_authopenid/urls.py:20
msgid "complete/"
msgstr ""
-#: urls.py:515
+#: urls.py:526
msgid "create/"
msgstr ""
-#: urls.py:525
+#: urls.py:536
msgid "delete/"
msgstr ""
-#: urls.py:560
+#: urls.py:571
msgid "upload/"
msgstr ""
-#: urls.py:585 setup_templates/settings.py:229
-#: templates/authopenid/providers_javascript.html:7
+#: urls.py:596 setup_templates/settings.py:232
+#: templates/authopenid/providers_javascript.html:8
msgid "account/"
msgstr ""
@@ -713,164 +747,172 @@ msgid "Enable email alerts"
msgstr ""
#: conf/email.py:62
-msgid "Maximum number of news entries in an email alert"
+msgid "Enable HTML-formatted email"
+msgstr ""
+
+#: conf/email.py:63
+msgid "May not be supported by some email clients"
msgstr ""
#: conf/email.py:72
+msgid "Maximum number of news entries in an email alert"
+msgstr ""
+
+#: conf/email.py:82
msgid "Default notification frequency all questions"
msgstr ""
-#: conf/email.py:74
+#: conf/email.py:84
msgid "Option to define frequency of emailed updates for: all questions."
msgstr ""
-#: conf/email.py:86
+#: conf/email.py:96
msgid "Default notification frequency questions asked by the user"
msgstr ""
-#: conf/email.py:88
+#: conf/email.py:98
msgid ""
"Option to define frequency of emailed updates for: Question asked by the "
"user."
msgstr ""
-#: conf/email.py:100
+#: conf/email.py:110
msgid "Default notification frequency questions answered by the user"
msgstr ""
-#: conf/email.py:102
+#: conf/email.py:112
msgid ""
"Option to define frequency of emailed updates for: Question answered by the "
"user."
msgstr ""
-#: conf/email.py:114
+#: conf/email.py:124
msgid ""
"Default notification frequency questions individually "
"selected by the user"
msgstr ""
-#: conf/email.py:117
+#: conf/email.py:127
msgid ""
"Option to define frequency of emailed updates for: Question individually "
"selected by the user."
msgstr ""
-#: conf/email.py:129
+#: conf/email.py:139
msgid ""
"Default notification frequency for mentions and "
"comments"
msgstr ""
-#: conf/email.py:132
+#: conf/email.py:142
msgid ""
"Option to define frequency of emailed updates for: Mentions and comments."
msgstr ""
-#: conf/email.py:143
+#: conf/email.py:153
msgid "Send periodic reminders about unanswered questions"
msgstr ""
-#: conf/email.py:145
+#: conf/email.py:155
msgid ""
"NOTE: in order to use this feature, it is necessary to run the management "
"command \"send_unanswered_question_reminders\" (for example, via a cron job "
"- with an appropriate frequency) "
msgstr ""
-#: conf/email.py:158
+#: conf/email.py:168
msgid "Days before starting to send reminders about unanswered questions"
msgstr ""
-#: conf/email.py:169
+#: conf/email.py:179
msgid ""
"How often to send unanswered question reminders (in days between the "
"reminders sent)."
msgstr ""
-#: conf/email.py:181
+#: conf/email.py:191
msgid "Max. number of reminders to send about unanswered questions"
msgstr ""
-#: conf/email.py:192
+#: conf/email.py:202
msgid "Send periodic reminders to accept the best answer"
msgstr ""
-#: conf/email.py:194
+#: conf/email.py:204
msgid ""
"NOTE: in order to use this feature, it is necessary to run the management "
"command \"send_accept_answer_reminders\" (for example, via a cron job - with "
"an appropriate frequency) "
msgstr ""
-#: conf/email.py:207
+#: conf/email.py:217
msgid "Days before starting to send reminders to accept an answer"
msgstr ""
-#: conf/email.py:218
+#: conf/email.py:228
msgid ""
"How often to send accept answer reminders (in days between the reminders "
"sent)."
msgstr ""
-#: conf/email.py:230
+#: conf/email.py:240
msgid "Max. number of reminders to send to accept the best answer"
msgstr ""
-#: conf/email.py:242
+#: conf/email.py:252
msgid "Require email verification before allowing to post"
msgstr ""
-#: conf/email.py:243
+#: conf/email.py:253
msgid ""
"Active email verification is done by sending a verification key in email"
msgstr ""
-#: conf/email.py:252
+#: conf/email.py:262
msgid "Fake email for anonymous user"
msgstr ""
-#: conf/email.py:253
+#: conf/email.py:263
msgid "Use this setting to control gravatar for email-less user"
msgstr ""
-#: conf/email.py:262
+#: conf/email.py:272
msgid "Allow posting questions by email"
msgstr ""
-#: conf/email.py:264
+#: conf/email.py:274
msgid ""
"Before enabling this setting - please fill out IMAP settings in the settings."
"py file"
msgstr ""
-#: conf/email.py:275
+#: conf/email.py:285
msgid "Replace space in emailed tags with dash"
msgstr ""
-#: conf/email.py:277
+#: conf/email.py:287
msgid ""
"This setting applies to tags written in the subject line of questions asked "
"by email"
msgstr ""
-#: conf/email.py:288
+#: conf/email.py:298
msgid "Enable posting answers and comments by email"
msgstr ""
-#: conf/email.py:291
+#: conf/email.py:301
msgid "To enable this feature make sure lamson is running"
msgstr ""
-#: conf/email.py:302
+#: conf/email.py:312
msgid "Emailed post: when to notify author about publishing"
msgstr ""
-#: conf/email.py:327
+#: conf/email.py:337
msgid "Reply by email hostname"
msgstr ""
-#: conf/email.py:338
+#: conf/email.py:348
msgid ""
"Email replies having fewer words than this number will be posted as comments "
"instead of answers"
@@ -1105,8 +1147,8 @@ msgstr ""
#: conf/forum_data_rules.py:135
msgid ""
-"To use folded mode, please first set minimum question body length to 0. Also "
-"- please make tags optional."
+"<b style=\"color:red;\">To use folded mode, please first set minimum "
+"question body length to 0. Also - please make tags optional.</b>"
msgstr ""
#: conf/forum_data_rules.py:147
@@ -1147,156 +1189,160 @@ msgstr ""
msgid "Enable accepting best answer"
msgstr ""
-#: conf/forum_data_rules.py:231
+#: conf/forum_data_rules.py:233
+msgid "How to sort answers by default"
+msgstr ""
+
+#: conf/forum_data_rules.py:241
msgid "Are tags required?"
msgstr ""
-#: conf/forum_data_rules.py:237
+#: conf/forum_data_rules.py:247
msgid "category tree"
msgstr ""
-#: conf/forum_data_rules.py:238
+#: conf/forum_data_rules.py:248
#, fuzzy
msgid "user input"
msgstr "User login"
-#: conf/forum_data_rules.py:245
+#: conf/forum_data_rules.py:255
msgid "Source of tags"
msgstr ""
-#: conf/forum_data_rules.py:256
+#: conf/forum_data_rules.py:266
msgid "Mandatory tags"
msgstr ""
-#: conf/forum_data_rules.py:259
+#: conf/forum_data_rules.py:269
msgid ""
"At least one of these tags will be required for any new or newly edited "
"question. A mandatory tag may be wildcard, if the wildcard tags are active."
msgstr ""
-#: conf/forum_data_rules.py:271
+#: conf/forum_data_rules.py:281
msgid "Force lowercase the tags"
msgstr ""
-#: conf/forum_data_rules.py:273
+#: conf/forum_data_rules.py:283
msgid ""
"Attention: after checking this, please back up the database, and run a "
"management command: <code>python manage.py fix_question_tags</code> to "
"globally rename the tags"
msgstr ""
-#: conf/forum_data_rules.py:287
+#: conf/forum_data_rules.py:297
msgid "Format of tag list"
msgstr ""
-#: conf/forum_data_rules.py:289
+#: conf/forum_data_rules.py:299
msgid ""
"Select the format to show tags in, either as a simple list, or as a tag cloud"
msgstr ""
-#: conf/forum_data_rules.py:301
+#: conf/forum_data_rules.py:311
msgid "Use wildcard tags"
msgstr ""
-#: conf/forum_data_rules.py:303
+#: conf/forum_data_rules.py:313
msgid ""
"Wildcard tags can be used to follow or ignore many tags at once, a valid "
"wildcard tag has a single wildcard at the very end"
msgstr ""
-#: conf/forum_data_rules.py:315
+#: conf/forum_data_rules.py:325
msgid "Use separate set for subscribed tags"
msgstr ""
-#: conf/forum_data_rules.py:317
+#: conf/forum_data_rules.py:327
msgid ""
"If enabled, users will have a third set of tag selections - \"subscribed"
"\" (by email) in additon to \"interesting\" and \"ignored\""
msgstr ""
-#: conf/forum_data_rules.py:325
+#: conf/forum_data_rules.py:335
msgid "Always, for all users"
msgstr ""
-#: conf/forum_data_rules.py:326
+#: conf/forum_data_rules.py:336
msgid "Never, for all users"
msgstr ""
-#: conf/forum_data_rules.py:327
+#: conf/forum_data_rules.py:337
msgid "Let users decide"
msgstr ""
-#: conf/forum_data_rules.py:335
+#: conf/forum_data_rules.py:345
msgid "Publicly show user tag selections"
msgstr ""
-#: conf/forum_data_rules.py:344
+#: conf/forum_data_rules.py:354
msgid "Enable separate tag search box on main page"
msgstr ""
-#: conf/forum_data_rules.py:354
+#: conf/forum_data_rules.py:364
msgid "Default max number of comments to display under posts"
msgstr ""
-#: conf/forum_data_rules.py:365
+#: conf/forum_data_rules.py:375
#, python-format
msgid "Maximum comment length, must be < %(max_len)s"
msgstr ""
-#: conf/forum_data_rules.py:375
+#: conf/forum_data_rules.py:385
msgid "Limit time to edit comments"
msgstr ""
-#: conf/forum_data_rules.py:377
+#: conf/forum_data_rules.py:387
msgid "If unchecked, there will be no time limit to edit the comments"
msgstr ""
-#: conf/forum_data_rules.py:388
+#: conf/forum_data_rules.py:398
msgid "Minutes allowed to edit a comment"
msgstr ""
-#: conf/forum_data_rules.py:389
+#: conf/forum_data_rules.py:399
msgid "To enable this setting, check the previous one"
msgstr ""
-#: conf/forum_data_rules.py:398
+#: conf/forum_data_rules.py:408
msgid "Save comment by pressing <Enter> key"
msgstr ""
-#: conf/forum_data_rules.py:400
+#: conf/forum_data_rules.py:410
msgid ""
"This may be useful when only one-line comments are desired. Will not work "
"with TinyMCE editor."
msgstr ""
-#: conf/forum_data_rules.py:411
+#: conf/forum_data_rules.py:421
msgid "Minimum length of search term for Ajax search"
msgstr ""
-#: conf/forum_data_rules.py:412
+#: conf/forum_data_rules.py:422
msgid "Must match the corresponding database backend setting"
msgstr ""
-#: conf/forum_data_rules.py:421
+#: conf/forum_data_rules.py:431
msgid "Do not make text query sticky in search"
msgstr ""
-#: conf/forum_data_rules.py:423
+#: conf/forum_data_rules.py:433
msgid ""
"Check to disable the \"sticky\" behavior of the search query. This may be "
"useful if you want to move the search bar away from the default position or "
"do not like the default sticky behavior of the text search query."
msgstr ""
-#: conf/forum_data_rules.py:436
+#: conf/forum_data_rules.py:446
msgid "Maximum number of tags per question"
msgstr ""
-#: conf/forum_data_rules.py:448
+#: conf/forum_data_rules.py:458
msgid "Number of questions to list by default"
msgstr ""
-#: conf/forum_data_rules.py:458
+#: conf/forum_data_rules.py:468
msgid "What should \"unanswered question\" mean?"
msgstr ""
@@ -1900,7 +1946,7 @@ msgstr ""
msgid "Main page sidebar"
msgstr ""
-#: conf/sidebar_main.py:20 conf/sidebar_question.py:67
+#: conf/sidebar_main.py:20 conf/sidebar_question.py:76
msgid "Custom sidebar header"
msgstr ""
@@ -1914,7 +1960,7 @@ msgstr ""
#: conf/sidebar_main.py:36 conf/sidebar_main.py:111 conf/sidebar_profile.py:37
#: conf/sidebar_question.py:34 conf/sidebar_question.py:58
-#: conf/sidebar_question.py:84 conf/sidebar_question.py:149
+#: conf/sidebar_question.py:93 conf/sidebar_question.py:158
msgid "Show above only to anonymous users"
msgstr ""
@@ -1949,11 +1995,11 @@ msgid ""
"Uncheck this if you want to hide the tag cloud or tag list from the sidebar "
msgstr ""
-#: conf/sidebar_main.py:94 conf/sidebar_question.py:132
+#: conf/sidebar_main.py:94 conf/sidebar_question.py:141
msgid "Custom sidebar footer"
msgstr ""
-#: conf/sidebar_main.py:97 conf/sidebar_question.py:135
+#: conf/sidebar_main.py:97 conf/sidebar_question.py:144
msgid ""
"Use this area to enter content at the BOTTOM of the sidebarin HTML format. "
"When using this option (as well as the sidebar header), please use the HTML "
@@ -1989,12 +2035,16 @@ msgstr ""
#: conf/sidebar_question.py:45
msgid ""
-"This banner will show above the second answer. When using this option, "
-"please use the HTML validation service to make sure that your input is valid "
-"and works well in all browsers."
+"This banner will show under the first answer. When using this option, please "
+"use the HTML validation service to make sure that your input is valid and "
+"works well in all browsers."
msgstr ""
-#: conf/sidebar_question.py:70
+#: conf/sidebar_question.py:67
+msgid "Show answers banner even if there are no answers"
+msgstr ""
+
+#: conf/sidebar_question.py:79
msgid ""
"Use this area to enter content at the TOP of the sidebarin HTML format. When "
"using this option (as well as the sidebar footer), please use the HTML "
@@ -2002,29 +2052,29 @@ msgid ""
"all browsers."
msgstr ""
-#: conf/sidebar_question.py:92
+#: conf/sidebar_question.py:101
msgid "Show tag list in sidebar"
msgstr ""
-#: conf/sidebar_question.py:94
+#: conf/sidebar_question.py:103
msgid "Uncheck this if you want to hide the tag list from the sidebar "
msgstr ""
-#: conf/sidebar_question.py:105
+#: conf/sidebar_question.py:114
msgid "Show meta information in sidebar"
msgstr ""
-#: conf/sidebar_question.py:107
+#: conf/sidebar_question.py:116
msgid ""
"Uncheck this if you want to hide the meta information about the question "
"(post date, views, last updated). "
msgstr ""
-#: conf/sidebar_question.py:119
+#: conf/sidebar_question.py:128
msgid "Show related questions in sidebar"
msgstr ""
-#: conf/sidebar_question.py:121
+#: conf/sidebar_question.py:130
msgid "Uncheck this if you want to hide the list of related questions. "
msgstr ""
@@ -2112,105 +2162,25 @@ msgstr ""
msgid "To change the logo, select new file, then submit this whole form."
msgstr ""
-#: conf/skin_general_settings.py:34
-msgid "English"
-msgstr ""
-
-#: conf/skin_general_settings.py:35
-msgid "Spanish"
-msgstr ""
-
-#: conf/skin_general_settings.py:36
-msgid "Catalan"
-msgstr ""
-
-#: conf/skin_general_settings.py:37
-msgid "German"
-msgstr ""
-
-#: conf/skin_general_settings.py:38
-msgid "Greek"
-msgstr ""
-
-#: conf/skin_general_settings.py:39
-msgid "Finnish"
-msgstr ""
-
-#: conf/skin_general_settings.py:40
-msgid "French"
-msgstr ""
-
-#: conf/skin_general_settings.py:41
-msgid "Hindi"
-msgstr ""
-
#: conf/skin_general_settings.py:42
-msgid "Hungarian"
-msgstr ""
-
-#: conf/skin_general_settings.py:43
-msgid "Italian"
-msgstr ""
-
-#: conf/skin_general_settings.py:44
-msgid "Japanese"
-msgstr ""
-
-#: conf/skin_general_settings.py:45
-msgid "Korean"
-msgstr ""
-
-#: conf/skin_general_settings.py:46
-msgid "Portuguese"
-msgstr ""
-
-#: conf/skin_general_settings.py:47
-msgid "Brazilian Portuguese"
-msgstr ""
-
-#: conf/skin_general_settings.py:48
-msgid "Romanian"
-msgstr ""
-
-#: conf/skin_general_settings.py:49
-msgid "Russian"
+msgid "Select Language"
msgstr ""
#: conf/skin_general_settings.py:50
-msgid "Serbian"
-msgstr ""
-
-#: conf/skin_general_settings.py:51
-msgid "Turkish"
-msgstr ""
-
-#: conf/skin_general_settings.py:52
-msgid "Vietnamese"
-msgstr ""
-
-#: conf/skin_general_settings.py:53
-msgid "Chinese"
-msgstr ""
-
-#: conf/skin_general_settings.py:54
-msgid "Chinese (Taiwan)"
-msgstr ""
-
-#: conf/skin_general_settings.py:73
msgid "Show logo"
msgstr ""
-#: conf/skin_general_settings.py:75
+#: conf/skin_general_settings.py:52
msgid ""
"Check if you want to show logo in the forum header or uncheck in the case "
"you do not want the logo to appear in the default location"
msgstr ""
-#: conf/skin_general_settings.py:87
+#: conf/skin_general_settings.py:64
msgid "Site favicon"
msgstr ""
-#: conf/skin_general_settings.py:89
+#: conf/skin_general_settings.py:66
#, python-format
msgid ""
"A small 16x16 or 32x32 pixel icon image used to distinguish your site in the "
@@ -2218,40 +2188,40 @@ msgid ""
"href=\"%(favicon_info_url)s\">this page</a>."
msgstr ""
-#: conf/skin_general_settings.py:105
+#: conf/skin_general_settings.py:82
msgid "Password login button"
msgstr ""
-#: conf/skin_general_settings.py:107
+#: conf/skin_general_settings.py:84
msgid ""
"An 88x38 pixel image that is used on the login screen for the password login "
"button."
msgstr ""
-#: conf/skin_general_settings.py:120
+#: conf/skin_general_settings.py:97
msgid "Show all UI functions to all users"
msgstr ""
-#: conf/skin_general_settings.py:122
+#: conf/skin_general_settings.py:99
msgid ""
"If checked, all forum functions will be shown to users, regardless of their "
"reputation. However to use those functions, moderation rules, reputation and "
"other limits will still apply."
msgstr ""
-#: conf/skin_general_settings.py:137
+#: conf/skin_general_settings.py:114
msgid "Select skin"
msgstr ""
-#: conf/skin_general_settings.py:148
+#: conf/skin_general_settings.py:125
msgid "Customize HTML <HEAD>"
msgstr ""
-#: conf/skin_general_settings.py:157
+#: conf/skin_general_settings.py:134
msgid "Custom portion of the HTML <HEAD>"
msgstr ""
-#: conf/skin_general_settings.py:159
+#: conf/skin_general_settings.py:136
msgid ""
"<strong>To use this option</strong>, check \"Customize HTML &lt;HEAD&gt;\" "
"above. Contents of this box will be inserted into the &lt;HEAD&gt; portion "
@@ -2263,11 +2233,11 @@ msgid ""
"please test the site with the W3C HTML validator service."
msgstr ""
-#: conf/skin_general_settings.py:181
+#: conf/skin_general_settings.py:158
msgid "Custom header additions"
msgstr ""
-#: conf/skin_general_settings.py:183
+#: conf/skin_general_settings.py:160
msgid ""
"Header is the bar at the top of the content that contains user info and site "
"links, and is common to all pages. Use this area to enter contents of the "
@@ -2276,21 +2246,21 @@ msgid ""
"sure that your input is valid and works well in all browsers."
msgstr ""
-#: conf/skin_general_settings.py:198
+#: conf/skin_general_settings.py:175
msgid "Site footer mode"
msgstr ""
-#: conf/skin_general_settings.py:200
+#: conf/skin_general_settings.py:177
msgid ""
"Footer is the bottom portion of the content, which is common to all pages. "
"You can disable, customize, or use the default footer."
msgstr ""
-#: conf/skin_general_settings.py:217
+#: conf/skin_general_settings.py:194
msgid "Custom footer (HTML format)"
msgstr ""
-#: conf/skin_general_settings.py:219
+#: conf/skin_general_settings.py:196
msgid ""
"<strong>To enable this function</strong>, please select option 'customize' "
"in the \"Site footer mode\" above. Use this area to enter contents of the "
@@ -2299,21 +2269,21 @@ msgid ""
"that your input is valid and works well in all browsers."
msgstr ""
-#: conf/skin_general_settings.py:234
+#: conf/skin_general_settings.py:211
msgid "Apply custom style sheet (CSS)"
msgstr ""
-#: conf/skin_general_settings.py:236
+#: conf/skin_general_settings.py:213
msgid ""
"Check if you want to change appearance of your form by adding custom style "
"sheet rules (please see the next item)"
msgstr ""
-#: conf/skin_general_settings.py:248
+#: conf/skin_general_settings.py:225
msgid "Custom style sheet (CSS)"
msgstr ""
-#: conf/skin_general_settings.py:250
+#: conf/skin_general_settings.py:227
msgid ""
"<strong>To use this function</strong>, check \"Apply custom style sheet\" "
"option above. The CSS rules added in this window will be applied after the "
@@ -2322,19 +2292,19 @@ msgid ""
"depends (default is empty string) on the url configuration in your urls.py."
msgstr ""
-#: conf/skin_general_settings.py:266
+#: conf/skin_general_settings.py:243
msgid "Add custom javascript"
msgstr ""
-#: conf/skin_general_settings.py:269
+#: conf/skin_general_settings.py:246
msgid "Check to enable javascript that you can enter in the next field"
msgstr ""
-#: conf/skin_general_settings.py:279
+#: conf/skin_general_settings.py:256
msgid "Custom javascript"
msgstr ""
-#: conf/skin_general_settings.py:281
+#: conf/skin_general_settings.py:258
msgid ""
"Type or paste plain javascript that you would like to run on your site. Link "
"to the script will be inserted at the bottom of the HTML output and will be "
@@ -2345,19 +2315,19 @@ msgid ""
"above)."
msgstr ""
-#: conf/skin_general_settings.py:299
+#: conf/skin_general_settings.py:276
msgid "Skin media revision number"
msgstr ""
-#: conf/skin_general_settings.py:301
+#: conf/skin_general_settings.py:278
msgid "Will be set automatically but you can modify it if necessary."
msgstr ""
-#: conf/skin_general_settings.py:312
+#: conf/skin_general_settings.py:289
msgid "Hash to update the media revision number automatically."
msgstr ""
-#: conf/skin_general_settings.py:316
+#: conf/skin_general_settings.py:293
msgid "Will be set automatically, it is not necesary to modify manually."
msgstr ""
@@ -2555,6 +2525,208 @@ msgid ""
"question poster"
msgstr ""
+#: conf/words.py:14
+msgid "Site term vocalbulary"
+msgstr ""
+
+#: conf/words.py:22 conf/words.py:23 templates/ask.html:4
+msgid "Ask Your Question"
+msgstr ""
+
+#: conf/words.py:24 conf/words.py:34 conf/words.py:44 conf/words.py:54
+#: conf/words.py:64
+msgid "Used on a button"
+msgstr ""
+
+#: conf/words.py:32 conf/words.py:33
+msgid "Ask the Group"
+msgstr ""
+
+#: conf/words.py:42 conf/words.py:43
+msgid "Post Your Answer"
+msgstr ""
+
+#: conf/words.py:52 conf/words.py:53
+msgid "Answer Your Own Question"
+msgstr ""
+
+#: conf/words.py:62 conf/words.py:63
+msgid "Edit Your Previous Answer"
+msgstr ""
+
+#: conf/words.py:72 conf/words.py:73
+msgid "asked"
+msgstr ""
+
+#: conf/words.py:81 conf/words.py:82
+msgid "answered"
+msgstr ""
+
+#: conf/words.py:91
+msgid "question (noun, singular)"
+msgstr ""
+
+#: conf/words.py:100
+msgid "questions (noun, plural)"
+msgstr ""
+
+#: conf/words.py:108 templates/widgets/question_summary.html:30
+msgid "answer"
+msgstr ""
+
+#: conf/words.py:109
+msgid "answer (noun, sungular)"
+msgstr ""
+
+#: conf/words.py:117 conf/words.py:118
+msgid "Show only questions from"
+msgstr ""
+
+#: conf/words.py:126 conf/words.py:127
+msgid "Please ask your question here"
+msgstr ""
+
+#: conf/words.py:135 conf/words.py:136
+msgid "Please enter your question"
+msgstr ""
+
+#: conf/words.py:144 conf/words.py:145
+msgid "ask a question interesting to this community"
+msgstr ""
+
+#: conf/words.py:153 conf/words.py:154
+msgid "No questions here."
+msgstr ""
+
+#: conf/words.py:162 conf/words.py:163
+msgid "Please follow some questions or follow some users."
+msgstr ""
+
+#: conf/words.py:171 conf/words.py:172
+msgid "Please feel free to ask your question!"
+msgstr ""
+
+#: conf/words.py:180 conf/words.py:181
+msgid "swap with question"
+msgstr ""
+
+#: conf/words.py:189 conf/words.py:190
+msgid "repost as a question comment"
+msgstr ""
+
+#: conf/words.py:198 conf/words.py:199
+msgid "repost as a comment under older answer"
+msgstr ""
+
+#: conf/words.py:207 conf/words.py:208
+msgid "invite other to help answer this question"
+msgstr ""
+
+#: conf/words.py:216 conf/words.py:217
+msgid "Related questions"
+msgstr ""
+
+#: conf/words.py:225 conf/words.py:226
+msgid "Question Tools"
+msgstr ""
+
+#: conf/words.py:234 conf/words.py:235
+msgid "Phrase: this question is currently shared only with:"
+msgstr ""
+
+#: conf/words.py:243 conf/words.py:244
+msgid "Be the first one to answer this question!"
+msgstr ""
+
+#: conf/words.py:252 conf/words.py:253
+msgid "followed questions"
+msgstr ""
+
+#: conf/words.py:262
+msgid "Phrase: comments and answers to others questions"
+msgstr ""
+
+#: conf/words.py:270 conf/words.py:271
+msgid "You can post questions by emailing them at"
+msgstr ""
+
+#: conf/words.py:279 conf/words.py:280
+msgid "List of questions"
+msgstr ""
+
+#: conf/words.py:288 conf/words.py:289
+msgid "Community gives you awards for your questions, answers and votes"
+msgstr ""
+
+#: conf/words.py:297 conf/words.py:298
+msgid "Close question"
+msgstr ""
+
+#: conf/words.py:306 conf/words.py:307
+msgid "Edit question"
+msgstr ""
+
+#: conf/words.py:315 conf/words.py:316
+msgid "Question - in one sentence"
+msgstr ""
+
+#: conf/words.py:324 conf/words.py:325
+msgid "Retag question"
+msgstr ""
+
+#: conf/words.py:333 conf/words.py:334
+msgid "Reopen question"
+msgstr ""
+
+#: conf/words.py:342 conf/words.py:343
+msgid "There are no unanswered questions here"
+msgstr ""
+
+#: conf/words.py:351 conf/words.py:352
+#: templates/user_profile/user_answers_list.html:5
+msgid "this answer has been selected as correct"
+msgstr ""
+
+#: conf/words.py:360 conf/words.py:361
+msgid "mark this answer as correct"
+msgstr ""
+
+#: conf/words.py:369 conf/words.py:370
+msgid "Login/Signup to Answer"
+msgstr ""
+
+#: conf/words.py:378 conf/words.py:379
+msgid "Your Answer"
+msgstr ""
+
+#: conf/words.py:387 conf/words.py:388
+msgid "Add Answer"
+msgstr ""
+
+#: conf/words.py:396 conf/words.py:397
+msgid "give an answer interesting to this community"
+msgstr ""
+
+#: conf/words.py:405 conf/words.py:406
+msgid "try to give an answer, rather than engage into a discussion"
+msgstr ""
+
+#: conf/words.py:414 conf/words.py:415
+msgid "show only selected answers to enquirers"
+msgstr ""
+
+#: conf/words.py:423 conf/words.py:424
+msgid "UNANSWERED"
+msgstr ""
+
+#: conf/words.py:432 conf/words.py:433
+msgid "Edit Answer"
+msgstr ""
+
+#: conf/words.py:441 conf/words.py:442
+msgid "Answered"
+msgstr ""
+
#: const/__init__.py:11
msgid "duplicate question"
msgstr ""
@@ -2663,284 +2835,296 @@ msgid ""
"%(subject)s\">this link</a>"
msgstr ""
-#: const/__init__.py:122 templates/user_inbox/responses_and_flags.html:9
+#: const/__init__.py:114
+msgid "latest first"
+msgstr ""
+
+#: const/__init__.py:115
+msgid "oldest first"
+msgstr ""
+
+#: const/__init__.py:116
+msgid "most voted first"
+msgstr ""
+
+#: const/__init__.py:126 templates/user_inbox/responses_and_flags.html:9
msgid "all"
msgstr ""
-#: const/__init__.py:123
+#: const/__init__.py:127
msgid "unanswered"
msgstr ""
-#: const/__init__.py:124
+#: const/__init__.py:128
msgid "followed"
msgstr ""
-#: const/__init__.py:129
+#: const/__init__.py:133
msgid "list"
msgstr ""
-#: const/__init__.py:130
+#: const/__init__.py:134
msgid "cloud"
msgstr ""
-#: const/__init__.py:138
+#: const/__init__.py:143
msgid "Question has no answers"
msgstr ""
-#: const/__init__.py:139
+#: const/__init__.py:144
msgid "Question has no accepted answers"
msgstr ""
-#: const/__init__.py:195
+#: const/__init__.py:200
msgid "asked a question"
msgstr ""
-#: const/__init__.py:196
+#: const/__init__.py:201
msgid "answered a question"
msgstr ""
-#: const/__init__.py:197 const/__init__.py:301
+#: const/__init__.py:202 const/__init__.py:306
msgid "commented question"
msgstr ""
-#: const/__init__.py:198 const/__init__.py:302
+#: const/__init__.py:203 const/__init__.py:307
msgid "commented answer"
msgstr ""
-#: const/__init__.py:199
+#: const/__init__.py:204
msgid "edited question"
msgstr ""
-#: const/__init__.py:200
+#: const/__init__.py:205
msgid "edited answer"
msgstr ""
-#: const/__init__.py:201
+#: const/__init__.py:206
msgid "received badge"
msgstr ""
-#: const/__init__.py:202
+#: const/__init__.py:207
msgid "marked best answer"
msgstr ""
-#: const/__init__.py:203
+#: const/__init__.py:208
msgid "upvoted"
msgstr ""
-#: const/__init__.py:204
+#: const/__init__.py:209
msgid "downvoted"
msgstr ""
-#: const/__init__.py:205
+#: const/__init__.py:210
msgid "canceled vote"
msgstr ""
-#: const/__init__.py:206
+#: const/__init__.py:211
msgid "deleted question"
msgstr ""
-#: const/__init__.py:207
+#: const/__init__.py:212
msgid "deleted answer"
msgstr ""
-#: const/__init__.py:208
+#: const/__init__.py:213
msgid "marked offensive"
msgstr ""
-#: const/__init__.py:209
+#: const/__init__.py:214
msgid "updated tags"
msgstr ""
-#: const/__init__.py:210
+#: const/__init__.py:215
msgid "selected favorite"
msgstr ""
-#: const/__init__.py:211
+#: const/__init__.py:216
msgid "completed user profile"
msgstr ""
-#: const/__init__.py:212
+#: const/__init__.py:217
msgid "email update sent to user"
msgstr ""
-#: const/__init__.py:213
+#: const/__init__.py:218
msgid "a post was shared"
msgstr ""
-#: const/__init__.py:216
+#: const/__init__.py:221
msgid "reminder about unanswered questions sent"
msgstr ""
-#: const/__init__.py:220
+#: const/__init__.py:225
msgid "reminder about accepting the best answer sent"
msgstr ""
-#: const/__init__.py:222
+#: const/__init__.py:227
msgid "mentioned in the post"
msgstr ""
-#: const/__init__.py:225
+#: const/__init__.py:230
msgid "created tag description"
msgstr ""
-#: const/__init__.py:229
+#: const/__init__.py:234
msgid "updated tag description"
msgstr ""
-#: const/__init__.py:231
+#: const/__init__.py:236
msgid "made a new post"
msgstr ""
-#: const/__init__.py:234
+#: const/__init__.py:239
msgid "made an edit"
msgstr ""
-#: const/__init__.py:238
+#: const/__init__.py:243
msgid "created post reject reason"
msgstr ""
-#: const/__init__.py:242
+#: const/__init__.py:247
msgid "updated post reject reason"
msgstr ""
-#: const/__init__.py:300
+#: const/__init__.py:305
msgid "answered question"
msgstr ""
-#: const/__init__.py:303
+#: const/__init__.py:308
msgid "accepted answer"
msgstr ""
-#: const/__init__.py:307
+#: const/__init__.py:312
msgid "[closed]"
msgstr ""
-#: const/__init__.py:308
+#: const/__init__.py:313
msgid "[deleted]"
msgstr ""
-#: const/__init__.py:309 views/readers.py:642
+#: const/__init__.py:314 views/readers.py:681
msgid "initial version"
msgstr ""
-#: const/__init__.py:310
+#: const/__init__.py:315
msgid "retagged"
msgstr ""
-#: const/__init__.py:311
+#: const/__init__.py:316
msgid "[private]"
msgstr ""
-#: const/__init__.py:320
+#: const/__init__.py:325
msgid "show all tags"
msgstr ""
-#: const/__init__.py:321 const/__init__.py:330 const/__init__.py:336
-#: const/__init__.py:342
+#: const/__init__.py:326 const/__init__.py:335 const/__init__.py:341
+#: const/__init__.py:347
msgid "exclude ignored tags"
msgstr ""
-#: const/__init__.py:322 const/__init__.py:331 const/__init__.py:343
+#: const/__init__.py:327 const/__init__.py:336 const/__init__.py:348
msgid "only interesting tags"
msgstr ""
-#: const/__init__.py:326 const/__init__.py:337 const/__init__.py:344
+#: const/__init__.py:331 const/__init__.py:342 const/__init__.py:349
msgid "only subscribed tags"
msgstr ""
-#: const/__init__.py:329 const/__init__.py:335 const/__init__.py:341
+#: const/__init__.py:334 const/__init__.py:340 const/__init__.py:346
msgid "email for all tags"
msgstr ""
-#: const/__init__.py:348
+#: const/__init__.py:353
msgid "instantly"
msgstr ""
-#: const/__init__.py:349
+#: const/__init__.py:354
msgid "daily"
msgstr ""
-#: const/__init__.py:350
+#: const/__init__.py:355
msgid "weekly"
msgstr ""
-#: const/__init__.py:351
+#: const/__init__.py:356
msgid "no email"
msgstr ""
-#: const/__init__.py:358
+#: const/__init__.py:363
msgid "identicon"
msgstr ""
-#: const/__init__.py:359
+#: const/__init__.py:364
msgid "mystery-man"
msgstr ""
-#: const/__init__.py:360
+#: const/__init__.py:365
msgid "monsterid"
msgstr ""
-#: const/__init__.py:361
+#: const/__init__.py:366
msgid "wavatar"
msgstr ""
-#: const/__init__.py:362
+#: const/__init__.py:367
msgid "retro"
msgstr ""
-#: const/__init__.py:409 templates/badges.html:33
+#: const/__init__.py:414 templates/badges.html:34
msgid "gold"
msgstr ""
-#: const/__init__.py:410 templates/badges.html:43
+#: const/__init__.py:415 templates/badges.html:44
msgid "silver"
msgstr ""
-#: const/__init__.py:411 templates/badges.html:50
+#: const/__init__.py:416 templates/badges.html:51
msgid "bronze"
msgstr ""
-#: const/__init__.py:423
+#: const/__init__.py:428
msgid "None"
msgstr ""
-#: const/__init__.py:424
+#: const/__init__.py:429
msgid "Gravatar"
msgstr ""
-#: const/__init__.py:425
+#: const/__init__.py:430
msgid "Uploaded Avatar"
msgstr ""
-#: const/__init__.py:429
+#: const/__init__.py:434
msgid "date descendant"
msgstr ""
-#: const/__init__.py:430
+#: const/__init__.py:435
msgid "date ascendant"
msgstr ""
-#: const/__init__.py:431
+#: const/__init__.py:436
msgid "activity descendant"
msgstr ""
-#: const/__init__.py:432
+#: const/__init__.py:437
msgid "activity ascendant"
msgstr ""
-#: const/__init__.py:433
+#: const/__init__.py:438
msgid "answers descendant"
msgstr ""
-#: const/__init__.py:434
+#: const/__init__.py:439
msgid "answers ascendant"
msgstr ""
-#: const/__init__.py:435
+#: const/__init__.py:440
msgid "votes descendant"
msgstr ""
-#: const/__init__.py:436
+#: const/__init__.py:441
msgid "votes ascendant"
msgstr ""
@@ -3031,7 +3215,7 @@ msgid ""
"reach a resolution."
msgstr ""
-#: const/message_keys.py:52 models/__init__.py:1136
+#: const/message_keys.py:52 models/__init__.py:1158
msgid ""
"Sorry, your account appears to be suspended and you cannot make new posts "
"until this issue is resolved. You can, however edit your existing posts. "
@@ -3044,7 +3228,7 @@ msgid ""
"screen name, if necessary."
msgstr ""
-#: deps/django_authopenid/forms.py:112 deps/django_authopenid/views.py:206
+#: deps/django_authopenid/forms.py:112 deps/django_authopenid/views.py:208
msgid "i-names are not supported"
msgstr ""
@@ -3069,35 +3253,37 @@ msgstr ""
msgid "Passwords did not match"
msgstr ""
-#: deps/django_authopenid/forms.py:300
+#: deps/django_authopenid/forms.py:300 deps/django_authopenid/forms.py:355
#, python-format
-msgid "Please choose password > %(len)s characters"
+msgid "choose password > %(len)s characters"
msgstr ""
-#: deps/django_authopenid/forms.py:338
-msgid "Current password"
+#: deps/django_authopenid/forms.py:340 utils/forms.py:265
+msgid "password is required"
msgstr ""
-#: deps/django_authopenid/forms.py:349
-msgid ""
-"Old password is incorrect. Please enter the correct "
-"password."
+#: deps/django_authopenid/forms.py:346
+msgid "retype your password"
+msgstr ""
+
+#: deps/django_authopenid/forms.py:366 utils/forms.py:270
+msgid "entered passwords did not match, please try again"
msgstr ""
-#: deps/django_authopenid/forms.py:399
+#: deps/django_authopenid/forms.py:416
msgid "Sorry, we don't have this email address in the database"
msgstr ""
-#: deps/django_authopenid/forms.py:438
+#: deps/django_authopenid/forms.py:455
msgid "Your user name (<i>required</i>)"
msgstr ""
-#: deps/django_authopenid/forms.py:455
+#: deps/django_authopenid/forms.py:472
msgid "sorry, there is no such user name"
msgstr ""
#: deps/django_authopenid/urls.py:14 deps/django_authopenid/urls.py:20
-#: deps/django_authopenid/urls.py:23 setup_templates/settings.py:229
+#: deps/django_authopenid/urls.py:23 setup_templates/settings.py:232
msgid "signin/"
msgstr ""
@@ -3121,15 +3307,15 @@ msgstr ""
msgid "signup/"
msgstr ""
-#: deps/django_authopenid/urls.py:38
+#: deps/django_authopenid/urls.py:43
msgid "logout/"
msgstr ""
-#: deps/django_authopenid/urls.py:43
+#: deps/django_authopenid/urls.py:48
msgid "recover/"
msgstr ""
-#: deps/django_authopenid/urls.py:45
+#: deps/django_authopenid/urls.py:50
msgid "verify-email/"
msgstr ""
@@ -3138,7 +3324,7 @@ msgstr ""
msgid "%(site)s user name and password"
msgstr ""
-#: deps/django_authopenid/util.py:385 templates/authopenid/signin.html:117
+#: deps/django_authopenid/util.py:385 templates/authopenid/signin.html:120
#: templates/authopenid/widget_signin.html:120
msgid "Create a password-protected account"
msgstr ""
@@ -3225,88 +3411,88 @@ msgstr ""
msgid "Sign in with your %(provider)s account"
msgstr ""
-#: deps/django_authopenid/views.py:213
+#: deps/django_authopenid/views.py:215
#, python-format
msgid "OpenID %(openid_url)s is invalid"
msgstr ""
-#: deps/django_authopenid/views.py:391
+#: deps/django_authopenid/views.py:394
msgid ""
"Sorry, there was some problem connecting to the login provider, please try "
"again or use another login method"
msgstr ""
-#: deps/django_authopenid/views.py:520
-msgid "Your new password saved"
+#: deps/django_authopenid/views.py:523 deps/django_authopenid/views.py:798
+msgid "Your new password is saved"
msgstr ""
-#: deps/django_authopenid/views.py:568 deps/django_authopenid/views.py:583
+#: deps/django_authopenid/views.py:571 deps/django_authopenid/views.py:586
#, python-format
msgid ""
"Unfortunately, there was some problem when connecting to %(provider)s, "
"please try again or use another provider"
msgstr ""
-#: deps/django_authopenid/views.py:613
+#: deps/django_authopenid/views.py:616
msgid "The login password combination was not correct"
msgstr ""
-#: deps/django_authopenid/views.py:717
+#: deps/django_authopenid/views.py:720
msgid "Please click any of the icons below to sign in"
msgstr ""
-#: deps/django_authopenid/views.py:719
+#: deps/django_authopenid/views.py:722
msgid "Account recovery email sent"
msgstr ""
-#: deps/django_authopenid/views.py:722
+#: deps/django_authopenid/views.py:725
msgid "Please add one or more login methods."
msgstr ""
-#: deps/django_authopenid/views.py:724
+#: deps/django_authopenid/views.py:727
msgid "If you wish, please add, remove or re-validate your login methods"
msgstr ""
-#: deps/django_authopenid/views.py:726
+#: deps/django_authopenid/views.py:729
msgid "Please wait a second! Your account is recovered, but ..."
msgstr ""
-#: deps/django_authopenid/views.py:728
+#: deps/django_authopenid/views.py:731
msgid "Sorry, this account recovery key has expired or is invalid"
msgstr ""
-#: deps/django_authopenid/views.py:801
+#: deps/django_authopenid/views.py:818
#, python-format
msgid "Login method %(provider_name)s does not exist"
msgstr ""
-#: deps/django_authopenid/views.py:807
+#: deps/django_authopenid/views.py:824
msgid "Oops, sorry - there was some error - please try again"
msgstr ""
-#: deps/django_authopenid/views.py:882
+#: deps/django_authopenid/views.py:905
msgid ""
"If you are trying to sign in to another account, please sign out first. "
"Otherwise, please report the incident to the site administrator."
msgstr ""
-#: deps/django_authopenid/views.py:914
+#: deps/django_authopenid/views.py:937
#, python-format
msgid "Your %(provider)s login works fine"
msgstr ""
-#: deps/django_authopenid/views.py:1112
+#: deps/django_authopenid/views.py:1135
msgid ""
"Sorry, registration failed. The token can be already used or has expired. "
"Please try again"
msgstr ""
-#: deps/django_authopenid/views.py:1257
+#: deps/django_authopenid/views.py:1280
#, python-format
msgid "Recover your %(site)s account"
msgstr ""
-#: deps/django_authopenid/views.py:1292
+#: deps/django_authopenid/views.py:1315
msgid "Please check your email and visit the enclosed link."
msgstr ""
@@ -3398,23 +3584,23 @@ msgid ""
"of your user account</p>"
msgstr ""
-#: mail/lamson_handlers.py:160
+#: mail/lamson_handlers.py:162
msgid ""
"You were replying to an email address unknown to the system or "
"you were replying from a different address from the one where "
"you received the notification."
msgstr ""
-#: mail/lamson_handlers.py:251
+#: mail/lamson_handlers.py:265
#, python-format
msgid "Re: Welcome to %(site_name)s"
msgstr ""
-#: mail/lamson_handlers.py:258
+#: mail/lamson_handlers.py:272
msgid "Please reply to the welcome email without editing it"
msgstr ""
-#: mail/lamson_handlers.py:318
+#: mail/lamson_handlers.py:340
#, python-format
msgid "Re: %s"
msgstr ""
@@ -3432,14 +3618,14 @@ msgstr ""
msgid "Please accept the best answer for these questions:"
msgstr ""
-#: management/commands/send_email_alerts.py:434
+#: management/commands/send_email_alerts.py:436
#, python-format
msgid "%(question_count)d updated question about %(topics)s"
msgid_plural "%(question_count)d updated questions about %(topics)s"
msgstr[0] ""
msgstr[1] ""
-#: management/commands/send_email_alerts.py:455
+#: management/commands/send_email_alerts.py:457
msgid "new question"
msgstr ""
@@ -3455,90 +3641,94 @@ msgstr[1] ""
msgid "Please log in to use %s"
msgstr ""
-#: models/__init__.py:569 models/__init__.py:1388 views/writers.py:226
+#: models/__init__.py:591 models/__init__.py:1410 views/writers.py:226
msgid "Sorry, but you have only read access"
msgstr ""
-#: models/__init__.py:573
+#: models/__init__.py:595
msgid "Sorry, this operation is not allowed"
msgstr ""
-#: models/__init__.py:623
+#: models/__init__.py:645
msgid ""
"Sorry, you cannot accept or unaccept best answers because your account is "
"blocked"
msgstr ""
-#: models/__init__.py:627
+#: models/__init__.py:649
msgid ""
"Sorry, you cannot accept or unaccept best answers because your account is "
"suspended"
msgstr ""
-#: models/__init__.py:641
+#: models/__init__.py:663
#, python-format
msgid ""
">%(points)s points required to accept or unaccept your own answer to your "
"own question"
msgstr ""
-#: models/__init__.py:665
+#: models/__init__.py:687
#, python-format
msgid ""
"Sorry, you will be able to accept this answer only after %(will_be_able_at)s"
msgstr ""
-#: models/__init__.py:674
+#: models/__init__.py:696
#, python-format
msgid ""
"Sorry, only moderators or original author of the question - %(username)s - "
"can accept or unaccept the best answer"
msgstr ""
-#: models/__init__.py:697
+#: models/__init__.py:719
msgid "Sorry, you cannot vote for your own posts"
msgstr ""
-#: models/__init__.py:701
-msgid "Sorry your account appears to be blocked "
+#: models/__init__.py:723
+msgid ""
+"Sorry your account appears to be blocked and you cannot vote - please "
+"contact the site administrator to resolve the issue"
msgstr ""
-#: models/__init__.py:706
-msgid "Sorry your account appears to be suspended "
+#: models/__init__.py:728
+msgid ""
+"Sorry your account appears to be suspended and you cannot vote - please "
+"contact the site administrator to resolve the issue"
msgstr ""
-#: models/__init__.py:716
+#: models/__init__.py:738
#, python-format
msgid ">%(points)s points required to upvote"
msgstr ""
-#: models/__init__.py:722
+#: models/__init__.py:744
#, python-format
msgid ">%(points)s points required to downvote"
msgstr ""
-#: models/__init__.py:737
+#: models/__init__.py:759
msgid "Sorry, blocked users cannot upload files"
msgstr ""
-#: models/__init__.py:738
+#: models/__init__.py:760
msgid "Sorry, suspended users cannot upload files"
msgstr ""
-#: models/__init__.py:740
+#: models/__init__.py:762
#, python-format
msgid "sorry, file uploading requires karma >%(min_rep)s"
msgstr ""
-#: models/__init__.py:759
+#: models/__init__.py:781
msgid "Could not post, because your karma is insufficient to publish links"
msgstr ""
-#: models/__init__.py:785
+#: models/__init__.py:807
msgid "Sorry, you already gave an answer, please edit it instead."
msgstr ""
-#: models/__init__.py:809
+#: models/__init__.py:831
#, python-format
msgid ""
"Sorry, comments (except the last one) are editable only within %(minutes)s "
@@ -3549,56 +3739,56 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:821
+#: models/__init__.py:843
msgid "Sorry, but only post owners or moderators can edit comments"
msgstr ""
-#: models/__init__.py:850
+#: models/__init__.py:872
msgid ""
"Sorry, since your account is suspended you can comment only your own posts"
msgstr ""
-#: models/__init__.py:854
+#: models/__init__.py:876
#, python-format
msgid ""
"Sorry, to comment any post a minimum reputation of %(min_rep)s points is "
"required. You can still comment your own posts and answers to your questions"
msgstr ""
-#: models/__init__.py:884
+#: models/__init__.py:906
msgid ""
"This post has been deleted and can be seen only by post owners, site "
"administrators and moderators"
msgstr ""
-#: models/__init__.py:901
+#: models/__init__.py:923
msgid ""
"Sorry, only moderators, site administrators and post owners can edit deleted "
"posts"
msgstr ""
-#: models/__init__.py:917
+#: models/__init__.py:939
msgid "Sorry, since your account is blocked you cannot edit posts"
msgstr ""
-#: models/__init__.py:921
+#: models/__init__.py:943
msgid "Sorry, since your account is suspended you can edit only your own posts"
msgstr ""
-#: models/__init__.py:926
+#: models/__init__.py:948
#, python-format
msgid ""
"Sorry, to edit wiki posts, a minimum reputation of %(min_rep)s is required"
msgstr ""
-#: models/__init__.py:933
+#: models/__init__.py:955
#, python-format
msgid ""
"Sorry, to edit other people's posts, a minimum reputation of %(min_rep)s is "
"required"
msgstr ""
-#: models/__init__.py:996
+#: models/__init__.py:1018
msgid ""
"Sorry, cannot delete your question since it has an upvoted answer posted by "
"someone else"
@@ -3608,101 +3798,101 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:1011
+#: models/__init__.py:1033
msgid "Sorry, since your account is blocked you cannot delete posts"
msgstr ""
-#: models/__init__.py:1015
+#: models/__init__.py:1037
msgid ""
"Sorry, since your account is suspended you can delete only your own posts"
msgstr ""
-#: models/__init__.py:1019
+#: models/__init__.py:1041
#, python-format
msgid ""
"Sorry, to delete other people's posts, a minimum reputation of %(min_rep)s "
"is required"
msgstr ""
-#: models/__init__.py:1040
+#: models/__init__.py:1062
msgid "Sorry, since your account is blocked you cannot close questions"
msgstr ""
-#: models/__init__.py:1044
+#: models/__init__.py:1066
msgid "Sorry, since your account is suspended you cannot close questions"
msgstr ""
-#: models/__init__.py:1048
+#: models/__init__.py:1070
#, python-format
msgid ""
"Sorry, to close other people' posts, a minimum reputation of %(min_rep)s is "
"required"
msgstr ""
-#: models/__init__.py:1057
+#: models/__init__.py:1079
#, python-format
msgid ""
"Sorry, to close own question a minimum reputation of %(min_rep)s is required"
msgstr ""
-#: models/__init__.py:1084
+#: models/__init__.py:1106
#, python-format
msgid ""
"Sorry, only administrators, moderators or post owners with reputation > "
"%(min_rep)s can reopen questions."
msgstr ""
-#: models/__init__.py:1090
+#: models/__init__.py:1112
#, python-format
msgid ""
"Sorry, to reopen own question a minimum reputation of %(min_rep)s is required"
msgstr ""
-#: models/__init__.py:1095
+#: models/__init__.py:1117
msgid "Sorry, you cannot reopen questions because your account is blocked"
msgstr ""
-#: models/__init__.py:1100
+#: models/__init__.py:1122
msgid "Sorry, you cannot reopen questions because your account is suspended"
msgstr ""
-#: models/__init__.py:1123
+#: models/__init__.py:1145
msgid "You have flagged this question before and cannot do it more than once"
msgstr ""
-#: models/__init__.py:1131
+#: models/__init__.py:1153
msgid "Sorry, since your account is blocked you cannot flag posts as offensive"
msgstr ""
-#: models/__init__.py:1142
+#: models/__init__.py:1164
#, python-format
msgid ""
"Sorry, to flag posts as offensive a minimum reputation of %(min_rep)s is "
"required"
msgstr ""
-#: models/__init__.py:1163
+#: models/__init__.py:1185
#, python-format
msgid ""
"Sorry, you have exhausted the maximum number of %(max_flags_per_day)s "
"offensive flags per day."
msgstr ""
-#: models/__init__.py:1175
+#: models/__init__.py:1197
msgid "cannot remove non-existing flag"
msgstr ""
-#: models/__init__.py:1181
+#: models/__init__.py:1203
msgid "Sorry, since your account is blocked you cannot remove flags"
msgstr ""
-#: models/__init__.py:1185
+#: models/__init__.py:1207
msgid ""
"Sorry, your account appears to be suspended and you cannot remove flags. "
"Please contact the forum administrator to reach a resolution."
msgstr ""
-#: models/__init__.py:1191
+#: models/__init__.py:1213
#, python-format
msgid "Sorry, to flag posts a minimum reputation of %(min_rep)d is required"
msgid_plural ""
@@ -3710,221 +3900,221 @@ msgid_plural ""
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:1210
+#: models/__init__.py:1232
msgid "you don't have the permission to remove all flags"
msgstr ""
-#: models/__init__.py:1211
+#: models/__init__.py:1233
msgid "no flags for this entry"
msgstr ""
-#: models/__init__.py:1235
+#: models/__init__.py:1257
msgid ""
"Sorry, only question owners, site administrators and moderators can retag "
"deleted questions"
msgstr ""
-#: models/__init__.py:1243
+#: models/__init__.py:1265
msgid "Sorry, since your account is blocked you cannot retag questions"
msgstr ""
-#: models/__init__.py:1247
+#: models/__init__.py:1269
msgid ""
"Sorry, since your account is suspended you can retag only your own questions"
msgstr ""
-#: models/__init__.py:1251
+#: models/__init__.py:1273
#, python-format
msgid ""
"Sorry, to retag questions a minimum reputation of %(min_rep)s is required"
msgstr ""
-#: models/__init__.py:1270
+#: models/__init__.py:1292
msgid "Sorry, since your account is blocked you cannot delete comment"
msgstr ""
-#: models/__init__.py:1274
+#: models/__init__.py:1296
msgid ""
"Sorry, since your account is suspended you can delete only your own comments"
msgstr ""
-#: models/__init__.py:1278
+#: models/__init__.py:1300
#, python-format
msgid "Sorry, to delete comments reputation of %(min_rep)s is required"
msgstr ""
-#: models/__init__.py:1303
+#: models/__init__.py:1325
msgid "sorry, but older votes cannot be revoked"
msgstr ""
-#: models/__init__.py:1995 utils/functions.py:98
+#: models/__init__.py:2045 utils/functions.py:98
#, python-format
msgid "on %(date)s"
msgstr ""
-#: models/__init__.py:1997
+#: models/__init__.py:2047
msgid "in two days"
msgstr ""
-#: models/__init__.py:1999
+#: models/__init__.py:2049
msgid "tomorrow"
msgstr ""
-#: models/__init__.py:2001
+#: models/__init__.py:2051
#, python-format
msgid "in %(hr)d hour"
msgid_plural "in %(hr)d hours"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:2003
+#: models/__init__.py:2053
#, python-format
msgid "in %(min)d min"
msgid_plural "in %(min)d mins"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:2004
+#: models/__init__.py:2054
#, python-format
msgid "%(days)d day"
msgid_plural "%(days)d days"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:2006
+#: models/__init__.py:2056
#, python-format
msgid ""
"New users must wait %(days)s before answering their own question. You can "
"post an answer %(left)s"
msgstr ""
-#: models/__init__.py:2194 templates/email/feedback_email.txt:9
+#: models/__init__.py:2244 templates/email/feedback_email.txt:9
msgid "Anonymous"
msgstr ""
-#: models/__init__.py:2298
+#: models/__init__.py:2348
msgid "Site Adminstrator"
msgstr ""
-#: models/__init__.py:2300
+#: models/__init__.py:2350
msgid "Forum Moderator"
msgstr ""
-#: models/__init__.py:2302
+#: models/__init__.py:2352
msgid "Suspended User"
msgstr ""
-#: models/__init__.py:2304
+#: models/__init__.py:2354
msgid "Blocked User"
msgstr ""
-#: models/__init__.py:2306
+#: models/__init__.py:2356
msgid "Registered User"
msgstr ""
-#: models/__init__.py:2308
+#: models/__init__.py:2358
msgid "Watched User"
msgstr ""
-#: models/__init__.py:2310
+#: models/__init__.py:2360
msgid "Approved User"
msgstr ""
-#: models/__init__.py:2495
+#: models/__init__.py:2551
#, python-format
msgid "%(username)s karma is %(reputation)s"
msgstr ""
-#: models/__init__.py:2508
+#: models/__init__.py:2564
#, python-format
msgid "one gold badge"
msgid_plural "%(count)d gold badges"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:2515
+#: models/__init__.py:2571
#, python-format
msgid "one silver badge"
msgid_plural "%(count)d silver badges"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:2522
+#: models/__init__.py:2578
#, python-format
msgid "one bronze badge"
msgid_plural "%(count)d bronze badges"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:2533
+#: models/__init__.py:2589
#, python-format
msgid "%(item1)s and %(item2)s"
msgstr ""
-#: models/__init__.py:2535
+#: models/__init__.py:2591
#, python-format
msgid "%(user)s has %(badges)s"
msgstr ""
-#: models/__init__.py:2682
+#: models/__init__.py:2738
#, python-format
msgid "At least %d karma point is required to post links"
msgid_plural "At least %d karma points is required to post links"
msgstr[0] ""
msgstr[1] ""
-#: models/__init__.py:3159
+#: models/__init__.py:3221
#, python-format
msgid "%(user)s shared a %(post_link)s."
msgstr ""
-#: models/__init__.py:3162 models/__init__.py:3172
+#: models/__init__.py:3224 models/__init__.py:3234
#, python-format
msgid "%(user)s edited a %(post_link)s."
msgstr ""
-#: models/__init__.py:3164
+#: models/__init__.py:3226
#, python-format
msgid "%(user)s posted a %(post_link)s"
msgstr ""
-#: models/__init__.py:3167
+#: models/__init__.py:3229
#, python-format
msgid "%(user)s edited an %(post_link)s."
msgstr ""
-#: models/__init__.py:3169
+#: models/__init__.py:3231
#, python-format
msgid "%(user)s posted an %(post_link)s."
msgstr ""
-#: models/__init__.py:3174
+#: models/__init__.py:3236
#, python-format
msgid "%(user)s posted a %(post_link)s."
msgstr ""
-#: models/__init__.py:3190
+#: models/__init__.py:3252
msgid "To reply, PLEASE WRITE ABOVE THIS LINE."
msgstr ""
-#: models/__init__.py:3232
+#: models/__init__.py:3294
#, python-format
msgid "\"%(title)s\""
msgstr ""
-#: models/__init__.py:3384
+#: models/__init__.py:3446
#, python-format
msgid ""
"Congratulations, you have received a badge '%(badge_name)s'. Check out <a "
"href=\"%(user_profile)s\">your profile</a>."
msgstr ""
-#: models/__init__.py:3657
+#: models/__init__.py:3725
#, python-format
msgid "Welcome to %(site_name)s"
msgstr ""
-#: models/__init__.py:3678 views/commands.py:697
+#: models/__init__.py:3746 views/commands.py:697
msgid "Your tag subscription was saved, thanks!"
msgstr ""
@@ -4194,57 +4384,62 @@ msgstr ""
msgid "message"
msgstr ""
-#: models/post.py:414
+#: models/post.py:418
msgid "Question: "
msgstr ""
-#: models/post.py:416
+#: models/post.py:420
msgid "Answer: "
msgstr ""
-#: models/post.py:1570
+#: models/post.py:912 templates/question/answer_controls.html:58
+#: templatetags/extra_filters_jinja.py:128
+msgid "more"
+msgstr ""
+
+#: models/post.py:1602
msgid "Sorry, this question has been deleted and is no longer accessible"
msgstr ""
-#: models/post.py:1586
+#: models/post.py:1618
msgid ""
"Sorry, the answer you are looking for is no longer available, because the "
"parent question has been removed"
msgstr ""
-#: models/post.py:1593
+#: models/post.py:1625
msgid "Sorry, this answer has been removed and is no longer accessible"
msgstr ""
-#: models/post.py:1609
+#: models/post.py:1641
msgid ""
"Sorry, the comment you are looking for is no longer accessible, because the "
"parent question has been removed"
msgstr ""
-#: models/post.py:1616
+#: models/post.py:1648
msgid ""
"Sorry, the comment you are looking for is no longer accessible, because the "
"parent answer has been removed"
msgstr ""
-#: models/post.py:1638
+#: models/post.py:1670
msgid "This post is temporarily not available"
msgstr ""
-#: models/post.py:2147
+#: models/post.py:2179
#, python-format
msgid ""
"Thank you for your post to %(site)s. It will be published after the "
"moderators review."
msgstr ""
-#: models/post.py:2151
+#: models/post.py:2183
#, python-format
msgid "your post to %(site)s"
msgstr ""
-#: models/post.py:2158
+#: models/post.py:2190
msgid ""
"Your post was placed on the moderation queue and will be published after the "
"moderator approval."
@@ -4259,24 +4454,24 @@ msgstr ""
msgid "\" and more"
msgstr ""
-#: models/question.py:834
+#: models/question.py:840
#, python-format
msgid "%(count)d answer:"
msgid_plural "%(count)d answers:"
msgstr[0] ""
msgstr[1] ""
-#: models/question.py:1346
+#: models/question.py:1359
#, python-format
msgid "Tag %s is new and will be submitted for the moderators approval"
msgstr ""
-#: models/question.py:1351 models/tag.py:217
+#: models/question.py:1364 models/tag.py:217
#, python-format
msgid "Tags %s are new and will be submitted for the moderators approval"
msgstr ""
-#: models/question.py:1590
+#: models/question.py:1603
#, python-format
msgid "Please, <a href=\"%s\">review your question</a>."
msgstr ""
@@ -4410,7 +4605,7 @@ msgid "This might have happened for the following reasons:"
msgstr ""
#: templates/404.html:17
-msgid "this question or answer has been deleted;"
+msgid "This page has been deleted"
msgstr ""
#: templates/404.html:18
@@ -4435,18 +4630,6 @@ msgstr ""
msgid "report this problem"
msgstr ""
-#: templates/404.html:30 templates/500.html:11
-msgid "back to previous page"
-msgstr ""
-
-#: templates/404.html:31 templates/widgets/scope_nav.html:17
-msgid "see all questions"
-msgstr ""
-
-#: templates/404.html:32
-msgid "see all tags"
-msgstr ""
-
#: templates/500.html:3 templates/500.html.py:5
msgid "Internal server error"
msgstr ""
@@ -4456,34 +4639,23 @@ msgid "system error log is recorded, error will be fixed as soon as possible"
msgstr ""
#: templates/500.html:9
-msgid "please report the error to the site administrators if you wish"
-msgstr ""
-
-#: templates/500.html:12
-msgid "see latest questions"
-msgstr ""
-
-#: templates/500.html:13
-msgid "see tags"
-msgstr ""
-
-#: templates/answer_edit.html:4 templates/answer_edit.html.py:10
-msgid "Edit answer"
+msgid "please report the error to the site administrators"
msgstr ""
-#: templates/answer_edit.html:10 templates/question_edit.html:9
+#: templates/answer_edit.html:11 templates/question_edit.html:9
#: templates/question_retag.html:5 templates/revisions.html:7
msgid "back"
msgstr ""
-#: templates/answer_edit.html:41 templates/question_edit.html:61
+#: templates/answer_edit.html:42 templates/question_edit.html:61
msgid "Save edit"
msgstr ""
-#: templates/answer_edit.html:46 templates/close.html:16
+#: templates/answer_edit.html:47 templates/close.html:16
#: templates/feedback.html:64 templates/question_edit.html:62
-#: templates/question_retag.html:22 templates/reopen.html:28
-#: templates/subscribe_for_tags.html:16
+#: templates/question_retag.html:22 templates/reopen.html:26
+#: templates/subscribe_for_tags.html:16 templates/avatar/add.html:17
+#: templates/avatar/change.html:26 templates/avatar/confirm_delete.html:16
#: templates/user_profile/reject_post_dialog.html:36
#: templates/user_profile/reject_post_dialog.html:74
#: templates/user_profile/reject_post_dialog.html:104
@@ -4491,24 +4663,19 @@ msgstr ""
msgid "Cancel"
msgstr ""
-#: templates/answer_edit.html:86 templates/answer_edit.html.py:89
-#: templates/ask.html:81 templates/ask.html.py:84
+#: templates/answer_edit.html:87 templates/answer_edit.html.py:90
+#: templates/ask.html:82 templates/ask.html.py:85
#: templates/question_edit.html:100 templates/question_edit.html.py:103
-#: templates/question/javascript.html:72 templates/question/javascript.html:75
-#: templates/widgets/edit_post.html:73
+#: templates/question/javascript.html:103
+#: templates/question/javascript.html:106 templates/widgets/edit_post.html:73
msgid "hide preview"
msgstr ""
-#: templates/answer_edit.html:89 templates/ask.html:84
-#: templates/question_edit.html:103 templates/question/javascript.html:75
+#: templates/answer_edit.html:90 templates/ask.html:85
+#: templates/question_edit.html:103 templates/question/javascript.html:106
msgid "show preview"
msgstr ""
-#: templates/ask.html:4 templates/widgets/ask_button.html:9
-#: templates/widgets/ask_form.html:67
-msgid "Ask Your Question"
-msgstr ""
-
#: templates/ask.html:21
msgid ""
"since you are not logged in right now, you will be asked to sign in or "
@@ -4524,34 +4691,28 @@ msgid ""
"that. Meanwhile, your question will saved as pending."
msgstr ""
-#: templates/ask.html:29
-msgid "please, try to make your question interesting to this community"
-msgstr ""
-
-#: templates/ask.html:30 templates/widgets/answer_edit_tips.html:12
-#: templates/widgets/question_edit_tips.html:8
+#: templates/ask.html:30 templates/widgets/answer_edit_tips.html:8
+#: templates/widgets/question_edit_tips.html:6
msgid "provide enough details"
msgstr ""
-#: templates/ask.html:31 templates/widgets/answer_edit_tips.html:15
-#: templates/widgets/question_edit_tips.html:11
+#: templates/ask.html:31 templates/widgets/answer_edit_tips.html:9
+#: templates/widgets/question_edit_tips.html:7
msgid "be clear and concise"
msgstr ""
-#: templates/ask.html:36 templates/widgets/answer_edit_tips.html:20
-#: templates/widgets/question_edit_tips.html:16
+#: templates/ask.html:36
msgid "see frequently asked questions"
msgstr ""
#: templates/ask.html:36 templates/faq_static.html:3
-#: templates/faq_static.html.py:5 templates/widgets/answer_edit_tips.html:20
-#: templates/widgets/question_edit_tips.html:16 views/meta.py:71
+#: templates/faq_static.html.py:5 views/meta.py:71
msgid "FAQ"
msgstr ""
#: templates/badge.html:5 templates/badge.html.py:9
#: templates/user_profile/user_recent.html:18
-#: templates/user_profile/user_stats.html:73
+#: templates/user_profile/user_stats.html:72
#, python-format
msgid "%(name)s"
msgstr ""
@@ -4566,7 +4727,7 @@ msgid "Badge \"%(name)s\""
msgstr ""
#: templates/badge.html:9 templates/user_profile/user_recent.html:16
-#: templates/user_profile/user_stats.html:71
+#: templates/user_profile/user_stats.html:70
#, python-format
msgid "%(description)s"
msgstr ""
@@ -4581,37 +4742,33 @@ msgstr[1] ""
msgid "Badges"
msgstr ""
-#: templates/badges.html:7
-msgid "Community gives you awards for your questions, answers and votes."
-msgstr ""
-
-#: templates/badges.html:8
+#: templates/badges.html:9
msgid ""
"Below is the list of available badges and number of times each type of badge "
"has been awarded."
msgstr ""
-#: templates/badges.html:31
+#: templates/badges.html:32
msgid "Community badges"
msgstr "Badge levels"
-#: templates/badges.html:33
+#: templates/badges.html:34
msgid "gold badge: the highest honor and is very rare"
msgstr ""
-#: templates/badges.html:36
+#: templates/badges.html:37
msgid ""
"Gold badge is the highest award in this community. To obtain it you have to "
"show \n"
"profound knowledge and ability in addition to your active participation."
msgstr ""
-#: templates/badges.html:42 templates/badges.html.py:46
+#: templates/badges.html:43 templates/badges.html.py:47
msgid ""
"silver badge: occasionally awarded for the very high quality contributions"
msgstr ""
-#: templates/badges.html:49 templates/badges.html.py:53
+#: templates/badges.html:50 templates/badges.html.py:54
msgid "bronze badge: often given as a special honor"
msgstr ""
@@ -4620,14 +4777,6 @@ msgstr ""
msgid "RSS feed from %(site_title)s"
msgstr ""
-#: templates/close.html:3 templates/close.html.py:5
-msgid "Close question"
-msgstr ""
-
-#: templates/close.html:6
-msgid "Close the question"
-msgstr ""
-
#: templates/close.html:11
msgid "Reasons"
msgstr ""
@@ -4866,7 +5015,7 @@ msgid "Send Feedback"
msgstr ""
#: templates/groups.html:3 templates/groups.html.py:6
-#: templates/question/sidebar.html:108
+#: templates/question/sidebar.html:107
#: templates/tags/list_bulk_tag_subscription.html:15
msgid "Groups"
msgstr ""
@@ -5075,23 +5224,23 @@ msgstr ""
msgid "There are no questions with this tag yet"
msgstr ""
-#: templates/list_suggested_tags.html:62
+#: templates/list_suggested_tags.html:61
#, python-format
msgid "Apply tag \"%(name)s\" to all above questions"
msgstr ""
-#: templates/list_suggested_tags.html:63
+#: templates/list_suggested_tags.html:62
msgid "Reject tag"
msgstr ""
-#: templates/list_suggested_tags.html:71 templates/tags/content.html:5
+#: templates/list_suggested_tags.html:70 templates/tags/content.html:5
#: templates/tags/content.html.py:31
msgid "Nothing found"
msgstr ""
#: templates/macros.html:5
#, python-format
-msgid "Share this question on %(site)s"
+msgid "Share this content on %(site)s"
msgstr ""
#: templates/macros.html:44
@@ -5102,14 +5251,6 @@ msgstr ""
msgid "anonymous user"
msgstr ""
-#: templates/macros.html:91 templates/macros.html.py:110
-msgid "asked"
-msgstr ""
-
-#: templates/macros.html:93 templates/macros.html.py:112
-msgid "answered"
-msgstr ""
-
#: templates/macros.html:95 templates/macros.html.py:114
msgid "posted"
msgstr ""
@@ -5165,91 +5306,95 @@ msgstr ""
msgid "Comments"
msgstr ""
-#: templates/macros.html:430
+#: templates/macros.html:433
msgid "delete this comment"
msgstr ""
-#: templates/macros.html:443 templates/revisions.html:38
+#: templates/macros.html:446 templates/revisions.html:38
#: templates/revisions.html.py:41 templates/question/answer_controls.html:5
#: templates/question/question_controls.html:1
msgid "edit"
msgstr ""
-#: templates/macros.html:452
+#: templates/macros.html:455
msgid "convert to answer"
msgstr ""
-#: templates/macros.html:579
+#: templates/macros.html:582
#, python-format
msgid "follow %(alias)s"
msgstr ""
-#: templates/macros.html:582
+#: templates/macros.html:585
#, python-format
msgid "unfollow %(alias)s"
msgstr ""
-#: templates/macros.html:583
+#: templates/macros.html:586
#, python-format
msgid "following %(alias)s"
msgstr ""
-#: templates/macros.html:662 templatetags/extra_tags.py:44
+#: templates/macros.html:667 templatetags/extra_tags.py:44
#, python-format
msgid "%(username)s gravatar image"
msgstr ""
-#: templates/macros.html:671
+#: templates/macros.html:676
#, python-format
msgid "%(username)s's website is %(url)s"
msgstr ""
-#: templates/macros.html:686 templates/macros.html.py:687
-#: templates/macros.html:725 templates/macros.html.py:726
+#: templates/macros.html:697 templates/macros.html.py:698
+#: templates/macros.html:759 templates/macros.html.py:760
msgid "previous"
msgstr ""
-#: templates/macros.html:698 templates/macros.html.py:737
+#: templates/macros.html:716 templates/macros.html.py:771
msgid "current page"
msgstr ""
-#: templates/macros.html:700 templates/macros.html.py:707
-#: templates/macros.html:739 templates/macros.html.py:746
+#: templates/macros.html:722 templates/macros.html.py:735
+#: templates/macros.html:773 templates/macros.html.py:780
#, python-format
msgid "page %(num)s"
msgstr ""
-#: templates/macros.html:711 templates/macros.html.py:750
+#: templates/macros.html:744 templates/macros.html.py:784
msgid "next page"
msgstr ""
-#: templates/macros.html:762
+#: templates/macros.html:745
+msgid "next"
+msgstr ""
+
+#: templates/macros.html:796
#, python-format
msgid "responses for %(username)s"
msgstr ""
-#: templates/macros.html:765
+#: templates/macros.html:799
#, python-format
msgid "you have %(response_count)s new response"
msgid_plural "you have %(response_count)s new responses"
msgstr[0] ""
msgstr[1] ""
-#: templates/macros.html:768
+#: templates/macros.html:802
msgid "no new responses yet"
msgstr ""
-#: templates/macros.html:783 templates/macros.html.py:784
+#: templates/macros.html:817 templates/macros.html.py:818
#, python-format
msgid "%(new)s new flagged posts and %(seen)s previous"
msgstr ""
-#: templates/macros.html:786 templates/macros.html.py:787
+#: templates/macros.html:820 templates/macros.html.py:821
#, python-format
msgid "%(new)s new flagged posts"
msgstr ""
-#: templates/macros.html:792 templates/macros.html.py:793
+#: templates/macros.html:826 templates/macros.html.py:827
#, python-format
msgid "%(seen)s flagged posts"
msgstr ""
@@ -5258,34 +5403,18 @@ msgstr ""
msgid "Questions"
msgstr ""
-#: templates/question.html:230
+#: templates/question.html:232
msgid "see more comments"
msgstr ""
-#: templates/question.html:232 templates/question.html.py:335
+#: templates/question.html:234 templates/question.html.py:337
msgid "add a comment"
msgstr ""
-#: templates/question.html:245 templates/question/content.html:46
-msgid "Answer Your Own Question"
-msgstr ""
-
-#: templates/question.html:250
-msgid "Post Your Answer"
-msgstr ""
-
-#: templates/question.html:256 templates/widgets/ask_form.html:65
+#: templates/question.html:258 templates/widgets/ask_form.html:65
msgid "Login/Signup to Post"
msgstr ""
-#: templates/question_edit.html:4 templates/question_edit.html.py:9
-msgid "Edit question"
-msgstr ""
-
-#: templates/question_edit.html:16
-msgid "Question - in one sentence"
-msgstr ""
-
#: templates/question_edit.html:23
msgid "Details"
msgstr ""
@@ -5294,10 +5423,6 @@ msgstr ""
msgid "Change language"
msgstr ""
-#: templates/question_retag.html:3 templates/question_retag.html.py:5
-msgid "Retag question"
-msgstr ""
-
#: templates/question_retag.html:21
msgid "Retag"
msgstr ""
@@ -5318,10 +5443,6 @@ msgstr ""
msgid "up to 5 tags, less than 20 characters each"
msgstr ""
-#: templates/reopen.html:4 templates/reopen.html.py:6
-msgid "Reopen question"
-msgstr ""
-
#: templates/reopen.html:12
#, python-format
msgid ""
@@ -5337,14 +5458,6 @@ msgstr ""
msgid "When:"
msgstr ""
-#: templates/reopen.html:23
-msgid "Reopen this question?"
-msgstr ""
-
-#: templates/reopen.html:27
-msgid "Reopen this question"
-msgstr ""
-
#: templates/revisions.html:4 templates/revisions.html.py:7
msgid "Revision history"
msgstr ""
@@ -5404,7 +5517,7 @@ msgstr ""
#: templates/users.html:41 templates/user_profile/user_info.html:25
#: templates/user_profile/user_reputation.html:5
-#: templates/user_profile/user_tabs.html:24
+#: templates/user_profile/user_tabs.html:20
msgid "karma"
msgstr ""
@@ -5447,7 +5560,7 @@ msgid "(or select another login method above)"
msgstr ""
#: templates/authopenid/authopenid_macros.html:66
-#: templates/authopenid/signin.html:115
+#: templates/authopenid/signin.html:118
#: templates/authopenid/widget_signin.html:118
msgid "Sign in"
msgstr ""
@@ -5495,15 +5608,15 @@ msgstr ""
msgid "Registration"
msgstr ""
-#: templates/authopenid/complete.html:23
+#: templates/authopenid/complete.html:24
msgid "User registration"
msgstr ""
-#: templates/authopenid/complete.html:47
+#: templates/authopenid/complete.html:48
msgid "<strong>Screen Name</strong> (<i>will be shown to others</i>)"
msgstr ""
-#: templates/authopenid/complete.html:56
+#: templates/authopenid/complete.html:57
msgid ""
"<strong>Email Address</strong> (<i>will <strong>not</strong> be shared "
"with \n"
@@ -5511,7 +5624,7 @@ msgid ""
" "
msgstr ""
-#: templates/authopenid/complete.html:71
+#: templates/authopenid/complete.html:72
#: templates/authopenid/signup_with_password.html:5
#: templates/authopenid/signup_with_password.html:45
msgid "Signup"
@@ -5593,10 +5706,10 @@ msgstr "User login"
#: templates/authopenid/signin.html:15
#: templates/authopenid/widget_signin.html:19
-#, python-format
+#, fuzzy, python-format
msgid ""
"\n"
-" Your answer to %(title)s %(summary)s will be posted once you log in\n"
+" Your answer to %(title)s will be posted once you log in\n"
" "
msgstr ""
"\n"
@@ -5606,10 +5719,10 @@ msgstr ""
#: templates/authopenid/signin.html:22
#: templates/authopenid/widget_signin.html:26
-#, python-format
+#, fuzzy, python-format
msgid ""
"Your question \n"
-" %(title)s %(summary)s will be posted once you log in\n"
+" %(title)s will be posted once you log in\n"
" "
msgstr ""
"<span class=\"strong big\">Your question</span> <i>\"<strong>%(title)s</"
@@ -5652,109 +5765,109 @@ msgid ""
"account"
msgstr ""
-#: templates/authopenid/signin.html:90
+#: templates/authopenid/signin.html:93
msgid "or enter your <span>user name and password</span>"
msgstr ""
-#: templates/authopenid/signin.html:94
+#: templates/authopenid/signin.html:97
#: templates/authopenid/widget_signin.html:98
msgid "Please, sign in"
msgstr ""
-#: templates/authopenid/signin.html:101
+#: templates/authopenid/signin.html:104
#: templates/authopenid/widget_signin.html:105
msgid "Login failed, please try again"
msgstr ""
-#: templates/authopenid/signin.html:106
+#: templates/authopenid/signin.html:109
#: templates/authopenid/widget_signin.html:109
msgid "Login or email"
msgstr ""
-#: templates/authopenid/signin.html:110
+#: templates/authopenid/signin.html:113
#: templates/authopenid/widget_signin.html:113 utils/forms.py:264
msgid "Password"
msgstr ""
-#: templates/authopenid/signin.html:122
+#: templates/authopenid/signin.html:125
#: templates/authopenid/widget_signin.html:125
msgid "To change your password - please enter the new one twice, then submit"
msgstr ""
-#: templates/authopenid/signin.html:126
+#: templates/authopenid/signin.html:129
#: templates/authopenid/widget_signin.html:129
msgid "New password"
msgstr ""
-#: templates/authopenid/signin.html:135
+#: templates/authopenid/signin.html:138
#: templates/authopenid/widget_signin.html:138
msgid "Please, retype"
msgstr ""
-#: templates/authopenid/signin.html:145
+#: templates/authopenid/signin.html:148
#: templates/authopenid/widget_signin.html:148
#: templates/livesettings/site_settings.html:24
msgid "Change password"
msgstr ""
-#: templates/authopenid/signin.html:159
+#: templates/authopenid/signin.html:162
#: templates/authopenid/widget_signin.html:162
msgid "Here are your current login methods"
msgstr ""
-#: templates/authopenid/signin.html:163
+#: templates/authopenid/signin.html:166
#: templates/authopenid/widget_signin.html:166
msgid "provider"
msgstr ""
-#: templates/authopenid/signin.html:164
+#: templates/authopenid/signin.html:167
#: templates/authopenid/widget_signin.html:167
msgid "last used"
msgstr ""
-#: templates/authopenid/signin.html:165
+#: templates/authopenid/signin.html:168
#: templates/authopenid/widget_signin.html:168
msgid "delete, if you like"
msgstr ""
-#: templates/authopenid/signin.html:179
+#: templates/authopenid/signin.html:182
#: templates/authopenid/widget_signin.html:182
#: templates/question/answer_controls.html:33
#: templates/question/question_controls.html:36
msgid "delete"
msgstr ""
-#: templates/authopenid/signin.html:181
+#: templates/authopenid/signin.html:184
#: templates/authopenid/widget_signin.html:184
msgid "cannot be deleted"
msgstr ""
-#: templates/authopenid/signin.html:194
+#: templates/authopenid/signin.html:197
#: templates/authopenid/widget_signin.html:197
msgid "Still have trouble signing in?"
msgstr ""
-#: templates/authopenid/signin.html:199
+#: templates/authopenid/signin.html:202
#: templates/authopenid/widget_signin.html:202
-msgid "Please, enter your email address below and obtain a new key"
+msgid "Enter your email address below and obtain a new key"
msgstr ""
-#: templates/authopenid/signin.html:201
+#: templates/authopenid/signin.html:204
#: templates/authopenid/widget_signin.html:204
-msgid "Please, enter your email address below to recover your account"
+msgid "Enter your email address below to recover your account"
msgstr ""
-#: templates/authopenid/signin.html:204
+#: templates/authopenid/signin.html:207
#: templates/authopenid/widget_signin.html:207
msgid "recover your account via email"
msgstr ""
-#: templates/authopenid/signin.html:215
+#: templates/authopenid/signin.html:218
#: templates/authopenid/widget_signin.html:217
msgid "Send a new recovery key"
msgstr ""
-#: templates/authopenid/signin.html:217
+#: templates/authopenid/signin.html:220
#: templates/authopenid/widget_signin.html:219
msgid "Recover your account via email"
msgstr ""
@@ -5836,7 +5949,7 @@ msgstr ""
msgid "You haven't uploaded an avatar yet. Please upload one now."
msgstr ""
-#: templates/avatar/add.html:13
+#: templates/avatar/add.html:14
msgid "Upload New Image"
msgstr ""
@@ -5848,7 +5961,7 @@ msgstr ""
msgid "Choose new Default"
msgstr ""
-#: templates/avatar/change.html:22
+#: templates/avatar/change.html:23
msgid "Upload"
msgstr ""
@@ -5867,7 +5980,7 @@ msgid ""
"\">upload one</a> now."
msgstr ""
-#: templates/avatar/confirm_delete.html:12
+#: templates/avatar/confirm_delete.html:13
msgid "Delete These"
msgstr ""
@@ -5947,7 +6060,7 @@ msgstr ""
#: templates/email/insufficient_rep_to_post_by_email.html:10
#, python-format
-msgid "%(username)s, your question could not be posted by email just yet."
+msgid "%(username)s, your content could not be posted by email just yet."
msgstr ""
#: templates/email/insufficient_rep_to_post_by_email.html:14
@@ -5958,7 +6071,7 @@ msgstr ""
#: templates/email/insufficient_rep_to_post_by_email.html:15
#, python-format
-msgid "At this time, please post your question at %(link)s"
+msgid "At this time, please post your content at %(link)s"
msgstr ""
#: templates/email/macros.html:19
@@ -5974,10 +6087,6 @@ msgid ""
" "
msgstr ""
-#: templates/email/macros.html:26
-msgid "Question :"
-msgstr ""
-
#: templates/email/macros.html:33
#, python-format
msgid "Asked by %(author)s:"
@@ -6055,11 +6164,6 @@ msgstr[1] ""
msgid "Great, you are ready to use %(site_name)s!"
msgstr ""
-#: templates/email/re_welcome_lamson_on.html:7
-#, python-format
-msgid "You can post questions by emailing them at %(ask_address)s."
-msgstr ""
-
#: templates/email/re_welcome_lamson_on.html:8
msgid ""
"When you receive update notifications, you will be able to respond to them, "
@@ -6118,8 +6222,8 @@ msgstr ""
#: templates/email/welcome_lamson_on.html:14
#, python-format
msgid ""
-"Until we receive the response from you, you will not be able ask or answer "
-"questions on %(site_name)s by email."
+"Until we receive the response from you, you will not be able to post content "
+"on %(site_name)s by email."
msgstr ""
#: templates/embed/list_widgets.html:44
@@ -6153,10 +6257,6 @@ msgid ""
"Create and embed widgets into your sites, here a list of available widgets."
msgstr ""
-#: templates/embed/widgets.html:16
-msgid "Ask a question"
-msgstr ""
-
#: templates/embed/widgets.html:17 templates/embed/widgets.html.py:26
msgid "create"
msgstr ""
@@ -6165,10 +6265,6 @@ msgstr ""
msgid "view list"
msgstr ""
-#: templates/embed/widgets.html:25
-msgid "List of questions"
-msgstr ""
-
#: templates/group_messaging/email_alert.html:7
#, python-format
msgid "%(author)s wrote:"
@@ -6195,7 +6291,7 @@ msgid "Messages by sender:"
msgstr ""
#: templates/group_messaging/senders_list.html:5
-#: templates/user_inbox/base.html:6 templates/user_profile/user_tabs.html:12
+#: templates/user_inbox/base.html:6 templates/user_profile/user_tabs.html:10
msgid "inbox"
msgstr ""
@@ -6269,7 +6365,7 @@ msgstr ""
msgid "Uncollapse all"
msgstr ""
-#: templates/main_page/headline.html:4 views/readers.py:151
+#: templates/main_page/headline.html:4 views/readers.py:157
#, python-format
msgid "%(q_num)s question"
msgid_plural "%(q_num)s questions"
@@ -6319,18 +6415,6 @@ msgstr ""
msgid "add tags and a query to focus your search"
msgstr ""
-#: templates/main_page/nothing_found.html:4
-msgid "There are no unanswered questions here"
-msgstr ""
-
-#: templates/main_page/nothing_found.html:7
-msgid "No questions here. "
-msgstr ""
-
-#: templates/main_page/nothing_found.html:8
-msgid "Please follow some questions or follow some users."
-msgstr ""
-
#: templates/main_page/nothing_found.html:13
msgid "You can expand your search by "
msgstr ""
@@ -6348,20 +6432,12 @@ msgstr ""
msgid "starting over"
msgstr ""
-#: templates/main_page/nothing_found.html:30
-msgid "Please always feel free to ask your question!"
-msgstr ""
-
#: templates/main_page/questions_loop.html:9
msgid "Did not find what you were looking for?"
msgstr ""
-#: templates/main_page/questions_loop.html:10
-msgid "Ask your question!"
-msgstr ""
-
#: templates/main_page/tab_bar.html:11
-msgid "subscribe to the questions feed"
+msgid "subscribe to the feed"
msgstr ""
#: templates/main_page/tab_bar.html:12
@@ -6408,10 +6484,6 @@ msgstr ""
msgid "This response is published"
msgstr ""
-#: templates/question/answer_controls.html:2
-msgid "swap with question"
-msgstr ""
-
#: templates/question/answer_controls.html:11
msgid "remove offensive flag"
msgstr ""
@@ -6459,18 +6531,6 @@ msgstr ""
msgid "link"
msgstr ""
-#: templates/question/answer_controls.html:58
-msgid "more"
-msgstr ""
-
-#: templates/question/answer_controls.html:71
-msgid "repost as a question comment"
-msgstr ""
-
-#: templates/question/answer_controls.html:85
-msgid "repost as a comment under the older answer"
-msgstr ""
-
#: templates/question/answer_tab_bar.html:3
#, python-format
msgid ""
@@ -6500,47 +6560,34 @@ msgstr ""
msgid "most voted answers will be shown first"
msgstr ""
-#: templates/question/answer_vote_buttons.html:8
-#: templates/user_profile/users_answers.html:7
-msgid "this answer has been selected as correct"
-msgstr ""
-
#: templates/question/answer_vote_buttons.html:10
-msgid "mark this answer as correct (click again to undo)"
+msgid "(click again to undo)"
msgstr ""
#: templates/question/closed_question_info.html:2
+msgid "Closed for the following reason"
+msgstr ""
+
+#: templates/question/closed_question_info.html:3
+msgid "by"
+msgstr ""
+
+#: templates/question/closed_question_info.html:5
#, python-format
msgid ""
"The question has been closed for the following reason <b>\"%(close_reason)s"
"\"</b> <i>by"
msgstr ""
-#: templates/question/closed_question_info.html:4
+#: templates/question/closed_question_info.html:8
#, python-format
msgid "close date %(closed_at)s"
msgstr ""
-#: templates/question/content.html:33
-msgid "Edit Your Previous Answer"
-msgstr ""
-
-#: templates/question/content.html:34
+#: templates/question/content.html:39
msgid "(only one answer per user is allowed)"
msgstr ""
-#: templates/question/new_answer_form.html:12
-msgid "Login/Signup to Answer"
-msgstr ""
-
-#: templates/question/new_answer_form.html:20
-msgid "Your answer"
-msgstr ""
-
-#: templates/question/new_answer_form.html:22
-msgid "Be the first one to answer this question!"
-msgstr ""
-
#: templates/question/new_answer_form.html:28
msgid ""
"<span class='strong big'>Please start posting your answer anonymously</span> "
@@ -6570,10 +6617,6 @@ msgid ""
"best questions and answers!"
msgstr ""
-#: templates/question/new_answer_form.html:39
-msgid "Add answer"
-msgstr ""
-
#: templates/question/question_controls.html:5
msgid "retag"
msgstr ""
@@ -6586,12 +6629,8 @@ msgstr ""
msgid "close"
msgstr ""
-#: templates/question/sidebar.html:8
-msgid "Question tools"
-msgstr ""
-
#: templates/question/sidebar.html:11
-msgid "click to unfollow this question"
+msgid "click to unfollow "
msgstr ""
#: templates/question/sidebar.html:12
@@ -6603,7 +6642,7 @@ msgid "Unfollow"
msgstr ""
#: templates/question/sidebar.html:17
-msgid "click to follow this question"
+msgid "click to follow "
msgstr ""
#: templates/question/sidebar.html:18
@@ -6618,7 +6657,7 @@ msgstr[0] ""
msgstr[1] ""
#: templates/question/sidebar.html:33
-msgid "subscribe to this question rss feed"
+msgid "subscribe to the rss feed"
msgstr ""
#: templates/question/sidebar.html:34
@@ -6629,7 +6668,7 @@ msgstr ""
msgid "Invite"
msgstr ""
-#: templates/question/sidebar.html:50 templates/question/sidebar.html.py:56
+#: templates/question/sidebar.html:49 templates/question/sidebar.html.py:55
#: templates/user_profile/user_email_subscriptions.html:59
#: templates/widgets/tag_selector.html:20
#: templates/widgets/tag_selector.html:37
@@ -6637,68 +6676,60 @@ msgstr ""
msgid "add"
msgstr ""
-#: templates/question/sidebar.html:52 templates/question/sidebar.html.py:58
+#: templates/question/sidebar.html:51 templates/question/sidebar.html.py:57
msgid "- or -"
msgstr ""
-#: templates/question/sidebar.html:70
+#: templates/question/sidebar.html:69
msgid "share with everyone"
msgstr ""
-#: templates/question/sidebar.html:81
-msgid "This question is currently shared only with:"
-msgstr ""
-
-#: templates/question/sidebar.html:83
+#: templates/question/sidebar.html:82
msgid "Individual users"
msgstr ""
-#: templates/question/sidebar.html:88
+#: templates/question/sidebar.html:87
msgid "You"
msgstr ""
-#: templates/question/sidebar.html:95 templates/question/sidebar.html:115
+#: templates/question/sidebar.html:94 templates/question/sidebar.html:114
msgid "and"
msgstr ""
-#: templates/question/sidebar.html:120
+#: templates/question/sidebar.html:119
#, python-format
msgid "%(more_count)s more"
msgstr ""
-#: templates/question/sidebar.html:126
+#: templates/question/sidebar.html:125
msgid "Public thread"
msgstr ""
-#: templates/question/sidebar.html:127
+#: templates/question/sidebar.html:126
#, python-format
msgid "This thread is public, all members of %(site_name)s can read this page."
msgstr ""
-#: templates/question/sidebar.html:135
+#: templates/question/sidebar.html:134
msgid "Stats"
msgstr ""
-#: templates/question/sidebar.html:137
+#: templates/question/sidebar.html:136
msgid "Asked"
msgstr ""
-#: templates/question/sidebar.html:140
+#: templates/question/sidebar.html:139
msgid "Seen"
msgstr ""
-#: templates/question/sidebar.html:140
+#: templates/question/sidebar.html:139
msgid "times"
msgstr ""
-#: templates/question/sidebar.html:143
+#: templates/question/sidebar.html:142
msgid "Last updated"
msgstr ""
-#: templates/question/sidebar.html:151
-msgid "Related questions"
-msgstr ""
-
#: templates/tags/form_bulk_tag_subscription.html:4
msgid "Tag subscriptions"
msgstr ""
@@ -6925,6 +6956,18 @@ msgstr ""
msgid "%(username)s's profile"
msgstr ""
+#: templates/user_profile/user_answers_list.html:5
+#, python-format
+msgid "the answer has been voted for %(answer_score)s times"
+msgstr ""
+
+#: templates/user_profile/user_answers_list.html:15
+#, python-format
+msgid "(%(comment_count)s comment)"
+msgid_plural "the answer has been commented %(comment_count)s times"
+msgstr[0] ""
+msgstr[1] ""
+
#: templates/user_profile/user_edit.html:4
msgid "Edit user profile"
msgstr ""
@@ -6961,7 +7004,7 @@ msgid "Update"
msgstr ""
#: templates/user_profile/user_email_subscriptions.html:5
-#: templates/user_profile/user_tabs.html:44
+#: templates/user_profile/user_tabs.html:36
msgid "subscriptions"
msgstr ""
@@ -6994,11 +7037,6 @@ msgstr ""
msgid "Subscribed Tags"
msgstr ""
-#: templates/user_profile/user_favorites.html:4
-#: templates/user_profile/user_tabs.html:29
-msgid "followed questions"
-msgstr ""
-
#: templates/user_profile/user_info.html:37
msgid "update profile"
msgstr ""
@@ -7053,7 +7091,7 @@ msgid "votes left"
msgstr ""
#: templates/user_profile/user_moderate.html:4
-#: templates/user_profile/user_tabs.html:50
+#: templates/user_profile/user_tabs.html:41
msgid "moderation"
msgstr ""
@@ -7135,7 +7173,7 @@ msgid ""
msgstr ""
#: templates/user_profile/user_network.html:5
-#: templates/user_profile/user_tabs.html:18
+#: templates/user_profile/user_tabs.html:15
msgid "network"
msgstr ""
@@ -7165,13 +7203,12 @@ msgid "%(username)s's network is empty"
msgstr ""
#: templates/user_profile/user_recent.html:5
-#: templates/user_profile/user_tabs.html:31
-#: templates/user_profile/user_tabs.html:33
+#: templates/user_profile/user_tabs.html:27
msgid "activity"
msgstr ""
-#: templates/user_profile/user_recent.html:23
-#: templates/user_profile/user_recent.html:27
+#: templates/user_profile/user_recent.html:25
+#: templates/user_profile/user_recent.html:30
msgid "source"
msgstr ""
@@ -7185,7 +7222,7 @@ msgid "%(user_name)s's karma change log"
msgstr ""
#: templates/user_profile/user_stats.html:6
-#: templates/user_profile/user_tabs.html:7
+#: templates/user_profile/user_tabs.html:6
msgid "overview"
msgstr ""
@@ -7202,97 +7239,41 @@ msgid_plural "Answers"
msgstr[0] ""
msgstr[1] ""
-#: templates/user_profile/user_stats.html:23
+#: templates/user_profile/user_stats.html:22
#, python-format
msgid "<span class=\"count\">%(cnt)s</span> Vote"
msgid_plural "<span class=\"count\">%(cnt)s</span> Votes "
msgstr[0] ""
msgstr[1] ""
-#: templates/user_profile/user_stats.html:31
+#: templates/user_profile/user_stats.html:30
#, python-format
msgid "<span class=\"count\">%(counter)s</span> Tag"
msgid_plural "<span class=\"count\">%(counter)s</span> Tags"
msgstr[0] ""
msgstr[1] ""
-#: templates/user_profile/user_stats.html:65
+#: templates/user_profile/user_stats.html:64
#, python-format
msgid "<span class=\"count\">%(counter)s</span> Badge"
msgid_plural "<span class=\"count\">%(counter)s</span> Badges"
msgstr[0] ""
msgstr[1] ""
-#: templates/user_profile/user_stats.html:85
+#: templates/user_profile/user_stats.html:84
msgid "Answer to:"
msgstr ""
-#: templates/user_profile/user_tabs.html:5
-msgid "User profile"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:10 views/users.py:819
-msgid "comments and answers to others questions"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:16
-msgid "followers and followed users"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:22
-msgid "Graph of user karma"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:27
-msgid "questions that user is following"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:36 views/users.py:861
-msgid "user vote record"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:38
+#: templates/user_profile/user_tabs.html:31
#: templates/user_profile/user_votes.html:5
msgid "votes"
msgstr ""
-#: templates/user_profile/user_tabs.html:42 views/users.py:973
-msgid "email subscription settings"
-msgstr ""
-
-#: templates/user_profile/user_tabs.html:48 views/users.py:286
-msgid "moderate this user"
-msgstr ""
-
-#: templates/user_profile/users_answers.html:7
-#, python-format
-msgid "the answer has been voted for %(answer_score)s times"
-msgstr ""
-
-#: templates/user_profile/users_answers.html:17
-#, python-format
-msgid "(%(comment_count)s comment)"
-msgid_plural "the answer has been commented %(comment_count)s times"
-msgstr[0] ""
-msgstr[1] ""
-
#: templates/widgets/answer_edit_tips.html:3
#: templates/widgets/question_edit_tips.html:3
msgid "Tips"
msgstr ""
-#: templates/widgets/answer_edit_tips.html:6
-msgid "give an answer interesting to this community"
-msgstr ""
-
-#: templates/widgets/answer_edit_tips.html:9
-msgid "try to give an answer, rather than engage into a discussion"
-msgstr ""
-
-#: templates/widgets/ask_button.html:9
-msgid "Ask the Group"
-msgstr ""
-
#: templates/widgets/ask_form.html:22 templates/widgets/ask_form.html.py:24
msgid "Add details (optional)"
msgstr ""
@@ -7340,7 +7321,7 @@ msgstr ""
msgid "about"
msgstr ""
-#: templates/widgets/footer.html:40 templates/widgets/user_navigation.html:26
+#: templates/widgets/footer.html:40 templates/widgets/user_navigation.html:27
msgid "help"
msgstr ""
@@ -7373,11 +7354,7 @@ msgid "add logo"
msgstr ""
#: templates/widgets/group_info.html:46
-msgid "moderate emailed questions"
-msgstr ""
-
-#: templates/widgets/group_info.html:58
-msgid "show only selected answers to enquirers"
+msgid "moderate emailed content"
msgstr ""
#: templates/widgets/group_info.html:63
@@ -7392,38 +7369,30 @@ msgstr ""
msgid "Allow only read access"
msgstr ""
-#: templates/widgets/group_info.html:102
-msgid "list of email addresses of pre-approved users"
-msgstr ""
-
-#: templates/widgets/group_info.html:107
+#: templates/widgets/group_info.html:106
msgid "List of preapproved email addresses"
msgstr ""
-#: templates/widgets/group_info.html:108
+#: templates/widgets/group_info.html:107
msgid ""
"Users with these email adderesses will be added to the group automatically."
msgstr ""
-#: templates/widgets/group_info.html:109
+#: templates/widgets/group_info.html:108
msgid "edit preapproved emails"
msgstr ""
-#: templates/widgets/group_info.html:113
-msgid "list of preapproved email address domain names"
-msgstr ""
-
-#: templates/widgets/group_info.html:118
+#: templates/widgets/group_info.html:116
msgid "List of preapproved email domain names"
msgstr ""
-#: templates/widgets/group_info.html:119
+#: templates/widgets/group_info.html:117
msgid ""
"Users whose email adderesses belong to these domains will be added to the "
"group automatically."
msgstr ""
-#: templates/widgets/group_info.html:120
+#: templates/widgets/group_info.html:118
msgid "edit preapproved email domains"
msgstr ""
@@ -7477,34 +7446,24 @@ msgstr ""
msgid "learn more about Markdown"
msgstr ""
-#: templates/widgets/meta_nav.html:12
+#: templates/widgets/meta_nav.html:13
msgid "people & groups"
msgstr ""
-#: templates/widgets/meta_nav.html:20
+#: templates/widgets/meta_nav.html:21
msgid "users"
msgstr "people"
-#: templates/widgets/meta_nav.html:27
+#: templates/widgets/meta_nav.html:28
msgid "badges"
msgstr ""
-#: templates/widgets/question_edit_tips.html:5
-msgid "ask a question interesting to this community"
-msgstr ""
-
#: templates/widgets/question_summary.html:12
msgid "view"
msgid_plural "views"
msgstr[0] ""
msgstr[1] ""
-#: templates/widgets/question_summary.html:30
-msgid "answer"
-msgid_plural "answers"
-msgstr[0] ""
-msgstr[1] ""
-
#: templates/widgets/question_summary.html:41
msgid "vote"
msgid_plural "votes"
@@ -7515,26 +7474,10 @@ msgstr[1] ""
msgid "ALL"
msgstr ""
-#: templates/widgets/scope_nav.html:22
-msgid "see unanswered questions"
-msgstr ""
-
-#: templates/widgets/scope_nav.html:22
-msgid "UNANSWERED"
-msgstr ""
-
-#: templates/widgets/scope_nav.html:27
-msgid "see your followed questions"
-msgstr ""
-
#: templates/widgets/scope_nav.html:27
msgid "FOLLOWED"
msgstr ""
-#: templates/widgets/scope_nav.html:30
-msgid "Please ask your question here"
-msgstr ""
-
#: templates/widgets/tag_selector.html:4
msgid "Interesting tags"
msgstr ""
@@ -7547,10 +7490,6 @@ msgstr ""
msgid "Subscribed tags"
msgstr ""
-#: templates/widgets/tag_selector.html:59
-msgid "Show only questions from"
-msgstr ""
-
#: templates/widgets/tag_selector.html:70
msgid "Send me email alerts for"
msgstr ""
@@ -7587,19 +7526,19 @@ msgstr ""
msgid "badges:"
msgstr ""
-#: templates/widgets/user_navigation.html:17
+#: templates/widgets/user_navigation.html:18
msgid "sign out"
msgstr ""
-#: templates/widgets/user_navigation.html:20
+#: templates/widgets/user_navigation.html:21
msgid "Hi there! Please sign in"
msgstr ""
-#: templates/widgets/user_navigation.html:23
+#: templates/widgets/user_navigation.html:24
msgid "settings"
msgstr ""
-#: templates/widgets/user_navigation.html:24
+#: templates/widgets/user_navigation.html:25
msgid "widgets"
msgstr ""
@@ -7631,7 +7570,7 @@ msgstr ""
msgid "Post questions, answers and comments"
msgstr ""
-#: templatetags/extra_filters_jinja.py:332
+#: templatetags/extra_filters_jinja.py:342
msgid "no"
msgstr ""
@@ -7711,10 +7650,6 @@ msgstr ""
msgid "this email address is not authorized"
msgstr ""
-#: utils/forms.py:265
-msgid "password is required"
-msgstr ""
-
#: utils/forms.py:268
msgid "Password <i>(please retype)</i>"
msgstr ""
@@ -7723,10 +7658,6 @@ msgstr ""
msgid "please, retype your password"
msgstr ""
-#: utils/forms.py:270
-msgid "sorry, entered passwords did not match, please try again"
-msgstr ""
-
#: utils/functions.py:102
msgid "2 days ago"
msgstr ""
@@ -7882,92 +7813,108 @@ msgstr ""
msgid "Suggested tags"
msgstr ""
-#: views/readers.py:256
+#: views/readers.py:278
#, python-format
msgid ""
"Please go to <a href=\"%s\">\"settings->URLs, keywords and greetings\"</a> "
"and set the base url for your site to function properly"
msgstr ""
-#: views/readers.py:421
+#: views/readers.py:460
msgid ""
"Sorry, the comment you are looking for has been deleted and is no longer "
"accessible"
msgstr ""
+#: views/users.py:286
+msgid "moderate this user"
+msgstr ""
+
#: views/users.py:287
msgid "moderate user"
msgstr ""
-#: views/users.py:529
+#: views/users.py:532
msgid "user profile"
msgstr ""
-#: views/users.py:530
+#: views/users.py:533
msgid "user profile overview"
msgstr ""
-#: views/users.py:650
+#: views/users.py:656
msgid "recent user activity"
msgstr ""
-#: views/users.py:651
+#: views/users.py:657
msgid "profile - recent activity"
msgstr ""
-#: views/users.py:682
+#: views/users.py:688
msgid "group joining requests"
msgstr ""
-#: views/users.py:683
+#: views/users.py:689
msgid "profile - moderation"
msgstr ""
-#: views/users.py:739
+#: views/users.py:745
msgid "private messages"
msgstr ""
-#: views/users.py:740
+#: views/users.py:746
msgid "profile - messages"
msgstr ""
-#: views/users.py:820
+#: views/users.py:825
+msgid "comments and answers to others questions"
+msgstr ""
+
+#: views/users.py:826
msgid "profile - responses"
msgstr ""
-#: views/users.py:862
+#: views/users.py:867
+msgid "user vote record"
+msgstr ""
+
+#: views/users.py:868
msgid "profile - votes"
msgstr ""
-#: views/users.py:883
+#: views/users.py:889
msgid "user karma"
msgstr ""
-#: views/users.py:884
+#: views/users.py:890
msgid "Profile - User's Karma"
msgstr ""
-#: views/users.py:902
+#: views/users.py:925
msgid "users favorite questions"
msgstr ""
-#: views/users.py:903
+#: views/users.py:926
msgid "profile - favorite questions"
msgstr ""
-#: views/users.py:944 views/users.py:948
+#: views/users.py:970 views/users.py:974
msgid "changes saved"
msgstr ""
-#: views/users.py:954
+#: views/users.py:980
msgid "email updates canceled"
msgstr ""
-#: views/users.py:974
+#: views/users.py:999
+msgid "email subscription settings"
+msgstr ""
+
+#: views/users.py:1000
msgid "profile - email subscriptions"
msgstr ""
-#: views/users.py:995
+#: views/users.py:1021
#, python-format
msgid "profile - %(section)s"
msgstr ""
@@ -8033,11 +7980,11 @@ msgstr ""
msgid "sorry, we seem to have some technical difficulties"
msgstr ""
-#: views/writers.py:888
+#: views/writers.py:869
msgid "Error - could not find the destination post"
msgstr ""
-#: views/writers.py:912
+#: views/writers.py:893
#, python-format
msgid ""
"Cannot convert, because text has more characters than %(max_chars)s - "
@@ -8065,17 +8012,17 @@ msgstr ""
#~ msgstr "Verification Email from Q&A forum"
#~ msgid ""
-#~ "how to validate email info with %(send_email_key_url)s %(gravatar_faq_url)"
-#~ "s"
+#~ "how to validate email info with %(send_email_key_url)s "
+#~ "%(gravatar_faq_url)s"
#~ msgstr ""
-#~ "<form style='margin:0;padding:0;' action='%(send_email_key_url)"
-#~ "s'><p><span class=\"bigger strong\">How?</span> If you have just set or "
-#~ "changed your email address - <strong>check your email and click the "
-#~ "included link</strong>.<br>The link contains a key generated specifically "
-#~ "for you. You can also <button style='display:inline' "
-#~ "type='submit'><strong>get a new key</strong></button> and check your "
-#~ "email again.</p></form><span class=\"bigger strong\">Why?</span> Email "
-#~ "validation is required to make sure that <strong>only you can post "
+#~ "<form style='margin:0;padding:0;' "
+#~ "action='%(send_email_key_url)s'><p><span class=\"bigger strong\">How?</"
+#~ "span> If you have just set or changed your email address - <strong>check "
+#~ "your email and click the included link</strong>.<br>The link contains a "
+#~ "key generated specifically for you. You can also <button style='display:"
+#~ "inline' type='submit'><strong>get a new key</strong></button> and check "
+#~ "your email again.</p></form><span class=\"bigger strong\">Why?</span> "
+#~ "Email validation is required to make sure that <strong>only you can post "
#~ "messages</strong> on your behalf and to <strong>minimize spam</strong> "
#~ "posts.<br>With email you can <strong>subscribe for updates</strong> on "
#~ "the most interesting questions. Also, when you sign up for the first time "
diff --git a/askbot/locale/en/LC_MESSAGES/djangojs.mo b/askbot/locale/en/LC_MESSAGES/djangojs.mo
index 3eead943..f158cafd 100644
--- a/askbot/locale/en/LC_MESSAGES/djangojs.mo
+++ b/askbot/locale/en/LC_MESSAGES/djangojs.mo
Binary files differ
diff --git a/askbot/locale/en/LC_MESSAGES/djangojs.po b/askbot/locale/en/LC_MESSAGES/djangojs.po
index f3ea106e..e1245db1 100644
--- a/askbot/locale/en/LC_MESSAGES/djangojs.po
+++ b/askbot/locale/en/LC_MESSAGES/djangojs.po
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.7\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2013-07-13 14:07-0500\n"
+"POT-Creation-Date: 2013-10-16 16:35-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"
@@ -312,7 +312,7 @@ msgstr ""
msgid "Are you sure you don't want to post this comment?"
msgstr "Are you sure you do not want to post this comment?"
-#: media/js/post.js:1890 media/js/utils.js:3360 media/js/utils.js.c:3515
+#: media/js/post.js:1890 media/js/utils.js:3804 media/js/utils.js.c:3959
msgid "just now"
msgstr ""
@@ -374,7 +374,7 @@ msgstr[1] ""
msgid "Delete category?"
msgstr ""
-#: media/js/post.js:3888 media/js/utils.js:881
+#: media/js/post.js:3888 media/js/utils.js:1320
msgid "edit"
msgstr "edit"
@@ -395,10 +395,6 @@ msgstr ""
msgid "save tags"
msgstr ""
-#: media/js/post.js:4689 media/js/post.js.c:4723
-msgid "User name:"
-msgstr ""
-
#: media/js/post.js:4710
msgid "Group name:"
msgstr ""
@@ -519,202 +515,207 @@ msgstr ""
msgid "click to close"
msgstr ""
-#: media/js/utils.js:880
+#: media/js/utils.js:619
+#, c-format
+msgid "page %s"
+msgstr ""
+
+#: media/js/utils.js:1319
msgid "click to edit this comment"
msgstr "click to edit this comment"
-#: media/js/utils.js:905
+#: media/js/utils.js:1344
msgid "convert to answer"
msgstr ""
-#: media/js/utils.js:958
+#: media/js/utils.js:1397
msgid "Ok"
msgstr ""
-#: media/js/utils.js:959 media/js/utils.js.c:1407
+#: media/js/utils.js:1398 media/js/utils.js.c:1851
msgid "Cancel"
msgstr ""
-#: media/js/utils.js:1219
+#: media/js/utils.js:1663
#, c-format
msgid "Uploaded file: %s"
msgstr ""
-#: media/js/utils.js:1234
+#: media/js/utils.js:1678
msgid "Choose a different image"
msgstr ""
-#: media/js/utils.js:1236
+#: media/js/utils.js:1680
msgid "Choose a different file"
msgstr ""
-#: media/js/utils.js:1250
+#: media/js/utils.js:1694
msgid "Oops, looks like we had an error. Sorry."
msgstr ""
-#: media/js/utils.js:1311
+#: media/js/utils.js:1755
msgid "Choose an image to insert"
msgstr ""
-#: media/js/utils.js:1313
+#: media/js/utils.js:1757
msgid "Choose a file to insert"
msgstr ""
-#: media/js/utils.js:1326
+#: media/js/utils.js:1770
msgid "Allowed file types are:"
msgstr ""
-#: media/js/utils.js:1332
+#: media/js/utils.js:1776
#: media/js/tinymce/plugins/askbot_attachment/editor_plugin.js:35
msgid "Or paste file url here"
msgstr ""
-#: media/js/utils.js:1406
+#: media/js/utils.js:1850
msgid "Save"
msgstr ""
-#: media/js/utils.js:1478
+#: media/js/utils.js:1922
msgid "saved"
msgstr ""
-#: media/js/utils.js:1602
+#: media/js/utils.js:2046
msgid "enabled"
msgstr ""
-#: media/js/utils.js:1604
+#: media/js/utils.js:2048
msgid "disabled"
msgstr ""
-#: media/js/utils.js:2038
+#: media/js/utils.js:2482
msgid "group name"
msgstr ""
-#: media/js/utils.js:2046
+#: media/js/utils.js:2490
msgid "add new group"
msgstr ""
-#: media/js/utils.js:2138
+#: media/js/utils.js:2582
msgid "Group %(name)s already exists. Group names are case-insensitive."
msgstr ""
-#: media/js/utils.js:2311
+#: media/js/utils.js:2755
#, c-format
msgid "see questions tagged '%s'"
msgstr ""
-#: media/js/utils.js:3358
+#: media/js/utils.js:3802
msgid "ago"
msgstr ""
-#: media/js/utils.js:3359
+#: media/js/utils.js:3803
msgid "from now"
msgstr ""
-#: media/js/utils.js:3361
+#: media/js/utils.js:3805
msgid "about a minute"
msgstr ""
-#: media/js/utils.js:3362
+#: media/js/utils.js:3806
#, c-format
msgid "%d minutes"
msgstr ""
-#: media/js/utils.js:3363
+#: media/js/utils.js:3807
msgid "about an hour"
msgstr ""
-#: media/js/utils.js:3364
+#: media/js/utils.js:3808
#, c-format
msgid "%d hours"
msgstr ""
-#: media/js/utils.js:3365 media/js/utils.js.c:3493
+#: media/js/utils.js:3809 media/js/utils.js.c:3937
msgid "yesterday"
msgstr ""
-#: media/js/utils.js:3366
+#: media/js/utils.js:3810
#, c-format
msgid "%d days"
msgstr ""
-#: media/js/utils.js:3367
+#: media/js/utils.js:3811
msgid "about a month"
msgstr ""
-#: media/js/utils.js:3368
+#: media/js/utils.js:3812
#, c-format
msgid "%d months"
msgstr ""
-#: media/js/utils.js:3369
+#: media/js/utils.js:3813
msgid "about a year"
msgstr ""
-#: media/js/utils.js:3370
+#: media/js/utils.js:3814
#, c-format
msgid "%d years"
msgstr ""
-#: media/js/utils.js:3468
+#: media/js/utils.js:3912
msgid "Jan"
msgstr ""
-#: media/js/utils.js:3469
+#: media/js/utils.js:3913
msgid "Feb"
msgstr ""
-#: media/js/utils.js:3470
+#: media/js/utils.js:3914
msgid "Mar"
msgstr ""
-#: media/js/utils.js:3471
+#: media/js/utils.js:3915
msgid "Apr"
msgstr ""
-#: media/js/utils.js:3472
+#: media/js/utils.js:3916
msgid "May"
msgstr ""
-#: media/js/utils.js:3473
+#: media/js/utils.js:3917
msgid "Jun"
msgstr ""
-#: media/js/utils.js:3474
+#: media/js/utils.js:3918
msgid "Jul"
msgstr ""
-#: media/js/utils.js:3475
+#: media/js/utils.js:3919
msgid "Aug"
msgstr ""
-#: media/js/utils.js:3476
+#: media/js/utils.js:3920
msgid "Sep"
msgstr ""
-#: media/js/utils.js:3477
+#: media/js/utils.js:3921
msgid "Oct"
msgstr ""
-#: media/js/utils.js:3478
+#: media/js/utils.js:3922
msgid "Nov"
msgstr ""
-#: media/js/utils.js:3479
+#: media/js/utils.js:3923
msgid "Dec"
msgstr ""
-#: media/js/utils.js:3491
+#: media/js/utils.js:3935
msgid "2 days ago"
msgstr ""
-#: media/js/utils.js:3498
+#: media/js/utils.js:3942
#, c-format
msgid "%s hour ago"
msgid_plural "%s hours ago"
msgstr[0] ""
msgstr[1] ""
-#: media/js/utils.js:3508
+#: media/js/utils.js:3952
#, c-format
msgid "%s min ago"
msgid_plural "%s mins ago"
diff --git a/askbot/locale/fr/LC_MESSAGES/django.mo b/askbot/locale/fr/LC_MESSAGES/django.mo
index ad9b2875..de12940d 100644
--- a/askbot/locale/fr/LC_MESSAGES/django.mo
+++ b/askbot/locale/fr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/askbot/locale/fr/LC_MESSAGES/django.po b/askbot/locale/fr/LC_MESSAGES/django.po
index 9c58dd10..7f77242a 100644
--- a/askbot/locale/fr/LC_MESSAGES/django.po
+++ b/askbot/locale/fr/LC_MESSAGES/django.po
@@ -4,6 +4,7 @@
#
# Translators:
# Alban Tiberghien <alban.tiberghien@gmail.com>, 2012
+# noirbizarre <noirbizarre@gmail.com>, 2013
# Camille Baldock <mademoisellegeek42@gmail.com>, 2011
# Christophe kryskool <christophe.chauvet@gmail.com>, 2011-2013
# Emmanuel <emmanuel@raviart.com>, 2013
@@ -19,8 +20,8 @@ msgstr ""
"Project-Id-Version: askbot\n"
"Report-Msgid-Bugs-To: http://askbot.org/\n"
"POT-Creation-Date: 2013-07-13 14:06-0500\n"
-"PO-Revision-Date: 2013-08-10 20:34+0000\n"
-"Last-Translator: Emmanuel <emmanuel@raviart.com>\n"
+"PO-Revision-Date: 2013-09-27 13:14+0000\n"
+"Last-Translator: noirbizarre <noirbizarre@gmail.com>\n"
"Language-Team: French (http://www.transifex.com/projects/p/askbot/language/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -603,7 +604,7 @@ msgstr "Veuillez utiliser des espaces pour séparer les entrées, ne pas utilise
#: conf/badges.py:13
msgid "Badge settings"
-msgstr "Réglages des médailles"
+msgstr "Réglages des badges"
#: conf/badges.py:23
msgid "Disciplined: minimum upvotes for deleted post"
@@ -1109,7 +1110,7 @@ msgstr "Complètement ouvert par défaut"
#: conf/forum_data_rules.py:124
msgid "Folded by default"
-msgstr ""
+msgstr "Replié par défaut"
#: conf/forum_data_rules.py:133
msgid "Question details/body editor should be"
@@ -1342,7 +1343,7 @@ msgstr ""
#: conf/karma_and_badges_visibility.py:12
msgid "Karma & Badge visibility"
-msgstr "Visibilité de l'aura et des médailles "
+msgstr "Visibilité de l'aura et des badges "
#: conf/karma_and_badges_visibility.py:27
msgid "Visibility of karma"
@@ -1354,11 +1355,11 @@ msgstr "L'aura d'un utilisateur peut être montrée, soit publiquement, soit uni
#: conf/karma_and_badges_visibility.py:44
msgid "Visibility of badges"
-msgstr "Visibilité des médailles"
+msgstr "Visibilité des badges"
#: conf/karma_and_badges_visibility.py:47
msgid "Badges can be either publicly shown or completely hidden"
-msgstr "Les médailles peuvent être soit montrées publiquement, soit complètement cachées"
+msgstr "Les badges peuvent être soit montrées publiquement, soit complètement cachées"
#: conf/ldap.py:9
msgid "LDAP login configuration"
@@ -1815,7 +1816,7 @@ msgstr "Activer le sélecteur \"Toutes les questions\""
#: conf/question_lists.py:21 conf/question_lists.py:31
#: conf/question_lists.py:41
msgid "At least one of these selectors must be enabled"
-msgstr ""
+msgstr "Au moins un de ces sélecteurs doit être activé"
#: conf/question_lists.py:30
msgid "Enable \"Unanswered Questions\" selector"
@@ -1976,7 +1977,7 @@ msgstr "Identification utilisateur"
#: conf/sidebar_profile.py:20
msgid "Custom sidebar"
-msgstr ""
+msgstr "Barre latérale personnalisée"
#: conf/sidebar_question.py:11
msgid "Question page banners and sidebar"
@@ -2051,7 +2052,7 @@ msgid ""
"values, more suitable for the larger communities, <strong>WARNING:</strong> "
"your current values for Minimum reputation, Badge Settings and Vote Rules "
"will be changed after you modify this setting."
-msgstr "Le mode \"grand site\" augmente les seuils de réputation et de certaines médailles à des valeurs mieux adaptées à des communautés plus grandes. <strong>AVERTISSEMENT :</strong> les valeurs actuelles de la réputation minimum, des réglages des médailles et des règles de vote seront changées après que vous ayez modifié ce réglage."
+msgstr "Le mode \"grand site\" augmente les seuils de réputation et de certaines badges à des valeurs mieux adaptées à des communautés plus grandes. <strong>AVERTISSEMENT :</strong> les valeurs actuelles de la réputation minimum, des réglages des badges et des règles de vote seront changées après que vous ayez modifié ce réglage."
#: conf/site_settings.py:14
msgid "URLS, keywords & greetings"
@@ -2428,7 +2429,7 @@ msgstr "Clé Akismet pour activer la détection du spam"
#: conf/super_groups.py:5
msgid "Reputation, Badges, Votes & Flags"
-msgstr "Réputation, médailles, votes et marqueurs"
+msgstr "Réputation, badges, votes et marqueurs"
#: conf/super_groups.py:6
msgid "Static Content, URLS & UI"
@@ -2727,7 +2728,7 @@ msgstr "réponse modifiée"
#: const/__init__.py:201
msgid "received badge"
-msgstr "médaille reçue"
+msgstr "badge reçue"
#: const/__init__.py:202
msgid "marked best answer"
@@ -3467,7 +3468,7 @@ msgstr "Veuillez vous connecter pour utiliser %s"
#: models/__init__.py:569 models/__init__.py:1388 views/writers.py:226
msgid "Sorry, but you have only read access"
-msgstr ""
+msgstr "Désolé, mais vous n'avez q'un accès en lecture seule"
#: models/__init__.py:573
msgid "Sorry, this operation is not allowed"
@@ -3852,22 +3853,22 @@ msgstr "%(username)s a une aura de %(reputation)s"
#, python-format
msgid "one gold badge"
msgid_plural "%(count)d gold badges"
-msgstr[0] "une médaille d'or"
-msgstr[1] "%(count)d médailles d'or"
+msgstr[0] "un badge d'or"
+msgstr[1] "%(count)d badges d'or"
#: models/__init__.py:2515
#, python-format
msgid "one silver badge"
msgid_plural "%(count)d silver badges"
-msgstr[0] "une médaille d'argent"
-msgstr[1] "%(count)d médailles d'argent"
+msgstr[0] "un badge d'argent"
+msgstr[1] "%(count)d badges d'argent"
#: models/__init__.py:2522
#, python-format
msgid "one bronze badge"
msgid_plural "%(count)d bronze badges"
-msgstr[0] "une médaille de bronze"
-msgstr[1] "%(count)d médailles de bronze"
+msgstr[0] "un badge de bronze"
+msgstr[1] "%(count)d badges de bronze"
#: models/__init__.py:2533
#, python-format
@@ -3930,7 +3931,7 @@ msgstr "\"%(title)s\""
msgid ""
"Congratulations, you have received a badge '%(badge_name)s'. Check out <a "
"href=\"%(user_profile)s\">your profile</a>."
-msgstr "Félicitations, vous avez reçu une médaille '%(badge_name)s'. Consultez <a href=\"%(user_profile)s\">votre profil</a>."
+msgstr "Félicitations, vous avez reçu un badge '%(badge_name)s'. Consultez <a href=\"%(user_profile)s\">votre profil</a>."
#: models/__init__.py:3657
#, python-format
@@ -4571,12 +4572,12 @@ msgstr "%(name)s"
#: templates/badge.html:5
msgid "Badge"
-msgstr "Médaille"
+msgstr "Badge"
#: templates/badge.html:7
#, python-format
msgid "Badge \"%(name)s\""
-msgstr "Médaille \"%(name)s\""
+msgstr "Badge \"%(name)s\""
#: templates/badge.html:9 templates/user_profile/user_recent.html:16
#: templates/user_profile/user_stats.html:71
@@ -4587,12 +4588,12 @@ msgstr "%(description)s"
#: templates/badge.html:14
msgid "user received this badge:"
msgid_plural "users received this badge:"
-msgstr[0] "utilisateur a reçu cette médaille :"
-msgstr[1] "utilisateurs ont reçu cette médaille :"
+msgstr[0] "utilisateur a reçu ce badge :"
+msgstr[1] "utilisateurs ont reçu ce badge :"
#: templates/badges.html:3 templates/badges.html.py:5
msgid "Badges"
-msgstr "Médailles"
+msgstr "Badges"
#: templates/badges.html:7
msgid "Community gives you awards for your questions, answers and votes."
@@ -4602,11 +4603,11 @@ msgstr "La communauté récompense vos questions, vos réponses et vos votes en
msgid ""
"Below is the list of available badges and number of times each type of badge"
" has been awarded."
-msgstr "Ci-dessous figure la liste des médailles existantes et le nombre de fois où elles ont été décernées."
+msgstr "Ci-dessous figure la liste des badges existants et le nombre de fois où elles ont été décernées."
#: templates/badges.html:31
msgid "Community badges"
-msgstr "Niveaux de médailles"
+msgstr "Niveaux de badges"
#: templates/badges.html:33
msgid "gold badge: the highest honor and is very rare"
@@ -4616,7 +4617,7 @@ msgstr "badge en or: le plus haute distinction et est très rare"
msgid ""
"Gold badge is the highest award in this community. To obtain it you have to show \n"
"profound knowledge and ability in addition to your active participation."
-msgstr "La médaille d'or est la plus haute distinction dans cette communauté. Pour l'obtenir, vous devez montrer une profonde connaissance et de grandes capacités en plus de votre participation active."
+msgstr "Le badge d'or est la plus haute distinction dans cette communauté. Pour l'obtenir, vous devez montrer une profonde connaissance et de grandes capacités en plus de votre participation active."
#: templates/badges.html:42 templates/badges.html.py:46
msgid ""
@@ -4625,7 +4626,7 @@ msgstr "badge en argent: occasionnellement donné pour des contributions de trè
#: templates/badges.html:49 templates/badges.html.py:53
msgid "bronze badge: often given as a special honor"
-msgstr "médaille de bronze : souvent donnée en tant qu'honneur spécial"
+msgstr "badge de bronze : souvent donnée en tant qu'honneur spécial"
#: templates/base.html:23
#, python-format
@@ -5173,7 +5174,7 @@ msgstr "Voir les questions marquées par '%(tag)s'."
#: templates/macros.html:395
msgid "Comments"
-msgstr ""
+msgstr "Commentaires"
#: templates/macros.html:430
msgid "delete this comment"
@@ -5274,7 +5275,7 @@ msgstr "voir plus de commentaires"
#: templates/question.html:232 templates/question.html.py:335
msgid "add a comment"
-msgstr ""
+msgstr "ajouter un commentaire"
#: templates/question.html:245 templates/question/content.html:46
msgid "Answer Your Own Question"
@@ -5918,15 +5919,15 @@ msgstr ""
#: templates/email/delayed_email_alert.html:2
#, python-format
msgid "Dear %(name)s,"
-msgstr ""
+msgstr "Cher %(name)s,"
#: templates/email/delayed_email_alert.html:3
#, python-format
msgid "The following question has been updated %(site_name)s:"
msgid_plural ""
"The following %(num)s questions have been updated on %(site_name)s:"
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "La question suivante a été mise à jour %(site_name)s:"
+msgstr[1] "Les %(num)s questions suivantes ont été mises à jour sur %(site_name)s:"
#: templates/email/feedback_email.txt:2
#, python-format
@@ -6041,8 +6042,8 @@ msgid_plural ""
"\n"
" %(count)s comments:\n"
" "
-msgstr[0] ""
-msgstr[1] ""
+msgstr[0] "\n%(count)s commentaire:"
+msgstr[1] "\n%(count)s commentaires:"
#: templates/email/re_welcome_lamson_on.html:2
#: templates/email/re_welcome_lamson_on.html:3
@@ -6532,7 +6533,7 @@ msgstr "Votre réponse"
#: templates/question/new_answer_form.html:22
msgid "Be the first one to answer this question!"
-msgstr "Soyez le premier à répondre à cette quesion !"
+msgstr "Soyez le premier à répondre à cette question !"
#: templates/question/new_answer_form.html:28
msgid ""
@@ -7215,7 +7216,7 @@ msgstr[1] "<span class=\"count\">%(counter)s</span> Mots-clés"
msgid "<span class=\"count\">%(counter)s</span> Badge"
msgid_plural "<span class=\"count\">%(counter)s</span> Badges"
msgstr[0] "<span class=\"count\">%(counter)s</span> Badge"
-msgstr[1] "<span class=\"count\">%(counter)s</span> Médailles"
+msgstr[1] "<span class=\"count\">%(counter)s</span> Badges"
#: templates/user_profile/user_stats.html:85
msgid "Answer to:"
@@ -7481,7 +7482,7 @@ msgstr "Communauté"
#: templates/widgets/meta_nav.html:27
msgid "badges"
-msgstr "médailles"
+msgstr "badges"
#: templates/widgets/question_edit_tips.html:5
msgid "ask a question interesting to this community"
@@ -7543,11 +7544,11 @@ msgstr ""
#: templates/widgets/tag_selector.html:59
msgid "Show only questions from"
-msgstr ""
+msgstr "Ne montrer que les questions de"
#: templates/widgets/tag_selector.html:70
msgid "Send me email alerts for"
-msgstr ""
+msgstr "Envoyez-moi des alertes par email pour"
#: templates/widgets/tag_selector.html:86
msgid "Change frequency of emails"
@@ -7580,7 +7581,7 @@ msgstr "aura :"
#: templates/widgets/user_long_score_and_badge_summary.html:15
msgid "badges:"
-msgstr "médailles :"
+msgstr "badges :"
#: templates/widgets/user_navigation.html:17
msgid "sign out"
@@ -7684,7 +7685,7 @@ msgstr "veuillez utiliser au moins quelques caractères alphabétiques dans le n
#: utils/forms.py:110
msgid "symbol \"@\" is not allowed"
-msgstr ""
+msgstr "symbole \"@\" non autorisé"
#: utils/forms.py:222
msgid "Your email <i>(never shared)</i>"
@@ -7704,7 +7705,7 @@ msgstr "Cette adresse email est déjà utilisée par un autre utilisateur; veuil
#: utils/forms.py:227
msgid "this email address is not authorized"
-msgstr ""
+msgstr "cette adresse email n'est pas autorisée"
#: utils/forms.py:265
msgid "password is required"
@@ -7825,7 +7826,7 @@ msgstr "Veuillez vous inscrire pour supprimer/récupérer une publication"
#: views/commands.py:1042
#, python-format
msgid "Group %(name)s does not exist"
-msgstr ""
+msgstr "Le groupe %(name)s n'existe pas"
#: views/commands.py:1408 views/commands.py:1441
msgid "Sorry, looks like sharing request was invalid"
@@ -7920,7 +7921,7 @@ msgstr ""
#: views/users.py:739
msgid "private messages"
-msgstr ""
+msgstr "messages privés"
#: views/users.py:740
msgid "profile - messages"
diff --git a/askbot/locale/ru/LC_MESSAGES/django.mo b/askbot/locale/ru/LC_MESSAGES/django.mo
index ab73ba75..82b2b137 100644
--- a/askbot/locale/ru/LC_MESSAGES/django.mo
+++ b/askbot/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/askbot/locale/ru/LC_MESSAGES/django.po b/askbot/locale/ru/LC_MESSAGES/django.po
index dc0eff25..97464485 100644
--- a/askbot/locale/ru/LC_MESSAGES/django.po
+++ b/askbot/locale/ru/LC_MESSAGES/django.po
@@ -3866,7 +3866,7 @@ msgstr "%(reputation)s кармы %(username)s "
#, python-format
msgid "one gold badge"
msgid_plural "%(count)d gold badges"
-msgstr[0] "<span class=\"hidden\">%(count)d</span>золотая медаль"
+msgstr[0] "%(count)d золотая медаль"
msgstr[1] "%(count)d золотых медалей"
msgstr[2] "%(count)d золотых медалей"
@@ -3874,7 +3874,7 @@ msgstr[2] "%(count)d золотых медалей"
#, python-format
msgid "one silver badge"
msgid_plural "%(count)d silver badges"
-msgstr[0] "<span class=\"hidden\">%(count)d</span>серебряная медаль"
+msgstr[0] "%(count)d серебряная медаль"
msgstr[1] "%(count)d серебряных медалей"
msgstr[2] "%(count)d серебряных медалей"
@@ -3882,7 +3882,7 @@ msgstr[2] "%(count)d серебряных медалей"
#, python-format
msgid "one bronze badge"
msgid_plural "%(count)d bronze badges"
-msgstr[0] "<span class=\"hidden\">%(count)d</span>бронзовая медаль"
+msgstr[0] "%(count)d бронзовая медаль"
msgstr[1] "%(count)d бронзовых медалей"
msgstr[2] "%(count)d бронзовых медалей"
diff --git a/askbot/mail/__init__.py b/askbot/mail/__init__.py
index 0d9ee05f..cb6f86a0 100644
--- a/askbot/mail/__init__.py
+++ b/askbot/mail/__init__.py
@@ -16,6 +16,7 @@ from askbot.mail import parsing
from askbot.utils import url_utils
from askbot.utils.file_utils import store_file
from askbot.utils.html import absolutize_urls
+from askbot.utils.html import get_text_from_html
from bs4 import BeautifulSoup
from django.core import mail
from django.core.exceptions import PermissionDenied
@@ -87,19 +88,25 @@ def thread_headers(post, orig_post, update):
return headers
-def clean_html_email(email_body):
- """returns the content part from an HTML email.
- todo: needs more clenup might not work for other email templates
- that do not use table layout
- """
- soup = BeautifulSoup(email_body, 'html5lib')
- body_element = soup.find('body')
- filter_func = lambda s: bool(s.strip())
- phrases = map(
- lambda s: s.strip(),
- filter(filter_func, body_element.get_text().split('\n'))
- )
- return '\n\n'.join(phrases)
+def _send_mail(subject_line, body_text, sender_email, recipient_list, headers=None):
+ """base send_mail function, which will attach email in html format
+ if html email is enabled"""
+ html_enabled = askbot_settings.HTML_EMAIL_ENABLED
+ if html_enabled:
+ message_class = mail.EmailMultiAlternatives
+ else:
+ message_class = mail.EmailMessage
+
+ msg = message_class(
+ subject_line,
+ get_text_from_html(body_text),
+ sender_email,
+ recipient_list,
+ headers = headers
+ )
+ if html_enabled:
+ msg.attach_alternative(body_text, "text/html")
+ msg.send()
def send_mail(
subject_line=None,
@@ -130,15 +137,13 @@ def send_mail(
try:
assert(subject_line is not None)
subject_line = prefix_the_subject_line(subject_line)
- msg = mail.EmailMultiAlternatives(
- subject_line,
- clean_html_email(body_text),
- from_email,
- recipient_list,
- headers = headers
- )
- msg.attach_alternative(body_text, "text/html")
- msg.send()
+ _send_mail(
+ subject_line,
+ body_text,
+ from_email,
+ recipient_list,
+ headers=headers
+ )
logging.debug('sent update to %s' % ','.join(recipient_list))
if related_object is not None:
assert(activity_type is not None)
@@ -165,31 +170,22 @@ def mail_moderators(
).values_list('email', flat=True)
recipient_list = set(recipient_list)
- from_email = ''
- if hasattr(django_settings, 'DEFAULT_FROM_EMAIL'):
- from_email = django_settings.DEFAULT_FROM_EMAIL
+ send_mail(
+ subject_line=subject_line,
+ body_text=body_text,
+ from_email=getattr(django_settings, 'DEFAULT_FROM_EMAIL', ''),
+ recipient_list=recipient_list,
+ raise_on_failure=raise_on_failure,
+ headers=headers
+ )
- try:
- msg = mail.EmailMessage(
- subject_line,
- body_text,
- from_email,
- recipient_list,
- headers = headers or {}
- )
- msg.content_subtype = 'html'
- msg.send()
- except smtplib.SMTPException, error:
- sys.stderr.write('\n' + unicode(error).encode('utf-8') + '\n')
- if raise_on_failure == True:
- raise exceptions.EmailNotSent(unicode(error))
-INSTRUCTIONS_PREAMBLE = ugettext_lazy('<p>To ask by email, please:</p>')
+INSTRUCTIONS_PREAMBLE = ugettext_lazy('<p>To post by email, please:</p>')
QUESTION_TITLE_INSTRUCTION = ugettext_lazy(
'<li>Type title in the subject line</li>'
)
QUESTION_DETAILS_INSTRUCTION = ugettext_lazy(
- '<li>Type details of your question into the email body</li>'
+ '<li>Type details into the email body</li>'
)
OPTIONAL_TAGS_INSTRUCTION = ugettext_lazy(
"""<li>The beginning of the subject line can contain tags,
@@ -215,7 +211,7 @@ def bounce_email(
"""
if reason == 'problem_posting':
error_message = _(
- '<p>Sorry, there was an error posting your question '
+ '<p>Sorry, there was an error while processing your message '
'please contact the %(site)s administrator</p>'
) % {'site': askbot_settings.APP_SHORT_NAME}
@@ -242,7 +238,7 @@ def bounce_email(
elif reason == 'unknown_user':
error_message = _(
- '<p>Sorry, in order to post questions on %(site)s '
+ '<p>Sorry, in order to make posts to %(site)s '
'by email, please <a href="%(url)s">register first</a></p>'
) % {
'site': askbot_settings.APP_SHORT_NAME,
@@ -250,7 +246,7 @@ def bounce_email(
}
elif reason == 'permission_denied' and body_text is None:
error_message = _(
- '<p>Sorry, your question could not be posted '
+ '<p>Sorry, your post could not be made by email '
'due to insufficient privileges of your user account</p>'
)
elif body_text:
diff --git a/askbot/mail/parsing.py b/askbot/mail/parsing.py
index 46e5250f..ffec3ecc 100644
--- a/askbot/mail/parsing.py
+++ b/askbot/mail/parsing.py
@@ -19,8 +19,12 @@ QUOTE_REGEXES = (
r'\nOn [^\n]* wrote:\Z',
#GMAIL_SECOND_QUOTE_RE =
r'\n\d{4}/\d{1,2}/\d{1,2} [^\n]*\Z',
- #OUTLOOK1_QUOTE_RE =
- r'\n-+Original Message-+\nFrom:.*?\nSent:.*?\nTo:.*?\nSubject:.*?\Z',
+ #BLACKBERRY
+ r'_+\nFrom:.*?\nSent:.*?\nTo:.*?\nSubject:.*?\Z',
+ #OUTLOOK1
+ r'\n-+[\w -]+\nFrom:.*?\nSent:.*?\nTo:.*?\nSubject:.*?\Z',
+ #unknown
+ r'\n-+[\w -]+\nFrom:.*?\nDate:.*?\nTo:.*?\nSubject:.*?\Z',
#YAHOO_QUOTE_RE =
r'\n_+\n\s*From: [^\n]+\nTo: [^\n]+\nSent: [^\n]+\nSubject: [^\n]+\Z',
#KMAIL_QUOTE_RE =
diff --git a/askbot/management/commands/add_admin.py b/askbot/management/commands/add_admin.py
index 6f7c7034..3f8f7fea 100644
--- a/askbot/management/commands/add_admin.py
+++ b/askbot/management/commands/add_admin.py
@@ -41,5 +41,4 @@ class Command(NoArgsCommand):
self.confirm_action()
self.remove_signals()
- self.user.set_admin_status()
- self.user.save()
+ self.user.set_status('d')
diff --git a/askbot/management/commands/apply_hinted_tags.py b/askbot/management/commands/apply_hinted_tags.py
index 94bf2383..050143c8 100644
--- a/askbot/management/commands/apply_hinted_tags.py
+++ b/askbot/management/commands/apply_hinted_tags.py
@@ -1,6 +1,8 @@
import datetime
+from django.conf import settings as django_settings
from django.core.management.base import BaseCommand
from django.core.management.base import CommandError
+from django.utils import translation
from optparse import make_option
from askbot.utils.console import ProgressBar
from askbot.models import Thread
@@ -27,6 +29,7 @@ class Command(BaseCommand):
then applies tags to questions by matching them
with the question titles and content
"""
+ translation.activate(django_settings.LANGUAGE_CODE)
if kwargs['tags_file'] is None:
raise CommandError('parameter --tags-file is required')
try:
diff --git a/askbot/management/commands/askbot_add_osqa_content.py b/askbot/management/commands/askbot_add_osqa_content.py
new file mode 100644
index 00000000..acea0391
--- /dev/null
+++ b/askbot/management/commands/askbot_add_osqa_content.py
@@ -0,0 +1,482 @@
+from askbot.deps.django_authopenid.models import UserAssociation
+from askbot.management.commands.base import BaseImportXMLCommand
+from askbot.models import Award
+from askbot.models import BadgeData
+from askbot.models import Post
+from askbot.models import PostRevision
+from askbot.models import Thread
+from askbot.models import Tag
+from askbot.models import User
+from askbot.utils.slug import slugify_camelcase
+from bs4 import BeautifulSoup
+from datetime import datetime
+from django.db.models import Q
+from django.utils import translation
+from django.conf import settings as django_settings
+from django.utils.http import urlquote as django_urlquote
+from django.template.defaultfilters import slugify
+from HTMLParser import HTMLParser
+
+def decode_datetime(data):
+ """Decodes formats:
+ * '2013-10-25 09:46:34'
+ * '2013-10-25'
+ """
+ if data:
+ try:
+ return datetime.strptime(data, '%Y-%m-%d %H:%M:%S')
+ except ValueError:
+ return datetime.strptime(data, '%Y-%m-%d')
+ return None
+
+class DataObject(object):
+ def __init__(self, soup):
+ """Initializes object based on the values passed
+ via BeautifulSoup instance for that object"""
+ self.soup = soup
+ self.data = dict()
+
+ def decode_typed_value(self, field):
+ field_type = field['type']
+ value = field.text.strip()
+ if field_type == 'BooleanField':
+ if value == 'False':
+ return False
+ else:
+ return True
+ elif field_type in ('CharField', 'TextField'):
+ return value
+ elif 'Integer' in field_type:
+ return int(value)
+ elif field_type in ('DateField', 'DateTimeField'):
+ return decode_datetime(value)
+ else:
+ raise ValueError('unknown field type: %s' % field_type)
+
+ def decode_rel_value(self, field):
+ rel_type = field['rel']
+ if rel_type in ('ManyToOneRel', 'OneToOneRel'):
+ try:
+ return int(field.text)
+ except:
+ return None
+ elif rel_type == 'ManyToManyRel':
+ items = field.find_all('object')
+ return [item['pk'] for item in items]
+ else:
+ raise ValueError('unknown relation type %s' % rel_type)
+
+ def decode_value(self, key):
+ """
+ type="DateField">
+ type="DateTimeField">
+ """
+ if key in ('pk', 'id'):
+ return int(self.soup['pk'])
+ field = self.soup.find('field', attrs={'name': key})
+ if field is None:
+ raise ValueError('could not find field %s' % key)
+ if field.get('type') != None:
+ return self.decode_typed_value(field)
+ elif field.get('rel') != None:
+ return self.decode_rel_value(field)
+ else:
+ raise ValueError('unknown field class %s - neither data nor relation')
+
+
+ def __getattr__(self, key):
+ """Returns value of property, if decoded
+ or decodes the property first from the bs4 soup"""
+ if key not in self.data:
+ value = self.decode_value(key)
+ self.data[key] = value
+ return self.data[key]
+
+
+class Command(BaseImportXMLCommand):
+ args = '<xml file>'
+ help = 'Adds XML OSQA data produced by the "dumpdata" command'
+
+ def handle(self, *args, **options):
+ translation.activate(django_settings.LANGUAGE_CODE)
+
+ self.setup_run()
+ self.redirect_format = self.get_redirect_format(options['redirect_format'])
+
+ dump_file_name = args[0]
+ xml = open(dump_file_name, 'r').read()
+ self.soup = BeautifulSoup(xml, ['lxml', 'xml'])
+
+ #site settings
+ #forum.keyvalue
+ self.import_users()
+ self.import_user_logins()
+ #model="forum.tag"
+ self.import_tags()
+
+ #model="forum.question"/answer/comment - derivatives of the Node model
+ self.import_threads()
+ self.import_posts('question', True)
+ #inside we also mark accepted answer, b/c it's more convenient that way
+ self.import_posts('answer')
+ self.import_posts('comment')
+ #model="forum.noderevision"
+ self.import_post_revisions()
+
+ self.fix_answer_counts()
+ self.fix_comment_counts()
+
+ #model="forum.subscriptionsettings"
+ #this model has no correspondence in Askbot
+
+ #model="forum.actionrepute"
+ #model="forum.award"
+
+ #model="forum.nodestate"
+
+ #model="forum.question"
+ #model="forum.questionsubscription"
+ #model="forum.userproperty"
+ #model="forum.validationhash"
+ #model="forum.vote"
+
+ #self.import_marked_tags()
+
+ #self.apply_groups_to_threads()
+
+ #self.apply_question_followers()
+ self.import_votes()
+
+ self.import_badges()
+ #self.import_badge_awards()
+
+ def get_objects_for_model(self, model):
+ objects_soup = self.soup.find_all(attrs={'model': model})
+ for item_soup in objects_soup:
+ yield DataObject(item_soup)
+
+ def import_users(self):
+ """import OSQA users to Askbot users"""
+ #in OSQA user profile is split in two models
+ #auth.user
+ #forum.user
+ for from_user in self.get_objects_for_model('auth.user'):
+ try:
+ to_user = User.objects.get(email=from_user.email)
+ except User.DoesNotExist:
+ username = self.get_safe_username(from_user.username)
+ to_user = User.objects.create_user(username, from_user.email)
+
+ self.copy_string_parameter(from_user, to_user, 'first_name')
+ self.copy_string_parameter(from_user, to_user, 'last_name')
+ self.copy_string_parameter(from_user, to_user, 'password')
+ self.copy_bool_parameter(from_user, to_user, 'is_staff')
+ self.copy_bool_parameter(from_user, to_user, 'is_active')
+ self.copy_bool_parameter(from_user, to_user, 'is_superuser')
+ self.copy_numeric_parameter(from_user, to_user, 'last_login', operator='max')
+ self.copy_numeric_parameter(from_user, to_user, 'date_joined', operator='min')
+ to_user.save()
+
+ self.log_action(from_user, to_user)
+
+ for profile in self.get_objects_for_model('forum.user'):
+ user = self.get_imported_object_by_old_id(User, profile.id)
+ self.copy_bool_parameter(profile, user, 'email_isvalid')
+ user.reputation = max(user.reputation + profile.reputation - 1, 1)
+ user.gold += profile.gold
+ user.silver += profile.silver
+ user.bronze += profile.bronze
+ self.copy_string_parameter(profile, user, 'real_name')
+ self.copy_numeric_parameter(profile, user, 'last_seen', operator='max')
+ self.copy_string_parameter(profile, user, 'website')
+ self.copy_string_parameter(profile, user, 'location')
+ self.copy_numeric_parameter(profile, user, 'date_of_birth')
+ self.copy_string_parameter(profile, user, 'about')
+ user.save()
+
+ def import_user_logins(self):
+ """import user's login methods from OSQA to Askbot"""
+ for user_login in self.get_objects_for_model('forum.authkeyuserassociation'):
+ assoc = UserAssociation()
+ assoc.openid_url = user_login.key
+ assoc.user = self.get_imported_object_by_old_id(User, user_login.user)
+ assoc.provider_name = user_login.provider
+ assoc.last_used_timestamp = user_login.added_at
+ assoc.save()
+
+ def import_tags(self):
+ """imports OSQA tags to Askbot tags"""
+ """
+ <object model="forum.tag" pk="2">
+ <field name="name" type="CharField">
+ pro
+ </field>
+ <field name="created_by" rel="ManyToOneRel" to="forum.user">
+ 1
+ </field>
+ <field name="created_at" type="DateTimeField">
+ 2012-06-09 18:34:13
+ </field>
+ <field name="used_count" type="PositiveIntegerField">
+ 259
+ </field>
+ </object>
+ """
+ for osqa_tag in self.get_objects_for_model('forum.tag'):
+ tag = Tag()
+ tag.name = osqa_tag.name
+ tag.created_by = self.get_imported_object_by_old_id(User, osqa_tag.created_by)
+ tag.used_count = osqa_tag.used_count
+ tag.save()
+
+ def import_badges(self):
+ """remembers relation of OSQA badges with Askbot badges"""
+ #model="forum.badge"
+ for osqa_badge in self.get_objects_for_model('forum.badge'):
+ badge_slug = slugify_camelcase(osqa_badge.cls)
+ try:
+ askbot_badge = BadgeData.objects.get(slug=badge_slug)
+ except BadgeData.DoesNotExist:
+ print 'Could not find an equivalent to badge %s in Askbot' % osqa_badge.cls
+ continue
+ self.log_action(osqa_badge, askbot_badge)
+ """
+ <object model="forum.badge" pk="1">
+ <field name="type" type="SmallIntegerField">
+ 3
+ </field>
+ <field name="cls" type="CharField">
+ PopularQuestion
+ </field>
+ <field name="awarded_count" type="PositiveIntegerField">
+ 0
+ </field>
+ </object>
+ """
+ """
+ slug = models.SlugField(max_length=50, unique=True)
+ awarded_count = models.PositiveIntegerField(default=0)
+ awarded_to = models.ManyToManyField(
+ User, through='Award', related_name='badges'
+ )
+ """
+
+ def import_badge_awards(self):
+ """Makes sure that users are re-awarded all previously
+ awarded OSQA badges"""
+ for osqa_award in self.get_objects_for_model('forum.award'):
+ user = self.get_imported_object_by_old_id(User, osqa_award.user)
+ badge = self.get_imported_object_by_old_id(BadgeData, osqa_award.badge)
+ if badge is None:
+ continue
+ print 'awarding badge %s' % badge.slug
+ #if multiple or user does not have this badge, then award
+ if badge.is_multiple() or (not user.has_badge(badge)):
+ award = Award()
+ award.badge = badge
+ award.user = user
+ award.notified = True
+ #todo: here we need to map to the node object
+ #content_type = self.get_content_type_by_old_id(award.content_type_id)
+ #obj_class = content_type.model_class()
+ #award.object_id = self.get_imported_object_id_by_old_id(obj_class, award.object_id)
+ #award.content_type = content_type
+ award.save()
+ """
+ <object model="forum.award" pk="1">
+ <field name="user" rel="ManyToOneRel" to="forum.user">
+ 1
+ </field>
+ <field name="badge" rel="ManyToOneRel" to="forum.badge">
+ 32
+ </field>
+ <field name="node" rel="ManyToOneRel" to="forum.node">
+ <None/>
+ </field>
+ <field name="awarded_at" type="DateTimeField">
+ 2012-06-08 17:49:15
+ </field>
+ <field name="trigger" rel="ManyToOneRel" to="forum.action">
+ 4
+ </field>
+ <field name="action" rel="OneToOneRel" to="forum.action">
+ 6
+ </field>
+ </object>
+ """
+
+ def import_threads(self):
+ """import thread objects"""
+ count = 0
+ for osqa_thread in self.get_objects_for_model('forum.question'):
+ count += 1
+ #todo: there must be code lated to set the commented values
+ lang = django_settings.LANGUAGE_CODE
+ thread = Thread(
+ title=osqa_thread.title,
+ tagnames=osqa_thread.tagnames,
+ view_count=osqa_thread.extra_count,
+ #favourite_count=thread.favourite_count,
+ #answer_count=thread.answer_count,
+ last_activity_at=osqa_thread.last_activity_at,
+ last_activity_by=self.get_imported_object_by_old_id(User, osqa_thread.last_activity_by),
+ language_code=lang,
+ #"closed" data is stored differently in OSQA
+ #closed_by=self.get_imported_object_by_old_id(User, thread.closed_by_id),
+ #closed=thread.closed,
+ #closed_at=thread.closed_at,
+ #close_reason=thread.close_reason,
+ #deleted=False,
+ approved=True, #no equivalent in OSQA
+ #must be done later, after importing answers
+ #answer_accepted_at=thread.answer_accepted_at,
+ added_at=osqa_thread.added_at,
+ )
+
+ #apply tags to threads
+ tag_names = thread.get_tag_names()
+ if tag_names:
+
+ tag_filter = Q(name__iexact=tag_names[0])
+ for tag_name in tag_names[1:]:
+ tag_filter |= Q(name__iexact=tag_name)
+ tags = Tag.objects.filter(tag_filter & Q(language_code=lang))
+
+ thread.tagnames = ' '.join([tag.name for tag in tags])
+
+ thread.save()
+ for tag in tags:
+ thread.tags.add(tag)
+ tag.used_count += 1
+ tag.save()
+
+ else:
+ thread.save()
+
+ self.log_action(osqa_thread, thread)
+
+ def import_posts(self, post_type, save_redirects=False):
+ """imports osqa Nodes to askbot Post objects"""
+ if save_redirects:
+ redirects_file = self.open_unique_file('question_redirects')
+
+ models_map = {
+ 'question': 'forum.question',
+ 'answer': 'forum.answer',
+ 'comment': 'forum.comment'
+ }
+
+ model_name = models_map[post_type]
+
+ for osqa_node in self.get_objects_for_model(model_name):
+ #we iterate through all nodes, but pick only the ones we need
+ if osqa_node.node_type != post_type:
+ continue
+
+ #cheat: do not import deleted content
+ if '(deleted)' in osqa_node.state_string:
+ continue
+
+ post = Post()
+
+ #this line is a bit risky, but should work if we import things in correct order
+ if osqa_node.parent:
+ post.parent = self.get_imported_object_by_old_id(Post, osqa_node.parent)
+ if post.parent is None:
+ continue #deleted parent
+ post.thread = post.parent.thread
+ else:
+ post.thread = self.get_imported_object_by_old_id(Thread, osqa_node.id)
+ if post.thread is None:
+ continue #deleted thread
+
+ post.post_type = osqa_node.node_type
+ post.added_at = osqa_node.added_at
+
+ if save_redirects:
+ slug = django_urlquote(slugify(osqa_node.title))
+ #todo: add i18n to the old url
+ old_url = '/questions/%d/%s/' % (osqa_node.id, slug)
+
+ post.author = self.get_imported_object_by_old_id(User, osqa_node.author)
+ #html will de added with the revisions
+ #post.html = HTMLParser().unescape(osqa_node.body)
+ post.summary = post.get_snippet()
+
+ #these don't have direct equivalent in the OSQA Node object
+ #post.deleted_by - deleted nodes are not imported
+ #post.locked_by
+ #post.last_edited_by
+
+ #these are to be set later with the real values
+ post.points = 0
+ post.vote_up_count = 0
+ post.vote_down_count = 0
+ post.offensive_flag_count = 0
+
+ post.save()
+
+ #mark accepted answer
+ if osqa_node.node_type == 'answer':
+ if '(accepted)' in osqa_node.state_string:
+ post.thread.accepted_answer = post
+ post.thread.save()
+
+
+ if save_redirects:
+ new_url = post.get_absolute_url()
+ self.write_redirect(old_url, new_url, redirects_file)
+
+ self.log_action_with_old_id(osqa_node.id, post)
+
+ if save_redirects:
+ redirects_file.close()
+
+ def import_post_revisions(self):
+ """Imports OSQA revisions to Askbot revisions"""
+ for osqa_revision in self.get_objects_for_model('forum.noderevision'):
+ post = self.get_imported_object_by_old_id(Post, osqa_revision.node)
+ if post is None:
+ continue #deleted post
+ user = self.get_imported_object_by_old_id(User, osqa_revision.author)
+ revision = PostRevision(
+ post=post,
+ author=user,
+ text=osqa_revision.body,
+ title=osqa_revision.title,
+ tagnames=osqa_revision.tagnames,
+ revised_at=osqa_revision.revised_at,
+ summary=osqa_revision.summary,
+ revision=osqa_revision.revision
+ )
+ post.text = osqa_revision.body
+ if osqa_revision == 1:
+ post.added_at = osqa_revision.revised_at
+ else:
+ post.last_edited_at = osqa_revision.revised_at
+ post.last_edited_by = user
+
+ post.parse_and_save(author=user)
+ revision.save()
+
+ def import_votes(self):
+ """Imports OSQA votes to Askbot votes"""
+ for osqa_vote in self.get_objects_for_model('forum.vote'):
+ post = self.get_imported_object_by_old_id(Post, osqa_vote.node)
+ if post is None:
+ continue #deleted post
+ user = self.get_imported_object_by_old_id(User, osqa_vote.user)
+ if osqa_vote.value > 0:
+ user.upvote(post, timestamp=osqa_vote.voted_at, force=True)
+ elif osqa_vote.value < 0:
+ user.downvote(post, timestamp=osqa_vote.voted_at, force=True)
+
+ def fix_answer_counts(self):
+ for thread in Thread.objects.all():
+ thread.answer_count = thread.get_answers().count()
+ thread.save()
+
+ def fix_comment_counts(self):
+ for post in Post.objects.filter(post_type__in=('question', 'answer')):
+ post.comment_count = Post.objects.filter(post_type='comment', parent=post).count()
+ post.save()
diff --git a/askbot/management/commands/askbot_add_test_content.py b/askbot/management/commands/askbot_add_test_content.py
index a09fb086..5e5d4254 100644
--- a/askbot/management/commands/askbot_add_test_content.py
+++ b/askbot/management/commands/askbot_add_test_content.py
@@ -61,19 +61,24 @@ class Command(NoArgsCommand):
)
)
- def save_alert_settings(self):
+ def backup_settings(self):
settings = {}
for key in ALERT_SETTINGS_KEYS:
settings[key] = getattr(askbot_settings, key)
self.alert_settings = settings
+ self.limit_on_answer_setting = askbot_settings.LIMIT_ONE_ANSWER_PER_USER
- def stop_alerts(self):
+
+ def modify_settings(self):
for key in ALERT_SETTINGS_KEYS:
askbot_settings.update(key, 'n')
+ askbot_settings.update('LIMIT_ONE_ANSWER_PER_USER', False)
- def restore_saved_alert_settings(self):
+ def restore_settings(self):
for key in ALERT_SETTINGS_KEYS:
askbot_settings.update(key, self.alert_settings[key])
+ value = self.limit_on_answer_setting
+ askbot_settings.update('LIMIT_ONE_ANSWER_PER_USER', value)
def print_if_verbose(self, text):
"Only print if user chooses verbose output"
@@ -121,7 +126,7 @@ class Command(NoArgsCommand):
last_vote = False
# Each user posts a question
for i in range(NUM_QUESTIONS):
- user = users[i]
+ user = users[i % len(users)]#allows to post many questions all by less users
# Downvote/upvote the questions - It's reproducible, yet
# gives good randomized data
if not active_question is None:
@@ -164,7 +169,8 @@ class Command(NoArgsCommand):
active_answer = None
last_vote = False
# Now, fill the last added question with answers
- for user in users[:NUM_ANSWERS]:
+ for i in range(NUM_ANSWERS):
+ user = users[i % len(users)]
# We don't need to test for data validation, so ONLY users
# that aren't authors can post answer to the question
if not active_question.author is user:
@@ -213,7 +219,8 @@ class Command(NoArgsCommand):
active_question_comment = None
active_answer_comment = None
- for user in users[:NUM_COMMENTS]:
+ for i in range(NUM_COMMENTS):
+ user = users[i % len(users)]
active_question_comment = user.post_comment(
parent_post = active_question,
body_text = COMMENT_TEMPLATE
@@ -242,6 +249,8 @@ class Command(NoArgsCommand):
self.verbosity = int(options.get("verbosity", 1))
self.interactive = options.get("interactive")
+ # post a bunch of answers by admin now - that active_question is
+ # posted by someone else
if self.interactive:
answer = choice_dialog("This command will DELETE ALL DATA in the current database, and will fill the database with test data. Are you absolutely sure you want to proceed?",
choices = ("yes", "no", ))
@@ -249,16 +258,21 @@ class Command(NoArgsCommand):
return
translation.activate(django_settings.LANGUAGE_CODE)
-
- self.save_alert_settings()
- self.stop_alerts()# saves time on running the command
+ self.backup_settings()
+ self.modify_settings()# saves time on running the command
# Create Users
users = self.create_users()
- # Create Questions, vote for questions
+ # Create a bunch of questions and answers by a single user
+ # to test pagination in the user profile
+ active_question = self.create_questions(users[0:1])
+
+ # Create Questions, vote for questions by all other users
active_question = self.create_questions(users)
+ active_answer = self.create_answers(users[0:1], active_question)
+
# Create Answers, vote for the answers, vote for the active question
# vote for the active answer
active_answer = self.create_answers(users, active_question)
@@ -302,7 +316,5 @@ class Command(NoArgsCommand):
force = True,
)
self.print_if_verbose("User has accepted a best answer")
-
- self.restore_saved_alert_settings()
-
+ self.restore_settings()
self.print_if_verbose("DONE")
diff --git a/askbot/management/commands/askbot_add_xml_content.py b/askbot/management/commands/askbot_add_xml_content.py
new file mode 100644
index 00000000..84082253
--- /dev/null
+++ b/askbot/management/commands/askbot_add_xml_content.py
@@ -0,0 +1,558 @@
+from askbot.models import BadgeData
+from askbot.models import FavoriteQuestion
+from askbot.models import Group
+from askbot.models import ImportedObjectInfo
+from askbot.models import Post
+from askbot.models import Tag
+from askbot.models import Thread
+from askbot.models import User
+from askbot.management.commands.base import BaseImportXMLCommand
+from django.conf import settings as django_settings
+from django.contrib.auth.models import Group as AuthGroup
+from django.contrib.contenttypes.models import ContentType
+from django.db import transaction
+from django.db.models import Q
+
+if 'avatar' in django_settings.INSTALLED_APPS:
+ from avatar.models import Avatar
+
+def get_status_rank(status):
+ """returns integer rank of user account status,
+ the larger is the number the higher is the status"""
+ if len(status) != 1:
+ #default status - approved user
+ status = 'a'
+ try:
+ return 'bswamd'.index(status)
+ except ValueError:
+ return 0
+
+class Command(BaseImportXMLCommand):
+ help = 'Adds XML askbot data produced by the "dumpdata" command'
+
+ def handle_import(self):
+ self.read_content_types()
+
+ self.import_groups()
+ self.import_users()
+ #we don't import subscriptions
+ if 'avatar' in django_settings.INSTALLED_APPS:
+ self.import_avatars()
+
+ #we need this to link old user ids to
+ #new users' personal groups
+ #self.record_personal_groups()
+
+ self.import_user_logins()
+ self.import_tags()
+ self.import_marked_tags()
+
+ self.import_threads()
+ self.apply_groups_to_threads()
+
+ #model="askbot.posttogroup">
+ self.import_posts('question', save_redirects=True)
+ self.import_posts('answer')
+ self.import_posts('comment')
+ self.import_post_revisions()
+ self.apply_groups_to_posts()
+ self.apply_question_followers()
+ self.import_votes()
+
+ self.import_badges()
+ self.import_badge_awards()
+ self.delete_new_messages()
+ #we'll try to ignore importing this
+ #model="askbot.activity"
+
+ def log_personal_group(self, group):
+ info = ImportedObjectInfo()
+ info.old_id = group.id
+ info.new_id = int(group.name.split('_')[-1])
+ info.model = 'personal_group'
+ info.run = self.run
+ info.save()
+
+ def get_group_by_old_id(self, old_id):
+ normal_group = self.get_imported_object_by_old_id(AuthGroup, old_id)
+ if normal_group:
+ return Group.objects.get(group_ptr=normal_group)
+
+ log = ImportedObjectInfo.objects.get(
+ model='personal_group',
+ old_id=old_id,
+ run=self.run
+ )
+ old_user_id = log.new_id
+ new_user = self.get_imported_object_by_old_id(User, old_user_id)
+ return new_user.get_personal_group()
+
+ def read_content_types(self):
+ """reads content types from the data dump and makes
+ dictionary with keys of old content type ids and
+ values - active content type objects"""
+ ctypes_map = dict()
+ for old_ctype in self.get_objects_for_model('contenttypes.contenttype'):
+ try:
+ new_ctype = ContentType.objects.get(
+ app_label=old_ctype.app_label,
+ model=old_ctype.model
+ )
+ except ContentType.DoesNotExist:
+ continue
+ ctypes_map[old_ctype.id] = new_ctype
+
+ self.content_types_map = ctypes_map
+ """
+ <object pk="38" model="contenttypes.contenttype">
+ <field type="CharField" name="name">activity</field>
+ <field type="CharField" name="app_label">askbot</field>
+ <field type="CharField" name="model">activity</field>
+ </object>
+ """
+
+ def get_content_type_by_old_id(self, old_ctype_id):
+ return self.content_types_map[old_ctype_id]
+
+ @transaction.commit_manually
+ def import_groups(self):
+ """imports askbot group profiles"""
+
+ #redirects_file = self.open_unique_file('group_redirects')
+
+ #1) we import auth groups
+ for group in self.get_objects_for_model('auth.group'):
+
+ #old_url = group.get_absolute_url()
+ if group.name.startswith('_personal'):
+ #we don't import these groups, but log
+ #associations between old user ids and old personal
+ #group ids, because we create the personal groups
+ #anew and so need to have a connection
+ #old personal group id --> old user id --> new user id
+ # --> new pers. group id
+ self.log_personal_group(group)
+ continue
+ old_group_id = group.id
+ try:
+ group = AuthGroup.objects.get(name=group.name)
+ except AuthGroup.DoesNotExist:
+ group.id = None
+ group.save()
+
+ transaction.commit()
+
+ #new_url = group.get_absolute_url()
+
+ #if old_url != new_url:
+ # redirects_file.write('%s %s\n' % (old_url, new_url))
+
+ #we will later populate memberships only in these groups
+ self.log_action_with_old_id(old_group_id, group)
+
+ if transaction.is_dirty():
+ transaction.commit()
+
+ #redirects_file.close()
+
+ #2) we import askbot group profiles only for groups
+ for profile in self.get_objects_for_model('askbot.group'):
+ auth_group = self.get_imported_object_by_old_id(AuthGroup, profile.group_ptr_id)
+ if auth_group is None or auth_group.name.startswith('_personal'):
+ continue
+
+ #if profile for this group does not exist, then create new profile and save
+ try:
+ existing_profile = Group.objects.get(group_ptr__id=auth_group.id)
+ self.copy_string_parameter(profile, existing_profile, 'logo_url')
+ self.merge_words_parameter(profile, existing_profile, 'preapproved_emails')
+ self.merge_words_parameter(profile, existing_profile, 'preapproved_email_domains')
+ existing_profile.save()
+ except Group.DoesNotExist:
+ new_profile = Group.objects.create(
+ name=auth_group.name,
+ logo_url=profile.logo_url,
+ preapproved_emails=profile.preapproved_emails,
+ preapproved_email_domains=profile.preapproved_email_domains
+ )
+ new_profile.save()
+
+ transaction.commit()
+
+ if transaction.is_dirty():
+ transaction.commit()
+
+ def import_users(self):
+ redirects_file = self.open_unique_file('user_redirects')
+
+ model_path = str(User._meta)
+ dupes = 0
+ for from_user in self.get_objects_for_model('auth.user'):
+ log_info = dict()
+ log_info['notify_user'] = list()
+
+ old_url = from_user.get_absolute_url()
+
+ try:
+ to_user = User.objects.get(email=from_user.email)
+ dupes += 1
+ except User.DoesNotExist:
+ username = self.get_safe_username(from_user.username)
+ if username != from_user.username:
+ template = 'Your user name was changed from %s to %s'
+ log_info['notify_user'].append(template % (from_user.username, username))
+ to_user = User.objects.create_user(username, from_user.email)
+
+ #copy the data
+ if from_user.username != to_user.username:
+ names = (from_user.username, to_user.username)
+ log_info['notify_user'].append('Your user name has changed from %s to %s' % names)
+
+ self.copy_string_parameter(from_user, to_user, 'first_name')
+ self.copy_string_parameter(from_user, to_user, 'last_name')
+ self.copy_string_parameter(from_user, to_user, 'real_name')
+ self.copy_string_parameter(from_user, to_user, 'website')
+ self.copy_string_parameter(from_user, to_user, 'location')
+
+ to_user.country = from_user.country
+
+ self.copy_string_parameter(from_user, to_user, 'about')
+ self.copy_string_parameter(from_user, to_user, 'email_signature')
+ self.copy_string_parameter(from_user, to_user, 'twitter_access_token')
+ self.copy_string_parameter(from_user, to_user, 'twitter_handle')
+
+ self.merge_words_parameter(from_user, to_user, 'interesting_tags')
+ self.merge_words_parameter(from_user, to_user, 'ignored_tags')
+ self.merge_words_parameter(from_user, to_user, 'subscribed_tags')
+ self.merge_words_parameter(from_user, to_user, 'languages')
+
+ if to_user.password == '!' and from_user.password != '!':
+ to_user.password = from_user.password
+ self.copy_bool_parameter(from_user, to_user, 'is_staff')
+ self.copy_bool_parameter(from_user, to_user, 'is_active')
+ self.copy_bool_parameter(from_user, to_user, 'is_superuser')
+ self.copy_bool_parameter(from_user, to_user, 'is_fake', operator='and')
+ self.copy_bool_parameter(from_user, to_user, 'email_isvalid', operator='and')
+ self.copy_bool_parameter(from_user, to_user, 'show_country')
+ self.copy_bool_parameter(from_user, to_user, 'show_marked_tags')
+
+ self.copy_numeric_parameter(from_user, to_user, 'last_login')
+ self.copy_numeric_parameter(from_user, to_user, 'last_seen')
+ self.copy_numeric_parameter(from_user, to_user, 'date_joined', operator='min')
+ self.copy_numeric_parameter(from_user, to_user, 'email_tag_filter_strategy')
+ self.copy_numeric_parameter(from_user, to_user, 'display_tag_filter_strategy')
+ self.copy_numeric_parameter(
+ from_user,
+ to_user,
+ 'consecutive_days_visit_count',
+ operator='sum'
+ )
+ self.copy_numeric_parameter(from_user, to_user, 'social_sharing_mode')
+
+ #position of character in this string == rank of status
+ if get_status_rank(from_user.status) > get_status_rank(to_user.status):
+ to_user.status = from_user.status
+
+ to_user.save()
+
+ new_url = to_user.get_absolute_url()
+ self.write_redirect(old_url, new_url, redirects_file)
+
+ group_ids = self.get_m2m_ids_for_field(from_user, 'groups')
+ for group_id in group_ids:
+ #get group by old id,
+ #if group is private - skip,
+ #otherwise join this group
+ group = self.get_imported_object_by_old_id(Group, int(group_id))
+ if group is None or group.name.startswith('_personal'):
+ continue
+ #unfortunately, xml dump does not allow us to know of the membership status
+ #as m2m user -> group does not contain id of the m2m bridge relation, but
+ #only id of the group itself
+ to_user.join_group(group, force=True)
+
+ """
+ these were not imported:
+ <field type="CharField" name="email_key"><None></None></field>
+ <field type="PositiveIntegerField" name="reputation">1</field>
+ <field type="SmallIntegerField" name="gold">0</field>
+ <field type="SmallIntegerField" name="silver">0</field>
+ <field type="SmallIntegerField" name="bronze">0</field>
+ <field type="IntegerField" name="new_response_count">0</field>
+ <field type="IntegerField" name="seen_response_count">0</field>
+ """
+ self.log_action(from_user, to_user, extra_info=log_info)
+
+ redirects_file.close()
+
+ def import_avatars(self):
+ """imports user avatar, chooses later uploaded primary avatar"""
+ for avatar in self.get_objects_for_model('avatar.avatar'):
+ user = self.get_imported_object_by_old_id(User, avatar.user_id)
+
+ if avatar.primary:
+ #get other primary avatar and make the later one as primary
+ try:
+ existing_avatar = Avatar.objects.get(user=user, primary=True)
+ if existing_avatar.date_uploaded > avatar.date_uploaded:
+ avatar.primary = False
+ else:
+ existing_avatar.primary = False
+ existing_avatar.save()
+ except Avatar.DoesNotExist:
+ pass
+
+ avatar.user = user
+ avatar.id = None
+ avatar.save()
+ """
+ <object pk="9" model="avatar.avatar">
+ <field to="auth.user" name="user" rel="ManyToOneRel">33</field>
+ <field type="BooleanField" name="primary">True</field>
+ <field type="FileField" name="avatar">avatars/Valdir Barbosa/ValdirBarbosa.png</field>
+ <field type="DateTimeField" name="date_uploaded">2013-08-22T16:45:01.517315</field>
+ </object>
+ """
+
+ def import_marked_tags(self):
+ #model="askbot.markedtag">
+ for mark in self.get_objects_for_model('askbot.markedtag'):
+ tag = self.get_imported_object_by_old_id(Tag, mark.tag_id)
+ user = self.get_imported_object_by_old_id(User, mark.user_id)
+ user.mark_tags(tagnames=tag.name, reason=mark.reason, action='add')
+ """
+ <object pk="1" model="askbot.markedtag">
+ <field to="askbot.tag" name="tag" rel="ManyToOneRel">13</field>
+ <field to="auth.user" name="user" rel="ManyToOneRel">205</field>
+ <field type="CharField" name="reason">good</field>
+ </object>
+ """
+
+ @transaction.commit_manually
+ def import_user_logins(self):
+ #logins_soup = self.soup.find_all('object', {'model': 'django_authopenid.userassociation'})
+ #for login_info in self.get_objects_for_model('django_authopenid.userassociation'):
+ #for login_
+ for association in self.get_objects_for_model('django_authopenid.userassociation'):
+ #where possible, we should copy the login, but respecting the
+ #uniqueness constraints: ('user','provider_name'), ('openid_url', 'provider_name')
+ #1) get new user by old id
+ user = self.get_imported_object_by_old_id(User, association.user_id)
+ try:
+ association.id = None
+ association.user = user
+ association.save()
+ transaction.commit()
+ except:
+ transaction.rollback()
+
+ def import_tags(self):
+ """imports tag objects"""
+ for tag in self.get_objects_for_model('askbot.tag'):
+ old_tag_id = tag.id
+ try:
+ #try to get existing tag with this name
+ tag = Tag.objects.get(name__iexact=tag.name, language_code=tag.language_code)
+ except Tag.DoesNotExist:
+ tag.id = None
+ tag.tag_wiki = None
+ tag.created_by = self.get_imported_object_by_old_id(User, tag.created_by_id)
+ tag.deleted_by = self.get_imported_object_by_old_id(User, tag.deleted_by_id)
+ tag.save()
+ self.log_action_with_old_id(old_tag_id, tag)
+
+ def import_threads(self):
+ """import thread objects"""
+ count = 0
+ for thread in self.get_objects_for_model('askbot.thread'):
+ count += 1
+ new_thread = Thread(
+ title=thread.title,
+ tagnames=thread.tagnames,
+ view_count=thread.view_count,
+ favourite_count=thread.favourite_count,
+ answer_count=thread.answer_count,
+ last_activity_at=thread.last_activity_at,
+ last_activity_by=self.get_imported_object_by_old_id(User, thread.last_activity_by_id),
+ language_code=thread.language_code,
+ closed_by=self.get_imported_object_by_old_id(User, thread.closed_by_id),
+ closed=thread.closed,
+ closed_at=thread.closed_at,
+ close_reason=thread.close_reason,
+ deleted=thread.deleted,
+ approved=thread.approved,
+ answer_accepted_at=thread.answer_accepted_at,
+ added_at=thread.added_at,
+ )
+
+ #apply tags to threads
+ tag_names = thread.get_tag_names()
+ if tag_names:
+
+ tag_filter = Q(name__iexact=tag_names[0])
+ for tag_name in tag_names[1:]:
+ tag_filter |= Q(name__iexact=tag_name)
+ tags = Tag.objects.filter(tag_filter & Q(language_code=thread.language_code))
+
+ new_thread.tagnames = ' '.join([tag.name for tag in tags])
+
+ new_thread.save()
+ for tag in tags:
+ new_thread.tags.add(tag)
+ tag.used_count += 1
+ tag.save()
+
+ else:
+ new_thread.save()
+
+ self.log_action(thread, new_thread)
+ """
+ these are not handled here
+ <object pk="155" model="askbot.thread">
+ <field to="askbot.post" name="accepted_answer" rel="ManyToOneRel"><None></None></field>
+ <field type="IntegerField" name="points">0</field>
+ <field to="auth.user" name="followed_by" rel="ManyToManyRel"></field>
+ </object>
+ """
+
+ def apply_question_followers(self):
+ """mark followed questions"""
+ for fave in self.get_objects_for_model('askbot.favoritequestion'):
+ #askbot.favoritequestion
+ user = self.get_imported_object_by_old_id(User, fave.user_id)
+ thread = self.get_imported_object_by_old_id(Thread, fave.thread_id)
+ user.toggle_favorite_question(thread._question_post(), timestamp=fave.added_at)
+ """
+ <object pk="1" model="askbot.favoritequestion">
+ <field to="askbot.thread" name="thread" rel="ManyToOneRel">8</field>
+ <field to="auth.user" name="user" rel="ManyToOneRel">32</field>
+ <field type="DateTimeField" name="added_at">2012-12-28T17:34:17.289056</field>
+ </object>
+ """
+
+ def apply_groups_to_threads(self):
+ for link in self.get_objects_for_model('askbot.threadtogroup'):
+ thread = self.get_imported_object_by_old_id(Thread, link.thread_id)
+ group = self.get_group_by_old_id(link.group_id)
+ thread.add_to_groups([group,], visibility=link.visibility)
+
+ def import_posts(self, post_type, save_redirects=False):
+ """imports posts of specific post_type"""
+ if save_redirects:
+ redirects_file = self.open_unique_file('question_redirects')
+ for post in self.get_objects_for_model('askbot.post'):
+ if post.post_type != post_type:
+ continue
+
+ #this line is a bit risky, but should work if we import things in correct order
+ post.parent = self.get_imported_object_by_old_id(Post, post.parent_id)
+
+ post.thread = self.get_imported_object_by_old_id(Thread, post.thread_id)
+
+ if save_redirects:
+ old_url = post.get_absolute_url(thread=post.thread)
+
+ post.author = self.get_imported_object_by_old_id(User, post.author_id)
+ post.deleted_by = self.get_imported_object_by_old_id(User, post.deleted_by_id)
+ post.locked_by = self.get_imported_object_by_old_id(User, post.locked_by_id)
+ post.last_edited_by = self.get_imported_object_by_old_id(User, post.last_edited_by_id)
+ post.points = 0
+ post.vote_up_count = 0
+ post.vote_down_count = 0
+ post.offensive_flag_count = 0
+
+ old_post_id = post.id
+ post.id = None
+ post.save()
+
+ if save_redirects:
+ new_url = post.get_absolute_url()
+ self.write_redirect(old_url, new_url, redirects_file)
+
+ self.log_action_with_old_id(old_post_id, post)
+
+ if save_redirects:
+ redirects_file.close()
+
+ """
+ these were not imported
+ votes
+ <field type="PositiveIntegerField" name="comment_count">0</field>
+ <field type="SmallIntegerField" name="offensive_flag_count">0</field>
+ """
+
+ def apply_groups_to_posts(self):
+ for link in self.get_objects_for_model('askbot.posttogroup'):
+ post = self.get_imported_object_by_old_id(Post, link.post_id)
+ group = self.get_group_by_old_id(link.group_id)
+ post.add_to_groups([group,])
+
+ def import_post_revisions(self):
+ for revision in self.get_objects_for_model('askbot.postrevision'):
+ revision.post = self.get_imported_object_by_old_id(Post, revision.post_id)
+ revision.author = self.get_imported_object_by_old_id(User, revision.author_id)
+ revision.approved_by = self.get_imported_object_by_old_id(User, revision.approved_by_id)
+ revision.id = None
+ revision.save()
+
+ def import_badges(self):
+ """imports badgedata objects"""
+ for badge in self.get_objects_for_model('askbot.badgedata'):
+ #here we need to make sure that we don't create duplicate badges
+ old_badge_id = badge.id
+ try:
+ new_badge = BadgeData.objects.get(slug=badge.slug)
+ except BadgeData.DoesNotExist:
+ new_badge = badge
+ new_badge.id = None
+ new_badge.awarded_count = 0 #we will re-award this, restart count
+ new_badge.save()
+
+ self.log_action_with_old_id(old_badge_id, new_badge)
+ """
+ <object pk="36" model="askbot.badgedata">
+ <field type="SlugField" name="slug">taxonomist</field>
+ <field type="PositiveIntegerField" name="awarded_count">9</field>
+ </object>
+ """
+
+ def import_badge_awards(self):
+ for award in self.get_objects_for_model('askbot.award'):
+ award.user = self.get_imported_object_by_old_id(User, award.user_id)
+ badge = self.get_imported_object_by_old_id(BadgeData, award.badge_id)
+ #if multiple or user does not have this badge, then award
+ if badge.is_multiple() or (not award.user.has_badge(badge)):
+ award.badge = badge
+ content_type = self.get_content_type_by_old_id(award.content_type_id)
+ obj_class = content_type.model_class()
+ award.object_id = self.get_imported_object_id_by_old_id(obj_class, award.object_id)
+ award.content_type = content_type
+ award.id = None
+ award.save()
+ """
+ <object pk="1" model="askbot.award">
+ <field to="auth.user" name="user" rel="ManyToOneRel">2</field>
+ <field to="askbot.badgedata" name="badge" rel="ManyToOneRel">10</field>
+ <field to="contenttypes.contenttype" name="content_type" rel="ManyToOneRel">30</field>
+ <field type="PositiveIntegerField" name="object_id">1</field>
+ <field type="DateTimeField" name="awarded_at">2012-10-22T18:09:13.527031</field>
+ <field type="BooleanField" name="notified">False</field>
+ </object>
+ """
+
+ def import_votes(self):
+ for vote in self.get_objects_for_model('askbot.vote'):
+ post = self.get_imported_object_by_old_id(Post, vote.voted_post_id)
+ user = self.get_imported_object_by_old_id(User, vote.user_id)
+ if vote.vote == 1:
+ user.upvote(post, timestamp=vote.voted_at)
+ else:
+ user.downvote(post, timestamp=vote.voted_at)
+ """
+ <object pk="1" model="askbot.vote">
+ <field to="auth.user" name="user" rel="ManyToOneRel">8</field>
+ <field to="askbot.post" name="voted_post" rel="ManyToOneRel">20</field>
+ <field type="SmallIntegerField" name="vote">1</field>
+ <field type="DateTimeField" name="voted_at">2012-12-26T19:10:08.334818</field>
+ </object>
+ """
diff --git a/askbot/management/commands/askbot_import_jive.py b/askbot/management/commands/askbot_import_jive.py
index 04788c84..325107a5 100644
--- a/askbot/management/commands/askbot_import_jive.py
+++ b/askbot/management/commands/askbot_import_jive.py
@@ -2,39 +2,266 @@ from askbot import models
from askbot.conf import settings as askbot_settings
from askbot.utils.console import ProgressBar
from askbot.utils.slug import slugify
+from askbot.utils.jive import JiveConverter
+from askbot.utils.jive import internal_link_re
+from askbot.utils.file_utils import make_file_name
from bs4 import BeautifulSoup
from django.conf import settings as django_settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
+#from askbot.utils.transaction import dummy_transaction as transaction
from django.forms import EmailField, ValidationError
+from django.utils import translation
from datetime import datetime
+from optparse import make_option
+import re
+import os
+import shutil
+
+#todo: make a pass through all attachments
+#and make sure that mimetypes dictionary is up to date
+#raise an error if it's not
+FILE_TYPES = {
+ "application/java-archive": 'jar',
+ "application/msword": 'doc',
+ "application/octet-stream": 'txt',
+ "application/text": 'txt',
+ "application/vnd.visio": 'vsd',
+ "application/x-bzip": 'bz',
+ "application/x-gzip": 'gz',
+ "application/x-java-archive": 'jar',
+ "application/x-shellscript": 'sh',
+ "application/x-zip-compressed": 'zip',
+ "application/xml": 'xml',
+ "application/zip": 'zip',
+ "image/bmp": 'bmp',
+ "image/gif": 'gif',
+ "image/jpeg": 'jpeg',
+ "image/pjpeg": 'pjpeg',
+ "image/png": 'png',
+ "image/x-png": 'png',
+ "text/html": 'html',
+ "text/java": 'java',
+ "text/plain": 'txt',
+ "text/x-java": 'java',
+ "text/x-java-source": 'java',
+ "text/x-log": 'log',
+ "text/xml": 'xml'
+}
+
+jive = JiveConverter()
def parse_date(date_str):
return datetime.strptime(date_str[:-8], '%Y/%m/%d %H:%M:%S')
+def fix_internal_links_in_post(post):
+ """will replace old internal urls with the new ones."""
+
+ def link_is_naked(match):
+ """naked link either starts at the beginning of string
+ or is not inside the jive link construct: [...]"""
+ pos = match.start()
+ # the second test is rather naive as it assumes that a
+ # | will be preceded by something like [some link
+ # which we don't test here
+ return pos < 2 or post.text[pos-2] not in ('[', '|')
+
+ def internal_link_sub(match):
+ """pull post by the matched pars in the old link
+ and returns link to the new post"""
+ link_type = match.group(1)
+ item_id = int(match.group(2))
+ lookup_key = (link_type == 'message' and 'old_answer_id' or 'old_question_id')
+ try:
+ post = models.Post.objects.get(**{lookup_key: item_id})
+ # if original link is naked, we put in into brackets
+ # so that the formatter will render the result correctly
+ # otherwise "naked" /url will stay plain text
+ new_url = post.get_absolute_url()
+ return (link_is_naked(match) and '[%s]' % new_url or new_url)
+ except models.Post.DoesNotExist:
+ return ''
+
+ post.text = internal_link_re.sub(internal_link_sub, post.text)
+ post.save()
+
+def turn_first_company_user_to_admin(domain):
+ company_users = models.User.objects.filter(
+ email__endswith='@' + domain
+ ).order_by('id')
+ if company_users.count() == 0:
+ return None
+
+ user = company_users[0]
+ user.is_staff = True
+ user.is_superuser = True
+ user.save()
+ return user
+
+def thread_get_answer_from_company(thread, domain):
+ answers = thread.posts.filter(
+ post_type='answer'
+ ).select_related(
+ 'author__email'
+ )
+ for answer in answers:
+ if answer.author.email.endswith('@' + domain):
+ return answer
+ return None
+
+def thread_find_first_comment_from_company(thread, domain):
+ comments = thread.posts.filter(
+ post_type='comment'
+ ).select_related(
+ 'author__email'
+ ).order_by('added_at')
+ for comment in comments:
+ if comment.author.email.endswith('@' + domain):
+ return comment
+ return None
+
+COMPANY_DOMAIN_HELP = """If used - first response from user with that domain
+then first response in each question from user with matching email address
+will be posted as answer and accepted as correct. Also, first user
+with a matching email address will be a site administrator."""
+
+JIVE_REDIRECTS_HELP = """This file will contain redirects from the old
+posts to new"""
+
class Command(BaseCommand):
args = '<jive-dump.xml>'
+ option_list = BaseCommand.option_list + (
+ make_option('--company-domain',
+ action='store',
+ type='str',
+ dest='company_domain',
+ default=None,
+ help=COMPANY_DOMAIN_HELP
+ ),
+ make_option('--redirects_file',
+ action='store',
+ type='str',
+ dest='redirects_file',
+ default='',
+ help=JIVE_REDIRECTS_HELP
+ )
+ )
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
#relax certain settings
askbot_settings.update('LIMIT_ONE_ANSWER_PER_USER', False)
askbot_settings.update('MAX_COMMENT_LENGTH', 1000000)
- #askbot_settings.update('MIN_REP_TO_LEAVE_COMMENTS', 1)
+ askbot_settings.update('MIN_REP_TO_INSERT_LINK', 1)
+ askbot_settings.update('MIN_REP_TO_SUGGEST_LINK', 1)
+ askbot_settings.update('COMMENTS_EDITOR_TYPE', 'rich-text')
+ askbot_settings.update('MARKUP_CODE_FRIENDLY', True)
self.bad_email_count = 0
+ self.attachments_path = ''
+ self.soup = None
+ self.jive_url = None
def handle(self, *args, **kwargs):
+ translation.activate(django_settings.LANGUAGE_CODE)
assert len(args) == 1, 'Dump file name is required'
- xml = open(args[0], 'r').read()
+ dump_file_name = args[0]
+ xml = open(dump_file_name, 'r').read()
soup = BeautifulSoup(xml, ['lxml', 'xml'])
+ self.soup = soup
+ url_prop = self.soup.find('Property', attrs={'name': 'jiveURL'})
+ self.jive_url= url_prop['value']
+
+ dump_dir = os.path.dirname(os.path.abspath(dump_file_name))
+ self.attachments_path = os.path.join(dump_dir, 'attachments')
+
+ self.import_users()
+ self.import_forums()
+ if kwargs['company_domain']:
+ self.promote_company_replies(kwargs['company_domain'])
+ self.fix_internal_links()
+ self.add_legacy_links()
+ if kwargs['redirects_file']:
+ self.make_redirects(kwargs['redirects_file'])
+ self.convert_jive_markup_to_html()
+ models.Message.objects.all().delete()
+
+ @transaction.commit_manually
+ def add_legacy_links(self):
+ questions = models.Post.objects.filter(post_type='question')
+ count = questions.count()
+ message = 'Adding links to old forum'
+ template = """\n\n{quote}This thread was imported from the previous forum.
+For your reference, the original is [available here|%s]{quote}"""
+ for question in ProgressBar(questions.iterator(), count, message):
+ thread_id = question.old_question_id
+ jive_url = self.jive_url
+ old_url = '%s/thread.jspa?threadID=%s' % (jive_url, thread_id)
+ question.text += template % old_url
+ question.save()
+ transaction.commit()
+ transaction.commit()
+
+ @transaction.commit_manually
+ def make_redirects(self):
+ """todo: implement this when needed"""
+ pass
+
- self.import_users(soup.find_all('User'))
- self.import_forums(soup.find_all('Forum'))
+ @transaction.commit_manually
+ def convert_jive_markup_to_html(self):
+ posts = models.Post.objects.all()
+ count = posts.count()
+ message = 'Converting jive markup to html'
+ for post in ProgressBar(posts.iterator(), count, message):
+ post.html = jive.convert(post.text)
+ post.summary = post.get_snippet()
+ post.save()
+ transaction.commit()
+ transaction.commit()
+
+ @transaction.commit_manually
+ def fix_internal_links(self):
+ jive_url = self.jive_url
+ print 'Base url of old forum: %s' % jive_url
+ posts = models.Post.objects.filter(text__contains=jive_url)
+ count = posts.count()
+ message = 'Fixing internal links'
+ for post in ProgressBar(posts.iterator(), count, message):
+ post.text = post.text.replace(jive_url, '')
+ fix_internal_links_in_post(post)
+ transaction.commit()
+ transaction.commit()
@transaction.commit_manually
- def import_users(self, user_soup):
+ def promote_company_replies(self, domain):
+ admin = turn_first_company_user_to_admin(domain)
+ if admin is None:
+ print "Note: did not find any users with email matching %s" % domain
+ return
+ message = 'Promoting company replies to accepted answers:'
+ threads = models.Thread.objects.all()
+ count = threads.count()
+ for thread in ProgressBar(threads.iterator(), count, message):
+ answer = thread_get_answer_from_company(thread, domain)
+
+ if answer == None:
+ comment = thread_find_first_comment_from_company(thread, domain)
+ if comment:
+ admin.repost_comment_as_answer(comment)
+ answer = comment
+
+ if answer:
+ admin.accept_best_answer(answer=answer, force=True)
+
+ transaction.commit()
+ transaction.commit()
+
+ @transaction.commit_manually
+ def import_users(self):
"""import users from jive to askbot"""
+ user_soup = self.soup.find_all('User')
+
message = 'Importing users:'
for user in ProgressBar(iter(user_soup), len(user_soup), message):
username = user.find('Username').text
@@ -52,13 +279,16 @@ class Command(BaseCommand):
real_name=real_name,
date_joined=joined_timestamp
)
+ user.set_unusable_password()
user.save()
transaction.commit()
- def import_forums(self, forum_soup):
+ def import_forums(self):
"""import forums by associating each with a special tag,
and then importing all threads for the tag"""
admin = models.User.objects.get(id=1)
+ forum_soup = self.soup.find_all('Forum')
+ print 'Have %d forums' % len(forum_soup)
for forum in forum_soup:
threads_soup = forum.find_all('Thread')
self.import_threads(threads_soup, forum.find('Name').text)
@@ -70,10 +300,35 @@ class Command(BaseCommand):
self.import_thread(thread, tag_name)
transaction.commit()
+ def add_attachments_to_post(self, post, attachments):
+ if len(attachments) == 0:
+ return
+
+ post.text += '\nh4. Attachments\n'
+ for att in attachments:
+ att_id, name, mimetype = att
+ if mimetype not in FILE_TYPES:
+ continue
+ ext = '.' + FILE_TYPES[mimetype]
+ file_name = make_file_name(ext)
+ # copy attachment file to a new place
+ source_file = os.path.join(self.attachments_path, att_id + '.bin')
+ dest_file = os.path.join(django_settings.MEDIA_ROOT, file_name)
+ shutil.copyfile(source_file, dest_file)
+ # add link to file to the post text
+ post.text += '# [%s|%s%s]\n' % (name, django_settings.MEDIA_URL, file_name)
+
def import_thread(self, thread, tag_name):
"""import individual thread"""
question_soup = thread.find('Message')
- title, body, timestamp, user = self.parse_post(question_soup)
+ post_id, title, body, attachments, timestamp, user = \
+ self.parse_post(question_soup)
+
+ if models.Post.objects.filter(old_question_id=thread['id']).count() == 1:
+ #this allows restarting the process of importing forums
+ #any time
+ return
+
#post question
question = user.post_question(
title=title,
@@ -82,33 +337,59 @@ class Command(BaseCommand):
tags=tag_name,
language=django_settings.LANGUAGE_CODE
)
+ self.add_attachments_to_post(question, attachments)
+ question.html = jive.convert(question.text)
+ question.old_question_id = int(thread['id'])
+ question.old_answer_id = post_id
+ question.summary = question.get_snippet()
+ question.save()
#post answers
- if not question_soup.messagelist:
+ message_list = question_soup.find_all('MessageList', recursive=False)
+ if len(message_list) == 0:
return
- for answer_soup in question_soup.messagelist.find_all('Message', recursive=False):
- title, body, timestamp, user = self.parse_post(answer_soup)
+ for answer_soup in message_list[0].find_all('Message', recursive=False):
+ post_id, title, body, attachments, timestamp, user = \
+ self.parse_post(answer_soup)
answer = user.post_answer(
question=question,
body_text=body,
- timestamp=timestamp,
- language=django_settings.LANGUAGE_CODE
+ timestamp=timestamp
)
+ self.add_attachments_to_post(answer, attachments)
+ answer.html = jive.convert(answer.text)
+ answer.summary = answer.get_snippet()
+ answer.old_answer_id = post_id
+ answer.save()
comments = answer_soup.find_all('Message')
for comment in comments:
- title, body, timestamp, user = self.parse_post(comment)
- user.post_comment(
+ post_id, title, body, attachments, timestamp, user = \
+ self.parse_post(comment)
+ comment = user.post_comment(
parent_post=answer,
body_text=body,
- timestamp=timestamp,
- language=django_settings.LANGUAGE_CODE
+ timestamp=timestamp
)
+ comment.old_answer_id = post_id
+ self.add_attachments_to_post(comment, attachments)
+ comment.html = jive.convert(comment.text)
+ comment.summary = comment.get_snippet()
+ comment.save()
+
def parse_post(self, post):
title = post.find('Subject').text
added_at = parse_date(post.find('CreationDate').text)
username = post.find('Username').text
body = post.find('Body').text
+ attachments_soup = post.find_all('Attachment')
+ attachments = list()
+ for att in attachments_soup:
+ att_id = att['id']
+ name = att.find('Name').text
+ content_type = att['contentType']
+ attachments.append((att_id, name, content_type))
+
try:
user = models.User.objects.get(username=username)
except models.User.DoesNotExist:
@@ -116,4 +397,4 @@ class Command(BaseCommand):
self.bad_email_count += 1
user = models.User(username=username, email=email)
user.save()
- return title, body, added_at, user
+ return int(post['id']), title, body, attachments, added_at, user
diff --git a/askbot/management/commands/base.py b/askbot/management/commands/base.py
new file mode 100644
index 00000000..dce02e20
--- /dev/null
+++ b/askbot/management/commands/base.py
@@ -0,0 +1,240 @@
+from askbot.models import Message
+from askbot.models import User
+from askbot.models import ImportedObjectInfo
+from askbot.models import ImportRun
+from django.core.management.base import BaseCommand, CommandError
+from django.conf import settings as django_settings
+from bs4 import BeautifulSoup
+from collections import defaultdict
+from django.core import serializers
+from django.utils.encoding import smart_str
+from django.utils.translation import activate as activate_language
+from optparse import make_option
+import os
+import sys
+from tempfile import mkstemp
+
+class BaseImportXMLCommand(BaseCommand):
+ help = 'Base command for adding XML data from other forums to Askbot'
+
+ option_list = BaseCommand.option_list + (
+ make_option('--redirect-format',
+ action = 'store',
+ dest = 'redirect_format',
+ default = 'none',
+ help = 'Format for the redirect files (apache|nginx|none)'
+ ),
+ )
+
+ def handle(self, *args, **kwargs):
+
+ activate_language(django_settings.LANGUAGE_CODE)
+
+ #init the redirects file format table
+ self.redirect_format = self.get_redirect_format(kwargs['redirect_format'])
+
+ self.setup_run()
+ self.read_xml_file(args[0])
+
+ self.remember_message_ids()
+ self.handle_import()
+ self.delete_new_messages()
+
+ def handle_import(self):
+ """this method should contain the actual work of importing data
+
+ If necessary, create redirect files using methods
+ redirects_file = self.open_unique_file('user_redirects')
+ self.write_redirect(old_url, new_url, redirects_file)
+ redirects_file.close()
+ where old_url and new_url are urls of the corresponding objects
+ before and after importation
+ """
+ raise NotImplementedError('Implement this method to import data')
+
+
+ def get_redirect_format(self, format_setting):
+ format_table = {
+ 'nginx': 'rewrite ^%s$ %s break;\n',
+ 'apache': 'Redirect permanent %s %s\n',
+ }
+ format_table = defaultdict(lambda: '%s %s\n', format_table)
+ return format_table[format_setting]
+
+ def setup_run(self):
+ """remembers the run information,
+ for the logging purposes
+ """
+ command = ' '.join(sys.argv)
+ run = ImportRun.objects.create(command=command)
+ self.run = run
+
+ def read_xml_file(self, filename):
+ """reads xml data int BeautifulSoup instance"""
+ if not os.path.isfile(filename):
+ raise CommandError('File %s does not exist') % filename
+ xml = open(filename, 'r').read()
+ self.soup = BeautifulSoup(xml, ['lxml', 'xml'])
+
+ def remember_message_ids(self):
+ """remembers messages ids of existing messages - we use these
+ to delete any messages added automatically during the import"""
+ self.message_ids = list(Message.objects.values_list('id', flat=True))
+
+ def log_action_with_old_id(self, from_object_id, to_object, extra_info=None):
+ info = ImportedObjectInfo()
+ info.old_id = from_object_id
+ info.new_id = to_object.id
+ info.model = str(to_object._meta)
+ info.run = self.run
+ info.extra_info = extra_info or dict()
+ info.save()
+
+ def log_action(self, from_object, to_object, extra_info=None):
+ self.log_action_with_old_id(from_object.id, to_object, extra_info=extra_info)
+
+ def get_imported_object_id_by_old_id(self, model_class, old_id):
+ """Returts id of imported object by old id"""
+ if old_id is None:
+ return None
+ try:
+ log = ImportedObjectInfo.objects.get(
+ model=str(model_class._meta),
+ old_id=old_id,
+ run=self.run
+ )
+ return log.new_id
+ except ImportedObjectInfo.DoesNotExist:
+ return None
+
+ def get_imported_object_by_old_id(self, model_class, old_id):
+ """Returns new imported object by id of corresponding old object"""
+ new_id = self.get_imported_object_id_by_old_id(model_class, old_id)
+ if new_id:
+ return model_class.objects.get(id=new_id)
+ return None
+
+ def get_objects_for_model(self, model_name):
+ """returns iterator of objects from the django
+ xml dump by name"""
+ object_soup = self.soup.find_all('object', {'model': model_name})
+ for datum in object_soup:
+ yield self.get_deserialized_object(datum)
+
+ def delete_new_messages(self):
+ """deletes any messages that were added by askbot during the import process"""
+ Message.objects.exclude(id__in=self.message_ids).delete()
+
+ def open_unique_file(self, name_hint):
+ """return a file using name_hint as the hint
+ for the file name, if file with that name exists,
+ create a unique file name containing hint as part of
+ the name"""
+ if os.path.exists(name_hint):
+ info = mkstemp(dir=os.getcwd(), prefix=name_hint + '_')
+ name_hint = info[1]
+ print 'saving file: %s' % name_hint
+ return open(name_hint, 'w')
+
+ def write_redirect(self, from_url, to_url, redirects_file):
+ """writes redirect clause to a file in format
+ chosen earlier in the `handle` function"""
+ if from_url != to_url:
+ redirects_file.write(self.redirect_format % (from_url, to_url))
+
+ def get_safe_username(self, username):
+ """get unique username similar to `username`
+ to avoid the uniqueness clash"""
+ existing_names = User.objects.filter(
+ username__istartswith=username
+ ).values_list('username', flat=True)
+
+ if len(existing_names) == 0:
+ return username
+
+ num = 1
+ while True:
+ new_name = username + str(num)
+ if new_name in existing_names:
+ num += 1
+ else:
+ return new_name
+
+ def get_deserialized_object(self, xml_soup):
+ """returns deserialized django object for xml soup with one item"""
+ item_xml = smart_str(xml_soup)
+ #below call assumes a single item within
+ obj = serializers.deserialize('xml', item_xml).next().object
+ obj._source_xml = item_xml
+ return obj
+
+ def get_m2m_ids_for_field(self, obj, field_name):
+ xml = obj._source_xml
+ soup = BeautifulSoup(xml)
+ ids = list()
+ for field in soup.findAll('field', attrs={'name': field_name}):
+ objs = field.findAll('object')
+ for obj in objs:
+ ids.append(obj.attrs['pk'])
+ return ids
+
+ def copy_string_parameter(self, from_obj, to_obj, from_param_name, to_param_name=None):
+ """copy value of string parameter from old to new object"""
+
+ to_param_name = to_param_name or from_param_name
+
+ from_par = getattr(from_obj, from_param_name)
+ to_par = getattr(to_obj, to_param_name)
+ if from_par is None and to_par is None:
+ return
+ from_par = from_par or ''
+ to_par = to_par or ''
+ if from_par.strip() == '' and to_par.strip() != '':
+ setattr(to_obj, to_param_name, from_par)
+
+ def copy_bool_parameter(self, from_obj, to_obj, from_param_name, to_param_name=None, operator='or'):
+ """copy value of boolean parameter from old to new object"""
+
+ to_param_name = to_param_name or from_param_name
+
+ from_par = getattr(from_obj, from_param_name)
+ to_par = getattr(to_obj, to_param_name)
+ if operator == 'or':
+ value = from_par or to_par
+ elif operator == 'and':
+ value = from_par and to_par
+ else:
+ raise ValueError('unsupported operator "%s"' % operator)
+ setattr(to_obj, to_param_name, value)
+
+ def merge_words_parameter(self, from_obj, to_obj, from_param_name, to_param_name=None):
+ """merge unique words from the two objects and assign to the new object"""
+
+ to_param_name = to_param_name or from_param_name
+
+ from_words = getattr(from_obj, from_param_name).split()
+ to_words = getattr(to_obj, to_param_name).split()
+ value = ' '.join(set(from_words)|set(to_words))
+ setattr(to_obj, to_param_name, value)
+
+ def copy_numeric_parameter(self, from_obj, to_obj, from_param_name, to_param_name=None, operator='max'):
+
+ to_param_name = to_param_name or from_param_name
+
+ from_par = getattr(from_obj, from_param_name)
+ to_par = getattr(to_obj, to_param_name)
+
+ if from_par is None:
+ return to_par
+ elif to_par is None:
+ return from_par
+
+ if operator == 'max':
+ value = max(from_par, to_par)
+ elif operator == 'min':
+ value = min(from_par, to_par)
+ elif operator == 'sum':
+ value = from_par + to_par
+ else:
+ raise ValueError('unsupported operator "%s"' % operator)
+ setattr(to_obj, to_param_name, value)
diff --git a/askbot/management/commands/create_tag_synonyms.py b/askbot/management/commands/create_tag_synonyms.py
index e4324639..167beb9a 100644
--- a/askbot/management/commands/create_tag_synonyms.py
+++ b/askbot/management/commands/create_tag_synonyms.py
@@ -4,6 +4,7 @@ all corresponding questions are retagged
import sys
from optparse import make_option
+from django.conf import settings as django_settings
from django.core import management
from django.core.management.base import BaseCommand, CommandError
from askbot import models
@@ -47,6 +48,13 @@ remove source_tag"""
default = None,
help = 'id of the user who will be marked as a performer of this operation'
),
+ make_option('--lang',
+ action='store',
+ type='str',
+ dest='lang',
+ default=django_settings.LANGUAGE_CODE,
+ help='language code of the tag, e.g. "en"'
+ )
)
def handle(self, *args, **options):
@@ -75,7 +83,7 @@ remove source_tag"""
is_source_tag_created = False
try:
- source_tag = models.Tag.objects.get(name=source_tag_name)
+ source_tag = models.Tag.objects.get(name=source_tag_name, language_code=options['lang'])
except models.Tag.DoesNotExist:
if not options.get('is_force', False):
prompt = """source tag %s doesn't exist, are you sure you want to create a TagSynonym
@@ -84,16 +92,21 @@ remove source_tag"""
if choice == 'no':
print 'Cancled'
sys.exit()
- source_tag = models.Tag.objects.create(name=source_tag_name,
- created_by=admin
- )
+ source_tag = models.Tag.objects.create(
+ name=source_tag_name,
+ created_by=admin,
+ language_code=options['lang']
+ )
is_source_tag_created = True
# test if target_tag is actually synonym for yet another tag
# when user asked tag2->tag3, we already had tag3->tag4.
try:
- tag_synonym_tmp = models.TagSynonym.objects.get(source_tag_name = target_tag_name)
+ tag_synonym_tmp = models.TagSynonym.objects.get(
+ source_tag_name=target_tag_name,
+ language_code=options['lang']
+ )
if not options.get('is_force', False):
prompt = """There exists a TagSynonym %s ==> %s,
hence we will create a tag synonym %s ==> %s instead. Proceed?""" % (tag_synonym_tmp.source_tag_name, tag_synonym_tmp.target_tag_name,
@@ -108,27 +121,34 @@ remove source_tag"""
pass
try:
- models.Tag.objects.get(name=target_tag_name)
+ models.Tag.objects.get(name=target_tag_name, language_code=options['lang'])
except models.Tag.DoesNotExist:
# we are creating a target tag, let's copy source tag's info
# used_count are updated later
- models.Tag.objects.create(name=target_tag_name,
- created_by = admin,
- status = source_tag.status,
- tag_wiki = source_tag.tag_wiki
- )
-
- tag_synonym_tmp, created = models.TagSynonym.objects.get_or_create(source_tag_name = source_tag_name,
- target_tag_name = target_tag_name,
- owned_by = admin
- )
+ models.Tag.objects.create(
+ name=target_tag_name,
+ created_by = admin,
+ status = source_tag.status,
+ tag_wiki = source_tag.tag_wiki,
+ language_code=options['lang']
+ )
+
+ tag_synonym_tmp, created = models.TagSynonym.objects.get_or_create(
+ source_tag_name=source_tag_name,
+ target_tag_name=target_tag_name,
+ owned_by=admin,
+ language_code=options['lang']
+ )
management.call_command('rename_tags', *args, **options)
# When source_tag_name is a target_tag_name of already existing TagSynonym.
# ie. if tag1->tag2 exists when user asked tag2->tag3
# we are going to convert all tag1->tag2 to tag1->tag3 as well
- existing_tag_synonyms = models.TagSynonym.objects.filter(target_tag_name=source_tag_name)
+ existing_tag_synonyms = models.TagSynonym.objects.filter(
+ target_tag_name=source_tag_name,
+ language_code=options['lang']
+ )
for existing_tag_synonym in existing_tag_synonyms:
new_options = options.copy()
new_options['from'] = existing_tag_synonym.source_tag_name
diff --git a/askbot/management/commands/create_thousand_tags.py b/askbot/management/commands/create_thousand_tags.py
index 05c77f46..45953e74 100644
--- a/askbot/management/commands/create_thousand_tags.py
+++ b/askbot/management/commands/create_thousand_tags.py
@@ -1,4 +1,5 @@
from django.core.management.base import NoArgsCommand
+from django.conf import settings as django_settings
from django.db import transaction
from askbot import models
import sys
@@ -10,9 +11,10 @@ class Command(NoArgsCommand):
for i in xrange(1000):
name = 'tag' + str(i)
models.Tag.objects.create(
- name = name,
- created_by = user
- )
+ name=name,
+ created_by=user,
+ language_code=django_settings.LANGUAGE_CODE
+ )
if i % 1000 == 0:
transaction.commit()
transaction.commit()
diff --git a/askbot/management/commands/createsuperuser.py b/askbot/management/commands/createsuperuser.py
index eb363bbd..d0faf253 100644
--- a/askbot/management/commands/createsuperuser.py
+++ b/askbot/management/commands/createsuperuser.py
@@ -100,8 +100,7 @@ class Command(Command):
self.remove_signals()
u = User.objects.create_superuser(username, email, password)
- u.set_admin_status()
- u.save()
+ u.set_status('d')
if verbosity >= 1:
self.stdout.write("Askbot Superuser created successfully.\n")
diff --git a/askbot/management/commands/dump_forum.py b/askbot/management/commands/dump_forum.py
deleted file mode 100644
index 0bead908..00000000
--- a/askbot/management/commands/dump_forum.py
+++ /dev/null
@@ -1,36 +0,0 @@
-import sys
-import optparse
-from django.core import management
-from django.core.management.base import BaseCommand
-from askbot.utils import console
-
-class Command(BaseCommand):
- help = """Dumps askbot forum data into the file for the later use with "load_forum".
-The extension ".json" will be added automatically."""
-
- option_list = BaseCommand.option_list + (
- optparse.make_option('--dump-name',
- type = 'str',
- dest = 'dump_file'
- ),
- )
- def handle(self, *args, **options):
- dump_file = console.open_new_file(
- 'Please enter file name (no extension): ',
- hint = options.get('dump_file', None),
- extension = '.json'
- )
- print ("Saving file %s ..." % dump_file.name).encode('utf-8')
- stdout_orig = sys.stdout
- try:
- sys.stdout = dump_file
- management.call_command(
- 'dumpdata',
- #exclude = ('contenttypes',),
- indent = 4
- )
- sys.stdout = stdout_orig
- print "Done."
- except KeyboardInterrupt:
- sys.stdout = stdout_orig
- print "\nCanceled."
diff --git a/askbot/management/commands/export_osqa.py b/askbot/management/commands/export_osqa.py
new file mode 100644
index 00000000..5dd6eb5d
--- /dev/null
+++ b/askbot/management/commands/export_osqa.py
@@ -0,0 +1,215 @@
+from django.core.exceptions import ImproperlyConfigured
+from django.core.management.base import BaseCommand, CommandError
+from django.core.serializers.xml_serializer import Serializer
+from django.db import connections, router, DEFAULT_DB_ALIAS
+from django.utils.datastructures import SortedDict
+from StringIO import StringIO
+
+from optparse import make_option
+
+class XMLExportSerializer(Serializer):
+ def serialize(self, queryset, **options):
+ """
+ Serialize a queryset.
+ Copy-paste from the base serializer with a minor difference
+ commented below
+ """
+ self.options = options
+
+ self.stream = options.pop("stream", StringIO())
+ self.selected_fields = options.pop("fields", None)
+ self.use_natural_keys = options.pop("use_natural_keys", False)
+
+ self.start_serialization()
+ for obj in queryset:
+ self.start_object(obj)
+ #the line below is the only one that was changed
+ for field in obj._meta.fields:
+ if field.serialize:
+ if field.rel is None:
+ if self.selected_fields is None or field.attname in self.selected_fields:
+ self.handle_field(obj, field)
+ else:
+ if self.selected_fields is None or field.attname[:-3] in self.selected_fields:
+ self.handle_fk_field(obj, field)
+ for field in obj._meta.many_to_many:
+ if field.serialize:
+ if self.selected_fields is None or field.attname in self.selected_fields:
+ self.handle_m2m_field(obj, field)
+ self.end_object(obj)
+ self.end_serialization()
+ return self.getvalue()
+
+
+class Command(BaseCommand):
+ option_list = BaseCommand.option_list + (
+ make_option('--indent', default=4, dest='indent', type='int',
+ help='Specifies the indent level to use when pretty-printing output'),
+ make_option('--database', action='store', dest='database',
+ default=DEFAULT_DB_ALIAS, help='Nominates a specific database to load '
+ 'fixtures into. Defaults to the "default" database.'),
+ make_option('-e', '--exclude', dest='exclude',action='append', default=['sessions', 'contenttypes'],
+ help='An appname or appname.ModelName to exclude (use multiple --exclude to exclude multiple apps/models).'),
+ make_option('-n', '--natural', action='store_true', dest='use_natural_keys', default=False,
+ help='Use natural keys if they are available.'),
+ make_option('-a', '--all', action='store_true', dest='use_base_manager', default=False,
+ help="Use Django's base manager to dump all models stored in the database, including those that would otherwise be filtered or modified by a custom manager."),
+ )
+ help = ("Output the contents of the OSQA database as XML fixture of the given "
+ "format (using each model's default manager unless --all is "
+ "specified).")
+ args = '[appname appname.ModelName ...]'
+
+ def handle(self, *app_labels, **options):
+ from django.db.models import get_app, get_apps, get_models, get_model
+
+ indent = options.get('indent', None)
+ using = options.get('database', DEFAULT_DB_ALIAS)
+ connection = connections[using]
+ excludes = options.get('exclude',[])
+ show_traceback = options.get('traceback', False)
+ use_natural_keys = options.get('use_natural_keys', False)
+ use_base_manager = options.get('use_base_manager', False)
+
+ excluded_apps = set()
+ excluded_models = set()
+ for exclude in excludes:
+ if '.' in exclude:
+ app_label, model_name = exclude.split('.', 1)
+ model_obj = get_model(app_label, model_name)
+ if not model_obj:
+ raise CommandError('Unknown model in excludes: %s' % exclude)
+ excluded_models.add(model_obj)
+ else:
+ try:
+ app_obj = get_app(exclude)
+ excluded_apps.add(app_obj)
+ except ImproperlyConfigured:
+ raise CommandError('Unknown app in excludes: %s' % exclude)
+
+ if len(app_labels) == 0:
+ app_list = SortedDict((app, None) for app in get_apps() if app not in excluded_apps)
+ else:
+ app_list = SortedDict()
+ for label in app_labels:
+ try:
+ app_label, model_label = label.split('.')
+ try:
+ app = get_app(app_label)
+ except ImproperlyConfigured:
+ raise CommandError("Unknown application: %s" % app_label)
+ if app in excluded_apps:
+ continue
+ model = get_model(app_label, model_label)
+ if model is None:
+ raise CommandError("Unknown model: %s.%s" % (app_label, model_label))
+
+ if app in app_list.keys():
+ if app_list[app] and model not in app_list[app]:
+ app_list[app].append(model)
+ else:
+ app_list[app] = [model]
+ except ValueError:
+ # This is just an app - no model qualifier
+ app_label = label
+ try:
+ app = get_app(app_label)
+ except ImproperlyConfigured:
+ raise CommandError("Unknown application: %s" % app_label)
+ if app in excluded_apps:
+ continue
+ app_list[app] = None
+
+ # Now collate the objects to be serialized.
+ objects = []
+ for model in sort_dependencies(app_list.items()):
+ if model in excluded_models:
+ continue
+ if not model._meta.proxy and router.allow_syncdb(using, model):
+ if use_base_manager:
+ objects.extend(model._base_manager.using(using).all())
+ else:
+ objects.extend(model._default_manager.using(using).all())
+
+ try:
+ serializer = XMLExportSerializer()
+ return serializer.serialize(objects, indent=indent, use_natural_keys=use_natural_keys)
+ except Exception, e:
+ if show_traceback:
+ raise
+ raise CommandError("Unable to serialize database: %s" % e)
+
+def sort_dependencies(app_list):
+ """Sort a list of app,modellist pairs into a single list of models.
+
+ The single list of models is sorted so that any model with a natural key
+ is serialized before a normal model, and any model with a natural key
+ dependency has it's dependencies serialized first.
+ """
+ from django.db.models import get_model, get_models
+ # Process the list of models, and get the list of dependencies
+ model_dependencies = []
+ models = set()
+ for app, model_list in app_list:
+ if model_list is None:
+ model_list = get_models(app)
+
+ for model in model_list:
+ models.add(model)
+ # Add any explicitly defined dependencies
+ if hasattr(model, 'natural_key'):
+ deps = getattr(model.natural_key, 'dependencies', [])
+ if deps:
+ deps = [get_model(*d.split('.')) for d in deps]
+ else:
+ deps = []
+
+ # Now add a dependency for any FK or M2M relation with
+ # a model that defines a natural key
+ for field in model._meta.fields:
+ if hasattr(field.rel, 'to'):
+ rel_model = field.rel.to
+ if hasattr(rel_model, 'natural_key'):
+ deps.append(rel_model)
+ for field in model._meta.many_to_many:
+ rel_model = field.rel.to
+ if hasattr(rel_model, 'natural_key'):
+ deps.append(rel_model)
+ model_dependencies.append((model, deps))
+
+ model_dependencies.reverse()
+ # Now sort the models to ensure that dependencies are met. This
+ # is done by repeatedly iterating over the input list of models.
+ # If all the dependencies of a given model are in the final list,
+ # that model is promoted to the end of the final list. This process
+ # continues until the input list is empty, or we do a full iteration
+ # over the input models without promoting a model to the final list.
+ # If we do a full iteration without a promotion, that means there are
+ # circular dependencies in the list.
+ model_list = []
+ while model_dependencies:
+ skipped = []
+ changed = False
+ while model_dependencies:
+ model, deps = model_dependencies.pop()
+
+ # If all of the models in the dependency list are either already
+ # on the final model list, or not on the original serialization list,
+ # then we've found another model with all it's dependencies satisfied.
+ found = True
+ for candidate in ((d not in models or d in model_list) for d in deps):
+ if not candidate:
+ found = False
+ if found:
+ model_list.append(model)
+ changed = True
+ else:
+ skipped.append((model, deps))
+ if not changed:
+ raise CommandError("Can't resolve dependencies for %s in serialized app list." %
+ ', '.join('%s.%s' % (model._meta.app_label, model._meta.object_name)
+ for model, deps in sorted(skipped, key=lambda obj: obj[0].__name__))
+ )
+ model_dependencies = skipped
+
+ return model_list
diff --git a/askbot/management/commands/find_bodyless_questions.py b/askbot/management/commands/find_bodyless_questions.py
index 75312620..b137ba54 100644
--- a/askbot/management/commands/find_bodyless_questions.py
+++ b/askbot/management/commands/find_bodyless_questions.py
@@ -2,19 +2,28 @@
that do not have revisions by creating a fake initial revision
based on the content stored in the post itself
"""
-from django.core.management.base import NoArgsCommand
+from django.core.management.base import BaseCommand
from askbot import models
from askbot import const
from askbot.utils.console import ProgressBar
+from optparse import make_option
def print_results(items):
template = 'id=%d, title=%s'
for thread in items:
print template % (thread.id, thread.title.encode('utf8'))
-class Command(NoArgsCommand):
+class Command(BaseCommand):
"""Command class for "fix_bodyless_questions"
"""
+ option_list = BaseCommand.option_list + (
+ make_option('--delete',
+ action='store_true',
+ dest='delete',
+ default=False,
+ help='Delete poll instead of closing it',
+ ),
+ )
def handle(self, *arguments, **options):
"""function that handles the command job
"""
@@ -39,6 +48,12 @@ class Command(NoArgsCommand):
if len(bodyless):
print '\nQuestions without body text:'
print_results(bodyless)
+ if options['delete']:
+ for thread in bodyless:
+ thread.delete()
if len(multi_body):
print '\nQuestions with >1 instances of body text'
print_results(multi_body)
+ if options['delete']:
+ for thread in multi_body:
+ thread.delete()
diff --git a/askbot/management/commands/fix_comment_counts.py b/askbot/management/commands/fix_comment_counts.py
new file mode 100644
index 00000000..3a7f16f8
--- /dev/null
+++ b/askbot/management/commands/fix_comment_counts.py
@@ -0,0 +1,34 @@
+"""Management command for fixing comment counts in questions and answers
+
+Bug in converting answer to comment stored wrong comment count in target
+question or answer, and in some cases that makes it imposible for users to view
+all the comments.
+"""
+
+from django.core.management.base import NoArgsCommand
+from django.db.models import signals, Count, F
+from askbot.models import Post
+from askbot.utils.console import ProgressBar
+
+class Command(NoArgsCommand):
+
+ help = "Fixes the wrong comment counts on questions and answers, "\
+ "where answers have been converted to comments.\n"
+
+ def remove_save_signals(self):
+ """Prevent possible unvanted side effects of saving
+ """
+ signals.pre_save.receivers = []
+ signals.post_save.receivers = []
+
+ def handle(self, *arguments, **options):
+ """Function that handles the command job
+ """
+ self.remove_save_signals()
+ posts = Post.objects.annotate(real_comment_count=Count('comments')
+ ).exclude(real_comment_count=F('comment_count'))
+ count = posts.count()
+ message = 'Fixing comment counts'
+ for post in ProgressBar(posts.iterator(), count, message):
+ post.comment_count = post.comments.count();
+ post.save()
diff --git a/askbot/management/commands/fix_question_tags.py b/askbot/management/commands/fix_question_tags.py
index ed1ee6fb..48168ee8 100644
--- a/askbot/management/commands/fix_question_tags.py
+++ b/askbot/management/commands/fix_question_tags.py
@@ -1,70 +1,159 @@
+import re
import sys
from django.core.management.base import NoArgsCommand
+from django.conf import settings as django_settings
from django.db import transaction
+from django.utils import translation
+from askbot import const
from askbot import models
from askbot import forms
from askbot.utils import console
+from askbot.utils.slug import slugify_camelcase
from askbot.models import signals
from askbot.conf import settings as askbot_settings
+from askbot.management.commands.rename_tags import get_admin
-class Command(NoArgsCommand):
- def handle_noargs(self, **options):
+def get_valid_tag_name(tag):
+ """Returns valid version of the tag name.
+ If necessary, lowercases the tag.
+ Strips the forbidden first characters in the tag.
+ """
+ name = tag.name
+ if askbot_settings.FORCE_LOWERCASE_TAGS:
+ #name = slugify_camelcase(name)
+ name = name.lower()
+ #if tag name starts with forbidden character, chop off that character
+ #until no more forbidden chars are at the beginning
+ first_char_regex = re.compile('^%s+' % const.TAG_FORBIDDEN_FIRST_CHARS)
+ return first_char_regex.sub('', name)
+
+class Command(Command):
+ def handle_noargs(self, *args, **options):
signal_data = signals.pop_all_db_signal_receivers()
- self.run_command()
+ languages = models.Tag.objects.values_list('language_code').distinct()
+ for lang in languages:
+ self.run_command(lang)
signals.set_all_db_signal_receivers(signal_data)
+ def retag_threads(self, from_tags, to_tag):
+ """finds threads matching the `from_tags`
+ removes the `from_tags` from them and applies the
+ to_tags"""
+ threads = models.Thread.objects.filter(tags__in=from_tags)
+ from_tag_names = [tag.name for tag in from_tags]
+ for thread in threads:
+ tagnames = set(thread.get_tag_names())
+ tagnames.difference_update(from_tag_names)
+ tagnames.add(to_tag.name)
+ self.admin.retag_question(
+ question=thread._question_post(),
+ tags=' '.join(tagnames)
+ )
+
+
@transaction.commit_manually
- def run_command(self):
+ def run_command(self, lang):
"""method that runs the actual command"""
#go through tags and find character case duplicates and eliminate them
- tagnames = models.Tag.objects.values_list('name', flat = True)
+ translation.activate(lang)
+ tagnames = models.Tag.objects.filter(
+ language_code=lang
+ ).values_list('name', flat=True)
+ self.admin = get_admin()
+
+ #1) first we go through all tags and
+ #either fix or delete illegal tags
+ found_count = 0
+
for name in tagnames:
- dupes = models.Tag.objects.filter(name__iexact = name)
- first_tag = dupes[0]
- if dupes.count() > 1:
- line = 'Found duplicate tags for %s: ' % first_tag.name
- print line,
- for idx in xrange(1, dupes.count()):
- print dupes[idx].name + ' ',
- dupes[idx].delete()
- print ''
- if askbot_settings.FORCE_LOWERCASE_TAGS:
- lowercased_name = first_tag.name.lower()
- if first_tag.name != lowercased_name:
- print 'Converting tag %s to lower case' % first_tag.name
- first_tag.name = lowercased_name
- first_tag.save()
+ try:
+ tag = models.Tag.objects.get(
+ name=name,
+ language_code=lang
+ )
+ except models.Tag.DoesNotExist:
+ #tag with this name was already deleted,
+ #because it was an invalid duplicate version
+ #of other valid tag
+ continue
+
+
+ fixed_name = get_valid_tag_name(tag)
+
+ #if fixed name is empty after cleaning, delete the tag
+ if fixed_name == '':
+ print 'Deleting invalid tag: %s' % name
+ tag.delete()
+ found_count += 1
+ continue
+
+ if fixed_name != name:
+ print 'Renaming tag: %s -> %s' % (name, fixed_name)
+
+ #if tag name changed, see if there is a duplicate
+ #with the same name, in which case we re-assign questions
+ #with the current tag to that other duplicate
+ #then delete the current tag as no longer used
+ if fixed_name != name:
+ try:
+ duplicate_tag = models.Tag.objects.get(
+ name=fixed_name,
+ language_code=lang
+ )
+ except models.Tag.DoesNotExist:
+ pass
+ self.retag_threads([tag], duplicate_tag)
+ tag.delete()
+ found_count += 1
+ continue
+
+
+ #if there are case variant dupes, we assign questions
+ #from the case variants to the current tag and
+ #delete the case variant tags
+ dupes = models.Tag.objects.filter(
+ name__iexact=fixed_name,
+ language_code=lang
+ ).exclude(pk=tag.id)
+
+ dupes_count = dupes.count()
+ if dupes_count:
+ self.retag_threads(dupes, tag)
+ dupes.delete()
+ found_count += dupes_count
+
+ if tag.name != fixed_name:
+ tag.name = fixed_name
+ tag.save()
+
transaction.commit()
- #go through questions and fix tag records on each
+ #2) go through questions and fix tag records on each
+ # and recalculate all the denormalised tag names on threads
threads = models.Thread.objects.all()
checked_count = 0
- found_count = 0
total_count = threads.count()
- print "Searching for questions with inconsistent tag records:",
+ print "Searching for questions with inconsistent copies of tag records:",
for thread in threads:
+ #make sure that denormalized tag set is the same as normalized
+ #we just add both the tags together and try to apply them
+ #to the question
tags = thread.tags.all()
denorm_tag_set = set(thread.get_tag_names())
norm_tag_set = set(thread.tags.values_list('name', flat=True))
- if norm_tag_set != denorm_tag_set:
-
- if thread.last_edited_by:
- user = thread.last_edited_by
- timestamp = thread.last_edited_at
- else:
- user = thread.author
- timestamp = thread.added_at
-
- tagnames = forms.TagNamesField().clean(thread.tagnames)
- thread.update_tags(
- tagnames = tagnames,
- user = user,
- timestamp = timestamp
+ if norm_tag_set != denorm_tag_set:
+ denorm_tag_set.update(norm_tag_set)
+ cleaned_tag_set = set(
+ models.Tag.objects.filter(
+ name__in=denorm_tag_set,
+ language_code=lang
+ ).values_list('name', flat=True)
+ )
+ self.admin.retag_question(
+ question=thread._question_post(),
+ tags=' '.join(cleaned_tag_set)
)
- thread.tagnames = tagnames
- thread.save()
- found_count += 1
transaction.commit()
checked_count += 1
diff --git a/askbot/management/commands/generate_post_snippets.py b/askbot/management/commands/generate_post_snippets.py
new file mode 100644
index 00000000..0f45c3da
--- /dev/null
+++ b/askbot/management/commands/generate_post_snippets.py
@@ -0,0 +1,21 @@
+from django.core.management.base import NoArgsCommand
+from django.db import transaction
+from askbot.models import Post
+from askbot.utils.console import ProgressBar
+
+class Command(NoArgsCommand):
+ help = 'Generates snippets for all posts'
+ @transaction.commit_manually
+ def handle_noargs(self, *args, **kwargs):
+ posts = Post.objects.all()
+ count = posts.count()
+ message = 'Building post snippets'
+ for post in ProgressBar(posts.iterator(), count, message):
+ if hasattr(post, 'summary'):
+ post.summary = post.get_snippet()
+ post.html = post.parse_post_text()['html']
+ post.save()
+ transaction.commit()
+ if post.thread:
+ post.thread.invalidate_cached_data(lazy=True)
+ transaction.commit()
diff --git a/askbot/management/commands/load_forum.py b/askbot/management/commands/load_forum.py
deleted file mode 100644
index 65ac410f..00000000
--- a/askbot/management/commands/load_forum.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from django.core import management
-from django.db.models import signals
-from django.contrib.contenttypes.models import ContentType
-from django.core.management.base import BaseCommand
-from askbot import models
-
-class Command(BaseCommand):
- args = '<data file>'
- help = 'Loads askbot forum data from the dump file obtained with command "dump_forum"'
- def handle(self, *args, **options):
- #need to remove badge data b/c they are aslo in the dump
- models.BadgeData.objects.all().delete()
- ContentType.objects.all().delete()
- #turn off the post_save signal so than Activity can be copied
- signals.post_save.receivers = []
- management.call_command('loaddata', args[0])
diff --git a/askbot/management/commands/remove_admin.py b/askbot/management/commands/remove_admin.py
index 2aa95c20..eca0b7b6 100644
--- a/askbot/management/commands/remove_admin.py
+++ b/askbot/management/commands/remove_admin.py
@@ -41,5 +41,4 @@ class Command(NoArgsCommand):
self.confirm_action()
self.remove_signals()
- self.user.remove_admin_status()
- self.user.save()
+ self.user.set_status('a')
diff --git a/askbot/management/commands/rename_tags.py b/askbot/management/commands/rename_tags.py
index bd15d685..a4e49a34 100644
--- a/askbot/management/commands/rename_tags.py
+++ b/askbot/management/commands/rename_tags.py
@@ -4,8 +4,10 @@ retagged
"""
import sys
from optparse import make_option
+from django.conf import settings as django_settings
from django.core import management
from django.core.management.base import BaseCommand, CommandError
+from django.utils import translation
from askbot import api, models
from askbot.utils import console
@@ -81,6 +83,14 @@ ask you to confirm your action before making changes.
default = None,
help = 'id of the user who will be marked as a performer of this operation'
),
+ make_option('--lang',
+ action = 'store',
+ type = 'str',
+ dest = 'lang',
+ default = django_settings.LANGUAGE_CODE,
+ help = 'language code for the tags to rename e.g. "en"'
+ ),
+
)
#@transaction.commit_manually
@@ -94,6 +104,7 @@ ask you to confirm your action before making changes.
The data of tag id's is then delegated to the command "rename_tag_id"
"""
+ translation.activate(django_settings.LANGUAGE_CODE)
if options['from'] is None:
raise CommandError('the --from argument is required')
if options['to'] is None:
@@ -113,7 +124,11 @@ ask you to confirm your action before making changes.
from_tags = list()
try:
for tag_name in from_tag_names:
- from_tags.append(models.Tag.objects.get(name = tag_name))
+ tag = models.Tag.objects.get(
+ name=tag_name,
+ language_code=options['lang']
+ )
+ from_tags.append(tag)
except models.Tag.DoesNotExist:
error_message = u"""tag %s was not found. It is possible that the tag
exists but we were not able to match it's unicode value
@@ -124,19 +139,24 @@ Also, you can try command "rename_tag_id"
""" % tag_name
raise CommandError(error_message)
except models.Tag.MultipleObjectsReturned:
- raise CommandError(u'found more than one tag named %s' % from_tag_name)
+ raise CommandError(u'found more than one tag named %s' % tag_name)
admin = get_admin(seed_user_id = options['user_id'])
to_tags = list()
for tag_name in to_tag_names:
try:
- to_tags.append(models.Tag.objects.get(name = tag_name))
+ tag = models.Tag.objects.get(
+ name=tag_name,
+ language_code=options['lang']
+ )
+ to_tags.append(tag)
except models.Tag.DoesNotExist:
to_tags.append(
models.Tag.objects.create(
- name = tag_name,
- created_by = admin
+ name=tag_name,
+ created_by=admin,
+ language_code=options['lang']
)
)
except models.Tag.MultipleObjectsReturned:
diff --git a/askbot/management/commands/rename_tags_id.py b/askbot/management/commands/rename_tags_id.py
index 1da22868..7926d28d 100644
--- a/askbot/management/commands/rename_tags_id.py
+++ b/askbot/management/commands/rename_tags_id.py
@@ -8,8 +8,10 @@ also, corresponding questions are retagged
import re
import sys
from optparse import make_option
+from django.conf import settings as django_settings
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
+from django.utils import translation
from askbot import const, models
from askbot.utils import console
from askbot.management.commands.rename_tags import get_admin
@@ -84,6 +86,7 @@ rename_tags, but using tag id's
def handle(self, *args, **options):
"""command handle function. retrieves tags by id
"""
+ translation.activate(django_settings.LANGUAGE_CODE)
try:
from_tag_ids = parse_tag_ids(options['from'])
to_tag_ids = parse_tag_ids(options['to'])
@@ -101,6 +104,14 @@ rename_tags, but using tag id's
from_tags = get_tags_by_ids(from_tag_ids)
to_tags = get_tags_by_ids(to_tag_ids)
+
+ #all tags must belong to the same language
+ lang_codes = {tag.language_code for tag in (from_tags + to_tags)}
+ if len(lang_codes) != 1:
+ langs = ', '.join(lang_codes)
+ raise CommandError('all tags must belong to the same language, have: %s' % langs)
+ lang = list(lang_codes).pop()
+
admin = get_admin(options['user_id'])
questions = models.Thread.objects.all()
@@ -142,7 +153,10 @@ or repost a bug, if that does not help"""
#if user provided tag1 as to_tag, and tagsynonym tag1->tag2 exists.
for to_tag_name in to_tag_names:
try:
- tag_synonym = models.TagSynonym.objects.get(source_tag_name = to_tag_name)
+ tag_synonym = models.TagSynonym.objects.get(
+ source_tag_name=to_tag_name,
+ language_code=lang
+ )
raise CommandError(u'You gave %s as --to argument, but TagSynonym: %s -> %s exists, probably you want to provide %s as --to argument' % (to_tag_name, tag_synonym.source_tag_name, tag_synonym.target_tag_name, tag_synonym.target_tag_name))
except models.TagSynonym.DoesNotExist:
pass
@@ -161,6 +175,7 @@ or repost a bug, if that does not help"""
tags = u' '.join(tag_names),
#silent = True #do we want to timestamp activity on question
)
+ question.invalidate_cached_thread_content_fragment()
i += 1
sys.stdout.write('%6.2f%%' % (100*float(i)/float(question_count)))
sys.stdout.write('\b'*7)
@@ -191,4 +206,7 @@ or repost a bug, if that does not help"""
# we want to update tagsynonym (tag1->tag2) to (tag1->tag3)
for from_tag_name in from_tag_names:
# we need db_index for target_tag_name as well for this
- models.TagSynonym.objects.filter(target_tag_name = from_tag_name).update(target_tag_name = to_tag_name)
+ models.TagSynonym.objects.filter(
+ target_tag_name=from_tag_name,
+ language_code=lang
+ ).update(target_tag_name = to_tag_name)
diff --git a/askbot/management/commands/send_accept_answer_reminders.py b/askbot/management/commands/send_accept_answer_reminders.py
index 22ae311d..70baac62 100644
--- a/askbot/management/commands/send_accept_answer_reminders.py
+++ b/askbot/management/commands/send_accept_answer_reminders.py
@@ -57,15 +57,7 @@ class Command(NoArgsCommand):
if question_count == 0:
continue
- subject_line = _(
- 'Accept the best answer for %(question_count)d of your questions'
- ) % {'question_count': question_count}
-
- #todo - make a template for these
- if question_count == 1:
- reminder_phrase = _('Please accept the best answer for this question:')
- else:
- reminder_phrase = _('Please accept the best answer for these questions:')
+ reminder_phrase = _('Please select the best responses to:')
data = {
'site_url': site_url(''),#here we need only the domain name
@@ -76,6 +68,7 @@ class Command(NoArgsCommand):
template = get_template('email/accept_answer_reminder.html')
body_text = template.render(Context(data))#todo: set lang
+ subject_line = askbot_settings.WORDS_ACCEPT_BEST_ANSWERS_FOR_YOUR_QUESTIONS
if DEBUG_THIS_COMMAND:
print "User: %s<br>\nSubject:%s<br>\nText: %s<br>\n" % \
(user.email, subject_line, body_text)
diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py
index e1783316..1f04690f 100644
--- a/askbot/management/commands/send_email_alerts.py
+++ b/askbot/management/commands/send_email_alerts.py
@@ -432,14 +432,23 @@ class Command(NoArgsCommand):
question_count = len(q_list.keys())
- subject_line = ungettext(
- '%(question_count)d updated question about %(topics)s',
- '%(question_count)d updated questions about %(topics)s',
- question_count
- ) % {
- 'question_count': question_count,
- 'topics': tag_summary
- }
+ if tag_summary:
+ subject_line = ungettext(
+ '%(question_count)d update about %(topics)s',
+ '%(question_count)d updates about %(topics)s',
+ question_count
+ ) % {
+ 'question_count': question_count,
+ 'topics': tag_summary
+ }
+ else:
+ subject_line = ungettext(
+ '%(question_count)d update',
+ '%(question_count)d updates',
+ question_count
+ ) % {
+ 'question_count': question_count,
+ }
items_added = 0
items_unreported = 0
@@ -471,7 +480,7 @@ class Command(NoArgsCommand):
'name': user.username,
'admin_email': askbot_settings.ADMIN_EMAIL,
'site_name': askbot_settings.APP_SHORT_NAME,
- 'is_multilingual': django_settings.ASKBOT_MULTILINGUAL
+ 'is_multilingual': getattr(django_settings, 'ASKBOT_MULTILINGUAL', False)
})
if DEBUG_THIS_COMMAND == True:
diff --git a/askbot/management/commands/send_unanswered_question_reminders.py b/askbot/management/commands/send_unanswered_question_reminders.py
index 42ce5119..c428881d 100644
--- a/askbot/management/commands/send_unanswered_question_reminders.py
+++ b/askbot/management/commands/send_unanswered_question_reminders.py
@@ -64,11 +64,13 @@ class Command(NoArgsCommand):
tag_summary = Thread.objects.get_tag_summary_from_threads(threads)
subject_line = ungettext(
- '%(question_count)d unanswered question about %(topics)s',
- '%(question_count)d unanswered questions about %(topics)s',
+ '%(question_count)d %(unanswered_question)s about %(topics)s',
+ '%(question_count)d %(unanswered_questions)s about %(topics)s',
question_count
) % {
'question_count': question_count,
+ 'unanswered_question': askbot_settings.WORDS_UNANSWERED_QUESTION_SINGULAR,
+ 'unanswered_questions': askbot_settings.WORDS_UNANSWERED_QUESTION_PLURAL,
'topics': tag_summary
}
diff --git a/askbot/media/bootstrap/css/bootstrap.css b/askbot/media/bootstrap/css/bootstrap.css
index 37eb662a..a2ba8bf9 100644
--- a/askbot/media/bootstrap/css/bootstrap.css
+++ b/askbot/media/bootstrap/css/bootstrap.css
@@ -2970,7 +2970,6 @@ input[type="submit"].btn.btn-mini {
}
.pager a:hover {
text-decoration: none;
- background-color: #f5f5f5;
}
.pager .next a {
float: right;
diff --git a/askbot/media/jquery-openid/images/mozilla-persona.gif b/askbot/media/jquery-openid/images/mozilla-persona.gif
new file mode 100644
index 00000000..e4a8a2d4
--- /dev/null
+++ b/askbot/media/jquery-openid/images/mozilla-persona.gif
Binary files differ
diff --git a/askbot/media/jquery-openid/jquery.openid.js b/askbot/media/jquery-openid/jquery.openid.js
index 881b2098..dac4d297 100644
--- a/askbot/media/jquery-openid/jquery.openid.js
+++ b/askbot/media/jquery-openid/jquery.openid.js
@@ -74,7 +74,7 @@ $.fn.authenticator = function() {
if (confirm(message)){
$.ajax({
type: 'POST',
- url: authUrl + 'delete_login_method/',//url!!!
+ url: askbot['urls']['deleteLoginMethod'],
data: {provider_name: provider_name},
success: function(data, text_status, xhr){
$(provider_row).remove();
@@ -310,7 +310,7 @@ $.fn.authenticator = function() {
var setup_password_login_or_change = function(provider_name){
var token_name = extra_token_name[provider_name]
var password_action_input = $('input[name=password_action]');
- if (userIsAuthenticated === true){
+ if (userIsAuthenticated === true && askbot['settings']['useLdapForPasswordLogin'] == false){
var password_button = $('input[name=change_password]');
var submit_action = submit_change_password;
if (provider_name === 'local'){
@@ -334,8 +334,7 @@ $.fn.authenticator = function() {
password_action_input.val('change_password');
var focus_input = $('#id_new_password');
var submittable_input = $('#id_new_password_retyped');
- }
- else{
+ } else {
$('#password-heading>span').html(token_name);
var password_button = $('input[name=login_with_password]');
var submit_action = submit_login_with_password;
@@ -382,12 +381,35 @@ $.fn.authenticator = function() {
$('#id_email').focus();
};
+ var start_mozilla_persona_login = function() {
+ navigator.id.request();
+ return false;
+ };
+
var clear_password_fields = function(){
$('#id_password').val('');
$('#id_new_password').val('');
$('#id_new_password_retyped').val('');
};
+ var setupMozillaPersonaListeners = function() {
+ navigator.id.watch({
+ loggedInUser: askbot['data']['userEmail'],
+ onlogin: function(assertion) {
+ var assertionElement = signin_form.find('input[name=persona_assertion]');
+ assertionElement.val(assertion);
+ provider_name_input.val('mozilla-persona');
+ signin_form.submit();
+ return false;
+ },
+ onlogout: function() {
+ if (askbot['data']['userIsAuthenticated']) {
+ window.location.href = askbot['urls']['signOut'];
+ }
+ }
+ });
+ };
+
var setup_default_handlers = function(){
setup_event_handlers(
signin_page.find('input.openid-direct'),
@@ -404,6 +426,17 @@ $.fn.authenticator = function() {
start_login_with_extra_openid_token
);
+ var mozillaPersonaBtn = signin_page.find('input.mozilla-persona');
+
+ if (mozillaPersonaBtn.length) {
+ setupMozillaPersonaListeners();
+ setup_event_handlers(
+ signin_page.find('input.mozilla-persona'),
+ start_mozilla_persona_login
+ );
+ }
+
+
setup_event_handlers(
signin_page.find('input.oauth,input.oauth2'),
start_simple_login
@@ -433,3 +466,89 @@ $.fn.authenticator = function() {
clear_password_fields();
return this;
};
+
+/**
+ * @constructor
+ */
+var ChangePasswordForm = function() {
+ WrappedElement.call(this);
+};
+inherits(ChangePasswordForm, WrappedElement);
+
+ChangePasswordForm.prototype.showMessage = function(message, callback) {
+ var flash = new FlashAlert('...saved, thanks');
+ if (callback) {
+ flash.setPostRunHandler(callback);
+ }
+ this._passwordHeading.append(flash.getElement());
+ flash.run();
+};
+
+ChangePasswordForm.prototype.clearErrors = function() {
+ this._pwInput1Errors.html('');
+ this._pwInput2Errors.html('');
+};
+
+ChangePasswordForm.prototype.showErrors = function(errors) {
+ if (errors['new_password']) {
+ this._pwInput1Errors.html(errors['new_password'][0]);
+ }
+ if (errors['new_password_retyped']) {
+ this._pwInput2Errors.html(errors['new_password_retyped'][0]);
+ }
+ if (errors['__all__']) {
+ this._pwInput2Errors.html(errors['__all__'][0]);
+ }
+};
+
+ChangePasswordForm.prototype.getData = function() {
+ return {
+ 'new_password': this._pwInput1.val(),
+ 'new_password_retyped': this._pwInput2.val()
+ };
+};
+
+ChangePasswordForm.prototype.getSubmitHandler = function() {
+ var me = this;
+ return function() {
+ $.ajax({
+ type: 'POST',
+ dataType: 'json',
+ data: me.getData(),
+ url: askbot['urls']['changePassword'],
+ success: function(data) {
+ if (data['message']) {
+ if (me.inAccountRecovery()) {
+ var callback = function() {
+ window.location.href = askbot['urls']['questions'];
+ };
+ me.showMessage(data['message'], callback);
+ } else {
+ me.showMessage(data['message']);
+ }
+ me.clearErrors();
+ }
+ if (data['errors']) {
+ me.clearErrors();
+ me.showErrors(data['errors']);
+ }
+ }
+ });
+ return false;
+ };
+};
+
+ChangePasswordForm.prototype.inAccountRecovery = function() {
+ return ($('input[name="in_recovery"]').length === 1);
+};
+
+ChangePasswordForm.prototype.decorate = function(element) {
+ this._element = element;
+ this._pwInput1 = element.find('#id_new_password');
+ this._pwInput2 = element.find('#id_new_password_retyped');
+ this._pwInput1Errors = element.find('.new-password-errors');
+ this._pwInput2Errors = element.find('.new-password-retyped-errors');
+ this._button = element.find('input[name="change_password"]');
+ this._passwordHeading = element.find('#password-heading');
+ setupButtonEventHandlers(this._button, this.getSubmitHandler());
+};
diff --git a/askbot/media/jquery-openid/openid.css b/askbot/media/jquery-openid/openid.css
index b46522bd..3e214dee 100644
--- a/askbot/media/jquery-openid/openid.css
+++ b/askbot/media/jquery-openid/openid.css
@@ -1,4 +1,6 @@
-div#login-icons {padding: 0;}
+div#login-icons {
+ padding: 6px 0 0 0;
+}
ul.login-icons {width: 450px; margin:0;padding:0;text-align:left; list-style-type:none; display:block;}
ul.login-icons li {display:inline;}
ul.large input {height: 40px; width: 90px;border:1px solid #ccc;margin:0 5px 5px 0;}
@@ -15,6 +17,27 @@ ul.large input {height: 40px; width: 90px;border:1px solid #ccc;margin:0 5px 5px
/*#signin-form #account-recovery-form input {cursor:pointer;}
#signin-form #account-recovery-form input.text {cursor:default;}*/
+#login-list-enabler {
+ margin: 20px 0 10px 0;
+}
+#existing-login-methods {
+ margin: 10px 0;
+}
+#account-recovery-form {
+ margin: 25px 0 10px;
+}
+#account-recovery-form h2 {
+ line-height: 16px;
+}
+#signup-form {
+ margin-bottom: 25px;
+}
+#password-heading .flash-alert {
+ color: #fe6806;
+ display: inline;
+ padding: 5px;
+}
+
table.login {
text-align: right;
}
diff --git a/askbot/media/js/group_messaging.js b/askbot/media/js/group_messaging.js
index add50425..58572e2d 100644
--- a/askbot/media/js/group_messaging.js
+++ b/askbot/media/js/group_messaging.js
@@ -674,10 +674,10 @@ MessageCenter.prototype.hitThreadsList = function(url, senderId, requestMethod)
me.setThreadsList(threads);
me.setState('show-list');
me.setLoadingStatus(false);
- },
+ }
+ },
error: function() {
me.setLoadingStatus(false);
- }
}
});
this.setLoadingStatus(true);
diff --git a/askbot/media/js/less.min.js b/askbot/media/js/less.min.js
index 6e4d5cff..77ac4fca 100644
--- a/askbot/media/js/less.min.js
+++ b/askbot/media/js/less.min.js
@@ -1,16 +1,16 @@
-//
-// LESS - Leaner CSS v1.1.3
-// http://lesscss.org
-//
-// Copyright (c) 2009-2011, Alexis Sellier
-// Licensed under the Apache 2.0 License.
-//
-//
-// LESS - Leaner CSS v1.1.3
-// http://lesscss.org
-//
-// Copyright (c) 2009-2011, Alexis Sellier
-// Licensed under the Apache 2.0 License.
-//
-(function(a,b){function v(a,b){var c="less-error-message:"+p(b),e=["<ul>",'<li><label>[-1]</label><pre class="ctx">{0}</pre></li>',"<li><label>[0]</label><pre>{current}</pre></li>",'<li><label>[1]</label><pre class="ctx">{2}</pre></li>',"</ul>"].join("\n"),f=document.createElement("div"),g,h;f.id=c,f.className="less-error-message",h="<h3>"+(a.message||"There is an error in your .less file")+"</h3>"+'<p><a href="'+b+'">'+b+"</a> ",a.extract&&(h+="on line "+a.line+", column "+(a.column+1)+":</p>"+e.replace(/\[(-?\d)\]/g,function(b,c){return parseInt(a.line)+parseInt(c)||""}).replace(/\{(\d)\}/g,function(b,c){return a.extract[parseInt(c)]||""}).replace(/\{current\}/,a.extract[1].slice(0,a.column)+'<span class="error">'+a.extract[1].slice(a.column)+"</span>")),f.innerHTML=h,q([".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #ee4444;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.ctx {","color: #dd4444;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),f.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),d.env=="development"&&(g=setInterval(function(){document.body&&(document.getElementById(c)?document.body.replaceChild(f,document.getElementById(c)):document.body.insertBefore(f,document.body.firstChild),clearInterval(g))},10))}function u(a){d.env=="development"&&typeof console!="undefined"&&console.log("less: "+a)}function t(a){return a&&a.parentNode.removeChild(a)}function s(){if(a.XMLHttpRequest)return new XMLHttpRequest;try{return new ActiveXObject("MSXML2.XMLHTTP.3.0")}catch(b){u("browser doesn't support AJAX.");return null}}function r(a,b,c,e){function i(b,c,d){b.status>=200&&b.status<300?c(b.responseText,b.getResponseHeader("Last-Modified")):typeof d=="function"&&d(b.status,a)}var f=s(),h=g?!1:d.async;typeof f.overrideMimeType=="function"&&f.overrideMimeType("text/css"),f.open("GET",a,h),f.setRequestHeader("Accept",b||"text/x-less, text/css; q=0.9, */*; q=0.5"),f.send(null),g?f.status===0?c(f.responseText):e(f.status,a):h?f.onreadystatechange=function(){f.readyState==4&&i(f,c,e)}:i(f,c,e)}function q(a,b,c){var d,e=b.href?b.href.replace(/\?.*$/,""):"",f="less:"+(b.title||p(e));(d=document.getElementById(f))===null&&(d=document.createElement("style"),d.type="text/css",d.media=b.media||"screen",d.id=f,document.getElementsByTagName("head")[0].appendChild(d));if(d.styleSheet)try{d.styleSheet.cssText=a}catch(g){throw new Error("Couldn't reassign styleSheet.cssText.")}else(function(a){d.childNodes.length>0?d.firstChild.nodeValue!==a.nodeValue&&d.replaceChild(a,d.firstChild):d.appendChild(a)})(document.createTextNode(a));c&&h&&(u("saving "+e+" to cache."),h.setItem(e,a),h.setItem(e+":timestamp",c))}function p(a){return a.replace(/^[a-z]+:\/\/?[^\/]+/,"").replace(/^\//,"").replace(/\?.*$/,"").replace(/\.[^\.\/]+$/,"").replace(/[^\.\w-]+/g,"-").replace(/\./g,":")}function o(b,c,e,f){var g=a.location.href.replace(/[#?].*$/,""),i=b.href.replace(/\?.*$/,""),j=h&&h.getItem(i),k=h&&h.getItem(i+":timestamp"),l={css:j,timestamp:k};/^(https?|file):/.test(i)||(i.charAt(0)=="/"?i=a.location.protocol+"//"+a.location.host+i:i=g.slice(0,g.lastIndexOf("/")+1)+i),r(b.href,b.type,function(a,g){if(!e&&l&&g&&(new Date(g)).valueOf()===(new Date(l.timestamp)).valueOf())q(l.css,b),c(null,b,{local:!0,remaining:f});else try{(new d.Parser({optimization:d.optimization,paths:[i.replace(/[\w\.-]+$/,"")],mime:b.type})).parse(a,function(a,d){if(a)return v(a,i);try{c(d,b,{local:!1,lastModified:g,remaining:f}),t(document.getElementById("less-error-message:"+p(i)))}catch(a){v(a,i)}})}catch(h){v(h,i)}},function(a,b){throw new Error("Couldn't load "+b+" ("+a+")")})}function n(a,b){for(var c=0;c<d.sheets.length;c++)o(d.sheets[c],a,b,d.sheets.length-(c+1))}function m(){var a=document.getElementsByTagName("style");for(var b=0;b<a.length;b++)a[b].type.match(k)&&(new d.Parser).parse(a[b].innerHTML||"",function(c,d){a[b].type="text/css",a[b].innerHTML=d.toCSS()})}function c(b){return a.less[b.split("/")[1]]}Array.isArray||(Array.isArray=function(a){return Object.prototype.toString.call(a)==="[object Array]"||a instanceof Array}),Array.prototype.forEach||(Array.prototype.forEach=function(a,b){var c=this.length>>>0;for(var d=0;d<c;d++)d in this&&a.call(b,this[d],d,this)}),Array.prototype.map||(Array.prototype.map=function(a){var b=this.length>>>0,c=Array(b),d=arguments[1];for(var e=0;e<b;e++)e in this&&(c[e]=a.call(d,this[e],e,this));return c}),Array.prototype.filter||(Array.prototype.filter=function(a){var b=[],c=arguments[1];for(var d=0;d<this.length;d++)a.call(c,this[d])&&b.push(this[d]);return b}),Array.prototype.reduce||(Array.prototype.reduce=function(a){var b=this.length>>>0,c=0;if(b===0&&arguments.length===1)throw new TypeError;if(arguments.length>=2)var d=arguments[1];else for(;;){if(c in this){d=this[c++];break}if(++c>=b)throw new TypeError}for(;c<b;c++)c in this&&(d=a.call(null,d,this[c],c,this));return d}),Array.prototype.indexOf||(Array.prototype.indexOf=function(a){var b=this.length,c=arguments[1]||0;if(!b)return-1;if(c>=b)return-1;c<0&&(c+=b);for(;c<b;c++){if(!Object.prototype.hasOwnProperty.call(this,c))continue;if(a===this[c])return c}return-1}),Object.keys||(Object.keys=function(a){var b=[];for(var c in a)Object.prototype.hasOwnProperty.call(a,c)&&b.push(c);return b}),String.prototype.trim||(String.prototype.trim=function(){return String(this).replace(/^\s\s*/,"").replace(/\s\s*$/,"")});var d,e;typeof a=="undefined"?(d=exports,e=c("less/tree")):(typeof a.less=="undefined"&&(a.less={}),d=a.less,e=a.less.tree={}),d.Parser=function(a){function t(a){return typeof a=="string"?b.charAt(c)===a:a.test(j[f])?!0:!1}function s(a){var d,e,g,h,i,m,n,o;if(a instanceof Function)return a.call(l.parsers);if(typeof a=="string")d=b.charAt(c)===a?a:null,g=1,r();else{r();if(d=a.exec(j[f]))g=d[0].length;else return null}if(d){o=c+=g,m=c+j[f].length-g;while(c<m){h=b.charCodeAt(c);if(h!==32&&h!==10&&h!==9)break;c++}j[f]=j[f].slice(g+(c-o)),k=c,j[f].length===0&&f<j.length-1&&f++;return typeof d=="string"?d:d.length===1?d[0]:d}}function r(){c>k&&(j[f]=j[f].slice(c-k),k=c)}function q(){j[f]=g,c=h,k=c}function p(){g=j[f],h=c,k=c}var b,c,f,g,h,i,j,k,l,m=this,n=function(){},o=this.imports={paths:a&&a.paths||[],queue:[],files:{},mime:a&&a.mime,push:function(b,c){var e=this;this.queue.push(b),d.Parser.importer(b,this.paths,function(a){e.queue.splice(e.queue.indexOf(b),1),e.files[b]=a,c(a),e.queue.length===0&&n()},a)}};this.env=a=a||{},this.optimization="optimization"in this.env?this.env.optimization:1,this.env.filename=this.env.filename||null;return l={imports:o,parse:function(d,g){var h,l,m,o,p,q,r=[],t,u=null;c=f=k=i=0,j=[],b=d.replace(/\r\n/g,"\n"),j=function(c){var d=0,e=/[^"'`\{\}\/\(\)]+/g,f=/\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,g=0,h,i=c[0],j,k;for(var l=0,m,n;l<b.length;l++){e.lastIndex=l,(h=e.exec(b))&&h.index===l&&(l+=h[0].length,i.push(h[0])),m=b.charAt(l),f.lastIndex=l,!k&&!j&&m==="/"&&(n=b.charAt(l+1),(n==="/"||n==="*")&&(h=f.exec(b))&&h.index===l&&(l+=h[0].length,i.push(h[0]),m=b.charAt(l)));if(m==="{"&&!k&&!j)g++,i.push(m);else if(m==="}"&&!k&&!j)g--,i.push(m),c[++d]=i=[];else if(m==="("&&!k&&!j)i.push(m),j=!0;else if(m===")"&&!k&&j)i.push(m),j=!1;else{if(m==='"'||m==="'"||m==="`")k?k=k===m?!1:k:k=m;i.push(m)}}if(g>0)throw{type:"Syntax",message:"Missing closing `}`",filename:a.filename};return c.map(function(a){return a.join("")})}([[]]),h=new e.Ruleset([],s(this.parsers.primary)),h.root=!0,h.toCSS=function(c){var d,f,g;return function(g,h){function n(a){return a?(b.slice(0,a).match(/\n/g)||"").length:null}var i=[];g=g||{},typeof h=="object"&&!Array.isArray(h)&&(h=Object.keys(h).map(function(a){var b=h[a];b instanceof e.Value||(b instanceof e.Expression||(b=new e.Expression([b])),b=new e.Value([b]));return new e.Rule("@"+a,b,!1,0)}),i=[new e.Ruleset(null,h)]);try{var j=c.call(this,{frames:i}).toCSS([],{compress:g.compress||!1})}catch(k){f=b.split("\n"),d=n(k.index);for(var l=k.index,m=-1;l>=0&&b.charAt(l)!=="\n";l--)m++;throw{type:k.type,message:k.message,filename:a.filename,index:k.index,line:typeof d=="number"?d+1:null,callLine:k.call&&n(k.call)+1,callExtract:f[n(k.call)],stack:k.stack,column:m,extract:[f[d-1],f[d],f[d+1]]}}return g.compress?j.replace(/(\s)+/g,"$1"):j}}(h.eval);if(c<b.length-1){c=i,q=b.split("\n"),p=(b.slice(0,c).match(/\n/g)||"").length+1;for(var v=c,w=-1;v>=0&&b.charAt(v)!=="\n";v--)w++;u={name:"ParseError",message:"Syntax Error on line "+p,index:c,filename:a.filename,line:p,column:w,extract:[q[p-2],q[p-1],q[p]]}}this.imports.queue.length>0?n=function(){g(u,h)}:g(u,h)},parsers:{primary:function(){var a,b=[];while((a=s(this.mixin.definition)||s(this.rule)||s(this.ruleset)||s(this.mixin.call)||s(this.comment)||s(this.directive))||s(/^[\s\n]+/))a&&b.push(a);return b},comment:function(){var a;if(b.charAt(c)==="/"){if(b.charAt(c+1)==="/")return new e.Comment(s(/^\/\/.*/),!0);if(a=s(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/))return new e.Comment(a)}},entities:{quoted:function(){var a,d=c,f;b.charAt(d)==="~"&&(d++,f=!0);if(b.charAt(d)==='"'||b.charAt(d)==="'"){f&&s("~");if(a=s(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/))return new e.Quoted(a[0],a[1]||a[2],f)}},keyword:function(){var a;if(a=s(/^[A-Za-z-]+/))return new e.Keyword(a)},call:function(){var a,b,d=c;if(!!(a=/^([\w-]+|%)\(/.exec(j[f]))){a=a[1].toLowerCase();if(a==="url")return null;c+=a.length;if(a==="alpha")return s(this.alpha);s("("),b=s(this.entities.arguments);if(!s(")"))return;if(a)return new e.Call(a,b,d)}},arguments:function(){var a=[],b;while(b=s(this.expression)){a.push(b);if(!s(","))break}return a},literal:function(){return s(this.entities.dimension)||s(this.entities.color)||s(this.entities.quoted)},url:function(){var a;if(b.charAt(c)==="u"&&!!s(/^url\(/)){a=s(this.entities.quoted)||s(this.entities.variable)||s(this.entities.dataURI)||s(/^[-\w%@$\/.&=:;#+?~]+/)||"";if(!s(")"))throw new Error("missing closing ) for url()");return new e.URL(a.value||a.data||a instanceof e.Variable?a:new e.Anonymous(a),o.paths)}},dataURI:function(){var a;if(s(/^data:/)){a={},a.mime=s(/^[^\/]+\/[^,;)]+/)||"",a.charset=s(/^;\s*charset=[^,;)]+/)||"",a.base64=s(/^;\s*base64/)||"",a.data=s(/^,\s*[^)]+/);if(a.data)return a}},variable:function(){var a,d=c;if(b.charAt(c)==="@"&&(a=s(/^@@?[\w-]+/)))return new e.Variable(a,d)},color:function(){var a;if(b.charAt(c)==="#"&&(a=s(/^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})/)))return new e.Color(a[1])},dimension:function(){var a,d=b.charCodeAt(c);if(!(d>57||d<45||d===47))if(a=s(/^(-?\d*\.?\d+)(px|%|em|pc|ex|in|deg|s|ms|pt|cm|mm|rad|grad|turn)?/))return new e.Dimension(a[1],a[2])},javascript:function(){var a,d=c,f;b.charAt(d)==="~"&&(d++,f=!0);if(b.charAt(d)==="`"){f&&s("~");if(a=s(/^`([^`]*)`/))return new e.JavaScript(a[1],c,f)}}},variable:function(){var a;if(b.charAt(c)==="@"&&(a=s(/^(@[\w-]+)\s*:/)))return a[1]},shorthand:function(){var a,b;if(!!t(/^[@\w.%-]+\/[@\w.-]+/)&&(a=s(this.entity))&&s("/")&&(b=s(this.entity)))return new e.Shorthand(a,b)},mixin:{call:function(){var a=[],d,f,g,h=c,i=b.charAt(c);if(i==="."||i==="#"){while(d=s(/^[#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/))a.push(new e.Element(f,d)),f=s(">");s("(")&&(g=s(this.entities.arguments))&&s(")");if(a.length>0&&(s(";")||t("}")))return new e.mixin.Call(a,g,h)}},definition:function(){var a,d=[],f,g,h,i;if(!(b.charAt(c)!=="."&&b.charAt(c)!=="#"||t(/^[^{]*(;|})/)))if(f=s(/^([#.](?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+)\s*\(/)){a=f[1];while(h=s(this.entities.variable)||s(this.entities.literal)||s(this.entities.keyword)){if(h instanceof e.Variable)if(s(":"))if(i=s(this.expression))d.push({name:h.name,value:i});else throw new Error("Expected value");else d.push({name:h.name});else d.push({value:h});if(!s(","))break}if(!s(")"))throw new Error("Expected )");g=s(this.block);if(g)return new e.mixin.Definition(a,d,g)}}},entity:function(){return s(this.entities.literal)||s(this.entities.variable)||s(this.entities.url)||s(this.entities.call)||s(this.entities.keyword)||s(this.entities.javascript)||s(this.comment)},end:function(){return s(";")||t("}")},alpha:function(){var a;if(!!s(/^\(opacity=/i))if(a=s(/^\d+/)||s(this.entities.variable)){if(!s(")"))throw new Error("missing closing ) for alpha()");return new e.Alpha(a)}},element:function(){var a,b,c;c=s(this.combinator),a=s(/^(?:[.#]?|:*)(?:[\w-]|\\(?:[a-fA-F0-9]{1,6} ?|[^a-fA-F0-9]))+/)||s("*")||s(this.attribute)||s(/^\([^)@]+\)/);if(a)return new e.Element(c,a)},combinator:function(){var a,d=b.charAt(c);if(d===">"||d==="&"||d==="+"||d==="~"){c++;while(b.charAt(c)===" ")c++;return new e.Combinator(d)}if(d===":"&&b.charAt(c+1)===":"){c+=2;while(b.charAt(c)===" ")c++;return new e.Combinator("::")}return b.charAt(c-1)===" "?new e.Combinator(" "):new e.Combinator(null)},selector:function(){var a,d,f=[],g,h;while(d=s(this.element)){g=b.charAt(c),f.push(d);if(g==="{"||g==="}"||g===";"||g===",")break}if(f.length>0)return new e.Selector(f)},tag:function(){return s(/^[a-zA-Z][a-zA-Z-]*[0-9]?/)||s("*")},attribute:function(){var a="",b,c,d;if(!!s("[")){if(b=s(/^[a-zA-Z-]+/)||s(this.entities.quoted))(d=s(/^[|~*$^]?=/))&&(c=s(this.entities.quoted)||s(/^[\w-]+/))?a=[b,d,c.toCSS?c.toCSS():c].join(""):a=b;if(!s("]"))return;if(a)return"["+a+"]"}},block:function(){var a;if(s("{")&&(a=s(this.primary))&&s("}"))return a},ruleset:function(){var a=[],b,d,g;p();if(g=/^([.#: \w-]+)[\s\n]*\{/.exec(j[f]))c+=g[0].length-1,a=[new e.Selector([new e.Element(null,g[1])])];else while(b=s(this.selector)){a.push(b),s(this.comment);if(!s(","))break;s(this.comment)}if(a.length>0&&(d=s(this.block)))return new e.Ruleset(a,d);i=c,q()},rule:function(){var a,d,g=b.charAt(c),k,l;p();if(g!=="."&&g!=="#"&&g!=="&")if(a=s(this.variable)||s(this.property)){a.charAt(0)!="@"&&(l=/^([^@+\/'"*`(;{}-]*);/.exec(j[f]))?(c+=l[0].length-1,d=new e.Anonymous(l[1])):a==="font"?d=s(this.font):d=s(this.value),k=s(this.important);if(d&&s(this.end))return new e.Rule(a,d,k,h);i=c,q()}},"import":function(){var a;if(s(/^@import\s+/)&&(a=s(this.entities.quoted)||s(this.entities.url))&&s(";"))return new e.Import(a,o)},directive:function(){var a,d,f,g;if(b.charAt(c)==="@"){if(d=s(this["import"]))return d;if(a=s(/^@media|@page|@-[-a-z]+/)){g=(s(/^[^{]+/)||"").trim();if(f=s(this.block))return new e.Directive(a+" "+g,f)}else if(a=s(/^@[-a-z]+/))if(a==="@font-face"){if(f=s(this.block))return new e.Directive(a,f)}else if((d=s(this.entity))&&s(";"))return new e.Directive(a,d)}},font:function(){var a=[],b=[],c,d,f,g;while(g=s(this.shorthand)||s(this.entity))b.push(g);a.push(new e.Expression(b));if(s(","))while(g=s(this.expression)){a.push(g);if(!s(","))break}return new e.Value(a)},value:function(){var a,b=[],c;while(a=s(this.expression)){b.push(a);if(!s(","))break}if(b.length>0)return new e.Value(b)},important:function(){if(b.charAt(c)==="!")return s(/^! *important/)},sub:function(){var a;if(s("(")&&(a=s(this.expression))&&s(")"))return a},multiplication:function(){var a,b,c,d;if(a=s(this.operand)){while((c=s("/")||s("*"))&&(b=s(this.operand)))d=new e.Operation(c,[d||a,b]);return d||a}},addition:function(){var a,d,f,g;if(a=s(this.multiplication)){while((f=s(/^[-+]\s+/)||b.charAt(c-1)!=" "&&(s("+")||s("-")))&&(d=s(this.multiplication)))g=new e.Operation(f,[g||a,d]);return g||a}},operand:function(){var a,d=b.charAt(c+1);b.charAt(c)==="-"&&(d==="@"||d==="(")&&(a=s("-"));var f=s(this.sub)||s(this.entities.dimension)||s(this.entities.color)||s(this.entities.variable)||s(this.entities.call);return a?new e.Operation("*",[new e.Dimension(-1),f]):f},expression:function(){var a,b,c=[],d;while(a=s(this.addition)||s(this.entity))c.push(a);if(c.length>0)return new e.Expression(c)},property:function(){var a;if(a=s(/^(\*?-?[-a-z_0-9]+)\s*:/))return a[1]}}}},typeof a!="undefined"&&(d.Parser.importer=function(a,b,c,d){a.charAt(0)!=="/"&&b.length>0&&(a=b[0]+a),o({href:a,title:a,type:d.mime},c,!0)}),function(a){function d(a){return Math.min(1,Math.max(0,a))}function c(b){if(b instanceof a.Dimension)return parseFloat(b.unit=="%"?b.value/100:b.value);if(typeof b=="number")return b;throw{error:"RuntimeError",message:"color functions take numbers as parameters"}}function b(b){return a.functions.hsla(b.h,b.s,b.l,b.a)}a.functions={rgb:function(a,b,c){return this.rgba(a,b,c,1)},rgba:function(b,d,e,f){var g=[b,d,e].map(function(a){return c(a)}),f=c(f);return new a.Color(g,f)},hsl:function(a,b,c){return this.hsla(a,b,c,1)},hsla:function(a,b,d,e){function h(a){a=a<0?a+1:a>1?a-1:a;return a*6<1?g+(f-g)*a*6:a*2<1?f:a*3<2?g+(f-g)*(2/3-a)*6:g}a=c(a)%360/360,b=c(b),d=c(d),e=c(e);var f=d<=.5?d*(b+1):d+b-d*b,g=d*2-f;return this.rgba(h(a+1/3)*255,h(a)*255,h(a-1/3)*255,e)},hue:function(b){return new a.Dimension(Math.round(b.toHSL().h))},saturation:function(b){return new a.Dimension(Math.round(b.toHSL().s*100),"%")},lightness:function(b){return new a.Dimension(Math.round(b.toHSL().l*100),"%")},alpha:function(b){return new a.Dimension(b.toHSL().a)},saturate:function(a,c){var e=a.toHSL();e.s+=c.value/100,e.s=d(e.s);return b(e)},desaturate:function(a,c){var e=a.toHSL();e.s-=c.value/100,e.s=d(e.s);return b(e)},lighten:function(a,c){var e=a.toHSL();e.l+=c.value/100,e.l=d(e.l);return b(e)},darken:function(a,c){var e=a.toHSL();e.l-=c.value/100,e.l=d(e.l);return b(e)},fadein:function(a,c){var e=a.toHSL();e.a+=c.value/100,e.a=d(e.a);return b(e)},fadeout:function(a,c){var e=a.toHSL();e.a-=c.value/100,e.a=d(e.a);return b(e)},spin:function(a,c){var d=a.toHSL(),e=(d.h+c.value)%360;d.h=e<0?360+e:e;return b(d)},mix:function(b,c,d){var e=d.value/100,f=e*2-1,g=b.toHSL().a-c.toHSL().a,h=((f*g==-1?f:(f+g)/(1+f*g))+1)/2,i=1-h,j=[b.rgb[0]*h+c.rgb[0]*i,b.rgb[1]*h+c.rgb[1]*i,b.rgb[2]*h+c.rgb[2]*i],k=b.alpha*e+c.alpha*(1-e);return new a.Color(j,k)},greyscale:function(b){return this.desaturate(b,new a.Dimension(100))},e:function(b){return new a.Anonymous(b instanceof a.JavaScript?b.evaluated:b)},escape:function(b){return new a.Anonymous(encodeURI(b.value).replace(/=/g,"%3D").replace(/:/g,"%3A").replace(/#/g,"%23").replace(/;/g,"%3B").replace(/\(/g,"%28").replace(/\)/g,"%29"))},"%":function(b){var c=Array.prototype.slice.call(arguments,1),d=b.value;for(var e=0;e<c.length;e++)d=d.replace(/%[sda]/i,function(a){var b=a.match(/s/i)?c[e].value:c[e].toCSS();return a.match(/[A-Z]$/)?encodeURIComponent(b):b});d=d.replace(/%%/g,"%");return new a.Quoted('"'+d+'"',d)},round:function(b){if(b instanceof a.Dimension)return new a.Dimension(Math.round(c(b)),b.unit);if(typeof b=="number")return Math.round(b);throw{error:"RuntimeError",message:"math functions take numbers as parameters"}}}}(c("less/tree")),function(a){a.Alpha=function(a){this.value=a},a.Alpha.prototype={toCSS:function(){return"alpha(opacity="+(this.value.toCSS?this.value.toCSS():this.value)+")"},eval:function(a){this.value.eval&&(this.value=this.value.eval(a));return this}}}(c("less/tree")),function(a){a.Anonymous=function(a){this.value=a.value||a},a.Anonymous.prototype={toCSS:function(){return this.value},eval:function(){return this}}}(c("less/tree")),function(a){a.Call=function(a,b,c){this.name=a,this.args=b,this.index=c},a.Call.prototype={eval:function(b){var c=this.args.map(function(a){return a.eval(b)});if(!(this.name in a.functions))return new a.Anonymous(this.name+"("+c.map(function(a){return a.toCSS()}).join(", ")+")");try{return a.functions[this.name].apply(a.functions,c)}catch(d){throw{message:"error evaluating function `"+this.name+"`",index:this.index}}},toCSS:function(a){return this.eval(a).toCSS()}}}(c("less/tree")),function(a){a.Color=function(a,b){Array.isArray(a)?this.rgb=a:a.length==6?this.rgb=a.match(/.{2}/g).map(function(a){return parseInt(a,16)}):a.length==8?(this.alpha=parseInt(a.substring(0,2),16)/255,this.rgb=a.substr(2).match(/.{2}/g).map(function(a){return parseInt(a,16)})):this.rgb=a.split("").map(function(a){return parseInt(a+a,16)}),this.alpha=typeof b=="number"?b:1},a.Color.prototype={eval:function(){return this},toCSS:function(){return this.alpha<1?"rgba("+this.rgb.map(function(a){return Math.round(a)}).concat(this.alpha).join(", ")+")":"#"+this.rgb.map(function(a){a=Math.round(a),a=(a>255?255:a<0?0:a).toString(16);return a.length===1?"0"+a:a}).join("")},operate:function(b,c){var d=[];c instanceof a.Color||(c=c.toColor());for(var e=0;e<3;e++)d[e]=a.operate(b,this.rgb[e],c.rgb[e]);return new a.Color(d,this.alpha+c.alpha)},toHSL:function(){var a=this.rgb[0]/255,b=this.rgb[1]/255,c=this.rgb[2]/255,d=this.alpha,e=Math.max(a,b,c),f=Math.min(a,b,c),g,h,i=(e+f)/2,j=e-f;if(e===f)g=h=0;else{h=i>.5?j/(2-e-f):j/(e+f);switch(e){case a:g=(b-c)/j+(b<c?6:0);break;case b:g=(c-a)/j+2;break;case c:g=(a-b)/j+4}g/=6}return{h:g*360,s:h,l:i,a:d}}}}(c("less/tree")),function(a){a.Comment=function(a,b){this.value=a,this.silent=!!b},a.Comment.prototype={toCSS:function(a){return a.compress?"":this.value},eval:function(){return this}}}(c("less/tree")),function(a){a.Dimension=function(a,b){this.value=parseFloat(a),this.unit=b||null},a.Dimension.prototype={eval:function(){return this},toColor:function(){return new a.Color([this.value,this.value,this.value])},toCSS:function(){var a=this.value+this.unit;return a},operate:function(b,c){return new a.Dimension(a.operate(b,this.value,c.value),this.unit||c.unit)}}}(c("less/tree")),function(a){a.Directive=function(b,c){this.name=b,Array.isArray(c)?this.ruleset=new a.Ruleset([],c):this.value=c},a.Directive.prototype={toCSS:function(a,b){if(this.ruleset){this.ruleset.root=!0;return this.name+(b.compress?"{":" {\n ")+this.ruleset.toCSS(a,b).trim().replace(/\n/g,"\n ")+(b.compress?"}":"\n}\n")}return this.name+" "+this.value.toCSS()+";\n"},eval:function(a){a.frames.unshift(this),this.ruleset=this.ruleset&&this.ruleset.eval(a),a.frames.shift();return this},variable:function(b){return a.Ruleset.prototype.variable.call(this.ruleset,b)},find:function(){return a.Ruleset.prototype.find.apply(this.ruleset,arguments)},rulesets:function(){return a.Ruleset.prototype.rulesets.apply(this.ruleset)}}}(c("less/tree")),function(a){a.Element=function(b,c){this.combinator=b instanceof a.Combinator?b:new a.Combinator(b),this.value=c.trim()},a.Element.prototype.toCSS=function(a){return this.combinator.toCSS(a||{})+this.value},a.Combinator=function(a){a===" "?this.value=" ":this.value=a?a.trim():""},a.Combinator.prototype.toCSS=function(a){return{"":""," ":" ","&":"",":":" :","::":"::","+":a.compress?"+":" + ","~":a.compress?"~":" ~ ",">":a.compress?">":" > "}[this.value]}}(c("less/tree")),function(a){a.Expression=function(a){this.value=a},a.Expression.prototype={eval:function(b){return this.value.length>1?new a.Expression(this.value.map(function(a){return a.eval(b)})):this.value.length===1?this.value[0].eval(b):this},toCSS:function(a){return this.value.map(function(b){return b.toCSS(a)}).join(" ")}}}(c("less/tree")),function(a){a.Import=function(b,c){var d=this;this._path=b,b instanceof a.Quoted?this.path=/\.(le?|c)ss$/.test(b.value)?b.value:b.value+".less":this.path=b.value.value||b.value,this.css=/css$/.test(this.path),this.css||c.push(this.path,function(a){if(!a)throw new Error("Error parsing "+d.path);d.root=a})},a.Import.prototype={toCSS:function(){return this.css?"@import "+this._path.toCSS()+";\n":""},eval:function(b){var c;if(this.css)return this;c=new a.Ruleset(null,this.root.rules.slice(0));for(var d=0;d<c.rules.length;d++)c.rules[d]instanceof a.Import&&Array.prototype.splice.apply(c.rules,[d,1].concat(c.rules[d].eval(b)));return c.rules}}}(c("less/tree")),function(a){a.JavaScript=function(a,b,c){this.escaped=c,this.expression=a,this.index=b},a.JavaScript.prototype={eval:function(b){var c,d=this,e={},f=this.expression.replace(/@\{([\w-]+)\}/g,function(c,e){return a.jsify((new a.Variable("@"+e,d.index)).eval(b))});try{f=new Function("return ("+f+")")}catch(g){throw{message:"JavaScript evaluation error: `"+f+"`",index:this.index}}for(var h in b.frames[0].variables())e[h.slice(1)]={value:b.frames[0].variables()[h].value,toJS:function(){return this.value.eval(b).toCSS()}};try{c=f.call(e)}catch(g){throw{message:"JavaScript evaluation error: '"+g.name+": "+g.message+"'",index:this.index}}return typeof c=="string"?new a.Quoted('"'+c+'"',c,this.escaped,this.index):Array.isArray(c)?new a.Anonymous(c.join(", ")):new a.Anonymous(c)}}}(c("less/tree")),function(a){a.Keyword=function(a){this.value=a},a.Keyword.prototype={eval:function(){return this},toCSS:function(){return this.value}}}(c("less/tree")),function(a){a.mixin={},a.mixin.Call=function(b,c,d){this.selector=new a.Selector(b),this.arguments=c,this.index=d},a.mixin.Call.prototype={eval:function(a){var b,c,d=[],e=!1;for(var f=0;f<a.frames.length;f++)if((b=a.frames[f].find(this.selector)).length>0){c=this.arguments&&this.arguments.map(function(b){return b.eval(a)});for(var g=0;g<b.length;g++)if(b[g].match(c,a))try{Array.prototype.push.apply(d,b[g].eval(a,this.arguments).rules),e=!0}catch(h){throw{message:h.message,index:h.index,stack:h.stack,call:this.index}}if(e)return d;throw{message:"No matching definition was found for `"+this.selector.toCSS().trim()+"("+this.arguments.map(function(a){return a.toCSS()}).join(", ")+")`",index:this.index}}throw{message:this.selector.toCSS().trim()+" is undefined",index:this.index}}},a.mixin.Definition=function(b,c,d){this.name=b,this.selectors=[new a.Selector([new a.Element(null,b)])],this.params=c,this.arity=c.length,this.rules=d,this._lookups={},this.required=c.reduce(function(a,b){return!b.name||b.name&&!b.value?a+1:a},0),this.parent=a.Ruleset.prototype,this.frames=[]},a.mixin.Definition.prototype={toCSS:function(){return""},variable:function(a){return this.parent.variable.call(this,a)},variables:function(){return this.parent.variables.call(this)},find:function(){return this.parent.find.apply(this,arguments)},rulesets:function(){return this.parent.rulesets.apply(this)},eval:function(b,c){var d=new a.Ruleset(null,[]),e,f=[];for(var g=0,h;g<this.params.length;g++)if(this.params[g].name)if(h=c&&c[g]||this.params[g].value)d.rules.unshift(new a.Rule(this.params[g].name,h.eval(b)));else throw{message:"wrong number of arguments for "+this.name+" ("+c.length+" for "+this.arity+")"};for(var g=0;g<Math.max(this.params.length,c&&c.length);g++)f.push(c[g]||this.params[g].value);d.rules.unshift(new a.Rule("@arguments",(new a.Expression(f)).eval(b)));return(new a.Ruleset(null,this.rules.slice(0))).eval({frames:[this,d].concat(this.frames,b.frames)})},match:function(a,b){var c=a&&a.length||0,d;if(c<this.required)return!1;if(this.required>0&&c>this.params.length)return!1;d=Math.min(c,this.arity);for(var e=0;e<d;e++)if(!this.params[e].name&&a[e].eval(b).toCSS()!=this.params[e].value.eval(b).toCSS())return!1;return!0}}}(c("less/tree")),function(a){a.Operation=function(a,b){this.op=a.trim(),this.operands=b},a.Operation.prototype.eval=function(b){var c=this.operands[0].eval(b),d=this.operands[1].eval(b),e;if(c instanceof a.Dimension&&d instanceof a.Color)if(this.op==="*"||this.op==="+")e=d,d=c,c=e;else throw{name:"OperationError",message:"Can't substract or divide a color from a number"};return c.operate(this.op,d)},a.operate=function(a,b,c){switch(a){case"+":return b+c;case"-":return b-c;case"*":return b*c;case"/":return b/c}}}(c("less/tree")),function(a){a.Quoted=function(a,b,c,d){this.escaped=c,this.value=b||"",this.quote=a.charAt(0),this.index=d},a.Quoted.prototype={toCSS:function(){return this.escaped?this.value:this.quote+this.value+this.quote},eval:function(b){var c=this,d=this.value.replace(/`([^`]+)`/g,function(d,e){return(new a.JavaScript(e,c.index,!0)).eval(b).value}).replace(/@\{([\w-]+)\}/g,function(d,e){var f=(new a.Variable("@"+e,c.index)).eval(b);return f.value||f.toCSS()});return new a.Quoted(this.quote+d+this.quote,d,this.escaped,this.index)}}}(c("less/tree")),function(a){a.Rule=function(b,c,d,e){this.name=b,this.value=c instanceof a.Value?c:new a.Value([c]),this.important=d?" "+d.trim():"",this.index=e,b.charAt(0)==="@"?this.variable=!0:this.variable=!1},a.Rule.prototype.toCSS=function(a){return this.variable?"":this.name+(a.compress?":":": ")+this.value.toCSS(a)+this.important+";"},a.Rule.prototype.eval=function(b){return new a.Rule(this.name,this.value.eval(b),this.important,this.index)},a.Shorthand=function(a,b){this.a=a,this.b=b},a.Shorthand.prototype={toCSS:function(a){return this.a.toCSS(a)+"/"+this.b.toCSS(a)},eval:function(){return this}}}(c("less/tree")),function(a){a.Ruleset=function(a,b){this.selectors=a,this.rules=b,this._lookups={}},a.Ruleset.prototype={eval:function(b){var c=new a.Ruleset(this.selectors,this.rules.slice(0));c.root=this.root,b.frames.unshift(c);if(c.root)for(var d=0;d<c.rules.length;d++)c.rules[d]instanceof a.Import&&Array.prototype.splice.apply(c.rules,[d,1].concat(c.rules[d].eval(b)));for(var d=0;d<c.rules.length;d++)c.rules[d]instanceof a.mixin.Definition&&(c.rules[d].frames=b.frames.slice(0));for(var d=0;d<c.rules.length;d++)c.rules[d]instanceof a.mixin.Call&&Array.prototype.splice.apply(c.rules,[d,1].concat(c.rules[d].eval(b)));for(var d=0,e;d<c.rules.length;d++)e=c.rules[d],e instanceof a.mixin.Definition||(c.rules[d]=e.eval?e.eval(b):e);b.frames.shift();return c},match:function(a){return!a||a.length===0},variables:function(){return this._variables?this._variables:this._variables=this.rules.reduce(function(b,c){c instanceof a.Rule&&c.variable===!0&&(b[c.name]=c);return b},{})},variable:function(a){return this.variables()[a]},rulesets:function(){return this._rulesets?this._rulesets:this._rulesets=this.rules.filter(function(b){return b instanceof a.Ruleset||b instanceof a.mixin.Definition})},find:function(b,c){c=c||this;var d=[],e,f,g=b.toCSS();if(g in this._lookups)return this._lookups[g];this.rulesets().forEach(function(e){if(e!==c)for(var g=0;g<e.selectors.length;g++)if(f=b.match(e.selectors[g])){b.elements.length>1?Array.prototype.push.apply(d,e.find(new a.Selector(b.elements.slice(1)),c)):d.push(e);break}});return this._lookups[g]=d},toCSS:function(b,c){var d=[],e=[],f=[],g=[],h,i;if(!this.root)if(b.length===0)g=this.selectors.map(function(a){return[a]});else for(var j=0;j<this.selectors.length;j++)for(var k=0;k<b.length;k++)g.push(b[k].concat([this.selectors[j]]));for(var l=0;l<this.rules.length;l++)i=this.rules[l],i.rules||i instanceof a.Directive?f.push(i.toCSS(g,c)):i instanceof a.Comment?i.silent||(this.root?f.push(i.toCSS(c)):e.push(i.toCSS(c))):i.toCSS&&!i.variable?e.push(i.toCSS(c)):i.value&&!i.variable&&e.push(i.value.toString());f=f.join(""),this.root?d.push(e.join(c.compress?"":"\n")):e.length>0&&(h=g.map(function(a){return a.map(function(a){return a.toCSS(c)}).join("").trim()}).join(c.compress?",":g.length>3?",\n":", "),d.push(h,(c.compress?"{":" {\n ")+e.join(c.compress?"":"\n ")+(c.compress?"}":"\n}\n"))),d.push(f);return d.join("")+(c.compress?"\n":"")}}}(c("less/tree")),function(a){a.Selector=function(a){this.elements=a,this.elements[0].combinator.value===""&&(this.elements[0].combinator.value=" ")},a.Selector.prototype.match=function(a){return this.elements[0].value===a.elements[0].value?!0:!1},a.Selector.prototype.toCSS=function(a){if(this._css)return this._css;return this._css=this.elements.map(function(b){return typeof b=="string"?" "+b.trim():b.toCSS(a)}).join("")}}(c("less/tree")),function(b){b.URL=function(b,c){b.data?this.attrs=b:(!/^(?:https?:\/|file:\/|data:\/)?\//.test(b.value)&&c.length>0&&typeof a!="undefined"&&(b.value=c[0]+(b.value.charAt(0)==="/"?b.value.slice(1):b.value)),this.value=b,this.paths=c)},b.URL.prototype={toCSS:function(){return"url("+(this.attrs?"data:"+this.attrs.mime+this.attrs.charset+this.attrs.base64+this.attrs.data:this.value.toCSS())+")"},eval:function(a){return this.attrs?this:new b.URL(this.value.eval(a),this.paths)}}}(c("less/tree")),function(a){a.Value=function(a){this.value=a,this.is="value"},a.Value.prototype={eval:function(b){return this.value.length===1?this.value[0].eval(b):new a.Value(this.value.map(function(a){return a.eval(b)}))},toCSS:function(a){return this.value.map(function(b){return b.toCSS(a)}).join(a.compress?",":", ")}}}(c("less/tree")),function(a){a.Variable=function(a,b){this.name=a,this
-.index=b},a.Variable.prototype={eval:function(b){var c,d,e=this.name;e.indexOf("@@")==0&&(e="@"+(new a.Variable(e.slice(1))).eval(b).value);if(c=a.find(b.frames,function(a){if(d=a.variable(e))return d.value.eval(b)}))return c;throw{message:"variable "+e+" is undefined",index:this.index}}}}(c("less/tree")),c("less/tree").find=function(a,b){for(var c=0,d;c<a.length;c++)if(d=b.call(a,a[c]))return d;return null},c("less/tree").jsify=function(a){return Array.isArray(a.value)&&a.value.length>1?"["+a.value.map(function(a){return a.toCSS(!1)}).join(", ")+"]":a.toCSS(!1)};var g=location.protocol==="file:"||location.protocol==="chrome:"||location.protocol==="chrome-extension:"||location.protocol==="resource:";d.env=d.env||(location.hostname=="127.0.0.1"||location.hostname=="0.0.0.0"||location.hostname=="localhost"||location.port.length>0||g?"development":"production"),d.async=!1,d.poll=d.poll||(g?1e3:1500),d.watch=function(){return this.watchMode=!0},d.unwatch=function(){return this.watchMode=!1},d.env==="development"?(d.optimization=0,/!watch/.test(location.hash)&&d.watch(),d.watchTimer=setInterval(function(){d.watchMode&&n(function(a,b,c){a&&q(a.toCSS(),b,c.lastModified)})},d.poll)):d.optimization=3;var h;try{h=typeof a.localStorage=="undefined"?null:a.localStorage}catch(i){h=null}var j=document.getElementsByTagName("link"),k=/^text\/(x-)?less$/;d.sheets=[];for(var l=0;l<j.length;l++)(j[l].rel==="stylesheet/less"||j[l].rel.match(/stylesheet/)&&j[l].type.match(k))&&d.sheets.push(j[l]);d.refresh=function(a){var b,c;b=c=new Date,n(function(a,d,e){e.local?u("loading "+d.href+" from cache."):(u("parsed "+d.href+" successfully."),q(a.toCSS(),d,e.lastModified)),u("css for "+d.href+" generated in "+(new Date-c)+"ms"),e.remaining===0&&u("css generated in "+(new Date-b)+"ms"),c=new Date},a),m()},d.refreshStyles=m,d.refresh(d.env==="development")})(window) \ No newline at end of file
+/*!
+ * LESS - Leaner CSS v1.7.0
+ * http://lesscss.org
+ *
+ * Copyright (c) 2009-2014, Alexis Sellier <self@cloudhead.net>
+ * Licensed under the Apache v2 License.
+ *
+ */
+
+ /** * @license Apache v2
+ */
+
+!function(a,b){function c(b){return a.less[b.split("/")[1]]}function d(a,b){"undefined"!=typeof console&&w.logLevel>=b&&console.log("less: "+a)}function e(a){return a.replace(/^[a-z-]+:\/+?[^\/]+/,"").replace(/^\//,"").replace(/\.[a-zA-Z]+$/,"").replace(/[^\.\w-]+/g,"-").replace(/\./g,":")}function f(a,c){var e="{line} {content}",f=a.filename||c,g=[],h=(a.type||"Syntax")+"Error: "+(a.message||"There is an error in your .less file")+" in "+f+" ",i=function(a,c,d){a.extract[c]!==b&&g.push(e.replace(/\{line\}/,(parseInt(a.line,10)||0)+(c-1)).replace(/\{class\}/,d).replace(/\{content\}/,a.extract[c]))};a.extract?(i(a,0,""),i(a,1,"line"),i(a,2,""),h+="on line "+a.line+", column "+(a.column+1)+":\n"+g.join("\n")):a.stack&&(h+=a.stack),d(h,z.errors)}function g(a,b,c){var f=b.href||"",g="less:"+(b.title||e(f)),h=document.getElementById(g),i=!1,j=document.createElement("style");if(j.setAttribute("type","text/css"),b.media&&j.setAttribute("media",b.media),j.id=g,j.styleSheet)try{j.styleSheet.cssText=a}catch(k){throw new Error("Couldn't reassign styleSheet.cssText.")}else j.appendChild(document.createTextNode(a)),i=null!==h&&h.childNodes.length>0&&j.childNodes.length>0&&h.firstChild.nodeValue===j.firstChild.nodeValue;var l=document.getElementsByTagName("head")[0];if(null===h||i===!1){var m=b&&b.nextSibling||null;m?m.parentNode.insertBefore(j,m):l.appendChild(j)}if(h&&i===!1&&h.parentNode.removeChild(h),c&&D){d("saving "+f+" to cache.",z.info);try{D.setItem(f,a),D.setItem(f+":timestamp",c)}catch(k){d("failed to save",z.errors)}}}function h(a){return w.postProcessor&&"function"==typeof w.postProcessor&&(a=w.postProcessor.call(a,a)||a),a}function i(a,c){var d,f,h="less-error-message:"+e(c||""),i='<li><label>{line}</label><pre class="{class}">{content}</pre></li>',j=document.createElement("div"),k=[],l=a.filename||c,m=l.match(/([^\/]+(\?.*)?)$/)[1];j.id=h,j.className="less-error-message",f="<h3>"+(a.type||"Syntax")+"Error: "+(a.message||"There is an error in your .less file")+'</h3><p>in <a href="'+l+'">'+m+"</a> ";var n=function(a,c,d){a.extract[c]!==b&&k.push(i.replace(/\{line\}/,(parseInt(a.line,10)||0)+(c-1)).replace(/\{class\}/,d).replace(/\{content\}/,a.extract[c]))};a.extract?(n(a,0,""),n(a,1,"line"),n(a,2,""),f+="on line "+a.line+", column "+(a.column+1)+":</p><ul>"+k.join("")+"</ul>"):a.stack&&(f+="<br/>"+a.stack.split("\n").slice(1).join("<br/>")),j.innerHTML=f,g([".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #dd6666;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.line {","color: #ff0000;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),j.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),"development"==w.env&&(d=setInterval(function(){document.body&&(document.getElementById(h)?document.body.replaceChild(j,document.getElementById(h)):document.body.insertBefore(j,document.body.firstChild),clearInterval(d))},10))}function j(a,b){w.errorReporting&&"html"!==w.errorReporting?"console"===w.errorReporting?f(a,b):"function"==typeof w.errorReporting&&w.errorReporting("add",a,b):i(a,b)}function k(a){var b=document.getElementById("less-error-message:"+e(a));b&&b.parentNode.removeChild(b)}function l(){}function m(a){w.errorReporting&&"html"!==w.errorReporting?"console"===w.errorReporting?l(a):"function"==typeof w.errorReporting&&w.errorReporting("remove",a):k(a)}function n(a){for(var b,c=document.getElementsByTagName("style"),d=0;d<c.length;d++)if(b=c[d],b.type.match(C)){var e=new w.tree.parseEnv(w),f=b.innerHTML||"";e.filename=document.location.href.replace(/#.*$/,""),(a||w.globalVars)&&(e.useFileCache=!0);var g=function(a){return function(b,c){if(b)return j(b,"inline");var d=c.toCSS(w);a.type="text/css",a.styleSheet?a.styleSheet.cssText=d:a.innerHTML=d}}(b);new w.Parser(e).parse(f,g,{globalVars:w.globalVars,modifyVars:a})}}function o(a,b){var c,d,e=/^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/i,f=a.match(e),g={},h=[];if(!f)throw new Error("Could not parse sheet href - '"+a+"'");if(!f[1]||f[2]){if(d=b.match(e),!d)throw new Error("Could not parse page url - '"+b+"'");f[1]=f[1]||d[1]||"",f[2]||(f[3]=d[3]+f[3])}if(f[3]){for(h=f[3].replace(/\\/g,"/").split("/"),c=0;c<h.length;c++)"."===h[c]&&(h.splice(c,1),c-=1);for(c=0;c<h.length;c++)".."===h[c]&&c>0&&(h.splice(c-1,2),c-=2)}return g.hostPart=f[1],g.directories=h,g.path=f[1]+h.join("/"),g.fileUrl=g.path+(f[4]||""),g.url=g.fileUrl+(f[5]||""),g}function p(a,b){var c,d,e,f,g=o(a),h=o(b),i="";if(g.hostPart!==h.hostPart)return"";for(d=Math.max(h.directories.length,g.directories.length),c=0;d>c&&h.directories[c]===g.directories[c];c++);for(f=h.directories.slice(c),e=g.directories.slice(c),c=0;c<f.length-1;c++)i+="../";for(c=0;c<e.length-1;c++)i+=e[c]+"/";return i}function q(){if(a.XMLHttpRequest&&("file:"!==a.location.protocol||!a.ActiveXObject))return new XMLHttpRequest;try{return new ActiveXObject("Microsoft.XMLHTTP")}catch(b){return d("browser doesn't support AJAX.",z.errors),null}}function r(a,b,c,e){function f(b,c,d){b.status>=200&&b.status<300?c(b.responseText,b.getResponseHeader("Last-Modified")):"function"==typeof d&&d(b.status,a)}var g=q(),h=y?w.fileAsync:w.async;"function"==typeof g.overrideMimeType&&g.overrideMimeType("text/css"),d("XHR: Getting '"+a+"'",z.debug),g.open("GET",a,h),g.setRequestHeader("Accept",b||"text/x-less, text/css; q=0.9, */*; q=0.5"),g.send(null),y&&!w.fileAsync?0===g.status||g.status>=200&&g.status<300?c(g.responseText):e(g.status,a):h?g.onreadystatechange=function(){4==g.readyState&&f(g,c,e)}:f(g,c,e)}function s(b,c,d,e){c&&c.currentDirectory&&!/^([a-z-]+:)?\//.test(b)&&(b=c.currentDirectory+b);var f=o(b,a.location.href),g=f.url,h={currentDirectory:f.path,filename:g};if(c?(h.entryPath=c.entryPath,h.rootpath=c.rootpath,h.rootFilename=c.rootFilename,h.relativeUrls=c.relativeUrls):(h.entryPath=f.path,h.rootpath=w.rootpath||f.path,h.rootFilename=g,h.relativeUrls=e.relativeUrls),h.relativeUrls&&(h.rootpath=e.rootpath?o(e.rootpath+p(f.path,h.entryPath)).path:f.path),e.useFileCache&&E[g])try{var i=E[g];d(null,i,g,h,{lastModified:new Date})}catch(j){d(j,null,g)}else r(g,e.mime,function(a,b){E[g]=a;try{d(null,a,g,h,{lastModified:b})}catch(c){d(c,null,g)}},function(a,b){d({type:"File",message:"'"+b+"' wasn't found ("+a+")"},null,g)})}function t(a,b,c,d,e){var f=new w.tree.parseEnv(w);f.mime=a.type,(e||w.globalVars)&&(f.useFileCache=!0),s(a.href,null,function(h,i,j,k,l){if(l){l.remaining=d;var n=D&&D.getItem(j),o=D&&D.getItem(j+":timestamp");if(!c&&o&&l.lastModified&&new Date(l.lastModified).valueOf()===new Date(o).valueOf())return g(n,a),l.local=!0,void b(null,null,i,a,l,j)}m(j),i?(f.currentFileInfo=k,new w.Parser(f).parse(i,function(c,d){if(c)return b(c,null,null,a);try{b(c,d,i,a,l,j)}catch(c){b(c,null,null,a)}},{modifyVars:e,globalVars:w.globalVars})):b(h,null,null,a,l,j)},f,e)}function u(a,b,c){for(var d=0;d<w.sheets.length;d++)t(w.sheets[d],a,b,w.sheets.length-(d+1),c)}function v(){"development"===w.env?(w.optimization=0,w.watchTimer=setInterval(function(){w.watchMode&&u(function(a,b,c,d,e){if(a)j(a,d.href);else if(b){var f=b.toCSS(w);f=h(f),g(f,d,e.lastModified)}})},w.poll)):w.optimization=3}("undefined"==typeof a.less||"undefined"!=typeof a.less.nodeType)&&(a.less={}),w=a.less,x=a.less.tree={},w.mode="browser";var w,x;w===b&&(w=exports,x=c("./tree"),w.mode="node"),w.Parser=function(a){function d(){D=y,G.push({current:C,i:y,j:z})}function e(){var a=G.pop();C=a.current,D=y=a.i,z=a.j}function f(){G.pop()}function g(){y>D&&(C=C.slice(y-D),D=y)}function h(a,b){var c=a.charCodeAt(0|b);return 32>=c&&(32===c||10===c||9===c)}function i(a){var b,c,d=typeof a;return"string"===d?v.charAt(y)!==a?null:(l(1),a):(g(),(b=a.exec(C))?(c=b[0].length,l(c),"string"==typeof b?b:1===b.length?b[0]:b):null)}function j(a){y>D&&(C=C.slice(y-D),D=y);var b=a.exec(C);return b?(l(b[0].length),"string"==typeof b?b:1===b.length?b[0]:b):null}function k(a){return v.charAt(y)!==a?null:(l(1),a)}function l(a){for(var b,c=y,d=z,e=y-D,f=y+C.length-e,g=y+=a,h=v;f>y&&(b=h.charCodeAt(y),!(b>32))&&(32===b||10===b||9===b||13===b);y++);return C=C.slice(a+y-g+e),D=y,!C.length&&z<B.length-1?(C=B[++z],l(0),!0):c!==y||d!==z}function m(a,b){var c="[object Function]"===Object.prototype.toString.call(a)?a.call(F):i(a);return c?c:void o(b||("string"==typeof a?"expected '"+a+"' got '"+v.charAt(y)+"'":"unexpected token"))}function n(a,b){return v.charAt(y)===a?(l(1),a):void o(b||"expected '"+a+"' got '"+v.charAt(y)+"'")}function o(a,b){var c=new Error(a);throw c.index=y,c.type=b||"Syntax",c}function p(a){return"string"==typeof a?v.charAt(y)===a:a.test(C)}function q(a){return v.charAt(y)===a}function r(a,b){return a.filename&&b.currentFileInfo.filename&&a.filename!==b.currentFileInfo.filename?E.imports.contents[a.filename]:v}function s(a,b){for(var c=a+1,d=null,e=-1;--c>=0&&"\n"!==b.charAt(c);)e++;return"number"==typeof a&&(d=(b.slice(0,a).match(/\n/g)||"").length),{line:d,column:e}}function t(a,b,d){var e=d.currentFileInfo.filename;return"browser"!==w.mode&&"rhino"!==w.mode&&(e=c("path").resolve(e)),{lineNumber:s(a,b).line+1,fileName:e}}function u(a,b){var c=r(a,b),d=s(a.index,c),e=d.line,f=d.column,g=a.call&&s(a.call,c).line,h=c.split("\n");this.type=a.type||"Syntax",this.message=a.message,this.filename=a.filename||b.currentFileInfo.filename,this.index=a.index,this.line="number"==typeof e?e+1:null,this.callLine=g+1,this.callExtract=h[g],this.stack=a.stack,this.column=f,this.extract=[h[e-1],h[e],h[e+1]]}var v,y,z,A,B,C,D,E,F,G=[],H=a&&a.filename;a instanceof x.parseEnv||(a=new x.parseEnv(a));var I=this.imports={paths:a.paths||[],queue:[],files:a.files,contents:a.contents,contentsIgnoredChars:a.contentsIgnoredChars,mime:a.mime,error:null,push:function(b,c,d,e){var f=this;this.queue.push(b);var g=function(a,c,d){f.queue.splice(f.queue.indexOf(b),1);var g=d===H;f.files[d]=c,a&&!f.error&&(f.error=a),e(a,c,g,d)};w.Parser.importer?w.Parser.importer(b,c,g,a):w.Parser.fileLoader(b,c,function(b,e,f,h){if(b)return void g(b);var i=new x.parseEnv(a);i.currentFileInfo=h,i.processImports=!1,i.contents[f]=e,(c.reference||d.reference)&&(h.reference=!0),d.inline?g(null,e,f):new w.Parser(i).parse(e,function(a,b){g(a,b,f)})},a)}},J=j;return u.prototype=new Error,u.prototype.constructor=u,this.env=a=a||{},this.optimization="optimization"in this.env?this.env.optimization:1,E={imports:I,parse:function(d,e,f){var g,h,i,j,k,l=null,m="";if(y=z=D=A=0,j=f&&f.globalVars?w.Parser.serializeVars(f.globalVars)+"\n":"",k=f&&f.modifyVars?"\n"+w.Parser.serializeVars(f.modifyVars):"",(j||f&&f.banner)&&(m=(f&&f.banner?f.banner:"")+j,E.imports.contentsIgnoredChars[a.currentFileInfo.filename]=m.length),d=d.replace(/\r\n/g,"\n"),v=d=m+d.replace(/^\uFEFF/,"")+k,E.imports.contents[a.currentFileInfo.filename]=d,B=function(b){function c(b,c){l=new u({index:c||i,type:"Parse",message:b,filename:a.currentFileInfo.filename},a)}function d(a){var c=i-s;512>c&&!a||!c||(r.push(b.slice(s,i+1)),s=i+1)}var e,f,g,h,i,j,k,m,n,o=b.length,p=0,q=0,r=[],s=0;for(i=0;o>i;i++)if(k=b.charCodeAt(i),!(k>=97&&122>=k||34>k))switch(k){case 40:q++,f=i;continue;case 41:if(--q<0)return c("missing opening `(`");continue;case 59:q||d();continue;case 123:p++,e=i;continue;case 125:if(--p<0)return c("missing opening `{`");p||q||d();continue;case 92:if(o-1>i){i++;continue}return c("unescaped `\\`");case 34:case 39:case 96:for(n=0,j=i,i+=1;o>i;i++)if(m=b.charCodeAt(i),!(m>96)){if(m==k){n=1;break}if(92==m){if(i==o-1)return c("unescaped `\\`");i++}}if(n)continue;return c("unmatched `"+String.fromCharCode(k)+"`",j);case 47:if(q||i==o-1)continue;if(m=b.charCodeAt(i+1),47==m)for(i+=2;o>i&&(m=b.charCodeAt(i),!(13>=m)||10!=m&&13!=m);i++);else if(42==m){for(g=j=i,i+=2;o-1>i&&(m=b.charCodeAt(i),125==m&&(h=i),42!=m||47!=b.charCodeAt(i+1));i++);if(i==o-1)return c("missing closing `*/`",j);i++}continue;case 42:if(o-1>i&&47==b.charCodeAt(i+1))return c("unmatched `/*`");continue}return 0!==p?g>e&&h>g?c("missing closing `}` or `*/`",e):c("missing closing `}`",e):0!==q?c("missing closing `)`",f):(d(!0),r)}(d),l)return e(new u(l,a));C=B[0];try{g=new x.Ruleset(null,this.parsers.primary()),g.root=!0,g.firstRoot=!0}catch(n){return e(new u(n,a))}if(g.toCSS=function(d){return function(e,f){e=e||{};var g,h,i=new x.evalEnv(e);"object"!=typeof f||Array.isArray(f)||(f=Object.keys(f).map(function(a){var b=f[a];return b instanceof x.Value||(b instanceof x.Expression||(b=new x.Expression([b])),b=new x.Value([b])),new x.Rule("@"+a,b,!1,null,0)}),i.frames=[new x.Ruleset(null,f)]);try{var j,k=[],l=[new x.joinSelectorVisitor,new x.processExtendsVisitor,new x.toCSSVisitor({compress:Boolean(e.compress)})],m=this;if(e.plugins)for(j=0;j<e.plugins.length;j++)e.plugins[j].isPreEvalVisitor?k.push(e.plugins[j]):e.plugins[j].isPreVisitor?l.splice(0,0,e.plugins[j]):l.push(e.plugins[j]);for(j=0;j<k.length;j++)k[j].run(m);for(g=d.call(m,i),j=0;j<l.length;j++)l[j].run(g);e.sourceMap&&(g=new x.sourceMapOutput({contentsIgnoredCharsMap:E.imports.contentsIgnoredChars,writeSourceMap:e.writeSourceMap,rootNode:g,contentsMap:E.imports.contents,sourceMapFilename:e.sourceMapFilename,sourceMapURL:e.sourceMapURL,outputFilename:e.sourceMapOutputFilename,sourceMapBasepath:e.sourceMapBasepath,sourceMapRootpath:e.sourceMapRootpath,outputSourceFiles:e.outputSourceFiles,sourceMapGenerator:e.sourceMapGenerator})),h=g.toCSS({compress:Boolean(e.compress),dumpLineNumbers:a.dumpLineNumbers,strictUnits:Boolean(e.strictUnits),numPrecision:8})}catch(n){throw new u(n,a)}if(e.cleancss&&"node"===w.mode){var o=c("clean-css"),p=e.cleancssOptions||{};return p.keepSpecialComments===b&&(p.keepSpecialComments="*"),p.processImport=!1,p.noRebase=!0,p.noAdvanced===b&&(p.noAdvanced=!0),new o(p).minify(h)}return e.compress?h.replace(/(^(\s)+)|((\s)+$)/g,""):h}}(g.eval),y<v.length-1){y=A;var o=s(y,v);i=v.split("\n"),h=o.line+1,l={type:"Parse",message:"Unrecognised input",index:y,filename:a.currentFileInfo.filename,line:h,column:o.column,extract:[i[h-2],i[h-1],i[h]]}}var p=function(b){return b=l||b||E.imports.error,b?(b instanceof u||(b=new u(b,a)),e(b)):e(null,g)};return a.processImports===!1?p():void new x.importVisitor(this.imports,p).run(g)},parsers:F={primary:function(){for(var a,b=this.mixin,c=J,d=[];C;){if(a=this.extendRule()||b.definition()||this.rule()||this.ruleset()||b.call()||this.comment()||this.rulesetCall()||this.directive())d.push(a);else if(!c(/^[\s\n]+/)&&!c(/^;+/))break;if(q("}"))break}return d},comment:function(){var b;if("/"===v.charAt(y))return"/"===v.charAt(y+1)?new x.Comment(j(/^\/\/.*/),!0,y,a.currentFileInfo):(b=j(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/),b?new x.Comment(b,!1,y,a.currentFileInfo):void 0)},comments:function(){for(var a,b=[];;){if(a=this.comment(),!a)break;b.push(a)}return b},entities:{quoted:function(){var b,c,d=y,e=y;return"~"===v.charAt(d)&&(d++,c=!0),'"'===v.charAt(d)||"'"===v.charAt(d)?(c&&k("~"),b=j(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/),b?new x.Quoted(b[0],b[1]||b[2],c,e,a.currentFileInfo):void 0):void 0},keyword:function(){var a;if(a=j(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/)){var b=x.Color.fromKeyword(a);return b?b:new x.Keyword(a)}},call:function(){var b,c,d,e,f=y;if(b=/^([\w-]+|%|progid:[\w\.]+)\(/.exec(C)){if(b=b[1],c=b.toLowerCase(),"url"===c)return null;if(y+=b.length,"alpha"===c&&(e=F.alpha(),"undefined"!=typeof e))return e;if(k("("),d=this.arguments(),k(")"))return b?new x.Call(b,d,f,a.currentFileInfo):void 0}},arguments:function(){for(var a,b=[];;){if(a=this.assignment()||F.expression(),!a)break;if(b.push(a),!k(","))break}return b},literal:function(){return this.dimension()||this.color()||this.quoted()||this.unicodeDescriptor()},assignment:function(){var a,b;return a=j(/^\w+(?=\s?=)/i),a&&k("=")?(b=F.entity(),b?new x.Assignment(a,b):void 0):void 0},url:function(){var b;if("u"===v.charAt(y)&&j(/^url\(/))return b=this.quoted()||this.variable()||j(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/)||"",n(")"),new x.URL(null!=b.value||b instanceof x.Variable?b:new x.Anonymous(b),a.currentFileInfo)},variable:function(){var b,c=y;return"@"===v.charAt(y)&&(b=j(/^@@?[\w-]+/))?new x.Variable(b,c,a.currentFileInfo):void 0},variableCurly:function(){var b,c=y;return"@"===v.charAt(y)&&(b=j(/^@\{([\w-]+)\}/))?new x.Variable("@"+b[1],c,a.currentFileInfo):void 0},color:function(){var a;return"#"===v.charAt(y)&&(a=j(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))?new x.Color(a[1]):void 0},dimension:function(){var a,b=v.charCodeAt(y);if(!(b>57||43>b||47===b||44==b))return a=j(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/),a?new x.Dimension(a[1],a[2]):void 0},unicodeDescriptor:function(){var a;return a=j(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/),a?new x.UnicodeDescriptor(a[0]):void 0},javascript:function(){var c,d,e=y;return"~"===v.charAt(e)&&(e++,d=!0),"`"===v.charAt(e)?(a.javascriptEnabled===b||a.javascriptEnabled||o("You are using JavaScript, which has been disabled."),d&&k("~"),c=j(/^`([^`]*)`/),c?new x.JavaScript(c[1],y,d):void 0):void 0}},variable:function(){var a;return"@"===v.charAt(y)&&(a=j(/^(@[\w-]+)\s*:/))?a[1]:void 0},rulesetCall:function(){var a;return"@"===v.charAt(y)&&(a=j(/^(@[\w-]+)\s*\(\s*\)\s*;/))?new x.RulesetCall(a[1]):void 0},extend:function(a){var b,c,d,e,f,g=y;if(j(a?/^&:extend\(/:/^:extend\(/)){do{for(d=null,b=null;!(d=j(/^(all)(?=\s*(\)|,))/))&&(c=this.element());)b?b.push(c):b=[c];d=d&&d[1],f=new x.Extend(new x.Selector(b),d,g),e?e.push(f):e=[f]}while(k(","));return m(/^\)/),a&&m(/^;/),e}},extendRule:function(){return this.extend(!0)},mixin:{call:function(){var b,c,g,h,i,l,m=v.charAt(y),o=!1,p=y;if("."===m||"#"===m){for(d();;){if(b=y,h=j(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/),!h)break;g=new x.Element(i,h,b,a.currentFileInfo),c?c.push(g):c=[g],i=k(">")}return c&&(k("(")&&(l=this.args(!0).args,n(")")),F.important()&&(o=!0),F.end())?(f(),new x.mixin.Call(c,l,p,a.currentFileInfo,o)):void e()}},args:function(a){var b,c,g,h,i,l,m=E.parsers,n=m.entities,p={args:null,variadic:!1},q=[],r=[],s=[];for(d();;){if(a)l=m.detachedRuleset()||m.expression();else{if(m.comments(),"."===v.charAt(y)&&j(/^\.{3}/)){p.variadic=!0,k(";")&&!b&&(b=!0),(b?r:s).push({variadic:!0});break}l=n.variable()||n.literal()||n.keyword()}if(!l)break;h=null,l.throwAwayComments&&l.throwAwayComments(),i=l;var t=null;if(a?l.value&&1==l.value.length&&(t=l.value[0]):t=l,t&&t instanceof x.Variable)if(k(":")){if(q.length>0&&(b&&o("Cannot mix ; and , as delimiter types"),c=!0),i=a&&m.detachedRuleset()||m.expression(),!i){if(!a)return e(),p.args=[],p;o("could not understand value for named argument")}h=g=t.name}else{if(!a&&j(/^\.{3}/)){p.variadic=!0,k(";")&&!b&&(b=!0),(b?r:s).push({name:l.name,variadic:!0});break}a||(g=h=t.name,i=null)}i&&q.push(i),s.push({name:h,value:i}),k(",")||(k(";")||b)&&(c&&o("Cannot mix ; and , as delimiter types"),b=!0,q.length>1&&(i=new x.Value(q)),r.push({name:g,value:i}),g=null,q=[],c=!1)}return f(),p.args=b?r:s,p},definition:function(){var a,b,c,g,h=[],i=!1;if(!("."!==v.charAt(y)&&"#"!==v.charAt(y)||p(/^[^{]*\}/)))if(d(),b=j(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)){a=b[1];var l=this.args(!1);if(h=l.args,i=l.variadic,!k(")"))return A=y,void e();if(F.comments(),j(/^when/)&&(g=m(F.conditions,"expected condition")),c=F.block())return f(),new x.mixin.Definition(a,h,c,g,i);e()}else f()}},entity:function(){var a=this.entities;return a.literal()||a.variable()||a.url()||a.call()||a.keyword()||a.javascript()||this.comment()},end:function(){return k(";")||q("}")},alpha:function(){var a;if(j(/^\(opacity=/i))return a=j(/^\d+/)||this.entities.variable(),a?(n(")"),new x.Alpha(a)):void 0},element:function(){var b,c,g,h=y;return c=this.combinator(),b=j(/^(?:\d+\.\d+|\d+)%/)||j(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)||k("*")||k("&")||this.attribute()||j(/^\([^()@]+\)/)||j(/^[\.#](?=@)/)||this.entities.variableCurly(),b||(d(),k("(")?(g=this.selector())&&k(")")?(b=new x.Paren(g),f()):e():f()),b?new x.Element(c,b,h,a.currentFileInfo):void 0},combinator:function(){var a=v.charAt(y);if(">"===a||"+"===a||"~"===a||"|"===a||"^"===a){for(y++,"^"===v.charAt(y)&&(a="^^",y++);h(v,y);)y++;return new x.Combinator(a)}return new x.Combinator(h(v,y-1)?" ":null)},lessSelector:function(){return this.selector(!0)},selector:function(b){for(var c,d,e,f,g,h,i,j=y,k=J;(b&&(g=this.extend())||b&&(h=k(/^when/))||(f=this.element()))&&(h?i=m(this.conditions,"expected condition"):i?o("CSS guard can only be used at the end of selector"):g?d?d.push(g):d=[g]:(d&&o("Extend can only be used at the end of selector"),e=v.charAt(y),c?c.push(f):c=[f],f=null),"{"!==e&&"}"!==e&&";"!==e&&","!==e&&")"!==e););return c?new x.Selector(c,d,i,j,a.currentFileInfo):void(d&&o("Extend must be used to extend a selector, it cannot be used on its own"))},attribute:function(){if(k("[")){var a,b,c,d=this.entities;return(a=d.variableCurly())||(a=m(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/)),c=j(/^[|~*$^]?=/),c&&(b=d.quoted()||j(/^[0-9]+%/)||j(/^[\w-]+/)||d.variableCurly()),n("]"),new x.Attribute(a,c,b)}},block:function(){var a;return k("{")&&(a=this.primary())&&k("}")?a:void 0},blockRuleset:function(){var a=this.block();return a&&(a=new x.Ruleset(null,a)),a},detachedRuleset:function(){var a=this.blockRuleset();return a?new x.DetachedRuleset(a):void 0},ruleset:function(){var b,c,g,h;for(d(),a.dumpLineNumbers&&(h=t(y,v,a));;){if(c=this.lessSelector(),!c)break;if(b?b.push(c):b=[c],this.comments(),c.condition&&b.length>1&&o("Guards are only currently allowed on a single selector."),!k(","))break;c.condition&&o("Guards are only currently allowed on a single selector."),this.comments()}if(b&&(g=this.block())){f();var i=new x.Ruleset(b,g,a.strictImports);return a.dumpLineNumbers&&(i.debugInfo=h),i}A=y,e()},rule:function(b){var c,g,h,i,j,k=y,l=v.charAt(k);if("."!==l&&"#"!==l&&"&"!==l)if(d(),c=this.variable()||this.ruleProperty()){if(j="string"==typeof c,j&&(g=this.detachedRuleset()),g||(g=b||!a.compress&&!j?this.anonymousValue()||this.value():this.value()||this.anonymousValue(),h=this.important(),i=!j&&c.pop().value),g&&this.end())return f(),new x.Rule(c,g,h,i,k,a.currentFileInfo);if(A=y,e(),g&&!b)return this.rule(!0)}else f()},anonymousValue:function(){var a;return a=/^([^@+\/'"*`(;{}-]*);/.exec(C),a?(y+=a[0].length-1,new x.Anonymous(a[1])):void 0},"import":function(){var b,c,g=y;d();var h=j(/^@import?\s+/),i=(h?this.importOptions():null)||{};return h&&(b=this.entities.quoted()||this.entities.url())&&(c=this.mediaFeatures(),k(";"))?(f(),c=c&&new x.Value(c),new x.Import(b,c,i,g,a.currentFileInfo)):void e()},importOptions:function(){var a,b,c,d={};if(!k("("))return null;do if(a=this.importOption()){switch(b=a,c=!0,b){case"css":b="less",c=!1;break;case"once":b="multiple",c=!1}if(d[b]=c,!k(","))break}while(a);return n(")"),d},importOption:function(){var a=j(/^(less|css|multiple|once|inline|reference)/);return a?a[1]:void 0},mediaFeature:function(){var b,c,d=this.entities,e=[];do if(b=d.keyword()||d.variable())e.push(b);else if(k("(")){if(c=this.property(),b=this.value(),!k(")"))return null;if(c&&b)e.push(new x.Paren(new x.Rule(c,b,null,null,y,a.currentFileInfo,!0)));else{if(!b)return null;e.push(new x.Paren(b))}}while(b);return e.length>0?new x.Expression(e):void 0},mediaFeatures:function(){var a,b=this.entities,c=[];do if(a=this.mediaFeature()){if(c.push(a),!k(","))break}else if(a=b.variable(),a&&(c.push(a),!k(",")))break;while(a);return c.length>0?c:null},media:function(){var b,c,d,e;return a.dumpLineNumbers&&(e=t(y,v,a)),j(/^@media/)&&(b=this.mediaFeatures(),c=this.block())?(d=new x.Media(c,b,y,a.currentFileInfo),a.dumpLineNumbers&&(d.debugInfo=e),d):void 0},directive:function(){var b,c,g,h,i,l,m,n=y,p=!0;if("@"===v.charAt(y)){if(c=this["import"]()||this.media())return c;if(d(),b=j(/^@[a-z-]+/)){switch(h=b,"-"==b.charAt(1)&&b.indexOf("-",2)>0&&(h="@"+b.slice(b.indexOf("-",2)+1)),h){case"@charset":i=!0,p=!1;break;case"@namespace":l=!0,p=!1;break;case"@keyframes":i=!0;break;case"@host":case"@page":case"@document":case"@supports":m=!0}return i?(c=this.entity(),c||o("expected "+b+" identifier")):l?(c=this.expression(),c||o("expected "+b+" expression")):m&&(c=(j(/^[^{;]+/)||"").trim(),c&&(c=new x.Anonymous(c))),p&&(g=this.blockRuleset()),g||!p&&c&&k(";")?(f(),new x.Directive(b,c,g,n,a.currentFileInfo,a.dumpLineNumbers?t(n,v,a):null)):void e()}}},value:function(){var a,b=[];do if(a=this.expression(),a&&(b.push(a),!k(",")))break;while(a);return b.length>0?new x.Value(b):void 0},important:function(){return"!"===v.charAt(y)?j(/^! *important/):void 0},sub:function(){var a,b;return k("(")&&(a=this.addition())?(b=new x.Expression([a]),n(")"),b.parens=!0,b):void 0},multiplication:function(){var a,b,c,d,e;if(a=this.operand()){for(e=h(v,y-1);;){if(p(/^\/[*\/]/))break;if(c=k("/")||k("*"),!c)break;if(b=this.operand(),!b)break;a.parensInOp=!0,b.parensInOp=!0,d=new x.Operation(c,[d||a,b],e),e=h(v,y-1)}return d||a}},addition:function(){var a,b,c,d,e;if(a=this.multiplication()){for(e=h(v,y-1);;){if(c=j(/^[-+]\s+/)||!e&&(k("+")||k("-")),!c)break;if(b=this.multiplication(),!b)break;a.parensInOp=!0,b.parensInOp=!0,d=new x.Operation(c,[d||a,b],e),e=h(v,y-1)}return d||a}},conditions:function(){var a,b,c,d=y;if(a=this.condition()){for(;;){if(!p(/^,\s*(not\s*)?\(/)||!k(","))break;if(b=this.condition(),!b)break;c=new x.Condition("or",c||a,b,d)}return c||a}},condition:function(){var a,b,c,d,e=this.entities,f=y,g=!1;return j(/^not/)&&(g=!0),n("("),a=this.addition()||e.keyword()||e.quoted(),a?(d=j(/^(?:>=|<=|=<|[<=>])/),d?(b=this.addition()||e.keyword()||e.quoted(),b?c=new x.Condition(d,a,b,f,g):o("expected expression")):c=new x.Condition("=",a,new x.Keyword("true"),f,g),n(")"),j(/^and/)?new x.Condition("and",c,this.condition()):c):void 0},operand:function(){var a,b=this.entities,c=v.charAt(y+1);"-"!==v.charAt(y)||"@"!==c&&"("!==c||(a=k("-"));var d=this.sub()||b.dimension()||b.color()||b.variable()||b.call();return a&&(d.parensInOp=!0,d=new x.Negative(d)),d},expression:function(){var a,b,c=[];do a=this.addition()||this.entity(),a&&(c.push(a),p(/^\/[\/*]/)||(b=k("/"),b&&c.push(new x.Anonymous(b))));while(a);return c.length>0?new x.Expression(c):void 0},property:function(){var a=j(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);return a?a[1]:void 0},ruleProperty:function(){function b(a){var b=a.exec(e);return b?(g.push(y+h),h+=b[0].length,e=e.slice(b[1].length),f.push(b[1])):void 0}var c,d,e=C,f=[],g=[],h=0;for(b(/^(\*?)/);b(/^((?:[\w-]+)|(?:@\{[\w-]+\}))/););if(f.length>1&&b(/^\s*((?:\+_|\+)?)\s*:/)){for(l(h),""===f[0]&&(f.shift(),g.shift()),d=0;d<f.length;d++)c=f[d],f[d]="@"!==c.charAt(0)?new x.Keyword(c):new x.Variable("@"+c.slice(2,-1),g[d],a.currentFileInfo);return f}}}}},w.Parser.serializeVars=function(a){var b="";for(var c in a)if(Object.hasOwnProperty.call(a,c)){var d=a[c];b+=("@"===c[0]?"":"@")+c+": "+d+(";"===(""+d).slice(-1)?"":";")}return b},function(d){function e(a,b,c){if(!(c instanceof d.Dimension))throw{type:"Argument",message:"argument must be a number"};return null==b?b=c.unit:c=c.unify(),new d.Dimension(a(parseFloat(c.value)),b)}function f(a,b,c){var e,f,g,h,i=b.alpha,j=c.alpha,k=[];g=j+i*(1-j);for(var l=0;3>l;l++)e=b.rgb[l]/255,f=c.rgb[l]/255,h=a(e,f),g&&(h=(j*f+i*(e-j*(e+f-h)))/g),k[l]=255*h;return new d.Color(k,g)}function g(){var a,b=d.functions;for(a in l)l.hasOwnProperty(a)&&(b[a]=e.bind(null,Math[a],l[a]));for(a in m)m.hasOwnProperty(a)&&(b[a]=f.bind(null,m[a]));a=d.defaultFunc,b["default"]=a.eval.bind(a)}function h(a){return d.functions.hsla(a.h,a.s,a.l,a.a)}function i(a,b){return a instanceof d.Dimension&&a.unit.is("%")?parseFloat(a.value*b/100):j(a)}function j(a){if(a instanceof d.Dimension)return parseFloat(a.unit.is("%")?a.value/100:a.value);if("number"==typeof a)return a;throw{error:"RuntimeError",message:"color functions take numbers as parameters"}}function k(a){return Math.min(1,Math.max(0,a))}d.functions={rgb:function(a,b,c){return this.rgba(a,b,c,1)},rgba:function(a,b,c,e){var f=[a,b,c].map(function(a){return i(a,255)});return e=j(e),new d.Color(f,e)},hsl:function(a,b,c){return this.hsla(a,b,c,1)},hsla:function(a,b,c,d){function e(a){return a=0>a?a+1:a>1?a-1:a,1>6*a?g+(f-g)*a*6:1>2*a?f:2>3*a?g+(f-g)*(2/3-a)*6:g}a=j(a)%360/360,b=k(j(b)),c=k(j(c)),d=k(j(d));var f=.5>=c?c*(b+1):c+b-c*b,g=2*c-f;return this.rgba(255*e(a+1/3),255*e(a),255*e(a-1/3),d)},hsv:function(a,b,c){return this.hsva(a,b,c,1)},hsva:function(a,b,c,d){a=j(a)%360/360*360,b=j(b),c=j(c),d=j(d);var e,f;e=Math.floor(a/60%6),f=a/60-e;var g=[c,c*(1-b),c*(1-f*b),c*(1-(1-f)*b)],h=[[0,3,1],[2,0,1],[1,0,3],[1,2,0],[3,1,0],[0,1,2]];return this.rgba(255*g[h[e][0]],255*g[h[e][1]],255*g[h[e][2]],d)},hue:function(a){return new d.Dimension(Math.round(a.toHSL().h))},saturation:function(a){return new d.Dimension(Math.round(100*a.toHSL().s),"%")},lightness:function(a){return new d.Dimension(Math.round(100*a.toHSL().l),"%")},hsvhue:function(a){return new d.Dimension(Math.round(a.toHSV().h))},hsvsaturation:function(a){return new d.Dimension(Math.round(100*a.toHSV().s),"%")},hsvvalue:function(a){return new d.Dimension(Math.round(100*a.toHSV().v),"%")},red:function(a){return new d.Dimension(a.rgb[0])},green:function(a){return new d.Dimension(a.rgb[1])},blue:function(a){return new d.Dimension(a.rgb[2])},alpha:function(a){return new d.Dimension(a.toHSL().a)},luma:function(a){return new d.Dimension(Math.round(a.luma()*a.alpha*100),"%")},luminance:function(a){var b=.2126*a.rgb[0]/255+.7152*a.rgb[1]/255+.0722*a.rgb[2]/255;return new d.Dimension(Math.round(b*a.alpha*100),"%")},saturate:function(a,b){if(!a.rgb)return null;var c=a.toHSL();return c.s+=b.value/100,c.s=k(c.s),h(c)},desaturate:function(a,b){var c=a.toHSL();return c.s-=b.value/100,c.s=k(c.s),h(c)},lighten:function(a,b){var c=a.toHSL();return c.l+=b.value/100,c.l=k(c.l),h(c)},darken:function(a,b){var c=a.toHSL();return c.l-=b.value/100,c.l=k(c.l),h(c)},fadein:function(a,b){var c=a.toHSL();return c.a+=b.value/100,c.a=k(c.a),h(c)},fadeout:function(a,b){var c=a.toHSL();return c.a-=b.value/100,c.a=k(c.a),h(c)},fade:function(a,b){var c=a.toHSL();return c.a=b.value/100,c.a=k(c.a),h(c)},spin:function(a,b){var c=a.toHSL(),d=(c.h+b.value)%360;return c.h=0>d?360+d:d,h(c)},mix:function(a,b,c){c||(c=new d.Dimension(50));var e=c.value/100,f=2*e-1,g=a.toHSL().a-b.toHSL().a,h=((f*g==-1?f:(f+g)/(1+f*g))+1)/2,i=1-h,j=[a.rgb[0]*h+b.rgb[0]*i,a.rgb[1]*h+b.rgb[1]*i,a.rgb[2]*h+b.rgb[2]*i],k=a.alpha*e+b.alpha*(1-e);return new d.Color(j,k)},greyscale:function(a){return this.desaturate(a,new d.Dimension(100))},contrast:function(a,b,c,d){if(!a.rgb)return null;if("undefined"==typeof c&&(c=this.rgba(255,255,255,1)),"undefined"==typeof b&&(b=this.rgba(0,0,0,1)),b.luma()>c.luma()){var e=c;c=b,b=e}return d="undefined"==typeof d?.43:j(d),a.luma()<d?c:b},e:function(a){return new d.Anonymous(a instanceof d.JavaScript?a.evaluated:a)},escape:function(a){return new d.Anonymous(encodeURI(a.value).replace(/=/g,"%3D").replace(/:/g,"%3A").replace(/#/g,"%23").replace(/;/g,"%3B").replace(/\(/g,"%28").replace(/\)/g,"%29"))},replace:function(a,b,c,e){var f=a.value;return f=f.replace(new RegExp(b.value,e?e.value:""),c.value),new d.Quoted(a.quote||"",f,a.escaped)},"%":function(a){for(var b=Array.prototype.slice.call(arguments,1),c=a.value,e=0;e<b.length;e++)c=c.replace(/%[sda]/i,function(a){var c=a.match(/s/i)?b[e].value:b[e].toCSS();return a.match(/[A-Z]$/)?encodeURIComponent(c):c});return c=c.replace(/%%/g,"%"),new d.Quoted(a.quote||"",c,a.escaped)
+},unit:function(a,b){if(!(a instanceof d.Dimension))throw{type:"Argument",message:"the first argument to unit must be a number"+(a instanceof d.Operation?". Have you forgotten parenthesis?":"")};return b=b?b instanceof d.Keyword?b.value:b.toCSS():"",new d.Dimension(a.value,b)},convert:function(a,b){return a.convertTo(b.value)},round:function(a,b){var c="undefined"==typeof b?0:b.value;return e(function(a){return a.toFixed(c)},null,a)},pi:function(){return new d.Dimension(Math.PI)},mod:function(a,b){return new d.Dimension(a.value%b.value,a.unit)},pow:function(a,b){if("number"==typeof a&&"number"==typeof b)a=new d.Dimension(a),b=new d.Dimension(b);else if(!(a instanceof d.Dimension&&b instanceof d.Dimension))throw{type:"Argument",message:"arguments must be numbers"};return new d.Dimension(Math.pow(a.value,b.value),a.unit)},_minmax:function(a,c){switch(c=Array.prototype.slice.call(c),c.length){case 0:throw{type:"Argument",message:"one or more arguments required"}}var e,f,g,h,i,j,k,l,m=[],n={};for(e=0;e<c.length;e++)if(g=c[e],g instanceof d.Dimension)if(h=""===g.unit.toString()&&l!==b?new d.Dimension(g.value,l).unify():g.unify(),j=""===h.unit.toString()&&k!==b?k:h.unit.toString(),k=""!==j&&k===b||""!==j&&""===m[0].unify().unit.toString()?j:k,l=""!==j&&l===b?g.unit.toString():l,f=n[""]!==b&&""!==j&&j===k?n[""]:n[j],f!==b)i=""===m[f].unit.toString()&&l!==b?new d.Dimension(m[f].value,l).unify():m[f].unify(),(a&&h.value<i.value||!a&&h.value>i.value)&&(m[f]=g);else{if(k!==b&&j!==k)throw{type:"Argument",message:"incompatible types"};n[j]=m.length,m.push(g)}else Array.isArray(c[e].value)&&Array.prototype.push.apply(c,Array.prototype.slice.call(c[e].value));return 1==m.length?m[0]:(c=m.map(function(a){return a.toCSS(this.env)}).join(this.env.compress?",":", "),new d.Anonymous((a?"min":"max")+"("+c+")"))},min:function(){return this._minmax(!0,arguments)},max:function(){return this._minmax(!1,arguments)},"get-unit":function(a){return new d.Anonymous(a.unit)},argb:function(a){return new d.Anonymous(a.toARGB())},percentage:function(a){return new d.Dimension(100*a.value,"%")},color:function(a){if(a instanceof d.Quoted){var b,c=a.value;if(b=d.Color.fromKeyword(c))return b;if(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/.test(c))return new d.Color(c.slice(1));throw{type:"Argument",message:"argument must be a color keyword or 3/6 digit hex e.g. #FFF"}}throw{type:"Argument",message:"argument must be a string"}},iscolor:function(a){return this._isa(a,d.Color)},isnumber:function(a){return this._isa(a,d.Dimension)},isstring:function(a){return this._isa(a,d.Quoted)},iskeyword:function(a){return this._isa(a,d.Keyword)},isurl:function(a){return this._isa(a,d.URL)},ispixel:function(a){return this.isunit(a,"px")},ispercentage:function(a){return this.isunit(a,"%")},isem:function(a){return this.isunit(a,"em")},isunit:function(a,b){return a instanceof d.Dimension&&a.unit.is(b.value||b)?d.True:d.False},_isa:function(a,b){return a instanceof b?d.True:d.False},tint:function(a,b){return this.mix(this.rgb(255,255,255),a,b)},shade:function(a,b){return this.mix(this.rgb(0,0,0),a,b)},extract:function(a,b){return b=b.value-1,Array.isArray(a.value)?a.value[b]:Array(a)[b]},length:function(a){var b=Array.isArray(a.value)?a.value.length:1;return new d.Dimension(b)},"data-uri":function(b,e){if("undefined"!=typeof a)return new d.URL(e||b,this.currentFileInfo).eval(this.env);var f=b.value,g=e&&e.value,h=c("fs"),i=c("path"),j=!1;if(arguments.length<2&&(g=f),this.env.isPathRelative(g)&&(g=this.currentFileInfo.relativeUrls?i.join(this.currentFileInfo.currentDirectory,g):i.join(this.currentFileInfo.entryPath,g)),arguments.length<2){var k;try{k=c("mime")}catch(l){k=d._mime}f=k.lookup(g);var m=k.charsets.lookup(f);j=["US-ASCII","UTF-8"].indexOf(m)<0,j&&(f+=";base64")}else j=/;base64$/.test(f);var n=h.readFileSync(g),o=32,p=parseInt(n.length/1024,10);if(p>=o&&this.env.ieCompat!==!1)return this.env.silent||console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!",g,p,o),new d.URL(e||b,this.currentFileInfo).eval(this.env);n=j?n.toString("base64"):encodeURIComponent(n);var q='"data:'+f+","+n+'"';return new d.URL(new d.Anonymous(q))},"svg-gradient":function(a){function e(){throw{type:"Argument",message:"svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]"}}arguments.length<3&&e();var f,g,h,i,j,k,l,m=Array.prototype.slice.call(arguments,1),n="linear",o='x="0" y="0" width="1" height="1"',p=!0,q={compress:!1},r=a.toCSS(q);switch(r){case"to bottom":f='x1="0%" y1="0%" x2="0%" y2="100%"';break;case"to right":f='x1="0%" y1="0%" x2="100%" y2="0%"';break;case"to bottom right":f='x1="0%" y1="0%" x2="100%" y2="100%"';break;case"to top right":f='x1="0%" y1="100%" x2="100%" y2="0%"';break;case"ellipse":case"ellipse at center":n="radial",f='cx="50%" cy="50%" r="75%"',o='x="-50" y="-50" width="101" height="101"';break;default:throw{type:"Argument",message:"svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'"}}for(g='<?xml version="1.0" ?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" height="100%" viewBox="0 0 1 1" preserveAspectRatio="none"><'+n+'Gradient id="gradient" gradientUnits="userSpaceOnUse" '+f+">",h=0;h<m.length;h+=1)m[h].value?(i=m[h].value[0],j=m[h].value[1]):(i=m[h],j=b),i instanceof d.Color&&((0===h||h+1===m.length)&&j===b||j instanceof d.Dimension)||e(),k=j?j.toCSS(q):0===h?"0%":"100%",l=i.alpha,g+='<stop offset="'+k+'" stop-color="'+i.toRGB()+'"'+(1>l?' stop-opacity="'+l+'"':"")+"/>";if(g+="</"+n+"Gradient><rect "+o+' fill="url(#gradient)" /></svg>',p)try{g=c("./encoder").encodeBase64(g)}catch(s){p=!1}return g="'data:image/svg+xml"+(p?";base64":"")+","+g+"'",new d.URL(new d.Anonymous(g))}},d._mime={_types:{".htm":"text/html",".html":"text/html",".gif":"image/gif",".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png"},lookup:function(a){var e=c("path").extname(a),f=d._mime._types[e];if(f===b)throw new Error('Optional dependency "mime" is required for '+e);return f},charsets:{lookup:function(a){return a&&/^text\//.test(a)?"UTF-8":""}}};var l={ceil:null,floor:null,sqrt:null,abs:null,tan:"",sin:"",cos:"",atan:"rad",asin:"rad",acos:"rad"},m={multiply:function(a,b){return a*b},screen:function(a,b){return a+b-a*b},overlay:function(a,b){return a*=2,1>=a?m.multiply(a,b):m.screen(a-1,b)},softlight:function(a,b){var c=1,d=a;return b>.5&&(d=1,c=a>.25?Math.sqrt(a):((16*a-12)*a+4)*a),a-(1-2*b)*d*(c-a)},hardlight:function(a,b){return m.overlay(b,a)},difference:function(a,b){return Math.abs(a-b)},exclusion:function(a,b){return a+b-2*a*b},average:function(a,b){return(a+b)/2},negation:function(a,b){return 1-Math.abs(a+b-1)}};d.defaultFunc={eval:function(){var a=this.value_,b=this.error_;if(b)throw b;return null!=a?a?d.True:d.False:void 0},value:function(a){this.value_=a},error:function(a){this.error_=a},reset:function(){this.value_=this.error_=null}},g(),d.fround=function(a,b){var c;return a&&null!=a.numPrecision?(c=Math.pow(10,a.numPrecision),Math.round(b*c)/c):b},d.functionCall=function(a,b){this.env=a,this.currentFileInfo=b},d.functionCall.prototype=d.functions}(c("./tree")),function(a){a.colors={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgrey:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",grey:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"}}(c("./tree")),function(a){a.debugInfo=function(b,c,d){var e="";if(b.dumpLineNumbers&&!b.compress)switch(b.dumpLineNumbers){case"comments":e=a.debugInfo.asComment(c);break;case"mediaquery":e=a.debugInfo.asMediaQuery(c);break;case"all":e=a.debugInfo.asComment(c)+(d||"")+a.debugInfo.asMediaQuery(c)}return e},a.debugInfo.asComment=function(a){return"/* line "+a.debugInfo.lineNumber+", "+a.debugInfo.fileName+" */\n"},a.debugInfo.asMediaQuery=function(a){return"@media -sass-debug-info{filename{font-family:"+("file://"+a.debugInfo.fileName).replace(/([.:\/\\])/g,function(a){return"\\"==a&&(a="/"),"\\"+a})+"}line{font-family:\\00003"+a.debugInfo.lineNumber+"}}\n"},a.find=function(a,b){for(var c,d=0;d<a.length;d++)if(c=b.call(a,a[d]))return c;return null},a.jsify=function(a){return Array.isArray(a.value)&&a.value.length>1?"["+a.value.map(function(a){return a.toCSS(!1)}).join(", ")+"]":a.toCSS(!1)},a.toCSS=function(a){var b=[];return this.genCSS(a,{add:function(a){b.push(a)},isEmpty:function(){return 0===b.length}}),b.join("")},a.outputRuleset=function(a,b,c){var d,e=c.length;if(a.tabLevel=(0|a.tabLevel)+1,a.compress){for(b.add("{"),d=0;e>d;d++)c[d].genCSS(a,b);return b.add("}"),void a.tabLevel--}var f="\n"+Array(a.tabLevel).join(" "),g=f+" ";if(e){for(b.add(" {"+g),c[0].genCSS(a,b),d=1;e>d;d++)b.add(g),c[d].genCSS(a,b);b.add(f+"}")}else b.add(" {"+f+"}");a.tabLevel--}}(c("./tree")),function(a){a.Alpha=function(a){this.value=a},a.Alpha.prototype={type:"Alpha",accept:function(a){this.value=a.visit(this.value)},eval:function(b){return this.value.eval?new a.Alpha(this.value.eval(b)):this},genCSS:function(a,b){b.add("alpha(opacity="),this.value.genCSS?this.value.genCSS(a,b):b.add(this.value),b.add(")")},toCSS:a.toCSS}}(c("../tree")),function(a){a.Anonymous=function(a,b,c,d){this.value=a.value||a,this.index=b,this.mapLines=d,this.currentFileInfo=c},a.Anonymous.prototype={type:"Anonymous",eval:function(){return new a.Anonymous(this.value,this.index,this.currentFileInfo,this.mapLines)},compare:function(a){if(!a.toCSS)return-1;var b=this.toCSS(),c=a.toCSS();return b===c?0:c>b?-1:1},genCSS:function(a,b){b.add(this.value,this.currentFileInfo,this.index,this.mapLines)},toCSS:a.toCSS}}(c("../tree")),function(a){a.Assignment=function(a,b){this.key=a,this.value=b},a.Assignment.prototype={type:"Assignment",accept:function(a){this.value=a.visit(this.value)},eval:function(b){return this.value.eval?new a.Assignment(this.key,this.value.eval(b)):this},genCSS:function(a,b){b.add(this.key+"="),this.value.genCSS?this.value.genCSS(a,b):b.add(this.value)},toCSS:a.toCSS}}(c("../tree")),function(a){a.Call=function(a,b,c,d){this.name=a,this.args=b,this.index=c,this.currentFileInfo=d},a.Call.prototype={type:"Call",accept:function(a){this.args&&(this.args=a.visitArray(this.args))},eval:function(b){var c,d,e=this.args.map(function(a){return a.eval(b)}),f=this.name.toLowerCase();if(f in a.functions)try{if(d=new a.functionCall(b,this.currentFileInfo),c=d[f].apply(d,e),null!=c)return c}catch(g){throw{type:g.type||"Runtime",message:"error evaluating function `"+this.name+"`"+(g.message?": "+g.message:""),index:this.index,filename:this.currentFileInfo.filename}}return new a.Call(this.name,e,this.index,this.currentFileInfo)},genCSS:function(a,b){b.add(this.name+"(",this.currentFileInfo,this.index);for(var c=0;c<this.args.length;c++)this.args[c].genCSS(a,b),c+1<this.args.length&&b.add(", ");b.add(")")},toCSS:a.toCSS}}(c("../tree")),function(a){function b(a){return"#"+a.map(function(a){return a=c(Math.round(a),255),(16>a?"0":"")+a.toString(16)}).join("")}function c(a,b){return Math.min(Math.max(a,0),b)}a.Color=function(a,b){this.rgb=Array.isArray(a)?a:6==a.length?a.match(/.{2}/g).map(function(a){return parseInt(a,16)}):a.split("").map(function(a){return parseInt(a+a,16)}),this.alpha="number"==typeof b?b:1};var d="transparent";a.Color.prototype={type:"Color",eval:function(){return this},luma:function(){var a=this.rgb[0]/255,b=this.rgb[1]/255,c=this.rgb[2]/255;return a=.03928>=a?a/12.92:Math.pow((a+.055)/1.055,2.4),b=.03928>=b?b/12.92:Math.pow((b+.055)/1.055,2.4),c=.03928>=c?c/12.92:Math.pow((c+.055)/1.055,2.4),.2126*a+.7152*b+.0722*c},genCSS:function(a,b){b.add(this.toCSS(a))},toCSS:function(b,e){var f=b&&b.compress&&!e,g=a.fround(b,this.alpha);if(1>g)return 0===g&&this.isTransparentKeyword?d:"rgba("+this.rgb.map(function(a){return c(Math.round(a),255)}).concat(c(g,1)).join(","+(f?"":" "))+")";var h=this.toRGB();if(f){var i=h.split("");i[1]===i[2]&&i[3]===i[4]&&i[5]===i[6]&&(h="#"+i[1]+i[3]+i[5])}return h},operate:function(b,c,d){for(var e=[],f=this.alpha*(1-d.alpha)+d.alpha,g=0;3>g;g++)e[g]=a.operate(b,c,this.rgb[g],d.rgb[g]);return new a.Color(e,f)},toRGB:function(){return b(this.rgb)},toHSL:function(){var a,b,c=this.rgb[0]/255,d=this.rgb[1]/255,e=this.rgb[2]/255,f=this.alpha,g=Math.max(c,d,e),h=Math.min(c,d,e),i=(g+h)/2,j=g-h;if(g===h)a=b=0;else{switch(b=i>.5?j/(2-g-h):j/(g+h),g){case c:a=(d-e)/j+(e>d?6:0);break;case d:a=(e-c)/j+2;break;case e:a=(c-d)/j+4}a/=6}return{h:360*a,s:b,l:i,a:f}},toHSV:function(){var a,b,c=this.rgb[0]/255,d=this.rgb[1]/255,e=this.rgb[2]/255,f=this.alpha,g=Math.max(c,d,e),h=Math.min(c,d,e),i=g,j=g-h;if(b=0===g?0:j/g,g===h)a=0;else{switch(g){case c:a=(d-e)/j+(e>d?6:0);break;case d:a=(e-c)/j+2;break;case e:a=(c-d)/j+4}a/=6}return{h:360*a,s:b,v:i,a:f}},toARGB:function(){return b([255*this.alpha].concat(this.rgb))},compare:function(a){return a.rgb?a.rgb[0]===this.rgb[0]&&a.rgb[1]===this.rgb[1]&&a.rgb[2]===this.rgb[2]&&a.alpha===this.alpha?0:-1:-1}},a.Color.fromKeyword=function(b){if(b=b.toLowerCase(),a.colors.hasOwnProperty(b))return new a.Color(a.colors[b].slice(1));if(b===d){var c=new a.Color([0,0,0],0);return c.isTransparentKeyword=!0,c}}}(c("../tree")),function(a){a.Comment=function(a,b,c,d){this.value=a,this.silent=!!b,this.currentFileInfo=d},a.Comment.prototype={type:"Comment",genCSS:function(b,c){this.debugInfo&&c.add(a.debugInfo(b,this),this.currentFileInfo,this.index),c.add(this.value.trim())},toCSS:a.toCSS,isSilent:function(a){var b=this.currentFileInfo&&this.currentFileInfo.reference&&!this.isReferenced,c=a.compress&&!this.value.match(/^\/\*!/);return this.silent||b||c},eval:function(){return this},markReferenced:function(){this.isReferenced=!0}}}(c("../tree")),function(a){a.Condition=function(a,b,c,d,e){this.op=a.trim(),this.lvalue=b,this.rvalue=c,this.index=d,this.negate=e},a.Condition.prototype={type:"Condition",accept:function(a){this.lvalue=a.visit(this.lvalue),this.rvalue=a.visit(this.rvalue)},eval:function(a){var b,c=this.lvalue.eval(a),d=this.rvalue.eval(a),e=this.index;return b=function(a){switch(a){case"and":return c&&d;case"or":return c||d;default:if(c.compare)b=c.compare(d);else{if(!d.compare)throw{type:"Type",message:"Unable to perform comparison",index:e};b=d.compare(c)}switch(b){case-1:return"<"===a||"=<"===a||"<="===a;case 0:return"="===a||">="===a||"=<"===a||"<="===a;case 1:return">"===a||">="===a}}}(this.op),this.negate?!b:b}}}(c("../tree")),function(a){a.DetachedRuleset=function(a,b){this.ruleset=a,this.frames=b},a.DetachedRuleset.prototype={type:"DetachedRuleset",accept:function(a){this.ruleset=a.visit(this.ruleset)},eval:function(b){var c=this.frames||b.frames.slice(0);return new a.DetachedRuleset(this.ruleset,c)},callEval:function(b){return this.ruleset.eval(this.frames?new a.evalEnv(b,this.frames.concat(b.frames)):b)}}}(c("../tree")),function(a){a.Dimension=function(c,d){this.value=parseFloat(c),this.unit=d&&d instanceof a.Unit?d:new a.Unit(d?[d]:b)},a.Dimension.prototype={type:"Dimension",accept:function(a){this.unit=a.visit(this.unit)},eval:function(){return this},toColor:function(){return new a.Color([this.value,this.value,this.value])},genCSS:function(b,c){if(b&&b.strictUnits&&!this.unit.isSingular())throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString());var d=a.fround(b,this.value),e=String(d);if(0!==d&&1e-6>d&&d>-1e-6&&(e=d.toFixed(20).replace(/0+$/,"")),b&&b.compress){if(0===d&&this.unit.isLength())return void c.add(e);d>0&&1>d&&(e=e.substr(1))}c.add(e),this.unit.genCSS(b,c)},toCSS:a.toCSS,operate:function(b,c,d){var e=a.operate(b,c,this.value,d.value),f=this.unit.clone();if("+"===c||"-"===c)if(0===f.numerator.length&&0===f.denominator.length)f.numerator=d.unit.numerator.slice(0),f.denominator=d.unit.denominator.slice(0);else if(0===d.unit.numerator.length&&0===f.denominator.length);else{if(d=d.convertTo(this.unit.usedUnits()),b.strictUnits&&d.unit.toString()!==f.toString())throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '"+f.toString()+"' and '"+d.unit.toString()+"'.");e=a.operate(b,c,this.value,d.value)}else"*"===c?(f.numerator=f.numerator.concat(d.unit.numerator).sort(),f.denominator=f.denominator.concat(d.unit.denominator).sort(),f.cancel()):"/"===c&&(f.numerator=f.numerator.concat(d.unit.denominator).sort(),f.denominator=f.denominator.concat(d.unit.numerator).sort(),f.cancel());return new a.Dimension(e,f)},compare:function(b){if(b instanceof a.Dimension){var c,d,e,f;if(this.unit.isEmpty()||b.unit.isEmpty())c=this,d=b;else if(c=this.unify(),d=b.unify(),0!==c.unit.compare(d.unit))return-1;return e=c.value,f=d.value,f>e?-1:e>f?1:0}return-1},unify:function(){return this.convertTo({length:"px",duration:"s",angle:"rad"})},convertTo:function(b){var c,d,e,f,g,h=this.value,i=this.unit.clone(),j={};if("string"==typeof b){for(c in a.UnitConversions)a.UnitConversions[c].hasOwnProperty(b)&&(j={},j[c]=b);b=j}g=function(a,b){return e.hasOwnProperty(a)?(b?h/=e[a]/e[f]:h*=e[a]/e[f],f):a};for(d in b)b.hasOwnProperty(d)&&(f=b[d],e=a.UnitConversions[d],i.map(g));return i.cancel(),new a.Dimension(h,i)}},a.UnitConversions={length:{m:1,cm:.01,mm:.001,"in":.0254,px:.0254/96,pt:.0254/72,pc:.0254/72*12},duration:{s:1,ms:.001},angle:{rad:1/(2*Math.PI),deg:1/360,grad:.0025,turn:1}},a.Unit=function(a,b,c){this.numerator=a?a.slice(0).sort():[],this.denominator=b?b.slice(0).sort():[],this.backupUnit=c},a.Unit.prototype={type:"Unit",clone:function(){return new a.Unit(this.numerator.slice(0),this.denominator.slice(0),this.backupUnit)},genCSS:function(a,b){this.numerator.length>=1?b.add(this.numerator[0]):this.denominator.length>=1?b.add(this.denominator[0]):a&&a.strictUnits||!this.backupUnit||b.add(this.backupUnit)},toCSS:a.toCSS,toString:function(){var a,b=this.numerator.join("*");for(a=0;a<this.denominator.length;a++)b+="/"+this.denominator[a];return b},compare:function(a){return this.is(a.toString())?0:-1},is:function(a){return this.toString()===a},isLength:function(){return Boolean(this.toCSS().match(/px|em|%|in|cm|mm|pc|pt|ex/))},isEmpty:function(){return 0===this.numerator.length&&0===this.denominator.length},isSingular:function(){return this.numerator.length<=1&&0===this.denominator.length},map:function(a){var b;for(b=0;b<this.numerator.length;b++)this.numerator[b]=a(this.numerator[b],!1);for(b=0;b<this.denominator.length;b++)this.denominator[b]=a(this.denominator[b],!0)},usedUnits:function(){var b,c,d={};c=function(a){return b.hasOwnProperty(a)&&!d[e]&&(d[e]=a),a};for(var e in a.UnitConversions)a.UnitConversions.hasOwnProperty(e)&&(b=a.UnitConversions[e],this.map(c));return d},cancel:function(){var a,b,c,d={};for(b=0;b<this.numerator.length;b++)a=this.numerator[b],c||(c=a),d[a]=(d[a]||0)+1;for(b=0;b<this.denominator.length;b++)a=this.denominator[b],c||(c=a),d[a]=(d[a]||0)-1;this.numerator=[],this.denominator=[];for(a in d)if(d.hasOwnProperty(a)){var e=d[a];if(e>0)for(b=0;e>b;b++)this.numerator.push(a);else if(0>e)for(b=0;-e>b;b++)this.denominator.push(a)}0===this.numerator.length&&0===this.denominator.length&&c&&(this.backupUnit=c),this.numerator.sort(),this.denominator.sort()}}}(c("../tree")),function(a){a.Directive=function(a,b,c,d,e,f){this.name=a,this.value=b,c&&(this.rules=c,this.rules.allowImports=!0),this.index=d,this.currentFileInfo=e,this.debugInfo=f},a.Directive.prototype={type:"Directive",accept:function(a){var b=this.value,c=this.rules;c&&(c=a.visit(c)),b&&(b=a.visit(b))},genCSS:function(b,c){var d=this.value,e=this.rules;c.add(this.name,this.currentFileInfo,this.index),d&&(c.add(" "),d.genCSS(b,c)),e?a.outputRuleset(b,c,[e]):c.add(";")},toCSS:a.toCSS,eval:function(b){var c=this.value,d=this.rules;return c&&(c=c.eval(b)),d&&(d=d.eval(b),d.root=!0),new a.Directive(this.name,c,d,this.index,this.currentFileInfo,this.debugInfo)},variable:function(b){return this.rules?a.Ruleset.prototype.variable.call(this.rules,b):void 0},find:function(){return this.rules?a.Ruleset.prototype.find.apply(this.rules,arguments):void 0},rulesets:function(){return this.rules?a.Ruleset.prototype.rulesets.apply(this.rules):void 0},markReferenced:function(){var a,b;if(this.isReferenced=!0,this.rules)for(b=this.rules.rules,a=0;a<b.length;a++)b[a].markReferenced&&b[a].markReferenced()}}}(c("../tree")),function(a){a.Element=function(b,c,d,e){this.combinator=b instanceof a.Combinator?b:new a.Combinator(b),this.value="string"==typeof c?c.trim():c?c:"",this.index=d,this.currentFileInfo=e},a.Element.prototype={type:"Element",accept:function(a){var b=this.value;this.combinator=a.visit(this.combinator),"object"==typeof b&&(this.value=a.visit(b))},eval:function(b){return new a.Element(this.combinator,this.value.eval?this.value.eval(b):this.value,this.index,this.currentFileInfo)},genCSS:function(a,b){b.add(this.toCSS(a),this.currentFileInfo,this.index)},toCSS:function(a){var b=this.value.toCSS?this.value.toCSS(a):this.value;return""===b&&"&"===this.combinator.value.charAt(0)?"":this.combinator.toCSS(a||{})+b}},a.Attribute=function(a,b,c){this.key=a,this.op=b,this.value=c},a.Attribute.prototype={type:"Attribute",eval:function(b){return new a.Attribute(this.key.eval?this.key.eval(b):this.key,this.op,this.value&&this.value.eval?this.value.eval(b):this.value)},genCSS:function(a,b){b.add(this.toCSS(a))},toCSS:function(a){var b=this.key.toCSS?this.key.toCSS(a):this.key;return this.op&&(b+=this.op,b+=this.value.toCSS?this.value.toCSS(a):this.value),"["+b+"]"}},a.Combinator=function(a){this.value=" "===a?" ":a?a.trim():""},a.Combinator.prototype={type:"Combinator",_outputMap:{"":""," ":" ",":":" :","+":" + ","~":" ~ ",">":" > ","|":"|","^":" ^ ","^^":" ^^ "},_outputMapCompressed:{"":""," ":" ",":":" :","+":"+","~":"~",">":">","|":"|","^":"^","^^":"^^"},genCSS:function(a,b){b.add((a.compress?this._outputMapCompressed:this._outputMap)[this.value])},toCSS:a.toCSS}}(c("../tree")),function(a){a.Expression=function(a){this.value=a},a.Expression.prototype={type:"Expression",accept:function(a){this.value&&(this.value=a.visitArray(this.value))},eval:function(b){var c,d=this.parens&&!this.parensInOp,e=!1;return d&&b.inParenthesis(),this.value.length>1?c=new a.Expression(this.value.map(function(a){return a.eval(b)})):1===this.value.length?(this.value[0].parens&&!this.value[0].parensInOp&&(e=!0),c=this.value[0].eval(b)):c=this,d&&b.outOfParenthesis(),this.parens&&this.parensInOp&&!b.isMathOn()&&!e&&(c=new a.Paren(c)),c},genCSS:function(a,b){for(var c=0;c<this.value.length;c++)this.value[c].genCSS(a,b),c+1<this.value.length&&b.add(" ")},toCSS:a.toCSS,throwAwayComments:function(){this.value=this.value.filter(function(b){return!(b instanceof a.Comment)})}}}(c("../tree")),function(a){a.Extend=function(b,c,d){switch(this.selector=b,this.option=c,this.index=d,this.object_id=a.Extend.next_id++,this.parent_ids=[this.object_id],c){case"all":this.allowBefore=!0,this.allowAfter=!0;break;default:this.allowBefore=!1,this.allowAfter=!1}},a.Extend.next_id=0,a.Extend.prototype={type:"Extend",accept:function(a){this.selector=a.visit(this.selector)},eval:function(b){return new a.Extend(this.selector.eval(b),this.option,this.index)},clone:function(){return new a.Extend(this.selector,this.option,this.index)},findSelfSelectors:function(a){var b,c,d=[];for(b=0;b<a.length;b++)c=a[b].elements,b>0&&c.length&&""===c[0].combinator.value&&(c[0].combinator.value=" "),d=d.concat(a[b].elements);this.selfSelectors=[{elements:d}]}}}(c("../tree")),function(a){a.Import=function(a,c,d,e,f){if(this.options=d,this.index=e,this.path=a,this.features=c,this.currentFileInfo=f,this.options.less!==b||this.options.inline)this.css=!this.options.less||this.options.inline;else{var g=this.getPath();g&&/css([\?;].*)?$/.test(g)&&(this.css=!0)}},a.Import.prototype={type:"Import",accept:function(a){this.features&&(this.features=a.visit(this.features)),this.path=a.visit(this.path),!this.options.inline&&this.root&&(this.root=a.visit(this.root))},genCSS:function(a,b){this.css&&(b.add("@import ",this.currentFileInfo,this.index),this.path.genCSS(a,b),this.features&&(b.add(" "),this.features.genCSS(a,b)),b.add(";"))},toCSS:a.toCSS,getPath:function(){if(this.path instanceof a.Quoted){var c=this.path.value;return this.css!==b||/(\.[a-z]*$)|([\?;].*)$/.test(c)?c:c+".less"}return this.path instanceof a.URL?this.path.value.value:null},evalForImport:function(b){return new a.Import(this.path.eval(b),this.features,this.options,this.index,this.currentFileInfo)},evalPath:function(b){var c=this.path.eval(b),d=this.currentFileInfo&&this.currentFileInfo.rootpath;if(!(c instanceof a.URL)){if(d){var e=c.value;e&&b.isPathRelative(e)&&(c.value=d+e)}c.value=b.normalizePath(c.value)}return c},eval:function(b){var c,d=this.features&&this.features.eval(b);if(this.skip&&("function"==typeof this.skip&&(this.skip=this.skip()),this.skip))return[];if(this.options.inline){var e=new a.Anonymous(this.root,0,{filename:this.importedFilename},!0);return this.features?new a.Media([e],this.features.value):[e]}if(this.css){var f=new a.Import(this.evalPath(b),d,this.options,this.index);if(!f.css&&this.error)throw this.error;return f}return c=new a.Ruleset(null,this.root.rules.slice(0)),c.evalImports(b),this.features?new a.Media(c.rules,this.features.value):c.rules}}}(c("../tree")),function(a){a.JavaScript=function(a,b,c){this.escaped=c,this.expression=a,this.index=b},a.JavaScript.prototype={type:"JavaScript",eval:function(b){var c,d=this,e={},f=this.expression.replace(/@\{([\w-]+)\}/g,function(c,e){return a.jsify(new a.Variable("@"+e,d.index).eval(b))});try{f=new Function("return ("+f+")")}catch(g){throw{message:"JavaScript evaluation error: "+g.message+" from `"+f+"`",index:this.index}}var h=b.frames[0].variables();for(var i in h)h.hasOwnProperty(i)&&(e[i.slice(1)]={value:h[i].value,toJS:function(){return this.value.eval(b).toCSS()}});try{c=f.call(e)}catch(g){throw{message:"JavaScript evaluation error: '"+g.name+": "+g.message.replace(/["]/g,"'")+"'",index:this.index}}return"number"==typeof c?new a.Dimension(c):"string"==typeof c?new a.Quoted('"'+c+'"',c,this.escaped,this.index):new a.Anonymous(Array.isArray(c)?c.join(", "):c)}}}(c("../tree")),function(a){a.Keyword=function(a){this.value=a},a.Keyword.prototype={type:"Keyword",eval:function(){return this},genCSS:function(a,b){if("%"===this.value)throw{type:"Syntax",message:"Invalid % without number"};b.add(this.value)},toCSS:a.toCSS,compare:function(b){return b instanceof a.Keyword?b.value===this.value?0:1:-1}},a.True=new a.Keyword("true"),a.False=new a.Keyword("false")}(c("../tree")),function(a){a.Media=function(b,c,d,e){this.index=d,this.currentFileInfo=e;var f=this.emptySelectors();this.features=new a.Value(c),this.rules=[new a.Ruleset(f,b)],this.rules[0].allowImports=!0},a.Media.prototype={type:"Media",accept:function(a){this.features&&(this.features=a.visit(this.features)),this.rules&&(this.rules=a.visitArray(this.rules))},genCSS:function(b,c){c.add("@media ",this.currentFileInfo,this.index),this.features.genCSS(b,c),a.outputRuleset(b,c,this.rules)},toCSS:a.toCSS,eval:function(b){b.mediaBlocks||(b.mediaBlocks=[],b.mediaPath=[]);var c=new a.Media(null,[],this.index,this.currentFileInfo);this.debugInfo&&(this.rules[0].debugInfo=this.debugInfo,c.debugInfo=this.debugInfo);var d=!1;b.strictMath||(d=!0,b.strictMath=!0);try{c.features=this.features.eval(b)}finally{d&&(b.strictMath=!1)}return b.mediaPath.push(c),b.mediaBlocks.push(c),b.frames.unshift(this.rules[0]),c.rules=[this.rules[0].eval(b)],b.frames.shift(),b.mediaPath.pop(),0===b.mediaPath.length?c.evalTop(b):c.evalNested(b)},variable:function(b){return a.Ruleset.prototype.variable.call(this.rules[0],b)},find:function(){return a.Ruleset.prototype.find.apply(this.rules[0],arguments)},rulesets:function(){return a.Ruleset.prototype.rulesets.apply(this.rules[0])},emptySelectors:function(){var b=new a.Element("","&",this.index,this.currentFileInfo),c=[new a.Selector([b],null,null,this.index,this.currentFileInfo)];return c[0].mediaEmpty=!0,c},markReferenced:function(){var a,b=this.rules[0].rules;for(this.rules[0].markReferenced(),this.isReferenced=!0,a=0;a<b.length;a++)b[a].markReferenced&&b[a].markReferenced()},evalTop:function(b){var c=this;if(b.mediaBlocks.length>1){var d=this.emptySelectors();c=new a.Ruleset(d,b.mediaBlocks),c.multiMedia=!0}return delete b.mediaBlocks,delete b.mediaPath,c},evalNested:function(b){var c,d,e=b.mediaPath.concat([this]);for(c=0;c<e.length;c++)d=e[c].features instanceof a.Value?e[c].features.value:e[c].features,e[c]=Array.isArray(d)?d:[d];return this.features=new a.Value(this.permute(e).map(function(b){for(b=b.map(function(b){return b.toCSS?b:new a.Anonymous(b)}),c=b.length-1;c>0;c--)b.splice(c,0,new a.Anonymous("and"));return new a.Expression(b)})),new a.Ruleset([],[])},permute:function(a){if(0===a.length)return[];if(1===a.length)return a[0];for(var b=[],c=this.permute(a.slice(1)),d=0;d<c.length;d++)for(var e=0;e<a[0].length;e++)b.push([a[0][e]].concat(c[d]));return b},bubbleSelectors:function(b){b&&(this.rules=[new a.Ruleset(b.slice(0),[this.rules[0]])])}}}(c("../tree")),function(a){a.mixin={},a.mixin.Call=function(b,c,d,e,f){this.selector=new a.Selector(b),this.arguments=c&&c.length?c:null,this.index=d,this.currentFileInfo=e,this.important=f},a.mixin.Call.prototype={type:"MixinCall",accept:function(a){this.selector&&(this.selector=a.visit(this.selector)),this.arguments&&(this.arguments=a.visitArray(this.arguments))
+},eval:function(b){var c,d,e,f,g,h,i,j,k,l,m,n,o=[],p=!1,q=[],r=[],s=a.defaultFunc,t=0,u=1,v=2;for(e=this.arguments&&this.arguments.map(function(a){return{name:a.name,value:a.value.eval(b)}}),f=0;f<b.frames.length;f++)if((c=b.frames[f].find(this.selector)).length>0){for(j=!0,g=0;g<c.length;g++){for(d=c[g],i=!1,h=0;h<b.frames.length;h++)if(!(d instanceof a.mixin.Definition)&&d===(b.frames[h].originalRuleset||b.frames[h])){i=!0;break}if(!i&&d.matchArgs(e,b)){if(l={mixin:d,group:t},d.matchCondition){for(h=0;2>h;h++)s.value(h),r[h]=d.matchCondition(e,b);(r[0]||r[1])&&(r[0]!=r[1]&&(l.group=r[1]?u:v),q.push(l))}else q.push(l);p=!0}}for(s.reset(),n=[0,0,0],g=0;g<q.length;g++)n[q[g].group]++;if(n[t]>0)m=v;else if(m=u,n[u]+n[v]>1)throw{type:"Runtime",message:"Ambiguous use of `default()` found when matching for `"+this.format(e)+"`",index:this.index,filename:this.currentFileInfo.filename};for(g=0;g<q.length;g++)if(l=q[g].group,l===t||l===m)try{d=q[g].mixin,d instanceof a.mixin.Definition||(d=new a.mixin.Definition("",[],d.rules,null,!1),d.originalRuleset=c[g].originalRuleset||c[g]),Array.prototype.push.apply(o,d.evalCall(b,e,this.important).rules)}catch(w){throw{message:w.message,index:this.index,filename:this.currentFileInfo.filename,stack:w.stack}}if(p){if(!this.currentFileInfo||!this.currentFileInfo.reference)for(f=0;f<o.length;f++)k=o[f],k.markReferenced&&k.markReferenced();return o}}throw j?{type:"Runtime",message:"No matching definition was found for `"+this.format(e)+"`",index:this.index,filename:this.currentFileInfo.filename}:{type:"Name",message:this.selector.toCSS().trim()+" is undefined",index:this.index,filename:this.currentFileInfo.filename}},format:function(a){return this.selector.toCSS().trim()+"("+(a?a.map(function(a){var b="";return a.name&&(b+=a.name+":"),b+=a.value.toCSS?a.value.toCSS():"???"}).join(", "):"")+")"}},a.mixin.Definition=function(b,c,d,e,f,g){this.name=b,this.selectors=[new a.Selector([new a.Element(null,b,this.index,this.currentFileInfo)])],this.params=c,this.condition=e,this.variadic=f,this.arity=c.length,this.rules=d,this._lookups={},this.required=c.reduce(function(a,b){return!b.name||b.name&&!b.value?a+1:a},0),this.parent=a.Ruleset.prototype,this.frames=g},a.mixin.Definition.prototype={type:"MixinDefinition",accept:function(a){this.params&&this.params.length&&(this.params=a.visitArray(this.params)),this.rules=a.visitArray(this.rules),this.condition&&(this.condition=a.visit(this.condition))},variable:function(a){return this.parent.variable.call(this,a)},variables:function(){return this.parent.variables.call(this)},find:function(){return this.parent.find.apply(this,arguments)},rulesets:function(){return this.parent.rulesets.apply(this)},evalParams:function(b,c,d,e){var f,g,h,i,j,k,l,m,n=new a.Ruleset(null,null),o=this.params.slice(0),p=0;if(c=new a.evalEnv(c,[n].concat(c.frames)),d)for(d=d.slice(0),p=d.length,h=0;p>h;h++)if(g=d[h],k=g&&g.name){for(l=!1,i=0;i<o.length;i++)if(!e[i]&&k===o[i].name){e[i]=g.value.eval(b),n.prependRule(new a.Rule(k,g.value.eval(b))),l=!0;break}if(l){d.splice(h,1),h--;continue}throw{type:"Runtime",message:"Named argument for "+this.name+" "+d[h].name+" not found"}}for(m=0,h=0;h<o.length;h++)if(!e[h]){if(g=d&&d[m],k=o[h].name)if(o[h].variadic){for(f=[],i=m;p>i;i++)f.push(d[i].value.eval(b));n.prependRule(new a.Rule(k,new a.Expression(f).eval(b)))}else{if(j=g&&g.value)j=j.eval(b);else{if(!o[h].value)throw{type:"Runtime",message:"wrong number of arguments for "+this.name+" ("+p+" for "+this.arity+")"};j=o[h].value.eval(c),n.resetCache()}n.prependRule(new a.Rule(k,j)),e[h]=j}if(o[h].variadic&&d)for(i=m;p>i;i++)e[i]=d[i].value.eval(b);m++}return n},eval:function(b){return new a.mixin.Definition(this.name,this.params,this.rules,this.condition,this.variadic,this.frames||b.frames.slice(0))},evalCall:function(b,c,d){var e,f,g=[],h=this.frames?this.frames.concat(b.frames):b.frames,i=this.evalParams(b,new a.evalEnv(b,h),c,g);return i.prependRule(new a.Rule("@arguments",new a.Expression(g).eval(b))),e=this.rules.slice(0),f=new a.Ruleset(null,e),f.originalRuleset=this,f=f.eval(new a.evalEnv(b,[this,i].concat(h))),d&&(f=this.parent.makeImportant.apply(f)),f},matchCondition:function(b,c){return this.condition&&!this.condition.eval(new a.evalEnv(c,[this.evalParams(c,new a.evalEnv(c,this.frames.concat(c.frames)),b,[])].concat(this.frames).concat(c.frames)))?!1:!0},matchArgs:function(a,b){var c,d=a&&a.length||0;if(this.variadic){if(d<this.required-1)return!1}else{if(d<this.required)return!1;if(d>this.params.length)return!1}c=Math.min(d,this.arity);for(var e=0;c>e;e++)if(!this.params[e].name&&!this.params[e].variadic&&a[e].value.eval(b).toCSS()!=this.params[e].value.eval(b).toCSS())return!1;return!0}}}(c("../tree")),function(a){a.Negative=function(a){this.value=a},a.Negative.prototype={type:"Negative",accept:function(a){this.value=a.visit(this.value)},genCSS:function(a,b){b.add("-"),this.value.genCSS(a,b)},toCSS:a.toCSS,eval:function(b){return b.isMathOn()?new a.Operation("*",[new a.Dimension(-1),this.value]).eval(b):new a.Negative(this.value.eval(b))}}}(c("../tree")),function(a){a.Operation=function(a,b,c){this.op=a.trim(),this.operands=b,this.isSpaced=c},a.Operation.prototype={type:"Operation",accept:function(a){this.operands=a.visit(this.operands)},eval:function(b){var c=this.operands[0].eval(b),d=this.operands[1].eval(b);if(b.isMathOn()){if(c instanceof a.Dimension&&d instanceof a.Color&&(c=c.toColor()),d instanceof a.Dimension&&c instanceof a.Color&&(d=d.toColor()),!c.operate)throw{type:"Operation",message:"Operation on an invalid type"};return c.operate(b,this.op,d)}return new a.Operation(this.op,[c,d],this.isSpaced)},genCSS:function(a,b){this.operands[0].genCSS(a,b),this.isSpaced&&b.add(" "),b.add(this.op),this.isSpaced&&b.add(" "),this.operands[1].genCSS(a,b)},toCSS:a.toCSS},a.operate=function(a,b,c,d){switch(b){case"+":return c+d;case"-":return c-d;case"*":return c*d;case"/":return c/d}}}(c("../tree")),function(a){a.Paren=function(a){this.value=a},a.Paren.prototype={type:"Paren",accept:function(a){this.value=a.visit(this.value)},genCSS:function(a,b){b.add("("),this.value.genCSS(a,b),b.add(")")},toCSS:a.toCSS,eval:function(b){return new a.Paren(this.value.eval(b))}}}(c("../tree")),function(a){a.Quoted=function(a,b,c,d,e){this.escaped=c,this.value=b||"",this.quote=a.charAt(0),this.index=d,this.currentFileInfo=e},a.Quoted.prototype={type:"Quoted",genCSS:function(a,b){this.escaped||b.add(this.quote,this.currentFileInfo,this.index),b.add(this.value),this.escaped||b.add(this.quote)},toCSS:a.toCSS,eval:function(b){var c=this,d=this.value.replace(/`([^`]+)`/g,function(d,e){return new a.JavaScript(e,c.index,!0).eval(b).value}).replace(/@\{([\w-]+)\}/g,function(d,e){var f=new a.Variable("@"+e,c.index,c.currentFileInfo).eval(b,!0);return f instanceof a.Quoted?f.value:f.toCSS()});return new a.Quoted(this.quote+d+this.quote,d,this.escaped,this.index,this.currentFileInfo)},compare:function(a){if(!a.toCSS)return-1;var b=this.toCSS(),c=a.toCSS();return b===c?0:c>b?-1:1}}}(c("../tree")),function(a){function b(a,b){var c,d="",e=b.length,f={add:function(a){d+=a}};for(c=0;e>c;c++)b[c].eval(a).genCSS(a,f);return d}a.Rule=function(b,c,d,e,f,g,h){this.name=b,this.value=c instanceof a.Value||c instanceof a.Ruleset?c:new a.Value([c]),this.important=d?" "+d.trim():"",this.merge=e,this.index=f,this.currentFileInfo=g,this.inline=h||!1,this.variable=b.charAt&&"@"===b.charAt(0)},a.Rule.prototype={type:"Rule",accept:function(a){this.value=a.visit(this.value)},genCSS:function(a,b){b.add(this.name+(a.compress?":":": "),this.currentFileInfo,this.index);try{this.value.genCSS(a,b)}catch(c){throw c.index=this.index,c.filename=this.currentFileInfo.filename,c}b.add(this.important+(this.inline||a.lastRule&&a.compress?"":";"),this.currentFileInfo,this.index)},toCSS:a.toCSS,eval:function(c){var d,e=!1,f=this.name;"string"!=typeof f&&(f=1===f.length&&f[0]instanceof a.Keyword?f[0].value:b(c,f)),"font"!==f||c.strictMath||(e=!0,c.strictMath=!0);try{if(d=this.value.eval(c),!this.variable&&"DetachedRuleset"===d.type)throw{message:"Rulesets cannot be evaluated on a property.",index:this.index,filename:this.currentFileInfo.filename};return new a.Rule(f,d,this.important,this.merge,this.index,this.currentFileInfo,this.inline)}catch(g){throw"number"!=typeof g.index&&(g.index=this.index,g.filename=this.currentFileInfo.filename),g}finally{e&&(c.strictMath=!1)}},makeImportant:function(){return new a.Rule(this.name,this.value,"!important",this.merge,this.index,this.currentFileInfo,this.inline)}}}(c("../tree")),function(a){a.RulesetCall=function(a){this.variable=a},a.RulesetCall.prototype={type:"RulesetCall",accept:function(){},eval:function(b){var c=new a.Variable(this.variable).eval(b);return c.callEval(b)}}}(c("../tree")),function(a){a.Ruleset=function(a,b,c){this.selectors=a,this.rules=b,this._lookups={},this.strictImports=c},a.Ruleset.prototype={type:"Ruleset",accept:function(a){this.paths?a.visitArray(this.paths,!0):this.selectors&&(this.selectors=a.visitArray(this.selectors)),this.rules&&this.rules.length&&(this.rules=a.visitArray(this.rules))},eval:function(b){var c,d,e,f,g=this.selectors,h=a.defaultFunc,i=!1;if(g&&(d=g.length)){for(c=[],h.error({type:"Syntax",message:"it is currently only allowed in parametric mixin guards,"}),f=0;d>f;f++)e=g[f].eval(b),c.push(e),e.evaldCondition&&(i=!0);h.reset()}else i=!0;var j,k,l=this.rules?this.rules.slice(0):null,m=new a.Ruleset(c,l,this.strictImports);m.originalRuleset=this,m.root=this.root,m.firstRoot=this.firstRoot,m.allowImports=this.allowImports,this.debugInfo&&(m.debugInfo=this.debugInfo),i||(l.length=0);var n=b.frames;n.unshift(m);var o=b.selectors;o||(b.selectors=o=[]),o.unshift(this.selectors),(m.root||m.allowImports||!m.strictImports)&&m.evalImports(b);var p=m.rules,q=p?p.length:0;for(f=0;q>f;f++)(p[f]instanceof a.mixin.Definition||p[f]instanceof a.DetachedRuleset)&&(p[f]=p[f].eval(b));var r=b.mediaBlocks&&b.mediaBlocks.length||0;for(f=0;q>f;f++)p[f]instanceof a.mixin.Call?(l=p[f].eval(b).filter(function(b){return b instanceof a.Rule&&b.variable?!m.variable(b.name):!0}),p.splice.apply(p,[f,1].concat(l)),q+=l.length-1,f+=l.length-1,m.resetCache()):p[f]instanceof a.RulesetCall&&(l=p[f].eval(b).rules.filter(function(b){return b instanceof a.Rule&&b.variable?!1:!0}),p.splice.apply(p,[f,1].concat(l)),q+=l.length-1,f+=l.length-1,m.resetCache());for(f=0;f<p.length;f++)j=p[f],j instanceof a.mixin.Definition||j instanceof a.DetachedRuleset||(p[f]=j=j.eval?j.eval(b):j);for(f=0;f<p.length;f++)if(j=p[f],j instanceof a.Ruleset&&j.selectors&&1===j.selectors.length&&j.selectors[0].isJustParentSelector()){p.splice(f--,1);for(var s=0;s<j.rules.length;s++)k=j.rules[s],k instanceof a.Rule&&k.variable||p.splice(++f,0,k)}if(n.shift(),o.shift(),b.mediaBlocks)for(f=r;f<b.mediaBlocks.length;f++)b.mediaBlocks[f].bubbleSelectors(c);return m},evalImports:function(b){var c,d,e=this.rules;if(e)for(c=0;c<e.length;c++)e[c]instanceof a.Import&&(d=e[c].eval(b),d&&d.length?(e.splice.apply(e,[c,1].concat(d)),c+=d.length-1):e.splice(c,1,d),this.resetCache())},makeImportant:function(){return new a.Ruleset(this.selectors,this.rules.map(function(a){return a.makeImportant?a.makeImportant():a}),this.strictImports)},matchArgs:function(a){return!a||0===a.length},matchCondition:function(b,c){var d=this.selectors[this.selectors.length-1];return d.evaldCondition?d.condition&&!d.condition.eval(new a.evalEnv(c,c.frames))?!1:!0:!1},resetCache:function(){this._rulesets=null,this._variables=null,this._lookups={}},variables:function(){return this._variables||(this._variables=this.rules?this.rules.reduce(function(b,c){return c instanceof a.Rule&&c.variable===!0&&(b[c.name]=c),b},{}):{}),this._variables},variable:function(a){return this.variables()[a]},rulesets:function(){if(!this.rules)return null;var b,c,d=a.Ruleset,e=a.mixin.Definition,f=[],g=this.rules,h=g.length;for(b=0;h>b;b++)c=g[b],(c instanceof d||c instanceof e)&&f.push(c);return f},prependRule:function(a){var b=this.rules;b?b.unshift(a):this.rules=[a]},find:function(b,c){c=c||this;var d,e=[],f=b.toCSS();return f in this._lookups?this._lookups[f]:(this.rulesets().forEach(function(f){if(f!==c)for(var g=0;g<f.selectors.length;g++)if(d=b.match(f.selectors[g])){b.elements.length>d?Array.prototype.push.apply(e,f.find(new a.Selector(b.elements.slice(d)),c)):e.push(f);break}}),this._lookups[f]=e,e)},genCSS:function(b,c){var d,e,f,g,h,i,j=[],k=[];b.tabLevel=b.tabLevel||0,this.root||b.tabLevel++;var l,m=b.compress?"":Array(b.tabLevel+1).join(" "),n=b.compress?"":Array(b.tabLevel).join(" ");for(d=0;d<this.rules.length;d++)h=this.rules[d],h.rules||h instanceof a.Media||h instanceof a.Directive||this.root&&h instanceof a.Comment?k.push(h):j.push(h);if(!this.root){g=a.debugInfo(b,this,n),g&&(c.add(g),c.add(n));var o,p=this.paths,q=p.length;for(l=b.compress?",":",\n"+n,d=0;q>d;d++)if(i=p[d],o=i.length)for(d>0&&c.add(l),b.firstSelector=!0,i[0].genCSS(b,c),b.firstSelector=!1,e=1;o>e;e++)i[e].genCSS(b,c);c.add((b.compress?"{":" {\n")+m)}for(d=0;d<j.length;d++)h=j[d],d+1!==j.length||this.root&&0!==k.length&&!this.firstRoot||(b.lastRule=!0),h.genCSS?h.genCSS(b,c):h.value&&c.add(h.value.toString()),b.lastRule?b.lastRule=!1:c.add(b.compress?"":"\n"+m);if(this.root||(c.add(b.compress?"}":"\n"+n+"}"),b.tabLevel--),l=(b.compress?"":"\n")+(this.root?m:n),f=k.length)for(j.length&&l&&c.add(l),k[0].genCSS(b,c),d=1;f>d;d++)l&&c.add(l),k[d].genCSS(b,c);c.isEmpty()||b.compress||!this.firstRoot||c.add("\n")},toCSS:a.toCSS,markReferenced:function(){if(this.selectors)for(var a=0;a<this.selectors.length;a++)this.selectors[a].markReferenced()},joinSelectors:function(a,b,c){for(var d=0;d<c.length;d++)this.joinSelector(a,b,c[d])},joinSelector:function(b,c,d){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,s;for(e=0;e<d.elements.length;e++)j=d.elements[e],"&"===j.value&&(h=!0);if(h){for(r=[],i=[[]],e=0;e<d.elements.length;e++)if(j=d.elements[e],"&"!==j.value)r.push(j);else{for(s=[],r.length>0&&this.mergeElementsOnToSelectors(r,i),f=0;f<i.length;f++)if(k=i[f],0===c.length)k.length>0&&(k[0].elements=k[0].elements.slice(0),k[0].elements.push(new a.Element(j.combinator,"",j.index,j.currentFileInfo))),s.push(k);else for(g=0;g<c.length;g++)l=c[g],m=[],n=[],p=!0,k.length>0?(m=k.slice(0),q=m.pop(),o=d.createDerived(q.elements.slice(0)),p=!1):o=d.createDerived([]),l.length>1&&(n=n.concat(l.slice(1))),l.length>0&&(p=!1,o.elements.push(new a.Element(j.combinator,l[0].elements[0].value,j.index,j.currentFileInfo)),o.elements=o.elements.concat(l[0].elements.slice(1))),p||m.push(o),m=m.concat(n),s.push(m);i=s,r=[]}for(r.length>0&&this.mergeElementsOnToSelectors(r,i),e=0;e<i.length;e++)i[e].length>0&&b.push(i[e])}else if(c.length>0)for(e=0;e<c.length;e++)b.push(c[e].concat(d));else b.push([d])},mergeElementsOnToSelectors:function(b,c){var d,e;if(0===c.length)return void c.push([new a.Selector(b)]);for(d=0;d<c.length;d++)e=c[d],e.length>0?e[e.length-1]=e[e.length-1].createDerived(e[e.length-1].elements.concat(b)):e.push(new a.Selector(b))}}}(c("../tree")),function(a){a.Selector=function(a,b,c,d,e,f){this.elements=a,this.extendList=b,this.condition=c,this.currentFileInfo=e||{},this.isReferenced=f,c||(this.evaldCondition=!0)},a.Selector.prototype={type:"Selector",accept:function(a){this.elements&&(this.elements=a.visitArray(this.elements)),this.extendList&&(this.extendList=a.visitArray(this.extendList)),this.condition&&(this.condition=a.visit(this.condition))},createDerived:function(b,c,d){d=null!=d?d:this.evaldCondition;var e=new a.Selector(b,c||this.extendList,null,this.index,this.currentFileInfo,this.isReferenced);return e.evaldCondition=d,e.mediaEmpty=this.mediaEmpty,e},match:function(a){var b,c,d=this.elements,e=d.length;if(a.CacheElements(),b=a._elements.length,0===b||b>e)return 0;for(c=0;b>c;c++)if(d[c].value!==a._elements[c])return 0;return b},CacheElements:function(){var a,b,c,d="";if(!this._elements){for(a=this.elements.length,c=0;a>c;c++)if(b=this.elements[c],d+=b.combinator.value,b.value.value){if("string"!=typeof b.value.value){d="";break}d+=b.value.value}else d+=b.value;this._elements=d.match(/[,&#\.\w-]([\w-]|(\\.))*/g),this._elements?"&"===this._elements[0]&&this._elements.shift():this._elements=[]}},isJustParentSelector:function(){return!this.mediaEmpty&&1===this.elements.length&&"&"===this.elements[0].value&&(" "===this.elements[0].combinator.value||""===this.elements[0].combinator.value)},eval:function(a){var b=this.condition&&this.condition.eval(a),c=this.elements,d=this.extendList;return c=c&&c.map(function(b){return b.eval(a)}),d=d&&d.map(function(b){return b.eval(a)}),this.createDerived(c,d,b)},genCSS:function(a,b){var c,d;if(a&&a.firstSelector||""!==this.elements[0].combinator.value||b.add(" ",this.currentFileInfo,this.index),!this._css)for(c=0;c<this.elements.length;c++)d=this.elements[c],d.genCSS(a,b)},toCSS:a.toCSS,markReferenced:function(){this.isReferenced=!0},getIsReferenced:function(){return!this.currentFileInfo.reference||this.isReferenced},getIsOutput:function(){return this.evaldCondition}}}(c("../tree")),function(a){a.UnicodeDescriptor=function(a){this.value=a},a.UnicodeDescriptor.prototype={type:"UnicodeDescriptor",genCSS:function(a,b){b.add(this.value)},toCSS:a.toCSS,eval:function(){return this}}}(c("../tree")),function(a){a.URL=function(a,b,c){this.value=a,this.currentFileInfo=b,this.isEvald=c},a.URL.prototype={type:"Url",accept:function(a){this.value=a.visit(this.value)},genCSS:function(a,b){b.add("url("),this.value.genCSS(a,b),b.add(")")},toCSS:a.toCSS,eval:function(b){var c,d=this.value.eval(b);if(!this.isEvald&&(c=this.currentFileInfo&&this.currentFileInfo.rootpath,c&&"string"==typeof d.value&&b.isPathRelative(d.value)&&(d.quote||(c=c.replace(/[\(\)'"\s]/g,function(a){return"\\"+a})),d.value=c+d.value),d.value=b.normalizePath(d.value),b.urlArgs&&!d.value.match(/^\s*data:/))){var e=-1===d.value.indexOf("?")?"?":"&",f=e+b.urlArgs;-1!==d.value.indexOf("#")?d.value=d.value.replace("#",f+"#"):d.value+=f}return new a.URL(d,this.currentFileInfo,!0)}}}(c("../tree")),function(a){a.Value=function(a){this.value=a},a.Value.prototype={type:"Value",accept:function(a){this.value&&(this.value=a.visitArray(this.value))},eval:function(b){return 1===this.value.length?this.value[0].eval(b):new a.Value(this.value.map(function(a){return a.eval(b)}))},genCSS:function(a,b){var c;for(c=0;c<this.value.length;c++)this.value[c].genCSS(a,b),c+1<this.value.length&&b.add(a&&a.compress?",":", ")},toCSS:a.toCSS}}(c("../tree")),function(a){a.Variable=function(a,b,c){this.name=a,this.index=b,this.currentFileInfo=c||{}},a.Variable.prototype={type:"Variable",eval:function(b){var c,d=this.name;if(0===d.indexOf("@@")&&(d="@"+new a.Variable(d.slice(1)).eval(b).value),this.evaluating)throw{type:"Name",message:"Recursive variable definition for "+d,filename:this.currentFileInfo.file,index:this.index};if(this.evaluating=!0,c=a.find(b.frames,function(a){var c=a.variable(d);return c?c.value.eval(b):void 0}))return this.evaluating=!1,c;throw{type:"Name",message:"variable "+d+" is undefined",filename:this.currentFileInfo.filename,index:this.index}}}}(c("../tree")),function(a){var b=["paths","optimization","files","contents","contentsIgnoredChars","relativeUrls","rootpath","strictImports","insecure","dumpLineNumbers","compress","processImports","syncImport","javascriptEnabled","mime","useFileCache","currentFileInfo"];a.parseEnv=function(a){if(d(a,this,b),this.contents||(this.contents={}),this.contentsIgnoredChars||(this.contentsIgnoredChars={}),this.files||(this.files={}),!this.currentFileInfo){var c=a&&a.filename||"input",e=c.replace(/[^\/\\]*$/,"");a&&(a.filename=null),this.currentFileInfo={filename:c,relativeUrls:this.relativeUrls,rootpath:a&&a.rootpath||"",currentDirectory:e,entryPath:e,rootFilename:c}}};var c=["silent","verbose","compress","yuicompress","ieCompat","strictMath","strictUnits","cleancss","sourceMap","importMultiple","urlArgs"];a.evalEnv=function(a,b){d(a,this,c),this.frames=b||[]},a.evalEnv.prototype.inParenthesis=function(){this.parensStack||(this.parensStack=[]),this.parensStack.push(!0)},a.evalEnv.prototype.outOfParenthesis=function(){this.parensStack.pop()},a.evalEnv.prototype.isMathOn=function(){return this.strictMath?this.parensStack&&this.parensStack.length:!0},a.evalEnv.prototype.isPathRelative=function(a){return!/^(?:[a-z-]+:|\/)/.test(a)},a.evalEnv.prototype.normalizePath=function(a){var b,c=a.split("/").reverse();for(a=[];0!==c.length;)switch(b=c.pop()){case".":break;case"..":0===a.length||".."===a[a.length-1]?a.push(b):a.pop();break;default:a.push(b)}return a.join("/")};var d=function(a,b,c){if(a)for(var d=0;d<c.length;d++)a.hasOwnProperty(c[d])&&(b[c[d]]=a[c[d]])}}(c("./tree")),function(a){function b(a){return a}function c(a,b){var d,e;for(d in a)if(a.hasOwnProperty(d))switch(e=a[d],typeof e){case"function":e.prototype&&e.prototype.type&&(e.prototype.typeIndex=b++);break;case"object":b=c(e,b)}return b}var d={visitDeeper:!0},e=!1;a.visitor=function(b){this._implementation=b,this._visitFnCache=[],e||(c(a,1),e=!0)},a.visitor.prototype={visit:function(a){if(!a)return a;var c=a.typeIndex;if(!c)return a;var e,f=this._visitFnCache,g=this._implementation,h=c<<1,i=1|h,j=f[h],k=f[i],l=d;if(l.visitDeeper=!0,j||(e="visit"+a.type,j=g[e]||b,k=g[e+"Out"]||b,f[h]=j,f[i]=k),j!==b){var m=j.call(g,a,l);g.isReplacing&&(a=m)}return l.visitDeeper&&a&&a.accept&&a.accept(this),k!=b&&k.call(g,a),a},visitArray:function(a,b){if(!a)return a;var c,d=a.length;if(b||!this._implementation.isReplacing){for(c=0;d>c;c++)this.visit(a[c]);return a}var e=[];for(c=0;d>c;c++){var f=this.visit(a[c]);f.splice?f.length&&this.flatten(f,e):e.push(f)}return e},flatten:function(a,b){b||(b=[]);var c,d,e,f,g,h;for(d=0,c=a.length;c>d;d++)if(e=a[d],e.splice)for(g=0,f=e.length;f>g;g++)h=e[g],h.splice?h.length&&this.flatten(h,b):b.push(h);else b.push(e);return b}}}(c("./tree")),function(a){a.importVisitor=function(b,c,d,e,f){if(this._visitor=new a.visitor(this),this._importer=b,this._finish=c,this.env=d||new a.evalEnv,this.importCount=0,this.onceFileDetectionMap=e||{},this.recursionDetector={},f)for(var g in f)f.hasOwnProperty(g)&&(this.recursionDetector[g]=!0)},a.importVisitor.prototype={isReplacing:!0,run:function(a){var b;try{this._visitor.visit(a)}catch(c){b=c}this.isFinished=!0,0===this.importCount&&this._finish(b)},visitImport:function(b,c){var d,e=this,f=b.options.inline;if(!b.css||f){try{d=b.evalForImport(this.env)}catch(g){g.filename||(g.index=b.index,g.filename=b.currentFileInfo.filename),b.css=!0,b.error=g}if(d&&(!d.css||f)){b=d,this.importCount++;var h=new a.evalEnv(this.env,this.env.frames.slice(0));b.options.multiple&&(h.importMultiple=!0),this._importer.push(b.getPath(),b.currentFileInfo,b.options,function(c,d,g,i){c&&!c.filename&&(c.index=b.index,c.filename=b.currentFileInfo.filename),h.importMultiple||(b.skip=g?!0:function(){return i in e.onceFileDetectionMap?!0:(e.onceFileDetectionMap[i]=!0,!1)});var j=function(a){e.importCount--,0===e.importCount&&e.isFinished&&e._finish(a)};if(d){b.root=d,b.importedFilename=i;var k=g||i in e.recursionDetector;if(!f&&(h.importMultiple||!k))return e.recursionDetector[i]=!0,void new a.importVisitor(e._importer,j,h,e.onceFileDetectionMap,e.recursionDetector).run(d)}j()})}}return c.visitDeeper=!1,b},visitRule:function(a,b){return b.visitDeeper=!1,a},visitDirective:function(a){return this.env.frames.unshift(a),a},visitDirectiveOut:function(){this.env.frames.shift()},visitMixinDefinition:function(a){return this.env.frames.unshift(a),a},visitMixinDefinitionOut:function(){this.env.frames.shift()},visitRuleset:function(a){return this.env.frames.unshift(a),a},visitRulesetOut:function(){this.env.frames.shift()},visitMedia:function(a){return this.env.frames.unshift(a.ruleset),a},visitMediaOut:function(){this.env.frames.shift()}}}(c("./tree")),function(a){a.joinSelectorVisitor=function(){this.contexts=[[]],this._visitor=new a.visitor(this)},a.joinSelectorVisitor.prototype={run:function(a){return this._visitor.visit(a)},visitRule:function(a,b){b.visitDeeper=!1},visitMixinDefinition:function(a,b){b.visitDeeper=!1},visitRuleset:function(a){var b,c=this.contexts[this.contexts.length-1],d=[];this.contexts.push(d),a.root||(b=a.selectors,b&&(b=b.filter(function(a){return a.getIsOutput()}),a.selectors=b.length?b:b=null,b&&a.joinSelectors(d,c,b)),b||(a.rules=null),a.paths=d)},visitRulesetOut:function(){this.contexts.length=this.contexts.length-1},visitMedia:function(a){var b=this.contexts[this.contexts.length-1];a.rules[0].root=0===b.length||b[0].multiMedia}}}(c("./tree")),function(a){a.toCSSVisitor=function(b){this._visitor=new a.visitor(this),this._env=b},a.toCSSVisitor.prototype={isReplacing:!0,run:function(a){return this._visitor.visit(a)},visitRule:function(a){return a.variable?[]:a},visitMixinDefinition:function(a){return a.frames=[],[]},visitExtend:function(){return[]},visitComment:function(a){return a.isSilent(this._env)?[]:a},visitMedia:function(a,b){return a.accept(this._visitor),b.visitDeeper=!1,a.rules.length?a:[]},visitDirective:function(b){if(b.currentFileInfo.reference&&!b.isReferenced)return[];if("@charset"===b.name){if(this.charset){if(b.debugInfo){var c=new a.Comment("/* "+b.toCSS(this._env).replace(/\n/g,"")+" */\n");return c.debugInfo=b.debugInfo,this._visitor.visit(c)}return[]}this.charset=!0}return b},checkPropertiesInRoot:function(b){for(var c,d=0;d<b.length;d++)if(c=b[d],c instanceof a.Rule&&!c.variable)throw{message:"properties must be inside selector blocks, they cannot be in the root.",index:c.index,filename:c.currentFileInfo?c.currentFileInfo.filename:null}},visitRuleset:function(b,c){var d,e=[];if(b.firstRoot&&this.checkPropertiesInRoot(b.rules),b.root)b.accept(this._visitor),c.visitDeeper=!1,(b.firstRoot||b.rules&&b.rules.length>0)&&e.splice(0,0,b);else{b.paths&&(b.paths=b.paths.filter(function(b){var c;for(" "===b[0].elements[0].combinator.value&&(b[0].elements[0].combinator=new a.Combinator("")),c=0;c<b.length;c++)if(b[c].getIsReferenced()&&b[c].getIsOutput())return!0;return!1}));for(var f=b.rules,g=f?f.length:0,h=0;g>h;)d=f[h],d&&d.rules?(e.push(this._visitor.visit(d)),f.splice(h,1),g--):h++;g>0?b.accept(this._visitor):b.rules=null,c.visitDeeper=!1,f=b.rules,f&&(this._mergeRules(f),f=b.rules),f&&(this._removeDuplicateRules(f),f=b.rules),f&&f.length>0&&b.paths.length>0&&e.splice(0,0,b)}return 1===e.length?e[0]:e},_removeDuplicateRules:function(b){if(b){var c,d,e,f={};for(e=b.length-1;e>=0;e--)if(d=b[e],d instanceof a.Rule)if(f[d.name]){c=f[d.name],c instanceof a.Rule&&(c=f[d.name]=[f[d.name].toCSS(this._env)]);var g=d.toCSS(this._env);-1!==c.indexOf(g)?b.splice(e,1):c.push(g)}else f[d.name]=d}},_mergeRules:function(b){if(b){for(var c,d,e,f={},g=0;g<b.length;g++)d=b[g],d instanceof a.Rule&&d.merge&&(e=[d.name,d.important?"!":""].join(","),f[e]?b.splice(g--,1):f[e]=[],f[e].push(d));Object.keys(f).map(function(b){function e(b){return new a.Expression(b.map(function(a){return a.value}))}function g(b){return new a.Value(b.map(function(a){return a}))}if(c=f[b],c.length>1){d=c[0];var h=[],i=[];c.map(function(a){"+"===a.merge&&(i.length>0&&h.push(e(i)),i=[]),i.push(a)}),h.push(e(i)),d.value=g(h)}})}}}}(c("./tree")),function(a){a.extendFinderVisitor=function(){this._visitor=new a.visitor(this),this.contexts=[],this.allExtendsStack=[[]]},a.extendFinderVisitor.prototype={run:function(a){return a=this._visitor.visit(a),a.allExtends=this.allExtendsStack[0],a},visitRule:function(a,b){b.visitDeeper=!1},visitMixinDefinition:function(a,b){b.visitDeeper=!1},visitRuleset:function(b){if(!b.root){var c,d,e,f,g=[],h=b.rules,i=h?h.length:0;for(c=0;i>c;c++)b.rules[c]instanceof a.Extend&&(g.push(h[c]),b.extendOnEveryPath=!0);var j=b.paths;for(c=0;c<j.length;c++){var k=j[c],l=k[k.length-1],m=l.extendList;for(f=m?m.slice(0).concat(g):g,f&&(f=f.map(function(a){return a.clone()})),d=0;d<f.length;d++)this.foundExtends=!0,e=f[d],e.findSelfSelectors(k),e.ruleset=b,0===d&&(e.firstExtendOnThisSelectorPath=!0),this.allExtendsStack[this.allExtendsStack.length-1].push(e)}this.contexts.push(b.selectors)}},visitRulesetOut:function(a){a.root||(this.contexts.length=this.contexts.length-1)},visitMedia:function(a){a.allExtends=[],this.allExtendsStack.push(a.allExtends)},visitMediaOut:function(){this.allExtendsStack.length=this.allExtendsStack.length-1},visitDirective:function(a){a.allExtends=[],this.allExtendsStack.push(a.allExtends)},visitDirectiveOut:function(){this.allExtendsStack.length=this.allExtendsStack.length-1}},a.processExtendsVisitor=function(){this._visitor=new a.visitor(this)},a.processExtendsVisitor.prototype={run:function(b){var c=new a.extendFinderVisitor;return c.run(b),c.foundExtends?(b.allExtends=b.allExtends.concat(this.doExtendChaining(b.allExtends,b.allExtends)),this.allExtendsStack=[b.allExtends],this._visitor.visit(b)):b},doExtendChaining:function(b,c,d){var e,f,g,h,i,j,k,l,m=[],n=this;for(d=d||0,e=0;e<b.length;e++)for(f=0;f<c.length;f++)j=b[e],k=c[f],j.parent_ids.indexOf(k.object_id)>=0||(i=[k.selfSelectors[0]],g=n.findMatch(j,i),g.length&&j.selfSelectors.forEach(function(b){h=n.extendSelector(g,i,b),l=new a.Extend(k.selector,k.option,0),l.selfSelectors=h,h[h.length-1].extendList=[l],m.push(l),l.ruleset=k.ruleset,l.parent_ids=l.parent_ids.concat(k.parent_ids,j.parent_ids),k.firstExtendOnThisSelectorPath&&(l.firstExtendOnThisSelectorPath=!0,k.ruleset.paths.push(h))}));if(m.length){if(this.extendChainCount++,d>100){var o="{unable to calculate}",p="{unable to calculate}";try{o=m[0].selfSelectors[0].toCSS(),p=m[0].selector.toCSS()}catch(q){}throw{message:"extend circular reference detected. One of the circular extends is currently:"+o+":extend("+p+")"}}return m.concat(n.doExtendChaining(m,c,d+1))}return m},visitRule:function(a,b){b.visitDeeper=!1},visitMixinDefinition:function(a,b){b.visitDeeper=!1},visitSelector:function(a,b){b.visitDeeper=!1},visitRuleset:function(a){if(!a.root){var b,c,d,e,f=this.allExtendsStack[this.allExtendsStack.length-1],g=[],h=this;for(d=0;d<f.length;d++)for(c=0;c<a.paths.length;c++)if(e=a.paths[c],!a.extendOnEveryPath){var i=e[e.length-1].extendList;i&&i.length||(b=this.findMatch(f[d],e),b.length&&f[d].selfSelectors.forEach(function(a){g.push(h.extendSelector(b,e,a))}))}a.paths=a.paths.concat(g)}},findMatch:function(a,b){var c,d,e,f,g,h,i,j=this,k=a.selector.elements,l=[],m=[];for(c=0;c<b.length;c++)for(d=b[c],e=0;e<d.elements.length;e++)for(f=d.elements[e],(a.allowBefore||0===c&&0===e)&&l.push({pathIndex:c,index:e,matched:0,initialCombinator:f.combinator}),h=0;h<l.length;h++)i=l[h],g=f.combinator.value,""===g&&0===e&&(g=" "),!j.isElementValuesEqual(k[i.matched].value,f.value)||i.matched>0&&k[i.matched].combinator.value!==g?i=null:i.matched++,i&&(i.finished=i.matched===k.length,i.finished&&!a.allowAfter&&(e+1<d.elements.length||c+1<b.length)&&(i=null)),i?i.finished&&(i.length=k.length,i.endPathIndex=c,i.endPathElementIndex=e+1,l.length=0,m.push(i)):(l.splice(h,1),h--);return m},isElementValuesEqual:function(b,c){if("string"==typeof b||"string"==typeof c)return b===c;if(b instanceof a.Attribute)return b.op!==c.op||b.key!==c.key?!1:b.value&&c.value?(b=b.value.value||b.value,c=c.value.value||c.value,b===c):b.value||c.value?!1:!0;if(b=b.value,c=c.value,b instanceof a.Selector){if(!(c instanceof a.Selector)||b.elements.length!==c.elements.length)return!1;for(var d=0;d<b.elements.length;d++){if(b.elements[d].combinator.value!==c.elements[d].combinator.value&&(0!==d||(b.elements[d].combinator.value||" ")!==(c.elements[d].combinator.value||" ")))return!1;if(!this.isElementValuesEqual(b.elements[d].value,c.elements[d].value))return!1}return!0}return!1},extendSelector:function(b,c,d){var e,f,g,h,i,j=0,k=0,l=[];for(e=0;e<b.length;e++)h=b[e],f=c[h.pathIndex],g=new a.Element(h.initialCombinator,d.elements[0].value,d.elements[0].index,d.elements[0].currentFileInfo),h.pathIndex>j&&k>0&&(l[l.length-1].elements=l[l.length-1].elements.concat(c[j].elements.slice(k)),k=0,j++),i=f.elements.slice(k,h.index).concat([g]).concat(d.elements.slice(1)),j===h.pathIndex&&e>0?l[l.length-1].elements=l[l.length-1].elements.concat(i):(l=l.concat(c.slice(j,h.pathIndex)),l.push(new a.Selector(i))),j=h.endPathIndex,k=h.endPathElementIndex,k>=c[j].elements.length&&(k=0,j++);
+return j<c.length&&k>0&&(l[l.length-1].elements=l[l.length-1].elements.concat(c[j].elements.slice(k)),j++),l=l.concat(c.slice(j,c.length))},visitRulesetOut:function(){},visitMedia:function(a){var b=a.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);b=b.concat(this.doExtendChaining(b,a.allExtends)),this.allExtendsStack.push(b)},visitMediaOut:function(){this.allExtendsStack.length=this.allExtendsStack.length-1},visitDirective:function(a){var b=a.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);b=b.concat(this.doExtendChaining(b,a.allExtends)),this.allExtendsStack.push(b)},visitDirectiveOut:function(){this.allExtendsStack.length=this.allExtendsStack.length-1}}}(c("./tree")),function(a){a.sourceMapOutput=function(a){this._css=[],this._rootNode=a.rootNode,this._writeSourceMap=a.writeSourceMap,this._contentsMap=a.contentsMap,this._contentsIgnoredCharsMap=a.contentsIgnoredCharsMap,this._sourceMapFilename=a.sourceMapFilename,this._outputFilename=a.outputFilename,this._sourceMapURL=a.sourceMapURL,a.sourceMapBasepath&&(this._sourceMapBasepath=a.sourceMapBasepath.replace(/\\/g,"/")),this._sourceMapRootpath=a.sourceMapRootpath,this._outputSourceFiles=a.outputSourceFiles,this._sourceMapGeneratorConstructor=a.sourceMapGenerator||c("source-map").SourceMapGenerator,this._sourceMapRootpath&&"/"!==this._sourceMapRootpath.charAt(this._sourceMapRootpath.length-1)&&(this._sourceMapRootpath+="/"),this._lineNumber=0,this._column=0},a.sourceMapOutput.prototype.normalizeFilename=function(a){return a=a.replace(/\\/g,"/"),this._sourceMapBasepath&&0===a.indexOf(this._sourceMapBasepath)&&(a=a.substring(this._sourceMapBasepath.length),("\\"===a.charAt(0)||"/"===a.charAt(0))&&(a=a.substring(1))),(this._sourceMapRootpath||"")+a},a.sourceMapOutput.prototype.add=function(a,b,c,d){if(a){var e,f,g,h,i;if(b){var j=this._contentsMap[b.filename];this._contentsIgnoredCharsMap[b.filename]&&(c-=this._contentsIgnoredCharsMap[b.filename],0>c&&(c=0),j=j.slice(this._contentsIgnoredCharsMap[b.filename])),j=j.substring(0,c),f=j.split("\n"),h=f[f.length-1]}if(e=a.split("\n"),g=e[e.length-1],b)if(d)for(i=0;i<e.length;i++)this._sourceMapGenerator.addMapping({generated:{line:this._lineNumber+i+1,column:0===i?this._column:0},original:{line:f.length+i,column:0===i?h.length:0},source:this.normalizeFilename(b.filename)});else this._sourceMapGenerator.addMapping({generated:{line:this._lineNumber+1,column:this._column},original:{line:f.length,column:h.length},source:this.normalizeFilename(b.filename)});1===e.length?this._column+=g.length:(this._lineNumber+=e.length-1,this._column=g.length),this._css.push(a)}},a.sourceMapOutput.prototype.isEmpty=function(){return 0===this._css.length},a.sourceMapOutput.prototype.toCSS=function(a){if(this._sourceMapGenerator=new this._sourceMapGeneratorConstructor({file:this._outputFilename,sourceRoot:null}),this._outputSourceFiles)for(var b in this._contentsMap)if(this._contentsMap.hasOwnProperty(b)){var c=this._contentsMap[b];this._contentsIgnoredCharsMap[b]&&(c=c.slice(this._contentsIgnoredCharsMap[b])),this._sourceMapGenerator.setSourceContent(this.normalizeFilename(b),c)}if(this._rootNode.genCSS(a,this),this._css.length>0){var d,e=JSON.stringify(this._sourceMapGenerator.toJSON());this._sourceMapURL?d=this._sourceMapURL:this._sourceMapFilename&&(d=this.normalizeFilename(this._sourceMapFilename)),this._writeSourceMap?this._writeSourceMap(e):d="data:application/json,"+encodeURIComponent(e),d&&this._css.push("/*# sourceMappingURL="+d+" */")}return this._css.join("")}}(c("./tree"));var y=/^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);w.env=w.env||("127.0.0.1"==location.hostname||"0.0.0.0"==location.hostname||"localhost"==location.hostname||location.port&&location.port.length>0||y?"development":"production");var z={debug:3,info:2,errors:1,none:0};if(w.logLevel="undefined"!=typeof w.logLevel?w.logLevel:"development"===w.env?z.debug:z.errors,w.async=w.async||!1,w.fileAsync=w.fileAsync||!1,w.poll=w.poll||(y?1e3:1500),w.functions)for(var A in w.functions)w.functions.hasOwnProperty(A)&&(w.tree.functions[A]=w.functions[A]);var B=/!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);B&&(w.dumpLineNumbers=B[1]);var C=/^text\/(x-)?less$/,D=null,E={};if(w.watch=function(){return w.watchMode||(w.env="development",v()),this.watchMode=!0,!0},w.unwatch=function(){return clearInterval(w.watchTimer),this.watchMode=!1,!1},/!watch/.test(location.hash)&&w.watch(),"development"!=w.env)try{D="undefined"==typeof a.localStorage?null:a.localStorage}catch(F){}var G=document.getElementsByTagName("link");w.sheets=[];for(var H=0;H<G.length;H++)("stylesheet/less"===G[H].rel||G[H].rel.match(/stylesheet/)&&G[H].type.match(C))&&w.sheets.push(G[H]);w.modifyVars=function(a){w.refresh(!1,a)},w.refresh=function(a,b){var c,e;c=e=new Date,u(function(a,b,f,i,k){if(a)return j(a,i.href);if(k.local)d("loading "+i.href+" from cache.",z.info);else{d("parsed "+i.href+" successfully.",z.debug);var l=b.toCSS(w);l=h(l),g(l,i,k.lastModified)}d("css for "+i.href+" generated in "+(new Date-e)+"ms",z.info),0===k.remaining&&d("less has finished. css generated in "+(new Date-c)+"ms",z.info),e=new Date},a,b),n(b)},w.refreshStyles=n,w.Parser.fileLoader=s,w.refresh("development"===w.env),"function"==typeof define&&define.amd&&define(function(){return w})}(window);
diff --git a/askbot/media/js/live_search.js b/askbot/media/js/live_search.js
index f6eecff1..e833059f 100644
--- a/askbot/media/js/live_search.js
+++ b/askbot/media/js/live_search.js
@@ -162,6 +162,7 @@ SearchDropMenu.prototype.showWaitIcon = function() {
this._footer.hide();
this._element.addClass('empty');
}
+ this._element.addClass('waiting');
};
SearchDropMenu.prototype.hideWaitIcon = function() {
@@ -170,6 +171,7 @@ SearchDropMenu.prototype.hideWaitIcon = function() {
this._footer.show();
this._element.removeClass('empty');
}
+ this._element.removeClass('waiting');
};
SearchDropMenu.prototype.hideHeader = function() {
@@ -896,7 +898,7 @@ FullTextSearch.prototype.decorate = function(element) {
dropMenu.setAskHandler(this.makeAskHandler());
dropMenu.setAskButtonEnabled(this._askButtonEnabled);
this._dropMenu = dropMenu;
- element.parent().after(this._dropMenu.getElement());
+ element.parent().append(this._dropMenu.getElement());
$(element).click(function(e) { return false });
$(document).click(function() { dropMenu.reset(); });
diff --git a/askbot/media/js/post.js b/askbot/media/js/post.js
index 519b1c81..d077ce9e 100644
--- a/askbot/media/js/post.js
+++ b/askbot/media/js/post.js
@@ -83,35 +83,6 @@ function setupFormValidation(form, validationRules, validationMessages, onSubmit
});
}
-/**
- * generic tag cleaning function, settings
- * are from askbot live settings and askbot.const
- */
-var cleanTag = function(tag_name, settings) {
- var tag_regex = new RegExp(settings['tag_regex']);
- if (tag_regex.test(tag_name) === false) {
- throw settings['messages']['wrong_chars']
- }
-
- var max_length = settings['max_tag_length'];
- if (tag_name.length > max_length) {
- throw interpolate(
- ngettext(
- 'must be shorter than %(max_chars)s character',
- 'must be shorter than %(max_chars)s characters',
- max_length
- ),
- {'max_chars': max_length },
- true
- );
- }
- if (settings['force_lowercase_tags']) {
- return tag_name.toLowerCase();
- } else {
- return tag_name;
- }
-};
-
var validateTagLength = function(value){
var tags = getUniqueWords(value);
var are_tags_ok = true;
@@ -171,11 +142,15 @@ var CPValidator = function() {
required: " " + gettext('enter your question'),
minlength: interpolate(
ngettext(
- 'question must have > %s character',
- 'question must have > %s characters',
+ '%(question)s must have > %(length)s character',
+ '%(question)s must have > %(length)s characters',
askbot['settings']['minTitleLength']
),
- [askbot['settings']['minTitleLength'], ]
+ {
+ 'question': askbot['messages']['questionSingular'],
+ 'length': askbot['settings']['minTitleLength']
+ },
+ true
)
}
};
@@ -193,11 +168,15 @@ var CPValidator = function() {
required: " " + gettext('content cannot be empty'),
minlength: interpolate(
ngettext(
- 'answer must be > %s character',
- 'answer must be > %s characters',
+ '%(answer)s must be > %(length)s character',
+ '%(answer)s must be > %(length)s characters',
askbot['settings']['minAnswerBodyLength']
),
- [askbot['settings']['minAnswerBodyLength'], ]
+ {
+ 'answer': askbot['messages']['answerSingular'],
+ 'length': askbot['settings']['minAnswerBodyLength']
+ },
+ true
)
},
}
@@ -546,12 +525,17 @@ var Vote = function(){
var questionSubscribeSidebar= 'question-subscribe-sidebar';
var acceptAnonymousMessage = gettext('insufficient privilege');
- var acceptOwnAnswerMessage = gettext('cannot pick own answer as best');
var pleaseLogin = " <a href='" + askbot['urls']['user_signin'] + ">"
+ gettext('please login') + "</a>";
- var favoriteAnonymousMessage = gettext('anonymous users cannot follow questions') + pleaseLogin;
+ var tmpMsg = interpolate(
+ gettext('anonymous users cannot %(follow_questions)s'),
+ {'follow_questions': askbot['messages']['followQuestions']},
+ true
+ );
+ var favoriteAnonymousMessage = tmpMsg + pleaseLogin;
+ //todo: this below is probably not used
var subscribeAnonymousMessage = gettext('anonymous users cannot subscribe to questions') + pleaseLogin;
var voteAnonymousMessage = gettext('anonymous users cannot vote') + pleaseLogin;
//there were a couple of more messages...
@@ -795,7 +779,12 @@ var Vote = function(){
showMessage(object, acceptAnonymousMessage);
}
else if(data.allowed == "-1"){
- showMessage(object, acceptOwnAnswerMessage);
+ var message = interpolate(
+ gettext('sorry, you cannot %(accept_own_answer)s'),
+ {'accept_own_answer': askbot['messages']['acceptOwnAnswer']},
+ true
+ );
+ showMessage(object, message);
}
else if(data.status == "1"){
$("#"+answerContainerIdPrefix+postId).removeClass("accepted-answer");
@@ -2403,7 +2392,10 @@ PostCommentsWidget.prototype.getActivateHandler = function(){
}
else {
//if user can't post, we tell him something and refuse
- if (askbot['data']['userIsAuthenticated']) {
+ if (askbot['settings']['readOnlyModeEnabled'] === true) {
+ var message = askbot['messages']['readOnlyMessage'];
+ showMessage(button, message, 'after');
+ } else if (askbot['data']['userIsAuthenticated']) {
me.startNewComment();
} else {
var message = gettext('please sign in or register to post comments');
@@ -2514,7 +2506,7 @@ var socialSharing = function(){
URL = window.location.href;
var urlBits = URL.split('/');
URL = urlBits.slice(0, -2).join('/') + '/';
- TEXT = encodeURIComponent($('h1 > a').html());
+ TEXT = encodeURIComponent($('h1 > a').text());
var hashtag = encodeURIComponent(
askbot['settings']['sharingSuffixText']
);
@@ -3388,9 +3380,55 @@ var TagEditor = function() {
WrappedElement.call(this);
this._has_hot_backspace = false;
this._settings = JSON.parse(askbot['settings']['tag_editor']);
+ /*
+ tags: {
+ required: askbot['settings']['tagsAreRequired'],
+ maxlength: askbot['settings']['maxTagsPerPost'] * askbot['settings']['maxTagLength'],
+ limit_tag_count: true,
+ limit_tag_length: true
+ },
+ tags: {
+ required: " " + gettext('tags cannot be empty'),
+ maxlength: askbot['messages']['tagLimits'],
+ limit_tag_count: askbot['messages']['maxTagsPerPost'],
+ limit_tag_length: askbot['messages']['maxTagLength']
+ },
+ */
};
inherits(TagEditor, WrappedElement);
+/* retagger function
+ var doRetag = function(){
+ $.ajax({
+ type: "POST",
+ url: retagUrl,//todo add this url to askbot['urls']
+ dataType: "json",
+ data: { tags: getUniqueWords(tagInput.val()).join(' ') },
+ success: function(json) {
+ if (json['success'] === true){
+ new_tags = getUniqueWords(json['new_tags']);
+ oldTagsHtml = '';
+ cancelRetag();
+ drawNewTags(new_tags.join(' '));
+ if (json['message']) {
+ notify.show(json['message']);
+ }
+ }
+ else {
+ cancelRetag();
+ showMessage(tagsDiv, json['message']);
+ }
+ },
+ error: function(xhr, textStatus, errorThrown) {
+ showMessage(tagsDiv, gettext('sorry, something is not right here'));
+ cancelRetag();
+ }
+ });
+ return false;
+ }
+*/
+
+
TagEditor.prototype.getSelectedTags = function() {
return $.trim(this._hidden_tags_input.val()).split(/\s+/);
};
diff --git a/askbot/media/js/tag_selector.js b/askbot/media/js/tag_selector.js
index dfc8a29a..c905ab86 100644
--- a/askbot/media/js/tag_selector.js
+++ b/askbot/media/js/tag_selector.js
@@ -274,10 +274,16 @@ function pickedTags(){
});
}
+ var tagSettings = JSON.parse(askbot['settings']['tag_editor']);
var clean_tagnames = [];
$.each(tagnames, function(idx, tagname){
if (!(tagname in to_target)){
- clean_tagnames.push(tagname);
+ try {
+ cleanTag(tagname, tagSettings);
+ clean_tagnames.push(tagname);
+ } catch (e) {
+ alert(e);
+ }
}
});
diff --git a/askbot/media/js/user.js b/askbot/media/js/user.js
index b5a999a5..90f35f2f 100644
--- a/askbot/media/js/user.js
+++ b/askbot/media/js/user.js
@@ -1069,6 +1069,47 @@ Tweeting.prototype.decorate = function(element) {
}
};
+var UserQuestionsPaginator = function() {
+ Paginator.call(this);
+};
+inherits(UserQuestionsPaginator, Paginator);
+
+UserQuestionsPaginator.prototype.renderPage = function(data) {
+ $('.users-questions').html(data['questions']);
+ $('.timeago').timeago();
+};
+
+UserQuestionsPaginator.prototype.getPageDataUrl = function(pageNo) {
+ var userId = askbot['data']['viewUserId'];
+ var pageSize = askbot['data']['userPostsPageSize'];
+ var url = QSutils.patch_query_string('', 'author:' + userId);
+ url = QSutils.patch_query_string(url, 'sort:votes-desc');
+ url = QSutils.patch_query_string(url, 'page:' + pageNo);
+ url = QSutils.patch_query_string(url, 'page-size:'+ pageSize);
+ return askbot['urls']['questions'] + url;
+};
+
+var UserAnswersPaginator = function() {
+ Paginator.call(this);
+};
+inherits(UserAnswersPaginator, Paginator);
+
+UserAnswersPaginator.prototype.renderPage = function(data) {
+ $('.users-answers').html(data['html']);
+ $('.timeago').timeago();
+};
+
+UserAnswersPaginator.prototype.getPageDataUrl = function() {
+ return askbot['urls']['getTopAnswers'];
+};
+
+UserAnswersPaginator.prototype.getPageDataUrlParams = function(pageNo) {
+ return {
+ user_id: askbot['data']['viewUserId'],
+ page_number: pageNo
+ }
+};
+
(function(){
var fbtn = $('.follow-user-toggle');
if (fbtn.length === 1){
@@ -1092,4 +1133,17 @@ Tweeting.prototype.decorate = function(element) {
var tweetingControl = new Tweeting();
tweetingControl.decorate(tweeting);
}
+
+ var qPager = $('.user-questions-pager');
+ if (qPager.length) {
+ var qPaginator = new UserQuestionsPaginator();
+ qPaginator.decorate(qPager);
+ }
+
+ var aPager = $('.user-answers-pager');
+ if (aPager.length) {
+ var aPaginator = new UserAnswersPaginator();
+ aPaginator.decorate(aPager);
+ }
+
})();
diff --git a/askbot/media/js/utils.js b/askbot/media/js/utils.js
index 7011063d..a3eb2028 100644
--- a/askbot/media/js/utils.js
+++ b/askbot/media/js/utils.js
@@ -6,6 +6,21 @@ var mediaUrl = function(resource){
return askbot['settings']['static_url'] + 'default' + '/' + resource;
};
+var getCookie = function(name) {
+ var cookieValue = null;
+ if (document.cookie && document.cookie != '') {
+ var cookies = document.cookie.split(';');
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = $.trim(cookies[i]);
+ // Does this cookie string begin with the name we want?
+ if (cookie.substring(0, name.length + 1) == (name + '=')) {
+ return decodeURIComponent(cookie.substring(name.length + 1));
+ }
+ }
+ }
+ return cookieValue;
+};
+
var cleanUrl = function(url){
var re = new RegExp('//', 'g');
return url.replace(re, '/');
@@ -44,6 +59,41 @@ var getNewUniqueInt = function() {
return num;
};
+/**
+ * generic tag cleaning function, settings
+ * are from askbot live settings and askbot.const
+ */
+var cleanTag = function(tag_name, settings) {
+ var tag_regex = new RegExp(settings['tag_regex']);
+ if (tag_regex.test(tag_name) === false) {
+ var firstChar = tag_name.substring(0, 1);
+ if (settings['tag_forbidden_first_chars'].indexOf(firstChar) > -1) {
+ throw settings['messages']['wrong_first_char'];
+ } else {
+ throw settings['messages']['wrong_chars'];
+ }
+ }
+
+ var max_length = settings['max_tag_length'];
+ if (tag_name.length > max_length) {
+ throw interpolate(
+ ngettext(
+ 'must be shorter than %(max_chars)s character',
+ 'must be shorter than %(max_chars)s characters',
+ max_length
+ ),
+ {'max_chars': max_length },
+ true
+ );
+ }
+ if (settings['force_lowercase_tags']) {
+ return tag_name.toLowerCase();
+ } else {
+ return tag_name;
+ }
+};
+
+
var getSingletonController = function(controllerClass, name) {
askbot['controllers'] = askbot['controllers'] || {};
var controller = askbot['controllers'][name];
@@ -242,6 +292,7 @@ var notify = function() {
};
}();
+
/* **************************************************** */
// Search query-string manipulation utils
/* **************************************************** */
@@ -292,6 +343,7 @@ QSutils.patch_query_string = function (query_string, patch, remove) {
add_selector('tags');
add_selector('author');
add_selector('page');
+ add_selector('page-size');
add_selector('query');
return new_query_string;
@@ -501,6 +553,254 @@ WaitIcon.prototype.createDom = function() {
this.setVisible(this._isVisible);
};
+var Paginator = function() {
+ WrappedElement.call(this);
+};
+inherits(Paginator, WrappedElement);
+
+/**
+ * A mandotory method.
+ * this method needs to be implemented by the subclass
+ * @interface
+ * @param data is json dict returted by the server
+ */
+Paginator.prototype.renderPage = function(data) {
+ throw 'implement me in the subclass';
+};
+
+/**
+ * A mandatory method.
+ * @interface - implement in subclass
+ * returns url that can be used to retrieve page data
+ */
+Paginator.prototype.getPageDataUrl = function(pageNo) {
+ throw 'implement me in the subclass';
+};
+
+/**
+ * Optional method
+ * @interface - implement in subclass
+ * returns url parameters for the page request
+ */
+Paginator.prototype.getPageDataUrlParams = function(pageNo) {};
+
+Paginator.prototype.setIsLoading = function(isLoading) {
+ this._isLoading = isLoading;
+};
+
+Paginator.prototype.startLoadingPageData = function(pageNo) {
+ if (this._isLoading) {
+ return;
+ }
+ var me = this;
+ var requestParams = {
+ type: 'GET',
+ dataType: 'json',
+ url: this.getPageDataUrl(pageNo),
+ cache: false,
+ success: function(data) {
+ me.renderPage(data);
+ me.setCurrentPage(pageNo);
+ me.setIsLoading(false);
+ },
+ failure: function() {
+ me.setIsLoading(false);
+ }
+ };
+ var urlParams = this.getPageDataUrlParams(pageNo);
+ if (urlParams) {
+ requestParams['data'] = urlParams;
+ }
+ $.ajax(requestParams);
+ me.setIsLoading(true);
+ return false;
+};
+
+Paginator.prototype.getCurrentPageNo = function() {
+ var page = this._element.find('.curr');
+ return parseInt(page.attr('data-page'));
+};
+
+Paginator.prototype.getIncrementalPageHandler = function(direction) {
+ var me = this;
+ return function() {
+ var pageNo = me.getCurrentPageNo();
+ if (direction === 'next') {
+ pageNo = pageNo + 1;
+ } else {
+ pageNo = pageNo - 1;
+ }
+ me.startLoadingPageData(pageNo);
+ return false;
+ };
+};
+
+Paginator.prototype.getWindowStart = function(pageNo) {
+ var totalPages = this._numPages;
+ var activePages = this._numActivePages;
+
+ //paginator is "short" w/o prev or next, no need to rerender
+ if (totalPages === activePages) {
+ return 1;
+ }
+
+ //we are in leading range
+ if (pageNo < activePages) {
+ return 1;
+ }
+
+ //we are in trailing range
+ var lastWindowStart = totalPages - activePages + 1;
+ if (pageNo > lastWindowStart) {
+ return lastWindowStart;
+ }
+
+ return pageNo - Math.floor(activePages/2);
+};
+
+Paginator.prototype.renderPaginatorWindow = function(windowStart) {
+ var anchors = this._paginatorAnchors;
+ for (var i = 0; i < anchors.length; i++) {
+ var anchor = $(anchors[i]);
+ removeButtonEventHandlers(anchor);
+ var pageNo = windowStart + i;
+ //re-render displayed number
+ anchor.html(pageNo);
+ //re-render the tooltip text
+ var labelTpl = gettext('page %s');
+ anchor.attr('title', interpolate(labelTpl, [pageNo]));
+ //re-render the "page" data value
+ anchor.parent().attr('data-page', pageNo);
+ //setup new event handler
+ var pageHandler = this.getPageButtonHandler(pageNo);
+ setupButtonEventHandlers(anchor, pageHandler);
+ }
+};
+
+Paginator.prototype.renderPaginatorEdges = function(windowStart, pageNo) {
+ //first page button
+ var first = this._firstPageNav;
+ if (windowStart === 1) {
+ first.hide();
+ } else {
+ first.show();
+ }
+
+ //last page button
+ var lastWindowStart = this._numPages - this._numActivePages + 1;
+ var last = this._lastPageNav;
+ if (windowStart === lastWindowStart) {
+ last.hide();
+ } else {
+ last.show();
+ }
+
+ //show or hide "prev" and "next" buttons
+ if (this._numPages === this._numActivePages) {
+ this._prevPageButton.hide();
+ this._nextPageButton.hide();
+ } else {
+ if (pageNo === 1) {
+ this._prevPageButton.hide();
+ } else {
+ this._prevPageButton.show();
+ }
+ if (pageNo === this._numPages) {
+ this._nextPageButton.hide();
+ } else {
+ this._nextPageButton.show();
+ }
+ }
+};
+
+Paginator.prototype.setCurrentPage = function(pageNo) {
+
+ var currPageNo = this.getCurrentPageNo();
+ var currWindow = this.getWindowStart(currPageNo);
+ var newWindow = this.getWindowStart(pageNo);
+ if (newWindow !== currWindow) {
+ this.renderPaginatorWindow(newWindow);
+ }
+
+ //select the current page
+ var page = this._mainPagesNav.find('[data-page="' + pageNo + '"]');
+ if (page.length === 1) {
+ var curr = this._element.find('.curr');
+ curr.removeClass('curr');
+ page.addClass('curr');
+ }
+
+ //show or hide ellipses (...) and the last/first page buttons
+ //newWindow is starting page of the new paginator window
+ this.renderPaginatorEdges(newWindow, pageNo);
+};
+
+Paginator.prototype.createButton = function(cls, label) {
+ var btn = this.makeElement('span');
+ btn.addClass(cls);
+ var link = this.makeElement('a');
+ link.html(label);
+ btn.append(link);
+ return btn;
+};
+
+Paginator.prototype.getPageButtonHandler = function(pageNo) {
+ var me = this;
+ return function() {
+ if (me.getCurrentPageNo() !== pageNo) {
+ me.startLoadingPageData(pageNo);
+ }
+ return false;
+ };
+};
+
+Paginator.prototype.decorate = function(element) {
+ this._element = element;
+ var pages = element.find('.page');
+ this._firstPageNav = element.find('.first-page-nav');
+ this._lastPageNav = element.find('.last-page-nav');
+ this._mainPagesNav = element.find('.main-pages-nav');
+ var paginatorButtons = element.find('.paginator');
+ this._numPages = paginatorButtons.data('numPages');
+
+ var mainNavButtons = element.find('.main-pages-nav a');
+ this._paginatorAnchors = mainNavButtons;
+ this._numActivePages = mainNavButtons.length;
+
+ for (var i = 0; i < pages.length; i++) {
+ var page = $(pages[i]);
+ var pageNo = page.data('page');
+ var link = page.find('a');
+ var pageHandler = this.getPageButtonHandler(pageNo);
+ setupButtonEventHandlers(link, pageHandler);
+ }
+
+ var currPageNo = element.find('.curr').data('page');
+
+ //next page button
+ var nextPage = element.find('.next');
+ this._nextPageButton = nextPage;
+ if (currPageNo === this._numPages) {
+ this._nextPageButton.hide();
+ }
+
+ setupButtonEventHandlers(
+ this._nextPageButton,
+ this.getIncrementalPageHandler('next')
+ );
+
+ var prevPage = element.find('.prev');
+ this._prevPageButton = prevPage;
+ if (currPageNo === 1) {
+ this._prevPageButton.hide();
+ }
+
+ setupButtonEventHandlers(
+ this._prevPageButton,
+ this.getIncrementalPageHandler('prev')
+ );
+};
+
/**
* makes images never take more spaces then they can take
* @param {<Array>} breakPoints
@@ -788,6 +1088,51 @@ TippedInput.prototype.decorate = function(element){
};
/**
+ * creates an alert that will momentarily flash
+ * and then self-destruct
+ */
+var FlashAlert = function(text) {
+ WrappedElement.call(this);
+ this._text = text;
+};
+inherits(FlashAlert, WrappedElement);
+
+FlashAlert.prototype.setPostRunHandler = function(handler) {
+ this._postRunHandler = handler;
+};
+
+FlashAlert.prototype.setText = function(text) {
+ this._text = text;
+ if (this.hasElement()) {
+ this._element.html(text);
+ };
+};
+
+FlashAlert.prototype.run = function() {
+ var element = this._element;
+ var me = this;
+ var postRunHandler = this._postRunHandler;
+ var finish = function() {
+ element.fadeOut();
+ me.dispose();
+ if (postRunHandler) {
+ postRunHandler();
+ }
+ };
+ element.fadeIn(function() { setTimeout(finish, 1000) });
+};
+
+FlashAlert.prototype.createDom = function() {
+ var element = this.makeElement('div');
+ element.addClass('flash-alert');
+ element.hide();
+ this._element = element;
+ if (this._text) {
+ element.html(this._text);
+ }
+};
+
+/**
* will setup a bootstrap.js alert
* programmatically
*/
@@ -928,6 +1273,86 @@ SimpleControl.prototype.decorate = function(element) {
this.setHandlerInternal();
};
+/**
+ * @constructor
+ */
+var PostExpander = function() {
+ SimpleControl.call(this);
+ this._handler = this.getExpandHandler();
+};
+inherits(PostExpander, SimpleControl);
+
+PostExpander.prototype.setPostId = function(postId) {
+ this._postId = postId;
+};
+
+PostExpander.prototype.getPostId = function() {
+ return this._postId;
+};
+
+PostExpander.prototype.showWaitIcon = function() {
+ var icon = new WaitIcon();
+ this._element.html(icon.getElement());
+ icon.show();
+};
+
+PostExpander.prototype.updateTheDots = function() {
+ var numDots = this._cNumDots + 1;
+ if (numDots > this._maxNumDots) {
+ numDots = 1;
+ }
+ this._cNumDots = numDots;
+ var dots = '';
+ for (var i = 0; i < numDots; i++) {
+ dots += '&bull;';
+ }
+ for (; i < this._maxNumDots; i++) {
+ dots += '&nbsp;';
+ }
+ this._element.html(dots);
+};
+
+PostExpander.prototype.stopTheDots = function() {
+ clearInterval(this._dotsInterval);
+};
+
+PostExpander.prototype.startTheDots = function() {
+ var numDots = this._element.find('a').html().length + 2;
+ this._maxNumDots = numDots;
+ this._cNumDots = 0;
+ var dots = '';
+ for (var i = 0; i < numDots; i++) {
+ dots += '&nbsp;';
+ }
+ this._element.html(dots);
+ var me = this;
+ var handler = function() {
+ me.updateTheDots();
+ };
+ this._dotsInterval = setInterval(handler, 150);
+};
+
+PostExpander.prototype.getExpandHandler = function() {
+ var me = this;
+ return function() {
+ var element = me.getElement();
+ var snippet = $(element.parents('.snippet')[0]);
+ $.ajax({
+ type: 'GET',
+ dataType: 'json',
+ data: {'post_id': me.getPostId()},
+ url: askbot['urls']['getPostHtml'],
+ success: function(data) {
+ if (data['post_html']) {
+ snippet.hide();
+ snippet.html(data['post_html']);
+ snippet.fadeIn();
+ }
+ }
+ });
+ me.showWaitIcon();
+ };
+};
var EditLink = function(){
SimpleControl.call(this)
@@ -965,6 +1390,12 @@ CommentConvertLink.prototype.createDom = function(){
hidden_input.attr('id', 'id_comment_id');
element.append(hidden_input);
+ var csrf_token = this.makeElement('input');
+ csrf_token.attr('type', 'hidden');
+ csrf_token.attr('name', 'csrfmiddlewaretoken');
+ csrf_token.attr('value', getCookie(askbot['settings']['csrfCookieName']));
+ element.append(csrf_token);
+
var submit = this.makeElement('input');
submit.attr('type', 'submit');
submit.attr('value', gettext('convert to answer'));
@@ -2376,10 +2807,9 @@ Tag.prototype.createDom = function(){
}
this._inner_element.addClass('tag tag-right');
this._inner_element.attr('rel', 'tag');
- if (this._title === null){
- this.setTitle(
- interpolate(gettext("see questions tagged '%s'"), [this.getName()])
- );
+ if (this._title === null) {
+ var name = this.getName();
+ this.setTitle(interpolate(gettext("see questions tagged '%s'"), [name,]));
}
this._inner_element.attr('title', this._title);
this._inner_element.html(this.getDisplayTagName());
diff --git a/askbot/media/js/wmd/wmd.js b/askbot/media/js/wmd/wmd.js
index 6f7505d0..f84cd6e4 100644
--- a/askbot/media/js/wmd/wmd.js
+++ b/askbot/media/js/wmd/wmd.js
@@ -122,14 +122,15 @@ Attacklab.wmdBase = function(){
// Adds a listener callback to a DOM element which is fired on a specified
// event.
util.addEvent = function(elem, event, listener){
- if (elem && elem.attachEvent) {
- // IE only. The "on" is mandatory.
- elem.attachEvent("on" + event, listener);
- }
- else {
- // Other browsers.
- elem.addEventListener(event, listener, false);
- }
+ if (elem) {
+ if (elem.attachEvent) {
+ // IE only. The "on" is mandatory.
+ elem.attachEvent("on" + event, listener);
+ } else {
+ // Other browsers.
+ elem.addEventListener(event, listener, false);
+ }
+ }
};
diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css
index 4e0e505a..d86b8fc7 100644
--- a/askbot/media/style/style.css
+++ b/askbot/media/style/style.css
@@ -198,6 +198,9 @@ h1 {
/* ----- Extra space above for messages ----- */
body.user-messages {
margin-top: 2.4em;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
}
/* ----- Custom positions ----- */
.left {
@@ -327,6 +330,9 @@ body.user-messages {
background: white;
display: block;
}
+.select-language {
+ margin-top: 6px;
+}
#userToolsNav {
/* Navigation bar containing login link or user information, check widgets/user_navigation.html*/
@@ -460,11 +466,21 @@ body.user-messages {
border-top: #fcfcfc 1px solid;
margin-bottom: 10px;
font-family: 'Open Sans Condensed', Arial, sans-serif;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+}
+#secondaryHeader td {
+ padding: 0;
+}
+#secondaryHeader td.search-bar {
+ padding: 0 32px 0 15px;
+ position: relative;
}
#homeButton {
border-right: #afaf9e 1px solid;
background: -6px -36px url(../images/sprites.png) no-repeat;
- height: 55px;
+ height: 54px;
width: 43px;
display: block;
float: left;
@@ -473,8 +489,7 @@ body.user-messages {
background: -51px -36px url(../images/sprites.png) no-repeat;
}
.scope-selector {
- display: block;
- float: left;
+ display: inline-block;
font-size: 20px;
color: #7a7a6b;
height: 55px;
@@ -482,7 +497,7 @@ body.user-messages {
margin-left: 16px;
}
.scope-selector.on {
- background: url(../images/scopearrow.png) no-repeat center bottom;
+ background: url(../images/scopearrow.png) no-repeat center 41px;
}
.scope-selector.ask-message {
font-size: 24px;
@@ -505,12 +520,12 @@ body.user-messages {
#searchBar {
/* Main search form , check widgets/search_bar.html */
- display: block;
- background-color: #fff;
border: 1px solid #c9c9b5;
+ background: white;
height: 41px;
z-index: 1000;
position: relative;
+ width: 100%;
/* the guts are absolute-positioned */
}
@@ -570,17 +585,20 @@ body.user-messages {
}
.search-drop-menu {
box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
background: whitesmoke;
- border: 1px solid #c9c9b5;
border-top: none;
+ top: 42px;
margin: 0;
- position: relative;
+ outline: 1px solid #dadad4;
+ position: absolute;
z-index: 10000;
}
.search-drop-menu ul {
list-style: none;
overflow: auto;
- padding: 0 0 10px 0;
+ padding: 0;
margin: 0;
position: relative;
width: 100%;
@@ -598,12 +616,13 @@ body.user-messages {
.search-drop-menu ul li.selected a {
color: whitesmoke;
}
-.search-drop-menu ul.empty {
- margin-bottom: 0;
-}
.search-drop-menu .footer {
text-align: center;
- padding: 9px 0 10px 0;
+ margin: 0 0 1px 0;
+ padding: 4px 0 10px 0;
+}
+.search-drop-menu.empty .footer {
+ padding-top: 9px;
}
.search-drop-menu.empty ul {
padding: 1px;
@@ -623,7 +642,7 @@ input[type="submit"].searchBtn {
border: #FFF 1px solid;
line-height: 22px;
text-align: center;
- margin: 1px 0 0 0;
+ margin: 0;
width: 48px;
background: -98px -37px url(../images/sprites.png) no-repeat;
border-radius: 0;
@@ -652,13 +671,21 @@ input[type="submit"].searchBtn {
color: white;
height: 0;
z-index: 0;
+ position: absolute;
+ left: -1000px;
+ /* hide away */
+
+}
+.ask-page .search-drop-menu {
+ top: 37px;
}
.ask-page .search-drop-menu.empty {
border: none;
- padding: 0;
+ outline: none;
+ padding: 0 !important;
}
.ask-page .search-drop-menu.empty ul {
- padding: 0;
+ padding: 0 !important;
}
input[type="submit"].searchBtn:hover {
background-image: none;
@@ -801,73 +828,23 @@ form.ajax-file-upload img.spinner {
font-size: 20px;
height: 42px;
line-height: 44px;
- margin: 6px 0 0 0;
+ margin: -1px 0 0 0;
text-transform: uppercase;
width: 200px;
/* to match width of sidebar */
}
-/*
- Put the secondary navigation together:
- 1) raise the search bar by 55px
- 2) add padding to fit the buttons
-*/
-#searchBar {
- margin: 0 228px 0 327px;
- width: auto;
- margin-top: -49px;
- padding: 0 49px 0 8px;
-}
-/* line up drop menu the same way as the search bar */
.search-drop-menu {
- margin: 0 228px 0 327px;
- width: auto;
+ width: 100%;
}
.ask-page .search-drop-menu,
body.anon.ask-page .search-drop-menu {
- margin: -9px 0 10px;
+ padding: 5px 10px 6px 0;
}
#scopeNav {
- height: 41px;
- float: left;
- width: 280px;
-}
-.scopes-True-True-False #searchBar,
-.scopes-True-True-False .search-drop-menu {
- margin-left: 228px;
-}
-.scopes-True-True-False #scopeNav {
- width: 180px;
-}
-.scopes-True-False-True #searchBar,
-.scopes-True-False-True .search-drop-menu {
- margin-left: 203px;
-}
-.scopes-True-False-True #scopeNav {
- width: 150px;
-}
-.scopes-False-True-True #searchBar,
-.scopes-False-True-True .search-drop-menu {
- margin-left: 286px;
-}
-.scopes-False-True-True #scopeNav {
- width: 238px;
-}
-.scopes-True-False-False #searchBar,
-.scopes-False-True-False #searchBar,
-.scopes-False-False-True #searchBar,
-.scopes-False-False-False #searchBar,
-.scopes-True-False-False .search-drop-menu,
-.scopes-False-True-False .search-drop-menu,
-.scopes-False-False-True .search-drop-menu,
-.scopes-False-False-False .search-drop-menu {
- margin-left: 52px;
-}
-.scopes-True-False-False #scopeNav,
-.scopes-False-True-False #scopeNav,
-.scopes-False-False-True #scopeNav,
-.scopes-False-False-False #scopeNav {
- width: 0;
+ height: 54px;
+ display: inline-block;
+ white-space: nowrap;
}
/* ----- Content layout, check two_column_body.html or one_column_body.html ----- */
#ContentLeft {
@@ -1078,7 +1055,7 @@ body.anon.ask-page .search-drop-menu {
-webkit-box-shadow: 1px 1px 3px #999999;
-moz-box-shadow: 1px 1px 3px #999999;
box-shadow: 1px 1px 3px #999999;
- padding: 7px 10px 1px 10px;
+ padding: 7px 10px 9px 0;
margin-bottom: 10px;
width: 100%;
}
@@ -1199,7 +1176,8 @@ body.anon.ask-page .search-drop-menu {
ul#searchTags {
margin-left: 10px;
float: right;
- padding-top: 2px;
+ padding: 8px 6px 0px 6px;
+ min-width: 43px;
}
.search-tips {
font-size: 16px;
@@ -1374,31 +1352,34 @@ ul#searchTags {
.paginator a {
color: #7ea9b3;
}
-.paginator .prev {
- margin-right: .5em;
-}
+.paginator .prev,
.paginator .next {
- margin-left: .5em;
+ margin-right: .5em;
}
.paginator .page a,
.paginator .page a:visited,
.paginator .curr {
- padding: .25em;
- background-color: #fff;
- margin: 0em .25em;
- color: #ff;
+ padding: 0 .25em;
+ margin: 0em .15em 0 0;
}
.paginator .curr {
- background-color: #8ebcc7;
+ /*background-color: #8ebcc7;*/
+
color: #fff;
font-weight: bold;
}
-.paginator .next a,
-.paginator .prev a {
- color: #7ea9b3;
+.paginator .curr a {
+ background: #8ebcc7;
+ color: white;
+ /*padding: 0;*/
+
+}
+.paginator .curr.page a:hover {
+ background: #8ebcc7;
+ color: white;
+ padding: 0 0.25em;
}
.paginator .page a:hover,
-.paginator .curr a:hover,
.paginator .prev a:hover,
.paginator .next a:hover {
color: #8C8C8C;
@@ -1504,7 +1485,7 @@ ul#related-tags li {
background: #f3f6f6;
border: #fff 1px solid ;
border-top: #fff 2px solid;
- outline: #cfdbdb 1px solid;
+ outline: #cfdbdb 1px solid !important;
/* .box-shadow(0px,1px,0px,#88a8a8);*/
display: block;
@@ -1632,6 +1613,13 @@ ul#related-tags li {
.delete-icon:hover {
background: #b32f2f;
}
+.expander .wait-icon-box {
+ display: inline-block;
+ margin: 0;
+}
+.snippet:hover .expander a {
+ text-decoration: underline;
+}
.tag-number {
font-weight: normal;
float: left;
@@ -1659,6 +1647,7 @@ ul#related-tags li {
padding: 4px 0 0 0;
margin-top: 0px;
width: 100%;
+ position: relative;
}
#askFormBar p {
margin: 0 0 5px 0;
@@ -1747,7 +1736,11 @@ ul#related-tags li {
}
.ask-page .lang-selector,
.edit-question-page .lang-selector {
- margin: 1px 0 0 5px;
+ margin: 9px 0 0 0;
+}
+.ask-page .lang-selector select,
+.edit-question-page .lang-selector select {
+ margin: 3px 0;
}
.ask-page #id_post_author_username,
.question-page #id_post_author_username,
@@ -2112,6 +2105,7 @@ ul#related-tags li {
font-family: Arial;
color: #4b4b4b;
word-wrap: break-word;
+ max-width: 683px;
}
.question-page .post-body a {
color: #1b79bd;
@@ -2358,6 +2352,9 @@ ul#related-tags li {
font-size: 12px;
clear: both;
}
+.question-page .comments .truncated-post:nth-last-child(3) {
+ float: left;
+}
.question-page .comments div.controls {
width: 100%;
margin: 3px 0 20px 5px;
@@ -2756,6 +2753,24 @@ ul#related-tags li {
background-position: -52px 0px;
}
/* -----Content pages, Login, About, FAQ, Users----- */
+.openid-signin input[type="text"],
+.openid-signin input[type="password"] {
+ color: #525252;
+ font-size: 12px;
+ line-height: 25px;
+ height: 25px;
+ margin: 0px;
+ padding: 0 5px;
+ width: 200px;
+}
+.registration-page .login input[type="text"] {
+ height: 22px;
+ font-size: 12px;
+ line-height: 22px;
+ color: #525252;
+ padding: 0 5px;
+ width: 200px;
+}
.openid-signin,
.meta,
.user-profile-edit-page {
@@ -2780,24 +2795,20 @@ ul#related-tags li {
padding-left: 0px;
font-size: 16px;
}
-.openid-signin form,
.meta form,
.users-page form,
.user-profile-edit-page form,
.user-profile-page form {
margin-bottom: 15px;
}
-.openid-signin input[type="text"],
.meta input[type="text"],
.users-page input[type="text"],
.user-profile-edit-page input[type="text"],
.user-profile-page input[type="text"],
-.openid-signin input[type="password"],
.meta input[type="password"],
.users-page input[type="password"],
.user-profile-edit-page input[type="password"],
.user-profile-page input[type="password"],
-.openid-signin select,
.meta select,
.users-page select,
.user-profile-edit-page select,
@@ -2808,7 +2819,6 @@ ul#related-tags li {
width: 395px;
font-size: 14px;
}
-.openid-signin select,
.meta select,
.users-page select,
.user-profile-edit-page select,
@@ -2816,7 +2826,6 @@ ul#related-tags li {
width: 405px;
height: 30px;
}
-.openid-signin textarea,
.meta textarea,
.users-page textarea,
.user-profile-edit-page textarea,
@@ -2826,7 +2835,6 @@ ul#related-tags li {
width: 395px;
font-size: 14px;
}
-.openid-signin input.submit,
.meta input.submit,
.users-page input.submit,
.user-profile-edit-page input.submit,
@@ -2834,7 +2842,6 @@ ul#related-tags li {
font-weight: normal;
margin: 5px 0px;
}
-.openid-signin .cancel,
.meta .cancel,
.users-page .cancel,
.user-profile-edit-page .cancel,
@@ -2842,14 +2849,12 @@ ul#related-tags li {
background: url(../images/small-button-cancel.png) repeat-x top !important;
color: #525252 !important;
}
-.openid-signin .cancel:hover,
.meta .cancel:hover,
.users-page .cancel:hover,
.user-profile-edit-page .cancel:hover,
.user-profile-page .cancel:hover {
background: url(../images/small-button-cancel.png) repeat-x bottom !important;
}
-.openid-signin .re,
.meta .re,
.users-page .re,
.user-profile-edit-page .re,
@@ -2881,12 +2886,10 @@ ul#related-tags li {
.openid-signin form {
margin-bottom: 5px;
}
-#email-input-fs input[type="text"],
#local_login_buttons input[type="text"],
#password-fs input[type="text"],
#openid-fs input[type="text"],
#signup-form input[type="text"],
-#email-input-fs input[type="password"],
#local_login_buttons input[type="password"],
#password-fs input[type="password"],
#openid-fs input[type="password"],
@@ -3005,10 +3008,26 @@ a:hover.medal {
border: #eee 1px solid;
padding: 5px;
}
+.user-profile-page form {
+ margin-bottom: 0;
+}
.user-profile-page h2 {
- padding: 10px 0px 10px 0px;
+ padding: 4px 0px 10px 0px;
font-family: 'Open Sans Condensed', Arial, sans-serif;
}
+.user-profile-page h3 {
+ padding: 0;
+ margin-top: -3px;
+}
+.user-profile-page .submit-row {
+ margin-bottom: 0;
+}
+.user-profile-page .user-stats-table h2 {
+ padding-top: 10px;
+}
+.user-profile-page .user-stats-table.badges {
+ margin-bottom: 30px;
+}
.user-profile-page .up-votes,
.user-profile-page .down-votes {
display: inline-block;
@@ -3029,8 +3048,27 @@ a:hover.medal {
margin-top: 10px;
margin-bottom: 10px;
}
-.user-profile-page select {
- margin-bottom: 12px;
+.user-profile-page .pager {
+ margin-bottom: 0;
+}
+.user-profile-page .paginator {
+ margin-bottom: 0;
+ padding-bottom: 0;
+}
+.user-profile-page .users-questions {
+ margin-top: -10px;
+}
+.user-profile-page .avatar p {
+ margin-bottom: 0px;
+}
+.user-profile-page .tabBar a#stats {
+ margin-left: 0;
+}
+.user-profile-page img.gravatar {
+ margin: 2px 0 3px 0;
+}
+.user-profile-page .vote-notification h3 {
+ padding: 10px;
}
.user-details {
font-size: 13px;
@@ -3552,6 +3590,14 @@ a.offensive {
.message p {
margin-bottom: 0px;
}
+.system-messages {
+ color: red;
+ background: yellow;
+ font-size: 21px;
+ font-weight: bold;
+ line-height: 25px;
+ padding: 0 5px;
+}
p.space-above {
margin-top: 10px;
}
@@ -3754,8 +3800,9 @@ ul.post-retag {
}
ul.post-retag input {
width: 400px;
- height: 1.5em;
- margin: 3px 0 0 -3px;
+ height: 22px;
+ margin: 3px 0 0 -5px;
+ padding: 0 5px;
}
#question-controls .tags {
margin: 0 0 3px 0;
@@ -3847,25 +3894,6 @@ p.signup_p {
.avatar-page li {
display: inline;
}
-.user-profile-page .avatar p {
- margin-bottom: 0px;
-}
-.user-profile-page .tabBar a#stats {
- margin-left: 0;
-}
-.user-profile-page img.gravatar {
- margin: 2px 0 3px 0;
-}
-.user-profile-page h3 {
- padding: 0;
- margin-top: -3px;
-}
-.user-profile-page .vote-notification h3 {
- padding: 10px;
-}
-.user-profile-page ul.tags {
- margin-left: 5px;
-}
.userList {
font-size: 13px;
}
@@ -4169,30 +4197,34 @@ textarea.tipped-input {
}
/* tag editor */
.tag-editor {
- height: 64px;
- border: #ccc 3px solid;
- padding-left: 8px;
+ height: 32px;
+ border: #cce6ec 3px solid;
+ padding-left: 6px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
}
.tag-editor ul.tags {
margin: 0;
}
.tag-editor ul.tags li {
- margin-top: 8px;
+ margin-top: 6px;
height: 13px;
}
.tag-editor input.new-tags-input,
.tag-editor input.new-tags-input:focus {
border: none;
- font-size: 15px;
+ box-shadow: none;
+ font-size: 14px;
font-color: #707070;
height: 16px;
line-height: 16px;
- margin-top: 9px;
+ margin: 9px 0 0 -6px;
-webkit-box-shadow: none;
/* undo bootstrap glow */
-moz-box-shadow: none;
- box-shadow: none;
+ padding: 0 0 0 6px;
}
/* fixes for bootstrap */
.caret {
@@ -4331,6 +4363,10 @@ td.setting-input textarea {
-o-transition: border linear 0.2s, box-shadow linear 0.2s;
transition: border linear 0.2s, box-shadow linear 0.2s;
}
+#recaptcha_area,
+#recaptcha_table {
+ line-height: 0!important;
+}
.acInput {
width: 200px;
}
@@ -4546,17 +4582,26 @@ td.setting-input textarea {
.short-summary:first-child {
padding-top: 0;
}
- #searchBar,
- body.anon #searchBar {
- margin: -49px 8px 0 52px;
- }
- .search-drop-menu,
- body.anon .search-drop-menu {
- margin: 0 8px 0 52px;
- }
.short-summary {
width: 100%;
}
+ #secondaryHeader td.search-bar {
+ padding: 0 6px;
+ }
+}
+@media screen and (max-width: 540px) {
+ .question-page .post-controls {
+ float: left;
+ margin-left: -8px;
+ padding-left: 0;
+ text-align: left;
+ }
+ .question-page .post-controls .question-close {
+ display: none;
+ }
+ .question-page div.comments.empty {
+ margin-top: 0;
+ }
}
@media screen and (max-width: 480px) {
.openid-signin h1,
@@ -4577,6 +4622,15 @@ td.setting-input textarea {
width: 100%;
}
}
+@media screen and (max-width: 380px) {
+ .wmd-button-bar .wmd-hr-button,
+ .wmd-button-bar .wmd-heading-button {
+ display: none;
+ }
+ .editor-status {
+ margin-right: 0;
+ }
+}
@media screen and (max-width: 338px) {
#scopeNav {
display: none;
@@ -4592,6 +4646,9 @@ td.setting-input textarea {
width: 100%;
margin: 6px 0;
}
+ .question-page .post-controls .offensive-flag {
+ display: none;
+ }
}
/* language-specific fixes */
body.lang-zh .box .inputs #interestingTagInput,
@@ -4662,39 +4719,12 @@ body.lang-zh-tw.question-page a.submit,
body.lang-zh_TW.question-page a.submit {
line-height: 30px !important;
}
-body.lang-zh .scopes-True-True-False #searchBar,
-body.lang-zh_CN .scopes-True-True-False #searchBar,
-body.lang-zh-cn .scopes-True-True-False #searchBar,
-body.lang-zh-tw .scopes-True-True-False #searchBar,
-body.lang-zh_TW .scopes-True-True-False #searchBar,
-body.lang-zh .scopes-True-True-False .search-drop-menu,
-body.lang-zh_CN .scopes-True-True-False .search-drop-menu,
-body.lang-zh-cn .scopes-True-True-False .search-drop-menu,
-body.lang-zh-tw .scopes-True-True-False .search-drop-menu,
-body.lang-zh_TW .scopes-True-True-False .search-drop-menu {
- margin-left: 194px;
-}
-body.lang-zh .scopes-True-True-True #searchBar,
-body.lang-zh_CN .scopes-True-True-True #searchBar,
-body.lang-zh-cn .scopes-True-True-True #searchBar,
-body.lang-zh-tw .scopes-True-True-True #searchBar,
-body.lang-zh_TW .scopes-True-True-True #searchBar,
-body.lang-zh .scopes-True-True-True .search-drop-menu,
-body.lang-zh_CN .scopes-True-True-True .search-drop-menu,
-body.lang-zh-cn .scopes-True-True-True .search-drop-menu,
-body.lang-zh-tw .scopes-True-True-True .search-drop-menu,
-body.lang-zh_TW .scopes-True-True-True .search-drop-menu {
- margin-left: 274px;
-}
body.lang-hu .scope-selector {
font-size: 17px;
margin-left: 10px;
}
-body.lang-hu .scopes-True-True-False #scopeNav {
- width: 200px;
-}
-body.lang-hu .scopes-True-True-False #searchBar,
-body.lang-hu .scopes-True-True-False .search-drop-menu {
+body.lang-hu #searchBar,
+body.lang-hu .search-drop-menu {
margin-left: 252px;
}
body.lang-hu .box .inputs #interestingTagInput,
@@ -4723,34 +4753,9 @@ body.lang-es .short-summary .counts .views div,
body.lang-es .short-summary .counts .votes div {
font-size: 10px;
}
-body.lang-es .scopes-True-True-False #scopeNav {
- width: 187px;
-}
-body.lang-es .scopes-True-True-False #searchBar,
-body.lang-es .scopes-True-True-False .search-drop-menu {
- margin-left: 250px;
-}
-body.lang-es .scopes-True-True-True #searchBar,
-body.lang-es .scopes-True-True-True .search-drop-menu {
- margin-left: 327px;
-}
body.lang-de .scope-selector {
font-size: 17px;
}
-body.lang-de .scopes-True-True-False #scopeNav {
- width: 218px;
-}
-body.lang-de .scopes-True-True-False #searchBar,
-body.lang-de .scopes-True-True-False .search-drop-menu {
- margin-left: 250px;
-}
-body.lang-de .scopes-True-True-True #scopeNav {
- width: 354px;
-}
-body.lang-de .scopes-True-True-True #searchBar,
-body.lang-de .scopes-True-True-True .search-drop-menu {
- margin-left: 354px;
-}
body.lang-de #metaNav a {
font-size: 16px;
}
@@ -4769,24 +4774,12 @@ body.lang-gl .box .inputs #subscribedTagInput,
body.lang-gl .box .inputs #ab-tag-search {
width: 127px;
}
-body.lang-gl .scopes-True-True-True #searchBar,
-body.lang-gl .scopes-True-True-True .search-drop-menu {
- margin-left: 299px;
-}
body.lang-pt_BR .box .inputs #interestingTagInput,
body.lang-pt_BR .box .inputs #ignoredTagInput,
body.lang-pt_BR .box .inputs #subscribedTagInput,
body.lang-pt_BR .box .inputs #ab-tag-search {
width: 116px;
}
-body.lang-pt_BR .scopes-True-True-False #searchBar,
-body.lang-pt_BR .scopes-True-True-False .search-drop-menu {
- margin-left: 236px;
-}
-body.lang-fr .scopes-True-True-True #searchBar,
-body.lang-fr .scopes-True-True-True .search-drop-menu {
- margin-left: 287px;
-}
body.lang-fr .box .inputs #interestingTagInput,
body.lang-fr .box .inputs #ignoredTagInput,
body.lang-fr .box .inputs #subscribedTagInput,
@@ -4802,34 +4795,15 @@ body.lang-fi #scopeNav {
body.lang-fi .scope-selector {
font-size: 17px;
}
-body.lang-fi .scopes-True-True-True #searchBar,
-body.lang-fi .scopes-True-True-True .search-drop-menu {
- margin-left: 371px;
-}
-body.lang-fi .scopes-True-True-False #searchBar,
-body.lang-fi .scopes-True-True-False .search-drop-menu {
- margin-left: 268px;
-}
body.lang-fi .box .inputs #interestingTagInput,
body.lang-fi .box .inputs #ignoredTagInput,
body.lang-fi .box .inputs #subscribedTagInput,
body.lang-fi .box .inputs #ab-tag-search {
width: 142px;
}
-body.lang-ru #scopeNav {
- width: 315px;
-}
body.lang-ru .scope-selector {
font-size: 17px;
}
-body.lang-ru .scopes-True-True-True #searchBar,
-body.lang-ru .scopes-True-True-True .search-drop-menu {
- margin-left: 326px;
-}
-body.lang-ru .scopes-True-True-False #searchBar,
-body.lang-ru .scopes-True-True-False .search-drop-menu {
- margin-left: 227px;
-}
body.lang-ru .box .inputs #interestingTagInput,
body.lang-ru .box .inputs #ignoredTagInput,
body.lang-ru .box .inputs #subscribedTagInput,
@@ -4839,32 +4813,12 @@ body.lang-ru .box .inputs #ab-tag-search {
body.lang-ko #scopeNav {
width: 315px;
}
-body.lang-ko .scopes-True-True-True #searchBar,
-body.lang-ko .scopes-True-True-True .search-drop-menu {
- margin-left: 248px;
-}
-body.lang-ko .scopes-True-True-False #searchBar,
-body.lang-ko .scopes-True-True-False .search-drop-menu {
- margin-left: 183px;
-}
body.lang-ko .box .inputs #interestingTagInput,
body.lang-ko .box .inputs #ignoredTagInput,
body.lang-ko .box .inputs #subscribedTagInput,
body.lang-ko .box .inputs #ab-tag-search {
width: 144px;
}
-body.lang-nb_NO .scopes-True-True-True #searchBar,
-body.lang-nb_NO .scopes-True-True-True #searchBar,
-body.lang-nb_NO .scopes-True-True-True .search-drop-menu,
-body.lang-nb_NO .scopes-True-True-True .search-drop-menu {
- margin-left: 289px;
-}
-body.lang-nb_NO .scopes-True-True-False #searchBar,
-body.lang-nb_NO .scopes-True-True-False #searchBar,
-body.lang-nb_NO .scopes-True-True-False .search-drop-menu,
-body.lang-nb_NO .scopes-True-True-False .search-drop-menu {
- margin-left: 220px;
-}
body.lang-nb_NO .box .inputs #interestingTagInput,
body.lang-nb_NO .box .inputs #interestingTagInput,
body.lang-nb_NO .box .inputs #ignoredTagInput,
diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less
index 609165da..09b2810f 100644
--- a/askbot/media/style/style.less
+++ b/askbot/media/style/style.less
@@ -205,6 +205,9 @@ h1 {
body.user-messages {
margin-top: 2.4em;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
}
/* ----- Custom positions ----- */
@@ -335,6 +338,9 @@ body.user-messages {
display: block;
}
}
+.select-language {
+ margin-top: 6px;
+}
#userToolsNav {/* Navigation bar containing login link or user information, check widgets/user_navigation.html*/
height: 20px;
@@ -487,37 +493,44 @@ body.user-messages {
border-top:#fcfcfc 1px solid;
margin-bottom:10px;
font-family:@main-font;
+ box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ td {
+ padding: 0;
+ &.search-bar {
+ padding: 0 32px 0 15px;
+ position: relative;
+ }
+ }
}
#homeButton{
border-right: #afaf9e 1px solid;
.sprites(-6px,-36px);
- height:55px;
- width:43px;
- display:block;
- float:left;
+ height: 54px;
+ width: 43px;
+ display: block;
+ float: left;
}
-#homeButton:hover{
+#homeButton:hover {
.sprites(-51px,-36px);
}
.scope-selector {
- display:block;
- float:left;
- font-size:20px;
- color:#7a7a6b;
- height:55px;
- line-height:55px;
- margin-left:16px
-}
-
-.scope-selector.on {
- background:url(../images/scopearrow.png) no-repeat center bottom;
-}
-
-.scope-selector.ask-message {
- font-size:24px;
+ display: inline-block;
+ font-size: 20px;
+ color: #7a7a6b;
+ height: 55px;
+ line-height: 55px;
+ margin-left: 16px;
+ &.on {
+ background: url(../images/scopearrow.png) no-repeat center 41px;
+ }
+ &.ask-message {
+ font-size: 24px;
+ }
}
.validate-email-page {
@@ -539,12 +552,12 @@ body.user-messages {
}
#searchBar { /* Main search form , check widgets/search_bar.html */
- display: block;
- background-color: #fff;
border: 1px solid #c9c9b5;
+ background: white;
height: 41px;
z-index: 1000;
position: relative;
+ width: 100%;
/* the guts are absolute-positioned */
input.searchInput,
@@ -605,17 +618,20 @@ body.user-messages {
.search-drop-menu {
box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
background: whitesmoke;
- border: 1px solid #c9c9b5;
border-top: none;
+ top: 42px;
margin: 0;
- position: relative;
+ outline: 1px solid #dadad4;
+ position: absolute;
z-index: 10000;
ul {
list-style: none;
overflow: auto;
- padding: 0 0 10px 0;
+ padding: 0;
margin: 0;
position: relative;
width: 100%;
@@ -634,13 +650,13 @@ body.user-messages {
}
}
- ul.empty {
- margin-bottom: 0;
- }
-
.footer {
text-align: center;
- padding: 9px 0 10px 0;
+ margin: 0 0 1px 0;
+ padding: 4px 0 10px 0;
+ }
+ &.empty .footer {
+ padding-top: 9px;
}
}
@@ -666,7 +682,7 @@ input[type="submit"].searchBtn {
border:#FFF 1px solid;
line-height: 22px;
text-align: center;
- margin: 1px 0 0 0;
+ margin: 0;
width: 48px;
.sprites(-98px,-37px);
.rounded-corners(0);
@@ -694,12 +710,18 @@ input[type="submit"].searchBtn {
color: white;
height: 0;
z-index: 0;
+ position: absolute;
+ left: -1000px;/* hide away */
}
- .search-drop-menu.empty {
- border: none;
- padding: 0;
- ul {
- padding: 0;
+ .search-drop-menu {
+ top: 37px;
+ &.empty {
+ border: none;
+ outline: none;
+ padding: 0 !important;
+ ul {
+ padding: 0 !important;
+ }
}
}
}
@@ -822,74 +844,22 @@ form.ajax-file-upload {
font-size: 20px;
height: 42px;
line-height: 44px;
- margin: 6px 0 0 0;
+ margin: -1px 0 0 0;
text-transform: uppercase;
width: 200px;/* to match width of sidebar */
}
-/*
- Put the secondary navigation together:
- 1) raise the search bar by 55px
- 2) add padding to fit the buttons
-*/
-#searchBar {
- margin: 0 228px 0 327px;
- width: auto;
- margin-top: -49px;
- padding: 0 49px 0 8px;
-}
-/* line up drop menu the same way as the search bar */
.search-drop-menu {
- margin: 0 228px 0 327px;
- width: auto;
+ width: 100%;
}
.ask-page .search-drop-menu,
body.anon.ask-page .search-drop-menu {
- margin: -9px 0 10px;
+ padding: 5px 10px 6px 0;
}
#scopeNav {
- height: 41px;
- float: left;
- width: 280px;
-}
-.scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 228px;
- }
- #scopeNav {
- width: 180px;
- }
-}
-.scopes-True-False-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 203px;
- }
- #scopeNav {
- width: 150px;
- }
-}
-.scopes-False-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 286px;
- }
- #scopeNav {
- width: 238px;
- }
-}
-.scopes-True-False-False,
-.scopes-False-True-False,
-.scopes-False-False-True,
-.scopes-False-False-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 52px;
- }
- #scopeNav {
- width: 0;
- }
+ height: 54px;
+ display: inline-block;
+ white-space: nowrap;
}
/* ----- Content layout, check two_column_body.html or one_column_body.html ----- */
@@ -1121,7 +1091,7 @@ body.anon.ask-page .search-drop-menu {
-khtml-border-radius: 5px;
-webkit-border-radius: 5px;
.box-shadow(1px, 1px, 3px, #999);
- padding: 7px 10px 1px 10px;
+ padding: 7px 10px 9px 0;
margin-bottom: 10px;
width: 100%;
ul {
@@ -1255,7 +1225,8 @@ body.anon.ask-page .search-drop-menu {
ul#searchTags {
margin-left:10px;
float:right;
- padding-top:2px;
+ padding: 8px 6px 0px 6px;
+ min-width: 43px;
}
.search-tips {
@@ -1449,38 +1420,42 @@ ul#searchTags {
.prev a, .prev a:visited,
.next a, .next a:visited {
- background-color: #fff;
+ background-color: #fff;
color: #777;
padding: 2px 4px 3px 4px;
}
- a{
- color:@section-title;
- }
- .prev {
- margin-right: .5em;
+ a {
+ color: @section-title;
}
-
+ .prev,
.next {
- margin-left: .5em;
+ margin-right: .5em;
}
.page a, .page a:visited, .curr {
- padding: .25em;
- background-color: #fff;
- margin: 0em .25em;
- color: #ff;
+ padding: 0 .25em;
+ margin: 0em .15em 0 0;
}
.curr {
- background-color: #8ebcc7;
+ /*background-color: #8ebcc7;*/
color: #fff;
font-weight: bold;
+ a {
+ background: #8ebcc7;
+ color: white;
+ /*padding: 0;*/
+ }
}
- .next a, .prev a{
- color:@section-title
+ .curr.page {
+ a:hover {
+ background: #8ebcc7;
+ color: white;
+ padding: 0 0.25em;
+ }
}
+
.page a:hover,
- .curr a:hover,
.prev a:hover,
.next a:hover {
color: #8C8C8C;
@@ -1602,7 +1577,7 @@ ul#related-tags li {
background: #f3f6f6;
border:#fff 1px solid ;
border-top:#fff 2px solid;
- outline:#cfdbdb 1px solid;
+ outline:#cfdbdb 1px solid !important;
/* .box-shadow(0px,1px,0px,#88a8a8);*/
display: block;
float: left;
@@ -1725,7 +1700,18 @@ ul#related-tags li {
background: #b32f2f;
}
+.expander {
+ .wait-icon-box {
+ display: inline-block;
+ margin: 0;
+ }
+}
+.snippet:hover {
+ .expander a {
+ text-decoration: underline;
+ }
+}
.tag-number {
font-weight: normal;
@@ -1759,6 +1745,7 @@ ul#related-tags li {
padding: 4px 0 0 0;
margin-top:0px;
width: 100%;
+ position: relative;
p {
margin:0 0 5px 0;
@@ -1845,7 +1832,10 @@ ul#related-tags li {
max-width: 395px;
}
.lang-selector {
- margin: 1px 0 0 5px;
+ margin: 9px 0 0 0;
+ select {
+ margin: 3px 0;
+ }
}
}
@@ -2234,6 +2224,7 @@ ul#related-tags li {
font-family: @body-font;
color: #4b4b4b;
word-wrap: break-word;
+ max-width: 683px;
a {
color:@link;
@@ -2468,6 +2459,10 @@ ul#related-tags li {
.comments {
font-size: 12px;
clear: both;
+
+ .truncated-post:nth-last-child(3) {
+ float: left;
+ }
div.controls {
width: 100%;
@@ -2894,6 +2889,32 @@ ul#related-tags li {
/* -----Content pages, Login, About, FAQ, Users----- */
+.openid-signin {
+ input[type="text"],
+ input[type="password"] {
+ color: #525252;
+ font-size: 12px;
+ line-height: 25px;
+ height: 25px;
+ margin: 0px;
+ padding: 0 5px;
+ width: 200px;
+ }
+}
+
+.registration-page {
+ .login {
+ input[type="text"] {
+ height: 22px;
+ font-size: 12px;
+ line-height: 22px;
+ color: #525252;
+ padding: 0 5px;
+ width: 200px;
+ }
+ }
+}
+
.openid-signin,
.meta,
.user-profile-edit-page,
@@ -2916,18 +2937,17 @@ ul#related-tags li {
}
}
-.openid-signin,
.meta,
.users-page,
.user-profile-edit-page,
.user-profile-page {
- form{
+ form {
margin-bottom:15px;
}
input[type="text"],
input[type="password"],
- select{
+ select {
height:25px;
line-height: 25px;
padding-left:5px;
@@ -2994,7 +3014,6 @@ ul#related-tags li {
margin-bottom: 5px;
}
-#email-input-fs,
#local_login_buttons,
#password-fs,
#openid-fs,
@@ -3120,7 +3139,7 @@ a:hover.medal {
/* profile page */
-.tabBar-profile{
+.tabBar-profile {
width:100%;
margin-bottom:5px;
float:left;
@@ -3129,7 +3148,7 @@ a:hover.medal {
.user-profile-page {
color:@info-text-dark;
- p{
+ p {
font-size:13px;
line-height:1.3;
color:@info-text-dark;
@@ -3138,9 +3157,31 @@ a:hover.medal {
border:#eee 1px solid;
padding:5px;
}
+
+ form {
+ margin-bottom: 0;
+ }
+
h2 {
- padding:10px 0px 10px 0px;
- font-family:@main-font;
+ padding: 4px 0px 10px 0px;
+ font-family: @main-font;
+ }
+
+ h3 {
+ padding: 0;
+ margin-top: -3px;
+ }
+
+ .submit-row {
+ margin-bottom: 0;
+ }
+
+ .user-stats-table h2 {
+ padding-top: 10px;
+ }
+
+ .user-stats-table.badges {
+ margin-bottom: 30px;
}
.up-votes,
@@ -3153,9 +3194,11 @@ a:hover.medal {
line-height: 22px;
margin: 0 15px 0 2px;
}
+
.up-votes {
background: url(../images/vote-arrow-up-on.png) no-repeat;
}
+
.down-votes {
background: url(../images/vote-arrow-down-on.png) no-repeat;
}
@@ -3164,8 +3207,28 @@ a:hover.medal {
margin-top: 10px;
margin-bottom: 10px;
}
- select {
- margin-bottom: 12px;
+ .pager {
+ margin-bottom: 0;
+ }
+ .paginator {
+ margin-bottom: 0;
+ padding-bottom: 0;
+ }
+ .users-questions {
+ margin-top: -10px;
+ }
+
+ .avatar p {
+ margin-bottom: 0px;
+ }
+ .tabBar a#stats {
+ margin-left: 0;
+ }
+ img.gravatar {
+ margin: 2px 0 3px 0;
+ }
+ .vote-notification h3 {
+ padding: 10px;
}
}
@@ -3742,6 +3805,15 @@ a.offensive {
margin-bottom: 0px;
}
+.system-messages {
+ color: red;
+ background: yellow;
+ font-size: 21px;
+ font-weight: bold;
+ line-height: 25px;
+ padding: 0 5px;
+}
+
p.space-above {
margin-top: 10px;
}
@@ -3969,12 +4041,13 @@ ul.post-tags {
}
ul.post-retag {
- margin-bottom:0px;
- margin-left:5px;
+ margin-bottom: 0px;
+ margin-left: 5px;
input {
width: 400px;
- height: 1.5em;
- margin: 3px 0 0 -3px;
+ height: 22px;
+ margin: 3px 0 0 -5px;
+ padding: 0 5px;
}
}
@@ -4089,28 +4162,6 @@ p.signup_p {
display: inline;
}
-.user-profile-page {
- .avatar p {
- margin-bottom: 0px;
- }
- .tabBar a#stats {
- margin-left: 0;
- }
- img.gravatar {
- margin: 2px 0 3px 0;
- }
- h3 {
- padding: 0;
- margin-top: -3px;
- }
- .vote-notification h3 {
- padding: 10px;
- }
- ul.tags {
- margin-left: 5px;
- }
-}
-
.userList {
font-size: 13px;
}
@@ -4354,27 +4405,31 @@ textarea.tipped-input {
/* tag editor */
.tag-editor {
- height: 64px;
- border: #ccc 3px solid;
- padding-left: 8px;
+ height: 32px;
+ border: #cce6ec 3px solid;
+ padding-left: 6px;
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
ul.tags {
margin: 0;
li {
- margin-top: 8px;
+ margin-top: 6px;
height: 13px;
}
}
input.new-tags-input,
input.new-tags-input:focus {
border: none;
- font-size: 15px;
+ box-shadow: none;
+ font-size: 14px;
font-color: @info-text;
height: 16px;
line-height: 16px;
- margin-top: 9px;
+ margin: 9px 0 0 -6px;
-webkit-box-shadow: none;/* undo bootstrap glow */
-moz-box-shadow: none;
- box-shadow: none;
+ padding: 0 0 0 6px;
}
}
@@ -4525,6 +4580,10 @@ td.setting-input {
}
}
+#recaptcha_area, #recaptcha_table {
+ line-height: 0!important;
+}
+
.acInput {
width: 200px;
}
@@ -4755,17 +4814,29 @@ td.setting-input {
.short-summary:first-child {
padding-top: 0;
}
- #searchBar,
- body.anon #searchBar {
- margin: -49px 8px 0 52px;
- }
- .search-drop-menu,
- body.anon .search-drop-menu {
- margin: 0 8px 0 52px;
- }
.short-summary {
width: 100%;
}
+ #secondaryHeader td.search-bar {
+ padding: 0 6px;
+ }
+}
+
+@media screen and (max-width: 540px) {
+ .question-page {
+ .post-controls {
+ float: left;
+ margin-left: -8px;
+ padding-left: 0;
+ text-align: left;
+ .question-close {
+ display: none;
+ }
+ }
+ div.comments.empty {
+ margin-top: 0;
+ }
+ }
}
@media screen and (max-width: 480px) {
@@ -4788,6 +4859,18 @@ td.setting-input {
}
}
+@media screen and (max-width: 380px) {
+ .wmd-button-bar {
+ .wmd-hr-button,
+ .wmd-heading-button {
+ display: none;
+ }
+ }
+ .editor-status {
+ margin-right: 0;
+ }
+}
+
@media screen and (max-width: 338px) {
#scopeNav {
display: none;
@@ -4802,6 +4885,11 @@ td.setting-input {
margin: 6px 0;
}
}
+ .question-page .post-controls {
+ .offensive-flag {
+ display: none;
+ }
+ }
}
/* language-specific fixes */
@@ -4832,33 +4920,16 @@ body.lang-zh_TW {
&.question-page a.submit {
line-height: 30px !important;
}
- .scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 194px;
- }
- }
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 274px;
- }
- }
}
body.lang-hu {
- .scope-selector{
+ .scope-selector {
font-size: 17px;
margin-left: 10px;
}
- .scopes-True-True-False {
- #scopeNav {
- width: 200px;
- }
- #searchBar,
- .search-drop-menu {
+ #searchBar,
+ .search-drop-menu {
margin-left: 252px;
- }
}
.box {
.inputs{
@@ -4897,45 +4968,12 @@ body.lang-es {
.short-summary .counts .votes div {
font-size: 10px;
}
- .scopes-True-True-False {
- #scopeNav {
- width: 187px;
- }
- #searchBar,
- .search-drop-menu {
- margin-left: 250px;
- }
- }
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 327px;
- }
- }
}
body.lang-de {
.scope-selector {
font-size: 17px;
}
- .scopes-True-True-False {
- #scopeNav {
- width: 218px;
- }
- #searchBar,
- .search-drop-menu {
- margin-left: 250px;
- }
- }
- .scopes-True-True-True {
- #scopeNav {
- width: 354px;
- }
- #searchBar,
- .search-drop-menu {
- margin-left: 354px;
- }
- }
#metaNav a {
font-size: 16px;
}
@@ -4965,12 +5003,6 @@ body.lang-gl {
}
}
}
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 299px;
- }
- }
}
body.lang-pt_BR {
@@ -4984,21 +5016,9 @@ body.lang-pt_BR {
}
}
}
- .scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 236px;
- }
- }
}
body.lang-fr {
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 287px;
- }
- }
.box {
.inputs {
#interestingTagInput,
@@ -5021,18 +5041,6 @@ body.lang-fi {
.scope-selector {
font-size: 17px;
}
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 371px;
- }
- }
- .scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 268px;
- }
- }
.box {
.inputs {
#interestingTagInput,
@@ -5046,24 +5054,9 @@ body.lang-fi {
}
body.lang-ru {
- #scopeNav {
- width: 315px;
- }
.scope-selector {
font-size: 17px;
}
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 326px;
- }
- }
- .scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 227px;
- }
- }
.box {
.inputs {
#interestingTagInput,
@@ -5080,18 +5073,6 @@ body.lang-ko {
#scopeNav {
width: 315px;
}
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 248px;
- }
- }
- .scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 183px;
- }
- }
.box {
.inputs {
#interestingTagInput,
@@ -5106,18 +5087,6 @@ body.lang-ko {
body.lang-nb_NO,
body.lang-nb_NO, {
- .scopes-True-True-True {
- #searchBar,
- .search-drop-menu {
- margin-left: 289px;
- }
- }
- .scopes-True-True-False {
- #searchBar,
- .search-drop-menu {
- margin-left: 220px;
- }
- }
.box {
.inputs {
#interestingTagInput,
diff --git a/askbot/middleware/cancel.py b/askbot/middleware/cancel.py
index f13d8d69..ac2f3ded 100644
--- a/askbot/middleware/cancel.py
+++ b/askbot/middleware/cancel.py
@@ -8,7 +8,7 @@ class CancelActionMiddleware(object):
msg = getattr(view_func,'CANCEL_MESSAGE')
except AttributeError:
msg = 'action canceled'
- request.user.message_set.create(message=msg)
+ request.user.message_set.create(message=unicode(msg))
return HttpResponseRedirect(get_next_url(request))
else:
return None
diff --git a/askbot/middleware/forum_mode.py b/askbot/middleware/forum_mode.py
index 331fe85b..5fd2bda3 100644
--- a/askbot/middleware/forum_mode.py
+++ b/askbot/middleware/forum_mode.py
@@ -8,13 +8,14 @@ from django.conf import settings
from django.core.urlresolvers import resolve
from askbot.shims.django_shims import ResolverMatch
from askbot.conf import settings as askbot_settings
+import urllib
PROTECTED_VIEW_MODULES = (
'askbot.views',
'askbot.feed',
)
ALLOWED_VIEWS = (
- 'askbot.views.meta.media',
+ 'askbot.views.meta.config_variable',
)
def is_view_protected(view_func):
@@ -63,5 +64,9 @@ class ForumModeMiddleware(object):
_('Please log in to use %s') % \
askbot_settings.APP_SHORT_NAME
)
- return HttpResponseRedirect(settings.LOGIN_URL)
+ redirect_url = '%s?next=%s' % (
+ settings.LOGIN_URL,
+ urllib.quote_plus(request.get_full_path())
+ )
+ return HttpResponseRedirect(redirect_url)
return None
diff --git a/askbot/middleware/remote_ip.py b/askbot/middleware/remote_ip.py
new file mode 100644
index 00000000..18280d0d
--- /dev/null
+++ b/askbot/middleware/remote_ip.py
@@ -0,0 +1,21 @@
+"""Fixes REMOTE_IP meta value
+based on the HTTP_X_FORWARDED_FOR value, if used.
+Enable this middleware if using django site behind a proxy
+server or a load balancer.
+
+Add to the MIDDLEWARE_CLASSES:
+
+ 'askbot.middleware.remote_ip.SetRemoteIPFromXForwardedFor',
+"""
+
+class SetRemoteIPFromXForwardedFor(object):
+ def process_request(self, request):
+ try:
+ real_ip = request.META['HTTP_X_FORWARDED_FOR']
+ except KeyError:
+ pass
+ else:
+ # HTTP_X_FORWARDED_FOR can be a comma-separated list of IPs.
+ # Take just the first one.
+ real_ip = real_ip.split(",")[0]
+ request.META['REMOTE_ADDR'] = real_ip
diff --git a/askbot/migrations/0006_add_subscription_setting_for_comments_and_mentions.py b/askbot/migrations/0006_add_subscription_setting_for_comments_and_mentions.py
index 1dbfe24f..711e87e4 100644
--- a/askbot/migrations/0006_add_subscription_setting_for_comments_and_mentions.py
+++ b/askbot/migrations/0006_add_subscription_setting_for_comments_and_mentions.py
@@ -45,7 +45,7 @@ class Migration(DataMigration):
new_feed.save()
verbose_frequency = unicode(dict(const.NOTIFICATION_DELIVERY_SCHEDULE_CHOICES)[frequency])
print 'added \'%s\' subscription for %s (%d)' % (
- verbose_frequency,
+ unicode(verbose_frequency),
unidecode(user.username),
user.id
)
diff --git a/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py b/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py
index 2c58d82a..d7c28d3e 100644
--- a/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py
+++ b/askbot/migrations/0032_auto__del_field_badgedata_multiple__del_field_badgedata_description__d.py
@@ -9,7 +9,7 @@ class Migration(SchemaMigration):
def forwards(self, orm):
# Removing unique constraint on 'BadgeData', fields ['type', 'name']
- #db.delete_unique('askbot_badgedata', ['type', 'name'])
+ db.delete_unique('askbot_badgedata', ['type', 'name'])
# Deleting field 'BadgeData.multiple'
db.delete_column('askbot_badgedata', 'multiple')
@@ -17,6 +17,7 @@ class Migration(SchemaMigration):
# Deleting field 'BadgeData.description'
db.delete_column('askbot_badgedata', 'description')
+
# Deleting field 'BadgeData.type'
db.delete_column('askbot_badgedata', 'type')
diff --git a/askbot/migrations/0055_auto__chg_field_question_thread.py b/askbot/migrations/0055_auto__chg_field_question_thread.py
index 9fa7885b..f0f04916 100644
--- a/askbot/migrations/0055_auto__chg_field_question_thread.py
+++ b/askbot/migrations/0055_auto__chg_field_question_thread.py
@@ -9,7 +9,7 @@ class Migration(SchemaMigration):
def forwards(self, orm):
# Changing field 'Question.thread'
- db.alter_column(u'question', 'thread_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, unique=True, to=orm['askbot.Thread']))
+ db.alter_column(u'question', 'thread_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, unique=True, to=orm['askbot.Thread']))
def backwards(self, orm):
diff --git a/askbot/migrations/0080_transplant_favquestions_2.py b/askbot/migrations/0080_transplant_favquestions_2.py
index 3edd95c3..81a8ea56 100644
--- a/askbot/migrations/0080_transplant_favquestions_2.py
+++ b/askbot/migrations/0080_transplant_favquestions_2.py
@@ -12,7 +12,7 @@ class Migration(SchemaMigration):
db.delete_column(u'favorite_question', 'question_id')
# Changing field 'FavoriteQuestion.thread'
- db.alter_column(u'favorite_question', 'thread_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['askbot.Thread']))
+ db.alter_column(u'favorite_question', 'thread_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['askbot.Thread']))
def backwards(self, orm):
diff --git a/askbot/migrations/0093_auto__del_field_vote_content_type__del_field_vote_object_id__chg_field.py b/askbot/migrations/0093_auto__del_field_vote_content_type__del_field_vote_object_id__chg_field.py
index 830a3cd6..ccbb3e25 100644
--- a/askbot/migrations/0093_auto__del_field_vote_content_type__del_field_vote_object_id__chg_field.py
+++ b/askbot/migrations/0093_auto__del_field_vote_content_type__del_field_vote_object_id__chg_field.py
@@ -18,7 +18,7 @@ class Migration(SchemaMigration):
db.delete_column(u'vote', 'object_id')
# Changing field 'Vote.voted_post'
- db.alter_column(u'vote', 'voted_post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['askbot.Post']))
+ db.alter_column(u'vote', 'voted_post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['askbot.Post']))
# Adding unique constraint on 'Vote', fields ['user', 'voted_post']
db.create_unique(u'vote', ['user_id', 'voted_post_id'])
diff --git a/askbot/migrations/0099_auto__del_field_thread_accepted_answer__del_field_anonymousanswer_ques.py b/askbot/migrations/0099_auto__del_field_thread_accepted_answer__del_field_anonymousanswer_ques.py
index ac9507a5..36dd4850 100644
--- a/askbot/migrations/0099_auto__del_field_thread_accepted_answer__del_field_anonymousanswer_ques.py
+++ b/askbot/migrations/0099_auto__del_field_thread_accepted_answer__del_field_anonymousanswer_ques.py
@@ -21,13 +21,13 @@ class Migration(SchemaMigration):
db.delete_column('askbot_anonymousanswer', 'question_id')
# Changing field 'AnonymousAnswer.question_post'
- db.alter_column('askbot_anonymousanswer', 'question_post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['askbot.Post']))
+ db.alter_column('askbot_anonymousanswer', 'question_post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['askbot.Post']))
# Deleting field 'QuestionView.question'
db.delete_column('askbot_questionview', 'question_id')
# Changing field 'QuestionView.question_post'
- db.alter_column('askbot_questionview', 'question_post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=0, to=orm['askbot.Post']))
+ db.alter_column('askbot_questionview', 'question_post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['askbot.Post']))
# Deleting field 'PostRevision.question'
db.delete_column('askbot_postrevision', 'question_id')
diff --git a/askbot/migrations/0171_auto__add_importedobjectinfo__add_importrun.py b/askbot/migrations/0171_auto__add_importedobjectinfo__add_importrun.py
new file mode 100644
index 00000000..8822dda8
--- /dev/null
+++ b/askbot/migrations/0171_auto__add_importedobjectinfo__add_importrun.py
@@ -0,0 +1,439 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'ImportedObjectInfo'
+ db.create_table('askbot_importedobjectinfo', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('old_id', self.gf('django.db.models.fields.IntegerField')()),
+ ('new_id', self.gf('django.db.models.fields.IntegerField')()),
+ ('model', self.gf('django.db.models.fields.CharField')(default='', max_length=255)),
+ ('run', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['askbot.ImportRun'])),
+ ('extra_info', self.gf('picklefield.fields.PickledObjectField')()),
+ ))
+ db.send_create_signal('askbot', ['ImportedObjectInfo'])
+
+ # Adding model 'ImportRun'
+ db.create_table('askbot_importrun', (
+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+ ('command', self.gf('django.db.models.fields.TextField')(default='')),
+ ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('askbot', ['ImportRun'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'ImportedObjectInfo'
+ db.delete_table('askbot_importedobjectinfo')
+
+ # Deleting model 'ImportRun'
+ db.delete_table('askbot_importrun')
+
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['askbot'] \ No newline at end of file
diff --git a/askbot/migrations/0172_sync_thread_deleted.py b/askbot/migrations/0172_sync_thread_deleted.py
new file mode 100644
index 00000000..961df840
--- /dev/null
+++ b/askbot/migrations/0172_sync_thread_deleted.py
@@ -0,0 +1,419 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import DataMigration
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ for q in orm.Post.objects.filter(post_type='question', deleted=False,
+ thread__deleted=True):
+ q.thread.deleted = False
+ q.thread.save()
+
+ def backwards(self, orm):
+ pass
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': u"orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': u"orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': u"orm['auth.User']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': u"orm['auth.User']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': u"orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': u"orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': [u'auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ u'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ u'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': u"orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': u"orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': u"orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': u"orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': u"orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': u"orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': u"orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': u"orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': u"orm['auth.User']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': u"orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': u"orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': u"orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': u"orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': u"orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
+ },
+ u'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ u'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': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ u'auth.permission': {
+ 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ u'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ u'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'}),
+ u'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'})
+ }
+ }
+
+ complete_apps = ['askbot']
+ symmetrical = True
diff --git a/askbot/migrations/0173_auto__del_unique_tag_name__chg_field_thread_added_at.py b/askbot/migrations/0173_auto__del_unique_tag_name__chg_field_thread_added_at.py
new file mode 100644
index 00000000..0550c79d
--- /dev/null
+++ b/askbot/migrations/0173_auto__del_unique_tag_name__chg_field_thread_added_at.py
@@ -0,0 +1,426 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Removing unique constraint on 'Tag', fields ['name']
+ db.delete_unique(u'tag', ['name'])
+
+
+ # Changing field 'Thread.added_at'
+ db.alter_column('askbot_thread', 'added_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True))
+
+ def backwards(self, orm):
+ # Adding unique constraint on 'Tag', fields ['name']
+ db.create_unique(u'tag', ['name'])
+
+
+ # Changing field 'Thread.added_at'
+ db.alter_column('askbot_thread', 'added_at', self.gf('django.db.models.fields.DateTimeField')())
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['askbot'] \ No newline at end of file
diff --git a/askbot/migrations/0174_auto__add_field_tag_language_code.py b/askbot/migrations/0174_auto__add_field_tag_language_code.py
new file mode 100644
index 00000000..bac3dd7b
--- /dev/null
+++ b/askbot/migrations/0174_auto__add_field_tag_language_code.py
@@ -0,0 +1,423 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'Tag.language_code'
+ db.add_column(u'tag', 'language_code',
+ self.gf('django.db.models.fields.CharField')(default='en', max_length=16),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'Tag.language_code'
+ db.delete_column(u'tag', 'language_code')
+
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['askbot'] \ No newline at end of file
diff --git a/askbot/migrations/0175_auto__add_unique_tag_name_language_code.py b/askbot/migrations/0175_auto__add_unique_tag_name_language_code.py
new file mode 100644
index 00000000..daa404af
--- /dev/null
+++ b/askbot/migrations/0175_auto__add_unique_tag_name_language_code.py
@@ -0,0 +1,421 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding unique constraint on 'Tag', fields ['name', 'language_code']
+ db.create_unique(u'tag', ['name', 'language_code'])
+
+
+ def backwards(self, orm):
+ # Removing unique constraint on 'Tag', fields ['name', 'language_code']
+ db.delete_unique(u'tag', ['name', 'language_code'])
+
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'unique_together': "(('name', 'language_code'),)", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['askbot'] \ No newline at end of file
diff --git a/askbot/migrations/0176_populate_tag_language_code.py b/askbot/migrations/0176_populate_tag_language_code.py
new file mode 100644
index 00000000..6c9f0aa7
--- /dev/null
+++ b/askbot/migrations/0176_populate_tag_language_code.py
@@ -0,0 +1,464 @@
+# -*- coding: utf-8 -*-
+import datetime
+from askbot.utils.console import ProgressBar
+from south.db import db
+from south.v2 import DataMigration
+from django.conf import settings as django_settings
+from django.db import models
+
+class Migration(DataMigration):
+
+ def forwards(self, orm):
+ "Write your forwards methods here."
+ # Note: Don't use "from appname.models import ModelName".
+ # Use orm.ModelName to refer to models in this application,
+ # and orm['appname.ModelName'] for models in other applications.
+ threads = orm['askbot.Thread'].objects.all()
+ count = threads.count()
+ message = 'Applying language code to tags:'
+ for thread in ProgressBar(threads.iterator(), count, message):
+ tags = thread.tags.all()
+ #load each tag by name + language code
+ for tag in tags:
+ #maybe fix language code of thread, unlikely
+ if thread.language_code == '':
+ thread.language_code = django_settings.LANGUAGE_CODE
+ thread.save()
+
+ #if tg has no language code, we just copy it from thread
+ if tag.language_code == '':
+ tag.language_code = thread.language_code
+ tag.save()
+ elif tag.language_code != thread.language_code:
+ try:
+ new_tag = orm['askbot.Tag'].objects.get(
+ name=tag.name,
+ language_code=thread.language_code
+ )
+ if tags.filter(id=new_tag.id).exists():
+ continue
+ new_tag.used_count += 1
+
+ except orm['askbot.Tag'].DoesNotExist:
+ new_tag = orm['askbot.Tag']()
+ new_tag.name = tag.name
+ new_tag.language_code = thread.language_code
+ new_tag.created_by = thread.last_activity_by
+ new_tag.used_count = 1
+ new_tag.status = 1
+ new_tag.save()
+ new_tag.suggested_by.add(thread.last_activity_by)
+
+ if tag.used_count > 1:
+ tag.used_count -= 1
+ tag.save()
+ thread.tags.remove(tag)
+ thread.tags.add(new_tag)
+
+
+ def backwards(self, orm):
+ "Write your backwards methods here."
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'unique_together': "(('name', 'language_code'),)", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['askbot']
+ symmetrical = True
diff --git a/askbot/migrations/0177_auto__add_field_tagsynonym_language_code.py b/askbot/migrations/0177_auto__add_field_tagsynonym_language_code.py
new file mode 100644
index 00000000..1a9d38c8
--- /dev/null
+++ b/askbot/migrations/0177_auto__add_field_tagsynonym_language_code.py
@@ -0,0 +1,424 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding field 'TagSynonym.language_code'
+ db.add_column('askbot_tagsynonym', 'language_code',
+ self.gf('django.db.models.fields.CharField')(default='en', max_length=16),
+ keep_default=False)
+
+
+ def backwards(self, orm):
+ # Deleting field 'TagSynonym.language_code'
+ db.delete_column('askbot_tagsynonym', 'language_code')
+
+
+ models = {
+ 'askbot.activity': {
+ 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"},
+ 'active_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'activity_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_auditted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True'}),
+ 'receiving_users': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'received_activity'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'recipients': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'incoming_activity'", 'symmetrical': 'False', 'through': "orm['askbot.ActivityAuditStatus']", 'to': "orm['auth.User']"}),
+ 'summary': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.activityauditstatus': {
+ 'Meta': {'unique_together': "(('user', 'activity'),)", 'object_name': 'ActivityAuditStatus'},
+ 'activity': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Activity']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.anonymousanswer': {
+ 'Meta': {'object_name': 'AnonymousAnswer'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'anonymous_answers'", 'to': "orm['askbot.Post']"}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.anonymousquestion': {
+ 'Meta': {'object_name': 'AnonymousQuestion'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ip_addr': ('django.db.models.fields.IPAddressField', [], {'max_length': '15'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'session_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'text': ('django.db.models.fields.TextField', [], {}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.askwidget': {
+ 'Meta': {'object_name': 'AskWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'include_text_field': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'inner_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'outer_style': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Tag']", 'null': 'True', 'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.award': {
+ 'Meta': {'object_name': 'Award', 'db_table': "u'award'"},
+ 'awarded_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'badge': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_badge'", 'to': "orm['askbot.BadgeData']"}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'notified': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'award_user'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.badgedata': {
+ 'Meta': {'ordering': "('slug',)", 'object_name': 'BadgeData'},
+ 'awarded_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'awarded_to': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'badges'", 'symmetrical': 'False', 'through': "orm['askbot.Award']", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50'})
+ },
+ 'askbot.bulktagsubscription': {
+ 'Meta': {'ordering': "['-date_added']", 'object_name': 'BulkTagSubscription'},
+ 'date_added': ('django.db.models.fields.DateField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Group']", 'symmetrical': 'False'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['askbot.Tag']", 'symmetrical': 'False'}),
+ 'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'symmetrical': 'False'})
+ },
+ 'askbot.draftanswer': {
+ 'Meta': {'object_name': 'DraftAnswer'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'draft_answers'", 'to': "orm['askbot.Thread']"})
+ },
+ 'askbot.draftquestion': {
+ 'Meta': {'object_name': 'DraftQuestion'},
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125', 'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300', 'null': 'True'})
+ },
+ 'askbot.emailfeedsetting': {
+ 'Meta': {'unique_together': "(('subscriber', 'feed_type'),)", 'object_name': 'EmailFeedSetting'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'feed_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'frequency': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '8'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reported_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+ 'subscriber': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notification_subscriptions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.favoritequestion': {
+ 'Meta': {'object_name': 'FavoriteQuestion', 'db_table': "u'favorite_question'"},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_favorite_questions'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.group': {
+ 'Meta': {'object_name': 'Group', '_ormbases': ['auth.Group']},
+ 'description': ('django.db.models.fields.related.OneToOneField', [], {'blank': 'True', 'related_name': "'described_group'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'group_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.Group']", 'unique': 'True', 'primary_key': 'True'}),
+ 'is_vip': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}),
+ 'moderate_answers_to_enquirers': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'openness': ('django.db.models.fields.SmallIntegerField', [], {'default': '2'}),
+ 'preapproved_email_domains': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'preapproved_emails': ('django.db.models.fields.TextField', [], {'default': "''", 'null': 'True', 'blank': 'True'}),
+ 'read_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+ },
+ 'askbot.groupmembership': {
+ 'Meta': {'object_name': 'GroupMembership', '_ormbases': ['auth.AuthUserGroups']},
+ 'authusergroups_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.AuthUserGroups']", 'unique': 'True', 'primary_key': 'True'}),
+ 'level': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.importedobjectinfo': {
+ 'Meta': {'object_name': 'ImportedObjectInfo'},
+ 'extra_info': ('picklefield.fields.PickledObjectField', [], {}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255'}),
+ 'new_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'old_id': ('django.db.models.fields.IntegerField', [], {}),
+ 'run': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.ImportRun']"})
+ },
+ 'askbot.importrun': {
+ 'Meta': {'object_name': 'ImportRun'},
+ 'command': ('django.db.models.fields.TextField', [], {'default': "''"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'})
+ },
+ 'askbot.markedtag': {
+ 'Meta': {'object_name': 'MarkedTag'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'reason': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
+ 'tag': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_selections'", 'to': "orm['askbot.Tag']"}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_selections'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.post': {
+ 'Meta': {'object_name': 'Post'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'posts'", 'to': "orm['auth.User']"}),
+ 'comment_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_posts'", 'symmetrical': 'False', 'through': "orm['askbot.PostToGroup']", 'to': "orm['askbot.Group']"}),
+ 'html': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_edited_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'last_edited_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'last_edited_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'locked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'locked_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'locked_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locked_posts'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'offensive_flag_count': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'old_answer_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_comment_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'old_question_id': ('django.db.models.fields.PositiveIntegerField', [], {'default': 'None', 'unique': 'True', 'null': 'True', 'blank': 'True'}),
+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'comments'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
+ 'summary': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'related_name': "'posts'", 'null': 'True', 'blank': 'True', 'to': "orm['askbot.Thread']"}),
+ 'vote_down_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'vote_up_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'wiki': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'wikified_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
+ },
+ 'askbot.postflagreason': {
+ 'Meta': {'object_name': 'PostFlagReason'},
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
+ 'details': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'post_reject_reasons'", 'to': "orm['askbot.Post']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '128'})
+ },
+ 'askbot.postrevision': {
+ 'Meta': {'ordering': "('-revision',)", 'unique_together': "(('post', 'revision'),)", 'object_name': 'PostRevision'},
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'approved_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'approved_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'postrevisions'", 'to': "orm['auth.User']"}),
+ 'by_email': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_address': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'is_anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'revisions'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'revised_at': ('django.db.models.fields.DateTimeField', [], {}),
+ 'revision': ('django.db.models.fields.PositiveIntegerField', [], {}),
+ 'summary': ('django.db.models.fields.CharField', [], {'max_length': '300', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '125', 'blank': 'True'}),
+ 'text': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'})
+ },
+ 'askbot.posttogroup': {
+ 'Meta': {'unique_together': "(('post', 'group'),)", 'object_name': 'PostToGroup', 'db_table': "'askbot_post_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']"})
+ },
+ 'askbot.questionview': {
+ 'Meta': {'object_name': 'QuestionView'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'viewed'", 'to': "orm['askbot.Post']"}),
+ 'when': ('django.db.models.fields.DateTimeField', [], {}),
+ 'who': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'question_views'", 'to': "orm['auth.User']"})
+ },
+ 'askbot.questionwidget': {
+ 'Meta': {'object_name': 'QuestionWidget'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']", 'null': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'order_by': ('django.db.models.fields.CharField', [], {'default': "'-added_at'", 'max_length': '18'}),
+ 'question_number': ('django.db.models.fields.PositiveIntegerField', [], {'default': '7'}),
+ 'search_query': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '50', 'null': 'True', 'blank': 'True'}),
+ 'style': ('django.db.models.fields.TextField', [], {'default': '"\\n@import url(\'http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700\');\\nbody {\\n overflow: hidden;\\n}\\n\\n#container {\\n width: 200px;\\n height: 350px;\\n}\\nul {\\n list-style: none;\\n padding: 5px;\\n margin: 5px;\\n}\\nli {\\n border-bottom: #CCC 1px solid;\\n padding-bottom: 5px;\\n padding-top: 5px;\\n}\\nli:last-child {\\n border: none;\\n}\\na {\\n text-decoration: none;\\n color: #464646;\\n font-family: \'Yanone Kaffeesatz\', sans-serif;\\n font-size: 15px;\\n}\\n"', 'blank': 'True'}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '50'}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ },
+ 'askbot.replyaddress': {
+ 'Meta': {'object_name': 'ReplyAddress'},
+ 'address': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '25'}),
+ 'allowed_from_email': ('django.db.models.fields.EmailField', [], {'max_length': '150'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'reply_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'reply_action': ('django.db.models.fields.CharField', [], {'default': "'auto_answer_or_comment'", 'max_length': '32'}),
+ 'response_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'edit_addresses'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_at': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.repute': {
+ 'Meta': {'object_name': 'Repute', 'db_table': "u'repute'"},
+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'negative': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'positive': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Post']", 'null': 'True', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.IntegerField', [], {'default': '1'}),
+ 'reputation_type': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'reputed_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ 'askbot.tag': {
+ 'Meta': {'ordering': "('-used_count', 'name')", 'unique_together': "(('name', 'language_code'),)", 'object_name': 'Tag', 'db_table': "u'tag'"},
+ 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'created_tags'", 'to': "orm['auth.User']"}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'deleted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'deleted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'deleted_tags'", 'null': 'True', 'to': "orm['auth.User']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+ 'status': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'suggested_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'suggested_tags'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'tag_wiki': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'described_tag'", 'unique': 'True', 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'used_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.tagsynonym': {
+ 'Meta': {'object_name': 'TagSynonym'},
+ 'auto_rename_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_auto_rename_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
+ 'owned_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tag_synonyms'", 'to': "orm['auth.User']"}),
+ 'source_tag_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'target_tag_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'})
+ },
+ 'askbot.thread': {
+ 'Meta': {'object_name': 'Thread'},
+ 'accepted_answer': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'to': "orm['askbot.Post']"}),
+ 'added_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'answer_accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'answer_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'approved': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'db_index': 'True'}),
+ 'close_reason': ('django.db.models.fields.SmallIntegerField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'closed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+ 'closed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}),
+ 'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}),
+ 'favorited_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'unused_favorite_threads'", 'symmetrical': 'False', 'through': "orm['askbot.FavoriteQuestion']", 'to': "orm['auth.User']"}),
+ 'favourite_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+ 'followed_by': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'followed_threads'", 'symmetrical': 'False', 'to': "orm['auth.User']"}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'group_threads'", 'symmetrical': 'False', 'through': "orm['askbot.ThreadToGroup']", 'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'language_code': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '16'}),
+ 'last_activity_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_activity_by': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'unused_last_active_in_threads'", 'to': "orm['auth.User']"}),
+ 'points': ('django.db.models.fields.IntegerField', [], {'default': '0', 'db_column': "'score'"}),
+ 'tagnames': ('django.db.models.fields.CharField', [], {'max_length': '125'}),
+ 'tags': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'threads'", 'symmetrical': 'False', 'to': "orm['askbot.Tag']"}),
+ 'title': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
+ 'view_count': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+ },
+ 'askbot.threadtogroup': {
+ 'Meta': {'unique_together': "(('thread', 'group'),)", 'object_name': 'ThreadToGroup', 'db_table': "'askbot_thread_groups'"},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['askbot.Thread']"}),
+ 'visibility': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'})
+ },
+ 'askbot.vote': {
+ 'Meta': {'unique_together': "(('user', 'voted_post'),)", 'object_name': 'Vote', 'db_table': "u'vote'"},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['auth.User']"}),
+ 'vote': ('django.db.models.fields.SmallIntegerField', [], {}),
+ 'voted_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'voted_post': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'votes'", 'to': "orm['askbot.Post']"})
+ },
+ 'auth.authusergroups': {
+ 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'AuthUserGroups', 'db_table': "'auth_user_groups'", 'managed': 'False'},
+ 'group': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.Group']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ },
+ 'auth.user': {
+ 'Meta': {'object_name': 'User'},
+ 'about': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'avatar_type': ('django.db.models.fields.CharField', [], {'default': "'n'", 'max_length': '1'}),
+ 'bronze': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'consecutive_days_visit_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'blank': 'True'}),
+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'date_of_birth': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
+ 'display_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+ 'email_isvalid': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'email_key': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True'}),
+ 'email_signature': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'email_tag_filter_strategy': ('django.db.models.fields.SmallIntegerField', [], {'default': '1'}),
+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'gold': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'gravatar': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'ignored_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'interesting_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'is_fake': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'languages': ('django.db.models.fields.CharField', [], {'default': "'en'", 'max_length': '128'}),
+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+ 'last_seen': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+ 'location': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'new_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+ 'questions_per_page': ('django.db.models.fields.SmallIntegerField', [], {'default': '10'}),
+ 'real_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
+ 'reputation': ('django.db.models.fields.PositiveIntegerField', [], {'default': '1'}),
+ 'seen_response_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'show_country': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'show_marked_tags': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+ 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}),
+ 'social_sharing_mode': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+ 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}),
+ 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
+ 'twitter_access_token': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256'}),
+ 'twitter_handle': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32'}),
+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
+ 'website': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['askbot'] \ No newline at end of file
diff --git a/askbot/migrations_api/__init__.py b/askbot/migrations_api/__init__.py
index a9b20a69..5a65f9cf 100644
--- a/askbot/migrations_api/__init__.py
+++ b/askbot/migrations_api/__init__.py
@@ -13,7 +13,7 @@ def safe_add_column(table, column, column_data, keep_default = False):
so, we need to add these columns here in separate transactions
and roll back if they fail, if we want we could also record - which columns clash
"""
- if db.backend_name == 'mysql':
+ if db.backend_name in ('mysql', 'postgres'):
if len(db.execute('select column_name from information_schema.columns where table_name=%s and column_name=%s', params=[table, column])) == 0:
db.add_column(table, column, column_data, keep_default = keep_default)
else:
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index e94e3c22..93c8d599 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -19,9 +19,11 @@ import uuid
from celery import states
from celery.task import task
from django.core.urlresolvers import reverse, NoReverseMatch
+from django.core.paginator import Paginator
from django.db.models import signals as django_signals
from django.template import Context
from django.template.loader import get_template
+from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from django.utils.safestring import mark_safe
@@ -34,7 +36,7 @@ from django.core import exceptions as django_exceptions
from django_countries.fields import CountryField
from askbot import exceptions as askbot_exceptions
from askbot import const
-from askbot.const.message_keys import get_i18n_message
+from askbot.const import message_keys
from askbot.conf import settings as askbot_settings
from askbot.models.question import Thread
from askbot.skins import utils as skin_utils
@@ -53,10 +55,10 @@ from askbot.models.post import PostFlagReason, AnonymousAnswer
from askbot.models.post import PostToGroup
from askbot.models.post import DraftAnswer
from askbot.models.reply_by_email import ReplyAddress
-from askbot.models import signals
-from askbot.models.badges import award_badges_signal, get_badge, BadgeData
-from askbot.models.repute import Award, Repute, Vote
+from askbot.models.badges import award_badges_signal, get_badge
+from askbot.models.repute import Award, Repute, Vote, BadgeData
from askbot.models.widgets import AskWidget, QuestionWidget
+from askbot.models.meta import ImportRun, ImportedObjectInfo
from askbot import auth
from askbot.utils.decorators import auto_now_timestamp
from askbot.utils.markup import URL_RE
@@ -67,6 +69,7 @@ from askbot.utils.html import site_url
from askbot.utils.diff import textDiff as htmldiff
from askbot.utils.url_utils import strip_path
from askbot import mail
+from askbot.models import signals
from django import VERSION
@@ -75,6 +78,8 @@ DJANGO_VERSION = VERSION[:2]
if DJANGO_VERSION > (1, 3):
from askbot.models.message import Message
+else:
+ from django.contrib.messages.models import Message
def get_model(model_name):
"""a shortcut for getting model for an askbot app"""
@@ -94,8 +99,7 @@ def get_admin():
if User.objects.filter(username='_admin_').count() == 0:
admin = User.objects.create_user('_admin_', '')
admin.set_unusable_password()
- admin.set_admin_status()
- admin.save()
+ admin.set_status('d')
return admin
else:
raise User.DoesNotExist
@@ -261,13 +265,14 @@ User.add_to_class(
)
)
-GRAVATAR_TEMPLATE = "//www.gravatar.com/avatar/%(gravatar)s?" + \
+GRAVATAR_TEMPLATE = "%(gravatar_url)s/%(gravatar)s?" + \
"s=%(size)d&amp;d=%(type)s&amp;r=PG"
def user_get_gravatar_url(self, size):
"""returns gravatar url
"""
return GRAVATAR_TEMPLATE % {
+ 'gravatar_url': askbot_settings.GRAVATAR_BASE_URL,
'gravatar': self.gravatar,
'type': askbot_settings.GRAVATAR_TYPE,
'size': size,
@@ -302,11 +307,25 @@ def user_get_avatar_url(self, size=48):
raise django_exceptions.ImproperlyConfigured(message)
else:
return self.get_gravatar_url(size)
+ if askbot_settings.ENABLE_GRAVATAR:
+ return self.get_gravatar_url(size)
else:
- if askbot_settings.ENABLE_GRAVATAR:
- return self.get_gravatar_url(size)
- else:
- return self.get_default_avatar_url(size)
+ return self.get_default_avatar_url(size)
+
+def user_get_top_answers_paginator(self, visitor=None):
+ """get paginator for top answers by the user for a
+ specific visitor"""
+ answers = self.posts.get_answers(
+ visitor
+ ).filter(
+ deleted=False,
+ thread__deleted=False
+ ).select_related(
+ 'thread'
+ ).order_by(
+ '-points', '-added_at'
+ )
+ return Paginator(answers, const.USER_POSTS_PAGE_SIZE)
def user_update_avatar_type(self):
"""counts number of custom avatars
@@ -336,7 +355,10 @@ def user_strip_email_signature(self, text):
return text
def _check_gravatar(gravatar):
- gravatar_url = "http://www.gravatar.com/avatar/%s?d=404" % gravatar
+ return 'n'
+ #todo: think of whether we need this and if so
+ #how to check the avatar type appropriately
+ gravatar_url = askbot_settings.GRAVATAR_BASE_URL + "/%s?d=404" % gravatar
code = urllib.urlopen(gravatar_url).getcode()
if urllib.urlopen(gravatar_url).getcode() != 404:
return 'g' #gravatar
@@ -365,8 +387,9 @@ def user_get_marked_tags(self, reason):
return Tag.objects.none()
return Tag.objects.filter(
- user_selections__user = self,
- user_selections__reason = reason
+ user_selections__user=self,
+ user_selections__reason=reason,
+ language_code=get_language()
)
MARKED_TAG_PROPERTY_MAP = {
@@ -448,6 +471,13 @@ def user_has_interesting_wildcard_tags(self):
and self.interesting_tags != ''
)
+def user_has_badge(self, badge):
+ """True, if user was awarded a given badge,
+ ``badge`` is instance of BadgeData
+ """
+ return Award.objects.filter(user=self, badge=badge).count() > 0
+
+
def user_can_create_tags(self):
"""true if user can create tags"""
if askbot_settings.ENABLE_TAG_MODERATION:
@@ -513,6 +543,20 @@ def user_get_or_create_fake_user(self, username, email):
user.save()
return user
+def get_or_create_anonymous_user():
+ """returns fake anonymous user"""
+ username = get_name_of_anonymous_user()
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ user = User()
+ user.username = username
+ user.email = askbot_settings.ANONYMOUS_USER_EMAIL
+ user.is_fake = True
+ user.set_unusable_password()
+ user.save()
+ return user
+
def user_notify_users(
self, notification_type=None, recipients=None, content_object=None
):
@@ -549,19 +593,16 @@ def user_get_notifications(self, notification_types=None, **kwargs):
)
def _assert_user_can(
- user = None,
- post = None, #related post (may be parent)
- admin_or_moderator_required = False,
- owner_can = False,
- suspended_owner_cannot = False,
- owner_min_rep_setting = None,
- blocked_error_message = None,
- suspended_error_message = None,
- min_rep_setting = None,
- low_rep_error_message = None,
- owner_low_rep_error_message = None,
- general_error_message = None,
- ):
+ user=None,
+ post=None, #related post (may be parent)
+ admin_or_moderator_required=False,
+ owner_can=False,
+ action_display=None,
+ suspended_owner_cannot=False,
+ suspended_user_cannot=False,
+ blocked_user_cannot=False,
+ min_rep_setting=None
+ ):
"""generic helper assert for use in several
User.assert_can_XYZ() calls regarding changing content
@@ -570,69 +611,81 @@ def _assert_user_can(
if assertion fails, method raises exception.PermissionDenied
with appropriate text as a payload
"""
+ action_display = action_display or _('perform this action')
if askbot_settings.GROUPS_ENABLED:
if user.is_read_only():
message = _('Sorry, but you have only read access')
raise django_exceptions.PermissionDenied(message)
- if general_error_message is None:
- general_error_message = _('Sorry, this operation is not allowed')
- if blocked_error_message and user.is_blocked():
- error_message = blocked_error_message
+ if blocked_user_cannot and user.is_blocked():
+ error_message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % {
+ 'perform_action': action_display,
+ 'your_account_is': _('your account is blocked')
+ }
elif post and owner_can and user == post.get_owner():
- if owner_min_rep_setting:
- if post.get_owner().reputation < owner_min_rep_setting:
- if user.is_moderator() or user.is_administrator():
- return
- else:
- assert(owner_low_rep_error_message is not None)
- raise askbot_exceptions.InsufficientReputation(
- owner_low_rep_error_message
- )
- if suspended_owner_cannot and user.is_suspended():
- if suspended_error_message:
- error_message = suspended_error_message
- else:
- error_message = general_error_message
- assert(error_message is not None)
- raise django_exceptions.PermissionDenied(error_message)
+ if user.is_suspended() and suspended_owner_cannot:
+ error_message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % {
+ 'perform_action': action_display,
+ 'your_account_is': _('your account is suspended')
+ }
else:
return
- return
- elif suspended_error_message and user.is_suspended():
- error_message = suspended_error_message
+ elif suspended_user_cannot and user.is_suspended():
+ error_message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % {
+ 'perform_action': action_display,
+ 'your_account_is': _('your account is suspended')
+ }
elif user.is_administrator() or user.is_moderator():
return
elif user.is_post_moderator(post):
return
- elif low_rep_error_message and user.reputation < min_rep_setting:
- raise askbot_exceptions.InsufficientReputation(low_rep_error_message)
+ elif min_rep_setting and user.reputation < min_rep_setting:
+ raise askbot_exceptions.InsufficientReputation(
+ _(message_keys.MIN_REP_REQUIRED_TO_PERFORM_ACTION) % {
+ 'perform_action': action_display,
+ 'min_rep': min_rep_setting
+ }
+ )
+ elif admin_or_moderator_required:
+ if min_rep_setting is None:
+ #message about admins only
+ error_message = _(
+ 'Sorry, only moderators and site administrators can %(perform_action)s'
+ ) % {
+ 'perform_action': action_display
+ }
+ else:
+ #message with minimum reputation
+ error_message = _(
+ 'Sorry, only administrators, moderators '
+ 'or users with reputation > %(min_rep)s '
+ 'can %(perform_action)s'
+ ) % {
+ 'min_rep': min_rep_setting,
+ 'perform_action': action_display
+ }
else:
- if admin_or_moderator_required == False:
- return
+ return
- #if admin or moderator is required, then substitute the message
- if admin_or_moderator_required:
- error_message = general_error_message
assert(error_message is not None)
raise django_exceptions.PermissionDenied(error_message)
def user_assert_can_approve_post_revision(self, post_revision = None):
_assert_user_can(
- user = self,
- admin_or_moderator_required = True
+ user=self,
+ admin_or_moderator_required=True
)
def user_assert_can_unaccept_best_answer(self, answer = None):
assert getattr(answer, 'post_type', '') == 'answer'
- blocked_error_message = _(
- 'Sorry, you cannot accept or unaccept best answers '
- 'because your account is blocked'
- )
- suspended_error_message = _(
- 'Sorry, you cannot accept or unaccept best answers '
- 'because your account is suspended'
- )
+ suspended_error_message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % {
+ 'perform_action': askbot_settings.WORDS_ACCEPT_OR_UNACCEPT_THE_BEST_ANSWER,
+ 'your_account_is': _('your account is suspended')
+ }
+ blocked_error_message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % {
+ 'perform_action': askbot_settings.WORDS_ACCEPT_OR_UNACCEPT_THE_BEST_ANSWER,
+ 'your_account_is': _('your account is blocked')
+ }
if self.is_blocked():
error_message = blocked_error_message
@@ -642,18 +695,12 @@ def user_assert_can_unaccept_best_answer(self, answer = None):
if self == answer.get_owner():
if not self.is_administrator():
#check rep
- min_rep_setting = askbot_settings.MIN_REP_TO_ACCEPT_OWN_ANSWER
- low_rep_error_message = _(
- ">%(points)s points required to accept or unaccept "
- " your own answer to your own question"
- ) % {'points': min_rep_setting}
-
_assert_user_can(
- user = self,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- min_rep_setting = min_rep_setting,
- low_rep_error_message = low_rep_error_message
+ user=self,
+ action_display=askbot_settings.WORDS_ACCEPT_OR_UNACCEPT_OWN_ANSWER,
+ blocked_user_cannot=True,
+ suspended_owner_cannot=True,
+ min_rep_setting = askbot_settings.MIN_REP_TO_ACCEPT_OWN_ANSWER
)
return # success
@@ -667,19 +714,19 @@ def user_assert_can_unaccept_best_answer(self, answer = None):
)
if datetime.datetime.now() < will_be_able_at:
- error_message = _(
- 'Sorry, you will be able to accept this answer '
- 'only after %(will_be_able_at)s'
- ) % {'will_be_able_at': will_be_able_at.strftime('%d/%m/%Y')}
+ error_message = _(message_keys.CANNOT_PERFORM_ACTION_UNTIL) % {
+ 'perform_action': askbot_settings.WORDS_ACCEPT_OR_UNACCEPT_OWN_ANSWER,
+ 'until': will_be_able_at.strftime('%d/%m/%Y')
+ }
else:
return
else:
question_owner = answer.thread._question_post().get_owner()
- error_message = _(
- 'Sorry, only moderators or original author of the question '
- ' - %(username)s - can accept or unaccept the best answer'
- ) % {'username': question_owner.username}
+ error_message = _(message_keys.MODERATORS_OR_AUTHOR_CAN_PEFROM_ACTION) % {
+ 'post_author': askbot_settings.WORDS_AUTHOR_OF_THE_QUESTION,
+ 'perform_action': askbot_settings.WORDS_ACCEPT_OR_UNACCEPT_THE_BEST_ANSWER,
+ }
raise django_exceptions.PermissionDenied(error_message)
@@ -703,54 +750,32 @@ def user_assert_can_vote_for_post(
_('Sorry, you cannot vote for your own posts')
)
- blocked_error_message = _(
- 'Sorry your account appears to be blocked ' +
- 'and you cannot vote - please contact the ' +
- 'site administrator to resolve the issue'
- ),
- suspended_error_message = _(
- 'Sorry your account appears to be suspended ' +
- 'and you cannot vote - please contact the ' +
- 'site administrator to resolve the issue'
- )
-
assert(direction in ('up', 'down'))
if direction == 'up':
min_rep_setting = askbot_settings.MIN_REP_TO_VOTE_UP
- low_rep_error_message = _(
- ">%(points)s points required to upvote"
- ) % \
- {'points': askbot_settings.MIN_REP_TO_VOTE_UP}
+ action_display = _('upvote')
else:
min_rep_setting = askbot_settings.MIN_REP_TO_VOTE_DOWN
- low_rep_error_message = _(
- ">%(points)s points required to downvote"
- ) % \
- {'points': askbot_settings.MIN_REP_TO_VOTE_DOWN}
+ action_display = _('downvote')
_assert_user_can(
- user = self,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
+ user=self,
+ action_display=action_display,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
min_rep_setting = min_rep_setting,
- low_rep_error_message = low_rep_error_message
)
def user_assert_can_upload_file(request_user):
- blocked_error_message = _('Sorry, blocked users cannot upload files')
- suspended_error_message = _('Sorry, suspended users cannot upload files')
- low_rep_error_message = _(
- 'sorry, file uploading requires karma >%(min_rep)s',
- ) % {'min_rep': askbot_settings.MIN_REP_TO_UPLOAD_FILES }
-
_assert_user_can(
- user = request_user,
- suspended_error_message = suspended_error_message,
- min_rep_setting = askbot_settings.MIN_REP_TO_UPLOAD_FILES,
- low_rep_error_message = low_rep_error_message
+ user=request_user,
+ action_display=_('upload files'),
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
+ min_rep_setting=askbot_settings.MIN_REP_TO_UPLOAD_FILES
)
@@ -771,14 +796,11 @@ def user_assert_can_post_question(self):
"""raises exceptions.PermissionDenied with
text that has the reason for the denial
"""
-
- blocked_message = get_i18n_message('BLOCKED_USERS_CANNOT_POST')
- suspended_message = get_i18n_message('SUSPENDED_USERS_CANNOT_POST')
-
_assert_user_can(
- user = self,
- blocked_error_message = blocked_message,
- suspended_error_message = suspended_message
+ user=self,
+ action_display=askbot_settings.WORDS_ASK_QUESTIONS,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
)
@@ -788,8 +810,10 @@ def user_assert_can_post_answer(self, thread = None):
limit_answers = askbot_settings.LIMIT_ONE_ANSWER_PER_USER
if limit_answers and thread.has_answer_by_user(self):
message = _(
- 'Sorry, you already gave an answer, please edit it instead.'
- )
+ 'Sorry, %(you_already_gave_an_answer)s, please edit it instead.'
+ ) % {
+ 'you_already_gave_an_answer': askbot_settings.WORDS_YOU_ALREADY_GAVE_AN_ANSWER
+ }
raise askbot_exceptions.AnswerAlreadyGiven(message)
self.assert_can_post_question()
@@ -828,22 +852,35 @@ def user_assert_can_edit_comment(self, comment = None):
)
raise django_exceptions.PermissionDenied(error_message)
+def user_assert_can_convert_post(self, post = None):
+ """raises exceptions.PermissionDenied if user is not allowed to convert the
+ post to another type (comment -> answer, answer -> comment)
+
+ only owners, moderators or admins can convert posts
+ """
+ if self.is_administrator() or self.is_moderator() or post.author == self:
+ return
+
+ error_message = _(
+ 'Sorry, but only post owners or moderators convert posts'
+ )
+ raise django_exceptions.PermissionDenied(error_message)
+
def user_can_post_comment(self, parent_post = None):
"""a simplified method to test ability to comment
"""
- return True
- """
- #commented out to disable the min rep
- if self.reputation >= askbot_settings.MIN_REP_TO_LEAVE_COMMENTS:
- return True
- if parent_post and self == parent_post.author:
- return True
if self.is_administrator_or_moderator():
return True
- return False
- """
+ elif self.is_suspended():
+ if parent_post and self == parent_post.author:
+ return True
+ else:
+ return False
+ elif self.is_blocked():
+ return False
+ return True
def user_assert_can_post_comment(self, parent_post = None):
"""raises exceptions.PermissionDenied if
@@ -851,52 +888,35 @@ def user_assert_can_post_comment(self, parent_post = None):
the reason will be in text of exception
"""
+ _assert_user_can(
+ user=self,
+ post=parent_post,
+ action_display=_('post comments'),
+ owner_can=True,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
+ )
- suspended_error_message = _(
- 'Sorry, since your account is suspended '
- 'you can comment only your own posts'
- )
- low_rep_error_message = _(
- 'Sorry, to comment any post a minimum reputation of '
- '%(min_rep)s points is required. You can still comment '
- 'your own posts and answers to your questions'
- ) % {'min_rep': 0}#askbot_settings.MIN_REP_TO_LEAVE_COMMENTS}
-
- blocked_message = get_i18n_message('BLOCKED_USERS_CANNOT_POST')
-
- try:
- _assert_user_can(
- user = self,
- post = parent_post,
- owner_can = True,
- blocked_error_message = blocked_message,
- suspended_error_message = suspended_error_message,
- min_rep_setting = 0,#askbot_settings.MIN_REP_TO_LEAVE_COMMENTS,
- low_rep_error_message = low_rep_error_message,
- )
- except askbot_exceptions.InsufficientReputation, e:
- if parent_post.post_type == 'answer':
- if self == parent_post.thread._question_post().author:
- return
- raise e
-
-def user_assert_can_see_deleted_post(self, post = None):
+def user_assert_can_see_deleted_post(self, post=None):
"""attn: this assertion is independently coded in
Question.get_answers call
"""
+ try:
+ _assert_user_can(
+ user=self,
+ post=post,
+ admin_or_moderator_required=True,
+ owner_can=True
+ )
+ except django_exceptions.PermissionDenied, e:
+ #re-raise the same exception with a different message
+ error_message = _(
+ 'This post has been deleted and can be seen only '
+ 'by post owners, site administrators and moderators'
+ )
+ raise django_exceptions.PermissionDenied(error_message)
- error_message = _(
- 'This post has been deleted and can be seen only '
- 'by post owners, site administrators and moderators'
- )
- _assert_user_can(
- user = self,
- post = post,
- admin_or_moderator_required = True,
- owner_can = True,
- general_error_message = error_message
- )
def user_assert_can_edit_deleted_post(self, post = None):
assert(post.deleted == True)
@@ -904,9 +924,9 @@ def user_assert_can_edit_deleted_post(self, post = None):
self.assert_can_see_deleted_post(post)
except django_exceptions.PermissionDenied, e:
error_message = _(
- 'Sorry, only moderators, site administrators '
- 'and post owners can edit deleted posts'
- )
+ 'Sorry, only moderators, site administrators '
+ 'and post owners can edit deleted posts'
+ )
raise django_exceptions.PermissionDenied(error_message)
def user_assert_can_edit_post(self, post = None):
@@ -919,37 +939,21 @@ def user_assert_can_edit_post(self, post = None):
return
- blocked_error_message = _(
- 'Sorry, since your account is blocked '
- 'you cannot edit posts'
- )
- suspended_error_message = _(
- 'Sorry, since your account is suspended '
- 'you can edit only your own posts'
- )
if post.wiki == True:
- low_rep_error_message = _(
- 'Sorry, to edit wiki posts, a minimum '
- 'reputation of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_EDIT_WIKI}
+ action_display=_('edit wiki posts')
min_rep_setting = askbot_settings.MIN_REP_TO_EDIT_WIKI
else:
- low_rep_error_message = _(
- 'Sorry, to edit other people\'s posts, a minimum '
- 'reputation of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_EDIT_OTHERS_POSTS}
+ action_display=_('edit posts')
min_rep_setting = askbot_settings.MIN_REP_TO_EDIT_OTHERS_POSTS
_assert_user_can(
- user = self,
- post = post,
- owner_can = True,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
- min_rep_setting = min_rep_setting,
+ user=self,
+ post=post,
+ action_display=action_display,
+ owner_can=True,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
+ min_rep_setting = min_rep_setting
)
@@ -999,12 +1003,16 @@ def user_assert_can_delete_question(self, question = None):
return
else:
msg = ungettext(
- 'Sorry, cannot delete your question since it '
- 'has an upvoted answer posted by someone else',
- 'Sorry, cannot delete your question since it '
- 'has some upvoted answers posted by other users',
+ 'Sorry, cannot %(delete_your_question)s since it '
+ 'has an %(upvoted_answer)s posted by someone else',
+ 'Sorry, cannot %(delete_your_question)s since it '
+ 'has some %(upvoted_answers)s posted by other users',
answer_count
- )
+ ) % {
+ 'delete_your_question': askbot_settings.WORDS_DELETE_YOUR_QUESTION,
+ 'upvoted_answer': askbot_settings.WORDS_UPVOTED_ANSWER,
+ 'upvoted_answers': askbot_settings.WORDS_UPVOTED_ANSWERS
+ }
raise django_exceptions.PermissionDenied(msg)
@@ -1013,111 +1021,44 @@ def user_assert_can_delete_answer(self, answer = None):
instead of "answer", because this logic also applies to
assert on deleting question (in addition to some special rules)
"""
- blocked_error_message = _(
- 'Sorry, since your account is blocked '
- 'you cannot delete posts'
- )
- suspended_error_message = _(
- 'Sorry, since your account is suspended '
- 'you can delete only your own posts'
- )
- low_rep_error_message = _(
- 'Sorry, to delete other people\'s posts, a minimum '
- 'reputation of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_DELETE_OTHERS_POSTS}
min_rep_setting = askbot_settings.MIN_REP_TO_DELETE_OTHERS_POSTS
-
-
_assert_user_can(
- user = self,
- post = answer,
- owner_can = True,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
- min_rep_setting = min_rep_setting,
+ user=self,
+ post=answer,
+ action_display=_('delete posts'),
+ owner_can=True,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
+ min_rep_setting=min_rep_setting,
)
def user_assert_can_close_question(self, question = None):
assert(getattr(question, 'post_type', '') == 'question')
- blocked_error_message = _(
- 'Sorry, since your account is blocked '
- 'you cannot close questions'
- )
- suspended_error_message = _(
- 'Sorry, since your account is suspended '
- 'you cannot close questions'
- )
- low_rep_error_message = _(
- 'Sorry, to close other people\' posts, a minimum '
- 'reputation of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS}
min_rep_setting = askbot_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS
-
- owner_min_rep_setting = askbot_settings.MIN_REP_TO_CLOSE_OWN_QUESTIONS
-
- owner_low_rep_error_message = _(
- 'Sorry, to close own question '
- 'a minimum reputation of %(min_rep)s is required'
- ) % {'min_rep': owner_min_rep_setting}
-
-
_assert_user_can(
user = self,
post = question,
+ action_display=askbot_settings.WORDS_CLOSE_QUESTIONS,
owner_can = True,
suspended_owner_cannot = True,
- owner_min_rep_setting = owner_min_rep_setting,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
- owner_low_rep_error_message = owner_low_rep_error_message,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
min_rep_setting = min_rep_setting,
)
def user_assert_can_reopen_question(self, question = None):
assert(question.post_type == 'question')
-
- #for some reason rep to reopen own questions != rep to close own q's
- owner_min_rep_setting = askbot_settings.MIN_REP_TO_REOPEN_OWN_QUESTIONS
- min_rep_setting = askbot_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS
-
- general_error_message = _(
- 'Sorry, only administrators, moderators '
- 'or post owners with reputation > %(min_rep)s '
- 'can reopen questions.'
- ) % {'min_rep': owner_min_rep_setting }
-
- owner_low_rep_error_message = _(
- 'Sorry, to reopen own question '
- 'a minimum reputation of %(min_rep)s is required'
- ) % {'min_rep': owner_min_rep_setting}
-
- blocked_error_message = _(
- 'Sorry, you cannot reopen questions '
- 'because your account is blocked'
- )
-
- suspended_error_message = _(
- 'Sorry, you cannot reopen questions '
- 'because your account is suspended'
- )
-
_assert_user_can(
- user = self,
- post = question,
- owner_can = True,
- suspended_owner_cannot = True,
- owner_min_rep_setting = owner_min_rep_setting,
- min_rep_setting = min_rep_setting,
- owner_low_rep_error_message = owner_low_rep_error_message,
- general_error_message = general_error_message,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message
+ user=self,
+ post=question,
+ action_display=_('reopen questions'),
+ suspended_owner_cannot=True,
+ #for some reason rep to reopen own questions != rep to close own q's
+ min_rep_setting=askbot_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
)
@@ -1126,37 +1067,21 @@ def user_assert_can_flag_offensive(self, post = None):
assert(post is not None)
double_flagging_error_message = _(
- 'You have flagged this question before and '
+ 'You have flagged this post before and '
'cannot do it more than once'
)
if self.get_flags_for_post(post).count() > 0:
raise askbot_exceptions.DuplicateCommand(double_flagging_error_message)
- blocked_error_message = _(
- 'Sorry, since your account is blocked '
- 'you cannot flag posts as offensive'
- )
-
- suspended_error_message = _(
- 'Sorry, your account appears to be suspended and you cannot make new posts '
- 'until this issue is resolved. You can, however edit your existing posts. '
- 'Please contact the forum administrator to reach a resolution.'
- )
-
- low_rep_error_message = _(
- 'Sorry, to flag posts as offensive a minimum reputation '
- 'of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_FLAG_OFFENSIVE}
min_rep_setting = askbot_settings.MIN_REP_TO_FLAG_OFFENSIVE
_assert_user_can(
user = self,
post = post,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
+ action_display=_('flag posts as offensive'),
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
min_rep_setting = min_rep_setting
)
#one extra assertion
@@ -1183,28 +1108,13 @@ def user_assert_can_remove_flag_offensive(self, post = None):
if self.get_flags_for_post(post).count() < 1:
raise django_exceptions.PermissionDenied(non_existing_flagging_error_message)
- blocked_error_message = _(
- 'Sorry, since your account is blocked you cannot remove flags'
- )
-
- suspended_error_message = _(
- 'Sorry, your account appears to be suspended and you cannot remove flags. '
- 'Please contact the forum administrator to reach a resolution.'
- )
-
min_rep_setting = askbot_settings.MIN_REP_TO_FLAG_OFFENSIVE
- low_rep_error_message = ungettext(
- 'Sorry, to flag posts a minimum reputation of %(min_rep)d is required',
- 'Sorry, to flag posts a minimum reputation of %(min_rep)d is required',
- min_rep_setting
- ) % {'min_rep': min_rep_setting}
-
_assert_user_can(
user = self,
post = post,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
+ action_display=_('remove flags'),
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
min_rep_setting = min_rep_setting
)
#one extra assertion
@@ -1234,67 +1144,30 @@ def user_assert_can_remove_all_flags_offensive(self, post = None):
def user_assert_can_retag_question(self, question = None):
if question.deleted == True:
- try:
- self.assert_can_edit_deleted_post(question)
- except django_exceptions.PermissionDenied:
- error_message = _(
- 'Sorry, only question owners, '
- 'site administrators and moderators '
- 'can retag deleted questions'
- )
- raise django_exceptions.PermissionDenied(error_message)
-
-
- blocked_error_message = _(
- 'Sorry, since your account is blocked '
- 'you cannot retag questions'
- )
- suspended_error_message = _(
- 'Sorry, since your account is suspended '
- 'you can retag only your own questions'
- )
- low_rep_error_message = _(
- 'Sorry, to retag questions a minimum '
- 'reputation of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_RETAG_OTHERS_QUESTIONS}
- min_rep_setting = askbot_settings.MIN_REP_TO_RETAG_OTHERS_QUESTIONS
+ self.assert_can_edit_deleted_post(question)
_assert_user_can(
- user = self,
- post = question,
- owner_can = True,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
- min_rep_setting = min_rep_setting,
+ user=self,
+ post=question,
+ action_display=askbot_settings.WORDS_RETAG_QUESTIONS,
+ owner_can=True,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
+ min_rep_setting=askbot_settings.MIN_REP_TO_RETAG_OTHERS_QUESTIONS
)
def user_assert_can_delete_comment(self, comment = None):
- blocked_error_message = _(
- 'Sorry, since your account is blocked '
- 'you cannot delete comment'
- )
- suspended_error_message = _(
- 'Sorry, since your account is suspended '
- 'you can delete only your own comments'
- )
- low_rep_error_message = _(
- 'Sorry, to delete comments '
- 'reputation of %(min_rep)s is required'
- ) % \
- {'min_rep': askbot_settings.MIN_REP_TO_DELETE_OTHERS_COMMENTS}
min_rep_setting = askbot_settings.MIN_REP_TO_DELETE_OTHERS_COMMENTS
_assert_user_can(
user = self,
post = comment,
+ action_display=_('delete comments'),
owner_can = True,
- blocked_error_message = blocked_error_message,
- suspended_error_message = suspended_error_message,
- low_rep_error_message = low_rep_error_message,
+ blocked_user_cannot=True,
+ suspended_user_cannot=True,
min_rep_setting = min_rep_setting,
)
@@ -1402,12 +1275,15 @@ def user_post_anonymous_askbot_content(user, session_key):
aa.save()
#maybe add pending posts message?
else:
- if user.is_blocked():
- msg = get_i18n_message('BLOCKED_USERS_CANNOT_POST')
- user.message_set.create(message = msg)
- elif user.is_suspended():
- msg = get_i18n_message('SUSPENDED_USERS_CANNOT_POST')
- user.message_set.create(message = msg)
+ if user.is_blocked() or user.is_suspended():
+ if user.is_blocked():
+ account_status = _('your account is blocked')
+ elif user.is_suspended():
+ account_status = _('your account is suspended')
+ user.message_set.create(message = _(message_keys.ACCOUNT_CANNOT_PERFORM_ACTION) % {
+ 'perform_action': _('make posts'),
+ 'your_account_is': account_status
+ })
else:
for aq in aq_list:
aq.publish(user)
@@ -1446,20 +1322,27 @@ def user_mark_tags(
tagnames = list()
#figure out which tags don't yet exist
+ language_code = get_language()
existing_tagnames = Tag.objects.filter(
- name__in=tagnames
+ name__in=tagnames,
+ language_code=language_code
).values_list(
'name', flat=True
)
non_existing_tagnames = set(tagnames) - set(existing_tagnames)
#create those tags, and if tags are moderated make them suggested
if (len(non_existing_tagnames) > 0):
- Tag.objects.create_in_bulk(tag_names=tagnames, user=self)
+ Tag.objects.create_in_bulk(
+ tag_names=tagnames,
+ user=self,
+ language_code=language_code
+ )
#below we update normal tag selections
marked_ts = MarkedTag.objects.filter(
user = self,
- tag__name__in = tagnames
+ tag__name__in=tagnames,
+ tag__language_code=language_code
)
#Marks for "good" and "bad" reasons are exclusive,
#to make it impossible to "like" and "dislike" something at the same time
@@ -1481,7 +1364,10 @@ def user_mark_tags(
marked_names = marked_ts.values_list('tag__name', flat = True)
if len(marked_names) < len(tagnames):
unmarked_names = set(tagnames).difference(set(marked_names))
- ts = Tag.objects.filter(name__in = unmarked_names)
+ ts = Tag.objects.filter(
+ name__in=unmarked_names,
+ language_code=language_code
+ )
new_marks = list()
for tag in ts:
MarkedTag(
@@ -1522,6 +1408,34 @@ def user_retag_question(
timestamp = timestamp
)
+
+def user_repost_comment_as_answer(self, comment):
+ """converts comment to answer under the
+ parent question"""
+
+ #todo: add assertion
+ self.assert_can_convert_post(comment)
+
+ comment.post_type = 'answer'
+ old_parent = comment.parent
+
+ comment.parent = comment.thread._question_post()
+ comment.save()
+
+ comment.thread.update_answer_count()
+
+ comment.parent.comment_count += 1
+ comment.parent.save()
+
+ #to avoid db constraint error
+ if old_parent.comment_count >= 1:
+ old_parent.comment_count -= 1
+ else:
+ old_parent.comment_count = 0
+
+ old_parent.save()
+ comment.thread.invalidate_cached_data()
+
@auto_now_timestamp
def user_accept_best_answer(
self, answer = None,
@@ -1697,6 +1611,9 @@ def user_restore_post(
post.deleted_by = None
post.deleted_at = None
post.save()
+ if post.post_type == 'question':
+ post.thread.deleted = False
+ post.thread.save()
post.thread.invalidate_cached_data()
if post.post_type == 'answer':
post.thread.update_answer_count()
@@ -2009,9 +1926,13 @@ def user_post_answer(
left = ungettext('in %(min)d min','in %(min)d mins',minutes) % {'min':minutes}
day = ungettext('%(days)d day','%(days)d days',askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION) % {'days':askbot_settings.MIN_DAYS_TO_ANSWER_OWN_QUESTION}
error_message = _(
- 'New users must wait %(days)s before answering their own question. '
+ 'New users must wait %(days)s to %(answer_own_questions)s. '
' You can post an answer %(left)s'
- ) % {'days': day,'left': left}
+ ) % {
+ 'days': day,
+ 'left': left,
+ 'answer_own_questions': askbot_settings.WORDS_ANSWER_OWN_QUESTIONS
+ }
assert(error_message is not None)
raise django_exceptions.PermissionDenied(error_message)
@@ -2362,11 +2283,14 @@ def user_get_tag_filtered_questions(self, questions = None):
if questions is None:
questions = Post.objects.get_questions()
+ language_code = get_language()
+
if self.email_tag_filter_strategy == const.EXCLUDE_IGNORED:
ignored_tags = Tag.objects.filter(
user_selections__reason = 'bad',
- user_selections__user = self
+ user_selections__user = self,
+ language_code=language_code
)
wk = self.ignored_tags.strip().split()
@@ -2387,7 +2311,8 @@ def user_get_tag_filtered_questions(self, questions = None):
selected_tags = Tag.objects.filter(
user_selections__reason = reason,
- user_selections__user = self
+ user_selections__user = self,
+ language_code=language_code
)
selected_by_wildcards = Tag.objects.get_by_wildcards(wk)
@@ -2454,11 +2379,12 @@ def user_get_primary_group(self):
first non-personal non-everyone group
works only for one real private group per-person
"""
- groups = self.get_groups(private=True)
- for group in groups:
- if group.is_personal():
- continue
- return group
+ if askbot_settings.GROUPS_ENABLED:
+ groups = self.get_groups(private=True)
+ for group in groups:
+ if group.is_personal():
+ continue
+ return group
return None
def user_can_make_group_private_posts(self):
@@ -2710,7 +2636,7 @@ def user_is_following_question(user, question):
return question.thread.followed_by.filter(id=user.id).exists()
-def upvote(self, post, timestamp=None, cancel=False, force = False):
+def upvote(self, post, timestamp=None, cancel=False, force=False):
#force parameter not used yet
return _process_vote(
self,
@@ -2720,7 +2646,7 @@ def upvote(self, post, timestamp=None, cancel=False, force = False):
vote_type=Vote.VOTE_UP
)
-def downvote(self, post, timestamp=None, cancel=False, force = False):
+def downvote(self, post, timestamp=None, cancel=False, force=False):
#force not used yet
return _process_vote(
self,
@@ -2834,11 +2760,13 @@ def user_update_response_counts(user):
def user_receive_reputation(self, num_points):
- new_points = self.reputation + num_points
+ old_points = self.reputation
+ new_points = old_points + num_points
if new_points > 0:
self.reputation = new_points
else:
self.reputation = const.MIN_REPUTATION
+ signals.reputation_received.send(None, user=self, reputation_before=old_points)
def user_update_wildcard_tag_selections(
self,
@@ -2963,6 +2891,10 @@ User.add_to_class(
user_get_followed_question_alert_frequency
)
User.add_to_class(
+ 'get_top_answers_paginator',
+ user_get_top_answers_paginator
+)
+User.add_to_class(
'subscribe_for_followed_question_alerts',
user_subscribe_for_followed_question_alerts
)
@@ -2988,6 +2920,7 @@ User.add_to_class('update_avatar_type', user_update_avatar_type)
User.add_to_class('post_question', user_post_question)
User.add_to_class('edit_question', user_edit_question)
User.add_to_class('retag_question', user_retag_question)
+User.add_to_class('repost_comment_as_answer', user_repost_comment_as_answer)
User.add_to_class('post_answer', user_post_answer)
User.add_to_class('edit_answer', user_edit_answer)
User.add_to_class('edit_post', user_edit_post)
@@ -3048,6 +2981,7 @@ User.add_to_class('has_interesting_wildcard_tags', user_has_interesting_wildcard
User.add_to_class('has_ignored_wildcard_tags', user_has_ignored_wildcard_tags)
User.add_to_class('can_moderate_user', user_can_moderate_user)
User.add_to_class('has_affinity_to_question', user_has_affinity_to_question)
+User.add_to_class('has_badge', user_has_badge)
User.add_to_class('moderate_user_reputation', user_moderate_user_reputation)
User.add_to_class('set_status', user_set_status)
User.add_to_class('get_badge_summary', user_get_badge_summary)
@@ -3095,6 +3029,8 @@ User.add_to_class('assert_can_delete_post', user_assert_can_delete_post)
User.add_to_class('assert_can_restore_post', user_assert_can_restore_post)
User.add_to_class('assert_can_delete_comment', user_assert_can_delete_comment)
User.add_to_class('assert_can_edit_comment', user_assert_can_edit_comment)
+User.add_to_class('assert_can_convert_post', user_assert_can_convert_post)
+
User.add_to_class('assert_can_delete_answer', user_assert_can_delete_answer)
User.add_to_class('assert_can_delete_question', user_assert_can_delete_question)
User.add_to_class('assert_can_accept_best_answer', user_assert_can_accept_best_answer)
@@ -3160,13 +3096,13 @@ def format_instant_notification_email(
)
#todo: remove hardcoded style
else:
- content_preview = post.format_for_email(is_leaf_post = True)
+ content_preview = post.format_for_email(is_leaf_post=True, recipient=to_user)
#add indented summaries for the parent posts
- content_preview += post.format_for_email_as_parent_thread_summary()
+ content_preview += post.format_for_email_as_parent_thread_summary(recipient=to_user)
#content_preview += '<p>======= Full thread summary =======</p>'
- #content_preview += post.thread.format_for_email(user=to_user)
+ #content_preview += post.thread.format_for_email(recipient=to_user)
if update_type == 'post_shared':
user_action = _('%(user)s shared a %(post_link)s.')
@@ -3190,10 +3126,22 @@ def format_instant_notification_email(
post_url = site_url(post.get_absolute_url())
user_url = site_url(from_user.get_absolute_url())
+
+ if to_user.is_administrator_or_moderator() and askbot_settings.SHOW_ADMINS_PRIVATE_USER_DATA:
+ user_link_fmt = '<a href="%(profile_url)s">%(username)s</a> (<a href="mailto:%(email)s">%(email)s</a>)'
+ user_link = user_link_fmt % {
+ 'profile_url': user_url,
+ 'username': from_user.username,
+ 'email': from_user.email
+ }
+ elif post.is_anonymous:
+ user_link = from_user.get_name_of_anonymous_user()
+ else:
+ user_link = '<a href="%s">%s</a>' % (user_url, from_user.username)
+
user_action = user_action % {
- 'user': '<a href="%s">%s</a>' % (user_url, from_user.username),
+ 'user': user_link,
'post_link': '<a href="%s">%s</a>' % (post_url, _(post.post_type))
- #'post_link': '%s <a href="%s">>>></a>' % (_(post.post_type), post_url)
}
can_reply = to_user.can_post_by_email()
@@ -3660,6 +3608,10 @@ def greet_new_user(user, **kwargs):
if askbot_settings.NEW_USER_GREETING:
user.message_set.create(message = askbot_settings.NEW_USER_GREETING)
+ import sys
+ if 'test' in sys.argv:
+ return
+
if askbot_settings.REPLY_BY_EMAIL:#with this on we also collect signature
template_name = 'email/welcome_lamson_on.html'
else:
@@ -3737,22 +3689,16 @@ def set_user_avatar_type_flag(instance, created, **kwargs):
def update_user_avatar_type_flag(instance, **kwargs):
instance.user.update_avatar_type()
-def make_admin_if_first_user(instance, **kwargs):
+def make_admin_if_first_user(user, **kwargs):
"""first user automatically becomes an administrator
the function is run only once in the interpreter session
+
+ function is run when user registers
"""
import sys
- #have to check sys.argv to satisfy the test runner
- #which fails with the cache-based skipping
- #for real the setUp() code in the base test case must
- #clear the cache!!!
- if 'test' not in sys.argv and cache.cache.get('admin-created'):
- #no need to hit the database every time!
- return
user_count = User.objects.all().count()
- if user_count == 0:
- instance.set_admin_status()
- cache.cache.set('admin-created', True)
+ if user_count == 1:
+ user.set_status('d')
def moderate_group_joining(sender, instance=None, created=False, **kwargs):
if created and instance.level == GroupMembership.PENDING:
@@ -3770,8 +3716,21 @@ def tweet_new_post(sender, user=None, question=None, answer=None, form_data=None
post = question or answer
tweet_new_post_task.delay(post.id)
+def autoapprove_reputable_user(user=None, reputation_before=None, *args, **kwargs):
+ """if user is 'watched' we change status to 'approved'
+ if user's rep crossed the auto-approval margin"""
+ margin = askbot_settings.MIN_REP_TO_AUTOAPPROVE_USER
+ if user.is_watched() and reputation_before < margin and user.reputation >= margin:
+ user.set_status('a')
+
+def init_badge_data(sender, created_models=None, **kwargs):
+ if BadgeData in created_models:
+ from askbot.models import badges
+ badges.init_badges()
+
+django_signals.post_syncdb.connect(init_badge_data)
+
#signal for User model save changes
-django_signals.pre_save.connect(make_admin_if_first_user, sender=User)
django_signals.pre_save.connect(calculate_gravatar_hash, sender=User)
django_signals.post_save.connect(add_missing_subscriptions, sender=User)
django_signals.post_save.connect(add_user_to_global_group, sender=User)
@@ -3786,7 +3745,7 @@ django_signals.post_save.connect(moderate_group_joining, sender=GroupMembership)
if 'avatar' in django_settings.INSTALLED_APPS:
from avatar.models import Avatar
- django_signals.post_save.connect(set_user_avatar_type_flag,sender=Avatar)
+ django_signals.post_save.connect(set_user_avatar_type_flag, sender=Avatar)
django_signals.post_delete.connect(update_user_avatar_type_flag, sender=Avatar)
django_signals.post_delete.connect(record_cancel_vote, sender=Vote)
@@ -3797,12 +3756,14 @@ signals.flag_offensive.connect(record_flag_offensive, sender=Post)
signals.remove_flag_offensive.connect(remove_flag_offensive, sender=Post)
signals.tags_updated.connect(record_update_tags)
signals.user_registered.connect(greet_new_user)
+signals.user_registered.connect(make_admin_if_first_user)
signals.user_updated.connect(record_user_full_updated, sender=User)
signals.user_logged_in.connect(complete_pending_tag_subscriptions)#todo: add this to fake onlogin middleware
signals.user_logged_in.connect(post_anonymous_askbot_content)
signals.post_updated.connect(record_post_update_activity)
signals.new_answer_posted.connect(tweet_new_post)
signals.new_question_posted.connect(tweet_new_post)
+signals.reputation_received.connect(autoapprove_reputable_user)
#probably we cannot use post-save here the point of this is
#to tell when the revision becomes publicly visible, not when it is saved
@@ -3845,6 +3806,9 @@ __all__ = [
'User',
'ReplyAddress',
+
+ 'ImportRun',
+ 'ImportedObjectInfo',
'get_model',
]
diff --git a/askbot/models/badges.py b/askbot/models/badges.py
index 244c8e2f..44eda0c3 100644
--- a/askbot/models/badges.py
+++ b/askbot/models/badges.py
@@ -21,10 +21,8 @@ import datetime
from django.template.defaultfilters import slugify
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext as _
+from django.utils.translation import ungettext
from django.dispatch import Signal
-from askbot.models.repute import BadgeData, Award
-from askbot.models.user import Activity
-from askbot.models.question import FavoriteQuestion as Fave#name collision
from askbot.models.post import Post
from askbot import const
from askbot.conf import settings as askbot_settings
@@ -57,6 +55,7 @@ class Badge(object):
self.css_class = const.BADGE_CSS_CLASSES[self.level]
def get_stored_data(self):
+ from askbot.models.repute import BadgeData
data, created = BadgeData.objects.get_or_create(slug = self.key)
return data
@@ -86,7 +85,7 @@ class Badge(object):
def award(self, recipient = None, context_object = None, timestamp = None):
"""do award, the recipient was proven to deserve"""
-
+ from askbot.models.repute import Award
if self.multiple == False:
if recipient.badges.filter(slug = self.key).count() != 0:
return False
@@ -171,8 +170,11 @@ class PeerPressure(Badge):
class Teacher(Badge):
def __init__(self):
description = _(
- 'Received at least %(votes)s upvote for an answer for the first time'
- ) % {'votes': askbot_settings.TEACHER_BADGE_MIN_UPVOTES}
+ 'Gave an %(answer_voted_up)s at least %(votes)s times for the first time'
+ ) % {
+ 'votes': askbot_settings.TEACHER_BADGE_MIN_UPVOTES,
+ 'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
+ }
super(Teacher, self).__init__(
key = 'teacher',
name = _('Teacher'),
@@ -186,7 +188,7 @@ class Teacher(Badge):
if context_object.post_type != 'answer':
return False
- if context_object.points>= askbot_settings.TEACHER_BADGE_MIN_UPVOTES:
+ if context_object.points >= askbot_settings.TEACHER_BADGE_MIN_UPVOTES:
return self.award(context_object.author, context_object, timestamp)
return False
@@ -249,14 +251,16 @@ class CivicDuty(Badge):
class SelfLearner(Badge):
def __init__(self):
- description = _('Answered own question with at least %(num)s up votes')
- min_votes = askbot_settings.SELF_LEARNER_BADGE_MIN_UPVOTES
+ description = _('%(answered_own_question)s with at least %(num)s up votes') % {
+ 'num': askbot_settings.SELF_LEARNER_BADGE_MIN_UPVOTES,
+ 'answered_own_question': askbot_settings.WORDS_ANSWERED_OWN_QUESTION
+ }
super(SelfLearner, self).__init__(
- key = 'self-learner',
- name = _('Self-Learner'),
- description = description % {'num': min_votes},
- level = const.BRONZE_BADGE,
- multiple = True
+ key='self-learner',
+ name=_('Self-Learner'),
+ description=description,
+ level=const.BRONZE_BADGE,
+ multiple=True
)
def consider_award(self, actor = None,
@@ -301,72 +305,90 @@ class QualityPost(Badge):
class NiceAnswer(QualityPost):
def __new__(cls):
self = super(NiceAnswer, cls).__new__(cls)
- self.name = _('Nice Answer')
+ self.name = askbot_settings.WORDS_NICE_ANSWER
self.key = 'nice-answer'
self.level = const.BRONZE_BADGE
self.multiple = True
self.min_votes = askbot_settings.NICE_ANSWER_BADGE_MIN_UPVOTES
- self.description = _('Answer voted up %(num)s times') % {'num': self.min_votes}
+ self.description = _('%(answer_voted_up)s %(num)s times') % {
+ 'num': self.min_votes,
+ 'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
+ }
self.post_type = 'answer'
return self
class GoodAnswer(QualityPost):
def __new__(cls):
self = super(GoodAnswer, cls).__new__(cls)
- self.name = _('Good Answer')
+ self.name = askbot_settings.WORDS_GOOD_ANSWER
self.key = 'good-answer'
self.level = const.SILVER_BADGE
self.multiple = True
self.min_votes = askbot_settings.GOOD_ANSWER_BADGE_MIN_UPVOTES
- self.description = _('Answer voted up %(num)s times') % {'num': self.min_votes}
+ self.description = _('%(answer_voted_up)s %(num)s times') % {
+ 'num': self.min_votes,
+ 'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
+ }
self.post_type = 'answer'
return self
class GreatAnswer(QualityPost):
def __new__(cls):
self = super(GreatAnswer, cls).__new__(cls)
- self.name = _('Great Answer')
+ self.name = askbot_settings.WORDS_GREAT_ANSWER
self.key = 'great-answer'
self.level = const.GOLD_BADGE
self.multiple = True
self.min_votes = askbot_settings.GREAT_ANSWER_BADGE_MIN_UPVOTES
- self.description = _('Answer voted up %(num)s times') % {'num': self.min_votes}
+ self.description = _('%(answer_voted_up)s %(num)s times') % {
+ 'num': self.min_votes,
+ 'answer_voted_up': askbot_settings.WORDS_ANSWER_VOTED_UP
+ }
self.post_type = 'answer'
return self
class NiceQuestion(QualityPost):
def __new__(cls):
self = super(NiceQuestion, cls).__new__(cls)
- self.name = _('Nice Question')
+ self.name = askbot_settings.WORDS_NICE_QUESTION
self.key = 'nice-question'
self.level = const.BRONZE_BADGE
self.multiple = True
self.min_votes = askbot_settings.NICE_QUESTION_BADGE_MIN_UPVOTES
- self.description = _('Question voted up %(num)s times') % {'num': self.min_votes}
+ self.description = _('%(question_voted_up)s up %(num)s times') % {
+ 'num': self.min_votes,
+ 'question_voted_up': askbot_settings.WORDS_QUESTION_VOTED_UP
+ }
self.post_type = 'question'
return self
class GoodQuestion(QualityPost):
def __new__(cls):
self = super(GoodQuestion, cls).__new__(cls)
- self.name = _('Good Question')
+ self.name = askbot_settings.WORDS_GOOD_QUESTION
self.key = 'good-question'
self.level = const.SILVER_BADGE
self.multiple = True
self.min_votes = askbot_settings.GOOD_QUESTION_BADGE_MIN_UPVOTES
- self.description = _('Question voted up %(num)s times') % {'num': self.min_votes}
+ self.description = _('%(question_voted_up)s up %(num)s times') % {
+ 'num': self.min_votes,
+ 'question_voted_up': askbot_settings.WORDS_QUESTION_VOTED_UP
+ }
self.post_type = 'question'
return self
class GreatQuestion(QualityPost):
def __new__(cls):
self = super(GreatQuestion, cls).__new__(cls)
- self.name = _('Great Question')
+ self.name = askbot_settings.WORDS_GREAT_QUESTION
self.key = 'great-question'
self.level = const.GOLD_BADGE
self.multiple = True
self.min_votes = askbot_settings.GREAT_QUESTION_BADGE_MIN_UPVOTES
- self.description = _('Question voted up %(num)s times') % {'num': self.min_votes}
+ self.description = _('%(question_voted_up)s %(num)s times') % {
+ 'num': self.min_votes,
+ 'question_voted_up': askbot_settings.WORDS_QUESTION_VOTED_UP
+ }
self.post_type = 'question'
return self
@@ -378,7 +400,9 @@ class Student(QualityPost):
self.level = const.BRONZE_BADGE
self.multiple = False
self.min_votes = 1
- self.description = _('Asked first question with at least one up vote')
+ self.description = _('%(asked_first_question)s with at least one up vote') % {
+ 'asked_first_question': askbot_settings.WORDS_ASKED_FIRST_QUESTION
+ }
self.post_type = 'question'
return self
@@ -411,35 +435,41 @@ class FrequentedQuestion(Badge):
class PopularQuestion(FrequentedQuestion):
def __new__(cls):
self = super(PopularQuestion, cls).__new__(cls)
- self.name = _('Popular Question')
+ self.name = askbot_settings.WORDS_POPULAR_QUESTION
self.key = 'popular-question'
self.level = const.BRONZE_BADGE
self.min_views = askbot_settings.POPULAR_QUESTION_BADGE_MIN_VIEWS
- self.description = _('Asked a question with %(views)s views') \
- % {'views' : self.min_views}
+ self.description = _('%(asked_a_question)s with %(views)s views') % {
+ 'views' : self.min_views,
+ 'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
+ }
return self
class NotableQuestion(FrequentedQuestion):
def __new__(cls):
self = super(NotableQuestion, cls).__new__(cls)
- self.name = _('Notable Question')
+ self.name = askbot_settings.WORDS_NOTABLE_QUESTION
self.key = 'notable-question'
self.level = const.SILVER_BADGE
self.min_views = askbot_settings.NOTABLE_QUESTION_BADGE_MIN_VIEWS
- self.description = _('Asked a question with %(views)s views') \
- % {'views' : self.min_views}
+ self.description = _('%(asked_a_question)s with %(views)s views') % {
+ 'views' : self.min_views,
+ 'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
+ }
return self
class FamousQuestion(FrequentedQuestion):
def __new__(cls):
self = super(FamousQuestion, cls).__new__(cls)
- self.name = _('Famous Question')
+ self.name = askbot_settings.WORDS_FAMOUS_QUESTION
self.key = 'famous-question'
self.level = const.GOLD_BADGE
self.multiple = True
self.min_views = askbot_settings.FAMOUS_QUESTION_BADGE_MIN_VIEWS
- self.description = _('Asked a question with %(views)s views') \
- % {'views' : self.min_views}
+ self.description = _('%(asked_a_question)s with %(views)s views') % {
+ 'views' : self.min_views,
+ 'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
+ }
return self
class Scholar(Badge):
@@ -447,7 +477,10 @@ class Scholar(Badge):
he/she accepts an answer for the first time
"""
def __init__(self):
- description = _('Asked a question and accepted an answer')
+ description = _('%(asked_a_question)s and %(accepted_an_answer)s') % {
+ 'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION,
+ 'accepted_an_answer': askbot_settings.WORDS_ACCEPTED_AN_ANSWER
+ }
super(Scholar, self).__init__(
key = 'scholar',
name = _('Scholar'),
@@ -496,8 +529,11 @@ class Enlightened(VotedAcceptedAnswer):
self.level = const.SILVER_BADGE
self.multiple = False
self.min_votes = askbot_settings.ENLIGHTENED_BADGE_MIN_UPVOTES
- descr = _('First answer was accepted with %(num)s or more votes')
- self.description = descr % {'num': self.min_votes}
+ descr = _('%(gave_accepted_answer)s upvoted %(num)s or more times')
+ self.description = descr % {
+ 'num': self.min_votes,
+ 'gave_accepted_answer': askbot_settings.WORDS_GAVE_ACCEPTED_ANSWER
+ }
return self
class Guru(VotedAcceptedAnswer):
@@ -507,24 +543,31 @@ class Guru(VotedAcceptedAnswer):
self.name = _('Guru')
self.level = const.GOLD_BADGE
self.multiple = True
- descr = _('Answer accepted with %(num)s or more votes')
self.min_votes = askbot_settings.GURU_BADGE_MIN_UPVOTES
- self.description = descr % {'num': self.min_votes}
+ descr = _('%(gave_accepted_answer)s upvoted %(num)s or more times')
+ self.description = descr % {
+ 'num': self.min_votes,
+ 'gave_accepted_answer': askbot_settings.WORDS_GAVE_ACCEPTED_ANSWER
+ }
return self
class Necromancer(Badge):
def __init__(self):
- description = _(
- 'Answered a question more than %(days)s days '
- 'later with at least %(votes)s votes'
- )
days = askbot_settings.NECROMANCER_BADGE_MIN_DELAY
votes = askbot_settings.NECROMANCER_BADGE_MIN_UPVOTES
+ description = _(
+ '%(answered_a_question)s more than %(days)s days '
+ 'later with at least %(votes)s votes'
+ ) % {
+ 'days':days,
+ 'votes':votes,
+ 'answered_a_question': askbot_settings.WORDS_ANSWERED_A_QUESTION
+ }
super(Necromancer, self).__init__(
key = 'necromancer',
name = _('Necromancer'),
level = const.SILVER_BADGE,
- description = description % {'days':days, 'votes':votes},
+ description = description,
multiple = True
)
@@ -602,6 +645,7 @@ class EditorTypeBadge(Badge):
const.TYPE_ACTIVITY_UPDATE_ANSWER
)
filters = {'user': actor, 'activity_type__in': atypes}
+ from askbot.models.user import Activity
if Activity.objects.filter(**filters).count() == self.min_edits:
return self.award(actor, context_object, timestamp)
@@ -660,19 +704,25 @@ class FavoriteTypeBadge(Badge):
must provide min_stars property for the badge
"""
def __init__(self):
- descr = _('Question favorited by %(num)s users')
+ description = _(
+ '%(asked_a_question)s with %(num)s followers'
+ ) % {
+ 'num': self.min_stars,
+ 'asked_a_question': askbot_settings.WORDS_ASKED_A_QUESTION
+ }
super(FavoriteTypeBadge, self).__init__(
- key = self.key,
- name = self.name,
- level = self.level,
- multiple = True,
- description = descr % {'num': self.min_stars}
+ key=self.key,
+ name=self.name,
+ level=self.level,
+ multiple=True,
+ description=description
)
def consider_award(self, actor = None,
context_object = None, timestamp = None):
question = context_object
#model FavoriteQuestion imported under alias of Fave
+ from askbot.models.question import FavoriteQuestion as Fave#name collision
count = Fave.objects.filter(
thread = question.thread
).exclude(
@@ -686,7 +736,7 @@ class StellarQuestion(FavoriteTypeBadge):
def __new__(cls):
self = super(StellarQuestion, cls).__new__(cls)
self.key = 'stellar-question'
- self.name = _('Stellar Question')
+ self.name = askbot_settings.WORDS_STELLAR_QUESTION
self.level = const.GOLD_BADGE
self.min_stars = askbot_settings.STELLAR_QUESTION_BADGE_MIN_STARS
return self
@@ -695,7 +745,7 @@ class FavoriteQuestion(FavoriteTypeBadge):
def __new__(cls):
self = super(FavoriteQuestion, cls).__new__(cls)
self.key = 'favorite-question'
- self.name = _('Favorite Question')
+ self.name = askbot_settings.WORDS_FAVORITE_QUESTION
self.level = const.SILVER_BADGE
self.min_stars = askbot_settings.FAVORITE_QUESTION_BADGE_MIN_STARS
return self
@@ -752,8 +802,10 @@ class Taxonomist(Badge):
name = _('Taxonomist'),
level = const.SILVER_BADGE,
multiple = False,
- description = _(
- 'Created a tag used by %(num)s questions'
+ description = ungettext(
+ 'Created a tag used %(num)s time',
+ 'Created a tag used %(num)s times',
+ askbot_settings.TAXONOMIST_BADGE_MIN_USE_COUNT
) % {'num': askbot_settings.TAXONOMIST_BADGE_MIN_USE_COUNT}
)
@@ -880,6 +932,7 @@ def init_badges():
get_badge(key).get_stored_data()
#remove any badges from the database
#that are no longer in the BADGES dictionary
+ from askbot.models.repute import BadgeData
BadgeData.objects.exclude(
slug__in = map(slugify, BADGES.keys())
).delete()
diff --git a/askbot/models/meta.py b/askbot/models/meta.py
new file mode 100644
index 00000000..013e918a
--- /dev/null
+++ b/askbot/models/meta.py
@@ -0,0 +1,32 @@
+"""Models that are not essential to operation of
+an askbot instance, but may be used in some cases.
+Data in these models can be erased without loss of function.
+"""
+from django.db import models
+from picklefield.fields import PickledObjectField
+
+class ImportRun(models.Model):
+ """records information about the data import run"""
+ command = models.TextField(default='')
+ timestamp = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ app_label = 'askbot'
+
+class ImportedObjectInfo(models.Model):
+ """records data about objects imported into askbot
+ from other sources.
+ This is useful to create redirect urls when object id's change
+ """
+ old_id = models.IntegerField(help_text='Old object id in the source database')
+ new_id = models.IntegerField(help_text='New object id in the current database')
+ model = models.CharField(
+ default='',
+ help_text='dotted python path to model',
+ max_length=255
+ )
+ run = models.ForeignKey(ImportRun)
+ extra_info = PickledObjectField(help_text='to hold dictionary for various data')
+
+ class Meta:
+ app_label = 'askbot'
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 5c84d253..47af2a42 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -10,6 +10,7 @@ from django.contrib.auth.models import User
from django.core import urlresolvers
from django.db import models
from django.utils import html as html_utils
+from django.utils.text import truncate_html_words
from django.utils.translation import activate as activate_language
from django.utils.translation import get_language
from django.utils.translation import ugettext as _
@@ -24,16 +25,14 @@ import askbot
from askbot.utils.slug import slugify
from askbot import const
-from askbot.models.user import Activity
-from askbot.models.user import EmailFeedSetting
-from askbot.models.user import Group
-from askbot.models.user import GroupMembership
from askbot.models.tag import Tag, MarkedTag
from askbot.models.tag import tags_match_some_wildcard
from askbot.conf import settings as askbot_settings
from askbot import exceptions
from askbot.utils import markup
-from askbot.utils.html import sanitize_html, strip_tags
+from askbot.utils.html import get_word_count
+from askbot.utils.html import sanitize_html
+from askbot.utils.html import strip_tags
from askbot.utils.html import site_url
from askbot.models.base import BaseQuerySetManager, DraftContent
@@ -43,7 +42,7 @@ from askbot.search import mysql
class PostToGroup(models.Model):
post = models.ForeignKey('Post')
- group = models.ForeignKey(Group)
+ group = models.ForeignKey('Group')
class Meta:
unique_together = ('post', 'group')
@@ -59,6 +58,7 @@ class PostQuerySet(models.query.QuerySet):
#as all methods on this class seem to want to
#belong to Thread manager or Query set.
def get_for_user(self, user):
+ from askbot.models.user import Group
if askbot_settings.GROUPS_ENABLED:
if user is None or user.is_anonymous():
groups = [Group.objects.get_global_group()]
@@ -337,13 +337,14 @@ class PostManager(BaseQuerySetManager):
class Post(models.Model):
post_type = models.CharField(max_length=255, db_index=True)
+ #NOTE!!! if these fields are deleted - then jive import needs fixing!!!
old_question_id = models.PositiveIntegerField(null=True, blank=True, default=None, unique=True)
old_answer_id = models.PositiveIntegerField(null=True, blank=True, default=None, unique=True)
old_comment_id = models.PositiveIntegerField(null=True, blank=True, default=None, unique=True)
parent = models.ForeignKey('Post', blank=True, null=True, related_name='comments') # Answer or Question for Comment
thread = models.ForeignKey('Thread', blank=True, null=True, default = None, related_name='posts')
- groups = models.ManyToManyField(Group, through='PostToGroup', related_name = 'group_posts')#used for group-private posts
+ groups = models.ManyToManyField('Group', through='PostToGroup', related_name = 'group_posts')#used for group-private posts
author = models.ForeignKey(User, related_name='posts')
added_at = models.DateTimeField(default=datetime.datetime.now)
@@ -376,7 +377,11 @@ class Post(models.Model):
html = models.TextField(null=True)#html rendition of the latest revision
text = models.TextField(null=True)#denormalized copy of latest revision
- language_code = models.CharField(max_length=16, default=django_settings.LANGUAGE_CODE)
+ language_code = models.CharField(
+ choices=django_settings.LANGUAGES,
+ default=django_settings.LANGUAGE_CODE,
+ max_length=16,
+ )
# Denormalised data
summary = models.TextField(null=True)
@@ -411,9 +416,9 @@ class Post(models.Model):
"""
url = site_url(self.get_absolute_url(no_slug=True))
if self.post_type == 'question':
- tweet = _('Question: ')
+ tweet = askbot_settings.WORDS_QUESTION_SINGULAR + ': '
elif self.post_type == 'answer':
- tweet = _('Answer: ')
+ tweet = askbot_settings.WORDS_ANSWER_SINGULAR + ': '
chars_left = 140 - (len(url) + len(tweet) + 1)
title_str = self.thread.title[:chars_left]
@@ -445,10 +450,14 @@ class Post(models.Model):
removed_mentions = list()
if '@' in text:
op = self.get_origin_post()
- anticipated_authors = op.get_author_list(
- include_comments = True,
- recursive = True
- )
+
+ if op.id:
+ anticipated_authors = op.get_author_list(
+ include_comments = True,
+ recursive = True
+ )
+ else:
+ anticipated_authors = list()
extra_name_seeds = markup.extract_mentioned_name_seeds(text)
@@ -500,7 +509,6 @@ class Post(models.Model):
"""generic method to use with posts to be used prior to saving
post edit or addition
"""
-
assert(author is not None)
last_revision = self.html
@@ -639,6 +647,7 @@ class Post(models.Model):
#vip groups to the list behind the scenes.
groups = list(groups)
#add moderator groups to the post implicitly
+ from askbot.models.user import Group
vips = Group.objects.filter(is_vip=True)
groups.extend(vips)
#todo: use bulk-creation
@@ -684,6 +693,7 @@ class Post(models.Model):
else:
summary = self.get_snippet()
+ from askbot.models import Activity
update_activity = Activity(
user = updated_by,
active_at = timestamp,
@@ -697,6 +707,7 @@ class Post(models.Model):
update_activity.add_recipients(notify_sets['for_inbox'])
#create new mentions (barring the double-adds)
+ from askbot.models import Activity
for u in notify_sets['for_mentions'] - notify_sets['for_inbox']:
Activity.objects.create_new_mention(
mentioned_whom = u,
@@ -736,6 +747,7 @@ class Post(models.Model):
"""makes post private within user's groups
todo: this is a copy-paste in thread and post
"""
+ from askbot.models.user import Group
if group_id:
group = Group.objects.get(id=group_id)
groups = [group]
@@ -763,12 +775,14 @@ class Post(models.Model):
def make_public(self):
"""removes the privacy mark from users groups"""
+ from askbot.models.user import Group
groups = (Group.objects.get_global_group(),)
self.add_to_groups(groups)
def is_private(self):
"""true, if post belongs to the global group"""
if askbot_settings.GROUPS_ENABLED:
+ from askbot.models.user import Group
group = Group.objects.get_global_group()
return not self.groups.filter(id=group.id).exists()
return False
@@ -884,10 +898,38 @@ class Post(models.Model):
return slugify(self.thread.title)
slug = property(_get_slug)
- def get_snippet(self, max_length = 120):
- """returns an abbreviated snippet of the content
+ def get_snippet(self, max_length=None):
+ """returns an abbreviated HTML snippet of the content
+ or full content, depending on how long it is
+ todo: remove the max_length parameter
"""
- return html_utils.strip_tags(self.html)[:max_length] + ' ...'
+ if max_length is None:
+ if self.post_type == 'comment':
+ max_words = 150
+ else:
+ max_words = 500
+ else:
+ max_words = int(max_length/5)
+
+ #todo: truncate so that we have max number of lines
+ #the issue is that code blocks have few words
+ #but very tall, while paragraphs can be dense on words
+ #and fit into fewer lines
+ truncated = truncate_html_words(self.html, max_words)
+ new_count = get_word_count(truncated)
+ orig_count = get_word_count(self.html)
+ if new_count + 1 < orig_count:
+ expander = '<span class="expander"> <a>(' + _('more') + ')</a></span>'
+ if truncated.endswith('</p>'):
+ #better put expander inside the paragraph
+ snippet = truncated[:-4] + expander + '</p>'
+ else:
+ snippet = truncated + expander
+ #it is important to have div here, so that we can make
+ #the expander work
+ return '<div class="snippet">' + snippet + '</div>'
+ else:
+ return self.html
def filter_authorized_users(self, candidates):
"""returns list of users who are allowed to see this post"""
@@ -904,6 +946,7 @@ class Post(models.Model):
return list()
#load group memberships for the candidates
+ from askbot.models.user import GroupMembership
memberships = GroupMembership.objects.filter(
user__in=candidates,
group__in=groups
@@ -919,7 +962,8 @@ class Post(models.Model):
return filtered_candidates
def format_for_email(
- self, quote_level=0, is_leaf_post=False, format=None
+ self, quote_level=0, is_leaf_post=False, format=None,
+ recipient=None
):
"""format post for the output in email,
if quote_level > 0, the post will be indented that number of times
@@ -930,13 +974,14 @@ class Post(models.Model):
template = get_template('email/quoted_post.html')
data = {
'post': self,
+ 'recipient': recipient,
'quote_level': quote_level,
'is_leaf_post': is_leaf_post,
'format': format
}
return template.render(Context(data))#todo: set lang
- def format_for_email_as_parent_thread_summary(self):
+ def format_for_email_as_parent_thread_summary(self, recipient=None):
"""format for email as summary of parent posts
all the way to the original question"""
quote_level = 0
@@ -948,20 +993,25 @@ class Post(models.Model):
break
quote_level += 1
output += parent_post.format_for_email(
- quote_level = quote_level,
- format = 'parent_subthread'
+ quote_level=quote_level,
+ format='parent_subthread',
+ recipient=recipient
)
current_post = parent_post
return output
- def format_for_email_as_subthread(self):
+ def format_for_email_as_subthread(self, recipient=None):
"""outputs question or answer and all it's comments
returns empty string for all other post types
"""
from django.template import Context
from django.template.loader import get_template
template = get_template('email/post_as_subthread.html')
- return template.render(Context({'post': self}))#todo: set lang
+ data = {
+ 'post': self,
+ 'recipient': recipient
+ }
+ return template.render(Context(data))#todo: set lang
def set_cached_comments(self, comments):
"""caches comments in the lifetime of the object
@@ -1045,6 +1095,7 @@ class Post(models.Model):
tag_names = self.get_tag_names()
tag_selections = MarkedTag.objects.filter(
tag__name__in = tag_names,
+ tag__language_code=get_language(),
reason = tag_mark_reason
)
subscribers = set(
@@ -1106,6 +1157,7 @@ class Post(models.Model):
"""
subscriber_set = set()
+ from askbot.models.user import EmailFeedSetting
global_subscriptions = EmailFeedSetting.objects.filter(
feed_type = 'q_all',
frequency = 'i'
@@ -1183,6 +1235,7 @@ class Post(models.Model):
#print 'potential subscribers: ', potential_subscribers
#1) mention subscribers - common to questions and answers
+ from askbot.models.user import EmailFeedSetting
if mentioned_users:
mention_subscribers = EmailFeedSetting.objects.filter_subscribers(
potential_subscribers = mentioned_users,
@@ -1276,6 +1329,7 @@ class Post(models.Model):
if mentioned_users:
potential_subscribers.update(mentioned_users)
+ from askbot.models.user import EmailFeedSetting
if potential_subscribers:
comment_subscribers = EmailFeedSetting.objects.filter_subscribers(
potential_subscribers = potential_subscribers,
@@ -1427,6 +1481,8 @@ class Post(models.Model):
)
elif user.email_tag_filter_strategy == const.INCLUDE_ALL:
return True
+ elif user.email_tag_filter_strategy == const.INCLUDE_SUBSCRIBED:
+ return user.has_affinity_to_question(question, affinity_type='like')
else:
raise ValueError(
'unexpected User.email_tag_filter_strategy %s'\
@@ -1566,10 +1622,7 @@ class Post(models.Model):
if self.is_approved() is False:
raise exceptions.QuestionHidden()
if self.deleted:
- message = _(
- 'Sorry, this question has been '
- 'deleted and is no longer accessible'
- )
+ message = _('Sorry, this content is no longer available')
if user.is_anonymous():
raise exceptions.QuestionHidden(message)
try:
@@ -1582,17 +1635,10 @@ class Post(models.Model):
try:
self.thread._question_post().assert_is_visible_to(user)
except exceptions.QuestionHidden:
- message = _(
- 'Sorry, the answer you are looking for is '
- 'no longer available, because the parent '
- 'question has been removed'
- )
+ message = _('Sorry, this content is no longer available')
raise exceptions.QuestionHidden(message)
if self.deleted:
- message = _(
- 'Sorry, this answer has been '
- 'removed and is no longer accessible'
- )
+ message = _('Sorry, this content is no longer available')
if user.is_anonymous():
raise exceptions.AnswerHidden(message)
try:
@@ -1605,18 +1651,10 @@ class Post(models.Model):
try:
self.parent.assert_is_visible_to(user)
except exceptions.QuestionHidden:
- message = _(
- 'Sorry, the comment you are looking for is no '
- 'longer accessible, because the parent question '
- 'has been removed'
- )
+ message = _('Sorry, this comment is no longer available')
raise exceptions.QuestionHidden(message)
except exceptions.AnswerHidden:
- message = _(
- 'Sorry, the comment you are looking for is no '
- 'longer accessible, because the parent answer '
- 'has been removed'
- )
+ message = _('Sorry, this comment is no longer available')
raise exceptions.AnswerHidden(message)
def assert_is_visible_to_user_groups(self, user):
@@ -2103,6 +2141,7 @@ class PostRevision(models.Model):
#if sent by email to group and group does not want moderation
if self.by_email and self.email_address:
group_name = self.email_address.split('@')[0]
+ from askbot.models.user import Group
try:
group = Group.objects.get(name = group_name, deleted = False)
return group.group.profile.moderate_email
@@ -2269,7 +2308,7 @@ class PostRevision(models.Model):
return sanitized_html
def get_snippet(self, max_length = 120):
- """same as Post.get_snippet"""
+ """a little simpler than as Post.get_snippet"""
return html_utils.strip_tags(self.html)[:max_length] + '...'
diff --git a/askbot/models/question.py b/askbot/models/question.py
index 87b01f3c..65e1168a 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -4,6 +4,7 @@ import re
from django.conf import settings as django_settings
from django.db import models
+from django.db.models import F
from django.contrib.auth.models import User
from django.core import cache # import cache, not from cache import cache, to be able to monkey-patch cache.cache in test cases
from django.core import exceptions as django_exceptions
@@ -14,6 +15,7 @@ from django.utils.translation import ugettext as _
from django.utils.translation import ungettext
from django.utils.translation import string_concat
from django.utils.translation import get_language
+from django.utils.translation import activate as activate_language
import askbot
from askbot.conf import settings as askbot_settings
@@ -23,9 +25,8 @@ from askbot.models.tag import Tag, TagSynonym
from askbot.models.tag import get_tags_by_names
from askbot.models.tag import filter_accepted_tags, filter_suggested_tags
from askbot.models.tag import separate_unused_tags
-from askbot.models.base import DraftContent, BaseQuerySetManager
-from askbot.models.post import Post, PostRevision
-from askbot.models.post import PostToGroup
+from askbot.models.base import BaseQuerySetManager
+from askbot.models.base import DraftContent
from askbot.models.user import Group, PERSONAL_GROUP_NAME_PREFIX
from askbot.models import signals
from askbot import const
@@ -34,6 +35,27 @@ from askbot.search import mysql
from askbot.utils.slug import slugify
from askbot.search.state_manager import DummySearchState
+
+def clean_tagnames(tagnames):
+ """Cleans tagnames string so that the field fits the constraint of the
+ database.
+ TODO: remove this when the Thread.tagnames field is converted into
+ text_field
+ """
+ original = tagnames
+ tagnames = tagnames.strip().split()
+ #see if the tagnames field fits into 125 bytes
+ while True:
+ encoded_tagnames = ' '.join(tagnames).encode('utf-8')
+ length = len(encoded_tagnames)
+ if length == 0:
+ return ''
+ elif length <= 125:
+ return ' '.join(tagnames)
+ else:
+ tagnames.pop()
+
+
class ThreadQuerySet(models.query.QuerySet):
def get_visible(self, user):
"""filters out threads not belonging to the user groups"""
@@ -52,7 +74,7 @@ class ThreadQuerySet(models.query.QuerySet):
if getattr(django_settings, 'ENABLE_HAYSTACK_SEARCH', False):
from askbot.search.haystack.searchquery import AskbotSearchQuerySet
hs_qs = AskbotSearchQuerySet().filter(content=search_query).models(self.model)
- return hs_qs.get_django_queryset()
+ return self & hs_qs.get_django_queryset()
else:
db_engine_name = askbot.get_database_engine_name()
filter_parameters = {'deleted': False}
@@ -102,6 +124,8 @@ class ThreadManager(BaseQuerySetManager):
tag_list.sort(key=lambda t: tag_counts[t], reverse=True)
#note that double quote placement is important here
+ if len(tag_list) == 0:
+ return ''
if len(tag_list) == 1:
last_topic = '"'
elif len(tag_list) <= 5:
@@ -134,6 +158,7 @@ class ThreadManager(BaseQuerySetManager):
# TODO: Some of this code will go to Post.objects.create_new
language = language or get_language()
+ tagnames = clean_tagnames(tagnames)
thread = super(
ThreadManager,
@@ -147,6 +172,7 @@ class ThreadManager(BaseQuerySetManager):
)
#todo: code below looks like ``Post.objects.create_new()``
+ from askbot.models.post import Post
question = Post(
post_type='question',
thread=thread,
@@ -369,26 +395,30 @@ class ThreadManager(BaseQuerySetManager):
except User.DoesNotExist:
meta_data['author_name'] = None
else:
- qs = qs.filter(posts__post_type__in=('question', 'answer'), posts__author=u, posts__deleted=False)
+ qs = qs.filter(posts__post_type='question', posts__author=u, posts__deleted=False)
meta_data['author_name'] = u.username
#get users tag filters
if request_user and request_user.is_authenticated():
#mark questions tagged with interesting tags
#a kind of fancy annotation, would be nice to avoid it
+ lang = get_language()
interesting_tags = Tag.objects.filter(
- user_selections__user = request_user,
- user_selections__reason = 'good'
+ user_selections__user=request_user,
+ user_selections__reason='good',
+ language_code=lang
)
ignored_tags = Tag.objects.filter(
user_selections__user = request_user,
- user_selections__reason = 'bad'
+ user_selections__reason = 'bad',
+ language_code=lang
)
subscribed_tags = Tag.objects.none()
if askbot_settings.SUBSCRIBED_TAG_SELECTOR_ENABLED:
subscribed_tags = Tag.objects.filter(
user_selections__user = request_user,
- user_selections__reason = 'subscribed'
+ user_selections__reason = 'subscribed',
+ language_code=lang
)
meta_data['subscribed_tag_names'] = [tag.name for tag in subscribed_tags]
@@ -466,6 +496,7 @@ class ThreadManager(BaseQuerySetManager):
#threads = [thread for thread in threads if not thread.summary_html_cached()]
thread_ids = [obj.id for obj in threads]
+ from askbot.models.post import Post
page_questions = Post.objects.filter(
post_type='question', thread__id__in = thread_ids
).only(# pick only the used fields
@@ -491,6 +522,7 @@ class ThreadManager(BaseQuerySetManager):
def get_thread_contributors(self, thread_list):
"""Returns query set of Thread contributors"""
# INFO: Evaluate this query to avoid subquery in the subsequent query below (At least MySQL can be awfully slow on subqueries)
+ from askbot.models.post import Post
u_id = list(Post.objects.filter(post_type__in=('question', 'answer'), thread__in=thread_list).values_list('author', flat=True))
#todo: this does not belong gere - here we select users with real faces
@@ -505,6 +537,8 @@ class ThreadManager(BaseQuerySetManager):
def get_for_user(self, user):
"""returns threads where a given user had participated"""
+ from askbot.models.post import PostRevision
+ from askbot.models.post import Post
post_ids = PostRevision.objects.filter(
author = user
).values_list(
@@ -543,7 +577,7 @@ class ThreadToGroup(models.Model):
class Thread(models.Model):
- SUMMARY_CACHE_KEY_TPL = 'thread-question-summary-%d'
+ SUMMARY_CACHE_KEY_TPL = 'thread-question-summary-%d-%s'
ANSWER_LIST_KEY_TPL = 'thread-answer-list-%d'
title = models.CharField(max_length=300)
@@ -558,8 +592,14 @@ class Thread(models.Model):
answer_count = models.PositiveIntegerField(default=0)
last_activity_at = models.DateTimeField(default=datetime.datetime.now)
last_activity_by = models.ForeignKey(User, related_name='unused_last_active_in_threads')
- language_code = models.CharField(max_length=16, default=django_settings.LANGUAGE_CODE)
+ language_code = models.CharField(
+ choices=django_settings.LANGUAGES,
+ default=django_settings.LANGUAGE_CODE,
+ max_length=16
+ )
+ #todo: these two are redundant (we used to have a "star" and "subscribe"
+ #now merged into "followed")
followed_by = models.ManyToManyField(User, related_name='followed_threads')
favorited_by = models.ManyToManyField(User, through='FavoriteQuestion', related_name='unused_favorite_threads')
@@ -578,9 +618,9 @@ class Thread(models.Model):
#approvals - by whom and when
approved = models.BooleanField(default=True, db_index=True)
- accepted_answer = models.ForeignKey(Post, null=True, blank=True, related_name='+')
+ accepted_answer = models.ForeignKey('Post', null=True, blank=True, related_name='+')
answer_accepted_at = models.DateTimeField(null=True, blank=True)
- added_at = models.DateTimeField(default = datetime.datetime.now)
+ added_at = models.DateTimeField(auto_now_add=True)
#db_column will be removed later
points = models.IntegerField(default = 0, db_column='score')
@@ -605,6 +645,7 @@ class Thread(models.Model):
post = getattr(self, '_question_cache', None)
if post:
return post
+ from askbot.models.post import Post
self._question_cache = Post.objects.get(post_type='question', thread=self)
return self._question_cache
@@ -687,6 +728,12 @@ class Thread(models.Model):
return answers[0].id
return None
+ def get_latest_post(self):
+ """returns latest non-deleted post"""
+ if askbot_settings.GROUPS_ENABLED:
+ raise NotImplementedError()
+ return self.posts.filter(deleted=False).order_by('-added_at')[0]
+
def get_sharing_info(self, visitor=None):
"""returns a dictionary with abbreviated thread sharing info:
* users - up to a certain number of users, excluding the visitor
@@ -783,6 +830,65 @@ class Thread(models.Model):
self.save()
self.invalidate_cached_data()
+ def set_tags_language_code(self, language_code=None):
+ """sets language code to tags of this thread.
+ If lang code of the tag does not coincide with that
+ of thread, we replace the tag with the one of correct
+ lang code. If necessary, tags are created and
+ the used_counts are updated.
+ """
+ wrong_lang_tags = list()
+ for tag in self.tags.all():
+ if tag.language_code != language_code:
+ wrong_lang_tags.append(tag)
+
+ #remove wrong tags
+ self.tags.remove(*wrong_lang_tags)
+ #update used counts of the wrong tags
+ wrong_lang_tag_names = list()
+ for tag in wrong_lang_tags:
+ wrong_lang_tag_names.append(tag.name)
+ if tag.used_count > 0:
+ tag.used_count -= 1
+ tag.save()
+
+ #load existing tags and figure out which tags don't exist
+ reused_tags, new_tagnames = get_tags_by_names(
+ wrong_lang_tag_names,
+ language_code=language_code
+ )
+ reused_tags.mark_undeleted()
+ #tag moderation is in the call below
+ created_tags = Tag.objects.create_in_bulk(
+ language_code=self.language_code,
+ tag_names=new_tagnames,
+ user=self.last_activity_by,
+ auto_approve=True
+ )
+ #add the tags
+ added_tags = list(reused_tags) + list(created_tags)
+ self.tags.add(*added_tags)
+ #increment the used counts and save tags
+ tag_ids = [tag.id for tag in added_tags]
+ Tag.objects.filter(id__in=tag_ids).update(used_count=F('used_count') + 1)
+
+ def set_language_code(self, language_code=None):
+ assert(language_code)
+
+ #save language code on thread
+ self.language_code = language_code
+ self.save()
+
+ #save language code on all posts
+ #for some reason "update" fails in postgres - possibly b/c of the FTS
+ for post in self.posts.all():
+ post.language_code = language_code
+ post.save()
+
+ #make sure that tags have correct language code
+ self.set_tags_language_code(language_code)
+
+
def set_accepted_answer(self, answer, timestamp):
if answer and answer.thread != self:
raise ValueError("Answer doesn't belong to this thread")
@@ -805,16 +911,13 @@ class Thread(models.Model):
else:
return self.tagnames.split(u' ')
- def get_title(self, question=None):
- if not question:
- question = self._question_post() # allow for optimization if the caller has already fetched the question post for this thread
+ def get_title(self):
if self.is_private():
attr = const.POST_STATUS['private']
elif self.closed:
attr = const.POST_STATUS['closed']
- elif question.deleted:
+ elif self.deleted:
attr = const.POST_STATUS['deleted']
-
else:
attr = None
if attr is not None:
@@ -822,14 +925,15 @@ class Thread(models.Model):
else:
return self.title
- def format_for_email(self, user=None):
+ def format_for_email(self, recipient=None):
"""experimental function: output entire thread for email"""
question, answers, junk, published_ans_ids = \
- self.get_cached_post_data(user=user)
+ self.get_cached_post_data(user=recipient)
- output = question.format_for_email_as_subthread()
+ output = question.format_for_email_as_subthread(recipient=recipient)
if answers:
+ #todo: words
answer_heading = ungettext(
'%(count)d answer:',
'%(count)d answers:',
@@ -837,7 +941,7 @@ class Thread(models.Model):
) % {'count': len(answers)}
output += '<p>%s</p>' % answer_heading
for answer in answers:
- output += answer.format_for_email_as_subthread()
+ output += answer.format_for_email_as_subthread(recipient=recipient)
return output
def get_answers_by_user(self, user):
@@ -905,7 +1009,7 @@ class Thread(models.Model):
# )
def invalidate_cached_thread_content_fragment(self):
- cache.cache.delete(self.SUMMARY_CACHE_KEY_TPL % self.id)
+ cache.cache.delete(self.SUMMARY_CACHE_KEY_TPL % (self.id, get_language()))
def get_post_data_cache_key(self, sort_method = None):
return 'thread-data-%s-%s' % (self.id, sort_method)
@@ -1082,7 +1186,8 @@ class Thread(models.Model):
# we had question post id denormalized on the thread
tags_list = self.get_tag_names()
similar_threads = Thread.objects.filter(
- tags__name__in=tags_list
+ tags__name__in=tags_list,
+ language_code=self.language_code
).exclude(
id = self.id
).exclude(
@@ -1100,6 +1205,7 @@ class Thread(models.Model):
# Denormalize questions to speed up template rendering
# todo: just denormalize question_post_id on the thread!
thread_map = dict([(thread.id, thread) for thread in similar_threads])
+ from askbot.models.post import Post
questions = Post.objects.get_questions()
questions = questions.select_related('thread').filter(thread__in=similar_threads)
for q in questions:
@@ -1116,7 +1222,7 @@ class Thread(models.Model):
# this is a "legacy" problem inherited from the old models
if question_post:
url = question_post.get_absolute_url()
- title = thread.get_title(question_post)
+ title = thread.get_title()
result.append({'url': url, 'title': title})
return result
@@ -1144,6 +1250,7 @@ class Thread(models.Model):
#it is important that update method is called - not save,
#because we do not want the signals to fire here
thread_question = self._question_post()
+ from askbot.models.post import Post
Post.objects.filter(id=thread_question.id).update(is_anonymous=False)
thread_question.revisions.all().update(is_anonymous=False)
@@ -1177,6 +1284,7 @@ class Thread(models.Model):
"""removes child posts from given groups"""
post_ids = self.posts.all().values_list('id', flat=True)
group_ids = [group.id for group in groups]
+ from askbot.models.post import PostToGroup
PostToGroup.objects.filter(
post__id__in=post_ids,
tag__id__in=group_ids
@@ -1262,7 +1370,7 @@ class Thread(models.Model):
def update_tags(
- self, tagnames = None, user = None, timestamp = None
+ self, tagnames=None, user=None, timestamp=None
):
"""
Updates Tag associations for a thread to match the given
@@ -1290,7 +1398,10 @@ class Thread(models.Model):
updated_tagnames = set()
for tag_name in updated_tagnames_tmp:
try:
- tag_synonym = TagSynonym.objects.get(source_tag_name=tag_name)
+ tag_synonym = TagSynonym.objects.get(
+ source_tag_name=tag_name,
+ language_code=self.language_code
+ )
updated_tagnames.add(tag_synonym.target_tag_name)
tag_synonym.auto_rename_count += 1
tag_synonym.save()
@@ -1314,14 +1425,18 @@ class Thread(models.Model):
if added_tagnames:
#find reused tags
- reused_tags, new_tagnames = get_tags_by_names(added_tagnames)
+ reused_tags, new_tagnames = get_tags_by_names(
+ added_tagnames,
+ language_code=self.language_code
+ )
reused_tags.mark_undeleted()
added_tags = list(reused_tags)
#tag moderation is in the call below
created_tags = Tag.objects.create_in_bulk(
+ language_code=self.language_code,
tag_names=new_tagnames,
- user=user
+ user=user,
)
added_tags.extend(created_tags)
@@ -1394,19 +1509,18 @@ class Thread(models.Model):
silent=silent
)
+
def retag(self, retagged_by=None, retagged_at=None, tagnames=None, silent=False):
"""changes thread tags"""
if None in (retagged_by, retagged_at, tagnames):
raise Exception('arguments retagged_at, retagged_by and tagnames are required')
- if len(tagnames) > 125:#todo: remove magic number!!!
- raise django_exceptions.ValidationError('tagnames value too long')
+ tagnames = clean_tagnames(tagnames)
+ self.tagnames = tagnames
+ self.save()
thread_question = self._question_post()
- self.tagnames = tagnames.strip()
- self.save()
-
# Update the Question itself
if silent == False:
thread_question.last_edited_at = retagged_at
@@ -1420,6 +1534,8 @@ class Thread(models.Model):
# Create a new revision
latest_revision = thread_question.get_latest_revision()
+
+ from askbot.models.post import PostRevision
PostRevision.objects.create(
post=thread_question,
title=latest_revision.title,
@@ -1484,7 +1600,7 @@ class Thread(models.Model):
#parameter visitor is there to get summary out by the user groups
if askbot_settings.GROUPS_ENABLED:
return None
- return cache.cache.get(self.SUMMARY_CACHE_KEY_TPL % self.id)
+ return cache.cache.get(self.SUMMARY_CACHE_KEY_TPL % (self.id, get_language()))
def update_summary_html(self, visitor = None):
#todo: it is quite wrong that visitor is an argument here
@@ -1502,6 +1618,7 @@ class Thread(models.Model):
}
from askbot.views.context import get_extra as get_extra_context
context.update(get_extra_context('ASKBOT_QUESTION_SUMMARY_EXTRA_CONTEXT', None, context))
+ activate_language(self.language_code)
html = get_template('widgets/question_summary.html').render(context)
# INFO: Timeout is set to 30 days:
# * timeout=0/None is not a reliable cross-backend way to set infinite timeout
@@ -1509,17 +1626,17 @@ class Thread(models.Model):
# * Additionally, Memcached treats timeouts > 30day as dates (https://code.djangoproject.com/browser/django/tags/releases/1.3/django/core/cache/backends/memcached.py#L36),
# which probably doesn't break anything but if we can stick to 30 days then let's stick to it
cache.cache.set(
- self.SUMMARY_CACHE_KEY_TPL % self.id,
+ self.SUMMARY_CACHE_KEY_TPL % (self.id, get_language()),
html,
timeout=const.LONG_TIME
)
return html
def summary_html_cached(self):
- return cache.cache.has_key(self.SUMMARY_CACHE_KEY_TPL % self.id)
+ return cache.cache.has_key(self.SUMMARY_CACHE_KEY_TPL % (self.id, get_language()))
class QuestionView(models.Model):
- question = models.ForeignKey(Post, related_name='viewed')
+ question = models.ForeignKey('Post', related_name='viewed')
who = models.ForeignKey(User, related_name='question_views')
when = models.DateTimeField()
@@ -1592,9 +1709,3 @@ class AnonymousQuestion(DraftContent):
text=self.text,
tagnames=self.tagnames
)
- #add message with a link to the ask page
- extra_message = _(
- 'Please, <a href="%s">review your question</a>.'
- ) % reverse('ask')
- message = string_concat(unicode(error), u' ', extra_message)
- user.message_set.create(message=unicode(message))
diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py
index 983edc8f..0b164d24 100644
--- a/askbot/models/reply_by_email.py
+++ b/askbot/models/reply_by_email.py
@@ -5,7 +5,6 @@ import logging
from django.db import models
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
-from django.utils.translation import ugettext_lazy
from askbot.models.post import Post
from askbot.models.base import BaseQuerySetManager
from askbot.conf import settings as askbot_settings
@@ -35,12 +34,12 @@ class ReplyAddressManager(BaseQuerySetManager):
REPLY_ACTION_CHOICES = (
- ('post_answer', ugettext_lazy('Post an answer')),
- ('post_comment', ugettext_lazy('Post a comment')),
- ('replace_content', ugettext_lazy('Edit post')),
- ('append_content', ugettext_lazy('Append to post')),
- ('auto_answer_or_comment', ugettext_lazy('Answer or comment, depending on the size of post')),
- ('validate_email', ugettext_lazy('Validate email and record signature')),
+ ('post_answer', 'Post an answer'),
+ ('post_comment', 'Post a comment'),
+ ('replace_content', 'Edit post'),
+ ('append_content', 'Append to post'),
+ ('auto_answer_or_comment', 'Answer or comment, depending on the size of post'),
+ ('validate_email', 'Validate email and record signature'),
)
class ReplyAddress(models.Model):
"""Stores a reply address for the post
diff --git a/askbot/models/repute.py b/askbot/models/repute.py
index 5e9c295f..515356f0 100644
--- a/askbot/models/repute.py
+++ b/askbot/models/repute.py
@@ -101,6 +101,9 @@ class BadgeData(models.Model):
from askbot.models import badges
return badges.get_badge(self.slug)
+ def is_multiple(self):
+ return self._get_meta_data().multiple
+
def get_name(self):
return self._get_meta_data().name
@@ -213,21 +216,10 @@ class Repute(models.Model):
'username': self.user.username,
'question_title': self.question.thread.title
}
- if delta > 0:
- link_title = _(
- '%(points)s points were added for %(username)s\'s '
- 'contribution to question %(question_title)s'
- ) % link_title_data
- else:
- link_title = _(
- '%(points)s points were subtracted for %(username)s\'s '
- 'contribution to question %(question_title)s'
- ) % link_title_data
- return '<a href="%(url)s" title="%(link_title)s">%(question_title)s</a>' \
+ return '<a href="%(url)s">%(question_title)s</a>' \
% {
'url': self.question.get_absolute_url(),
'question_title': escape(self.question.thread.title),
- 'link_title': escape(link_title)
}
diff --git a/askbot/models/signals.py b/askbot/models/signals.py
index 42a9c787..00152a0a 100644
--- a/askbot/models/signals.py
+++ b/askbot/models/signals.py
@@ -52,6 +52,7 @@ post_revision_published = django.dispatch.Signal(
]
)
site_visited = django.dispatch.Signal(providing_args=['user', 'timestamp'])
+reputation_received = django.dispatch.Signal(providing_args=['user', 'reputation_before'])
def pop_signal_receivers(signal):
"""disables a given signal by removing listener functions
diff --git a/askbot/models/tag.py b/askbot/models/tag.py
index bf054627..1934f81a 100644
--- a/askbot/models/tag.py
+++ b/askbot/models/tag.py
@@ -1,9 +1,10 @@
import re
from django.db import models
from django.contrib.auth.models import User
+from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
-from django.conf import settings
+from django.conf import settings as django_settings
from askbot.models.base import BaseQuerySetManager
from askbot import const
from askbot.conf import settings as askbot_settings
@@ -12,13 +13,16 @@ from askbot.utils import category_tree
def delete_tags(tags):
"""deletes tags in the list"""
tag_ids = [tag.id for tag in tags]
- Tag.objects.filter(id__in = tag_ids).delete()
+ Tag.objects.filter(id__in=tag_ids).delete()
-def get_tags_by_names(tag_names):
+def get_tags_by_names(tag_names, language_code=None):
"""returns query set of tags
and a set of tag names that were not found
"""
- tags = Tag.objects.filter(name__in = tag_names)
+ tags = Tag.objects.filter(
+ name__in=tag_names,
+ language_code=language_code
+ )
#if there are brand new tags, create them
#and finalize the added tag list
if tags.count() < len(tag_names):
@@ -155,7 +159,7 @@ class TagQuerySet(models.query.QuerySet):
tag_filter = models.Q(name__startswith = first_tag[:-1])
for next_tag in wildcards:
tag_filter |= models.Q(name__startswith = next_tag[:-1])
- return self.filter(tag_filter)
+ return self.filter(tag_filter & models.Q(language_code=get_language()))
def get_related_to_search(self, threads, ignored_tag_names):
"""Returns at least tag names, along with use counts"""
@@ -177,9 +181,9 @@ class TagManager(BaseQuerySetManager):
"""temporary function that filters out the group tags"""
return self.all()
- def create(self, name=None, created_by=None, **kwargs):
+ def create(self, name=None, created_by=None, auto_approve=False, **kwargs):
"""Creates a new tag"""
- if created_by.can_create_tags() or is_preapproved_tag_name(name):
+ if auto_approve or created_by.can_create_tags() or is_preapproved_tag_name(name):
status = Tag.STATUS_ACCEPTED
else:
status = Tag.STATUS_SUGGESTED
@@ -219,7 +223,7 @@ class TagManager(BaseQuerySetManager):
) % ', '.join(tag_names)
user.message_set.create(message = msg)
- def create_in_bulk(self, tag_names = None, user = None):
+ def create_in_bulk(self, tag_names=None, user=None, language_code=None, auto_approve=False):
"""creates tags by names. If user can create tags,
then they are set status ``STATUS_ACCEPTED``,
otherwise the status will be set to ``STATUS_SUGGESTED``.
@@ -227,21 +231,24 @@ class TagManager(BaseQuerySetManager):
One exception: if suggested tag is in the category tree
and source of tags is category tree - then status of newly
created tag is ``STATUS_ACCEPTED``
+ if `auto_approve` is True then tags are auto-accepted
"""
#load suggested tags
pre_suggested_tags = self.filter(
- name__in = tag_names, status = Tag.STATUS_SUGGESTED
+ name__in=tag_names,
+ status=Tag.STATUS_SUGGESTED,
+ language_code=language_code
)
#deal with suggested tags
- if user.can_create_tags():
+ if auto_approve or user.can_create_tags():
#turn previously suggested tags into accepted
pre_suggested_tags.update(status = Tag.STATUS_ACCEPTED)
else:
#increment use count and add user to "suggested_by"
for tag in pre_suggested_tags:
- tag.times_used += 1
+ tag.used_count += 1
tag.suggested_by.add(user)
tag.save()
@@ -253,7 +260,12 @@ class TagManager(BaseQuerySetManager):
for tag_name in set(tag_names) - set(pre_suggested_tag_names):
#status for the new tags is automatically set within the create()
- new_tag = Tag.objects.create(name = tag_name, created_by = user)
+ new_tag = Tag.objects.create(
+ name=tag_name,
+ created_by=user,
+ language_code=language_code,
+ auto_approve=auto_approve
+ )
created_tags.append(new_tag)
if new_tag.status == Tag.STATUS_SUGGESTED:
@@ -273,9 +285,13 @@ class Tag(models.Model):
STATUS_SUGGESTED = 0
STATUS_ACCEPTED = 1
- name = models.CharField(max_length=255, unique=True)
+ name = models.CharField(max_length=255)
created_by = models.ForeignKey(User, related_name='created_tags')
-
+ language_code = models.CharField(
+ choices=django_settings.LANGUAGES,
+ default=django_settings.LANGUAGE_CODE,
+ max_length=16,
+ )
suggested_by = models.ManyToManyField(
User, related_name='suggested_tags',
help_text = 'Works only for suggested tags for tag moderation'
@@ -302,6 +318,7 @@ class Tag(models.Model):
app_label = 'askbot'
db_table = u'tag'
ordering = ('-used_count', 'name')
+ unique_together = ('name', 'language_code')
def __unicode__(self):
return self.name
@@ -328,6 +345,11 @@ class TagSynonym(models.Model):
owned_by = models.ForeignKey(User, related_name='tag_synonyms')
auto_rename_count = models.IntegerField(default=0)
last_auto_rename_at = models.DateTimeField(auto_now=True)
+ language_code = models.CharField(
+ choices=django_settings.LANGUAGES,
+ default=django_settings.LANGUAGE_CODE,
+ max_length=16,
+ )
class Meta:
app_label = 'askbot'
diff --git a/askbot/models/user.py b/askbot/models/user.py
index ad49222b..5501f30f 100644
--- a/askbot/models/user.py
+++ b/askbot/models/user.py
@@ -9,6 +9,7 @@ from django.contrib.auth.models import User
from django.contrib.auth.models import Group as AuthGroup
from django.core import exceptions
from django.forms import EmailField, URLField
+from django.utils import translation
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.utils.html import strip_tags
@@ -16,11 +17,6 @@ from askbot import const
from askbot.conf import settings as askbot_settings
from askbot.utils import functions
from askbot.models.base import BaseQuerySetManager
-from askbot.models.tag import Tag
-from askbot.models.tag import clean_group_name#todo - delete this
-from askbot.models.tag import get_tags_by_names
-from askbot.forms import DomainNameField
-from askbot.utils.forms import email_is_allowed
from collections import defaultdict
PERSONAL_GROUP_NAME_PREFIX = '_personal_'
@@ -312,6 +308,7 @@ class EmailFeedSetting(models.Model):
'q_sel': 'i',
'm_and_c': 'i'
}
+ #todo: words
FEED_TYPE_CHOICES = (
('q_all', ugettext_lazy('Entire forum')),
('q_ask', ugettext_lazy('Questions that I asked')),
@@ -455,6 +452,7 @@ class GroupQuerySet(models.query.QuerySet):
return self.filter(user = user)
def get_by_name(self, group_name = None):
+ from askbot.models.tag import clean_group_name#todo - delete this
return self.get(name = clean_group_name(group_name))
@@ -589,6 +587,7 @@ class Group(AuthGroup):
return 'open'
#relying on a specific method of storage
+ from askbot.utils.forms import email_is_allowed
if email_is_allowed(
user.email,
allowed_emails=self.preapproved_emails,
@@ -619,6 +618,7 @@ class Group(AuthGroup):
self.preapproved_emails = ' ' + '\n'.join(emails) + ' '
domains = functions.split_list(self.preapproved_email_domains)
+ from askbot.forms import DomainNameField
domain_field = DomainNameField()
try:
map(lambda v: domain_field.clean(v), domains)
@@ -635,9 +635,13 @@ class Group(AuthGroup):
class BulkTagSubscriptionManager(BaseQuerySetManager):
def create(
- self, tag_names=None,
- user_list=None, group_list=None,
- tag_author=None, **kwargs
+ self,
+ tag_names=None,
+ user_list=None,
+ group_list=None,
+ tag_author=None,
+ language_code=None,
+ **kwargs
):
tag_names = tag_names or []
@@ -648,17 +652,20 @@ class BulkTagSubscriptionManager(BaseQuerySetManager):
tag_name_list = []
if tag_names:
- tags, new_tag_names = get_tags_by_names(tag_names)
+ from askbot.models.tag import get_tags_by_names
+ tags, new_tag_names = get_tags_by_names(tag_names, language_code)
if new_tag_names:
assert(tag_author)
tags_id_list= [tag.id for tag in tags]
tag_name_list = [tag.name for tag in tags]
+ from askbot.models.tag import Tag
new_tags = Tag.objects.create_in_bulk(
- tag_names=new_tag_names,
- user=tag_author
- )
+ tag_names=new_tag_names,
+ user=tag_author,
+ language_code=translation.get_language()
+ )
tags_id_list.extend([tag.id for tag in new_tags])
tag_name_list.extend([tag.name for tag in new_tags])
@@ -687,7 +694,7 @@ class BulkTagSubscriptionManager(BaseQuerySetManager):
class BulkTagSubscription(models.Model):
date_added = models.DateField(auto_now_add=True)
- tags = models.ManyToManyField(Tag)
+ tags = models.ManyToManyField('Tag')
users = models.ManyToManyField(User)
groups = models.ManyToManyField(Group)
diff --git a/askbot/patches/__init__.py b/askbot/patches/__init__.py
index 6145097c..947e1816 100644
--- a/askbot/patches/__init__.py
+++ b/askbot/patches/__init__.py
@@ -20,6 +20,14 @@ def patch_django():
if major == 1 and minor <=2:
django_patches.add_render_shortcut()
+ if major == 1 and minor > 4:
+ # This shouldn't be required with django < 1.4.x
+ # And not after kee_lazy lands in django.utils.functional
+ try:
+ from django.utils.functional import keep_lazy
+ except ImportError:
+ django_patches.fix_lazy_double_escape()
+
def patch_coffin():
"""coffin before version 0.3.4
does not have csrf_token template tag.
diff --git a/askbot/patches/django_patches.py b/askbot/patches/django_patches.py
index fe0e2fe7..5907d794 100644
--- a/askbot/patches/django_patches.py
+++ b/askbot/patches/django_patches.py
@@ -352,3 +352,33 @@ def add_render_shortcut():
import django.shortcuts
django.shortcuts.render = render
+
+
+from django.utils import six
+from django.utils.functional import Promise
+import django.utils.html
+
+def fix_lazy_double_escape():
+ """
+ Wrap django.utils.html.escape to fix the double escape issue visible at
+ least with field labels with localization
+ """
+ django.utils.html.escape = wrap_escape(django.utils.html.escape)
+
+
+def wrap_escape(func):
+ """
+ Decorator adapted from https://github.com/django/django/pull/1007
+ """
+ @wraps(func)
+ def wrapper(*args, **kwargs):
+ for arg in list(args) + list(six.itervalues(kwargs)):
+ if isinstance(arg, Promise):
+ break
+ else:
+ return func(*args, **kwargs)
+ return lazy(func, six.text_type)(*args, **kwargs)
+ @wraps(wrapper)
+ def wrapped(*args, **kwargs):
+ return mark_safe(func(*args, **kwargs))
+ return wrapped
diff --git a/askbot/schedules.py b/askbot/schedules.py
deleted file mode 100644
index b9bbdbc8..00000000
--- a/askbot/schedules.py
+++ /dev/null
@@ -1,19 +0,0 @@
-"""tests on whether certain scheduled tasks need
-to be performed at the moment"""
-from datetime import datetime
-
-def should_update_avatar_data(request):
- """True if it is time to update user's avatar data
- user is taken from the request object
- """
- user = request.user
- if user.is_authenticated():
- if (datetime.today() - user.last_login).days <= 1:
- #avatar is updated on login anyway
- return False
- updated_at = request.session.get('avatar_data_updated_at', None)
- if updated_at is None:
- return True
- else:
- return (datetime.now() - updated_at).days > 0
- return False
diff --git a/askbot/search/haystack/__init__.py b/askbot/search/haystack/__init__.py
index 05580bc4..77308180 100644
--- a/askbot/search/haystack/__init__.py
+++ b/askbot/search/haystack/__init__.py
@@ -32,7 +32,7 @@ class PostIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
post_text = indexes.CharField(model_attr='text')
author = indexes.CharField()
- thread_id = indexes.IntegerField(model_attr='thread__pk')
+ thread_id = indexes.IntegerField(model_attr='thread__pk', null=True)
def get_model(self):
diff --git a/askbot/search/state_manager.py b/askbot/search/state_manager.py
index 5c4e0776..5fede602 100644
--- a/askbot/search/state_manager.py
+++ b/askbot/search/state_manager.py
@@ -86,11 +86,13 @@ class SearchState(object):
@classmethod
def get_empty(cls):
- return cls(scope=None, sort=None, query=None, tags=None, author=None, page=None, user_logged_in=None)
+ return cls(scope=None, sort=None, query=None, tags=None, author=None, page=None, page_size=None, user_logged_in=None)
- def __init__(self, scope, sort, query, tags, author, page, user_logged_in):
+ def __init__(self,
+ scope=None, sort=None, query=None, tags=None,
+ author=None, page=None, page_size=None, user_logged_in=False
+ ):
# INFO: zip(*[('a', 1), ('b', 2)])[0] == ('a', 'b')
-
if (scope not in zip(*const.POST_SCOPE_LIST)[0]) or (scope == 'followed' and not user_logged_in):
if user_logged_in:
self.scope = askbot_settings.DEFAULT_SCOPE_AUTHENTICATED
@@ -137,6 +139,9 @@ class SearchState(object):
if self.page == 0: # in case someone likes jokes :)
self.page = 1
+ default_page_size = int(askbot_settings.DEFAULT_QUESTIONS_PAGE_SIZE)
+ self.page_size = int(page_size) if page_size else default_page_size
+
self._questions_url = urlresolvers.reverse('questions')
def __str__(self):
diff --git a/askbot/setup_templates/urls.py b/askbot/setup_templates/urls.py
index 21338a0a..ea6e3905 100644
--- a/askbot/setup_templates/urls.py
+++ b/askbot/setup_templates/urls.py
@@ -2,11 +2,12 @@
main url configuration file for the askbot site
"""
from django.conf import settings
-from django.conf.urls.defaults import handler404
-from django.conf.urls.defaults import handler500
-from django.conf.urls.defaults import include
-from django.conf.urls.defaults import patterns
-from django.conf.urls.defaults import url
+try:
+ from django.conf.urls import handler404, handler500
+ from django.conf.urls import include, patterns, url
+except ImportError:
+ from django.conf.urls.defaults import handler404, handler500
+ from django.conf.urls.defaults import include, patterns, url
from django.conf import settings
from django.contrib import admin
diff --git a/askbot/skins/loaders.py b/askbot/skins/loaders.py
index afdf758e..1a9e8edb 100644
--- a/askbot/skins/loaders.py
+++ b/askbot/skins/loaders.py
@@ -86,7 +86,8 @@ def load_skins(language_code):
skin_code = skin_name + '-' + language_code
skins[skin_code] = SkinEnvironment(
skin = skin_name,
- extensions=['jinja2.ext.i18n',]
+ extensions=['jinja2.ext.i18n',],
+ globals={'settings': askbot_settings}
)
skins[skin_code].set_language(language_code)
#from askbot.templatetags import extra_filters_jinja as filters
diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py
index e3888e53..3adbd781 100644
--- a/askbot/startup_procedures.py
+++ b/askbot/startup_procedures.py
@@ -1054,20 +1054,11 @@ def run_startup_tests():
if 'manage.py test' in ' '.join(sys.argv):
test_settings_for_test_runner()
-@transaction.commit_manually
+#@transaction.commit_manually
def run():
- """runs all the startup procedures"""
try:
if getattr(django_settings, 'ASKBOT_SELF_TEST', True):
run_startup_tests()
except AskbotConfigError, error:
- transaction.rollback()
print error
sys.exit(1)
- try:
- from askbot.models import badges
- badges.init_badges()
- transaction.commit()
- except Exception, error:
- print error
- transaction.rollback()
diff --git a/askbot/tasks.py b/askbot/tasks.py
index 3d3c57a7..ec3e405b 100644
--- a/askbot/tasks.py
+++ b/askbot/tasks.py
@@ -26,6 +26,7 @@ from django.contrib.contenttypes.models import ContentType
from django.template import Context
from django.template.loader import get_template
from django.utils.translation import ugettext as _
+from django.utils.translation import activate as activate_language
from django.utils import simplejson
from celery.decorators import task
from askbot.conf import settings as askbot_settings
@@ -95,7 +96,7 @@ def notify_author_of_published_revision_celery_task(revision):
if revision.post.post_type == 'question':
mailto_link_subject = revision.post.thread.title
else:
- mailto_link_subject = _('An edit for my answer')
+ mailto_link_subject = _('make an edit by email')
#todo: possibly add more mailto thread headers to organize messages
prompt = _('To add to your post EDIT ABOVE THIS LINE')
@@ -111,6 +112,7 @@ def notify_author_of_published_revision_celery_task(revision):
}
#load the template
+ activate_language(revision.post.language_code)
template = get_template('email/notify_author_about_approved_post.html')
#todo: possibly add headers to organize messages in threads
headers = {'Reply-To': append_content_address}
@@ -248,6 +250,7 @@ def send_instant_notifications_about_activity_in_post(
reply_address, alt_reply_address = get_reply_to_addresses(user, post)
+ activate_language(post.language_code)
subject_line, body_text = format_instant_notification_email(
to_user = user,
from_user = update_activity.user,
diff --git a/askbot/templates/404.html b/askbot/templates/404.html
index 565ff164..8388a42f 100644
--- a/askbot/templates/404.html
+++ b/askbot/templates/404.html
@@ -14,7 +14,7 @@
<div style="margin-top:5px">
{% trans %}This might have happened for the following reasons:{% endtrans %}<br/>
<ul>
- <li>{% trans %}this question or answer has been deleted;{% endtrans %}</li>
+ <li>{% trans %}This page has been deleted{% endtrans %}</li>
<li>{% trans %}url has error - please check it;{% endtrans %}</li>
<li>{% trans %}the page you tried to visit is protected or you don't have sufficient points, see{% endtrans %} <a href="{% url faq %}">{% trans %}faq{% endtrans %}</a>;</li>
<li>{% trans %}if you believe this error 404 should not have occured, please{% endtrans %}
@@ -26,11 +26,6 @@
var GOOG_FIXURL_SITE = '{{ site_url }}';
</script>
<script type="text/javascript" src="http://linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
- <ul>
- <li><a href="#" id="linkPrevious">{% trans %}back to previous page{% endtrans %} »</li>
- <li><a href="{% url questions %}">{% trans %}see all questions{% endtrans %} »</a></li>
- <li><a href="{% url tags %}">{% trans %}see all tags{% endtrans %} »</a></li>
- </u>
</div>
</div>
{% endblock %}
diff --git a/askbot/templates/500.html b/askbot/templates/500.html
index 297ae736..3cf6778b 100644
--- a/askbot/templates/500.html
+++ b/askbot/templates/500.html
@@ -6,20 +6,8 @@
<div id="main-body">
<div style="padding:5px 0px 10px 0;line-height:25px">
{% trans %}system error log is recorded, error will be fixed as soon as possible{% endtrans %}<br/>
- {% trans %}please report the error to the site administrators if you wish{% endtrans %}
- <ul>
- <li><a href="#" id="linkPrevious">{% trans %}back to previous page{% endtrans %}</li>
- <li><a href="{% url questions %}">{% trans %}see latest questions{% endtrans %}</a></li>
- <li><a href="{% url tags %}">{% trans %}see tags{% endtrans %}</a></li>
- </u>
+ {% trans %}please report the error to the site administrators{% endtrans %}
</div>
</div>
{% endblock %}
-{% block endjs %}
- <script type="text/javascript">
- $().ready(function(){
- $("#linkPrevious").bind("click", back=function(){history.go(-1);})
- });
- </script>
-{% endblock %}
<!-- end template 500.html -->
diff --git a/askbot/templates/answer_edit.html b/askbot/templates/answer_edit.html
index 3c6b7a4c..c49f3ccd 100644
--- a/askbot/templates/answer_edit.html
+++ b/askbot/templates/answer_edit.html
@@ -1,13 +1,14 @@
{% extends "two_column_body.html" %}
{% import "macros.html" as macros %}
<!-- template answer_edit.html -->
-{% block title %}{% spaceless %}{% trans %}Edit answer{% endtrans %}{% endspaceless %}{% endblock %}
+{% block title %}{% spaceless %}{{ settings.WORDS_EDIT_ANSWER|escape }}{% endspaceless %}{% endblock %}
{% block forestyle %}
<link rel="stylesheet" type="text/css" href="{{"/js/wmd/wmd.css"|media}}" />
{% endblock %}
{% block content %}
<div class="section-title">
- {% trans %}Edit answer{% endtrans %} [<a href="{{ answer.thread._question_post().get_absolute_url() }}#{{ answer.id }}">{% trans %}back{% endtrans %}</a>]
+{{ settings.WORDS_EDIT_ANSWER|escape }}
+[<a href="{{ answer.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]
</div>
<div id="main-body" class="ask-body">
<form id="fmedit" action="{% url edit_answer answer.id %}" method="post" >{% csrf_token %}
@@ -20,6 +21,10 @@
editor_type = settings.EDITOR_TYPE
)
}}
+ {% if form.recaptcha %}
+ <div>{{ macros.form_field_with_errors(form.recaptcha) }}</div>
+ <div class="clearfix"></div>
+ {% endif %}
<div class="answer-options">
{% if settings.WIKI_ON and answer.wiki == False %}
{{ macros.checkbox_in_div(form.wiki) }}
@@ -39,12 +44,11 @@
id="edit_post_form_submit_button"
type="submit"
value="{% trans %}Save edit{% endtrans %}"
- class="submit"
/>&nbsp;
<input
type="button"
value="{% trans %}Cancel{% endtrans %}"
- class="submit cancel"
+ class="cancel"
onclick="history.back(-1);"
/>
</div>
diff --git a/askbot/templates/ask.html b/askbot/templates/ask.html
index 64a8a260..bf713a70 100644
--- a/askbot/templates/ask.html
+++ b/askbot/templates/ask.html
@@ -13,29 +13,7 @@
{% endblock %}
{% block content %}
<div class="question-instructions">
- {% if settings.QUESTION_INSTRUCTIONS %}
- {{ settings.QUESTION_INSTRUCTIONS|safe }}
- {% else %}
- <ul>
- {% if not request.user.is_authenticated() %}
- <li class="warning">{% trans %}since you are not logged in right now, you will be asked to sign in or register after posting your question{% endtrans %}</li>
- {% else %}
- {% if settings.EMAIL_VALIDATION %}
- {% if not request.user.email_isvalid %}
- <li class="warning">{% trans email=request.user.email %}Your email, {{ email }} has not yet been validated. To post messages you must verify your email, please see <a href='{{ email_validation_faq_url }}'>more details here</a>. You can submit your question now and validate email after that. Meanwhile, your question will saved as pending.{% endtrans %}</li>
- {% endif %}
- {% endif %}
- {% endif %}
- <li>{% trans %}please, try to make your question interesting to this community{% endtrans %}</li>
- <li>{% trans %}provide enough details{% endtrans %}</li>
- <li>{% trans %}be clear and concise{% endtrans %}</li>
- </ul>
- {% endif %}
- <p class='info-box-follow-up-links'>
- <!-- will be change to a popup windows
- <a href="{% url faq %}" target="_blank" title="{% trans %}see frequently asked questions{% endtrans %}">{% trans %}FAQ{% endtrans %} »</a>
- -->
- </p>
+ {% include "widgets/question_edit_tips.html" %}
</div>
{% include "widgets/ask_form.html" %}
{% endblock %}
diff --git a/askbot/templates/authopenid/changeemail.html b/askbot/templates/authopenid/changeemail.html
deleted file mode 100644
index 37de8369..00000000
--- a/askbot/templates/authopenid/changeemail.html
+++ /dev/null
@@ -1,29 +0,0 @@
-{% extends "one_column_body.html" %}
-{% block title %}{% spaceless %}{% trans %}Change Email{% endtrans %}{% endspaceless %}{% endblock %}
-{% block content %}
-{% if action_type=="validate" %}
- <div id="main-bar" class="headNormal">
- {% trans %}Validate email{% endtrans %}
- </div>
- <p class="message">
- {% trans %}<span class=\"strong big\">An email with a validation link has been sent to
-{{ email }}.</span> Please <strong>follow the emailed link</strong> with your
-web browser. Email validation is necessary to help insure the proper use of
-email on <span class=\"orange\">Q&amp;A</span>. If you would like to use
-<strong>another email</strong>, please <a
-href='{{ change_email_url }}'><strong>change it again</strong></a>.{% endtrans %}
- </p>
-{% elif action_type=="validation_complete" %}
- <div id="main-bar" class="headNormal">
- {% trans %}Email verified{% endtrans %}
- </div>
- <p class="message">
- {% trans %}<span class=\"big strong\">Thank you for verifying your email!</span> Now
-you can <strong>ask</strong> and <strong>answer</strong> questions. Also if
-you find a very interesting question you can <strong>subscribe for the
-updates</strong> - then will be notified about changes <strong>once a day</strong>
-or less frequently.{% endtrans %}
- </p>
-{% endif %}
-{% endblock %}
-<!-- end changeemail.html -->
diff --git a/askbot/templates/authopenid/complete.html b/askbot/templates/authopenid/complete.html
index becd1517..cefb1941 100644
--- a/askbot/templates/authopenid/complete.html
+++ b/askbot/templates/authopenid/complete.html
@@ -19,8 +19,9 @@ parameters:
#}
{% block head %}{% endblock %}
{% block title %}{% spaceless %}{% trans %}Registration{% endtrans %}{% endspaceless %}{% endblock %}
+{% block body_class %}registration-page{% endblock %}
{% block content %}
- <h1>{% trans %}User registration{% endtrans %}</h1>
+ <h1 class="section-title">{% trans %}User registration{% endtrans %}</h1>
{% if openid_register_form.errors %}
<ul class="errorlist">
{% for error in openid_register_form.non_field_errors() %}
@@ -62,11 +63,15 @@ anyone, must be valid</i>)
{% endif %}
{{ openid_register_form.email }}
</div>
+ {% if openid_register_form.recaptcha %}
+ <div class="form-row-vertical">
+ {{ macros.form_field_with_errors(openid_register_form.recaptcha ) }}
+ </div>
+ {% endif %}
<div class="submit-row">
<input
id="register-button"
type="submit"
- class="submit"
name="bnewaccount"
value="{% trans %}Signup{% endtrans %}"
/>
diff --git a/askbot/templates/authopenid/providers_javascript.html b/askbot/templates/authopenid/providers_javascript.html
index bf9f2542..d20f052f 100644
--- a/askbot/templates/authopenid/providers_javascript.html
+++ b/askbot/templates/authopenid/providers_javascript.html
@@ -1,31 +1,35 @@
-<script type='text/javascript' src='{{"/js/jquery.validate.min.js"|media}}'></script>
-<script type="text/javascript" src="{{"/jquery-openid/jquery.openid.js"|media}}"></script>
+<script type='text/javascript' src='{{ "/js/jquery.validate.min.js"|media }}'></script>
+{% if settings.SIGNIN_MOZILLA_PERSONA_ENABLED %}
+<script type="text/javascript" src="https://login.persona.org/include.js"></script>
+{% endif %}
+<script type="text/javascript" src="{{ "/jquery-openid/jquery.openid.js"|media }}"></script>
<script type="text/javascript">
+ askbot['urls']['changePassword'] = '{% url change_password %}';
+ askbot['urls']['deleteLoginMethod'] = '{% url delete_login_method %}';
var extra_token_name = {};
var create_pw_text = {};
var change_pw_text = {};
- var authUrl = '/{% trans %}account/{% endtrans %}';
var siteName = '{{settings.APP_SHORT_NAME}}';
var provider_count = {{existing_login_methods|length}};
{% for login_provider in major_login_providers %}
{%if settings['SIGNIN_' + login_provider.name.upper() + '_ENABLED'] == True %}
{% if login_provider.extra_token_name %}
- extra_token_name['{{login_provider.name}}'] = '{{login_provider.extra_token_name}}';
+ extra_token_name['{{login_provider.name}}'] = '{{ login_provider.extra_token_name|escapejs }}';
{% endif %}
{% if login_provider.type == 'password' %}
- create_pw_text['{{login_provider.name}}'] = '{{login_provider.create_password_prompt}}';
- change_pw_text['{{login_provider.name}}'] = '{{login_provider.change_password_prompt}}';
+ create_pw_text['{{login_provider.name}}'] = '{{ login_provider.create_password_prompt|escapejs }}';
+ change_pw_text['{{login_provider.name}}'] = '{{ login_provider.change_password_prompt|escapejs }}';
{% endif %}
{% endif %}
{% endfor %}
{% for login_provider in minor_login_providers %}
{% if settings['SIGNIN_' + login_provider.name.upper() + '_ENABLED'] == True %}
- {% if login_provider.extra_token_name %}
- extra_token_name['{{login_provider.name}}'] = '{{login_provider.extra_token_name}}';
+{% if login_provider.extra_token_name %}
+ extra_token_name['{{login_provider.name}}'] = '{{ login_provider.extra_token_name|escapejs }}';
{% endif %}
{% if login_provider.type == 'password' %}
- create_pw_text['{{login_provider.name}}'] = '{{login_provider.create_password_prompt}}';
- change_pw_text['{{login_provider.name}}'] = '{{login_provider.change_password_prompt}}';
+ create_pw_text['{{login_provider.name}}'] = '{{ login_provider.create_password_prompt|escapejs }}';
+ change_pw_text['{{login_provider.name}}'] = '{{ login_provider.change_password_prompt|escapejs }}';
{% endif %}
{% endif %}
{% endfor %}
@@ -35,5 +39,10 @@
var userIsAuthenticated = false;
{% endif %}
askbot['settings']['signin_always_show_local_login'] = {% if settings.SIGNIN_ALWAYS_SHOW_LOCAL_LOGIN %}true{% else %}false{% endif %};
+ askbot['settings']['useLdapForPasswordLogin'] = {{ settings.USE_LDAP_FOR_PASSWORD_LOGIN|as_js_bool}};
$("body").authenticator();
+ (function() {
+ var form = new ChangePasswordForm();
+ form.decorate($('body'));//a hack
+ })();
</script>
diff --git a/askbot/templates/authopenid/signin.html b/askbot/templates/authopenid/signin.html
index d1034d84..c2717023 100644
--- a/askbot/templates/authopenid/signin.html
+++ b/askbot/templates/authopenid/signin.html
@@ -10,20 +10,6 @@
{% if have_buttons or view_subtype == 'email_sent' %}
<h1 class="section-title">{{page_title}}</h1>
{% endif %}
- {% if answer %}
- <div class="message">
- {% trans title=answer.question.title|escape, summary=answer.summary|escape %}
- Your answer to {{title}} {{summary}} will be posted once you log in
- {% endtrans %}
- </div>
- {% endif %}
- {% if question %}
- <div class="message">
- {% trans title=question.title|escape, summary=question.summary|escape %}Your question
- {{title}} {{summary}} will be posted once you log in
- {% endtrans %}
- </div>
- {% endif %}
{% if not (view_subtype == 'default' and have_buttons) %}
<p id='login-intro'>
{% if view_subtype == 'add_openid' and have_buttons %}
@@ -53,8 +39,12 @@
wants to always show the password login form - then
the button is useless.
#}
+ {% if 'in_recovery' in request.session %}
+ <input type="hidden" name="in_recovery" value="true" />
+ {% endif %}
{{ login_form.login_provider_name }}
{{ login_form.next }}
+ {{ login_form.persona_assertion }}
{{
login_macros.provider_buttons(
login_form = login_form,
@@ -84,7 +74,7 @@
{% endif %}
>
{{login_form.password_action}}
- {% if user.is_anonymous() %}
+ {% if user.is_anonymous() or settings.USE_LDAP_FOR_PASSWORD_LOGIN %}
{% if have_buttons %}
<h2 id="password-heading">
{% trans %}or enter your <span>user name and password</span>{% endtrans %}
@@ -103,7 +93,13 @@
</tr>
{% endif %}
<tr>
- <td><label for="id_username">{% trans %}Login or email{% endtrans %}</label></td>
+ <td><label for="id_username">
+ {% if settings.USE_LDAP_FOR_PASSWORD_LOGIN %}
+ {% trans %}Login{% endtrans %}
+ {% else %}
+ {% trans %}Login or email{% endtrans %}
+ {% endif %}
+ </label></td>
<td>{{login_form.username}}</td>
</tr>
<tr>
@@ -117,7 +113,7 @@
<a class="create-password-account" style="vertical-align:middle" href="{% url user_signup_with_password %}?login_provider=local">{% trans %}Create a password-protected account{% endtrans %}</a>
{% endif %}
</p>
- {% elif settings.USE_LDAP_FOR_PASSWORD_LOGIN == False %}
+ {% else %}{# change password form #}
<h2 id="password-heading">
{% trans %}To change your password - please enter the new one twice, then submit{% endtrans %}
</h2>
@@ -127,8 +123,8 @@
<td>
{{login_form.new_password}}
</td>
- <td>
- <span class="error">{{login_form.new_password.errors[0]}}</span>
+ <td style="text-align: left">
+ <span class="error new-password-errors">{{login_form.new_password.errors[0]}}</span>
</td>
</tr>
<tr>
@@ -136,8 +132,8 @@
<td>
{{login_form.new_password_retyped}}
</td>
- <td>
- <span class="error">{{login_form.new_password_retyped.errors[0]}}</span>
+ <td style="text-align: left">
+ <span class="error new-password-retyped-errors">{{login_form.new_password_retyped.errors[0]}}</span>
</td>
</tr>
</table>
@@ -196,9 +192,9 @@
<p class="hint">
<span class="text">
{% if view_subtype == 'bad_key' %}
- {% trans %}Please, enter your email address below and obtain a new key{% endtrans %}
+ {% trans %}Enter your email address below and obtain a new key{% endtrans %}
{% else %}
- {% trans %}Please, enter your email address below to recover your account{% endtrans %}
+ {% trans %}Enter your email address below to recover your account{% endtrans %}
{% endif %}
</span>
<span style="display:none" class="link"> - <a href="#">{% trans %}recover your account via email{% endtrans %}</a></span>
diff --git a/askbot/templates/authopenid/signup_with_password.html b/askbot/templates/authopenid/signup_with_password.html
index c3625b62..ef8d8572 100644
--- a/askbot/templates/authopenid/signup_with_password.html
+++ b/askbot/templates/authopenid/signup_with_password.html
@@ -38,9 +38,8 @@ your login details with anyone and having to remember yet another password.{% en
<li><label for="password1_id">{{form.password1.label}}</label>{{form.password1}}{{form.password1.errors}}</li>
<li><label for="password2_id">{{form.password2.label}}</label>{{form.password2}}{{form.password2.errors}}</li>
</ul>
- {% if settings.USE_RECAPTCHA %}
- <p class="signup_p">{% trans %}Please read and type in the two words below to help us prevent automated account creation.{% endtrans %}</p>
- {{form.recaptcha}}
+ {% if form.recaptcha %}
+ {{ main_macros.form_field_with_errors(form.recaptcha) }}
{% endif %}
<div class="submit-row"><input id="signup-button" type="submit" class="button" value="{% trans %}Signup{% endtrans %}" />
{% if settings.PASSWORD_REGISTER_SHOW_PROVIDER_BUTTONS == False %}
diff --git a/askbot/templates/authopenid/verify_email.html b/askbot/templates/authopenid/verify_email.html
index 613ca589..1312658b 100644
--- a/askbot/templates/authopenid/verify_email.html
+++ b/askbot/templates/authopenid/verify_email.html
@@ -8,7 +8,7 @@
</label>
<form method="post">{% csrf_token %}
<input id="validation-code" type="text" name="validation_code" />
- <input type="submit" class="submit" value="{% trans %}Confirm email{% endtrans %}" />
+ <input type="submit" value="{% trans %}Confirm email{% endtrans %}" />
</form>
{% endblock %}
<!-- end changeemail.html -->
diff --git a/askbot/templates/authopenid/widget_signin.html b/askbot/templates/authopenid/widget_signin.html
index 72860120..4cedd014 100644
--- a/askbot/templates/authopenid/widget_signin.html
+++ b/askbot/templates/authopenid/widget_signin.html
@@ -14,20 +14,6 @@
{% if have_buttons or view_subtype == 'email_sent' %}
<h1 class="section-title">{{page_title}}</h1>
{% endif %}
- {% if answer %}
- <div class="message">
- {% trans title=answer.question.title|escape, summary=answer.summary|escape %}
- Your answer to {{title}} {{summary}} will be posted once you log in
- {% endtrans %}
- </div>
- {% endif %}
- {% if question %}
- <div class="message">
- {% trans title=question.title|escape, summary=question.summary|escape %}Your question
- {{title}} {{summary}} will be posted once you log in
- {% endtrans %}
- </div>
- {% endif %}
<p id='login-intro'>
{% if view_subtype == 'default' and have_buttons %}
{% trans %}Choose your favorite service below to sign in using secure OpenID or similar technology. Your external service password always stays confidential and you don't have to rememeber or create another one.{% endtrans %}
@@ -199,9 +185,9 @@
<p class="hint">
<span class="text">
{% if view_subtype == 'bad_key' %}
- {% trans %}Please, enter your email address below and obtain a new key{% endtrans %}
+ {% trans %}Enter your email address below and obtain a new key{% endtrans %}
{% else %}
- {% trans %}Please, enter your email address below to recover your account{% endtrans %}
+ {% trans %}Enter your email address below to recover your account{% endtrans %}
{% endif %}
</span>
<span style="display:none" class="link"> - <a href="#">{% trans %}recover your account via email{% endtrans %}</a></span>
diff --git a/askbot/templates/badges.html b/askbot/templates/badges.html
index 112adc61..4f4d0973 100644
--- a/askbot/templates/badges.html
+++ b/askbot/templates/badges.html
@@ -4,7 +4,8 @@
{% block content %}
<h1 class="section-title">{% trans %}Badges{% endtrans %}</h1>
<p>
-{% trans %}Community gives you awards for your questions, answers and votes.{% endtrans %}<br/>
+{{ settings.WORDS_COMMUNITY_GIVES_YOU_AWARDS|escape }}
+<br/>
{% trans %}Below is the list of available badges and number of times each type of badge has been awarded.{% endtrans %}
</p>
<div id="medalList">
diff --git a/askbot/templates/close.html b/askbot/templates/close.html
index bac2b3ee..b0c239e4 100644
--- a/askbot/templates/close.html
+++ b/askbot/templates/close.html
@@ -1,9 +1,9 @@
{% extends "one_column_body.html" %}
<!-- template close.html -->
-{% block title %}{% spaceless %}{% trans %}Close question{% endtrans %}{% endspaceless %}{% endblock %}
+{% block title %}{% spaceless %}{{ settings.WORDS_CLOSE_QUESTION|escape }}{% endspaceless %}{% endblock %}
{% block content %}
-<h1>{% trans %}Close question{% endtrans %}</h1>
- <p>{% trans %}Close the question{% endtrans %}: <a href="{{ question.get_absolute_url() }}">
+<h1>{{ settings.WORDS_CLOSE_QUESTION|escape }}</h1>
+ <p>{{ settings.WORDS_CLOSE_QUESTION|escape }}: <a href="{{ question.get_absolute_url() }}">
<strong>{{ question.get_question_title()|escape }}</strong></a>
</p>
<form id="fmclose" action="{% url close question.id %}" method="post" >{% csrf_token %}
@@ -12,8 +12,8 @@
{{ form.reason }}
</p>
<p>
- <input type="submit" value="{% trans %}OK to close{% endtrans %}" class="submit" />&nbsp;
- <input id="btBack" type="button" class="submit" value="{% trans %}Cancel{% endtrans %}" />
+ <input type="submit" value="{% trans %}OK to close{% endtrans %}"/>
+ <input id="btBack" type="button" value="{% trans %}Cancel{% endtrans %}"/>
</p>
</form>
{% endblock %}
diff --git a/askbot/templates/email/change_settings_info.html b/askbot/templates/email/change_settings_info.html
index 55bd729f..0f96caa4 100644
--- a/askbot/templates/email/change_settings_info.html
+++ b/askbot/templates/email/change_settings_info.html
@@ -5,7 +5,6 @@
{% else %}
{% trans %}To change freqency and content of these alerts, please visit <a href="{{ url }}">your user profile</a>.{% endtrans %}
{% endif %}
-</p>
-<p style="font-size:10px; font-style:italic;">
+ <br/>
{% trans %}If you believe that this message was sent in an error, please email about it the forum administrator at <a href="mailto:{{ admin_email }}">{{ admin_email }}</a>.{% endtrans %}
</p>
diff --git a/askbot/templates/email/delayed_email_alert.html b/askbot/templates/email/delayed_email_alert.html
index 3ccacea4..252a2337 100644
--- a/askbot/templates/email/delayed_email_alert.html
+++ b/askbot/templates/email/delayed_email_alert.html
@@ -1,6 +1,6 @@
{% block content %}
<p>{% trans %}Dear {{ name }},{% endtrans %}</p>
-<p>{% trans num=questions|length %}The following question has been updated {{ site_name }}:{% pluralize num %}The following {{ num }} questions have been updated on {{ site_name }}:{% endtrans %}</p>
+<p>{% trans %}{{ site_name }} has these updates, please have a look:{% endtrans %}</p>
<ul>
{% for q in questions %}
<li>
diff --git a/askbot/templates/email/insufficient_rep_to_post_by_email.html b/askbot/templates/email/insufficient_rep_to_post_by_email.html
index df7f797a..deabfbbd 100644
--- a/askbot/templates/email/insufficient_rep_to_post_by_email.html
+++ b/askbot/templates/email/insufficient_rep_to_post_by_email.html
@@ -7,12 +7,12 @@
* site_link - html for the link
#}
{% block headline %}
- {% trans user=username|escape %}{{ username }}, your question could not be posted by email just yet.{% endtrans %}
+ {% trans user=username|escape %}{{ username }}, your content could not be posted by email just yet.{% endtrans %}
{%endblock%}
{% block content %}
<p>
{% trans %}To make posts by email, you need to receive about {{min_upvotes}} upvotes.{% endtrans %}<br/>
- {% trans link=site_link|safe %}At this time, please post your question at {{link}}{% endtrans %}
+ {% trans link=site_link|safe %}At this time, please post your content at {{link}}{% endtrans %}
</p>
{% endblock %}
{% block footer %}
diff --git a/askbot/templates/email/macros.html b/askbot/templates/email/macros.html
index 77345a45..c5b7cfcc 100644
--- a/askbot/templates/email/macros.html
+++ b/askbot/templates/email/macros.html
@@ -1,60 +1,73 @@
+{%- macro mailto_link(user) -%}
+ <a href="mailto:{{ user.email }}">{{ user.email }}</a>
+{%- endmacro -%}
+
{% macro quoted_post(
- post = None,
- quote_level = 0,
- format = None,
- is_leaf_post = False
+ post=None,
+ recipient=None,
+ quote_level=0,
+ format=None,
+ is_leaf_post=False
)
%}
{% spaceless %}
{{ start_quote(quote_level) }}
- {% set author = post.author.username|escape %}
- {% if post.post_type == 'question' %}
- {% if quote_level > 0 %}
- <p style="font-size:10px; font-weight: bold;">
+
+ {% if recipient|can_see_private_user_data(post.author) %}
+ {% set author = post.author.username|escape ~ '/(' ~ mailto_link(post.author) ~ ')' %}
+ {% elif post.is_anonymous %}
+ {% set author = post.author.get_name_of_anonymous_user()|escape %}
{% else %}
- <p style="font-size:20px; font-weight: bold; margin: 10px 0">
+ {% set author = post.author.username|escape %}
{% endif %}
- {% if format == 'parent_subthread' %}
- {% if is_leaf_post %}
- {% trans %}Question by {{ author }}:{% endtrans %}
+
+ {% if post.post_type == 'question' %}
+ {% if quote_level > 0 %}
+ <p style="font-size:10px; font-weight: bold;">
+ {% else %}
+ <p style="font-size:20px; font-weight: bold; margin: 10px 0">
+ {% endif %}
+ {% if format == 'parent_subthread' %}
+ {% if is_leaf_post %}
+ {% trans %}Started by {{ author }}:{% endtrans %}
+ {% else %}
+ {% trans -%}
+ In reply to {{ author }}:
+ {%- endtrans %}
+ {% endif %}
{% else %}
- {% trans -%}
- In reply to {{ author }}'s question:
- {%- endtrans %}
+ {{ settings.WORDS_QUESTION_SINGULAR|title|escape }} :
{% endif %}
- {% else %}
- {% trans %}Question :{% endtrans %}
- {% endif %}
- {{ post.thread.title }}
- </p>
- {% if quote_level > 0 %}
- <p style="font-size:10px; font-weight: bold;">
- {% if format != 'parent_subthread' %}
- {% trans %}Asked by {{ author }}:{% endtrans %}
- {% endif %}
- </p>
- {% endif %}
- {% set tag_names = post.get_tag_names() %}
- {% if tag_names %}
- <p style="font-size:10px; font-style:italic;">
- {% trans %}Tags:{% endtrans %}
- {{ tag_names|join(', ') }}.
- </p>
- {% endif %}
+ {{ post.thread.title }}
+ </p>
+ {% if quote_level > 0 %}
+ <p style="font-size:10px; font-weight: bold;">
+ {% if format != 'parent_subthread' %}
+ {% trans %}Started by {{ author }}:{% endtrans %}
+ {% endif %}
+ </p>
+ {% endif %}
+ {% set tag_names = post.get_tag_names() %}
+ {% if tag_names %}
+ <p style="font-size:10px; font-style:italic;">
+ {% trans %}Tags:{% endtrans %}
+ {{ tag_names|join(', ') }}.
+ </p>
+ {% endif %}
{% elif post.post_type == 'answer' %}
<p style="font-size:10px; font-weight: bold;">
{% if format == 'parent_subthread' %}
{% if is_leaf_post %}
{% trans -%}
- {{ author }}'s answer:
+ {{ author }}'s response:
{%- endtrans %}
{% else %}
{% trans -%}
- In reply to {{ author }}'s answer:
+ In reply to {{ author }}:
{%- endtrans %}
{% endif %}
{% else %}
- {% trans %}Answered by {{ author }}:{% endtrans %}
+ {% trans %}Replied by {{ author }}:{% endtrans %}
{% endif %}
</p>
{% else %}
diff --git a/askbot/templates/email/post_as_subthread.html b/askbot/templates/email/post_as_subthread.html
index 08b35ac5..5c08247f 100644
--- a/askbot/templates/email/post_as_subthread.html
+++ b/askbot/templates/email/post_as_subthread.html
@@ -1,7 +1,7 @@
{% from "email/macros.html" import quoted_post %}
{% if post.post_type in ('question', 'answer') %}
- {{ quoted_post(post) }}
+ {{ quoted_post(post, recipient=recipient) }}
{% set comments = post.get_cached_comments() %}
{% if comments %}
<p>
@@ -12,7 +12,7 @@
{%- endtrans -%}
</p>
{% for comment in comments %}
- {{ quoted_post(comment, quote_level = 1) }}
+ {{ quoted_post(comment, quote_level=1, recipient=recipient) }}
{% endfor %}
{% endif %}
{% endif %}
diff --git a/askbot/templates/email/quoted_post.html b/askbot/templates/email/quoted_post.html
index ecc20ad9..2c3b6046 100644
--- a/askbot/templates/email/quoted_post.html
+++ b/askbot/templates/email/quoted_post.html
@@ -1,5 +1,9 @@
{% from "email/macros.html" import quoted_post %}
{{ quoted_post(
- post, quote_level, is_leaf_post = is_leaf_post, format = format
+ post=post,
+ quote_level=quote_level,
+ recipient=recipient,
+ is_leaf_post=is_leaf_post,
+ format=format
)
}}
diff --git a/askbot/templates/email/re_welcome_lamson_on.html b/askbot/templates/email/re_welcome_lamson_on.html
index f30345d1..af050960 100644
--- a/askbot/templates/email/re_welcome_lamson_on.html
+++ b/askbot/templates/email/re_welcome_lamson_on.html
@@ -4,7 +4,7 @@
{% block content %}
{% if can_post_by_email %}
- <p>{% trans %}You can post questions by emailing them at {{ ask_address }}.{% endtrans %}</p>
+ <p>{{ settings.WORDS_YOU_CAN_POST_QUESTIONS_BY_EMAILING_THEM_AT|escape }} {{ ask_address }}</p>
<p>{% trans %}When you receive update notifications, you will be able to respond to them, also by email.{% endtrans %}</p>
<p>{% trans %}Of course, you can always visit the {{ site_name }} at <a href="{{ site_url }}">{{ site_url }}</a>.{% endtrans %}</p>
{% else %}
diff --git a/askbot/templates/email/welcome_lamson_on.html b/askbot/templates/email/welcome_lamson_on.html
index 3e47f44f..898e4b3d 100644
--- a/askbot/templates/email/welcome_lamson_on.html
+++ b/askbot/templates/email/welcome_lamson_on.html
@@ -11,7 +11,7 @@ of the email code to detect the response signature that will appear under #}
{% trans %}Important: <em>Please reply</em> to this message, without editing it. We need this to determine your email signature and that the email address is valid and was typed correctly.{% endtrans %}
</p>
<p>
- {% trans %}Until we receive the response from you, you will not be able ask or answer questions on {{ site_name }} by email.{% endtrans %}
+ {% trans %}Until we receive the response from you, you will not be able to post content on {{ site_name }} by email.{% endtrans %}
</p>
{% endblock %}
{%block footer %}
diff --git a/askbot/templates/embed/ask_by_widget.html b/askbot/templates/embed/ask_by_widget.html
index fb38ad64..949284e1 100644
--- a/askbot/templates/embed/ask_by_widget.html
+++ b/askbot/templates/embed/ask_by_widget.html
@@ -167,7 +167,7 @@
<div class="title">{{widget.title}}</div>
<form action="." method="POST" accept-charset="utf-8">
{% csrf_token %}
- <label>{%trans%}Please enter your question{%endtrans%}</label>
+ <label>{{ settings.WORDS_PLEASE_ENTER_YOUR_QUESTION|escape }}</label>
<div class="input-title">
{{form.title}}
</div>
@@ -195,7 +195,7 @@
{% if form.ask_anonymously %}
<p>{{form.ask_anonymously.label_tag()}}: {{form.ask_anonymously}}</p>
{%endif%}
- <input type="submit" value="Ask your question" id="submit" />
+ <input type="submit" value="{{ settings.WORDS_ASK_YOUR_QUESTION|escape }}" id="submit" />
</form>
{%endblock%}
{% block endjs %}
diff --git a/askbot/templates/embed/widget_form.html b/askbot/templates/embed/widget_form.html
index ad1562aa..a03aba29 100644
--- a/askbot/templates/embed/widget_form.html
+++ b/askbot/templates/embed/widget_form.html
@@ -11,7 +11,7 @@
{{ form.as_table() }}
<tr>
<td colspan="2" style="text-align: center">
- <input type="submit" class="submit" value={% trans %}Save{% endtrans %} />
+ <input type="submit" value={% trans %}Save{% endtrans %} />
</td>
</tr>
</table>
diff --git a/askbot/templates/embed/widgets.html b/askbot/templates/embed/widgets.html
index 767ebc2c..7fcfa94b 100644
--- a/askbot/templates/embed/widgets.html
+++ b/askbot/templates/embed/widgets.html
@@ -13,7 +13,7 @@
</thead>
<tbody>
<tr>
- <td>{% trans %}Ask a question{% endtrans %}</td>
+ <td>"{{ settings.WORDS_ASK_YOUR_QUESTION|escape }}"</td>
<td><a href="{% url create_widget 'ask' %}">{% trans %}create{% endtrans %}</a></td>
<td>
{% if ask_widgets > 0 %}
@@ -22,7 +22,7 @@
</td>
</tr>
<tr>
- <td>{% trans %}List of questions{% endtrans %}</td>
+ <td>{{ settings.WORDS_LIST_OF_QUESTIONS|escape }}</td>
<td><a href="{% url create_widget 'question' %}">{% trans %}create{% endtrans %}</a></td>
<td>
{% if question_widgets > 0 %}
diff --git a/askbot/templates/faq_static.html b/askbot/templates/faq_static.html
index 3a2638be..20a090d2 100644
--- a/askbot/templates/faq_static.html
+++ b/askbot/templates/faq_static.html
@@ -18,7 +18,7 @@
{% trans %}Karma system allows users to earn rights to perform a variety of moderation tasks{% endtrans %}
</p>
<h2>{% trans %}How does karma system work?{% endtrans %}</h2>
-<p>{% trans %}When a question or answer is upvoted, the user who posted them will gain some points, which are called \"karma points\". These points serve as a rough measure of the community trust to him/her. Various moderation tasks are gradually assigned to the users based on those points.{% endtrans %}</p>
+<p>{% trans %}When a question or answer is upvoted, the user who posted them will gain some points, which are called "karma points". These points serve as a rough measure of the community trust to him/her. Various moderation tasks are gradually assigned to the users based on those points.{% endtrans %}</p>
<p>{% trans MAX_REP_GAIN_PER_USER_PER_DAY=settings.MAX_REP_GAIN_PER_USER_PER_DAY, REP_GAIN_FOR_RECEIVING_UPVOTE=settings.REP_GAIN_FOR_RECEIVING_UPVOTE, REP_LOSS_FOR_RECEIVING_DOWNVOTE=settings.REP_LOSS_FOR_RECEIVING_DOWNVOTE|absolute_value %}For example, if you ask an interesting question or give a helpful answer, your input will be upvoted. On the other hand if the answer is misleading - it will be downvoted. Each vote in favor will generate <strong>{{REP_GAIN_FOR_RECEIVING_UPVOTE}}</strong> points, each vote against will subtract <strong>{{REP_LOSS_FOR_RECEIVING_DOWNVOTE}}</strong> points. There is a limit of <strong>{{MAX_REP_GAIN_PER_USER_PER_DAY}}</strong> points that can be accumulated for a question or answer per day. The table below explains reputation point requirements for each type of moderation task.{% endtrans %}
</p>
@@ -40,14 +40,11 @@
<tr>
<td class="faq-rep-item"><strong>{{settings.MIN_REP_TO_VOTE_DOWN}}</strong></td>
<td>{% trans %}downvote{% endtrans %}</td>
- </tr><tr>
+ </tr>
+ <tr>
<td class="faq-rep-item"><strong>{{settings.MIN_REP_TO_ACCEPT_OWN_ANSWER}}</strong></td>
<td>{% trans %} accept own answer to own questions{% endtrans %}</td>
</tr>
- </tr><tr>
- <td class="faq-rep-item"><strong>{{settings.MIN_REP_TO_CLOSE_OWN_QUESTIONS}}</strong></td>
- <td>{% trans %}open and close own questions{% endtrans %}</td>
- </tr>
<tr>
<td class="faq-rep-item"><strong>{{settings.MIN_REP_TO_RETAG_OTHERS_QUESTIONS}}</strong></td>
<td>{% trans %}retag other's questions{% endtrans %}</td>
diff --git a/askbot/templates/feedback.html b/askbot/templates/feedback.html
index 04b9a5b4..040566c6 100644
--- a/askbot/templates/feedback.html
+++ b/askbot/templates/feedback.html
@@ -1,4 +1,5 @@
{% extends "two_column_body.html" %}
+{% import "macros.html" as macros %}
<!-- template feedback.html -->
{% block title %}{% spaceless %}{% trans %}Feedback{% endtrans %}{% endspaceless %}{% endblock %}
{% block content %}
@@ -51,17 +52,13 @@
</div>
{% if form.recaptcha %}
<div class="form-row">
- {% if form.errors.recaptcha%}
- <span class="error">{% trans %}(Please solve the captcha){% endtrans %}</span>
- </label>
- {% endif %}
- {{form.recaptcha}}
+ {{ macros.form_field_with_errors(form.recaptcha) }}
</div>
{% endif %}
{{form.next}}
<div class="submit-row">
- <input type="submit" class="submit" value="{% trans %}Send Feedback{% endtrans %}"/>&nbsp;
- <input type="submit" class="submit cancel" name="cancel" value="{% trans %}Cancel{% endtrans %}"/>
+ <input type="submit" value="{% trans %}Send Feedback{% endtrans %}"/>&nbsp;
+ <input type="submit" class="cancel" name="cancel" value="{% trans %}Cancel{% endtrans %}"/>
</div>
</form>
{% endblock %}
diff --git a/askbot/templates/flatpages/default.html b/askbot/templates/flatpages/default.html
new file mode 100644
index 00000000..6038e6be
--- /dev/null
+++ b/askbot/templates/flatpages/default.html
@@ -0,0 +1,7 @@
+{% extends "one_column_body.html" %}
+{% block title %}{{ flatpage.title }}{% endblock %}
+{% block body_class %}flat-page{% endblock %}
+{% block content %}
+<h1 class="section-title">{{ flatpage.title }}</h1>
+{{ flatpage.content }}
+{% endblock %}
diff --git a/askbot/templates/list_suggested_tags.html b/askbot/templates/list_suggested_tags.html
index 660c8308..7c69ddc3 100644
--- a/askbot/templates/list_suggested_tags.html
+++ b/askbot/templates/list_suggested_tags.html
@@ -11,7 +11,7 @@
<th class="tags-col">{% trans %}Tag{% endtrans %}</th>
<th class="users-col">{% trans %}Suggested by{% endtrans %}</th>
<th class="decision-col">{% trans %}Your decision{% endtrans %}</th>
- <th>{% trans %}Suggested tag was used for questions{% endtrans %}</th>
+ <th>{% trans %}Where the tag was used{% endtrans %}</th>
</tr>
</thead>
<tbody>
@@ -35,7 +35,7 @@
<button class="reject">{% trans %}Reject{% endtrans %}</button>
</td>
<td class="thread-links-col">
- <span>{% trans %}There are no questions with this tag yet{% endtrans %}</span>
+ <span>{% trans %}This tag has not yet been used{% endtrans %}</span>
</td>
</tr>
{% else %}
@@ -46,8 +46,7 @@
<button class="reject">{% trans %}Reject{% endtrans %}</button>
</td>
<td class="thread-links-col">
- <a title="{{ thread._question_post().summary|escape }}"
- href="{{ thread.get_absolute_url() }}"
+ <a href="{{ thread.get_absolute_url() }}"
>{{ thread.title|escape }}</a>
</td>
</tr>
@@ -59,7 +58,7 @@
<tr class="per-tag-controls" data-tag-id="{{ tag.id }}">
<td colspan="4">
{% if tag.threads.count() > 1 %}
- <button class="accept">{% trans name=tag.name %}Apply tag "{{ name }}" to all above questions{% endtrans %}</button>
+ <button class="accept">{% trans name=tag.name %}Apply tag "{{ name }}" to the above{% endtrans %}</button>
<button class="reject">{% trans %}Reject tag{% endtrans %}</button>
{% endif %}
</td>
diff --git a/askbot/templates/macros.html b/askbot/templates/macros.html
index 4b392abe..b314255c 100644
--- a/askbot/templates/macros.html
+++ b/askbot/templates/macros.html
@@ -2,7 +2,7 @@
{%- macro share(site = None, site_label = None, icon = False) -%}
<a class="{{ site }}-share{% if icon == True %} icon{% endif %}"
- title="{% trans %}Share this question on {{site}}{% endtrans %}"
+ title="{% trans %}Share this content on {{site}}{% endtrans %}"
>{% if icon == False %}{% if site_label %}{{ site_label }}{% else %}{{ site }}{% endif %}{% endif %}</a>
{%- endmacro -%}
@@ -59,7 +59,9 @@
{% else %}
{{ user_card(user, karma_mode=karma_mode, badges_mode=badges_mode) }}
{% endif %}
- {{ user_primary_group(user) }}
+ {% if settings.GROUPS_ENABLED %}
+ {{ user_primary_group(user) }}
+ {% endif %}
{%- endmacro -%}
{%- macro post_last_updater_and_creator_info(
@@ -88,11 +90,11 @@ poor design of the data or methods on data objects #}
{% if is_wiki %}
<p>
{%- if post.post_type == 'question' -%}
- {%- trans %}asked{% endtrans %}
+ {{ settings.WORDS_ASKED|escape }}
{% elif post.post_type == 'answer' %}
- {%- trans %}answered{% endtrans %}
+ {{ settings.WORDS_ANSWERED|escape }}
{% else %}
- {%- trans %}posted{% endtrans %}
+ {%- trans %}posted{% endtrans %}
{% endif %}
<strong>{{ timeago(post.added_at) }}</strong>
</p>
@@ -107,9 +109,9 @@ poor design of the data or methods on data objects #}
<p style="line-height:12px;">
{# todo: access to class names needs to be removed here #}
{% if post.post_type == 'question' %}
- {% trans %}asked{% endtrans %}
+ {{ settings.WORDS_ASKED|escape }}
{% elif post.post_type == 'answer' %}
- {% trans %}answered{% endtrans %}
+ {{ settings.WORDS_ANSWERED|escape }}
{% else %}
{% trans %}posted{% endtrans %}
{% endif %}
@@ -311,7 +313,6 @@ poor design of the data or methods on data objects #}
class="tag tag-right{% if css_class %} {{ css_class }}{% endif %}"
{% if is_link %}
href="{{ search_state.add_tag(tag).full_url() }}"
- title="{% trans tag=tag|escape %}see questions tagged '{{ tag }}'{% endtrans %}"
{% endif %}
rel="tag"
data-tag-name="{{ tag|replace('*', '&#10045;')|escape }}"
@@ -387,7 +388,8 @@ for the purposes of the AJAX comment editor #}
show_comment = None,
show_comment_position = None,
user=None,
- max_comments=None
+ max_comments=None,
+ csrf_token=None
)
-%}
{% spaceless %}
@@ -406,7 +408,10 @@ for the purposes of the AJAX comment editor #}
{% for comment in comments %}
{# Warning! Any changes to the comment markup IN THIS `FOR` LOOP must be duplicated in post.js
for the purposes of the AJAX comment editor #}
- <div class="comment" id="comment-{{comment.id}}">
+ <div class="comment"
+ id="comment-{{comment.id}}"
+ data-post-id="{{ comment.id }}"
+ >
<div class="comment-votes">
{% if comment.score > 0 %}
<div
@@ -431,7 +436,7 @@ for the purposes of the AJAX comment editor #}
></span>
</div>
<div class="comment-body">
- {{comment.html}}
+ {{ comment.summary }}
<a
class="author"
href="{{comment.author.get_profile_url()}}"
@@ -447,7 +452,7 @@ for the purposes of the AJAX comment editor #}
accept-charset="utf-8"
class='convert-comment'
>
- {% csrf_token %}
+ <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
<input type="hidden" value="{{comment.id}}" name="comment_id" id="id_comment_id">
<input type="submit" value="{% trans %}convert to answer{% endtrans %}">
</form>
@@ -639,7 +644,7 @@ for the purposes of the AJAX comment editor #}
{%- macro user_full_location(user) -%}
{% if user.location %}
- {{ user.location }},
+ {{ user.location|escape }},
{% endif %}
{{ user_country_name_and_flag(user) }}
{%- endmacro -%}
@@ -683,42 +688,71 @@ for the purposes of the AJAX comment editor #}
{%- macro paginator(p, position='left', anchor='') -%}{# p is paginator context dictionary #}
{% spaceless %}
{% if p.is_paginated %}
- <div class="paginator" style="float:{{position}}">
- {% if p.has_previous %}
- <span class="prev"><a href="{{p.base_url}}page={{ p.previous }}{{ anchor }}" title="{% trans %}previous{% endtrans %}">
- &laquo; {% trans %}previous{% endtrans %}</a></span>
- {% endif %}
- {% if not p.in_leading_range %}
- {% for num in p.pages_outside_trailing_range %}
- <span class="page"><a href="{{p.base_url}}page={{ num }}{{ anchor }}" >{{ num }}</a></span>
- {% endfor %}
- ...
- {% endif %}
+ <div
+ class="paginator"
+ style="float:{{ position }}"
+ data-num-pages="{{ p.pages }}"
+ >
+ <span class="prev"
+ {% if not p.has_previous %}style="display: none"{% endif %}
+ ><a href="{{p.base_url}}page={{ p.previous }}{{ anchor }}"
+ title="{% trans %}previous{% endtrans %}"
+ >&laquo; {% trans %}previous{% endtrans %}</a></span>
+
+ <span class="first-page-nav"
+ {% if p.in_leading_range %}style="display: none"{% endif %}
+ >
+ <span class="page first-page"
+ data-page="1"
+ >
+ <a href="{{p.base_url}}page={{1}}{{ anchor }}" >1</a>
+ </span>
+ <span class="leading-sep">...</span>
+ </span>
+ <span class="main-pages-nav">
{% for num in p.page_numbers %}
- {% if num == p.page and p.pages != 1%}
- <span class="curr" title="{% trans %}current page{% endtrans %}">{{ num }}</span>
- {% else %}
- <span class="page"><a href="{{p.base_url}}page={{ num }}{{ anchor }}" title="{% trans %}page {{num}}{% endtrans %}">{{ num }}</a></span>
- {% endif %}
+ {% if num == p.page %}
+ <span class="curr page"
+ data-page="{{ num }}"
+ title="{% trans %}current page{% endtrans %}"
+ ><a>{{ num }}</a></span>
+ {% else %}
+ <span class="page"
+ data-page="{{ num }}"
+ ><a href="{{p.base_url}}page={{ num }}{{ anchor }}"
+ title="{% trans %}page {{num}}{% endtrans %}"
+ >{{ num }}</a></span>
+ {% endif %}
{% endfor %}
-
- {% if not p.in_trailing_range %}
- ...
- {% for num in p.pages_outside_leading_range|reverse %}
- <span class="page"><a href="{{p.base_url}}page={{ num }}{{ anchor }}" title="{% trans %}page {{ num }}{% endtrans %}">{{ num }}</a></span>
- {% endfor %}
- {% endif %}
- {% if p.has_next %}
- <span class="next"><a href="{{p.base_url}}page={{ p.next }}{{ anchor }}" title="{% trans %}next page{% endtrans %}">{% trans %}next page{% endtrans %} &raquo;</a></span>
- {% endif %}
- </div>
+ </span>
+
+ {# the "last page" navigation #}
+ <span class="last-page-nav"
+ {% if p.in_trailing_range %}style="display: none"{% endif %}
+ >
+ <span class="trailing-sep">...</span>
+ <span class="page last-page" data-page="{{ p.pages }}">
+ <a href="{{ p.base_url }}page={{ p.pages }}{{ anchor }}"
+ title="{% trans num=p.pages %}page {{ num }}{% endtrans %}"
+ >{{ p.pages }}</a>
+ </span>
+ <span>
+
+ {# "next" nav link #}
+ <span class="next"
+ {% if not p.has_next %}style="display: none"{% endif %}
+ ><a href="{{p.base_url}}page={{ p.next }}{{ anchor }}"
+ title="{% trans %}next page{% endtrans %}"
+ >{% trans %}next{% endtrans %} &raquo;</a>
+ </span>
+
+ </div>
{% endif %}
{% endspaceless %}
{%- endmacro -%}
-
{%- macro paginator_main_page(p, position, search_state) -%} {# p is paginator context dictionary #}
{% spaceless %}
{% if p.is_paginated %}
@@ -736,7 +770,7 @@ for the purposes of the AJAX comment editor #}
{% for num in p.page_numbers %}
{% if num == p.page and p.pages != 1%}
- <span class="curr" title="{% trans %}current page{% endtrans %}">{{ num }}</span>
+ <span class="curr page" title="{% trans %}current page{% endtrans %}"><a>{{ num }}</a></span>
{% else %}
<span class="page"><a href="{{ search_state.change_page(num).full_url() }}" title="{% trans %}page {{num}}{% endtrans %}">{{ num }}</a></span>
{% endif %}
@@ -814,3 +848,10 @@ for the purposes of the AJAX comment editor #}
})();
</script>
{% endmacro %}
+
+{% macro form_field_with_errors(field) %}
+ {% if field.errors %}
+ <p class="error">{{ field.errors|join(", ") }}</p>
+ {% endif %}
+ {{ field }}
+{% endmacro %}
diff --git a/askbot/templates/main_page.html b/askbot/templates/main_page.html
index 207e064d..19c447a2 100644
--- a/askbot/templates/main_page.html
+++ b/askbot/templates/main_page.html
@@ -29,6 +29,10 @@
<script type="text/javascript">
{# cant cache this #}
askbot['settings']['showSortByRelevance'] = {{ show_sort_by_relevance|as_js_bool }};
+ askbot['messages']['questionSingular'] = '{{ settings.WORDS_QUESTION_SINGULAR|escapejs }}';
+ askbot['messages']['answerSingular'] = '{{ settings.WORDS_ANSWER_SINGULAR|escapejs }}';
+ askbot['messages']['acceptOwnAnswer'] = '{{ settings.WORDS_ACCEPT_OR_UNACCEPT_OWN_ANSWER|escapejs }}';
+ askbot['messages']['followQuestions'] = '{{ settings.WORDS_FOLLOW_QUESTIONS|escapejs }}';
</script>
{% include "main_page/javascript.html" %}
{% include "main_page/custom_javascript.html" ignore missing %}
diff --git a/askbot/templates/main_page/headline.html b/askbot/templates/main_page/headline.html
index 11f638e1..f597a114 100644
--- a/askbot/templates/main_page/headline.html
+++ b/askbot/templates/main_page/headline.html
@@ -1,7 +1,7 @@
{% import "macros.html" as macros %}
{% if questions_count > 0 %}
<h1 id="questionCount" class="search-result-summary">
- {% trans cnt=questions_count, q_num=questions_count|intcomma %}{{q_num}} question{% pluralize %}{{q_num}} questions{% endtrans %}
+ {{ questions_count|intcomma }} {{ settings.WORDS_QUESTIONS_COUNTABLE_FORMS|py_pluralize(questions_count) }}
{% if author_name %}
{% trans %}with {{author_name}}'s contributions{% endtrans %}
{% endif %}
diff --git a/askbot/templates/main_page/javascript.html b/askbot/templates/main_page/javascript.html
index 24182708..3a3c3d23 100644
--- a/askbot/templates/main_page/javascript.html
+++ b/askbot/templates/main_page/javascript.html
@@ -6,10 +6,6 @@
Hilite.exact = false;
Hilite.elementid = "question-list";
Hilite.debug_referrer = location.href;
- {% if update_avatar_data == True %}
- var today = new Date();{#add timestamp to prevent browser caching #}
- $.getJSON('{% url user_update_has_custom_avatar %}?t=' + today.getTime());
- {% endif %}
});
askbot['urls']['mark_interesting_tag'] = '{% url mark_interesting_tag %}';
@@ -17,6 +13,7 @@
askbot['urls']['mark_subscribed_tag'] = '{% url mark_subscribed_tag %}';
askbot['urls']['unmark_tag'] = '{% url unmark_tag %}';
askbot['urls']['set_tag_filter_strategy'] = '{% url "set_tag_filter_strategy" %}';
+ askbot['settings']['tag_editor'] = '{{ tag_editor_settings|escapejs }}';
if (Modernizr.history) {
// history management works!
diff --git a/askbot/templates/main_page/nothing_found.html b/askbot/templates/main_page/nothing_found.html
index 98629476..73497df0 100644
--- a/askbot/templates/main_page/nothing_found.html
+++ b/askbot/templates/main_page/nothing_found.html
@@ -1,11 +1,11 @@
{# todo: add tips to widen selection #}
<p class="evenMore" style="padding-top:30px;text-align:center;">
{% if search_state.scope == "unanswered" %}
- {% trans %}There are no unanswered questions here{% endtrans %}
+ {{ settings.WORDS_THERE_ARE_NO_UNANSWERED_QUESTIONS_HERE|escape }}
{% endif %}
{% if search_state.scope == "followed" %}
- {% trans %}No questions here. {% endtrans %}
- {% trans %}Please follow some questions or follow some users.{% endtrans %}
+ {{ settings.WORDS_NO_QUESTIONS_HERE|escape }}
+ {{ settings.WORDS_PLEASE_FOLLOW_QUESTIONS|escape }}
{% endif %}
</p>
{% if search_state.query or search_state.tags or search_state.author %}
@@ -27,5 +27,5 @@
</p>
{% endif %}
<p class="evenMore" style="text-align:center">
-<a href="{% url ask %}">{% trans %}Please always feel free to ask your question!{% endtrans %}</a>
+<a href="{% url ask %}">{{ settings.WORDS_PLEASE_FEEL_FREE_TO_ASK_YOUR_QUESTION|escape }}</a>
</p>
diff --git a/askbot/templates/main_page/questions_loop.html b/askbot/templates/main_page/questions_loop.html
index 10a7fce1..020649ee 100644
--- a/askbot/templates/main_page/questions_loop.html
+++ b/askbot/templates/main_page/questions_loop.html
@@ -5,9 +5,9 @@
{% for thread in threads.object_list %}
{{ thread.get_summary_html(search_state=search_state, visitor = request.user) }}
{% endfor %}
- <div class="evenMore">
+ {#<div class="evenMore">
{% trans %}Did not find what you were looking for?{% endtrans %}
- <a href="{% url ask %}">{% trans %}Ask your question!{% endtrans %}</a>
- </div>
+ <a href="{% url ask %}">{{ settings.WORDS_ASK_YOUR_QUESTION }}</a>
+ </div>#}
{% endif %}
diff --git a/askbot/templates/main_page/tab_bar.html b/askbot/templates/main_page/tab_bar.html
index c0b37955..ef66aebe 100644
--- a/askbot/templates/main_page/tab_bar.html
+++ b/askbot/templates/main_page/tab_bar.html
@@ -8,7 +8,7 @@
{% else %}
href="/feeds/rss/"
{% endif %}
- title="{% trans %}subscribe to the questions feed{% endtrans %}"
+ title="{% trans %}subscribe to the feed{% endtrans %}"
>{% trans %}RSS{% endtrans %}
</a>
{% endif %}
diff --git a/askbot/templates/meta/bottom_scripts.html b/askbot/templates/meta/bottom_scripts.html
index 5a307b19..c027258e 100644
--- a/askbot/templates/meta/bottom_scripts.html
+++ b/askbot/templates/meta/bottom_scripts.html
@@ -33,6 +33,7 @@
askbot['data']['maxCommentLength'] = {{ settings.MAX_COMMENT_LENGTH }};
askbot['settings']['editorType'] = '{{ settings.EDITOR_TYPE }}';
askbot['settings']['commentsEditorType'] = '{{ settings.COMMENTS_EDITOR_TYPE }}';
+ askbot['messages']['askYourQuestion'] = '{{ settings.WORDS_ASK_YOUR_QUESTION }}';
{% if settings.ALLOWED_UPLOAD_FILE_TYPES %}
askbot['settings']['allowedUploadFileTypes'] = [
"{{ settings.ALLOWED_UPLOAD_FILE_TYPES|join('", "')|replace('.','') }}"
@@ -42,6 +43,7 @@
{% endif %}
askbot['data']['haveFlashNotifications'] = {{ user_messages|as_js_bool }};
askbot['data']['activeTab'] = '{{ active_tab }}';
+ askbot['settings']['csrfCookieName'] = '{{ settings.CSRF_COOKIE_NAME }}';
{% if search_state %}
askbot['data']['searchUrl'] = '{{ search_state.query_string()|escapejs }}';
{% else %}
diff --git a/askbot/templates/meta/fonts.html b/askbot/templates/meta/fonts.html
index 1e0fe707..4332e1a6 100644
--- a/askbot/templates/meta/fonts.html
+++ b/askbot/templates/meta/fonts.html
@@ -10,6 +10,6 @@
</style>
{% else %}
{# note: for IE8 we ask for fonts separately #}
- <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&amp;subset=latin-ext' rel='stylesheet' type='text/css'>
- <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&amp;subset=cyrillic-ext' rel='stylesheet' type='text/css'>
+ <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&amp;subset=latin-ext' rel='stylesheet' type='text/css' />
+ <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&amp;subset=cyrillic-ext' rel='stylesheet' type='text/css' />
{% endif %}
diff --git a/askbot/templates/meta/html_head_javascript.html b/askbot/templates/meta/html_head_javascript.html
index 062a435f..da9c7f74 100644
--- a/askbot/templates/meta/html_head_javascript.html
+++ b/askbot/templates/meta/html_head_javascript.html
@@ -7,12 +7,14 @@
{% if request.user.is_authenticated() %}
askbot['data']['userId'] = {{ request.user.id }};
askbot['data']['userName'] = '{{ request.user.username|escape }}';
+ askbot['data']['userEmail'] = '{{ request.user.email|escape }}';
askbot['data']['userIsAdminOrMod'] = {{ request.user.is_administrator_or_moderator()|as_js_bool }};
askbot['data']['userIsAdmin'] = {{ request.user.is_administrator()|as_js_bool }};
askbot['data']['userReputation'] = {{ request.user.reputation }};
askbot['data']['userIsReadOnly'] = {{ request.user.is_read_only()|as_js_bool }};
{% else %}
askbot['data']['userReputation'] = 0;
+ askbot['data']['userEmail'] = null;
askbot['data']['userIsReadOnly'] = false;//in principle we allow anon users to start posting
{% endif %}
askbot['urls'] = {};
diff --git a/askbot/templates/question.html b/askbot/templates/question.html
index 592a0893..e1310e3c 100644
--- a/askbot/templates/question.html
+++ b/askbot/templates/question.html
@@ -15,6 +15,9 @@
//below is pure cross-browser javascript, no jQuery
askbot['data']['userIsThreadModerator'] = {% if user_is_thread_moderator %}true{% else %}false{% endif %};
askbot['data']['oldestAnswerId'] = {% if oldest_answer_id %}{{ oldest_answer_id }}{% else %}-1{% endif %};
+ {% if settings.READ_ONLY_MODE_ENABLED %}
+ askbot['settings']['readOnlyModeEnabled'] = true;
+ {% endif %}
(function(){
var hasClass = function(node, selector) {
@@ -174,7 +177,37 @@
}
}
+ function remove_all_controls(post_id) {
+ var deleteBtn = document.getElementById('post-' + post_id + '-delete');
+ var controls = deleteBtn.parentNode;
+ if (controls.className == 'comment-content') {
+ removeNode(deleteBtn);
+ var convertLinks = findChildrenByClassName(controls, 'convert-comment');
+ if (convertLinks.length) {
+ removeNode(convertLinks[0]);
+ }
+ var editLinks = findChildrenByClassName(controls, 'edit');
+ if (editLinks.length) {
+ removeNode(editLinks[0]);
+ }
+ } else {
+ var buttons = controls.childNodes;
+ var numButtons = buttons.length;
+ for (var i = numButtons - 1; i >= 0; i--) {
+ removeNode(buttons[i]);
+ }
+ }
+ };
+
+
function render_post_controls(post_id){
+
+ //in this case remove all post controls
+ if (askbot['settings']['readOnlyModeEnabled'] === true) {
+ remove_all_controls(post_id);
+ return;
+ }
+
if (data['userIsAdminOrMod']){
return;//all remaining functions stay on
}
@@ -244,12 +277,12 @@
add_answer_btn.className += ' answer-own-question';
add_answer_btn.setAttribute(
'value',
- '{% trans %}Answer Your Own Question{% endtrans %}'
+ '{{ settings.WORDS_ANSWER_YOUR_OWN_QUESTION|escapejs }}'
)
} else {
add_answer_btn.setAttribute(
'value',
- '{% trans %}Post Your Answer{% endtrans %}'
+ '{{ settings.WORDS_POST_YOUR_ANSWER|escapejs }}'
)
}
} else {
@@ -337,6 +370,9 @@
askbot['messages']['addComment'] = '{% trans %}add a comment{% endtrans %}';
askbot['messages']['userNamePrompt'] = '{% trans %}User name:{% endtrans %}';
askbot['messages']['userEmailPrompt'] = '{% trans %}Email address:{% endtrans %}';
+ {% if settings.READ_ONLY_MODE_ENABLED %}
+ askbot['messages']['readOnlyMessage'] = '{{ settings.READ_ONLY_MESSAGE }}';
+ {% endif %}
askbot['settings']['saveCommentOnEnter'] = {{ settings.SAVE_COMMENT_ON_ENTER|as_js_bool }};
askbot['settings']['tagSource'] = '{{ settings.TAG_SOURCE }}';
askbot['settings']['enableSharingGoogle'] = {{ settings.ENABLE_SHARING_GOOGLE|as_js_bool }};
diff --git a/askbot/templates/question/answer_card.html b/askbot/templates/question/answer_card.html
index 9d17e8c2..9a833a35 100644
--- a/askbot/templates/question/answer_card.html
+++ b/askbot/templates/question/answer_card.html
@@ -20,7 +20,7 @@
{% if answer.id in published_answer_ids %}
<p><strong>{% trans %}This response is published{% endtrans %}</strong></p>
{% endif %}
- {{ answer.html }}
+ {{ answer.summary }}
</div>
<div class="answer-controls post-controls">
{% include "question/answer_controls.html" %}
diff --git a/askbot/templates/question/answer_comments.html b/askbot/templates/question/answer_comments.html
index e6b5e1c5..c0a0dfd8 100644
--- a/askbot/templates/question/answer_comments.html
+++ b/askbot/templates/question/answer_comments.html
@@ -5,6 +5,7 @@
show_comment = show_comment,
show_comment_position = show_comment_position,
user = request.user,
- max_comments = settings.MAX_COMMENTS_TO_SHOW
+ max_comments = settings.MAX_COMMENTS_TO_SHOW,
+ csrf_token = csrf_token
)
}}
diff --git a/askbot/templates/question/answer_controls.html b/askbot/templates/question/answer_controls.html
index 21aafe47..1c66f147 100644
--- a/askbot/templates/question/answer_controls.html
+++ b/askbot/templates/question/answer_controls.html
@@ -1,5 +1,5 @@
{#<span class="action-link swap-qa">
- <a id="swap-question-with-answer-{{answer.id}}">{% trans %}swap with question{% endtrans %}</a>
+ <a id="swap-question-with-answer-{{answer.id}}">{{ settings.WORDS_SWAP_WITH_QUESTION|escape }}</a>
</span>uncomment if needed#}
<span id='post-{{answer.id}}-edit' class="action-link">
<a class="question-edit" href="{% url edit_answer answer.id %}">{% trans %}edit{% endtrans %}</a>
@@ -68,7 +68,7 @@
<input
type="submit"
class="link"
- value="{% trans %}repost as a question comment{% endtrans %}"
+ value="{{ settings.WORDS_REPOST_AS_A_QUESTION_COMMENT|escape }}"
/>
</form>
</li>
@@ -82,7 +82,7 @@
<input
type="submit"
class="link"
- value="{% trans %}repost as a comment under the older answer{% endtrans %}"
+ value="{{ settings.WORDS_REPOST_AS_A_COMMENT_UNDER_THE_OLDER_ANSWER|escape }}"
/>
</form>
</li>
diff --git a/askbot/templates/question/answer_tab_bar.html b/askbot/templates/question/answer_tab_bar.html
index bebf68b8..917ce6d9 100644
--- a/askbot/templates/question/answer_tab_bar.html
+++ b/askbot/templates/question/answer_tab_bar.html
@@ -1,23 +1,16 @@
<div class="tabBar tabBar-answer">
<h2 id="questionCount">
- {% trans counter=answer_count %}
- {{counter}} Answer
- {% pluralize %}
- {{counter}} Answers
- {% endtrans %}
+ {{ answer_count }} {{ settings.WORDS_ANSWERS_COUNTABLE_FORMS|py_pluralize(answer_count) }}
</h2>
<div class="tabsA">
<span class="label">
{% trans %}Sort by »{% endtrans %}
</span>
<a id="oldest" href="{{ question.get_absolute_url() }}?sort=oldest#sort-top"
- title="{% trans %}oldest answers will be shown first{% endtrans %}"
- ><span>{% trans %}oldest{% endtrans %}</span></a>
+ ><span>{% trans %}oldest{% endtrans %}</span></a>
<a id="latest" href="{{ question.get_absolute_url() }}?sort=latest#sort-top"
- title="{% trans %}newest answers will be shown first{% endtrans %}"
- ><span>{% trans %}newest{% endtrans %}</span></a>
+ ><span>{% trans %}newest{% endtrans %}</span></a>
<a id="votes" href="{{ question.get_absolute_url() }}?sort=votes#sort-top"
- title="{% trans %}most voted answers will be shown first{% endtrans %}"
- ><span>{% trans %}most voted{% endtrans %}</span></a>
+ ><span>{% trans %}most voted{% endtrans %}</span></a>
</div>
</div>
diff --git a/askbot/templates/question/answer_vote_buttons.html b/askbot/templates/question/answer_vote_buttons.html
index 68503310..84c901e0 100644
--- a/askbot/templates/question/answer_vote_buttons.html
+++ b/askbot/templates/question/answer_vote_buttons.html
@@ -4,10 +4,10 @@
<div
id="answer-img-accept-{{ answer.id }}"
class="answer-img-accept"
- {% if answer.accepted() %}
- title="{% trans %}this answer has been selected as correct{% endtrans %}"
- {% else %}
- title="{% trans %}mark this answer as correct (click again to undo){% endtrans %}"
- {% endif %}
+ {% if answer.accepted() %}
+ title="{{ settings.WORDS_THIS_ANSWER_HAS_BEEN_SELECTED_AS_CORRECT|escape }}"
+ {% else %}
+ title="{{ settings.WORDS_MARK_THIS_ANSWER_AS_CORRECT|escape }} {% trans %}(click again to undo){% endtrans %}"
+ {% endif %}
></div>
{% endif %}
diff --git a/askbot/templates/question/closed_question_info.html b/askbot/templates/question/closed_question_info.html
index f6f3f557..33fc9482 100644
--- a/askbot/templates/question/closed_question_info.html
+++ b/askbot/templates/question/closed_question_info.html
@@ -1,5 +1,7 @@
<div class="question-status">
- <h3>{% trans close_reason=thread.get_close_reason_display() %}The question has been closed for the following reason <b>"{{ close_reason }}"</b> <i>by{% endtrans %}
- <a href="{{ thread.closed_by.get_profile_url() }}">{{ thread.closed_by.username|escape }}</a> </i><br>
- {% trans closed_at=thread.closed_at %}close date {{closed_at}}{% endtrans %}</h3>
+ <h3>{% trans %}Closed for the following reason{% endtrans %}
+ <b>{{ thread.get_close_reason_display() }}</b> {% trans %}by{% endtrans %}
+ {% trans close_reason=thread.get_close_reason_display() %}Closed for the following reason <b>"{{ close_reason }}"</b> <i>by{% endtrans %}
+ <a href="{{ thread.closed_by.get_profile_url() }}">{{ thread.closed_by.username|escape }}</a> </i><br>
+ {% trans closed_at=thread.closed_at %}close date {{closed_at}}{% endtrans %}</h3>
</div>
diff --git a/askbot/templates/question/content.html b/askbot/templates/question/content.html
index 6a73ebef..50bfee55 100644
--- a/askbot/templates/question/content.html
+++ b/askbot/templates/question/content.html
@@ -35,8 +35,8 @@
<a
class="button submit"
href="{% url "edit_answer" previous_answer.id %}"
- >{% trans %}Edit Your Previous Answer{% endtrans %}</a>
- <span>{% trans %}(only one answer per user is allowed){% endtrans %}</span>
+ >{{ settings.WORDS_EDIT_YOUR_PREVIOUS_ANSWER|escape }}</a>
+ <span>{{ settings.WORDS_ONLY_ONE_ANSWER_PER_USER_IS_ALLOWED|escape }}</span>
<div class="invisible">
{# hidden because we still need js from the tinymce widget #}
{% include "question/new_answer_form.html" %}
@@ -48,6 +48,6 @@
type="button"
class="submit after-editor answer-own-question"
id="fmanswer_button"
- value="{% trans %}Answer Your Own Question{% endtrans %}"
+ value="{{ settings.WORDS_ANSWER_YOUR_OWN_QUESTION|escape }}"
/>
{% endif %}
diff --git a/askbot/templates/question/javascript.html b/askbot/templates/question/javascript.html
index 81272d45..3e6ba131 100644
--- a/askbot/templates/question/javascript.html
+++ b/askbot/templates/question/javascript.html
@@ -24,6 +24,7 @@
{% endcompress %}
<script type="text/javascript">
// define reputation needs for comments
+ askbot['urls']['getPostHtml'] = '{% url get_post_html %}';
$(document).ready(function(){
$("#nav_questions").attr('className',"on");
$("#" + askbot['data']['answersSortTab']).attr('className',"on");
@@ -63,6 +64,17 @@
draftHandler.setThreadId({{ thread.id }});
draftHandler.decorate($(document));
}
+
+ var expanders = $('.expander');
+ expanders.each(function(idx, item) {
+ var expanderElement = $(item);
+ var post = expanderElement.closest('.post,.comment');
+ if (post.length === 1) {
+ var expander = new PostExpander();
+ expander.setPostId(post.data('postId'));
+ expander.decorate(expanderElement);
+ }
+ });
});
$(window).bind('hashchange', animate_hashes);
diff --git a/askbot/templates/question/new_answer_form.html b/askbot/templates/question/new_answer_form.html
index 473a4bc2..9e8644d4 100644
--- a/askbot/templates/question/new_answer_form.html
+++ b/askbot/templates/question/new_answer_form.html
@@ -9,34 +9,31 @@
<a
class="button submit"
href="{{settings.LOGIN_URL}}?next={% url question question.id %}"
- >{% trans %}Login/Signup to Answer{% endtrans %}</a>
+ >{{ settings.WORDS_LOGIN_SIGNUP_TO_ANSWER|escape }}</a>
{% endif %}
{% else %}
- {% if not thread.closed %}
+ {% if not thread.closed and settings.READ_ONLY_MODE_ENABLED == False %}
<div>
{% spaceless %}
<h2>
{% if answers %}
- {% trans %}Your answer{% endtrans %}
+ {{ settings.WORDS_YOUR_ANSWER|escape }}
{% else %}
- {% trans %}Be the first one to answer this question!{% endtrans %}
+ {{ settings.WORDS_BE_THE_FIRST_TO_ANSWER_THIS_QUESTION|escape }}
{% endif %}
</h2>
{% endspaceless %}
</div>
+ <p class="message">
{% if request.user.is_anonymous() %}
- <div class="message">{% trans %}<span class='strong big'>Please start posting your answer anonymously</span> - your answer will be saved within the current session and published after you log in or create a new account. Please try to give a <strong>substantial answer</strong>, for discussions, <strong>please use comments</strong> and <strong>please do remember to vote</strong> (after you log in)!{% endtrans %}</div>
- {% else %}
- <p class="message">
- {% if request.user==question.author %}
- {% trans %}<span class='big strong'>You are welcome to answer your own question</span>, but please make sure to give an <strong>answer</strong>. Remember that you can always <strong>revise your original question</strong>. Please <strong>use comments for discussions</strong> and <strong>please don't forget to vote :)</strong> for the answers that you liked (or perhaps did not like)!{% endtrans %}
- {% else %}
- {% trans %}<span class='big strong'>Please try to give a substantial answer</span>. If you wanted to comment on the question or answer, just <strong>use the commenting tool</strong>. Please remember that you can always <strong>revise your answers</strong> - no need to answer the same question twice. Also, please <strong>don't forget to vote</strong> - it really helps to select the best questions and answers!{% endtrans %}
- {% endif %}
- </p>
+ {{ settings.WORDS_INSTRUCTION_TO_POST_ANONYMOUSLY }}
+ {% elif request.user==question.author %}
+ {{ settings.WORDS_INSTRUCTION_TO_ANSWER_OWN_QUESTION }}
{% endif %}
+ {{ settings.WORDS_INSTRUCTION_TO_GIVE_ANSWER }}
+ </p>
<div class="folded-editor{% if editor_is_unfolded %}unfolded{% endif %}" tabindex="6">
- <p class="prompt"><a>{% trans %}Add answer{% endtrans %}</a></p>
+ <p class="prompt"><a>{{ settings.WORDS_ADD_ANSWER|escape }}</a></p>
<div class="editor-proper">
{{ macros.edit_post(
answer,
@@ -44,6 +41,10 @@
editor_type = settings.EDITOR_TYPE
)
}}
+ {% if answer.recaptcha %}
+ <div>{{ macros.form_field_with_errors(answer.recaptcha) }}</div>
+ <div class="clearfix"></div>
+ {% endif %}
<input id="add-answer-btn" type="submit" class="submit after-editor" style="float:left"/>
<script type="text/javascript">
askbot['functions']['renderAddAnswerButton']();
diff --git a/askbot/templates/question/question_card.html b/askbot/templates/question/question_card.html
index dcabceec..d6c37260 100644
--- a/askbot/templates/question/question_card.html
+++ b/askbot/templates/question/question_card.html
@@ -1,10 +1,11 @@
<div class="post question" data-post-id="{{ question.id }}">
<div class="vote-buttons">
{% include "question/question_vote_buttons.html" %}
+
{% include "question/share_buttons.html" %}
</div>
<div id="post-id-{{question.id}}" class="post-content{% if question.deleted %} deleted{% endif %}">
- <h1><a href="{{ question.get_absolute_url() }}">{{ thread.get_title(question)|escape }}</a></h1>
+ <h1><a href="{{ question.get_absolute_url() }}">{{ thread.get_title()|escape }}</a></h1>
{% include "question/question_tags.html" %}
<div class="clearfix"></div>
@@ -12,7 +13,7 @@
<div class="post-update-info-container">
{% include "question/question_author_info.html" %}
</div>
- {{ question.html }}
+ {{ question.summary }}
</div>
<div id="question-controls" class="post-controls">
{% include "question/question_controls.html" %}
diff --git a/askbot/templates/question/question_comments.html b/askbot/templates/question/question_comments.html
index e9d3f724..a05f6328 100644
--- a/askbot/templates/question/question_comments.html
+++ b/askbot/templates/question/question_comments.html
@@ -5,6 +5,7 @@
show_comment = show_comment,
show_comment_position = show_comment_position,
user = request.user,
- max_comments = settings.MAX_COMMENTS_TO_SHOW
+ max_comments = settings.MAX_COMMENTS_TO_SHOW,
+ csrf_token = csrf_token
)
}}
diff --git a/askbot/templates/question/sidebar.html b/askbot/templates/question/sidebar.html
index c4301b6c..0894b935 100644
--- a/askbot/templates/question/sidebar.html
+++ b/askbot/templates/question/sidebar.html
@@ -5,16 +5,16 @@
</div>
{% endif %}
<div class="box vote-buttons">
- <h2 >{% trans %}Question tools{% endtrans %}</h2>
+ <h2>{{ settings.WORDS_QUESTION_TOOLS|escape }}</h2>
{% if favorited %}
<a class="button followed"
- alt="{% trans %}click to unfollow this question{% endtrans %}">
+ alt="{% trans %}click to unfollow {% endtrans %}">
<div>{% trans %}Following{% endtrans %}</div>
<div class='unfollow'>{% trans %}Unfollow{% endtrans %}</div>
</a>
{% else %}
<a class="button followed"
- alt="{% trans %}click to follow this question{% endtrans %}">
+ alt="{% trans %}click to follow {% endtrans %}">
{%trans %}Follow{%endtrans%}
</a>
{% endif %}
@@ -30,7 +30,7 @@
<p class="rss">
<a
href="{{ base_url }}/feeds/question/{{ question.id }}"
- title="{% trans %}subscribe to this question rss feed{% endtrans %}"
+ title="{% trans %}subscribe to the rss feed{% endtrans %}"
>{% trans %}subscribe to rss feed{% endtrans %}</a>
</p>
{% endif %}
@@ -42,8 +42,7 @@
<div class="box sharing-widget">
{% if thread.is_private() %}
<h2>{% trans %}Invite{% endtrans %}</h2>
- <p style="margin: 16px 0"
- >Invite others to help answer this question</p>
+ <p style="margin: 16px 0">{{ settings.WORDS_INVITE_OTHERS_TO_HELP_ANSWER_THIS_QUESTION|escape }}</p>
<form action="{% url share_question_with_user %}" method="post">{% csrf_token %}
<input id="share_user_name" type="text" class="groups-input" name="recipient_name" />
<input type="hidden" name="thread_id" value="{{ thread.id }}"/>
@@ -78,7 +77,7 @@
{% if shared_users_count or shared_groups_count %}
<p
style="margin:16px 0 4px 0"
- >{% trans %}This question is currently shared only with:{% endtrans %}</p>
+ >{{ settings.WORDS_THIS_QUESTION_IS_CURRENTLY_SHARED_ONLY_WITH|escape }}</p>
{% endif %}
<h3>{% trans %}Individual users{% endtrans %}</h3>
{% set comma = joiner(',') %}
@@ -148,7 +147,7 @@
{% if similar_threads.data() and settings.SIDEBAR_QUESTION_SHOW_RELATED %}
{#% cache 1800 "related_questions" related_questions question.id language_code %#}
<div class="box">
- <h2>{% trans %}Related questions{% endtrans %}</h2>
+ <h2>{{ settings.WORDS_RELATED_QUESTIONS|escape }}</h2>
<div class="questions-related">
{% for thread_dict in similar_threads.data() %}
<p>
diff --git a/askbot/templates/question_edit.html b/askbot/templates/question_edit.html
index 1b119f93..f6de6bdf 100644
--- a/askbot/templates/question_edit.html
+++ b/askbot/templates/question_edit.html
@@ -1,19 +1,19 @@
{% extends "two_column_body.html" %}
{% import "macros.html" as macros %}
<!-- question_edit.html -->
-{% block title %}{% spaceless %}{% trans %}Edit question{% endtrans %}{% endspaceless %}{% endblock %}
+{% block title %}{% spaceless %}{{ settings.WORDS_EDIT_QUESTION|escape }}{% endspaceless %}{% endblock %}
{% block forestyle %}
<link rel="stylesheet" type="text/css" href="{{"/js/wmd/wmd.css"|media}}" />
{% endblock %}
{% block content %}
-<div class="section-title">{% trans %}Edit question{% endtrans %} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</div>
+<div class="section-title">{{ settings.WORDS_EDIT_QUESTION|escape }} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</div>
<form id="fmedit" action="{% url edit_question question.id %}" method="post" >{% csrf_token %}
{% if revision_form.revision.errors %}{{ revision_form.revision.errors.as_ul() }}{% endif %}
{{ revision_form.revision }}
<input type="hidden" id="select_revision" name="select_revision" value="false"/>
<div class="form-item">
<label for="id_title" >
- {% trans %}Question - in one sentence{% endtrans %}
+ {{ settings.WORDS_QUESTION_IN_ONE_SENTENCE|escape }}
{% if form.title.errors %}
{{ form.title.errors }}
{% endif %}
@@ -35,6 +35,10 @@
)
}}
<div class="after-editor">
+ {% if form.recaptcha %}
+ <div>{{ macros.form_field_with_errors(form.recaptcha) }}</div>
+ <div class="clearfix"></div>
+ {% endif %}
<div class="question-options">
{% if settings.WIKI_ON and question.wiki == False %}
{{ macros.checkbox_in_div(form.wiki) }}
@@ -58,7 +62,7 @@
</div>
{% endif %}
</div>
- <input id="edit_post_form_submit_button" type="submit" value="{% trans %}Save edit{% endtrans %}" class="large submit" />&nbsp;
+ <input id="edit_post_form_submit_button" type="submit" value="{% trans %}Save edit{% endtrans %}" class="large submit" />
<input type="button" value="{% trans %}Cancel{% endtrans %}" class="large submit" onclick="history.back(-1);" />
</div>
@@ -66,7 +70,13 @@
{% endblock %}
{% block sidebar %}
-{% include "widgets/question_edit_tips.html" %}
+<div id ="tips" class="box">
+ <h2>{% trans %}Tips{% endtrans %}</h2>
+ {% include "widgets/question_edit_tips.html" %}
+</div>
+{% if settings.EDITOR_TYPE == 'markdown' %}
+ {% include "/widgets/markdown_help.html" %}
+{% endif %}
{% endblock %}
{% block endjs %}
diff --git a/askbot/templates/question_retag.html b/askbot/templates/question_retag.html
index c42b42f8..df8dc584 100644
--- a/askbot/templates/question_retag.html
+++ b/askbot/templates/question_retag.html
@@ -1,8 +1,8 @@
{% extends "two_column_body.html" %}
<!-- question_retag.html -->
-{% block title %}{% spaceless %}{% trans %}Retag question{% endtrans %}{% endspaceless %}{% endblock %}
+{% block title %}{% spaceless %}{{ settings.WORDS_RETAG_QUESTION|escape }}{% endspaceless %}{% endblock %}
{% block content %}
-<h1>{% trans %}Retag question{% endtrans %} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</h1>
+<h1>{{ settings.WORDS_RETAG_QUESTION|escape }} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</h1>
<form id="fmretag" action="{% url retag_question question.id %}" method="post" >{% csrf_token %}
<h2>
{{ question.thread.get_title()|escape }}
@@ -18,8 +18,8 @@
</div>
</div>
<div class="error" ></div>
- <input type="submit" value="{% trans %}Retag{% endtrans %}" class="submit" />&nbsp;
- <input type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" onclick="history.back(-1);" />
+ <input type="submit" value="{% trans %}Retag{% endtrans %}"/>&nbsp;
+ <input type="button" value="{% trans %}Cancel{% endtrans %}" onclick="history.back(-1);" />
</form>
{% endblock %}
diff --git a/askbot/templates/reopen.html b/askbot/templates/reopen.html
index 4ddd6f31..07208793 100644
--- a/askbot/templates/reopen.html
+++ b/askbot/templates/reopen.html
@@ -1,15 +1,15 @@
{% extends "two_column_body.html" %}
{% from "macros.html" import timeago %}
<!-- reopen.html -->
-{% block title %}{% spaceless %}{% trans %}Reopen question{% endtrans %}{% endspaceless %}{% endblock %}
+{% block title %}{{ settings.WORDS_REOPEN_QUESTION|escape }}{% endblock %}
{% block content %}
-<h1>{% trans %}Reopen question{% endtrans %}</h1>
+<h1>{{ settings.WORDS_REOPEN_QUESTION|escape }}</h1>
<p>{% trans %}Title{% endtrans %}:
<a href="{{ question.get_absolute_url() }}">
<span class="big">{{ question.get_question_title()|escape }}</span>
</a>
</p>
-<p>{% trans username = closed_by_username|escape %}This question has been closed by
+<p>{% trans username = closed_by_username|escape %}Closed by:
<a href="{{closed_by_profile_url}}">{{username}}</a>
{% endtrans %}
</p>
@@ -19,13 +19,11 @@
<p>
{% trans %}When:{% endtrans %} {{ timeago(question.thread.closed_at) }}
</p>
-<p>
- {% trans %}Reopen this question?{% endtrans %}
-</p>
+<p>{{ settings.WORDS_REOPEN_QUESTION|escape }}?</p>
<form id="fmclose" action="{% url reopen question.id %}" method="post" >{% csrf_token %}
<div id="" style="padding:20px 0 20px 0">
- <input type="submit" value="{% trans %}Reopen this question{% endtrans %}" class="submit" />&nbsp;
- <input id="btBack" type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" />
+ <input type="submit" value="{{ settings.WORDS_REOPEN_QUESTION|escape }}"/>
+ <input id="btBack" type="button" value="{% trans %}Cancel{% endtrans %}"/>
</div>
</form>
{% endblock %}
diff --git a/askbot/templates/revisions.html b/askbot/templates/revisions.html
index 1765b728..1617ff25 100644
--- a/askbot/templates/revisions.html
+++ b/askbot/templates/revisions.html
@@ -53,7 +53,7 @@
revision,
contributor_type,
False,
- 0
+ 0,
)
}}
</div>
diff --git a/askbot/templates/search/indexes/auth/user_text.txt b/askbot/templates/search/indexes/auth/user_text.txt
index b0baa3a6..e2b4ecd8 100644
--- a/askbot/templates/search/indexes/auth/user_text.txt
+++ b/askbot/templates/search/indexes/auth/user_text.txt
@@ -3,3 +3,5 @@
{{ object.last_name }}
{{ object.email }}
{{ object.full_name }}
+{{ object.about }}
+{{ object.location }}
diff --git a/askbot/templates/tags/form_bulk_tag_subscription.html b/askbot/templates/tags/form_bulk_tag_subscription.html
index 95168e45..b6e72a96 100644
--- a/askbot/templates/tags/form_bulk_tag_subscription.html
+++ b/askbot/templates/tags/form_bulk_tag_subscription.html
@@ -9,7 +9,7 @@
<form action="." method="POST" accept-charset="utf-8">
<table border="0">
{{form.as_table()}}
-<tr><td/><td><input type="submit" class="submit" value="Save"></td></tr>
+<tr><td/><td><input type="submit" value="Save"></td></tr>
</table>
</form>
{% endblock %}
diff --git a/askbot/templates/user_profile/user.html b/askbot/templates/user_profile/user.html
index c72dc857..1f939d11 100644
--- a/askbot/templates/user_profile/user.html
+++ b/askbot/templates/user_profile/user.html
@@ -22,9 +22,10 @@
<script type="text/javascript">
var viewUserID = {{view_user.id}};
askbot['data']['viewUserName'] = '{{ view_user.username|escape }}';
- askbot['data']['viewUserId'] = {{view_user.id}};
+ askbot['data']['viewUserId'] = {{ view_user.id }};
askbot['urls']['edit_group_membership'] = '{% url edit_group_membership %}';
askbot['urls']['getGroupsList'] = '{% url get_groups_list %}';
+ askbot['urls']['getTopAnswers'] = '{% url get_top_answers %}';
</script>
{% if request.user|can_moderate_user(view_user) %}
<script type='text/javascript' src='{{"/js/jquery.form.js"|media}}'></script>
diff --git a/askbot/templates/user_profile/user_answers_list.html b/askbot/templates/user_profile/user_answers_list.html
new file mode 100644
index 00000000..0215ee54
--- /dev/null
+++ b/askbot/templates/user_profile/user_answers_list.html
@@ -0,0 +1,14 @@
+{% for top_answer in top_answers %}
+<div class="answer-summary">
+ <a href="{{ top_answer.get_absolute_url() }}">
+ <span class="answer-votes {% if top_answer.accepted() %}answered-accepted{% endif %}">
+ {{ top_answer.score }}
+ </span>
+ </a>
+ <div class="answer-link">
+ {% spaceless %}
+ <a href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{top_answer.id}}">{{ top_answer.thread.title|escape }}</a>
+ {% endspaceless %}
+ </div>
+</div>
+{% endfor %}
diff --git a/askbot/templates/user_profile/user_edit.html b/askbot/templates/user_profile/user_edit.html
index c95bf815..fa9ecfdd 100644
--- a/askbot/templates/user_profile/user_edit.html
+++ b/askbot/templates/user_profile/user_edit.html
@@ -106,8 +106,8 @@
{% endif %}
</table>
<div style="margin:30px 0 60px 0">
- <input type="submit" value="{% trans %}Update{% endtrans %}" class="submit" >&nbsp;
- <input id="cancel" type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" >
+ <input type="submit" value="{% trans %}Update{% endtrans %}"/>
+ <input id="cancel" type="button" value="{% trans %}Cancel{% endtrans %}"/>
</div>
</div>
</form>
diff --git a/askbot/templates/user_profile/user_email_subscriptions.html b/askbot/templates/user_profile/user_email_subscriptions.html
index 4692456b..100ce789 100644
--- a/askbot/templates/user_profile/user_email_subscriptions.html
+++ b/askbot/templates/user_profile/user_email_subscriptions.html
@@ -7,7 +7,7 @@
{% block usercontent %}
<h2>{% trans %}Email subscription settings{% endtrans %}</h2>
<p class="message">
-{% trans %}<span class='big strong'>Adjust frequency of email updates.</span> Receive updates on interesting questions by email, <strong><br/>help the community</strong> by answering questions of your colleagues. If you do not wish to receive emails - select 'no email' on all items below.<br/>Updates are only sent when there is any new activity on selected items.{% endtrans %}</p>
+{% trans %}<span class='big strong'>Adjust frequency of email updates.</span> Receive updates on interesting content by email. If you do not wish to receive emails - select 'no email' on all items below.<br/>Updates are only sent when there is any new activity on selected items.{% endtrans %}</p>
<div>
{% if action_status %}
<p class="action-status"><span>{{action_status}}</span></p>
@@ -20,8 +20,8 @@
{{tag_filter_selection_form}}
</table>
<div class="submit-row text-align-right">
- <input type="submit" class="submit" name="save" value="{% trans %}Update{% endtrans %}"/>&nbsp;
- <input type="submit" class="submit" name="stop_email" value="{% trans %}Stop Email{% endtrans %}"/>
+ <input type="submit" name="save" value="{% trans %}Update{% endtrans %}"/>&nbsp;
+ <input type="submit" name="stop_email" value="{% trans %}Stop Email{% endtrans %}"/>
</div>
</form>
</div>
@@ -53,7 +53,7 @@
css_class = 'subscribed marked-tags special',
)
}}
- <br/>
+ <div class="clearfix"></div>
<div class="inputs">
<input id="subscribedTagInput" autocomplete="off" type="text"/>
<input id="subscribedTagAdd" type="submit" value="{% trans %}add{% endtrans%}"/>
@@ -61,14 +61,15 @@
</div>
{%endif%}
{% endblock %}
-{%block userjs%}
- <script type='text/javascript'>
- search = new FullTextSearch();
- askbot['controllers'] = askbot['controllers'] || {}
- askbot['controllers']['fullTextSearch'] = search;
- askbot['urls']['mark_subscribed_tag'] = '{% url mark_subscribed_tag %}';
- askbot['urls']['unmark_tag'] = '{% url unmark_tag %}';
- </script>
- <script type='text/javascript' src='{{"/js/tag_selector.js"|media}}'></script>
+{% block userjs %}
+ <script type='text/javascript'>
+ search = new FullTextSearch();
+ askbot['controllers'] = askbot['controllers'] || {}
+ askbot['controllers']['fullTextSearch'] = search;
+ askbot['urls']['mark_subscribed_tag'] = '{% url mark_subscribed_tag %}';
+ askbot['urls']['unmark_tag'] = '{% url unmark_tag %}';
+ askbot['settings']['tag_editor'] = '{{ tag_editor_settings|escapejs }}';
+ </script>
+ <script type='text/javascript' src='{{"/js/tag_selector.js"|media}}'></script>
{% endblock %}
<!-- end user_email_subscriptions.html -->
diff --git a/askbot/templates/user_profile/user_favorites.html b/askbot/templates/user_profile/user_favorites.html
index ffdcbd0d..e94015de 100644
--- a/askbot/templates/user_profile/user_favorites.html
+++ b/askbot/templates/user_profile/user_favorites.html
@@ -1,8 +1,6 @@
{% extends "user_profile/user.html" %}
<!-- user_favorites.html -->
-{% block profilesection %}
- {% trans %}followed questions{% endtrans %}
-{% endblock %}
+{% block profilesection %}{{ settings.WORDS_FOLLOWED_QUESTIONS|escape }}{% endblock %}
{% block usercontent %}
{% include "user_profile/users_questions.html" %}
{% endblock %}
diff --git a/askbot/templates/user_profile/user_info.html b/askbot/templates/user_profile/user_info.html
index 9786d4ec..6a9ff91e 100644
--- a/askbot/templates/user_profile/user_info.html
+++ b/askbot/templates/user_profile/user_info.html
@@ -52,7 +52,13 @@
{% if view_user.real_name %}
<tr>
<td>{% trans %}real name{% endtrans %}</td>
- <td><b>{{view_user.real_name}}</b></td>
+ <td><b>{{view_user.real_name|escape}}</b></td>
+ </tr>
+ {% endif %}
+ {% if request.user|can_see_private_user_data(view_user) %}
+ <tr>
+ <td>{% trans %}email{% endtrans %}</td>
+ <td>{{view_user.email}}</td>
</tr>
{% endif %}
{% if settings.GROUPS_ENABLED %}
@@ -119,7 +125,7 @@
</div>
<div class="col3 user-about">
{% if view_user.about and (not view_user.is_blocked()) %}
- {{view_user.about|linebreaks}}
+ {{view_user.about|linebreaks|escape}}
{% endif %}
</div>
</div>
diff --git a/askbot/templates/user_profile/user_moderate.html b/askbot/templates/user_profile/user_moderate.html
index a7f05b1c..75c6d6fe 100644
--- a/askbot/templates/user_profile/user_moderate.html
+++ b/askbot/templates/user_profile/user_moderate.html
@@ -17,7 +17,7 @@
</table>
<p id="id_user_status_info">
</p>
- <input type="submit" class="submit" name="change_status" value="{% trans %}Save{% endtrans %}" />
+ <input type="submit" name="change_status" value="{% trans %}Save{% endtrans %}" />
</form>
{% endif %}
<h3>
@@ -35,8 +35,8 @@
<table class="form-as-table">
{{ change_user_reputation_form.as_table() }}
</table>
- <input type="submit" class="submit" name="subtract_reputation" value="{% trans %}Subtract{% endtrans %}" />&nbsp;
- <input type="submit" class="submit" name="add_reputation" value="{% trans %}Add{% endtrans %}" />
+ <input type="submit" name="subtract_reputation" value="{% trans %}Subtract{% endtrans %}" />&nbsp;
+ <input type="submit" name="add_reputation" value="{% trans %}Add{% endtrans %}" />
</form>
{% if request.user != view_user %}
<hr/>
@@ -61,7 +61,7 @@
{% endif %}
{{ send_message_form.body_text}}
</div>
- <input type="submit" class="submit" name="send_message" value="{% trans %}Send message{% endtrans %}" />
+ <input type="submit" name="send_message" value="{% trans %}Send message{% endtrans %}" />
</form>
{% endif %}
{% endblock %}
diff --git a/askbot/templates/user_profile/user_stats.html b/askbot/templates/user_profile/user_stats.html
index 648280c4..344051af 100644
--- a/askbot/templates/user_profile/user_stats.html
+++ b/askbot/templates/user_profile/user_stats.html
@@ -9,15 +9,20 @@
{% include "user_profile/user_info.html" %}
<a name="questions"></a>
{% spaceless %}
- <h2>{% trans counter=question_count %}<span class="count">{{counter}}</span> Question{% pluralize %}<span class="count">{{counter}}</span> Questions{% endtrans %}</h2>
+ <h2>
+ <span class="count">{{ question_count }}</span>
+ {{ settings.WORDS_QUESTIONS_COUNTABLE_FORMS|py_pluralize(question_count)|capitalize }}
+ </h2>
{% endspaceless %}
{% include "user_profile/users_questions.html" %}
<a name="answers"></a>
{% spaceless %}
- <h2 style="clear:both;"><span class="count">{{ top_answer_count }}</span> {% trans counter=top_answer_count %}Answer{% pluralize %}Answers{% endtrans %}</h2>
+ <h2 style="clear:both;">
+ <span class="count">{{ top_answer_count }}</span>
+ {{ settings.WORDS_ANSWERS_COUNTABLE_FORMS|py_pluralize(top_answer_count)|capitalize }}
+ </h2>
{% endspaceless %}
{% include "user_profile/users_answers.html" %}
- <br/>
<a name="votes"></a>
{% spaceless %}
<h2>{% trans cnt=total_votes %}<span class="count">{{cnt}}</span> Vote{% pluralize %}<span class="count">{{cnt}}</span> Votes {% endtrans %}</h2>
@@ -80,7 +85,6 @@
{% if award.content_object and award.content_object_is_post %}
<li>
<a
- title="{{ award.content_object.get_snippet()|collapse }}"
href="{{ award.content_object.get_absolute_url() }}"
>{% if award.content_type.post_type == 'answer' %}{% trans %}Answer to:{% endtrans %}{% endif %} {{ award.content_object.thread.title|escape }}</a>
</li>
@@ -97,6 +101,7 @@
{% block endjs %}
{{ super() }}
<script type="text/javascript">
+ askbot['data']['userPostsPageSize'] = {{ page_size }};
askbot['urls']['join_or_leave_group'] = '{% url join_or_leave_group %}';
$(document).ready(function(){
setup_badge_details_toggle();
diff --git a/askbot/templates/user_profile/user_tabs.html b/askbot/templates/user_profile/user_tabs.html
index c7df4187..0abb89f7 100644
--- a/askbot/templates/user_profile/user_tabs.html
+++ b/askbot/templates/user_profile/user_tabs.html
@@ -2,50 +2,41 @@
<div class="tabBar tabBar-profile">
<div class="tabsC">
<a id="stats" {% if tab_name=="stats" %}class="on first"{%else%}class="first"{% endif %}
- title="{% trans %}User profile{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=stats"
><span>{% trans %}overview{% endtrans %}</span></a>
{% if request.user == view_user or request.user|can_moderate_user(view_user) %}
<a id="inbox" {% if tab_name=="inbox" %}class="on"{% endif %}
- title="{% trans %}comments and answers to others questions{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=inbox"
><span>{% trans %}inbox{% endtrans %}</span></a>
{% endif %}
{% if user_follow_feature_on %}
<a id="network" {% if tab_name=="network" %}class="on"{% endif %}
- title="{% trans %}followers and followed users{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=network"
><span>{% trans %}network{% endtrans %}</span></a>
{% endif %}
{% if can_show_karma %}
<a id="reputation" {% if tab_name=="reputation" %}class="on"{% endif %}
- title="{% trans %}Graph of user karma{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=reputation"
><span>{% trans %}karma{% endtrans %}</span></a>
{% endif %}
<a id="favorites" {% if tab_name=="favorites" %}class="on"{% endif %}
- title="{% trans %}questions that user is following{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=favorites"
- ><span>{% trans %}followed questions{% endtrans %}</span></a>
+ ><span>{{ settings.WORDS_FOLLOWED_QUESTIONS|escape }}</span></a>
<a id="recent" {% if tab_name=="recent" %}class="on"{% endif %}
- title="{% trans %}activity{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=recent"
><span>{% trans %}activity{% endtrans %}</span></a>
{% if request.user == view_user or request.user|can_moderate_user(view_user) %}
<a id="votes" {% if tab_name=="votes" %}class="on"{% endif %}
- title="{% trans %}user vote record{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=votes"
><span>{% trans %}votes{% endtrans %}</span></a>
{% endif %}
{% if request.user == view_user or request.user|can_moderate_user(view_user) %}
<a id="email_subscriptions" {% if tab_name=="email_subscriptions" %}class="on"{% endif %}
- title="{% trans %}email subscription settings{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=email_subscriptions"
><span>{% trans %}subscriptions{% endtrans %}</span></a>
{% endif %}
{% if request.user|can_moderate_user(view_user) %}
<a id="moderation" {% if tab_name=="moderation" %}class="on"{% endif %}
- title="{% trans %}moderate this user{% endtrans %}"
href="{% url user_profile view_user.id, view_user.username|slugify %}?sort=moderation"
><span>{% trans %}moderation{% endtrans %}</span></a>
{% endif %}
diff --git a/askbot/templates/user_profile/users_answers.html b/askbot/templates/user_profile/users_answers.html
index 3ac52c19..fb2dab97 100644
--- a/askbot/templates/user_profile/users_answers.html
+++ b/askbot/templates/user_profile/users_answers.html
@@ -1,23 +1,12 @@
+{% import "macros.html" as macros %}
<div class="user-stats-table">
- {% for top_answer in top_answers %}
- <div class="answer-summary">
- <a title="{{ top_answer.summary|collapse|escape }}"
- href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{ top_answer.id }}">
- <span class="answer-votes {% if top_answer.accepted() %}answered-accepted{% endif %}"
- title="{% trans answer_score=top_answer.score %}the answer has been voted for {{ answer_score }} times{% endtrans %} {% if top_answer.accepted() %}{% trans %}this answer has been selected as correct{% endtrans %}{%endif%}">
- {{ top_answer.score }}
- </span>
- </a>
- <div class="answer-link">
- {% spaceless %}
- <a href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{top_answer.id}}">{{ top_answer.thread.title|escape }}</a>
- {% endspaceless %}
- {% if top_answer.comment_count > 0 %}
- <span>
- {% trans comment_count=top_answer.comment_count %}({{ comment_count }} comment){% pluralize %}the answer has been commented {{ comment_count }} times{% endtrans %}
- </span>
- {% endif %}
- </div>
+ <div class="users-answers">
+ {% include "user_profile/user_answers_list.html" %}
</div>
- {% endfor %}
+ {% if top_answer_count > page_size %}
+ <div id="pager" class="pager user-answers-pager">
+ {{ macros.paginator(a_paginator_context) }}
+ <div class="clearfix"></div>
+ </div>
+ {% endif %}
</div>
diff --git a/askbot/templates/user_profile/users_questions.html b/askbot/templates/user_profile/users_questions.html
index ca66b7e2..4e29e0af 100644
--- a/askbot/templates/user_profile/users_questions.html
+++ b/askbot/templates/user_profile/users_questions.html
@@ -1,14 +1,20 @@
<!-- users_questions.html -->
{% import "macros.html" as macros %}
<div class="user-stats-table">
+ <div class="users-questions">
{% for question in questions %}
- {{
- macros.question_summary(
- question.thread, question,
- extra_class='narrow', search_state=search_state,
- visitor = request.user
- )
- }}
+ {{ question.thread.get_summary_html(
+ search_state=search_state,
+ visitor = request.user)
+ }}
{% endfor %}
+ </div>
+ {% if question_count > page_size %}
+ <div class="clearfix"></div>
+ <div id="pager" class="pager user-questions-pager">
+ {{ macros.paginator(q_paginator_context) }}
+ <div class="clearfix"></div>
+ </div>
+ {% endif %}
</div>
<!-- end users_questions.html -->
diff --git a/askbot/templates/widgets/answer_edit_tips.html b/askbot/templates/widgets/answer_edit_tips.html
index 2bb5b256..fdcd27b4 100644
--- a/askbot/templates/widgets/answer_edit_tips.html
+++ b/askbot/templates/widgets/answer_edit_tips.html
@@ -3,23 +3,11 @@
<h2>{% trans %}Tips{% endtrans %}</h2>
<div id="tips">
<ul >
- <li> <b>{% trans %}give an answer interesting to this community{% endtrans %}</b>
- </li>
- <li>
- {% trans %}try to give an answer, rather than engage into a discussion{% endtrans %}
- </li>
- <li>
- {% trans %}provide enough details{% endtrans %}
- </li>
- <li>
- {% trans %}be clear and concise{% endtrans %}
- </li>
+ <li>{{ settings.WORDS_GIVE_AN_ANSWER_INTERESTING_TO_THIS_COMMUNITY|escape }}</li>
+ <li>{{ settings.WORDS_TRY_TO_GIVE_AN_ANSWER|escape }}</li>
+ <li>{% trans %}provide enough details{% endtrans %}</li>
+ <li>{% trans %}be clear and concise{% endtrans %}</li>
</ul>
- <p class='info-box-follow-up-links'>
-<!-- will be change to a popup windows
- <a href="{% url faq %}" target="_blank" title="{% trans %}see frequently asked questions{% endtrans %}">{% trans %}FAQ{% endtrans %} »</a>
--->
- </p>
</div>
</div>
{% if settings.EDITOR_TYPE == 'markdown' %}
diff --git a/askbot/templates/widgets/ask_button.html b/askbot/templates/widgets/ask_button.html
index f8ea82bd..5d969a00 100644
--- a/askbot/templates/widgets/ask_button.html
+++ b/askbot/templates/widgets/ask_button.html
@@ -5,6 +5,10 @@
<a
id="askButton"
class="button"
- href="{{ search_state.full_ask_url() }}{% if group %}{% if '?' in search_state.full_ask_url() %}&{% else %}?{% endif %}group_id={{ group.id }}{% endif %}"
- >{% if group %}{% trans %}Ask the Group{% endtrans %}{% else %}{% trans %}Ask Your Question{% endtrans %}{% endif %}</a>
+ href="{{ search_state.full_ask_url() }}{% if group %}{% if '?' in search_state.full_ask_url() %}&amp;{% else %}?{% endif %}group_id={{ group.id }}{% endif %}"
+ >{% if group %}
+ {{ settings.WORDS_ASK_THE_GROUP|escape }}
+ {% else %}
+ {{ settings.WORDS_ASK_YOUR_QUESTION|escape }}
+ {% endif %}</a>
{% endif %}
diff --git a/askbot/templates/widgets/ask_form.html b/askbot/templates/widgets/ask_form.html
index 1d2642c7..dec52d79 100644
--- a/askbot/templates/widgets/ask_form.html
+++ b/askbot/templates/widgets/ask_form.html
@@ -61,10 +61,14 @@
</div>
{% endif %}
</div>
- {% if not request.user.is_authenticated() %}
- <input type="submit" name="post_anon" value="{% trans %}Login/Signup to Post{% endtrans %}" class="submit" />
+ {% if form.recaptcha %}
+ <div>{{ macros.form_field_with_errors(form.recaptcha) }}</div>
+ <div class="clearfix"></div>
+ {% endif %}
+ {% if request.user.is_anonymous() and not settings.ALLOW_ASK_UNREGISTERED %}
+ <input type="submit" name="post_anon" value="{% trans %}Login/Signup to Post{% endtrans %}" />
{% else %}
- <input type="submit" name="post" value="{% trans %}Ask Your Question{% endtrans %}" class="submit" />
+ <input type="submit" name="post" value="{{ settings.WORDS_ASK_YOUR_QUESTION|escape }}" />
{% endif %}
<div class="clean"></div>
</form>
diff --git a/askbot/templates/widgets/group_info.html b/askbot/templates/widgets/group_info.html
index 6a6b788d..05fcff9b 100644
--- a/askbot/templates/widgets/group_info.html
+++ b/askbot/templates/widgets/group_info.html
@@ -43,7 +43,7 @@
data-toggle-url="{% url toggle_group_profile_property %}"
/>
<label for="moderate-email">
- {% trans %}moderate emailed questions{% endtrans %}
+ {% trans %}moderate emailed content{% endtrans %}
</label>
<br/>
{% endif %}
@@ -55,7 +55,7 @@
data-toggle-url="{% url toggle_group_profile_property %}"
/>
<label for="moderate-answers-to-enquirers">
- {% trans %}show only selected answers to enquirers{% endtrans %}
+ {{ settings.WORDS_SHOW_ONLY_SELECTED_ANSWERS_TO_ENQUIRERS|escape }}
</label>
<br/>
@@ -99,7 +99,6 @@
<br/>
<a
id="preapproved-emails"
- title="{% trans %}list of email addresses of pre-approved users{% endtrans %}"
data-object-id="{{group.group_ptr_id}}"
data-model-name="Group"
data-property-name="preapproved_emails"
@@ -110,7 +109,6 @@
<br/>
<a
id="preapproved-email-domains"
- title="{% trans %}list of preapproved email address domain names{% endtrans %}"
data-object-id="{{group.group_ptr_id}}"
data-model-name="Group"
data-property-name="preapproved_email_domains"
diff --git a/askbot/templates/widgets/header.html b/askbot/templates/widgets/header.html
index 52e528bc..50a25564 100644
--- a/askbot/templates/widgets/header.html
+++ b/askbot/templates/widgets/header.html
@@ -16,5 +16,10 @@
</div>
<div class="clean"></div>
</div>
+ {% if settings.READ_ONLY_MODE_ENABLED %}
+ <div class="content-wrapper system-messages">
+ {{ settings.READ_ONLY_MESSAGE }}
+ </div>
+ {% endif %}
</div>
<!-- end template header.html -->
diff --git a/askbot/templates/widgets/question_edit_tips.html b/askbot/templates/widgets/question_edit_tips.html
index f60304c1..a6ac83b0 100644
--- a/askbot/templates/widgets/question_edit_tips.html
+++ b/askbot/templates/widgets/question_edit_tips.html
@@ -1,22 +1,19 @@
<!-- question_edit_tips.html -->
-<div id ="tips" class="box">
- <h2>{% trans %}Tips{% endtrans %}</h2>
+{% if settings.QUESTION_INSTRUCTIONS %}
+ {{ settings.QUESTION_INSTRUCTIONS|safe }}
+{% else %}
<ul>
- <li> <b>{% trans %}ask a question interesting to this community{% endtrans %}</b>
- </li>
- <li>
- {% trans %}provide enough details{% endtrans %}
- </li>
- <li>
- {% trans %}be clear and concise{% endtrans %}
- </li>
+ {% if request.user.is_anonymous() and not settings.ALLOW_ASK_UNREGISTERED %}
+ <li class="warning">{% trans %}since you are not logged in right now, you will be asked to sign in or register after making your post{% endtrans %}</li>
+ {% else %}
+ {% if settings.EMAIL_VALIDATION %}
+ {% if not request.user.email_isvalid %}
+ <li class="warning">{% trans email=request.user.email %}Your email, {{ email }} has not yet been validated. To post messages you must verify your email, please see <a href='{{ email_validation_faq_url }}'>more details here</a>. You can submit your post now and validate email after that.{% endtrans %}</li>
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ <li>{{ settings.WORDS_ASK_A_QUESTION_INTERESTING_TO_THIS_COMMUNITY|escape }}</li>
+ <li>{% trans %}provide enough details{% endtrans %}</li>
+ <li>{% trans %}be clear and concise{% endtrans %}</li>
</ul>
- <p class='info-box-follow-up-links'>
-<!-- will be change to a popup windows
- <a href="{% url faq %}" target="_blank" title="{% trans %}see frequently asked questions{% endtrans %}">{% trans %}FAQ{% endtrans %} »</a>
--->
- </p>
-</div>
-{% if settings.EDITOR_TYPE == 'markdown' %}
- {% include "/widgets/markdown_help.html" %}
{% endif %}
diff --git a/askbot/templates/widgets/question_summary.html b/askbot/templates/widgets/question_summary.html
index cc480e33..62c97a53 100644
--- a/askbot/templates/widgets/question_summary.html
+++ b/askbot/templates/widgets/question_summary.html
@@ -27,7 +27,7 @@
class="item-count"
>{{ answer_count|humanize_counter }}{% if thread.accepted_answer_id %}{% endif %}</span>
<div>
- {% trans cnt = answer_count %}answer{% pluralize %}answers{% endtrans %}
+ {{ settings.WORDS_ANSWERS_COUNTABLE_FORMS|py_pluralize(answer_count) }}
</div>
</div>
<div class="votes
@@ -44,15 +44,16 @@
<div style="clear:both"></div>
<div class="userinfo">
{{ timeago(thread.last_activity_at) }}
- {% if question.is_anonymous %}
+ {% if question.is_anonymous %}{# todo: here we need to look at the anonymity of the last edit instead #}
<span class="anonymous">{{ thread.last_activity_by.get_anonymous_name() }}</span>
{% else %}
<a href="{% url user_profile thread.last_activity_by.id, thread.last_activity_by.username|slugify %}">{{thread.last_activity_by.username|escape}}</a> {{ user_country_flag(thread.last_activity_by) }}
+ {% if thread.last_activity_by.get_primary_group() %}
+ - {{ user_primary_group(thread.last_activity_by) }}
+ {% endif %}
{% endif %}
- {% if thread.last_activity_by.get_primary_group() %}-{% endif %}
- {{ user_primary_group(thread.last_activity_by) }}
</div>
</div>
- <h2><a title="{{question.summary|escape}}" href="{{ question.get_absolute_url(thread=thread) }}">{{thread.get_title(question)|escape}}</a></h2>
+ <h2><a href="{{ question.get_absolute_url(thread=thread) }}">{{thread.get_title()|escape}}</a></h2>
{{ tag_list_widget(thread.get_tag_names(), search_state=search_state) }}
</div>
diff --git a/askbot/templates/widgets/scope_nav.html b/askbot/templates/widgets/scope_nav.html
index 254c3b48..155667ac 100644
--- a/askbot/templates/widgets/scope_nav.html
+++ b/askbot/templates/widgets/scope_nav.html
@@ -14,20 +14,20 @@
{% if settings.ALL_SCOPE_ENABLED %}
<a class="scope-selector {% if scope == 'all' %}on{% endif %}"
href="{{ search_state.change_scope('all').full_url() }}"
- title="{% trans %}see all questions{% endtrans %}">{% trans %}ALL{% endtrans %}</a>
+ >{% trans %}ALL{% endtrans %}</a>
{% endif %}
{% if settings.UNANSWERED_SCOPE_ENABLED %}
<a class="scope-selector {% if scope == 'unanswered' %}on{% endif %}"
href="{{ search_state.change_scope('unanswered').change_sort('answers-asc').full_url() }}"
- title="{% trans %}see unanswered questions{% endtrans %}">{% trans %}UNANSWERED{% endtrans %}</a>
+ >{{ settings.WORDS_UNANSWERED|escape }}</a>
{% endif %}
{% if request.user.is_authenticated() and settings.FOLLOWED_SCOPE_ENABLED %}
<a class="scope-selector {% if scope == 'followed' %}on{% endif %}"
href="{{ search_state.change_scope('followed').full_url() }}"
- title="{% trans %}see your followed questions{% endtrans %}">{% trans %}FOLLOWED{% endtrans %}</a>
+ >{% trans %}FOLLOWED{% endtrans %}</a>
{% endif %}
{% else %}
- <div class="scope-selector ask-message">{% trans %}Please ask your question here{% endtrans %}</div>
+ <div class="scope-selector ask-message">{{ settings.WORDS_PLEASE_ASK_YOUR_QUESTION_HERE|escape }}</div>
{% endif %}
{% endif %}
</div>
diff --git a/askbot/templates/widgets/secondary_header.html b/askbot/templates/widgets/secondary_header.html
index 69e5742c..92f34cff 100644
--- a/askbot/templates/widgets/secondary_header.html
+++ b/askbot/templates/widgets/secondary_header.html
@@ -19,28 +19,27 @@
{% endif %}
class="{{ enabled_scopes_class }}"
method="get">
- <div>
- {#
- Some or all contents of this div may be dropped
- over the search bar via negative margins,
- to make sure that the search bar can occupy 100%
- of the content width.
- Search bar may have padding on the left and right
- to accomodate the buttons.
- #}
- <a id="homeButton" href="{% url questions %}"></a>
- {% include "widgets/scope_nav.html" %}
- {#
- three buttons below are in the opposite order because
- they are floated at the right
- #}
- {% if settings.ASK_BUTTON_ENABLED %}
- {% include "widgets/ask_button.html" %}
- {% endif %}
- {# clears button floats #}
- <div class="clearfix"></div>
- </div>
- {% include "widgets/search_bar.html" %} {# include search form widget #}
+ {#
+ A single row table to help the semi-fixed width layout where:
+ * all cells have "tight width" without linebreaks
+ * except that the search bar cell takes the remaining width
+ We had hard time making this layout work without tables.
+ Please suggest if there is a better way.
+ #}
+ <table width="100%">
+ <tr>
+ {# width 1 means that cell will expand just enough to fit the contents #}
+ <td width="1"><a id="homeButton" href="{% url questions %}"></a></td>
+ <td width="1">{% include "widgets/scope_nav.html" %}</td>
+ {# width * means that the cell takes the remaining table width #}
+ <td width="*" class="search-bar">{% include "widgets/search_bar.html" %}</td>
+ {% if settings.ASK_BUTTON_ENABLED %}
+ <td width="1">
+ {% include "widgets/ask_button.html" %}
+ </td>
+ {% endif %}
+ </tr>
+ </table>
</form>
</div>
</div>
diff --git a/askbot/templates/widgets/system_messages.html b/askbot/templates/widgets/system_messages.html
index 10ba4a84..69f6672f 100644
--- a/askbot/templates/widgets/system_messages.html
+++ b/askbot/templates/widgets/system_messages.html
@@ -1,8 +1,10 @@
<div class="notify" style="display:none">
- {% if user_messages %}
- {% for message in user_messages %}
- <p class="notification">{{ message }}</p>
- {% endfor %}
- {% endif %}
- <a id="closeNotify" onclick="notify.close(true)"></a>
+ <div class="content-wrapper">
+ {% if user_messages %}
+ {% for message in user_messages %}
+ <p class="notification">{{ message }}</p>
+ {% endfor %}
+ {% endif %}
+ <a id="closeNotify" onclick="notify.close(true)"></a>
+ </div>
</div>
diff --git a/askbot/templates/widgets/tag_selector.html b/askbot/templates/widgets/tag_selector.html
index ba304d2c..720de4a1 100644
--- a/askbot/templates/widgets/tag_selector.html
+++ b/askbot/templates/widgets/tag_selector.html
@@ -56,7 +56,7 @@
<input id="subscribedTagAdd" type="submit" value="{% trans %}add{% endtrans%}"/>
</div>
{% endif %}
- <h3>{% trans %}Show only questions from{% endtrans%}</h3>
+ <h3>{{ settings.WORDS_SHOW_ONLY_QUESTIONS_FROM|escape }}</h3>
<div id="displayTagFilterControl">
{{
macros.radio_select(
diff --git a/askbot/templates/widgets/three_column_category_selector.html b/askbot/templates/widgets/three_column_category_selector.html
index ab0886c6..c6e10460 100644
--- a/askbot/templates/widgets/three_column_category_selector.html
+++ b/askbot/templates/widgets/three_column_category_selector.html
@@ -1,7 +1,7 @@
{# just a skeleton for the category selector - filled by js #}
<table class="category-selector">
<thead>
- <th colspan="3">{% trans %}Categorize your question using this tag selector or entering text in tag box.{% endtrans %}
+ <th colspan="3">{{ settings.WORDS_INSTRUCTION_FOR_THE_CATEGORY_SELECTOR|escape }}
<a style="display:none;"
class='category-editor-toggle'
data-on-state-text='{% trans %}(done editing){% endtrans %}'
diff --git a/askbot/templates/widgets/user_perms.html b/askbot/templates/widgets/user_perms.html
index 887a712f..33109140 100644
--- a/askbot/templates/widgets/user_perms.html
+++ b/askbot/templates/widgets/user_perms.html
@@ -18,7 +18,7 @@
{% if not user.is_administrator_or_moderator() %}
<p> {% trans %}Currently, you can:{% endtrans %}</p>
<ul>
- <li>{% trans %}Post questions, answers and comments{% endtrans %}</li>
+ <li>{% trans %}Make new posts{% endtrans %}</li>
{% for perm in perms_data %}
{% if user.reputation >= perm[1] %}
<li>{{ perm[0] }}</li>
diff --git a/askbot/templatetags/extra_filters_jinja.py b/askbot/templatetags/extra_filters_jinja.py
index c4121ed3..0d7ab0c8 100644
--- a/askbot/templatetags/extra_filters_jinja.py
+++ b/askbot/templatetags/extra_filters_jinja.py
@@ -13,6 +13,7 @@ from django.template import defaultfilters
from django.core.urlresolvers import reverse, resolve
from django.http import Http404
from django.utils import simplejson
+from django.utils.text import truncate_html_words
from askbot import exceptions as askbot_exceptions
from askbot.conf import settings as askbot_settings
from django.conf import settings as django_settings
@@ -22,6 +23,7 @@ from askbot.utils.html import site_url as site_url_func
from askbot.utils import functions
from askbot.utils import url_utils
from askbot.utils.slug import slugify
+from askbot.utils.pluralization import py_pluralize as _py_pluralize
from askbot.shims.django_shims import ResolverMatch
from django_countries import countries
@@ -93,6 +95,13 @@ def strip_path(url):
return url_utils.strip_path(url)
@register.filter
+def can_see_private_user_data(viewer, target):
+ if viewer.is_authenticated() and viewer.is_administrator_or_moderator():
+ #todo: take into account intersection of viewer and target user groups
+ return askbot_settings.SHOW_ADMINS_PRIVATE_USER_DATA
+ return False
+
+@register.filter
def clean_login_url(url):
"""pass through, unless user was originally on the logout page"""
try:
@@ -120,6 +129,15 @@ def transurl(url):
return url
@register.filter
+def truncate_html_post(post_html):
+ """truncates html if it is longer than 100 words"""
+ post_html = truncate_html_words(post_html, 5)
+ post_html = '<div class="truncated-post">' + post_html
+ post_html += '<span class="expander">(<a>' + _('more') + '</a>)</span>'
+ post_html += '<div class="clearfix"></div></div>'
+ return post_html
+
+@register.filter
def country_display_name(country_code):
country_dict = dict(countries.COUNTRIES)
return country_dict[country_code]
@@ -340,6 +358,10 @@ def humanize_counter(number):
else:
return str(number)
+@register.filter
+def py_pluralize(source, count):
+ plural_forms = source.strip().split('\n')
+ return _py_pluralize(plural_forms, count)
@register.filter
def absolute_value(number):
diff --git a/askbot/templatetags/extra_tags.py b/askbot/templatetags/extra_tags.py
index a74438de..68fea7e4 100644
--- a/askbot/templatetags/extra_tags.py
+++ b/askbot/templatetags/extra_tags.py
@@ -15,7 +15,7 @@ GRAVATAR_TEMPLATE = (
'<a style="text-decoration:none" '
'href="%(user_profile_url)s"><img class="gravatar" '
'width="%(size)s" height="%(size)s" '
- 'src="//www.gravatar.com/avatar/%(gravatar_hash)s'
+ 'src="%(gravatar_url)s/%(gravatar_hash)s'
'?s=%(size)s&amp;d=%(gravatar_type)s&amp;r=PG" '
'title="%(username)s" '
'alt="%(alt_text)s" /></a>')
@@ -37,6 +37,7 @@ def gravatar(user, size):
)
#safe_username = template.defaultfilters.urlencode(username)
return mark_safe(GRAVATAR_TEMPLATE % {
+ 'gravatar_url': askbot_settings.GRAVATAR_BASE_URL,
'user_profile_url': user_profile_url,
'size': size,
'gravatar_hash': functions.get_from_dict_or_object(user, 'gravatar'),
diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py
index 5facdb7d..f913da21 100644
--- a/askbot/tests/__init__.py
+++ b/askbot/tests/__init__.py
@@ -24,4 +24,5 @@ from askbot.tests.user_views_tests import *
from askbot.tests.utils_tests import *
from askbot.tests.view_context_tests import *
from askbot.tests.api_v1_tests import *
+from askbot.tests.jive_tests import *
from askbot.tests.signal_handler_tests import *
diff --git a/askbot/tests/db_api_tests.py b/askbot/tests/db_api_tests.py
index 6b08fb65..3e19a1c2 100644
--- a/askbot/tests/db_api_tests.py
+++ b/askbot/tests/db_api_tests.py
@@ -199,10 +199,10 @@ class DBApiTests(AskbotTestCase):
def test_retag_tags_too_long_raises(self):
tags = "aoaoesuouooeueooeuoaeuoeou aostoeuoaethoeastn oasoeoa nuhoasut oaeeots aoshootuheotuoehao asaoetoeatuoasu o aoeuethut aoaoe uou uoetu uouuou ao aouosutoeh"
question = self.post_question(user=self.user)
- self.assertRaises(
- exceptions.ValidationError,
- self.user.retag_question,
- question=question, tags=tags
+ self.user.retag_question(question=question, tags=tags)
+ self.assertEqual(
+ set(question.thread.tagnames.split()),
+ set('aoaoesuouooeueooeuoaeuoeou aostoeuoaethoeastn oasoeoa nuhoasut oaeeots aoshootuheotuoehao asaoetoeatuoasu o aoeuethut aoaoe'.split())
)
def test_search_with_apostrophe_works(self):
diff --git a/askbot/tests/email_alert_tests.py b/askbot/tests/email_alert_tests.py
index e2f328fc..902b810d 100644
--- a/askbot/tests/email_alert_tests.py
+++ b/askbot/tests/email_alert_tests.py
@@ -10,6 +10,7 @@ import django.core.mail
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import Client
+from django.utils import translation
from askbot.tests import utils
from askbot.tests.utils import with_settings
from askbot import models
@@ -155,6 +156,7 @@ class EmailAlertTests(TestCase):
between the default version (defined in the decorator) and the
desired version in the "real" test
"""
+ translation.activate('en')
pass
def setUpUsers(self):
diff --git a/askbot/tests/email_parsing_tests.py b/askbot/tests/email_parsing_tests.py
index dc1e82f2..b87bbebe 100644
--- a/askbot/tests/email_parsing_tests.py
+++ b/askbot/tests/email_parsing_tests.py
@@ -5,6 +5,7 @@ from django.template.loader import get_template
from askbot import mail
from askbot import models
from askbot.tests import utils
+from askbot.utils.html import get_text_from_html
class EmailParsingTests(utils.AskbotTestCase):
@@ -16,10 +17,6 @@ class EmailParsingTests(utils.AskbotTestCase):
self.rendered_template = template.render(Context(self.context))
self.expected_output = 'Welcome to askbot.com!\n\nImportant: Please reply to this message, without editing it. We need this to determine your email signature and that the email address is valid and was typed correctly.\n\nUntil we receive the response from you, you will not be able ask or answer questions on askbot.com by email.\n\nSincerely,askbot.com Administrator\n\nDwFwndQty'
- def test_clean_email_body(self):
- cleaned_body = mail.clean_html_email(self.rendered_template)
- self.assertEqual(self.expected_output, cleaned_body)
-
def test_gmail_rich_text_response_stripped(self):
text = u'\n\nthis is my reply!\n\nOn Wed, Oct 31, 2012 at 1:45 AM, <kp@kp-dev.askbot.com> wrote:\n\n> **\n> '
self.assertEqual(mail.extract_reply(text), 'this is my reply!')
@@ -65,3 +62,26 @@ Subject: "One more test question from email."
"""
self.assertEqual(mail.extract_reply(text), "some real text")
+
+ def test_blackberry(self):
+
+ text = """Lorem ipsum lorem ipsum
+blah blah blah
+
+some more text here
+
+Joe
+
+________________________________________
+From: forum@ask.askbot.com
+Sent: Thursday, August 15, 2013 1:58:21 AM
+To: Mister Joe
+Subject: Our forum: "some text in the subject line"
+"""
+ expected = """Lorem ipsum lorem ipsum
+blah blah blah
+
+some more text here
+
+Joe"""
+ self.assertEqual(mail.extract_reply(text), expected)
diff --git a/askbot/tests/form_tests.py b/askbot/tests/form_tests.py
index fed38f87..c21ac5bf 100644
--- a/askbot/tests/form_tests.py
+++ b/askbot/tests/form_tests.py
@@ -311,8 +311,7 @@ class UserNameFieldTest(AskbotTestCase):
#invalid username and username in reserved words
self.assertRaises(django_forms.ValidationError, self.username_field.clean, ' ')
self.assertRaises(django_forms.ValidationError, self.username_field.clean, 'fuck')
- self.assertRaises(django_forms.ValidationError, self.username_field.clean, '......')
-
+ self.assertEqual(self.username_field.clean('......'), '......')
#TODO: test more things
class AnswerEditorFieldTests(AskbotTestCase):
diff --git a/askbot/tests/jive_tests.py b/askbot/tests/jive_tests.py
new file mode 100644
index 00000000..f99be71e
--- /dev/null
+++ b/askbot/tests/jive_tests.py
@@ -0,0 +1,280 @@
+import unittest
+from askbot.utils.jive import JiveConverter
+from askbot.utils.jive import internal_link_re as link_re
+
+#class MockPost(object):
+# def __init__(self):
+# self.text = ''
+# def get_absolute_url(self):
+# return '/url'
+
+class JiveTests(unittest.TestCase):
+
+ def setUp(self):
+ self.converter = JiveConverter()
+
+ def convert(self, text):
+ return self.converter.convert(text)
+
+ def test_headings(self):
+ text = """
+h1. Heading1
+blah blah
+h2. Heading 2
+blah blah
+h3. Heading 3
+blah blah
+"""
+ expected = """<h1>Heading1</h1>
+<p>blah blah</p>
+<h2>Heading 2</h2>
+<p>blah blah</p>
+<h3>Heading 3</h3>
+<p>blah blah</p>"""
+ output = self.convert(text)
+ self.assertEqual(output, expected)
+
+ def test_horizontal_rules(self):
+ text = """
+-----
+some text
+"""
+ expected = '<hr/>\n<p>some text</p>'
+ self.assertEqual(self.convert(text), expected)
+
+ def test_list1(self):
+ text = """
+* one
+* two
+* three
+"""
+ expected = """<ul>
+<li>one</li>
+<li>two</li>
+<li>three</li>
+</ul>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_list2(self):
+ text = """
+# one
+# two
+# three
+"""
+ expected = """<ol>
+<li>one</li>
+<li>two</li>
+<li>three</li>
+</ol>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_list3(self):
+ text = """
+* one
+** two
+* three
+"""
+ expected = """<ul>
+<li>one</li>
+<li>
+<ul>
+<li>two</li>
+</ul>
+</li>
+<li>three</li>
+</ul>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_bq1(self):
+ text = """
+bq. two plus two equals four
+"""
+ expected = """<blockquote><p>two plus two equals four</p></blockquote>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_bq2(self):
+ text = """{quote}
+two plus two equals four
+{quote}
+"""
+ expected = """<blockquote><p>two plus two equals four</p></blockquote>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_bq3(self):
+ text = """[quote=alex]
+two plus two equals four
+{quote}
+"""
+ expected = """<blockquote><span class="quote-header">alex wrote:</span><br/>
+<p>two plus two equals four</p></blockquote>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_bq4(self):
+ text = """[quote=alex]
+two plus two equals four
+
+two plus two equals four
+{quote}
+"""
+ expected = """<blockquote><span class="quote-header">alex wrote:</span><br/>
+<p>two plus two equals four</p>
+<p>two plus two equals four</p></blockquote>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_bq5(self):
+ text = """> {quote:title=alex wrote:}{quote}
+> two plus two equals four
+>
+> two plus two equals four
+"""
+ expected = """<blockquote><span class="quote-header">alex wrote:</span><br/>
+<p>two plus two equals four</p>
+<p>two plus two equals four</p></blockquote>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_code0(self):
+ text = """something {code}#comment _haha_ http://example.com {code}"""
+ expected = """<p>something</p>
+<pre><code>#comment _haha_ http://example.com </code></pre>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_code1(self):
+ text = """something {code:html}#comment _haha_ http://example.com {code}"""
+ expected = """<p>something</p>
+<pre><code>#comment _haha_ http://example.com </code></pre>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_links1(self):
+ text = """[url]http://example.com/2[/url] blah
+http://example.com/1 blah
+[link text3|http://example.com/3|tooltip text3] blah2
+[link text4|http://example.com/4|tooltip text4]
+!http://example.com/img.png!
+[email@example.com]
+[/some/file/]
+"""
+ expected = """<p><a href="http://example.com/2">http://example.com/2</a> blah<br/>
+<a href="http://example.com/1">http://example.com/1</a> blah<br/>
+<a href="http://example.com/3" title="tooltip text3">link text3</a> blah2 <br/>
+<a href="http://example.com/4" title="tooltip text4">link text4</a><br/>
+<img src="http://example.com/img.png"/><br/>
+<a href="mailto:email@example.com">email@example.com</a><br/>
+<a href="/some/file/">/some/file/</a></p>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_bold(self):
+ text = "*some text*"
+ self.assertEqual(
+ self.convert(text),
+ '<p><strong>some text</strong></p>'
+ )
+
+ def test_italics(self):
+ text = "+some text+"
+ self.assertEqual(
+ self.convert(text),
+ '<p><em>some text</em></p>'
+ )
+
+ def test_underline(self):
+ text = "_some text_"
+ self.assertEqual(
+ self.convert(text),
+ '<p><span class="underline">some text</span></p>'
+ )
+
+ def test_super(self):
+ text = "e = mc^2^"
+ self.assertEqual(
+ self.convert(text),
+ '<p>e = mc<sup>2</sup></p>'
+ )
+
+ def test_sub(self):
+ text = "e~1~"
+ self.assertEqual(
+ self.convert(text),
+ '<p>e<sub>1</sub></p>'
+ )
+
+ def test_strike(self):
+ text = "--A--"
+ self.assertEqual(
+ self.convert(text),
+ '<p><strike>A</strike></p>'
+ )
+
+ def test_leading_spaces(self):
+ """test lazy copy-pasted code"""
+ text = """
+function() {
+ alert('hi');
+}
+"""
+ expected = """<p>function() {<br/>
+&nbsp;&nbsp;&nbsp;&nbsp;alert('hi');<br/>
+}</p>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_fancy(self):
+ text = """
+h1. Once [upon|http://example.com] a *time*
+There was a queen who said:
+{quote}
+I _find_ *this* interesting
+
+{code}e = mc^2;{code}
+
+As you said:
+
+# one
+# two
+# *three*
+# [four|http://example.com/four|item four]
+# *five*, --six--, +seven+, _eight_
+{quote}
+h2. Another time
+Nothing happened.
+"""
+ expected = """<h1>Once <a href="http://example.com">upon</a> a <strong>time</strong></h1>
+<p>There was a queen who said:</p>
+<blockquote><p>I <span class="underline">find</span> <strong>this</strong> interesting</p>
+<pre><code>e = mc^2;</code></pre>
+<p>As you said:</p>
+<ol>
+<li>one</li>
+<li>two</li>
+<li><strong>three</strong></li>
+<li><a href="http://example.com/four" title="item four">four</a></li>
+<li><strong>five</strong>, <strike>six</strike>, <em>seven</em>, <span class="underline">eight</span></li>
+</ol></blockquote>
+<h2>Another time</h2>
+<p>Nothing happened.</p>"""
+ self.assertEqual(self.convert(text), expected)
+
+ def test_internal_link_re(self):
+ self.assertTrue(link_re.search('soasao /message.jspa?messageID=8477 a'))
+ self.assertTrue(link_re.search('/thread.jspa?messageID=10175&amp;#10175'))
+ self.assertTrue(link_re.search('/thread.jspa?messageID=10662#10662'))
+ self.assertTrue(link_re.search('/thread.jspa?messageID=11058'))
+ self.assertTrue(link_re.search('/thread.jspa?threadID=1888&amp;tstart=210'))
+ self.assertTrue(link_re.search('/thread.jspa?threadID=3087&amp;tstart=-258'))
+
+# def test_fix_internal_links1(self):
+# from askbot.management.commands.askbot_import_jive import fix_internal_links_in_post
+# post = MockPost()
+# post.text = """/message.jspa?messageID=8477 sometext
+#sometext /thread.jspa?messageID=10175&amp;#10175 sometext
+#[sometext|/thread.jspa?messageID=10662#10662] [/thread.jspa?messageID=11058]
+#[sometext|/thread.jspa?threadID=1888&amp;tstart=210|title]
+#/thread.jspa?threadID=3087&amp;tstart=-258"""
+# expected = """<a href="/url">/url</a> sometext<br/>
+#sometext <a href="/url">/url</a> sometext<br/>
+#<a href="/url">sometext</a> <a href="/url">/url</a><br/>
+#<a href="/url" title="title">sometext</a><br/>
+#<a href="/url">/url</a>"""
+# fix_internal_links_in_post(post)
+# self.assertEqual(post.text, expected)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/askbot/tests/page_load_tests.py b/askbot/tests/page_load_tests.py
index 07c5f18d..1143d056 100644
--- a/askbot/tests/page_load_tests.py
+++ b/askbot/tests/page_load_tests.py
@@ -54,6 +54,7 @@ class PageLoadTestCase(AskbotTestCase):
#
@classmethod
def setUpClass(cls):
+ super(PageLoadTestCase, cls).setUpClass()
management.call_command('flush', verbosity=0, interactive=False)
activate_language(settings.LANGUAGE_CODE)
management.call_command('askbot_add_test_content', verbosity=0, interactive=False)
@@ -203,7 +204,6 @@ class PageLoadTestCase(AskbotTestCase):
"""test all reader views thoroughly
on non-crashiness (no correcteness tests here)
"""
-
self.try_url('sitemap')
self.try_url(
'get_groups_list',
diff --git a/askbot/tests/permission_assertion_tests.py b/askbot/tests/permission_assertion_tests.py
index 9d549450..16a5afde 100644
--- a/askbot/tests/permission_assertion_tests.py
+++ b/askbot/tests/permission_assertion_tests.py
@@ -322,7 +322,6 @@ class CloseQuestionPermissionAssertionTests(utils.AskbotTestCase):
self.create_user(username = 'other_user')
self.question = self.post_question()
self.min_rep = askbot_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS
- self.min_rep_own = askbot_settings.MIN_REP_TO_CLOSE_OWN_QUESTIONS
def assert_can_close(self, user = None):
user.assert_can_close_question(self.question)
@@ -359,11 +358,10 @@ class CloseQuestionPermissionAssertionTests(utils.AskbotTestCase):
def test_low_rep_owner_cannot_close(self):
assert(self.user.reputation < self.min_rep)
- assert(self.user.reputation < self.min_rep_own)
- self.assert_cannot_close(user = self.user)
+ self.assert_can_close(user=self.user)
def test_high_rep_owner_can_close(self):
- self.user.reputation = self.min_rep_own
+ self.user.reputation = self.min_rep
self.assert_can_close(user = self.user)
def test_high_rep_other_can_close(self):
@@ -380,14 +378,9 @@ class CloseQuestionPermissionAssertionTests(utils.AskbotTestCase):
self.other_user.reputation = self.min_rep
self.assert_cannot_close(user = self.other_user)
- def test_medium_rep_blocked_owner_cannot_close(self):
- self.user.set_status('b')
- self.user.reputation = self.min_rep_own
- self.assert_cannot_close(user = self.user)
-
def test_high_rep_blocked_owner_cannot_close(self):
self.user.set_status('b')
- self.user.reputation = self.min_rep
+ self.user.reputation = 2*self.min_rep
self.assert_cannot_close(user = self.user)
def test_low_rep_suspended_cannot_close(self):
@@ -402,12 +395,12 @@ class CloseQuestionPermissionAssertionTests(utils.AskbotTestCase):
def test_medium_rep_suspended_owner_cannot_close(self):
self.user.set_status('s')
- self.user.reputation = self.min_rep_own
+ self.user.reputation = self.min_rep
self.assert_cannot_close(user = self.user)
def test_high_rep_suspended_owner_cannot_close(self):
self.user.set_status('s')
- self.user.reputation = self.min_rep
+ self.user.reputation = 2*self.min_rep
self.assert_cannot_close(user = self.user)
@@ -423,7 +416,7 @@ class ReopenQuestionPermissionAssertionTests(utils.AskbotTestCase):
"""
def setUp(self):
- self.min_rep = askbot_settings.MIN_REP_TO_REOPEN_OWN_QUESTIONS
+ self.min_rep = askbot_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS
self.create_user()
self.create_user(username = 'other_user')
self.question = self.post_question()
@@ -1152,11 +1145,14 @@ class CommentPermissionAssertionTests(PermissionAssertionTestCase):
)
def test_suspended_user_can_comment_own_question(self):
+ #post question
question = self.post_question()
+ #suspend the poster
self.user.set_status('s')
+ #attempt to post a comment under the same question
comment = self.user.post_comment(
- parent_post = question,
- body_text = 'test comment'
+ parent_post=question,
+ body_text='test comment'
)
self.assertTrue(isinstance(comment, models.Post) and comment.is_comment())
self.assertTrue(
diff --git a/askbot/tests/post_model_tests.py b/askbot/tests/post_model_tests.py
index 2e785802..6d9233a2 100644
--- a/askbot/tests/post_model_tests.py
+++ b/askbot/tests/post_model_tests.py
@@ -365,7 +365,7 @@ class ThreadRenderLowLevelCachingTests(AskbotTestCase):
cache.cache = LocMemCache('', {}) # Enable local caching
thread = self.q.thread
- key = Thread.SUMMARY_CACHE_KEY_TPL % thread.id
+ key = Thread.SUMMARY_CACHE_KEY_TPL % (thread.id, 'en')
self.assertTrue(thread.summary_html_cached())
self.assertIsNotNone(thread.get_cached_summary_html())
diff --git a/askbot/tests/user_model_tests.py b/askbot/tests/user_model_tests.py
index b11fb151..786991a0 100644
--- a/askbot/tests/user_model_tests.py
+++ b/askbot/tests/user_model_tests.py
@@ -31,7 +31,8 @@ class UserModelTests(AskbotTestCase):
bulk_subscription = models.BulkTagSubscription.objects.create(
tag_names=[one_tag.name, another_tag.name],
group_list=[global_group],
- tag_author=the_boss
+ tag_author=the_boss,
+ language_code='en'
)
user = self.create_user('someone')
diff --git a/askbot/tests/utils.py b/askbot/tests/utils.py
index 141f229a..bb9a6103 100644
--- a/askbot/tests/utils.py
+++ b/askbot/tests/utils.py
@@ -1,8 +1,10 @@
"""utility functions used by Askbot test cases
"""
+from django.core.cache import cache
from django.test import TestCase
from functools import wraps
from askbot import models
+from askbot.models import signals
def with_settings(**settings_dict):
"""a decorator that will run function with settings
@@ -71,7 +73,6 @@ def create_user(
if date_joined is not None:
user.date_joined = date_joined
user.save()
- user.set_status(status)
if notification_schedule == None:
notification_schedule = models.EmailFeedSetting.NO_EMAIL_SCHEDULE
@@ -86,6 +87,10 @@ def create_user(
subscriber = user
)
feed.save()
+
+ signals.user_registered.send(None, user=user)
+ user.set_status(status)
+
return user
@@ -94,6 +99,10 @@ class AskbotTestCase(TestCase):
to django TestCase class
"""
+ @classmethod
+ def setUpClass(cls):
+ cache.clear()
+
def create_user(
self,
username='user',
@@ -281,7 +290,7 @@ class AskbotTestCase(TestCase):
except models.User.DoesNotExist:
user = self.create_user('tag_creator')
- tag = models.Tag(created_by = user, name = tag_name)
+ tag = models.Tag(created_by=user, name=tag_name, language_code='en')
tag.save()
return tag
diff --git a/askbot/tests/utils_tests.py b/askbot/tests/utils_tests.py
index ed845f48..a04edd82 100644
--- a/askbot/tests/utils_tests.py
+++ b/askbot/tests/utils_tests.py
@@ -3,6 +3,7 @@ from askbot.tests.utils import with_settings
from askbot.utils.url_utils import urls_equal
from askbot.utils.html import absolutize_urls
from askbot.utils.html import replace_links_with_text
+from askbot.utils.html import get_text_from_html
from askbot.conf import settings as askbot_settings
class UrlUtilsTests(TestCase):
@@ -103,3 +104,9 @@ class HTMLUtilsTests(TestCase):
absolutize_urls(text),
'<a href="http://example.com/upfiles/13487909784287052.png"><img src="http://example.com/upfiles/13487909942351405.png" style="max-width:500px;" alt="" /></a><img src="http://i2.cdn.turner.com/cnn/dam/assets/120927033530-ryder-cup-captains-wall-4-tease.jpg" alt="" width="160" height="90" border="0" />and some text<br />aouaosutoaehut'
)
+
+ def test_get_text_from_html(self):
+ self.assertEqual(
+ get_text_from_html('ataoesa uau <a>link</a>aueaotuosu ao <a href="http://cnn.com">CNN!</a>\nnaouaouuau<img> <img src="http://cnn.com/1.png"/> <img src="http://cnn.com/2.png" alt="sometext">'),
+ u'ataoesa uau linkaueaotuosu ao http://cnn.com (CNN!)\n\nnaouaouuau http://cnn.com/1.png http://cnn.com/2.png (sometext)'
+ )
diff --git a/askbot/tests/widget_tests.py b/askbot/tests/widget_tests.py
index 98c5a8aa..73a5f06e 100644
--- a/askbot/tests/widget_tests.py
+++ b/askbot/tests/widget_tests.py
@@ -4,12 +4,15 @@ from askbot import models
from askbot.tests.utils import AskbotTestCase
from django.test.client import Client
+from django.conf import settings as django_settings
from django.core.urlresolvers import reverse
+from django.utils import translation
class WidgetViewsTests(AskbotTestCase):
def setUp(self):
+ translation.activate(django_settings.LANGUAGE_CODE)
self.client = Client()
self.widget = models.AskWidget.objects.create(title='foo widget')
self.user = self.create_user('user1')
@@ -74,6 +77,7 @@ class WidgetLoginViewTest(AskbotTestCase):
class WidgetCreatorViewsTests(AskbotTestCase):
def setUp(self):
+ translation.activate(django_settings.LANGUAGE_CODE)
self.client = Client()
self.user = self.create_user('user1')
self.user.set_password('testpass')
@@ -136,6 +140,7 @@ class WidgetCreatorViewsTests(AskbotTestCase):
class QuestionWidgetViewsTests(AskbotTestCase):
def setUp(self):
+ translation.activate(django_settings.LANGUAGE_CODE)
self.user = self.create_user('testuser')
self.client = Client()
self.widget = models.QuestionWidget.objects.create(title="foo",
diff --git a/askbot/urls.py b/askbot/urls.py
index 8759754c..566173ce 100644
--- a/askbot/urls.py
+++ b/askbot/urls.py
@@ -4,8 +4,14 @@ askbot askbot url configuraion file
import os.path
import django
from django.conf import settings
-from django.conf.urls.defaults import url, patterns, include
-from django.conf.urls.defaults import handler500, handler404
+
+try:
+ from django.conf.urls import url, patterns, include
+ from django.conf.urls import handler500, handler404
+except ImportError:
+ from django.conf.urls.defaults import url, patterns, include
+ from django.conf.urls.defaults import handler500, handler404
+
from django.contrib import admin
from askbot import views
from askbot.feed import RssLastestQuestionsFeed, RssIndividualQuestionFeed
@@ -35,25 +41,37 @@ sitemaps = {
#except those that are namespaced
PREFIX = getattr(settings, 'ASKBOT_SERVICE_URL_PREFIX', '')
+MAIN_PAGE_BASE_URL = getattr(
+ settings,
+ 'ASKBOT_MAIN_PAGE_BASE_URL',
+ _('questions')
+ ).strip('/') + '/'
+QUESTION_PAGE_BASE_URL = getattr(
+ settings,
+ 'ASKBOT_QUESTION_PAGE_BASE_URL',
+ _('question')
+ ).strip('/') + '/'
+
APP_PATH = os.path.dirname(__file__)
urlpatterns = patterns('',
url(r'^$', views.readers.index, name='index'),
# BEGIN Questions (main page) urls. All this urls work both normally and through ajax
url(
# Note that all parameters, even if optional, are provided to the view. Non-present ones have None value.
- (r'^%s' % _('questions') +
+ (r'^%s' % MAIN_PAGE_BASE_URL.strip('/') +
r'(%s)?' % r'/scope:(?P<scope>\w+)' +
r'(%s)?' % r'/sort:(?P<sort>[\w\-]+)' +
r'(%s)?' % r'/tags:(?P<tags>[\w+.#,-]+)' + # Should match: const.TAG_CHARS + ','; TODO: Is `#` char decoded by the time URLs are processed ??
r'(%s)?' % r'/author:(?P<author>\d+)' +
r'(%s)?' % r'/page:(?P<page>\d+)' +
+ r'(%s)?' % r'/page-size:(?P<page_size>\d+)' +
r'(%s)?' % r'/query:(?P<query>.+)' + # INFO: query is last, b/c it can contain slash!!!
r'/$'),
views.readers.questions,
name='questions'
),
url(
- r'^%s(?P<id>\d+)/' % _('question/'),
+ r'^%s(?P<id>\d+)/' % QUESTION_PAGE_BASE_URL,
views.readers.question,
name='question'
),
@@ -107,11 +125,6 @@ urlpatterns = patterns('',
name='groups'
),
url(
- r'^%s$' % _('users/update_has_custom_avatar/'),
- views.users.update_has_custom_avatar,
- name='user_update_has_custom_avatar'
- ),
- url(
r'^%s$' % _('badges/'),
views.meta.badges,
name='badges'
@@ -166,6 +179,11 @@ urlpatterns = patterns('',
kwargs = {'post_type': 'answer'},
name='answer_revisions'
),
+ service_url(
+ r'^get-top-answers/',
+ views.readers.get_top_answers,
+ name='get_top_answers'
+ ),
# END main page urls
service_url(
r'^api/get_questions/',
@@ -218,32 +236,37 @@ urlpatterns = patterns('',
name='get_editor'
),
service_url(
- r'^%s%s$' % (_('questions/'), _('ask/')),
+ r'^get-post-html/',
+ views.readers.get_post_html,
+ name='get_post_html'
+ ),
+ service_url(
+ r'^%s%s$' % (MAIN_PAGE_BASE_URL, _('ask/')),
views.writers.ask,
name='ask'
),
service_url(
- r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('edit/')),
+ r'^%s(?P<id>\d+)/%s$' % (MAIN_PAGE_BASE_URL, _('edit/')),
views.writers.edit_question,
name='edit_question'
),
service_url(#this url is both regular and ajax
- r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('retag/')),
+ r'^%s(?P<id>\d+)/%s$' % (MAIN_PAGE_BASE_URL, _('retag/')),
views.writers.retag_question,
name='retag_question'
),
service_url(
- r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('close/')),
+ r'^%s(?P<id>\d+)/%s$' % (MAIN_PAGE_BASE_URL, _('close/')),
views.commands.close,
name='close'
),
service_url(
- r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('reopen/')),
+ r'^%s(?P<id>\d+)/%s$' % (MAIN_PAGE_BASE_URL, _('reopen/')),
views.commands.reopen,
name='reopen'
),
service_url(
- r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('answer/')),
+ r'^%s(?P<id>\d+)/%s$' % (MAIN_PAGE_BASE_URL, _('answer/')),
views.writers.answer,
name='answer'
),
@@ -253,7 +276,7 @@ urlpatterns = patterns('',
name='vote'
),
service_url(
- r'^%s(?P<id>\d+)/%s$' % (_('questions/'), _('revisions/')),
+ r'^%s(?P<id>\d+)/%s$' % (MAIN_PAGE_BASE_URL, _('revisions/')),
views.readers.revisions,
kwargs = {'post_type': 'question'},
name='question_revisions'
@@ -532,7 +555,7 @@ urlpatterns = patterns('',
name = 'list_widgets'
),
service_url(
- r'^widgets/questions/(?P<widget_id>\d+)/$',
+ r'^widgets/%s(?P<widget_id>\d+)/$' % MAIN_PAGE_BASE_URL,
views.widgets.question_widget,
name='question_widget'
),
diff --git a/askbot/user_messages/context_processors.py b/askbot/user_messages/context_processors.py
index 230e967c..59f348ff 100644
--- a/askbot/user_messages/context_processors.py
+++ b/askbot/user_messages/context_processors.py
@@ -9,7 +9,7 @@ from django.utils.encoding import StrAndUnicode
from askbot.user_messages import get_and_delete_messages
-def user_messages (request):
+def user_messages(request):
"""
Returns session messages for the current session.
@@ -18,16 +18,19 @@ def user_messages (request):
#todo: a hack, for real we need to remove this middleware
#and switch to the new-style session messages
return {}
- messages = request.user.get_and_delete_messages()
- #if request.user.is_authenticated():
- #else:
- # messages = LazyMessages(request)
- #import inspect
- #print inspect.stack()[1]
- #print messages
- return { 'user_messages': messages }
-
-class LazyMessages (StrAndUnicode):
+
+ #the get_and_delete_messages is added to anonymous user by the
+ #ConnectToSessionMessages middleware by the process_request,
+ #however - if the user is logging out via /admin/logout/
+ #the AnonymousUser is installed in the response and thus
+ #the Askbot's session messages hack will fail, so we have
+ #an extra if statement here.
+ if hasattr(request.user, 'get_and_delete_messages'):
+ messages = request.user.get_and_delete_messages()
+ return { 'user_messages': messages }
+ return {}
+
+class LazyMessages(StrAndUnicode):
"""
Lazy message container, so messages aren't actually retrieved from
session and deleted until the template asks for them.
diff --git a/askbot/utils/console.py b/askbot/utils/console.py
index ef318580..b955168c 100644
--- a/askbot/utils/console.py
+++ b/askbot/utils/console.py
@@ -25,7 +25,7 @@ def choice_dialog(prompt_phrase, choices = None, invalid_phrase = None):
assert(not isinstance(choices, basestring))
while 1:
response = raw_input(
- '\n%s (type %s)\n> ' % (prompt_phrase, '/'.join(choices))
+ '\n%s\ntype %s: ' % (prompt_phrase, '/'.join(choices))
)
if response in choices:
return response
diff --git a/askbot/utils/decorators.py b/askbot/utils/decorators.py
index 1cf20059..815e6e2d 100644
--- a/askbot/utils/decorators.py
+++ b/askbot/utils/decorators.py
@@ -42,7 +42,7 @@ def ajax_login_required(view_func):
return view_func(request, *args, **kwargs)
else:
json = simplejson.dumps({'login_required':True})
- return HttpResponseForbidden(json, mimetype='application/json')
+ return HttpResponseForbidden(json, content_type='application/json')
return wrap
@@ -107,7 +107,7 @@ def ajax_only(view_func):
'message': message,
'success': 0
}
- return HttpResponse(simplejson.dumps(data), mimetype='application/json')
+ return HttpResponse(simplejson.dumps(data), content_type='application/json')
if isinstance(data, HttpResponse):#is this used?
data.mimetype = 'application/json'
@@ -115,7 +115,7 @@ def ajax_only(view_func):
else:
data['success'] = 1
json = simplejson.dumps(data)
- return HttpResponse(json, mimetype='application/json')
+ return HttpResponse(json, content_type='application/json')
return wrapper
def check_authorization_to_post(func_or_message):
diff --git a/askbot/utils/file_utils.py b/askbot/utils/file_utils.py
index 3793b5ce..e3e0e5f3 100644
--- a/askbot/utils/file_utils.py
+++ b/askbot/utils/file_utils.py
@@ -4,20 +4,23 @@ import random
import time
import urlparse
from django.core.files.storage import get_storage_class
+from django.conf import settings as django_settings
+
+def make_file_name(ext, prefix=''):
+ name = str(
+ time.time()
+ ).replace(
+ '.', str(random.randint(0,100000))
+ )
+ return prefix + name + ext
def store_file(file_object, file_name_prefix = ''):
"""Creates an instance of django's file storage
object based on the file-like object,
returns the storage object, file name, file url
"""
- file_name = str(
- time.time()
- ).replace(
- '.',
- str(random.randint(0,100000))
- ) + os.path.splitext(file_object.name)[1].lower()
- file_name = file_name_prefix + file_name
-
+ file_ext = os.path.splitext(file_object.name)[1].lower()
+ file_name = make_file_name(file_ext, file_name_prefix)
file_storage = get_storage_class()()
# use default storage to store file
file_storage.save(file_name, file_object)
diff --git a/askbot/utils/forms.py b/askbot/utils/forms.py
index 9ff50506..278f21d1 100644
--- a/askbot/utils/forms.py
+++ b/askbot/utils/forms.py
@@ -267,7 +267,7 @@ class SetPasswordForm(forms.Form):
password2 = forms.CharField(widget=forms.PasswordInput(attrs=login_form_widget_attrs),
label=mark_safe(_('Password <i>(please retype)</i>')),
error_messages={'required':_('please, retype your password'),
- 'nomatch':_('sorry, entered passwords did not match, please try again')},
+ 'nomatch':_('entered passwords did not match, please try again')},
)
def __init__(self, data=None, user=None, *args, **kwargs):
diff --git a/askbot/utils/get_plurals.py b/askbot/utils/get_plurals.py
new file mode 100644
index 00000000..1e30de24
--- /dev/null
+++ b/askbot/utils/get_plurals.py
@@ -0,0 +1,22 @@
+"""reads pluralization formulae from the .po files
+and prints out list of languages for each formula"""
+import sys
+import os.path
+import collections
+
+def find_formula(item):
+ return item.startswith('"Plural-Forms:')
+
+lang_codes = collections.defaultdict(set)
+
+for filename in sys.argv:
+ if not filename.endswith('.po'):
+ continue
+ lines = open(filename).readlines()
+ formula = filter(find_formula, lines)[0]
+ lang = os.path.dirname(os.path.dirname(filename))
+ lang_codes[formula].add(lang.split('/')[-1])
+
+for formula in lang_codes:
+ print lang_codes[formula]
+ print formula
diff --git a/askbot/utils/html.py b/askbot/utils/html.py
index 47165e3b..3aa793f4 100644
--- a/askbot/utils/html.py
+++ b/askbot/utils/html.py
@@ -7,6 +7,7 @@ import re
import htmlentitydefs
from urlparse import urlparse
from django.core.urlresolvers import reverse
+from django.utils.html import strip_tags as strip_all_tags
from django.utils.html import urlize
from askbot.conf import settings as askbot_settings
@@ -63,7 +64,18 @@ def absolutize_urls(html):
#temporal fix for bad regex with wysiwyg editor
return url_re4.sub(replacement, html).replace('%s//' % base_url, '%s/' % base_url)
-def urlize_html(html):
+def get_word_count(html):
+ return len(strip_all_tags(html).split())
+
+def format_url_replacement(url, text):
+ url = url.strip()
+ text = text.strip()
+ url_domain = urlparse(url).netloc
+ if url and text and url_domain != text and url != text:
+ return '%s (%s)' % (url, text)
+ return url or text or ''
+
+def urlize_html(html, trim_url_limit=40):
"""will urlize html, while ignoring link
patterns inside anchors, <pre> and <code> tags
"""
@@ -77,7 +89,7 @@ def urlize_html(html):
#bs4 is weird, so we work around to replace nodes
#maybe there is a better way though
- urlized_text = urlize(node)
+ urlized_text = urlize(node, trim_url_limit=trim_url_limit)
if unicode(node) == urlized_text:
continue
@@ -111,14 +123,6 @@ def replace_links_with_text(html):
"""any absolute links will be replaced with the
url in plain text, same with any img tags
"""
- def format_url_replacement(url, text):
- url = url.strip()
- text = text.strip()
- url_domain = urlparse(url).netloc
- if url and text and url_domain != text and url != text:
- return '%s (%s)' % (url, text)
- return url or text or ''
-
soup = BeautifulSoup(html, 'html5lib')
abs_url_re = r'^http(s)?://'
@@ -141,6 +145,35 @@ def replace_links_with_text(html):
return unicode(soup.find('body').renderContents(), 'utf-8')
+def get_text_from_html(html_text):
+ """Returns the content part from an HTML document
+ retains links and references to images and line breaks.
+ """
+ soup = BeautifulSoup(html_text, 'html5lib')
+
+ #replace <a> links with plain text
+ links = soup.find_all('a')
+ for link in links:
+ url = link.get('href', '')
+ text = ''.join(link.text) or ''
+ link.replaceWith(format_url_replacement(url, text))
+
+ #replace <img> tags with plain text
+ images = soup.find_all('img')
+ for image in images:
+ url = image.get('src', '')
+ text = image.get('alt', '')
+ image.replaceWith(format_url_replacement(url, text))
+
+ #extract and join phrases
+ body_element = soup.find('body')
+ filter_func = lambda s: bool(s.strip())
+ phrases = map(
+ lambda s: s.strip(),
+ filter(filter_func, body_element.get_text().split('\n'))
+ )
+ return '\n\n'.join(phrases)
+
def strip_tags(html, tags=None):
"""strips tags from given html output"""
#a corner case
diff --git a/askbot/utils/jive.py b/askbot/utils/jive.py
new file mode 100755
index 00000000..43b4c4e4
--- /dev/null
+++ b/askbot/utils/jive.py
@@ -0,0 +1,462 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/env python
+# Copyright (c) Askbot S.p.A. 2013
+# License: MIT (http://www.opensource.org/licenses/mit-license.php)
+r"""Converter of Jive markup to markdown,
+some parts and the method are based on the
+python-markdown2 library.
+"""
+__version_info__ = (0, 0, 0)
+__version__ = '.'.join(map(lambda v: str(v), __version_info__))
+__author__ = "Evgeny Fadeev"
+
+import cgi
+import sys
+#from pprint import pprint
+import re
+import logging
+try:
+ from hashlib import md5
+except ImportError:
+ from md5 import md5
+from random import randint
+import codecs
+
+#---- Python version compat
+if sys.version_info[:2] < (2,4):
+ from sets import Set as set
+ def reversed(sequence):
+ for i in sequence[::-1]:
+ yield i
+ def _unicode_decode(s, encoding, errors='xmlcharrefreplace'):
+ return unicode(s, encoding, errors)
+else:
+ def _unicode_decode(s, encoding, errors='strict'):
+ return s.decode(encoding, errors)
+
+
+#---- globals
+
+DEBUG = False
+log = logging.getLogger("jiveMarkup")
+
+DEFAULT_TAB_WIDTH = 4
+
+"""
+Samples from the source (jive_url prefix is stripped)
+/message.jspa?messageID=8477
+/thread.jspa?messageID=10175&amp;#10175
+/thread.jspa?messageID=10662#10662
+/thread.jspa?messageID=11058
+/thread.jspa?threadID=1888&amp;tstart=210
+/thread.jspa?threadID=3087&amp;tstart=-258
+"""
+internal_link_pattern = r"""(?:message|thread) #junk
+ \.jspa\?
+ (message|thread) #link type
+ ID=(\d+) #either post or thread id
+ (?:(?:&amp;)?\#\d+|&amp;tstart=-?\d+)? #junk
+ """
+internal_link_re = re.compile(internal_link_pattern, re.X)
+
+
+try:
+ import uuid
+except ImportError:
+ SECRET_SALT = str(randint(0, 1000000))
+else:
+ SECRET_SALT = str(uuid.uuid4())
+def _hash_ascii(s):
+ #return md5(s).hexdigest() # Markdown.pl effectively does this.
+ return 'md5-' + md5(SECRET_SALT + s).hexdigest()
+def _hash_text(s):
+ return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()
+
+def _regularize_eols(text):
+ """strip eols and replace consecutive eols with single"""
+ text = re.sub(r'\n{2,}', '\n', text)
+ return text.strip('\n')
+
+class JiveConverter(object):
+ """converts Jive Markup into HTML"""
+
+ def __init__(self):
+ self.tab_width = 4
+ self._blocks = dict()
+ self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % self.tab_width, re.M)
+ self.list_level = 0
+
+ def reset(self):
+ """erase the memory of html blocks"""
+ self._blocks = dict()
+ self.list_level = 0
+
+ _ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
+ def convert(self, text):
+ """main function that converts markup --> html.
+ Strategy: go line by line then parse lines.
+ Determine state (e.g. in quote, etc.) then process
+ text according to current state.
+ """
+ self.reset()
+ text = self._normalize(text)
+ text = self._run_block_gamut(text)
+ text = self._unhash_html_blocks(text)
+ return _regularize_eols(text)#maybe prettyfy
+
+ def _hashed(self, html):
+ """hashes html block and returns the hash surrounded by eols"""
+ html_hash = _hash_text(html)
+ self._blocks[html_hash] = html
+ return '\n\n' + html_hash + '\n\n'
+
+ def _normalize(self, text):
+ if not isinstance(text, unicode):
+ #TODO: perhaps shouldn't presume UTF-8 for string input?
+ text = unicode(text, 'utf-8')
+
+ #escape any html special chars globally as in jive they can be anywhere
+ text = cgi.escape(text)
+ #delete "Edited by" comments
+ text = re.sub(r'\n\s*Edited by:[^\n]*(\n|$)', '\n', text)
+ # Standardize line endings:
+ text = re.sub("\r\n|\r", "\n", text)
+ # Convert all tabs to spaces.
+ text = self._detab(text)
+ # Strip any lines consisting only of spaces and tabs.
+ # This makes subsequent regexen easier to write, because we can
+ # match consecutive blank lines with /\n+/ instead of something
+ # contorted like /[ \t]*\n+/ .
+ text = self._ws_only_line_re.sub("", text)
+ # must end with a bunch of empty lines
+ return text + '\n\n'
+
+ # Cribbed from a post by Bart Lateur:
+ # <http://www.nntp.perl.org/group/perl.macperl.anyperl/154>
+ _detab_re = re.compile(r'(.*?)\t', re.M)
+ def _detab_sub(self, match):
+ g1 = match.group(1)
+ return g1 + (' ' * (self.tab_width - len(g1) % self.tab_width))
+
+ def _detab(self, text):
+ r"""Remove (leading?) tabs from a file.
+
+ >>> m = JiveConverter()
+ >>> m._detab("\tfoo")
+ ' foo'
+ >>> m._detab(" \tfoo")
+ ' foo'
+ >>> m._detab("\t foo")
+ ' foo'
+ >>> m._detab(" foo")
+ ' foo'
+ >>> m._detab(" foo\n\tbar\tblam")
+ ' foo\n bar blam'
+ """
+ if '\t' not in text:
+ return text
+ return self._detab_re.subn(self._detab_sub, text)[0]
+
+ def _run_block_gamut(self, text):
+ # These are all the transformations that form block-level
+ # tags like paragraphs, headers, and list items.
+ text = self._do_headers(text)
+ text = self._do_horizontal_rules(text)
+ text = self._do_lists(text)
+ text = self._do_code_blocks(text)
+ text = self._do_block_quotes(text)
+ return self._form_paragraphs(text)
+
+ def _run_span_gamut(self, text):
+ # These are all the transformations that occur *within* block-level
+ # tags like paragraphs, headers, and list items.
+
+ # Process anchor and image tags.
+ text = self._do_links(text)
+
+ text = self._do_inline_styling(text)
+
+ # Do hard breaks:
+ return re.sub(r" {2,}\n", "\n\n", text)
+
+ def _is_auto_link(self, s):
+ if ':' in s and self._auto_link_re.match(s):
+ return True
+ elif '@' in s and self._auto_email_link_re.match(s):
+ return True
+ return False
+
+ def _unhash_html_blocks(self, text):
+ for hash, html in self._blocks.items():
+ text = text.replace(hash, html)
+ return text
+
+ _email_pattern = r'[-.\w]+\@[-\w]+(?:\.[-\w]+)*\.[a-z]+'
+ _url_pattern = "((http|https|ftp)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&amp;%\$\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(\:[0-9]+)*(/($|[a-zA-Z0-9\.\,\?\'\\\+&amp;%\$#\=~_\-]+))*)"
+ _email_re = re.compile(_email_pattern, re.I | re.U)
+ _url_re = re.compile(_url_pattern)
+ _hypertext_link_re1 = re.compile(r'\[(.*?)\]')
+ def _hypertext_link_sub1(self, match):
+ link_bits = match.group(1).split('|')
+ num_bits = len(link_bits)
+ if num_bits == 0:
+ return match.group(0)#return the whole thing
+ elif num_bits == 1:
+ link = link_bits[0]
+ if self._url_re.match(link) or not self._email_re.match(link):
+ return '<a href="%s">%s</a>' % (link, link)
+ elif num_bits in (2, 3):
+ #if self._url_re.match(link_bits[1]):
+ if num_bits == 2:
+ return '<a href="%s">%s</a>' % (link_bits[1], link_bits[0])
+ else:
+ bits = link_bits
+ return '<a href="%s" title="%s">%s</a>' % (bits[1], bits[2], bits[0])
+ return '[' + '|'.join(link_bits) + ']'
+
+ _hypertext_link_re2 = re.compile(r'\[url\]%s\[/url\]' % _url_pattern)
+ def _hypertext_link_sub2(self, match):
+ """convert to plain autolink"""
+ link = match.group(1)
+ return link
+
+ def _do_hypertext_links(self, text):
+ """
+ """
+ #now do these:
+ #[Пример сайта|http://www.example.com|Образец подсказки по сайту]
+ #[Пример сайта|http://www.example.com]
+ #[http://www.example.com]
+ return self._hypertext_link_re1.sub(self._hypertext_link_sub1, text)
+
+ def _image_link_sub(self, match):
+ src = match.group(1)
+ return '<img src="%s"/>' % src
+
+ _image_link_re = re.compile(r'!(.*?)!')
+ def _do_image_links(self, text):
+ """
+ !http://../post.gif!
+ !post.gif!
+ """
+ return self._image_link_re.sub(self._image_link_sub, text)
+
+ def _do_links(self, text):
+ """
+ now do only image and href links
+ todo: more link types https://community.jivesoftware.com/markuphelpfull.jspa
+ """
+ #do this first [url]http://...[/url] --> plain http://...
+ text = self._hypertext_link_re2.sub(self._hypertext_link_sub2, text)
+ text = self._do_hypertext_links(text)
+ text = self._do_image_links(text)
+ #now take care of plain links
+ return self._do_auto_links(text)
+
+ _h_re = re.compile(r'''
+ ^h([1-6])\. # \1 = level
+ [ \t]*
+ (.+?) # \2 = Header text
+ \n+
+ ''', re.X | re.M)
+ def _h_sub(self, match):
+ n = match.group(1)
+ text = self._run_span_gamut(match.group(2))
+ html = '<h%s>%s</h%s>' % (n, text, n)
+ return self._hashed(html)
+
+ def _do_headers(self, text):
+ """convert
+ h1. Header1
+ to <hx></hx>
+ """
+ return self._h_re.sub(self._h_sub, text)
+
+ def _code_block_sub(self, match):
+ code_block = match.group(1).strip('\n')
+ return self._hashed('<pre><code>%s</code></pre>' % code_block)
+
+ def _do_code_blocks(self, text):
+ """Process Markdown `<pre><code>` blocks."""
+ code_block_re = re.compile(r'{code(?:\:\w+)?}(.*?){code}', re.S)
+ return code_block_re.sub(self._code_block_sub, text)
+
+ _bold_re = re.compile(r"\*(?=\S)(.+?[*_]*)(?<=\S)\*")
+ _italics_re = re.compile(r"\+(?=\S)(.+?)(?<=\S)\+")
+ _underline_re = re.compile(r"\_(?=\S)(.+?[*_]*)(?<=\S)\_")
+ _super_re = re.compile(r"\^(?=\S)(.+?)(?<=\S)\^")
+ _sub_re = re.compile(r"\~(?=\S)(.+?)(?<=\S)\~")
+ _strike_re = re.compile(r"--(?=\S)(.+?)(?<=\S)--")
+ def _do_inline_styling(self, text):
+ text = self._bold_re.sub(r'<strong>\1</strong>', text)
+ text = self._italics_re.sub(r'<em>\1</em>', text)
+ text = self._underline_re.sub(r'<span class="underline">\1</span>', text)
+ text = self._super_re.sub(r'<sup>\1</sup>', text)
+ text = self._sub_re.sub(r'<sub>\1</sub>', text)
+ return self._strike_re.sub(r'<strike>\1</strike>', text)
+
+ def _block_quote_sub0(self, match):
+ bq = match.group(1)
+ #span gamut on this type of quote
+ html = '<blockquote><p>%s</p></blockquote>' % self._run_span_gamut(bq)
+ return self._hashed(html)
+
+ _block_quote_re0 = re.compile(r'^bq. (.*?)\n', re.M)
+ def _do_block_quotes0(self, text):
+ """single line block quotes"""
+ return self._block_quote_re0.sub(self._block_quote_sub0, text)
+
+ def _block_quote_sub1(self, match):
+ num_groups = len(match.groups())
+
+ if num_groups == 1:
+ author = None
+ text = match.group(1)
+ else:
+ author = match.group(1)
+ text = match.group(2)
+
+ html = ''
+ if author:
+ #todo: add i18n
+ html = '<span class="quote-header">%s wrote:</span><br/>\n\n' % author
+
+ html += self._run_block_gamut(text)
+ return self._hashed('<blockquote>%s</blockquote>' % html)
+
+ _block_quote_regexes1 = (
+ re.compile(r'^{quote}(.*?){quote}', re.M | re.S),
+ re.compile(r'^\[quote=([^]]+)\](.*?){quote}', re.M | re.S)
+ )
+ def _do_block_quotes1(self, text):
+ """regexable multiline block quotes"""
+ for regex in self._block_quote_regexes1:
+ text = regex.sub(self._block_quote_sub1, text)
+ return text
+
+ _block_quote_sub2_re = re.compile('^&gt; ?', re.M | re.S)
+ def _block_quote_sub2(self, match):
+ title = match.group(1)
+ text = match.group(2)
+ text = self._block_quote_sub2_re.sub('', text)
+ html = '<span class="quote-header">%s:</span><br/>\n\n' % title
+ html = html + self._run_block_gamut(text)
+ return self._hashed('<blockquote>%s</blockquote>' % html)
+
+ _block_quote_re2 = re.compile(
+ r'^&gt; {quote:title\=(.+?):}{quote}\n((:?&gt;(:? .*?)?\n)+)',
+ re.S | re.M
+ )
+ def _do_block_quotes2(self, text):
+ """do block quote of type:
+ &gt; {quote:title=some title:}{quote}
+ &gt; some text
+ &gt;
+ &gt; some more ...
+ """
+ return self._block_quote_re2.sub(self._block_quote_sub2, text)
+
+ def _do_block_quotes(self, text):
+ #single line quotes
+ text = self._do_block_quotes0(text)
+ #potentially multiline quotes
+ text = self._do_block_quotes1(text)
+ return self._do_block_quotes2(text)
+
+ def _horizontal_rules_sub(self, match):
+ return self._hashed('<hr/>')
+
+ def _do_horizontal_rules(self, text):
+ hr_re = re.compile(r'^-{5,}\n', re.M)
+ return hr_re.sub(self._horizontal_rules_sub, text)
+
+ _nested_list_re = re.compile(
+ r'^((#|\*)(#|\*)+\s+(.*?)\n(\2(#|\*)*\s+(.*?)\n)*)',
+ re.M
+ )
+ def _nested_list_sub(self, match):
+ text = match.group(1)
+ #strip the first char of each line (either * or #)
+ text = re.sub(r'^.(.*?)\n', r'\1\n', text)
+ return '<li>%s</li>\n' % self._do_lists(text)
+
+ _list_item_re = re.compile(r'^(:?#|\*)\s+(.*?)\n', re.M)
+ def _list_item_sub(self, match):
+ list_item_html = self._run_span_gamut(match.group(2))
+ return '<li>%s</li>\n' % list_item_html
+
+ def _list_sub(self, match):
+ text = match.group(1)
+ text = self._list_item_re.sub(self._list_item_sub, text)
+ text = self._nested_list_re.sub(self._nested_list_sub, text)
+ tag = (match.group(2) == '*' and 'ul' or 'ol')
+ html = '<%s>\n%s</%s>' % (tag, text, tag)
+ return self._hashed(html)
+
+ _list_re = re.compile(
+ r'^((#|\*)(#|\*)*\s+(.*?)\n(\2(#|\*)*\s+(.*?)\n)*)',
+ re.M
+ )
+ def _do_lists(self, text):
+ #detect the list in the pattern and run the replacement
+ #this is used recursively for the nested lists
+ return self._list_re.sub(self._list_sub, text)
+
+ _leading_blanks_re = re.compile(r'^(\s*)(.*?)\n', re.M)
+ def _leading_blanks_sub(self, match):
+ spaces = match.group(1)
+ text = match.group(2)
+ return '%s%s\n' % ('&nbsp;'*len(spaces), text)
+
+ def _preserve_leading_blanks(self, text):
+ """replace leading blanks with &nbsp;"""
+ return self._leading_blanks_re.sub(self._leading_blanks_sub, text)
+
+ def _form_paragraphs(self, text):
+ # Strip leading and trailing lines:
+ text = text.strip('\n')
+
+ grafs = []
+ for i, graf in enumerate(re.split(r"\n{2,}", text)):
+ if graf in self._blocks:
+ # Unhashify HTML blocks
+ grafs.append(self._blocks[graf])
+ else:
+ graf = self._run_span_gamut(graf.strip())
+ #preserve the line breaks
+ graf = re.sub('\n', '<br/>\n', graf)
+ #convert leading blanks into nbsp
+ graf = self._preserve_leading_blanks(graf)
+ grafs.append('<p>%s</p>' % graf)
+
+ return '\n'.join(grafs)
+
+ _auto_link_re = re.compile("((?<!(href|.src|data)=['\"])%s)" % _url_pattern)
+ def _auto_link_sub(self, match):
+ """auto-links are just passed through"""
+ link = match.group(1)
+ return '<a href="%s">%s</a>' % (link, link)
+
+ _auto_email_link_re = re.compile(r"""
+ \[
+ (
+ %s
+ )
+ \]
+ """ % _email_pattern, re.I | re.X | re.U)
+ def _auto_email_link_sub(self, match):
+ email = match.group(1)
+ return '<a href="mailto:%s">%s</a>' % (email, email)
+
+ def _do_auto_links(self, text):
+ text = self._auto_link_re.sub(self._auto_link_sub, text)
+ return self._auto_email_link_re.sub(self._auto_email_link_sub, text)
+
+ _indent_re = re.compile(r'^(.*?)$', re.M)
+ def _indent(self, text, indent_pattern=r' \1'):
+ # Remove one level of line-leading tabs or spaces
+ return self._indent_re.sub(indent_pattern, text)
+
+ def _outdent(self, text):
+ # Remove one level of line-leading tabs or spaces
+ return self._outdent_re.sub('', text)
diff --git a/askbot/utils/markup.py b/askbot/utils/markup.py
index 61821bba..ffa1fdde 100644
--- a/askbot/utils/markup.py
+++ b/askbot/utils/markup.py
@@ -202,8 +202,8 @@ def plain_text_input_converter(text):
def markdown_input_converter(text):
"""markdown to html converter"""
text = get_parser().convert(text)
- text = urlize_html(text)
- return sanitize_html(text)
+ text = sanitize_html(text)
+ return urlize_html(text)
def tinymce_input_converter(text):
"""tinymce input to production html converter"""
diff --git a/askbot/utils/pluralization.py b/askbot/utils/pluralization.py
new file mode 100644
index 00000000..8d78fa77
--- /dev/null
+++ b/askbot/utils/pluralization.py
@@ -0,0 +1,144 @@
+"""pluralization formulae for the supported languages"""
+import logging
+
+def arabic(count):
+ """six forms for arabic:
+ n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"""
+ if count == 0:
+ return 0
+ elif count == 1:
+ return 1
+ elif count == 2:
+ return 2
+ else:
+ rem = count % 100
+ if rem >= 3 and rem <= 10:
+ return 3
+ if rem >= 11 and rem <= 99:
+ return 4
+ return 5
+
+def germannic(count):
+ """two forms for germannic languages"""
+ return int(count != 1)
+
+def francoid(count):
+ """french, portuguese"""
+ return int(count > 1)
+
+def singular(count):
+ return 0
+
+def slavic(count):
+ """'ru', 'sr', 'hr'"""
+ rem10 = count % 10
+ rem100 = count % 100
+ if rem10 == 1 and rem100 != 11:
+ return 0
+ elif rem10 >=2 and rem10 <= 4 and (rem100 < 10 or rem100 >= 20):
+ return 1
+ return 2
+
+def romanian(count):
+ if count == 1:
+ return 0
+ else:
+ rem100 = count % 100
+ if rem100 > 19 or (count and rem100 == 0):
+ return 2
+ return 1
+
+def polish(count):
+ if count == 1:
+ return 0
+ else:
+ rem10 = count % 10
+ rem100 = count % 100
+ if rem10 >=2 and rem10 <= 4 and (rem100 < 10 and rem100 >= 20):
+ return 1
+ return 2
+
+def slovenian(count):
+ rem100 = count % 100
+ if rem100 == 1:
+ return 0
+ elif rem100 == 2:
+ return 1
+ elif rem100 in (3, 4):
+ return 2
+ return 3
+
+def chech(count):
+ if count == 1:
+ return 0
+ elif count >=2 and count <=4:
+ return 1
+ return 2
+
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+FORMULAE = {
+ 'arabic': arabic,
+ 'germannic': germannic,
+ 'slavic': slavic,
+ 'singular': singular,
+ 'romanian': romanian,
+ 'slovenian': slovenian,
+ 'chech': chech,
+ 'francoid': francoid
+}
+
+GERMANNIC_FAMILY = (
+ 'en', 'bg', 'bg_BG', 'el', 'nb_NO', 'pt', 'ast', 'ca', 'de',
+ 'it', 'hu', 'hi', 'sv_SE', 'fi', 'he_IL', 'gl', 'es', 'bn_IN'
+)
+
+FRANCOID_FAMILY = ('fr', 'pt', 'pt_BR')
+
+SLAVIC_FAMILY = (
+ 'ru', 'sr', 'hr'
+)
+
+ROMANIAN_FAMILY = ('ro',)
+POLISH_FAMILY = ('pl',)
+SLOVENIAN_FAMILY = ('sl',)
+CHECH_FAMILY = ('cs', 'cs_CZ')
+
+SINGULAR_FAMILY = (
+ 'zh_HK', 'fa_IR', 'zh_CN', 'id_ID', 'zh_TW', 'ko', 'ms_MY', 'tr', 'tr_TR', 'vi', 'ja', 'uz', 'uz_UZ'
+)
+
+def get_formula(lang):
+ """returns pluralization formula, default to germannic"""
+ if lang == 'ar':
+ return arabic
+ elif lang in GERMANNIC_FAMILY:
+ return germannic
+ elif lang in SINGULAR_FAMILY:
+ return singular
+ elif lang in SLAVIC_FAMILY:
+ return slavic
+ elif lang in FRANCOID_FAMILY:
+ return francoid
+ elif lang in ROMANIAN_FAMILY:
+ return romanian
+ elif lang in POLISH_FAMILY:
+ return polish
+ elif lang in SLOVENIAN_FAMILY:
+ return slovenian
+ elif lang in SINGULAR_FAMILY:
+ return singular
+ logging.critical('language %s not supported by askbot.utils.pluralization' % lang)
+ return germannic
+
+def py_pluralize(plural_forms, count):
+ from django.utils.translation import get_language
+ lang = get_language()
+ formula = get_formula(lang)
+ num_forms = len(plural_forms)
+ form_number = formula(count)
+ if form_number >= num_forms:
+ template = 'not enough plural forms for %s in language %s'
+ logging.critical(template % (str(plural_forms), lang))
+ form_number = num_forms - 1
+ return plural_forms[form_number]
diff --git a/askbot/utils/slug.py b/askbot/utils/slug.py
index 1c95e1d4..c27c8b79 100644
--- a/askbot/utils/slug.py
+++ b/askbot/utils/slug.py
@@ -17,6 +17,12 @@ from django.utils.encoding import smart_unicode
# Extra characters outside of alphanumerics that we'll allow.
SLUG_OK = '-_~'
+def slugify_camelcase(camel):
+ """Converts CamelCase to camel-case"""
+ def subf(match):
+ return '-' + match.groups(1)[0].lower()
+ return re.sub('([A-Z])', subf, camel).strip('-')
+
def unicode_slugify(s, ok=SLUG_OK, lower=True, spaces=False):
"""Function copied from https://github.com/mozilla/unicode-slugify
@@ -67,4 +73,4 @@ def slugify(input_text, max_length=150):
#apply the cut-off directly
slug = slug[:max_length]
break
- return slug
+ return slug or '_'
diff --git a/askbot/utils/transaction.py b/askbot/utils/transaction.py
new file mode 100644
index 00000000..03d6ecad
--- /dev/null
+++ b/askbot/utils/transaction.py
@@ -0,0 +1,20 @@
+"""Utilities for working with database transactions"""
+
+class DummyTransaction(object):
+ """Dummy transaction class
+ that can be imported instead of the django
+ transaction management and debug issues
+ inside the code running inside the transaction blocks
+ """
+ @classmethod
+ def commit(cls):
+ pass
+
+ @classmethod
+ def commit_manually(cls, func):
+ def decorated(*args, **kwargs):
+ func(*args, **kwargs)
+ return decorated
+
+#a utility instance to use instead of the normal transaction object
+dummy_transaction = DummyTransaction()
diff --git a/askbot/utils/translation.py b/askbot/utils/translation.py
index 7e1231d3..7be7585b 100644
--- a/askbot/utils/translation.py
+++ b/askbot/utils/translation.py
@@ -1,3 +1,6 @@
+"""todo: move here all functions related to languages
+in other utils modules
+"""
from django.conf import settings as django_settings
from askbot.conf import settings as askbot_settings
from django.utils import translation
diff --git a/askbot/utils/url_utils.py b/askbot/utils/url_utils.py
index 3c4fa7b5..a656cde4 100644
--- a/askbot/utils/url_utils.py
+++ b/askbot/utils/url_utils.py
@@ -2,7 +2,10 @@ import os
import urlparse
from django.core.urlresolvers import reverse
from django.conf import settings
-from django.conf.urls.defaults import url
+try:
+ from django.conf.urls import url
+except ImportError:
+ from django.conf.urls.defaults import url
def service_url(*args, **kwargs):
"""adds the service prefix to the url"""
diff --git a/askbot/views/api_v1.py b/askbot/views/api_v1.py
index d55c8462..c8cab137 100644
--- a/askbot/views/api_v1.py
+++ b/askbot/views/api_v1.py
@@ -64,7 +64,7 @@ def info(request):
data['groups'] = 0
json_string = simplejson.dumps(data)
- return HttpResponse(json_string, mimetype='application/json')
+ return HttpResponse(json_string, content_type='application/json')
def user(request, user_id):
'''
@@ -76,7 +76,7 @@ def user(request, user_id):
data['answers'] = models.Post.objects.get_answers(user).count()
data['comments'] = models.Post.objects.filter(post_type='comment').count()
json_string = simplejson.dumps(data)
- return HttpResponse(json_string, mimetype='application/json')
+ return HttpResponse(json_string, content_type='application/json')
def users(request):
@@ -126,7 +126,7 @@ def users(request):
}
json_string = simplejson.dumps(response_dict)
- return HttpResponse(json_string, mimetype='application/json')
+ return HttpResponse(json_string, content_type='application/json')
def question(request, question_id):
@@ -141,7 +141,7 @@ def question(request, question_id):
)
datum = get_question_data(post.thread)
json_string = simplejson.dumps(datum)
- return HttpResponse(json_string, mimetype='application/json')
+ return HttpResponse(json_string, content_type='application/json')
def questions(request):
@@ -196,4 +196,4 @@ def questions(request):
'questions': question_list
}
response_data = simplejson.dumps(ajax_data)
- return HttpResponse(response_data, mimetype='application/json')
+ return HttpResponse(response_data, content_type='application/json')
diff --git a/askbot/views/commands.py b/askbot/views/commands.py
index 8e08c3c4..2ccad9d5 100644
--- a/askbot/views/commands.py
+++ b/askbot/views/commands.py
@@ -25,6 +25,7 @@ from django.template.loader import get_template
from django.views.decorators import csrf
from django.utils import simplejson
from django.utils.html import escape
+from django.utils import translation
from django.utils.translation import ugettext as _
from django.utils.translation import string_concat
from askbot.utils.slug import slugify
@@ -130,7 +131,7 @@ def manage_inbox(request):
response_data['success'] = True
data = simplejson.dumps(response_data)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
else:
raise exceptions.PermissionDenied(
_('Sorry, but anonymous users cannot access the inbox')
@@ -147,7 +148,7 @@ def manage_inbox(request):
response_data['message'] = message
response_data['success'] = False
data = simplejson.dumps(response_data)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
def process_vote(user = None, vote_direction = None, post = None):
@@ -283,9 +284,10 @@ def vote(request):
else:
raise exceptions.PermissionDenied(
- _('Sorry, but anonymous users cannot accept answers')
- )
-
+ _('Sorry, but anonymous users cannot %(perform_action)s') % {
+ 'perform_action': askbot_settings.WORDS_ACCEPT_OR_UNACCEPT_THE_BEST_ANSWER
+ }
+ )
elif vote_type in ('1', '2', '5', '6'):#Q&A up/down votes
###############################
@@ -376,14 +378,17 @@ def vote(request):
question = get_object_or_404(models.Post, post_type='question', id=id)
vote_type = request.POST.get('type')
- #accept answer
if vote_type == '4':
+ #follow question
fave = request.user.toggle_favorite_question(question)
response_data['count'] = models.FavoriteQuestion.objects.filter(thread = question.thread).count()
if fave == False:
response_data['status'] = 1
elif vote_type == '11':#subscribe q updates
+ #todo: this branch is not used anymore
+ #now we just follow question, we don't have the
+ #separate "subscribe" function
user = request.user
if user.is_authenticated():
if user not in question.thread.followed_by.all():
@@ -430,7 +435,7 @@ def vote(request):
response_data['message'] = unicode(e)
response_data['success'] = 0
data = simplejson.dumps(response_data)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
#internally grouped views - used by the tagging system
@csrf.csrf_exempt
@@ -468,12 +473,13 @@ def mark_tag(request, **kwargs):#tagging system
for name in wildcards:
if name in cleaned_wildcards:
tag_usage_counts[name] = models.Tag.objects.filter(
- name__startswith = name[:-1]
+ name__startswith = name[:-1],
+ language_code=translation.get_language()
).count()
else:
tag_usage_counts[name] = 0
- return HttpResponse(simplejson.dumps(tag_usage_counts), mimetype="application/json")
+ return HttpResponse(simplejson.dumps(tag_usage_counts), content_type="application/json")
#@decorators.ajax_only
@decorators.get_only
@@ -507,7 +513,7 @@ def get_thread_shared_users(request):
'users_count': users.count(),
'success': True
})
- return HttpResponse(re_data, mimetype='application/json')
+ return HttpResponse(re_data, content_type='application/json')
@decorators.get_only
def get_thread_shared_groups(request):
@@ -523,7 +529,7 @@ def get_thread_shared_groups(request):
'groups_count': groups.count(),
'success': True
})
- return HttpResponse(re_data, mimetype='application/json')
+ return HttpResponse(re_data, content_type='application/json')
@decorators.ajax_only
def get_html_template(request):
@@ -545,8 +551,9 @@ def get_tag_list(request):
function
"""
tags = models.Tag.objects.filter(
- deleted = False,
- status = models.Tag.STATUS_ACCEPTED
+ deleted=False,
+ status=models.Tag.STATUS_ACCEPTED,
+ language_code=translation.get_language()
)
tag_names = tags.values_list(
@@ -733,12 +740,14 @@ def create_bulk_tag_subscription(request):
tag_names = form.cleaned_data['tags'].split(' ')
user_list = form.cleaned_data.get('users')
group_list = form.cleaned_data.get('groups')
+ lang = translation.get_language()
bulk_subscription = models.BulkTagSubscription.objects.create(
tag_names=tag_names,
tag_author=request.user,
user_list=user_list,
- group_list=group_list
+ group_list=group_list,
+ language_code=lang
)
return HttpResponseRedirect(reverse('list_bulk_tag_subscription'))
@@ -768,12 +777,20 @@ def edit_bulk_tag_subscription(request, pk):
group_ids = [user.id for user in form.cleaned_data['groups']]
bulk_subscription.groups.add(*group_ids)
- tags, new_tag_names = get_tags_by_names(form.cleaned_data['tags'].split(' '))
+ lang = translation.get_language()
+
+ tags, new_tag_names = get_tags_by_names(
+ form.cleaned_data['tags'].split(' '),
+ language_code=lang
+ )
tag_id_list = [tag.id for tag in tags]
for new_tag_name in new_tag_names:
- new_tag = models.Tag.objects.create(name=new_tag_name,
- created_by=request.user)
+ new_tag = models.Tag.objects.create(
+ name=new_tag_name,
+ created_by=request.user,
+ language_code=lang
+ )
tag_id_list.append(new_tag.id)
bulk_subscription.tags.add(*tag_id_list)
@@ -998,7 +1015,7 @@ def delete_post(request):
@csrf.csrf_exempt
def read_message(request):#marks message a read
if request.method == "POST":
- if request.POST['formdata'] == 'required':
+ if request.POST.get('formdata') == 'required':
request.session['message_silent'] = 1
if request.user.is_authenticated():
request.user.delete_messages()
@@ -1246,13 +1263,21 @@ def moderate_suggested_tag(request):
tag_id = form.cleaned_data['tag_id']
thread_id = form.cleaned_data.get('thread_id', None)
+ lang = translation.get_language()
+
try:
- tag = models.Tag.objects.get(id=tag_id)#can tag not exist?
+ tag = models.Tag.objects.get(
+ id=tag_id,
+ language_code=lang
+ )#can tag not exist?
except models.Tag.DoesNotExist:
return
if thread_id:
- threads = models.Thread.objects.filter(id=thread_id)
+ threads = models.Thread.objects.filter(
+ id=thread_id,
+ language_code=lang
+ )
else:
threads = tag.threads.none()
@@ -1509,7 +1534,7 @@ def get_editor(request):
'scripts': parsed_scripts,
'success': True
}
- return HttpResponse(simplejson.dumps(data), mimetype='application/json')
+ return HttpResponse(simplejson.dumps(data), content_type='application/json')
@csrf.csrf_exempt
@decorators.ajax_only
diff --git a/askbot/views/context.py b/askbot/views/context.py
index 2b9ef5ea..eda8f6a7 100644
--- a/askbot/views/context.py
+++ b/askbot/views/context.py
@@ -13,13 +13,15 @@ def get_for_tag_editor():
#data for the tag editor
data = {
'tag_regex': const.TAG_REGEX,
+ 'tag_forbidden_first_chars': const.TAG_FORBIDDEN_FIRST_CHARS,
'tags_are_required': askbot_settings.TAGS_ARE_REQUIRED,
'max_tags_per_post': askbot_settings.MAX_TAGS_PER_POST,
'max_tag_length': askbot_settings.MAX_TAG_LENGTH,
'force_lowercase_tags': askbot_settings.FORCE_LOWERCASE_TAGS,
'messages': {
'required': _(msg.TAGS_ARE_REQUIRED_MESSAGE),
- 'wrong_chars': _(msg.TAG_WRONG_CHARS_MESSAGE)
+ 'wrong_chars': _(msg.TAG_WRONG_CHARS_MESSAGE),
+ 'wrong_first_char': _(msg.TAG_WRONG_FIRST_CHAR_MESSAGE),
}
}
return {'tag_editor_settings': simplejson.dumps(data)}
diff --git a/askbot/views/meta.py b/askbot/views/meta.py
index 692216da..a27f2971 100644
--- a/askbot/views/meta.py
+++ b/askbot/views/meta.py
@@ -9,8 +9,12 @@ from django.core.paginator import Paginator, EmptyPage, InvalidPage
from django.shortcuts import render
from django.template import RequestContext, Template
from django.template.loader import get_template
-from django.http import HttpResponseRedirect, HttpResponse, Http404
+from django.http import Http404
+from django.http import HttpResponse
+from django.http import HttpResponseForbidden
+from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
+from django.utils import translation
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy
from django.views import static
@@ -21,13 +25,16 @@ from askbot.conf import settings as askbot_settings
from askbot.forms import FeedbackForm
from askbot.utils.url_utils import get_login_url
from askbot.utils.forms import get_next_url
-from askbot.mail import mail_moderators
+from askbot.mail import mail_moderators, send_mail
from askbot.models import BadgeData, Award, User, Tag
from askbot.models import badges as badge_data
from askbot.skins.loaders import render_text_into_skin
from askbot.utils.decorators import admins_only
from askbot.utils.forms import get_next_url
from askbot.utils import functions
+from recaptcha_works.decorators import fix_recaptcha_remote_ip
+
+import re
def generic_view(request, template = None, page_class = None):
"""this may be not necessary, since it is just a rewrite of render"""
@@ -35,13 +42,18 @@ def generic_view(request, template = None, page_class = None):
return render_to_response('django_error.html')
return render(request, template, {'page_class': page_class})
+PUBLIC_VARIABLES = ('CUSTOM_CSS', 'CUSTOM_JS')
+
def config_variable(request, variable_name = None, mimetype = None):
"""Print value from the configuration settings
as response content. All parameters are required.
"""
- #todo add http header-based caching here!!!
- output = getattr(askbot_settings, variable_name, '')
- return HttpResponse(output, mimetype = mimetype)
+ if variable_name in PUBLIC_VARIABLES:
+ #todo add http header-based caching here!!!
+ output = getattr(askbot_settings, variable_name, '')
+ return HttpResponse(output, mimetype = mimetype)
+ else:
+ return HttpResponseForbidden()
def about(request, template='about.html'):
title = _('About %(site)s') % {'site': askbot_settings.APP_SHORT_NAME}
@@ -82,6 +94,7 @@ def faq(request):
return render(request, 'faq_static.html', data)
@csrf.csrf_protect
+@fix_recaptcha_remote_ip
def feedback(request):
data = {'page_class': 'meta'}
form = None
@@ -94,10 +107,7 @@ def feedback(request):
return HttpResponseRedirect(redirect_url)
if request.method == "POST":
- form = FeedbackForm(
- is_auth=request.user.is_authenticated(),
- data=request.POST
- )
+ form = FeedbackForm(user=request.user, data=request.POST)
if form.is_valid():
if not request.user.is_authenticated():
@@ -113,18 +123,29 @@ def feedback(request):
headers = {}
if data['email']:
headers = {'Reply-To': data['email']}
-
- mail_moderators(
- _('Q&A forum feedback'),
- message,
- headers=headers
- )
+ subject = _('Q&A forum feedback')
+ if askbot_settings.FEEDBACK_EMAILS:
+ recipients = re.split('\s*,\s*', askbot_settings.FEEDBACK_EMAILS)
+ send_mail(
+ subject_line=subject,
+ body_text=message,
+ headers=headers,
+ recipient_list=recipients,
+ )
+ else:
+ mail_moderators(
+ subject_line=subject,
+ body_text=message,
+ headers=headers
+ )
msg = _('Thanks for the feedback!')
request.user.message_set.create(message=msg)
return HttpResponseRedirect(get_next_url(request))
else:
- form = FeedbackForm(is_auth = request.user.is_authenticated(),
- initial={'next':get_next_url(request)})
+ form = FeedbackForm(
+ user=request.user,
+ initial={'next':get_next_url(request)}
+ )
data['form'] = form
return render(request, 'feedback.html', data)
@@ -188,7 +209,10 @@ def list_suggested_tags(request):
or cancel the moderation reuest."""
if askbot_settings.ENABLE_TAG_MODERATION == False:
raise Http404
- tags = Tag.objects.filter(status = Tag.STATUS_SUGGESTED)
+ tags = Tag.objects.filter(
+ status = Tag.STATUS_SUGGESTED,
+ language_code=translation.get_language()
+ )
tags = tags.order_by('-used_count', 'name')
#paginate moderated tags
paginator = Paginator(tags, 20)
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index 02bd8ce0..80f1d2ec 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -12,7 +12,11 @@ import urllib
import operator
from django.shortcuts import get_object_or_404
from django.shortcuts import render
-from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponseNotAllowed
+from django.http import HttpResponseRedirect
+from django.http import HttpResponse
+from django.http import Http404
+from django.http import HttpResponseNotAllowed
+from django.http import HttpResponseBadRequest
from django.core.paginator import Paginator, EmptyPage, InvalidPage
from django.template.loader import get_template
from django.template import RequestContext
@@ -31,11 +35,13 @@ from django.conf import settings as django_settings
import askbot
from askbot import exceptions
from askbot.utils.diff import textDiff as htmldiff
+from askbot.forms import AnswerForm
+from askbot.forms import ShowQuestionForm
+from askbot.forms import GetUserItemsForm
+from askbot.forms import GetDataForPostForm
from askbot.utils.loading import load_module
-from askbot.forms import AnswerForm, ShowQuestionForm
from askbot import conf
from askbot import models
-from askbot import schedules
from askbot.models.tag import Tag
from askbot import const
from askbot.startup_procedures import domain_is_bad
@@ -83,7 +89,6 @@ def questions(request, **kwargs):
user_logged_in=request.user.is_authenticated(),
**kwargs
)
- page_size = int(askbot_settings.DEFAULT_QUESTIONS_PAGE_SIZE)
qs, meta_data = models.Thread.objects.run_advanced_search(
request_user=request.user, search_state=search_state
@@ -91,7 +96,7 @@ def questions(request, **kwargs):
if meta_data['non_existing_tags']:
search_state = search_state.remove_tags(meta_data['non_existing_tags'])
- paginator = Paginator(qs, page_size)
+ paginator = Paginator(qs, search_state.page_size)
if paginator.num_pages < search_state.page:
search_state.page = 1
page = paginator.page(search_state.page)
@@ -116,12 +121,12 @@ def questions(request, **kwargs):
)
paginator_context = {
- 'is_paginated' : (paginator.count > page_size),
+ 'is_paginated' : (paginator.count > search_state.page_size),
'pages': paginator.num_pages,
'current_page_number': search_state.page,
'page_object': page,
'base_url' : search_state.query_string(),
- 'page_size' : page_size,
+ 'page_size' : search_state.page_size,
}
# We need to pass the rss feed url based
@@ -148,17 +153,18 @@ def questions(request, **kwargs):
if request.is_ajax():
q_count = paginator.count
+ #todo: words
question_counter = ungettext('%(q_num)s question', '%(q_num)s questions', q_count)
question_counter = question_counter % {'q_num': humanize.intcomma(q_count),}
- if q_count > page_size:
+ if q_count > search_state.page_size:
paginator_tpl = get_template('main_page/paginator.html')
paginator_html = paginator_tpl.render(
RequestContext(
request, {
'context': paginator_context,
'questions_count': q_count,
- 'page_size' : page_size,
+ 'page_size' : search_state.page_size,
'search_state': search_state,
}
)
@@ -189,15 +195,31 @@ def questions(request, **kwargs):
'faces': [],#[extra_tags.gravatar(contributor, 48) for contributor in contributors],
'feed_url': context_feed_url,
'query_string': search_state.query_string(),
- 'page_size' : page_size,
+ 'page_size' : search_state.page_size,
'questions': questions_html.replace('\n',''),
- 'non_existing_tags': meta_data['non_existing_tags']
+ 'non_existing_tags': meta_data['non_existing_tags'],
}
ajax_data['related_tags'] = [{
'name': escape(tag.name),
'used_count': humanize.intcomma(tag.local_used_count)
} for tag in related_tags]
+ #here we add and then delete some items
+ #to allow extra context processor to work
+ ajax_data['tags'] = related_tags
+ ajax_data['interesting_tag_names'] = None
+ ajax_data['threads'] = page
+ extra_context = context.get_extra(
+ 'ASKBOT_QUESTIONS_PAGE_EXTRA_CONTEXT',
+ request,
+ ajax_data
+ )
+ del ajax_data['tags']
+ del ajax_data['interesting_tag_names']
+ del ajax_data['threads']
+
+ ajax_data.update(extra_context)
+
return HttpResponse(simplejson.dumps(ajax_data), mimetype = 'application/json')
else: # non-AJAX branch
@@ -214,7 +236,7 @@ def questions(request, **kwargs):
'language_code': translation.get_language(),
'name_of_anonymous_user' : models.get_name_of_anonymous_user(),
'page_class': 'main-page',
- 'page_size': page_size,
+ 'page_size': search_state.page_size,
'query': search_state.query,
'threads' : page,
'questions_count' : paginator.count,
@@ -229,7 +251,6 @@ def questions(request, **kwargs):
'font_size' : extra_tags.get_tag_font_size(related_tags),
'display_tag_filter_strategy_choices': conf.get_tag_display_filter_strategy_choices(),
'email_tag_filter_strategy_choices': conf.get_tag_email_filter_strategy_choices(),
- 'update_avatar_data': schedules.should_update_avatar_data(request),
'query_string': search_state.query_string(),
'search_state': search_state,
'feed_url': context_feed_url,
@@ -240,7 +261,9 @@ def questions(request, **kwargs):
request,
template_data
)
+
template_data.update(extra_context)
+ template_data.update(context.get_for_tag_editor())
#and one more thing:) give admin user heads up about
#setting the domain name if they have not done that yet
@@ -262,6 +285,23 @@ def questions(request, **kwargs):
return render(request, 'main_page.html', template_data)
+def get_top_answers(request):
+ """returns a snippet of html of users answers"""
+ form = GetUserItemsForm(request.GET)
+ if form.is_valid():
+ owner = models.User.objects.get(id=form.cleaned_data['user_id'])
+ paginator = owner.get_top_answers_paginator(visitor=request.user)
+ answers = paginator.page(form.cleaned_data['page_number']).object_list
+ template = get_template('user_profile/user_answers_list.html')
+ answers_html = template.render({'top_answers': answers})
+ json_string = simplejson.dumps({
+ 'html': answers_html,
+ 'num_answers': paginator.count}
+ )
+ return HttpResponse(json_string, content_type='application/json')
+ else:
+ return HttpResponseBadRequest()
+
def tags(request):#view showing a listing of available tags - plain list
#1) Get parameters. This normally belongs to form cleaning.
@@ -281,7 +321,10 @@ def tags(request):#view showing a listing of available tags - plain list
tag_list_type = askbot_settings.TAG_LIST_FORMAT
#2) Get query set for the tags.
- query_params = {'deleted': False}
+ query_params = {
+ 'deleted': False,
+ 'language_code': translation.get_language()
+ }
if query != '':
query_params['name__icontains'] = query
@@ -297,7 +340,7 @@ def tags(request):#view showing a listing of available tags - plain list
'stag' : query,
'tab_id' : sortby,
'keywords' : query,
- 'search_state': SearchState(*[None for x in range(7)])
+ 'search_state': SearchState(*[None for x in range(8)])
}
if tag_list_type == 'list':
@@ -331,7 +374,7 @@ def tags(request):#view showing a listing of available tags - plain list
template_context = RequestContext(request, data)
json_data = {'success': True, 'html': template.render(template_context)}
json_string = simplejson.dumps(json_data)
- return HttpResponse(json_string, mimetype='application/json')
+ return HttpResponse(json_string, content_type='application/json')
else:
return render(request, 'tags.html', data)
@@ -699,8 +742,6 @@ def get_perms_data(request):
'MIN_REP_TO_UPLOAD_FILES',
'MIN_REP_TO_INSERT_LINK',
'MIN_REP_TO_SUGGEST_LINK',
- 'MIN_REP_TO_CLOSE_OWN_QUESTIONS',
- 'MIN_REP_TO_REOPEN_OWN_QUESTIONS',
'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS',
'MIN_REP_TO_RETAG_OTHERS_QUESTIONS',
'MIN_REP_TO_EDIT_WIKI',
@@ -729,3 +770,10 @@ def get_perms_data(request):
})
return {'html': html}
+
+@ajax_only
+@get_only
+def get_post_html(request):
+ post = models.Post.objects.get(id=request.GET['post_id'])
+ post.assert_is_visible_to(request.user)
+ return {'post_html': post.html}
diff --git a/askbot/views/users.py b/askbot/views/users.py
index b6948578..91f210d9 100644
--- a/askbot/views/users.py
+++ b/askbot/views/users.py
@@ -26,6 +26,7 @@ from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.http import HttpResponse, HttpResponseForbidden
from django.http import HttpResponseRedirect, Http404
+from django.utils.translation import get_language
from django.utils.translation import ugettext as _
from django.utils import simplejson
from django.utils.html import strip_tags as strip_all_tags
@@ -283,7 +284,6 @@ def user_moderate(request, subject, context):
'active_tab': 'users',
'page_class': 'user-profile-page',
'tab_name': 'moderation',
- 'tab_description': _('moderate this user'),
'page_title': _('moderate user'),
'change_user_status_form': user_status_form,
'change_user_reputation_form': user_rep_form,
@@ -393,7 +393,7 @@ def user_stats(request, user, context):
#
# Questions
#
- questions = user.posts.get_questions(
+ questions_qs = user.posts.get_questions(
user=request.user
).filter(
**question_filter
@@ -401,30 +401,33 @@ def user_stats(request, user, context):
'-points', '-thread__last_activity_at'
).select_related(
'thread', 'thread__last_activity_by'
- )[:100]
-
- #added this if to avoid another query if questions is less than 100
- if len(questions) < 100:
- question_count = len(questions)
- else:
- question_count = user.posts.get_questions().filter(**question_filter).count()
-
+ )
+
+ q_paginator = Paginator(questions_qs, const.USER_POSTS_PAGE_SIZE)
+ questions = q_paginator.page(1).object_list
+ question_count = q_paginator.count
+
+ q_paginator_context = functions.setup_paginator({
+ 'is_paginated' : (question_count > const.USER_POSTS_PAGE_SIZE),
+ 'pages': q_paginator.num_pages,
+ 'current_page_number': 1,
+ 'page_object': q_paginator.page(1),
+ 'base_url' : '?' #this paginator will be ajax
+ })
#
# Top answers
#
- top_answers = user.posts.get_answers(
- request.user
- ).filter(
- deleted=False,
- thread__posts__deleted=False,
- thread__posts__post_type='question',
- ).select_related(
- 'thread'
- ).order_by(
- '-points', '-added_at'
- )[:100]
-
- top_answer_count = len(top_answers)
+ a_paginator = user.get_top_answers_paginator(request.user)
+ top_answers = a_paginator.page(1).object_list
+ top_answer_count = a_paginator.count
+
+ a_paginator_context = functions.setup_paginator({
+ 'is_paginated' : (top_answer_count > const.USER_POSTS_PAGE_SIZE),
+ 'pages': a_paginator.num_pages,
+ 'current_page_number': 1,
+ 'page_object': a_paginator.page(1),
+ 'base_url' : '?' #this paginator will be ajax
+ })
#
# Votes
#
@@ -439,7 +442,10 @@ def user_stats(request, user, context):
# INFO: There's bug in Django that makes the following query kind of broken (GROUP BY clause is problematic):
# http://stackoverflow.com/questions/7973461/django-aggregation-does-excessive-group-by-clauses
# Fortunately it looks like it returns correct results for the test data
- user_tags = models.Tag.objects.filter(threads__posts__author=user).distinct().\
+ user_tags = models.Tag.objects.filter(
+ threads__posts__author=user,
+ language_code=get_language()
+ ).distinct().\
annotate(user_tag_usage_count=Count('threads')).\
order_by('-user_tag_usage_count')[:const.USER_VIEW_DATA_SIZE]
user_tags = list(user_tags) # evaluate
@@ -526,14 +532,16 @@ def user_stats(request, user, context):
'page_class': 'user-profile-page',
'support_custom_avatars': ('avatar' in django_settings.INSTALLED_APPS),
'tab_name' : 'stats',
- 'tab_description' : _('user profile'),
'page_title' : _('user profile overview'),
'user_status_for_display': user.get_status_display(soft = True),
'questions' : questions,
'question_count': question_count,
+ 'q_paginator_context': q_paginator_context,
'top_answers': top_answers,
'top_answer_count': top_answer_count,
+ 'a_paginator_context': a_paginator_context,
+ 'page_size': const.USER_POSTS_PAGE_SIZE,
'up_votes' : up_votes,
'down_votes' : down_votes,
@@ -647,7 +655,6 @@ def user_recent(request, user, context):
'active_tab': 'users',
'page_class': 'user-profile-page',
'tab_name' : 'recent',
- 'tab_description' : _('recent user activity'),
'page_title' : _('profile - recent activity'),
'activities' : activities
}
@@ -679,7 +686,6 @@ def show_group_join_requests(request, user, context):
'inbox_section': 'group-join-requests',
'page_class': 'user-profile-page',
'tab_name' : 'join_requests',
- 'tab_description' : _('group joining requests'),
'page_title' : _('profile - moderation'),
'groups_dict': groups_dict,
'join_requests': join_requests
@@ -736,7 +742,6 @@ def user_responses(request, user, context):
'page_class': 'user-profile-page',
'tab_name' : 'inbox',
'inbox_section': section,
- 'tab_description' : _('private messages'),
'page_title' : _('profile - messages')
}
context.update(data)
@@ -816,7 +821,6 @@ def user_responses(request, user, context):
'page_class': 'user-profile-page',
'tab_name' : 'inbox',
'inbox_section': section,
- 'tab_description' : _('comments and answers to others questions'),
'page_title' : _('profile - responses'),
'post_reject_reasons': reject_reasons,
'responses' : filtered_response_list,
@@ -858,7 +862,6 @@ def user_votes(request, user, context):
'active_tab':'users',
'page_class': 'user-profile-page',
'tab_name' : 'votes',
- 'tab_description' : _('user vote record'),
'page_title' : _('profile - votes'),
'votes' : votes[:const.USER_VIEW_DATA_SIZE]
}
@@ -880,7 +883,6 @@ def user_reputation(request, user, context):
'active_tab':'users',
'page_class': 'user-profile-page',
'tab_name': 'reputation',
- 'tab_description': _('user karma'),
'page_title': _("Profile - User's Karma"),
'reputation': reputes,
'reps': reps
@@ -891,17 +893,36 @@ def user_reputation(request, user, context):
def user_favorites(request, user, context):
favorite_threads = user.user_favorite_questions.values_list('thread', flat=True)
- questions = models.Post.objects.filter(post_type='question', thread__in=favorite_threads)\
- .select_related('thread', 'thread__last_activity_by')\
- .order_by('-points', '-thread__last_activity_at')[:const.USER_VIEW_DATA_SIZE]
+ questions_qs = models.Post.objects.filter(
+ post_type='question',
+ thread__in=favorite_threads
+ ).select_related(
+ 'thread', 'thread__last_activity_by'
+ ).order_by(
+ '-points', '-thread__last_activity_at'
+ )[:const.USER_VIEW_DATA_SIZE]
+
+ q_paginator = Paginator(questions_qs, const.USER_POSTS_PAGE_SIZE)
+ questions = q_paginator.page(1).object_list
+ question_count = q_paginator.count
+
+ q_paginator_context = functions.setup_paginator({
+ 'is_paginated' : (question_count > const.USER_POSTS_PAGE_SIZE),
+ 'pages': q_paginator.num_pages,
+ 'current_page_number': 1,
+ 'page_object': q_paginator.page(1),
+ 'base_url' : '?' #this paginator will be ajax
+ })
data = {
'active_tab':'users',
'page_class': 'user-profile-page',
'tab_name' : 'favorites',
- 'tab_description' : _('users favorite questions'),
- 'page_title' : _('profile - favorite questions'),
+ 'page_title' : _('profile - favorites'),
'questions' : questions,
+ 'q_paginator_context': q_paginator_context,
+ 'question_count': question_count,
+ 'page_size': const.USER_POSTS_PAGE_SIZE
}
context.update(data)
return render(request, 'user_profile/user_favorites.html', context)
@@ -970,7 +991,6 @@ def user_email_subscriptions(request, user, context):
'subscribed_tag_names': user.get_marked_tag_names('subscribed'),
'page_class': 'user-profile-page',
'tab_name': 'email_subscriptions',
- 'tab_description': _('email subscription settings'),
'page_title': _('profile - email subscriptions'),
'email_feeds_form': email_feeds_form,
'tag_filter_selection_form': tag_filter_form,
@@ -978,6 +998,8 @@ def user_email_subscriptions(request, user, context):
'user_languages': user.languages.split()
}
context.update(data)
+ #todo: really need only if subscribed tags are enabled
+ context.update(view_context.get_for_tag_editor())
return render(
request,
'user_profile/user_email_subscriptions.html',
@@ -1052,13 +1074,14 @@ def user(request, id, slug=None, tab_name=None):
user_view_func = USER_VIEW_CALL_TABLE.get(tab_name, user_stats)
- search_state = SearchState( # Non-default SearchState with user data set
+ search_state = SearchState(
scope=None,
sort=None,
query=None,
tags=None,
author=None,
page=None,
+ page_size=const.USER_POSTS_PAGE_SIZE,
user_logged_in=profile_owner.is_authenticated(),
)
@@ -1073,17 +1096,6 @@ def user(request, id, slug=None, tab_name=None):
context['custom_tab_slug'] = CUSTOM_TAB['SLUG']
return user_view_func(request, profile_owner, context)
-@csrf.csrf_exempt
-def update_has_custom_avatar(request):
- """updates current avatar type data for the user
- """
- if request.is_ajax() and request.user.is_authenticated():
- if request.user.avatar_type in ('n', 'g'):
- request.user.update_avatar_type()
- request.session['avatar_data_updated_at'] = datetime.datetime.now()
- return HttpResponse(simplejson.dumps({'status':'ok'}), mimetype='application/json')
- return HttpResponseForbidden()
-
def groups(request, id = None, slug = None):
"""output groups page
"""
diff --git a/askbot/views/widgets.py b/askbot/views/widgets.py
index 5c4042fa..41cf4c8f 100644
--- a/askbot/views/widgets.py
+++ b/askbot/views/widgets.py
@@ -60,6 +60,10 @@ def ask_widget(request, widget_id):
widget = get_object_or_404(models.AskWidget, id=widget_id)
if request.method == "POST":
+
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ return redirect('ask_by_widget')
+
form = forms.AskWidgetForm(
include_text=widget.include_text_field,
data=request.POST,
@@ -241,7 +245,7 @@ def render_ask_widget_js(request, widget_id):
'variable_name': variable_name
}
content = content_tpl.render(RequestContext(request, context_dict))
- return HttpResponse(content, mimetype='text/javascript')
+ return HttpResponse(content, content_type='text/javascript')
def render_ask_widget_css(request, widget_id):
widget = get_object_or_404(models.AskWidget, pk=widget_id)
@@ -254,7 +258,7 @@ def render_ask_widget_css(request, widget_id):
'variable_name': variable_name
}
content = content_tpl.render(RequestContext(request, context_dict))
- return HttpResponse(content, mimetype='text/css')
+ return HttpResponse(content, content_type='text/css')
def question_widget(request, widget_id):
"""Returns the first x questions based on certain tags.
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index dd5666f7..a11fb941 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -30,6 +30,7 @@ from django.core.urlresolvers import reverse
from django.core import exceptions
from django.conf import settings
from django.views.decorators import csrf
+from django.contrib.auth.models import User
from askbot import exceptions as askbot_exceptions
from askbot import forms
@@ -46,6 +47,7 @@ from askbot.views import context
from askbot.templatetags import extra_filters_jinja as template_filters
from askbot.importers.stackexchange import management as stackexchange#todo: may change
from askbot.utils.slug import slugify
+from recaptcha_works.decorators import fix_recaptcha_remote_ip
# used in index page
INDEX_PAGE_SIZE = 20
@@ -126,7 +128,7 @@ def upload(request):#ajax upload file to a question or answer
xml_template = "<result><msg><![CDATA[%s]]></msg><error><![CDATA[%s]]></error><file_url>%s</file_url><orig_file_name><![CDATA[%s]]></orig_file_name></result>"
xml = xml_template % (result, error, file_url, orig_file_name)
- return HttpResponse(xml, mimetype="application/xml")
+ return HttpResponse(xml, content_type="application/xml")
def __import_se_data(dump_file):
"""non-view function that imports the SE data
@@ -192,7 +194,7 @@ def import_data(request):
dump_storage.flush()
return HttpResponse(__import_se_data(dump_storage))
- #yield HttpResponse(_('StackExchange import complete.'), mimetype='text/plain')
+ #yield HttpResponse(_('StackExchange import complete.'), content_type='text/plain')
#dump_storage.close()
else:
form = forms.DumpUploadForm()
@@ -203,15 +205,9 @@ def import_data(request):
}
return render(request, 'import_data.html', data)
-#@login_required #actually you can post anonymously, but then must register
+@fix_recaptcha_remote_ip
@csrf.csrf_protect
-@decorators.check_authorization_to_post(ugettext_lazy(
- "<span class=\"strong big\">You are welcome to start submitting your question "
- "anonymously</span>. When you submit the post, you will be redirected to the "
- "login/signup page. Your question will be saved in the current session and "
- "will be published after you log in. Login/signup process is very simple. "
- "Login takes about 30 seconds, initial signup takes a minute or less."
-))
+@decorators.check_authorization_to_post(ugettext_lazy('Please log in to make posts'))
@decorators.check_spam('text')
def ask(request):#view used to ask a new question
"""a view to ask a new question
@@ -226,8 +222,11 @@ def ask(request):#view used to ask a new question
request.user.message_set.create(message=_('Sorry, but you have only read access'))
return HttpResponseRedirect(referer)
- form = forms.AskForm(request.REQUEST, user=request.user)
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ return HttpResponseRedirect(reverse('index'))
+
if request.method == 'POST':
+ form = forms.AskForm(request.POST, user=request.user)
if form.is_valid():
timestamp = datetime.datetime.now()
title = form.cleaned_data['title']
@@ -240,12 +239,16 @@ def ask(request):#view used to ask a new question
language = form.cleaned_data.get('language', None)
if request.user.is_authenticated():
- drafts = models.DraftQuestion.objects.filter(
- author=request.user
- )
+ drafts = models.DraftQuestion.objects.filter(author=request.user)
drafts.delete()
-
user = form.get_post_user(request.user)
+ elif request.user.is_anonymous() and askbot_settings.ALLOW_ASK_UNREGISTERED:
+ user = models.get_or_create_anonymous_user()
+ ask_anonymously = True
+ else:
+ user = None
+
+ if user:
try:
question = user.post_question(
title=title,
@@ -352,7 +355,7 @@ def retag_question(request, id):
response_data['message'] = message
data = simplejson.dumps(response_data)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
else:
return HttpResponseRedirect(question.get_absolute_url())
elif request.is_ajax():
@@ -361,7 +364,7 @@ def retag_question(request, id):
'success': False
}
data = simplejson.dumps(response_data)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
else:
form = forms.RetagQuestionForm(question)
@@ -378,7 +381,7 @@ def retag_question(request, id):
'success': False
}
data = simplejson.dumps(response_data)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
else:
request.user.message_set.create(message = unicode(e))
return HttpResponseRedirect(question.get_absolute_url())
@@ -386,12 +389,18 @@ def retag_question(request, id):
@login_required
@csrf.csrf_protect
@decorators.check_spam('text')
+@fix_recaptcha_remote_ip
def edit_question(request, id):
"""edit question view
"""
question = get_object_or_404(models.Post, id=id)
+
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ return HttpResponseRedirect(question.get_absolute_url())
+
revision = question.get_latest_revision()
revision_form = None
+
try:
request.user.assert_can_edit_question(question)
if request.method == 'POST':
@@ -432,9 +441,6 @@ def edit_question(request, id):
if form.cleaned_data['reveal_identity']:
question.thread.remove_author_anonymity()
- if 'language' in form.cleaned_data:
- question.thread.language_code = form.cleaned_data['language']
-
is_anon_edit = form.cleaned_data['stay_anonymous']
is_wiki = form.cleaned_data.get('wiki', question.wiki)
post_privately = form.cleaned_data['post_privately']
@@ -453,6 +459,10 @@ def edit_question(request, id):
is_private = post_privately,
suppress_email=suppress_email
)
+
+ if 'language' in form.cleaned_data:
+ question.thread.set_language_code(form.cleaned_data['language'])
+
return HttpResponseRedirect(question.get_absolute_url())
else:
#request type was "GET"
@@ -490,8 +500,13 @@ def edit_question(request, id):
@login_required
@csrf.csrf_protect
@decorators.check_spam('text')
+@fix_recaptcha_remote_ip
def edit_answer(request, id):
answer = get_object_or_404(models.Post, id=id)
+
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ return HttpResponseRedirect(answer.get_absolute_url())
+
revision = answer.get_latest_revision()
class_path = getattr(settings, 'ASKBOT_EDIT_ANSWER_FORM', None)
@@ -579,8 +594,9 @@ def edit_answer(request, id):
return HttpResponseRedirect(answer.get_absolute_url())
#todo: rename this function to post_new_answer
-@decorators.check_authorization_to_post(ugettext_lazy('Please log in to answer questions'))
+@decorators.check_authorization_to_post(ugettext_lazy('Please log in to make posts'))
@decorators.check_spam('text')
+@fix_recaptcha_remote_ip
def answer(request, id, form_class=forms.AnswerForm):#process a new answer
"""view that posts new answer
@@ -590,6 +606,10 @@ def answer(request, id, form_class=forms.AnswerForm):#process a new answer
authenticated users post directly
"""
question = get_object_or_404(models.Post, post_type='question', id=id)
+
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ return HttpResponseRedirect(question.get_absolute_url())
+
if request.method == "POST":
#this check prevents backward compatilibility
@@ -681,7 +701,7 @@ def __generate_comments_json(obj, user):#non-view generates json data for the po
json_comments.append(comment_data)
data = simplejson.dumps(json_comments)
- return HttpResponse(data, mimetype="application/json")
+ return HttpResponse(data, content_type="application/json")
@csrf.csrf_exempt
@decorators.check_spam('comment')
@@ -701,7 +721,7 @@ def post_comments(request):#generic ajax handler to load comments to an object
if request.method == 'POST':
form = forms.NewCommentForm(request.POST)
elif request.method == 'GET':
- form = forms.GetCommentsForPostForm(request.GET)
+ form = forms.GetDataForPostForm(request.GET)
if form.is_valid() == False:
return HttpResponseBadRequest(
@@ -727,6 +747,10 @@ def post_comments(request):#generic ajax handler to load comments to an object
'<a href="%(sign_in_url)s">sign in</a>.') % \
{'sign_in_url': url_utils.get_login_url()}
raise exceptions.PermissionDenied(msg)
+
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ raise exceptions.PermissionDenied(askbot_settings.READ_ONLY_MESSAGE)
+
comment = user.post_comment(
parent_post=post, body_text=form.cleaned_data['comment']
)
@@ -737,17 +761,20 @@ def post_comments(request):#generic ajax handler to load comments to an object
)
response = __generate_comments_json(post, user)
except exceptions.PermissionDenied, e:
- response = HttpResponseForbidden(unicode(e), mimetype="application/json")
+ response = HttpResponseForbidden(unicode(e), content_type="application/json")
return response
-#@csrf.csrf_exempt
+@csrf.csrf_exempt
@decorators.ajax_only
#@decorators.check_spam('comment')
def edit_comment(request):
if request.user.is_anonymous():
raise exceptions.PermissionDenied(_('Sorry, anonymous users cannot edit comments'))
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ raise exceptions.PermissionDenied(askbot_settings.READ_ONLY_MESSAGE)
+
form = forms.EditCommentForm(request.POST)
if form.is_valid() == False:
raise exceptions.PermissionDenied('This content is forbidden')
@@ -802,7 +829,7 @@ def delete_comment(request):
raise exceptions.PermissionDenied(msg)
if request.is_ajax():
- form = forms.DeleteCommentForm(request.POST)
+ form = forms.ProcessCommentForm(request.POST)
if form.is_valid() == False:
return HttpResponseBadRequest()
@@ -811,6 +838,9 @@ def delete_comment(request):
comment = get_object_or_404(models.Post, post_type='comment', id=comment_id)
request.user.assert_can_delete_comment(comment)
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ raise exceptions.PermissionDenied(askbot_settings.READ_ONLY_MESSAGE)
+
parent = comment.parent
comment.delete()
#attn: recalc denormalized field
@@ -829,37 +859,30 @@ def delete_comment(request):
mimetype = 'application/json'
)
+@login_required
@decorators.post_only
+@csrf.csrf_protect
def comment_to_answer(request):
- comment_id = request.POST.get('comment_id')
- if comment_id:
- comment_id = int(comment_id)
- comment = get_object_or_404(models.Post,
- post_type='comment', id=comment_id)
- comment.post_type = 'answer'
- old_parent = comment.parent
-
- comment.parent = comment.thread._question_post()
- comment.save()
-
- comment.thread.update_answer_count()
-
- comment.parent.comment_count += 1
- comment.parent.save()
+ if request.user.is_anonymous():
+ msg = _('Sorry, only logged in users can convert comments to answers. '
+ 'Please <a href="%(sign_in_url)s">sign in</a>.') % \
+ {'sign_in_url': url_utils.get_login_url()}
+ raise exceptions.PermissionDenied(msg)
- #to avoid db constraint error
- if old_parent.comment_count >= 1:
- old_parent.comment_count -= 1
- else:
- old_parent.comment_count = 0
+ form = forms.ProcessCommentForm(request.POST)
+ if form.is_valid() == False:
+ raise Http404
- old_parent.save()
+ comment = get_object_or_404(
+ models.Post,
+ post_type='comment',
+ id=form.cleaned_data['comment_id']
+ )
- comment.thread.invalidate_cached_data()
+ if askbot_settings.READ_ONLY_MODE_ENABLED is False:
+ request.user.repost_comment_as_answer(comment)
- return HttpResponseRedirect(comment.get_absolute_url())
- else:
- raise Http404
+ return HttpResponseRedirect(comment.get_absolute_url())
@decorators.post_only
@csrf.csrf_protect
@@ -871,12 +894,24 @@ def repost_answer_as_comment(request, destination=None):
'comment_under_previous_answer'
)
)
+ if request.user.is_anonymous():
+ msg = _('Sorry, only logged in users can convert answers to comments. '
+ 'Please <a href="%(sign_in_url)s">sign in</a>.') % \
+ {'sign_in_url': url_utils.get_login_url()}
+ raise exceptions.PermissionDenied(msg)
answer_id = request.POST.get('answer_id')
if answer_id:
- answer_id = int(answer_id)
+ try:
+ answer_id = int(answer_id)
+ except (ValueError, TypeError):
+ raise Http404
answer = get_object_or_404(models.Post,
post_type = 'answer', id=answer_id)
+ if askbot_settings.READ_ONLY_MODE_ENABLED:
+ return HttpResponseRedirect(answer.get_absolute_url())
+ request.user.assert_can_convert_post(post=answer)
+
if destination == 'comment_under_question':
destination_post = answer.thread._question_post()
else:
@@ -892,8 +927,8 @@ def repost_answer_as_comment(request, destination=None):
if len(answer.text) <= askbot_settings.MAX_COMMENT_LENGTH:
answer.post_type = 'comment'
answer.parent = destination_post
- #can we trust this?
- old_comment_count = answer.comment_count
+
+ new_comment_count = answer.comments.count() + 1
answer.comment_count = 0
answer_comments = models.Post.objects.get_comments().filter(parent=answer)
@@ -903,7 +938,7 @@ def repost_answer_as_comment(request, destination=None):
answer.parse_and_save(author=answer.author)
answer.thread.update_answer_count()
- answer.parent.comment_count = 1 + old_comment_count
+ answer.parent.comment_count += new_comment_count
answer.parent.save()
answer.thread.invalidate_cached_data()
diff --git a/askbot_requirements.txt b/askbot_requirements.txt
index 59694444..3b9fde6e 100644
--- a/askbot_requirements.txt
+++ b/askbot_requirements.txt
@@ -1,5 +1,5 @@
akismet
-django>=1.3.1
+django>=1.3.1,<1.6
Jinja2
Coffin>=0.3
South>=0.7.1
@@ -12,7 +12,7 @@ django-threaded-multihost
django-robots
unidecode
django-countries==1.0.5
-django-celery==2.2.7
+django-celery>=3.0.11
django-kombu==0.9.2
django-followit
django-recaptcha-works
diff --git a/askbot_requirements_dev.txt b/askbot_requirements_dev.txt
index e5c36577..476ed66f 100644
--- a/askbot_requirements_dev.txt
+++ b/askbot_requirements_dev.txt
@@ -1,5 +1,5 @@
akismet
-django==1.4.2
+django>=1.3.1,<1.6
Jinja2
Coffin>=0.3
South>=0.7.1
@@ -15,7 +15,7 @@ django-threaded-multihost
django-robots
unidecode
django-countries==1.0.5
-django-celery==2.2.7
+django-celery>=3.0.11
django-kombu==0.9.2
django-followit
django-recaptcha-works