diff options
243 files changed, 12194 insertions, 3629 deletions
@@ -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 Binary files differindex 9af75244..92662132 100644 --- a/askbot/locale/en/LC_MESSAGES/django.mo +++ b/askbot/locale/en/LC_MESSAGES/django.mo 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 <HEAD>\" " "above. Contents of this box will be inserted into the <HEAD> 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 Binary files differindex 3eead943..f158cafd 100644 --- a/askbot/locale/en/LC_MESSAGES/djangojs.mo +++ b/askbot/locale/en/LC_MESSAGES/djangojs.mo 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 Binary files differindex ad9b2875..de12940d 100644 --- a/askbot/locale/fr/LC_MESSAGES/django.mo +++ b/askbot/locale/fr/LC_MESSAGES/django.mo 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 Binary files differindex ab73ba75..82b2b137 100644 --- a/askbot/locale/ru/LC_MESSAGES/django.mo +++ b/askbot/locale/ru/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..e4a8a2d4 --- /dev/null +++ b/askbot/media/jquery-openid/images/mozilla-persona.gif 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 += '•'; + } + for (; i < this._maxNumDots; i++) { + dots += ' '; + } + 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 += ' '; + } + 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&d=%(type)s&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" /> <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&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" /> - <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 %}"/> - <input type="submit" class="submit cancel" name="cancel" value="{% trans %}Cancel{% endtrans %}"/> + <input type="submit" value="{% trans %}Send Feedback{% endtrans %}"/> + <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('*', '✽')|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 %}"> - « {% 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 %}" + >« {% 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 %} »</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 %} »</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&subset=latin-ext' rel='stylesheet' type='text/css'> - <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=cyrillic-ext' rel='stylesheet' type='text/css'> + <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin-ext' rel='stylesheet' type='text/css' /> + <link href='//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&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" /> + <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" /> - <input type="button" value="{% trans %}Cancel{% endtrans %}" class="submit" onclick="history.back(-1);" /> + <input type="submit" value="{% trans %}Retag{% endtrans %}"/> + <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" /> - <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" > - <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 %}"/> - <input type="submit" class="submit" name="stop_email" value="{% trans %}Stop Email{% endtrans %}"/> + <input type="submit" name="save" value="{% trans %}Update{% endtrans %}"/> + <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 %}" /> - <input type="submit" class="submit" name="add_reputation" value="{% trans %}Add{% endtrans %}" /> + <input type="submit" name="subtract_reputation" value="{% trans %}Subtract{% endtrans %}" /> + <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() %}&{% 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&d=%(gravatar_type)s&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/> + 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&#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&tstart=210')) + self.assertTrue(link_re.search('/thread.jspa?threadID=3087&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&#10175 sometext +#[sometext|/thread.jspa?messageID=10662#10662] [/thread.jspa?messageID=11058] +#[sometext|/thread.jspa?threadID=1888&tstart=210|title] +#/thread.jspa?threadID=3087&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&#10175 +/thread.jspa?messageID=10662#10662 +/thread.jspa?messageID=11058 +/thread.jspa?threadID=1888&tstart=210 +/thread.jspa?threadID=3087&tstart=-258 +""" +internal_link_pattern = r"""(?:message|thread) #junk + \.jspa\? + (message|thread) #link type + ID=(\d+) #either post or thread id + (?:(?:&)?\#\d+|&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\.&%\$\-]+)*@)*((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\.\,\?\'\\\+&%\$#\=~_\-]+))*)" + _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('^> ?', 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'^> {quote:title\=(.+?):}{quote}\n((:?>(:? .*?)?\n)+)', + re.S | re.M + ) + def _do_block_quotes2(self, text): + """do block quote of type: + > {quote:title=some title:}{quote} + > some text + > + > 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' % (' '*len(spaces), text) + + def _preserve_leading_blanks(self, text): + """replace leading blanks with """ + 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 |