diff options
67 files changed, 1875 insertions, 571 deletions
@@ -47,3 +47,4 @@ run recaptcha /.ve /db.sq3 +*.DS_Store diff --git a/askbot/__init__.py b/askbot/__init__.py index 12517182..859b2695 100644 --- a/askbot/__init__.py +++ b/askbot/__init__.py @@ -9,7 +9,7 @@ import smtplib import sys import logging -VERSION = (0, 7, 42) +VERSION = (0, 7, 43) #keys are module names used by python imports, #values - the package qualifier to use for pip diff --git a/askbot/askbot b/askbot/askbot new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/askbot/askbot diff --git a/askbot/conf/settings_wrapper.py b/askbot/conf/settings_wrapper.py index 78d16397..b6b5f302 100644 --- a/askbot/conf/settings_wrapper.py +++ b/askbot/conf/settings_wrapper.py @@ -58,8 +58,14 @@ class ConfigSettings(object): self.update(key, self.get_default(key)) def update(self, key, value): - setting = config_get(self.__group_map[key], key) - setting.update(value) + try: + setting = config_get(self.__group_map[key], key) + setting.update(value) + except: + from askbot.deps.livesettings.models import Setting + setting = Setting.objects.get(key=key) + setting.value = value + setting.save() #self.prime_cache() def register(self, value): diff --git a/askbot/conf/site_modes.py b/askbot/conf/site_modes.py index a88103b4..efbbcca0 100644 --- a/askbot/conf/site_modes.py +++ b/askbot/conf/site_modes.py @@ -76,9 +76,9 @@ settings.register( "Bootstrap mode lowers reputation and certain badge " "thresholds, to values, more suitable " "for the smaller communities, " - "<strong>WARNING:</strong> your current value for " + "<strong>WARNING:</strong> your current values for " "Minimum reputation, " - "Bagde Settings and " + "Badge Settings and " "Vote Rules will " "be changed after you modify this setting." ), diff --git a/askbot/conf/skin_general_settings.py b/askbot/conf/skin_general_settings.py index d81c984b..323550ce 100644 --- a/askbot/conf/skin_general_settings.py +++ b/askbot/conf/skin_general_settings.py @@ -30,6 +30,42 @@ settings.register( ) ) +LANGUAGE_CHOICES = ( + ('en', _("English")), + ('es', _("Spanish")), + ('ca', _("Catalan")), + ('de', _("German")), + ('el', _("Greek")), + ('fi', _("Finnish")), + ('fr', _("French")), + ('hi', _("Hindi")), + ('hu', _("Hungarian")), + ('it', _("Italian")), + ('ja', _("Japanese")), + ('ko', _("Korean")), + ('pt', _("Portuguese")), + ('pt_BR', _("Brazilian Portuguese")), + ('ro', _("Romanian")), + ('ru', _("Russian")), + ('sr', _("Serbian")), + ('tr', _("Turkish")), + ('vi', _("Vietnamese")), + ('zh_CN', _("Chinese")), + ('zh_TW', _("Chinese (Taiwan)")), + ) + +""" +settings.register( + values.StringValue( + GENERAL_SKIN_SETTINGS, + 'ASKBOT_LANGUAGE', + default = 'en', + choices = LANGUAGE_CHOICES, + description = _('Select Language'), + ) +) +""" + settings.register( values.BooleanValue( GENERAL_SKIN_SETTINGS, @@ -198,7 +234,7 @@ settings.register( description = _('Apply custom style sheet (CSS)'), help_text = _( 'Check if you want to change appearance ' - 'of your form by adding custom style sheet rules ' + 'of your form by adding custom style sheet rules ' '(please see the next item)' ), default = False @@ -214,7 +250,7 @@ settings.register( '<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 default style sheet rules. ' + 'after the default style sheet rules. ' 'The custom style sheet will be served dynamically at ' 'url "<forum url>/custom.css", where ' 'the "<forum url> part depends (default is ' diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py index 7eef91d1..f3091800 100644 --- a/askbot/const/__init__.py +++ b/askbot/const/__init__.py @@ -55,6 +55,10 @@ POST_SORT_METHODS = ( POST_TYPES = ('answer', 'comment', 'question', 'tag_wiki', 'reject_reason') REPLY_SEPARATOR_TEMPLATE = '==== %(user_action)s %(instruction)s -=-==' +REPLY_WITH_COMMENT_TEMPLATE = _( + 'Note: to reply with a comment, ' + 'please use <a href="mailto://%(addr)s">this link</a>' +) REPLY_SEPARATOR_REGEX = re.compile('==== .* -=-==', re.MULTILINE) ANSWER_SORT_METHODS = (#no translations needed here @@ -129,6 +133,7 @@ TYPE_ACTIVITY_MODERATED_NEW_POST = 24 TYPE_ACTIVITY_MODERATED_POST_EDIT = 25 TYPE_ACTIVITY_CREATE_REJECT_REASON = 26 TYPE_ACTIVITY_UPDATE_REJECT_REASON = 27 +TYPE_ACTIVITY_VALIDATION_EMAIL_SENT = 28 #TYPE_ACTIVITY_EDIT_QUESTION = 17 #TYPE_ACTIVITY_EDIT_ANSWER = 18 @@ -182,6 +187,10 @@ TYPE_ACTIVITY = ( TYPE_ACTIVITY_UPDATE_REJECT_REASON, _('updated post reject reason') ), + ( + TYPE_ACTIVITY_VALIDATION_EMAIL_SENT, + 'sent email address validation message'#don't translate, internal + ), ) diff --git a/askbot/context.py b/askbot/context.py index ea10a890..03a2d1d8 100644 --- a/askbot/context.py +++ b/askbot/context.py @@ -26,7 +26,16 @@ def application_settings(request): my_settings['LANGUAGE_CODE'] = getattr(request, 'LANGUAGE_CODE', settings.LANGUAGE_CODE) my_settings['ASKBOT_URL'] = settings.ASKBOT_URL my_settings['STATIC_URL'] = settings.STATIC_URL - my_settings['ASKBOT_CSS_DEVEL'] = getattr(settings, 'ASKBOT_CSS_DEVEL', False) + my_settings['ASKBOT_CSS_DEVEL'] = getattr( + settings, + 'ASKBOT_CSS_DEVEL', + False + ) + my_settings['USE_LOCAL_FONTS'] = getattr( + settings, + 'ASKBOT_USE_LOCAL_FONTS', + False + ) my_settings['DEBUG'] = settings.DEBUG my_settings['USING_RUNSERVER'] = 'runserver' in sys.argv my_settings['ASKBOT_VERSION'] = askbot.get_version() diff --git a/askbot/cron/askbot_cron_job b/askbot/cron/askbot_cron_job index 38bf0337..04ba2303 100644 --- a/askbot/cron/askbot_cron_job +++ b/askbot/cron/askbot_cron_job @@ -9,7 +9,7 @@ PROJECT_PARENT_DIR=/path/to/dir_containing_askbot_site PROJECT_DIR_NAME=askbot_site export PYTHONPATH=$PROJECT_PARENT_DIR:$PYTHONPATH -PROJECT_ROOT=$PYTHONPATH/$PROJECT_NAME +PROJECT_ROOT=$PROJECT_DIR_NAME/$PROJECT_NAME #these are actual commands that are to be run python $PROJECT_ROOT/manage.py send_email_alerts diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py index f3d8f64b..ed99e44f 100644 --- a/askbot/deps/django_authopenid/backends.py +++ b/askbot/deps/django_authopenid/backends.py @@ -10,6 +10,7 @@ from django.utils.translation import ugettext as _ from askbot.deps.django_authopenid.models import UserAssociation from askbot.deps.django_authopenid import util from askbot.conf import settings as askbot_settings +from askbot.models.signals import user_registered log = logging.getLogger('configuration') @@ -62,6 +63,7 @@ def ldap_authenticate(username, password): user.is_superuser = False user.is_active = True user.save() + user_registered.send(None, user = user) log.info('Created New User : [{0}]'.format(exact_username)) return user @@ -157,11 +159,13 @@ class AuthBackend(object): if created: user.set_password(password) user.save() + user_registered.send(None, user = user) else: #have username collision - so make up a more unique user name #bug: - if user already exists with the new username - we are in trouble new_username = '%s@%s' % (username, provider_name) user = User.objects.create_user(new_username, '', password) + user_registered.send(None, user = user) message = _( 'Welcome! Please set email address (important!) in your ' 'profile and adjust screen name, if necessary.' diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index 22be8460..2f80d366 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -78,11 +78,11 @@ from askbot.deps.django_authopenid.backends import AuthBackend import logging from askbot.utils.forms import get_next_url from askbot.utils.http import get_request_info +from askbot.models.signals import user_logged_in, user_registered #todo: decouple from askbot def login(request,user): from django.contrib.auth import login as _login - from askbot.models import signals # get old session key session_key = request.session.session_key @@ -93,7 +93,7 @@ def login(request,user): # send signal with old session key as argument logging.debug('logged in user %s with session key %s' % (user.username, session_key)) #todo: move to auth app - signals.user_logged_in.send( + user_logged_in.send( request = request, user = user, session_key=session_key, @@ -326,7 +326,7 @@ def signin(request): request = request, user = user, user_identifier = username, - login_provider_name = ldap_provider_name, + login_provider_name = provider_name, redirect_url = next_url ) @@ -829,6 +829,7 @@ def register(request, login_provider_name=None, user_identifier=None): email = register_form.cleaned_data['email'] user = User.objects.create_user(username, email) + user_registered.send(None, user = user) logging.debug('creating new openid user association for %s') @@ -951,7 +952,9 @@ def signup_with_password(request): email = form.cleaned_data['email'] provider_name = form.cleaned_data['login_provider'] - User.objects.create_user(username, email, password) + new_user = User.objects.create_user(username, email, password) + user_registered.send(None, user = new_user) + logging.debug('new user %s created' % username) if provider_name != 'local': raise NotImplementedError('must run create external user code') diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index 25d96e88..06808a9c 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -1,13 +1,20 @@ Changes in Askbot ================= -Future version --------------- +Development version +------------------- +* Welcome email for the case when replying by email is enabled (Evgeny) +* Detection of email signature based on the response to the welome email (Evgeny) + +0.7.43 (May 14, 2012) +--------------------- * User groups (Evgeny) * Public/Private/Hidden reputation (Evgeny) * Enabling/disabling the badges system (Evgeny) * Created a basic post moderation feature (Evgeny) * Created a way to specify reasons for rejecting posts in a modal dialog (Evgeny) +* A number of bug fixes (Adolfo Fitoria, Jim Tittsler, + Evgeny Fadeev, Robin Stocker, Radim Řehůřek, Silvio Heuberger) 0.7.41, 0.7.42 (April 21, 2012) ------------------------------- diff --git a/askbot/doc/source/contributors.rst b/askbot/doc/source/contributors.rst index 71bc5cc9..81729979 100644 --- a/askbot/doc/source/contributors.rst +++ b/askbot/doc/source/contributors.rst @@ -38,6 +38,7 @@ Programming and documentation * `Radim Řehůřek <https://github.com/piskvorky>`_ * `monkut <https://github.com/monkut>`_ * `Jim Tittsler <http://wikieducator.org/User:JimTittsler>`_ +* Silvio Heuberger Translations ------------ diff --git a/askbot/doc/source/index.rst b/askbot/doc/source/index.rst index 81f21fcc..353b9105 100644 --- a/askbot/doc/source/index.rst +++ b/askbot/doc/source/index.rst @@ -26,6 +26,7 @@ at the forum_ or by email at admin@askbot.org Appendix C: Optional modules <optional-modules> Appendix D: Askbot as reusable Django application <askbot-as-reusable-django-application> Appendix E: Customizing skin in askbot <customizing-skin-in-askbot> + Appendix F: Intranet setup <intranet-setup> Footnotes <footnotes> Contributors <contributors> Changelog<changelog> diff --git a/askbot/doc/source/intranet-setup.rst b/askbot/doc/source/intranet-setup.rst new file mode 100644 index 00000000..224ffb89 --- /dev/null +++ b/askbot/doc/source/intranet-setup.rst @@ -0,0 +1,14 @@ +========================================================== +Setting up Askbot for use on the closed network (Intranet) +========================================================== + +When using Askbot on the Intranet (for example - within your +Company network), it will be useful to disable references to +all external resources - such as custom fonts, gravatars. + +Please change the following settings in your ``settings.py`` file:: + + ASKBOT_USE_LOCAL_FONTS=True + +In addition, in the "live settings": +* disable gravatar in "settings->User settings" diff --git a/askbot/feed.py b/askbot/feed.py index c1933afe..776aad5e 100644 --- a/askbot/feed.py +++ b/askbot/feed.py @@ -25,20 +25,29 @@ from askbot.conf import settings as askbot_settings class RssIndividualQuestionFeed(Feed): """rss feed class for particular questions """ - title = askbot_settings.APP_TITLE + _(' - ')+ _('Individual question feed') - link = askbot_settings.APP_URL - description = askbot_settings.APP_DESCRIPTION - copyright = askbot_settings.APP_COPYRIGHT + + def title(self): + return askbot_settings.APP_TITLE + _(' - ') + \ + _('Individual question feed') + + def feed_copyright(self): + return askbot_settings.APP_COPYRIGHT + + def description(self): + return askbot_settings.APP_DESCRIPTION def get_object(self, bits): if len(bits) != 1: raise ObjectDoesNotExist return Post.objects.get_questions().get(id__exact = bits[0]) - + def item_link(self, item): """get full url to the item """ - return self.link + item.get_absolute_url() + return askbot_settings.APP_URL + item.get_absolute_url() + + def link(self): + return askbot_settings.APP_URL def item_pubdate(self, item): """get date of creation for the item @@ -56,7 +65,7 @@ class RssIndividualQuestionFeed(Feed): chain_elements.append( Post.objects.get_comments().filter(parent=item) ) - + answers = Post.objects.get_answers().filter(thread = item.thread) for answer in answers: chain_elements.append([answer,]) @@ -65,7 +74,7 @@ class RssIndividualQuestionFeed(Feed): ) return itertools.chain(*chain_elements) - + def item_title(self, item): """returns the title for the item """ @@ -77,7 +86,7 @@ class RssIndividualQuestionFeed(Feed): elif item.post_type == "comment": title = "Comment by %s for %s" % (item.author, self.title) return title - + def item_description(self, item): """returns the description for the item """ @@ -87,16 +96,24 @@ class RssIndividualQuestionFeed(Feed): class RssLastestQuestionsFeed(Feed): """rss feed class for the latest questions """ - title = askbot_settings.APP_TITLE + _(' - ')+ _('latest questions') - link = askbot_settings.APP_URL - description = askbot_settings.APP_DESCRIPTION - #ttl = 10 - copyright = askbot_settings.APP_COPYRIGHT + + def title(self): + return askbot_settings.APP_TITLE + _(' - ') + \ + _('Individual question feed') + + def feed_copyright(self): + return askbot_settings.APP_COPYRIGHT + + def description(self): + return askbot_settings.APP_DESCRIPTION def item_link(self, item): """get full url to the item """ - return self.link + item.get_absolute_url() + return askbot_settings.APP_URL + item.get_absolute_url() + + def link(self): + return askbot_settings.APP_URL def item_author_name(self, item): """get name of author @@ -117,10 +134,10 @@ class RssLastestQuestionsFeed(Feed): """returns url without the slug because the slug can change """ - return self.link + item.get_absolute_url(no_slug = True) - + return askbot_settings.APP_URL + item.get_absolute_url(no_slug = True) + def item_description(self, item): - """returns the desciption for the item + """returns the description for the item """ return item.text @@ -142,12 +159,12 @@ class RssLastestQuestionsFeed(Feed): if tags: #if there are tags in GET, filter the #questions additionally - for tag in tags: + for tag in tags: qs = qs.filter(thread__tags__name = tag) - + return qs.order_by('-thread__last_activity_at')[:30] - + def main(): """main function for use as a script diff --git a/askbot/lamson_handlers.py b/askbot/lamson_handlers.py index 9062594c..f6b6b752 100644 --- a/askbot/lamson_handlers.py +++ b/askbot/lamson_handlers.py @@ -1,12 +1,15 @@ import re -from lamson.routing import route, stateless -from lamson.server import Relay -from django.utils.translation import ugettext as _ +import functools from django.core.files.uploadedfile import SimpleUploadedFile from django.conf import settings as django_settings +from django.template import Context +from django.utils.translation import ugettext as _ +from lamson.routing import route, stateless +from lamson.server import Relay from askbot.models import ReplyAddress, Tag from askbot.utils import mail from askbot.conf import settings as askbot_settings +from askbot.skins.loaders import get_template #we might end up needing to use something like this @@ -114,11 +117,70 @@ def get_parts(message): parts.append((part_type, part_content)) return parts +def process_reply(func): + @functools.wraps(func) + def wrapped(message, host = None, address = None): + """processes forwarding rules, and run the handler + in the case of error, send a bounce email + """ + try: + for rule in django_settings.LAMSON_FORWARD: + if re.match(rule['pattern'], message.base['to']): + relay = Relay(host=rule['host'], + port=rule['port'], debug=1) + relay.deliver(message) + return + except AttributeError: + pass + + error = None + try: + reply_address = ReplyAddress.objects.get( + address = address, + allowed_from_email = message.From + ) + + #here is the business part of this function + func( + from_address = message.From, + subject_line = message['Subject'], + parts = get_parts(message), + reply_address_object = reply_address + ) + + except ReplyAddress.DoesNotExist: + error = _("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.") + except Exception, e: + import sys + sys.stderr.write(str(e)) + import traceback + sys.stderr.write(traceback.format_exc()) + + if error is not None: + template = get_template('email/reply_by_email_error.html') + body_text = template.render(Context({'error':error})) + mail.send_mail( + subject_line = "Error posting your reply", + body_text = body_text, + recipient_list = [message.From], + ) + + return wrapped + @route('(addr)@(host)', addr = '.+') @stateless def ASK(message, host = None, addr = None): + """lamson handler for asking by email, + to the forum in general and to a specific group""" + + #we need to exclude some other emails by prefix if addr.startswith('reply-'): return + if addr.startswith('welcome-'): + return + parts = get_parts(message) from_address = message.From subject = message['Subject']#why lamson does not give it normally? @@ -141,52 +203,69 @@ def ASK(message, host = None, addr = None): except Tag.MultipleObjectsReturned: return +@route('welcome-(address)@host', address='.+') +@stateless +@process_reply +def VALIDATE_EMAIL( + parts = None, + reply_address_object = None, + **kwargs +): + """process the validation email and save + the email signature + todo: go a step further and + """ + content, stored_files = mail.process_parts(parts) + reply_code = reply_address_object.address + if reply_code in content: + + #extract the signature + tail = list() + for line in reversed(content.splitlines()): + if reply_code in line: + break + tail.append(line) + signature = '\n'.join(reversed(tail)) + + #save the signature and mark email as valid + user = reply_address_object.user + user.email_signature = signature + user.email_isvalid = True + user.save() + + data = { + 'site_name': askbot_settings.APP_SHORT_NAME, + 'site_url': askbot_settings.APP_URL, + 'ask_address': 'ask@' + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + } + template = get_template('email/re_welcome_lamson_on.html') + + mail.send_mail( + subject_line = _('Re: Welcome to %(site_name)s') % data, + body_text = template.render(Context(data)), + recipient_list = [from_address,] + ) + + else: + raise ValueError( + _( + 'Please reply to the welcome email ' + 'without editing it' + ) + ) + @route('reply-(address)@(host)', address='.+') @stateless -def PROCESS(message, address = None, host = None): +@process_reply +def PROCESS( + parts = None, + reply_address_object = None, + **kwargs +): """handler to process the emailed message and make a post to askbot based on the contents of the email, including the text body and the file attachments""" - try: - for rule in django_settings.LAMSON_FORWARD: - if re.match(rule['pattern'], message.base['to']): - relay = Relay(host=rule['host'], - port=rule['port'], debug=1) - relay.deliver(message) - return - except AttributeError: - pass - - error = None - try: - reply_address = ReplyAddress.objects.get( - address = address, - allowed_from_email = message.From - ) - parts = get_parts(message) - if reply_address.was_used: - reply_address.edit_post(parts) - else: - reply_address.create_reply(parts) - except ReplyAddress.DoesNotExist: - error = _("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.") - except Exception, e: - import sys - sys.stderr.write(str(e)) - import traceback - sys.stderr.write(traceback.format_exc()) - - if error is not None: - from askbot.utils import mail - from django.template import Context - from askbot.skins.loaders import get_template - - template = get_template('reply_by_email_error.html') - body_text = template.render(Context({'error':error})) - mail.send_mail( - subject_line = "Error posting your reply", - body_text = body_text, - recipient_list = [message.From], - ) + if reply_address_object.was_used: + reply_address_object.edit_post(parts) + else: + reply_address_object.create_reply(parts) diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index 218bd9a9..b7624e21 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -475,7 +475,7 @@ class Command(NoArgsCommand): text += _( '<p>Please remember that you can always <a ' - 'hrefl"%(email_settings_link)s">adjust</a> frequency of the email updates or ' + 'href="%(email_settings_link)s">adjust</a> frequency of the email updates or ' 'turn them off entirely.<br/>If you believe that this message was sent in an ' 'error, please email about it the forum administrator at %(admin_email)s.</' 'p><p>Sincerely,</p><p>Your friendly %(sitename)s server.</p>' diff --git a/askbot/middleware/locale.py b/askbot/middleware/locale.py new file mode 100644 index 00000000..c92e977a --- /dev/null +++ b/askbot/middleware/locale.py @@ -0,0 +1,26 @@ +"Taken from django.middleware.locale: this is the locale selecting middleware that will look at accept headers" + +from django.utils.cache import patch_vary_headers +from django.utils import translation +from askbot.conf import settings + +class LocaleMiddleware(object): + """ + This is a very simple middleware that parses a request + and decides what translation object to install in the current + thread context. This allows pages to be dynamically + translated to the language the user desires (if the language + is available, of course). + """ + + def process_request(self, request): + language = settings.ASKBOT_LANGUAGE + translation.activate(language) + request.LANGUAGE_CODE = translation.get_language() + + def process_response(self, request, response): + patch_vary_headers(response, ('Accept-Language',)) + if 'Content-Language' not in response: + response['Content-Language'] = translation.get_language() + translation.deactivate() + return response 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 d26bdeb0..70ef2f8d 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 @@ -5,7 +5,7 @@ from south.v2 import SchemaMigration from django.db import models class Migration(SchemaMigration): - + def forwards(self, orm): # Removing unique constraint on 'BadgeData', fields ['type', 'name'] @@ -34,10 +34,10 @@ class Migration(SchemaMigration): except: db.rollback_transaction() - - + + def backwards(self, orm): - + # Adding field 'BadgeData.multiple' db.add_column('askbot_badgedata', 'multiple', self.gf('django.db.models.fields.BooleanField')(default=False, blank=True), keep_default=False) @@ -55,8 +55,8 @@ class Migration(SchemaMigration): # Removing unique constraint on 'BadgeData', fields ['slug'] db.delete_unique('askbot_badgedata', ['slug']) - - + + models = { 'askbot.activity': { 'Meta': {'object_name': 'Activity', 'db_table': "u'activity'"}, @@ -335,5 +335,5 @@ class Migration(SchemaMigration): 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) } } - + complete_apps = ['askbot'] diff --git a/askbot/migrations/0123_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py b/askbot/migrations/0123_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py new file mode 100644 index 00000000..ecb66552 --- /dev/null +++ b/askbot/migrations/0123_auto__add_field_post_is_private__add_field_replyaddress_reply_action.py @@ -0,0 +1,336 @@ +# -*- 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 'Post.is_private' + db.add_column('askbot_post', 'is_private', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + # Adding field 'ReplyAddress.reply_action' + db.add_column('askbot_replyaddress', 'reply_action', + self.gf('django.db.models.fields.CharField')(default='auto_answer_or_comment', max_length=32), + keep_default=False) + + # Changing field 'ReplyAddress.post' + db.alter_column('askbot_replyaddress', 'post_id', self.gf('django.db.models.fields.related.ForeignKey')(null=True, to=orm['askbot.Post'])) + + try: + # Adding field 'User.interesting_tags' + db.add_column(u'auth_user', 'email_signature', self.gf('django.db.models.fields.TextField')(blank=True, default = ''), keep_default=False) + except: + pass + + def backwards(self, orm): + db.delete_column('askbot_post', 'is_private') + db.delete_column('askbot_replyaddress', 'reply_action') + db.delete_column('auth_user', 'email_signature') + db.alter_column('askbot_replyaddress', 'post_id', self.gf('django.db.models.fields.related.ForeignKey')(default=None, to=orm['askbot.Post'])) + + 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'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + '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'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + '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.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.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.groupmembership': { + 'Meta': {'unique_together': "(('group', 'user'),)", 'object_name': 'GroupMembership'}, + 'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'user_memberships'", 'to': "orm['askbot.Tag']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group_memberships'", 'to': "orm['auth.User']"}) + }, + 'askbot.groupprofile': { + 'Meta': {'object_name': 'GroupProfile'}, + 'group_tag': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'group_profile'", 'unique': 'True', 'to': "orm['askbot.Tag']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_open': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'logo_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True'}), + 'moderate_email': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + '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'}) + }, + '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']"}), + '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'}), + 'is_private': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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']"}), + 'post_type': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'summary': ('django.db.models.fields.CharField', [], {'max_length': '180'}), + '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', [], {}), + 'revision_type': ('django.db.models.fields.SmallIntegerField', [], {}), + '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', [], {}), + 'title': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '300', 'blank': 'True'}) + }, + '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.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'", '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'}), + '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.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'}), + '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']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + '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']"}), + 'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + '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.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.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_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_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + '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'}), + 'silver': ('django.db.models.fields.SmallIntegerField', [], {'default': '0'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'w'", 'max_length': '2'}), + 'subscribed_tags': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + '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': '30'}), + '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'] diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py index 88c8f7b1..967880be 100644 --- a/askbot/models/__init__.py +++ b/askbot/models/__init__.py @@ -1,9 +1,10 @@ from askbot import startup_procedures startup_procedures.run() -import logging -import hashlib +import collections import datetime +import hashlib +import logging import urllib from django.core.urlresolvers import reverse, NoReverseMatch from django.db.models import signals as django_signals @@ -120,6 +121,7 @@ User.add_to_class('about', models.TextField(blank=True)) User.add_to_class('interesting_tags', models.TextField(blank = True)) User.add_to_class('ignored_tags', models.TextField(blank = True)) User.add_to_class('subscribed_tags', models.TextField(blank = True)) +User.add_to_class('email_signature', models.TextField(blank = True)) User.add_to_class( 'email_tag_filter_strategy', models.SmallIntegerField( @@ -203,6 +205,13 @@ def user_update_avatar_type(self): self.avatar_type = _check_gravatar(self.gravatar) self.save() +def user_strip_email_signature(self, text): + """strips email signature from the end of the text""" + text = '\n'.join(text.splitlines())#normalize the line endings + if text.endswith(self.email_signature): + return text[0:-len(self.email_signature)] + return text + def _check_gravatar(gravatar): gravatar_url = "http://www.gravatar.com/avatar/%s?d=404" % gravatar code = urllib.urlopen(gravatar_url).getcode() @@ -2035,6 +2044,35 @@ def get_profile_link(self): return mark_safe(profile_link) +def user_get_groups_membership_info(self, groups): + """returts a defaultdict with values that are + dictionaries with the following keys and values: + * key: can_join, value: True if user can join group + * key: is_member, value: True if user is member of group + + ``groups`` is a group tag query set + """ + groups = groups.select_related('group_profile') + + group_ids = groups.values_list('id', flat = True) + memberships = GroupMembership.objects.filter( + user__id = self.id, + group__id__in = group_ids + ) + + info = collections.defaultdict( + lambda: {'can_join': False, 'is_member': False} + ) + for membership in memberships: + info[membership.group_id]['is_member'] = True + + for group in groups: + info[group.id]['can_join'] = group.group_profile.can_accept_user(self) + + return info + + + def user_get_karma_summary(self): """returns human readable sentence about status of user's karma""" @@ -2416,6 +2454,8 @@ User.add_to_class('get_absolute_url', user_get_absolute_url) User.add_to_class('get_avatar_url', user_get_avatar_url) User.add_to_class('get_default_avatar_url', user_get_default_avatar_url) User.add_to_class('get_gravatar_url', user_get_gravatar_url) +User.add_to_class('strip_email_signature', user_strip_email_signature) +User.add_to_class('get_groups_membership_info', user_get_groups_membership_info) User.add_to_class('get_anonymous_name', user_get_anonymous_name) User.add_to_class('update_avatar_type', user_update_avatar_type) User.add_to_class('post_question', user_post_question) @@ -2534,6 +2574,7 @@ def format_instant_notification_email( to_user = None, from_user = None, post = None, + reply_with_comment_address = None, update_type = None, template = None, ): @@ -2630,6 +2671,9 @@ def format_instant_notification_email( 'user_action': user_action, 'instruction': _('To reply, PLEASE WRITE ABOVE THIS LINE.') } + if post.post_type == 'question' and reply_with_comment_address: + data = {'addr': reply_with_comment_address} + reply_separator += '<br>' + const.REPLY_WITH_COMMENT_TEMPLATE % data else: reply_separator = user_action @@ -2678,26 +2722,45 @@ def send_instant_notifications_about_activity_in_post( origin_post = post.get_origin_post() for user in recipients: - subject_line, body_text = format_instant_notification_email( - to_user = user, - from_user = update_activity.user, - post = post, - update_type = update_type, - template = get_template('instant_notification.html') - ) - #todo: this could be packaged as an "action" - a bundle #of executive function with the activity log recording #TODO check user reputation headers = mail.thread_headers(post, origin_post, update_activity.activity_type) + reply_with_comment_address = None#only used for questions in some cases if askbot_settings.REPLY_BY_EMAIL: - reply_address = "noreply" + reply_addr = "noreply" if user.reputation >= askbot_settings.MIN_REP_TO_POST_BY_EMAIL: - reply_address = ReplyAddress.objects.create_new(post, user).address - reply_to = 'reply-%s@%s' % (reply_address, askbot_settings.REPLY_BY_EMAIL_HOSTNAME) + + reply_args = { + 'post': post, + 'user': user, + 'reply_action': 'post_comment' + } + if post.post_type in ('answer', 'comment'): + reply_addr = ReplyAddress.objects.create_new(**reply_args) + elif post.post_type == 'question': + reply_with_comment_address = ReplyAddress.objects.create_new(**reply_args) + #default action is to post answer + reply_args['reply_action'] = 'post_answer' + reply_addr = ReplyAddress.objects.create_new(**reply_args) + + reply_to = 'reply-%s@%s' % ( + reply_addr, + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + ) headers.update({'Reply-To': reply_to}) else: reply_to = django_settings.DEFAULT_FROM_EMAIL + + subject_line, body_text = format_instant_notification_email( + to_user = user, + from_user = update_activity.user, + post = post, + reply_with_comment_address = reply_with_comment_address, + update_type = update_type, + template = get_template('instant_notification.html') + ) + mail.send_mail( subject_line = subject_line, body_text = body_text, @@ -2992,6 +3055,57 @@ def record_user_full_updated(instance, **kwargs): ) activity.save() +def send_respondable_email_validation_message( + user = None, subject_line = None, data = None, template_name = None +): + """sends email validation message to the user + + We validate email by getting user's reply + to the validation message by email, which also gives + an opportunity to extract user's email signature. + """ + reply_address = ReplyAddress.objects.create_new( + user = user, + reply_action = 'validate_email' + ) + data['email_code'] = reply_address.address + + from askbot.skins.loaders import get_template + template = get_template(template_name) + body_text = template.render(Context(data)) + + reply_to_address = 'welcome-%s@%s' % ( + reply_address.address, + askbot_settings.REPLY_BY_EMAIL_HOSTNAME + ) + + mail.send_mail( + subject_line = subject_line, + body_text = body_text, + recipient_list = [user.email, ], + activity_type = const.TYPE_ACTIVITY_VALIDATION_EMAIL_SENT, + headers = {'Reply-To': reply_to_address} + ) + + +def send_welcome_email(user, **kwargs): + """sends welcome email to the newly created user + + todo: second branch should send email with a simple + clickable link. + """ + if askbot_settings.REPLY_BY_EMAIL:#with this on we also collect signature + data = { + 'site_name': askbot_settings.APP_SHORT_NAME + } + send_respondable_email_validation_message( + user = user, + subject_line = _('Welcome to %(site_name)s') % data, + data = data, + template_name = 'email/welcome_email_lamson_on.html' + ) + + def complete_pending_tag_subscriptions(sender, request, *args, **kwargs): """save pending tag subscriptions saved in the session""" if 'subscribe_for_tags' in request.session: @@ -3088,6 +3202,7 @@ signals.delete_question_or_answer.connect(record_delete_question, sender=Post) 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(send_welcome_email) 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) diff --git a/askbot/models/post.py b/askbot/models/post.py index a0fdc4ff..b6ee6d2a 100644 --- a/askbot/models/post.py +++ b/askbot/models/post.py @@ -332,6 +332,11 @@ class Post(models.Model): #the reason is that the title and tags belong to thread, #but the question body to Post is_anonymous = models.BooleanField(default=False) + #When is_private == True + #the post is visible only to the some privileged users. + #The privilege may be defined through groups to which + #the thread belongs or in some other way. + is_private = models.BooleanField(default=False) objects = PostManager() diff --git a/askbot/models/reply_by_email.py b/askbot/models/reply_by_email.py index d329d38b..4398f693 100644 --- a/askbot/models/reply_by_email.py +++ b/askbot/models/reply_by_email.py @@ -1,9 +1,10 @@ from datetime import datetime import random import string +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 as _ from askbot.models.post import Post from askbot.models.base import BaseQuerySetManager from askbot.conf import settings as askbot_settings @@ -19,13 +20,10 @@ class ReplyAddressManager(BaseQuerySetManager): used_at__isnull = True ) - def create_new(self, post, user): + def create_new(self, **kwargs): """creates a new reply address""" - reply_address = ReplyAddress( - post = post, - user = user, - allowed_from_email = user.email - ) + kwargs['allowed_from_email'] = kwargs['user'].email + reply_address = ReplyAddress(**kwargs) while True: reply_address.address = ''.join(random.choice(string.letters + string.digits) for i in xrange(random.randint(12, 25))).lower() @@ -35,14 +33,26 @@ class ReplyAddressManager(BaseQuerySetManager): return reply_address +REPLY_ACTION_CHOICES = ( + ('post_answer', _('Post an answer')), + ('post_comment', _('Post a comment')), + ('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 and the user""" address = models.CharField(max_length = 25, unique = True) post = models.ForeignKey( Post, + null = True,#reply not necessarily to posts related_name = 'reply_addresses' )#the emailed post + reply_action = models.CharField( + max_length = 32, + choices = REPLY_ACTION_CHOICES, + default = 'auto_answer_or_comment' + ) response_post = models.ForeignKey( Post, null = True, @@ -92,19 +102,32 @@ class ReplyAddress(models.Model): by_email = True ) elif self.post.post_type == 'question': - wordcount = len(content)/6#this is a simplistic hack - if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL: + if self.reply_action == 'auto_answer_or_comment': + wordcount = len(content)/6#todo: this is a simplistic hack + if wordcount > askbot_settings.MIN_WORDS_FOR_ANSWER_BY_EMAIL: + reply_action = 'post_answer' + else: + reply_action = 'post_comment' + else: + reply_action = self.reply_action + + if reply_action == 'post_answer': result = self.user.post_answer( self.post, content, by_email = True ) - else: + elif reply_action == 'post_comment': result = self.user.post_comment( self.post, content, by_email = True ) + else: + logging.critical( + 'Unexpected reply action: "%s", post by email failed' % reply_action + ) + return None#todo: there may be a better action to take here... elif self.post.post_type == 'comment': result = self.user.post_comment( self.post.parent, diff --git a/askbot/models/signals.py b/askbot/models/signals.py index baa4c149..28fe70b0 100644 --- a/askbot/models/signals.py +++ b/askbot/models/signals.py @@ -21,6 +21,7 @@ delete_question_or_answer = django.dispatch.Signal( flag_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by']) remove_flag_offensive = django.dispatch.Signal(providing_args=['instance', 'mark_by']) user_updated = django.dispatch.Signal(providing_args=['instance', 'updated_by']) +user_registered = django.dispatch.Signal(providing_args=['user',]) #todo: move this to authentication app user_logged_in = django.dispatch.Signal(providing_args=['session']) @@ -65,6 +66,7 @@ def pop_all_db_signal_receivers(): remove_flag_offensive, user_updated, user_logged_in, + user_registered, post_updated, award_badges_signal, #django signals diff --git a/askbot/models/tag.py b/askbot/models/tag.py index 779c0b68..5a83c67c 100644 --- a/askbot/models/tag.py +++ b/askbot/models/tag.py @@ -82,21 +82,34 @@ class TagManager(BaseQuerySetManager): def get_query_set(self): return TagQuerySet(self.model) -#todo: implement this -#class GroupTagQuerySet(models.query.QuerySet): -# """Custom query set for the group""" -# def __init__(self, model): +class GroupTagQuerySet(TagQuerySet): + """Custom query set for the group""" + + def get_for_user(self, user = None): + return self.filter(user_memberships__user = user) + + def get_all(self): + return self.annotate( + member_count = models.Count('user_memberships') + ).filter( + member_count__gt = 0 + ) + + def get_by_name(self, group_name = None): + return self.get(name = clean_group_name(group_name)) + + def clean_group_name(name): """group names allow spaces, tag names do not, so we use this method to replace spaces with dashes""" return re.sub('\s+', '-', name.strip()) -class GroupTagManager(TagManager): +class GroupTagManager(BaseQuerySetManager): """manager for group tags""" -# def get_query_set(self): -# return GroupTagQuerySet(self.model) + def get_query_set(self): + return GroupTagQuerySet(self.model) def get_or_create(self, group_name = None, user = None): """creates a group tag or finds one, if exists""" @@ -114,21 +127,6 @@ class GroupTagManager(TagManager): group_profile.save() return tag - #todo: maybe move this to query set - def get_for_user(self, user = None): - return self.filter(user_memberships__user = user) - - #todo: remove this when the custom query set is done - def get_all(self): - return self.annotate( - member_count = models.Count('user_memberships') - ).filter( - member_count__gt = 0 - ) - - def get_by_name(self, group_name = None): - return self.get(name = clean_group_name(group_name)) - class Tag(models.Model): name = models.CharField(max_length=255, unique=True) created_by = models.ForeignKey(User, related_name='created_tags') diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py index 191d13f3..32af9920 100644 --- a/askbot/setup_templates/settings.py +++ b/askbot/setup_templates/settings.py @@ -98,6 +98,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( #'django.middleware.gzip.GZipMiddleware', + #'askbot.middleware.locale.LocaleMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', #'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache index 3287fb18..eb1cb1c1 100644 --- a/askbot/setup_templates/settings.py.mustache +++ b/askbot/setup_templates/settings.py.mustache @@ -97,6 +97,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( #'django.middleware.gzip.GZipMiddleware', + 'askbot.middleware.locale.LocaleMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', #'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', diff --git a/askbot/skins/common/media/js/editor.js b/askbot/skins/common/media/js/editor.js index 2d1f5670..ae4f5aea 100644 --- a/askbot/skins/common/media/js/editor.js +++ b/askbot/skins/common/media/js/editor.js @@ -46,6 +46,11 @@ function ajaxFileUpload(imageUrl, startUploadHandler) success: function (data, status) { var fileURL = $(data).find('file_url').text(); + /* + * hopefully a fix for the "fakepath" issue + * https://www.mediawiki.org/wiki/Special:Code/MediaWiki/83225 + */ + fileURL = fileURL.replace(/\w:.*\\(.*)$/,'$1'); var error = $(data).find('error').text(); if(error != ''){ alert(error); diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js index 294c5f41..7df01c83 100644 --- a/askbot/skins/common/media/js/post.js +++ b/askbot/skins/common/media/js/post.js @@ -2354,9 +2354,8 @@ UserGroupProfileEditor.prototype.decorate = function(element){ logo_changer.decorate(change_logo_btn); }; -var GroupJoinButton = function(group_id){ +var GroupJoinButton = function(){ TwoStateToggle.call(this); - this._group_id = group_id; }; inherits(GroupJoinButton, TwoStateToggle); @@ -2375,7 +2374,8 @@ GroupJoinButton.prototype.getHandler = function(){ url: askbot['urls']['join_or_leave_group'], success: function(data){ if (data['success']){ - me.setOn(data['is_member']); + var new_state = data['is_member'] ? 'on-state':'off-state'; + me.setState(new_state); } else { showMessage(me.getElement(), data['message']); } @@ -2384,6 +2384,11 @@ GroupJoinButton.prototype.getHandler = function(){ }; }; +GroupJoinButton.prototype.decorate = function(elem) { + GroupJoinButton.superClass_.decorate.call(this, elem); + this._group_id = this._element.data('groupId'); +}; + $(document).ready(function() { $('[id^="comments-for-"]').each(function(index, element){ var comments = new PostCommentsWidget(); diff --git a/askbot/skins/common/media/js/user.js b/askbot/skins/common/media/js/user.js index b7dc0951..ad0b8365 100644 --- a/askbot/skins/common/media/js/user.js +++ b/askbot/skins/common/media/js/user.js @@ -644,13 +644,13 @@ UserGroup.prototype.decorate = function(element){ this._name = $.trim(element.find('a').html()); var deleter = new DeleteIcon(); deleter.setHandler(this.getDeleteHandler()); - deleter.setContent('x'); - this._element.find('.group-name').append(deleter.getElement()); + deleter.setContent(gettext('Remove')); + this._element.find('td:last').append(deleter.getElement()); this._delete_icon = deleter; }; UserGroup.prototype.createDom = function(){ - var element = this.makeElement('li'); + var element = this.makeElement('tr'); element.html(this._content); this._element = element; this.decorate(element); @@ -675,7 +675,7 @@ GroupsContainer.prototype.decorate = function(element){ var group_names = []; var me = this; //collect list of groups - $.each(element.find('li'), function(idx, li){ + $.each(element.find('tr'), function(idx, li){ var group = new UserGroup(); group.setGroupsContainer(me); group.decorate($(li)); @@ -829,21 +829,25 @@ UserGroupsEditor.prototype.decorate = function(element){ adder.decorate(add_link); var groups_container = new GroupsContainer(); - groups_container.decorate(element.find('ul')); + groups_container.decorate(element.find('#groups-list')); adder.setGroupsContainer(groups_container); //todo - add group deleters }; (function(){ - var fbtn = $('.follow-toggle'); + var fbtn = $('.follow-user-toggle'); if (fbtn.length === 1){ var follow_user = new FollowUser(); follow_user.decorate(fbtn); follow_user.setUserName(askbot['data']['viewUserName']); } - if (askbot['data']['userIsAdminOrMod']){ - var group_editor = new UserGroupsEditor(); - group_editor.decorate($('#user-groups')); + if (askbot['data']['userId'] !== askbot['data']['viewUserId']) { + if (askbot['data']['userIsAdminOrMod']){ + var group_editor = new UserGroupsEditor(); + group_editor.decorate($('#user-groups')); + } else { + $('#add-group').remove(); + } } else { $('#add-group').remove(); } diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js index 297e3f9a..8c2a478e 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -786,9 +786,7 @@ TwoStateToggle.prototype.decorate = function(element){ this.toggleUrl = element.attr('data-toggle-url'); //detect state and save it - if ( - element.attr('nodeName') === 'INPUT' && element.attr('type', 'checkbox') - ) { + if (this.isCheckBox()) { this._state = element.attr('checked') ? 'state-on' : 'state-off'; } else { var text = $.trim(element.html()); diff --git a/askbot/skins/default/media/bootstrap/css/bootstrap.css b/askbot/skins/default/media/bootstrap/css/bootstrap.css index 9447a9a2..3e829732 100644 --- a/askbot/skins/default/media/bootstrap/css/bootstrap.css +++ b/askbot/skins/default/media/bootstrap/css/bootstrap.css @@ -918,18 +918,6 @@ input, textarea, select, .uneditable-input { - display: inline-block; - width: 210px; - height: 18px; - padding: 4px; - margin-bottom: 9px; - font-size: 13px; - line-height: 18px; - color: #555555; - border: 1px solid #cccccc; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; } .uneditable-textarea { width: auto; diff --git a/askbot/skins/default/media/images/OFL.txt b/askbot/skins/default/media/images/OFL.txt new file mode 100644 index 00000000..3bc11311 --- /dev/null +++ b/askbot/skins/default/media/images/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010, Jan Gerner (post@yanone.de)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/askbot/skins/default/media/images/YanoneKaffeesatz-Bold.ttf b/askbot/skins/default/media/images/YanoneKaffeesatz-Bold.ttf Binary files differnew file mode 100644 index 00000000..c693c4b3 --- /dev/null +++ b/askbot/skins/default/media/images/YanoneKaffeesatz-Bold.ttf diff --git a/askbot/skins/default/media/images/YanoneKaffeesatz-ExtraLight.ttf b/askbot/skins/default/media/images/YanoneKaffeesatz-ExtraLight.ttf Binary files differnew file mode 100644 index 00000000..b59e4894 --- /dev/null +++ b/askbot/skins/default/media/images/YanoneKaffeesatz-ExtraLight.ttf diff --git a/askbot/skins/default/media/images/YanoneKaffeesatz-Light.ttf b/askbot/skins/default/media/images/YanoneKaffeesatz-Light.ttf Binary files differnew file mode 100644 index 00000000..5026d3bd --- /dev/null +++ b/askbot/skins/default/media/images/YanoneKaffeesatz-Light.ttf diff --git a/askbot/skins/default/media/images/YanoneKaffeesatz-Regular.ttf b/askbot/skins/default/media/images/YanoneKaffeesatz-Regular.ttf Binary files differnew file mode 100644 index 00000000..808ce0d0 --- /dev/null +++ b/askbot/skins/default/media/images/YanoneKaffeesatz-Regular.ttf diff --git a/askbot/skins/default/media/images/Yanone_Kaffeesatz.zip b/askbot/skins/default/media/images/Yanone_Kaffeesatz.zip Binary files differnew file mode 100644 index 00000000..55e9731a --- /dev/null +++ b/askbot/skins/default/media/images/Yanone_Kaffeesatz.zip diff --git a/askbot/skins/default/media/style/lib_style.css b/askbot/skins/default/media/style/lib_style.css new file mode 100644 index 00000000..a92af477 --- /dev/null +++ b/askbot/skins/default/media/style/lib_style.css @@ -0,0 +1,22 @@ +/* General Predifined classes, read more in lesscss.org */ +/* Variables for Colors*/ +/* Variables for fonts*/ +/* "Trebuchet MS", sans-serif;*/ +/* Buttons */ +.button-style-hover { + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + text-decoration: none; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +/* General styles for gradients */ +/* Receive exactly positions for background Sprite */ +/* CSS3 Elements */ diff --git a/askbot/skins/default/media/style/lib_style.less b/askbot/skins/default/media/style/lib_style.less index 795733e5..63389526 100644 --- a/askbot/skins/default/media/style/lib_style.less +++ b/askbot/skins/default/media/style/lib_style.less @@ -17,6 +17,43 @@ @main-font:'Yanone Kaffeesatz', Arial, sans-serif; @secondary-font:Arial; +/* Buttons */ + +.button-style(@w:100px ,@h:20px, @f:14px){ + width:@w; + height:@h; + font-size:@f; + text-align:center; + text-decoration:none; + cursor:pointer; + color:@button-label; + font-family:@main-font; + .text-shadow(0px,1px,0px,#c6d9dd); + border-top:#eaf2f3 1px solid; + .linear-gradient(#d1e2e5,#a9c2c7); + .rounded-corners(4px); + .box-shadow(1px, 1px, 2px, #636363) +} + +.button-style-hover{ + .linear-gradient(#cde5e9,#94b3ba); + text-decoration:none; + .text-shadow(0px, 1px, 0px, #c6d9dd); +} + +/* General styles for gradients */ + +.linear-gradient(@start:#eee,@end:#fff,@stop:25%){ + background-color: @start; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@start), color-stop(@stop, @start), to(@end)); + background-image: -webkit-linear-gradient(@start, @start @stop, @end); + background-image: -moz-linear-gradient(top, @start, @start @stop, @end); + background-image: -ms-linear-gradient(@start, @start @stop, @end); + background-image: -o-linear-gradient(@start, @start @stop, @end); + background-image: linear-gradient(@start, @start @stop, @end); +} + /* Receive exactly positions for background Sprite */ .sprites(@hor,@vert,@back:url(../images/sprites.png)){ diff --git a/askbot/skins/default/media/style/style.css b/askbot/skins/default/media/style/style.css index 73664c57..3d53eba8 100644 --- a/askbot/skins/default/media/style/style.css +++ b/askbot/skins/default/media/style/style.css @@ -3,6 +3,22 @@ /* Variables for Colors*/ /* Variables for fonts*/ /* "Trebuchet MS", sans-serif;*/ +/* Buttons */ +.button-style-hover { + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + text-decoration: none; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +/* General styles for gradients */ /* Receive exactly positions for background Sprite */ /* CSS3 Elements */ /* Library of predifined less functions styles */ @@ -148,7 +164,7 @@ a:hover { } h1 { font-size: 24px; - padding: 10px 0 5px 0px; + padding: 0px 0 5px 0px; } /* ----- Extra space above for messages ----- */ body.user-messages { @@ -179,7 +195,7 @@ body.user-messages { text-align: center; background-color: #f5dd69; border-top: #fff 1px solid; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .notify p.notification { margin-top: 6px; @@ -206,7 +222,7 @@ body.user-messages { #header { margin-top: 0px; background: #16160f; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .content-wrapper { /* wrapper positioning class */ @@ -295,6 +311,9 @@ body.user-messages { #metaNav #navUsers { background: -125px -5px url(../images/sprites.png) no-repeat; } +#metaNav #navGroups { + background: -125px -5px url(../images/sprites.png) no-repeat; +} #metaNav #navBadges { background: -210px -5px url(../images/sprites.png) no-repeat; } @@ -318,7 +337,7 @@ body.user-messages { border-bottom: #d3d3c2 1px solid; border-top: #fcfcfc 1px solid; margin-bottom: 10px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } #secondaryHeader #homeButton { border-right: #afaf9e 1px solid; @@ -427,28 +446,49 @@ body.anon #searchBar .searchInputCancelable { #askButton { /* check blocks/secondary_header.html and widgets/ask_button.html*/ - background: url(../images/bigbutton.png) repeat-x bottom; line-height: 44px; - text-align: center; + margin-top: 6px; + float: right; + text-transform: uppercase; width: 200px; height: 42px; font-size: 23px; + text-align: center; + text-decoration: none; + cursor: pointer; color: #4a757f; - margin-top: 7px; - float: right; - text-transform: uppercase; - border-radius: 5px; - -ms-border-radius: 5px; - -moz-border-radius: 5px; - -webkit-border-radius: 5px; - -khtml-border-radius: 5px; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; -webkit-box-shadow: 1px 1px 2px #636363; -moz-box-shadow: 1px 1px 2px #636363; box-shadow: 1px 1px 2px #636363; } #askButton:hover { + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); text-decoration: none; - background: url(../images/bigbutton.png) repeat-x top; text-shadow: 0px 1px 0px #c6d9dd; -moz-text-shadow: 0px 1px 0px #c6d9dd; -webkit-text-shadow: 0px 1px 0px #c6d9dd; @@ -481,6 +521,8 @@ body.anon #searchBar .searchInputCancelable { } .box p { margin-bottom: 4px; + color: #707070; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .box p.info-box-follow-up-links { text-align: right; @@ -497,14 +539,15 @@ body.anon #searchBar .searchInputCancelable { color: #656565; padding-right: 10px; margin-bottom: 10px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + width: 190px; } .box h3 { color: #4a757f; font-size: 18px; text-align: left; font-weight: normal; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; padding-left: 0px; } .box .contributorback { @@ -516,12 +559,13 @@ body.anon #searchBar .searchInputCancelable { display: block; float: right; text-align: left; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; width: 80px; margin-right: 18px; } -.box #displayTagFilterControl label { - /*Especial width just for the display tag filter box in index page*/ +.box #displayTagFilterControl label, +.box #emailTagFilterControl label { + /*Especial width just for the tag filter boxes in index page*/ width: 160px; } @@ -547,57 +591,108 @@ body.anon #searchBar .searchInputCancelable { font-size: 15px; } .box .inputs #interestingTagInput, -.box .inputs #ignoredTagInput { +.box .inputs #ignoredTagInput, +.box .inputs #subscribedTagInput, +.box .inputs #ab-tag-search { width: 153px; padding-left: 5px; border: #c9c9b5 1px solid; height: 25px; } +.box .inputs #ab-tag-search { + width: 135px; +} .box .inputs #interestingTagAdd, -.box .inputs #ignoredTagAdd { - background: url(../images/small-button-blue.png) repeat-x top; +.box .inputs #ignoredTagAdd, +.box .inputs #subscribedTagAdd, +.box .inputs #ab-tag-search-add { border: 0; - color: #4a757f; font-weight: bold; - font-size: 12px; + margin-top: -2px; width: 30px; height: 27px; - margin-top: -2px; + font-size: 14px; + text-align: center; + text-decoration: none; cursor: pointer; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + border-radius: 4px; + -ms-border-radius: 4px; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + -khtml-border-radius: 4px; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; } .box .inputs #interestingTagAdd:hover, -.box .inputs #ignoredTagAdd:hover { - background: url(../images/small-button-blue.png) repeat-x bottom; +.box .inputs #ignoredTagAdd:hover, +.box .inputs #ab-tag-search-add:hover { + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + text-decoration: none; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +.box .inputs #ab-tag-search-add { + width: 47px; + margin-left: 3px; } .box img.gravatar { margin: 1px; } .box a.followed, .box a.follow { - background: url(../images/medium-button.png) top repeat-x; - height: 34px; line-height: 34px; - text-align: center; border: 0; - font-family: 'Yanone Kaffeesatz', sans-serif; - color: #4a757f; font-weight: normal; - font-size: 21px; margin-top: 3px; display: block; width: 120px; + height: 34px; + font-size: 21px; + text-align: center; text-decoration: none; + cursor: pointer; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; @@ -611,8 +706,18 @@ body.anon #searchBar .searchInputCancelable { } .box a.followed:hover, .box a.follow:hover { + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); text-decoration: none; - background: url(../images/medium-button.png) bottom repeat-x; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; text-shadow: 0px 1px 0px #c6d9dd; -moz-text-shadow: 0px 1px 0px #c6d9dd; -webkit-text-shadow: 0px 1px 0px #c6d9dd; @@ -638,6 +743,10 @@ body.anon #searchBar .searchInputCancelable { .box .notify-sidebar #question-subscribe-sidebar { margin: 7px 0 0 3px; } +.users-page .box label { + display: inline; + float: none; +} .statsWidget p { color: #707070; font-size: 16px; @@ -730,8 +839,7 @@ body.anon #searchBar .searchInputCancelable { .tabsC .label { float: left; color: #646464; - margin-top: 4px; - margin-right: 5px; + margin: 4px 5px 0px 8px; } .main-page .tabsA .label { margin-left: 8px; @@ -776,14 +884,14 @@ body.anon #searchBar .searchInputCancelable { float: left; margin-bottom: 8px; padding-top: 6px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } #listSearchTags { float: left; margin-top: 3px; color: #707070; font-size: 16px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } ul#searchTags { margin-left: 10px; @@ -797,7 +905,7 @@ ul#searchTags { margin: 5px 0 10px 0; padding: 0px; float: left; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .search-tips a { text-decoration: underline; @@ -811,6 +919,9 @@ ul#searchTags { padding: 0; width: 100%; } +.main-page #question-list { + margin-top: 10px; +} .short-summary { position: relative; filter: inherit; @@ -829,7 +940,7 @@ ul#searchTags { padding-left: 0; margin-bottom: 6px; display: block; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .short-summary a { color: #464646; @@ -840,7 +951,7 @@ ul#searchTags { font-family: Arial; padding-right: 4px; } -.short-summary .userinfo .relativetime, +.short-summary .userinfo .timeago, .short-summary span.anonymous { font-size: 11px; clear: both; @@ -854,12 +965,12 @@ ul#searchTags { .short-summary .counts { float: right; margin: 4px 0 0 5px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .short-summary .counts .item-count { padding: 0px 5px 0px 5px; font-size: 25px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .short-summary .counts .votes div, .short-summary .counts .views div, @@ -1049,7 +1160,7 @@ ul#related-tags { ul.tags li { float: left; display: block; - margin: 0 8px 0 0; + margin: 0 8px 8px 0; padding: 0; height: 20px; } @@ -1173,7 +1284,7 @@ ul#related-tags li { /* ----- Ask and Edit Question Form template----- */ .section-title { color: #7ea9b3; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; font-weight: bold; font-size: 24px; } @@ -1199,7 +1310,7 @@ ul#related-tags li { margin: 0px; padding: 0px 0 0 5px; border: #cce6ec 3px solid; - width: 725px; + width: 719px; } .ask-page div#question-list, .edit-question-page div#question-list { @@ -1261,14 +1372,28 @@ ul#related-tags li { .ask-page input.submit, .edit-question-page input.submit { float: left; - background: url(../images/medium-button.png) top repeat-x; - height: 34px; - border: 0; - font-family: 'Yanone Kaffeesatz', sans-serif; - color: #4a757f; font-weight: normal; - font-size: 21px; margin-top: 3px; + width: 160px; + height: 34px; + font-size: 21px; + text-align: center; + text-decoration: none; + cursor: pointer; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; @@ -1282,33 +1407,60 @@ ul#related-tags li { #fmanswer input.submit:hover, .ask-page input.submit:hover, .edit-question-page input.submit:hover { + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); text-decoration: none; - background: url(../images/medium-button.png) bottom repeat-x; text-shadow: 0px 1px 0px #c6d9dd; -moz-text-shadow: 0px 1px 0px #c6d9dd; -webkit-text-shadow: 0px 1px 0px #c6d9dd; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; +} +.wmd-container { + border: #cce6ec 3px solid; +} +.users-page .wmd-container { + width: 200px; +} +.ask-page .wmd-container, +.question-page .wmd-container, +.edit-question-page .wmd-container, +.edit-answer-page .wmd-container { + width: 723px; +} +.ask-page #editor, +.question-page #editor, +.edit-question-page #editor, +.edit-answer-page #editor { + width: 710px; + padding: 6px; } #editor { - /*adjustment for editor preview*/ + /* adjustment for editor preview */ + display: block; font-size: 100%; min-height: 200px; line-height: 18px; margin: 0; - border-left: #cce6ec 3px solid; - border-bottom: #cce6ec 3px solid; - border-right: #cce6ec 3px solid; - border-top: 0; - padding: 10px; - margin-bottom: 10px; - width: 717px; + border: 0; +} +.users-page #editor { + width: 192px; } #id_title { width: 100%; } .wmd-preview { - margin: 3px 0 5px 0; - padding: 6px; + margin: 0; + padding: 5px; background-color: #F5F5F5; min-height: 20px; overflow: auto; @@ -1320,6 +1472,9 @@ ul#related-tags li { line-height: 1.4; font-size: 14px; } +.wmd-preview p:last-child { + margin-bottom: 0; +} .wmd-preview pre { background-color: #E7F1F8; } @@ -1329,6 +1484,9 @@ ul#related-tags li { .wmd-preview IMG { max-width: 600px; } +.user-page .wmd-buttons { + width: 725px; +} .preview-toggle { width: 100%; color: #b6a475; @@ -1383,7 +1541,7 @@ ul#related-tags li { margin: 0px; padding: 0px 0 0 5px; border: #cce6ec 3px solid; - width: 725px; + width: 719px; margin-bottom: 10px; } .edit-question-page #id_summary, @@ -1403,7 +1561,7 @@ ul#related-tags li { /* ----- Question template ----- */ .question-page h1 { padding-top: 0px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .question-page h1 a { color: #464646; @@ -1421,7 +1579,7 @@ ul#related-tags li { margin-left: 0px !important; } .question-page p.rss a { - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; vertical-align: top; } .question-page .question-content { @@ -1586,7 +1744,7 @@ ul#related-tags li { } .question-page #questionCount { float: left; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; line-height: 15px; } .question-page .question-img-upvote, @@ -1632,13 +1790,11 @@ ul#related-tags li { color: #7ea9b3; width: 200px; float: left; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .question-page .comments { font-size: 12px; clear: both; - /* A small hack to solve 1px problem on webkit browsers */ - } .question-page .comments div.controls { clear: both; @@ -1688,11 +1844,6 @@ ul#related-tags li { padding-top: 3px; border: #cce6ec 3px solid; } -@media screen and (-webkit-min-device-pixel-ratio: 0) { - .question-page .comments textarea { - padding-left: 3px !important; - } -} .question-page .comments input { margin-left: 10px; margin-top: 1px; @@ -1700,31 +1851,49 @@ ul#related-tags li { width: 100px; } .question-page .comments button { - background: url(../images/small-button-blue.png) repeat-x top; - border: 0; - color: #4a757f; - font-family: Arial; - font-size: 13px; - width: 100px; - font-weight: bold; - height: 27px; line-height: 25px; margin-bottom: 5px; + width: 100px; + height: 27px; + font-size: 12px; + text-align: center; + text-decoration: none; cursor: pointer; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; + font-family: Arial; + font-weight: bold; } .question-page .comments button:hover { - background: url(../images/small-button-blue.png) bottom repeat-x; + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + text-decoration: none; text-shadow: 0px 1px 0px #c6d9dd; -moz-text-shadow: 0px 1px 0px #c6d9dd; -webkit-text-shadow: 0px 1px 0px #c6d9dd; @@ -1865,12 +2034,17 @@ ul#related-tags li { text-align: center; padding-top: 2px; margin: 10px 10px 0px 3px; + /* smalls IE fixes */ + + *margin: 0; + *height: 210px; + *width: 30px; } .question-page .vote-buttons IMG { cursor: pointer; } .question-page .vote-number { - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; padding: 0px 0 5px 0; font-size: 25px; font-weight: bold; @@ -1948,7 +2122,7 @@ ul#related-tags li { margin-top: 10px; } .question-page #fmanswer h2 { - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; color: #7ea9b3; font-size: 24px; } @@ -1993,7 +2167,6 @@ ul#related-tags li { /* -----Content pages, Login, About, FAQ, Users----- */ .openid-signin, .meta, -.users-page, .user-profile-edit-page { font-size: 13px; line-height: 1.3; @@ -2001,7 +2174,6 @@ ul#related-tags li { } .openid-signin p, .meta p, -.users-page p, .user-profile-edit-page p { font-size: 13px; color: #707070; @@ -2012,7 +2184,6 @@ ul#related-tags li { } .openid-signin h2, .meta h2, -.users-page h2, .user-profile-edit-page h2 { color: #525252; padding-left: 0px; @@ -2070,35 +2241,55 @@ ul#related-tags li { .users-page input.submit, .user-profile-edit-page input.submit, .user-profile-page input.submit { - background: url(../images/small-button-blue.png) repeat-x top; - border: 0; - color: #4a757f; - font-weight: bold; - font-size: 13px; - font-family: Arial; - height: 26px; + font-weight: normal; margin: 5px 0px; width: 100px; + height: 26px; + font-size: 15px; + text-align: center; + text-decoration: none; cursor: pointer; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; + font-family: Arial; } .openid-signin input.submit:hover, .meta input.submit:hover, .users-page input.submit:hover, .user-profile-edit-page input.submit:hover, .user-profile-page input.submit:hover { - background: url(../images/small-button-blue.png) repeat-x bottom; + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); text-decoration: none; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; } .openid-signin .cancel, .meta .cancel, @@ -2115,6 +2306,9 @@ ul#related-tags li { .user-profile-page .cancel:hover { background: url(../images/small-button-cancel.png) repeat-x bottom !important; } +.openid-signin form { + margin-bottom: 5px; +} #email-input-fs, #local_login_buttons, #password-fs, @@ -2145,34 +2339,55 @@ ul#related-tags li { #local_login_buttons .submit-b, #password-fs .submit-b, #openid-fs .submit-b { - background: url(../images/small-button-blue.png) repeat-x top; - border: 0; - color: #4a757f; - font-weight: bold; - font-size: 13px; - font-family: Arial; + width: 100px; height: 24px; - margin-top: -2px; - padding-left: 10px; - padding-right: 10px; + font-size: 15px; + text-align: center; + text-decoration: none; cursor: pointer; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; + font-family: Arial; + font-weight: bold; + padding-right: 10px; + border: 0; } #email-input-fs .submit-b:hover, #local_login_buttons .submit-b:hover, #password-fs .submit-b:hover, #openid-fs .submit-b:hover { - background: url(../images/small-button-blue.png) repeat-x bottom; + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + text-decoration: none; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; } .openid-input { background: url(../images/openid.gif) no-repeat; @@ -2199,20 +2414,15 @@ ul#related-tags li { font-size: 120%; } /* People page */ -.tabBar-user { - width: 375px; -} +/*.users-page .tabBar{ + width:375px; +}*/ .user { - padding: 5px; + padding: 5px 10px 5px 0; line-height: 140%; width: 166px; - border: #eee 1px solid; + height: 32px; margin-bottom: 5px; - border-radius: 3px; - -ms-border-radius: 3px; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - -khtml-border-radius: 3px; } .user .user-micro-info { color: #525252; @@ -2280,7 +2490,7 @@ a:hover.medal { } .user-profile-page h2 { padding: 10px 0px 10px 0px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } .user-details { font-size: 13px; @@ -2302,31 +2512,52 @@ a:hover.medal { .follow-toggle, .submit { border: 0 !important; - color: #4a757f; font-weight: bold; - font-size: 12px; - height: 26px; line-height: 26px; margin-top: -2px; - font-size: 15px; + width: 100px; + height: 26px; + font-size: 12px; + text-align: center; + text-decoration: none; cursor: pointer; - font-family: 'Yanone Kaffeesatz', sans-serif; - background: url(../images/small-button-blue.png) repeat-x top; + color: #4a757f; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; + border-top: #eaf2f3 1px solid; + background-color: #d1e2e5; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#d1e2e5), color-stop(25%, #d1e2e5), to(#a9c2c7)); + background-image: -webkit-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -moz-linear-gradient(top, #d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -ms-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: -o-linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); + background-image: linear-gradient(#d1e2e5, #d1e2e5 25%, #a9c2c7); border-radius: 4px; -ms-border-radius: 4px; -moz-border-radius: 4px; -webkit-border-radius: 4px; -khtml-border-radius: 4px; - text-shadow: 0px 1px 0px #e6f6fa; - -moz-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-text-shadow: 0px 1px 0px #e6f6fa; - -webkit-box-shadow: 1px 1px 2px #808080; - -moz-box-shadow: 1px 1px 2px #808080; - box-shadow: 1px 1px 2px #808080; + -webkit-box-shadow: 1px 1px 2px #636363; + -moz-box-shadow: 1px 1px 2px #636363; + box-shadow: 1px 1px 2px #636363; } .follow-toggle:hover, .submit:hover { - background: url(../images/small-button-blue.png) repeat-x bottom; + background-color: #cde5e9; + background-repeat: no-repeat; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#cde5e9), color-stop(25%, #cde5e9), to(#94b3ba)); + background-image: -webkit-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -moz-linear-gradient(top, #cde5e9, #cde5e9 25%, #94b3ba); + background-image: -ms-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: -o-linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + background-image: linear-gradient(#cde5e9, #cde5e9 25%, #94b3ba); + text-decoration: none; + text-shadow: 0px 1px 0px #c6d9dd; + -moz-text-shadow: 0px 1px 0px #c6d9dd; + -webkit-text-shadow: 0px 1px 0px #c6d9dd; text-decoration: none !important; } .follow-toggle .follow { @@ -2346,13 +2577,13 @@ a:hover.medal { display: none; } .count { - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; font-size: 200%; font-weight: 700; color: #777777; } .scoreNumber { - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; font-size: 35px; font-weight: 800; color: #777; @@ -2463,7 +2694,7 @@ a:hover.medal { color: #525252; } .revision h3 { - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; font-size: 21px; padding-left: 0px; } @@ -2570,7 +2801,7 @@ ins { padding: 6px 0 0 0; background: #16160f; font-size: 16px; - font-family: 'Yanone Kaffeesatz', sans-serif; + font-family: 'Yanone Kaffeesatz', Arial, sans-serif; } #ground p { margin-bottom: 0; @@ -2714,7 +2945,7 @@ span.form-error { padding: 0px; margin: 0px; } -.relativetime { +.timeago { font-weight: bold; text-decoration: none; } @@ -2935,6 +3166,9 @@ button::-moz-focus-inner { -khtml-border-radius: 5px; -webkit-border-radius: 5px; } +.list-table { + border-spacing: 0; +} .list-table td { vertical-align: top; } @@ -3144,7 +3378,6 @@ img.flag { .main-page img.flag { vertical-align: text-bottom; } - /* Pretty printing styles. Used with prettify.js. */ a.edit { padding-left: 3px; @@ -3247,3 +3480,110 @@ body.anon.lang-es #searchBar .searchInput { body.anon.lang-es #searchBar .searchInputCancelable { width: 390px; } +/* user groups */ +#user-groups ul { + margin-bottom: 0px; +} +#user-groups .delete-icon { + float: none; + display: inline; + color: #525252; + padding: 0 3px 0 3px; + background: #ccc; + border-radius: 4px; + line-height: inherit; + -moz-border-radius: 4px; + -khtml-border-radius: 4px; + -webkit-border-radius: 4px; +} +#user-groups .delete-icon:hover { + color: white; + background: #b32f2f; +} +.users-page .wmd-prompt-dialog { + background: #ccc; +} +.group-wiki .content > p:last-child { + margin-bottom: 5px; +} +.group-wiki .group-logo { + float: left; + margin: 0 5px 3px 0; +} +.group-wiki .follow-toggle.group-join-btn { + width: 150px; + margin: 4px auto 10px auto; + display: block; +} +.group-wiki .controls { + margin: 0 0 10px 0; +} +img.group-logo { + height: 60px; + /* important to align with the line spacing */ + +} +#groups-list { + margin-left: 0px; +} +#groups-list li { + display: inline; + list-style-type: none; + list-style-position: inside; + float: left; + text-align: center; +} +#groups-list .group-logo, +#groups-list .group-name { + display: block; +} +#reject-edit-modal input, +#reject-edit-modal textarea { + width: 514px; +} +input.tipped-input, +textarea.tipped-input { + padding-left: 5px; +} +.tipped-input.blank { + color: #707070; +} +.select-box { + margin: 0; +} +.select-box li { + list-style-type: none; + list-style-position: inside; + padding-left: 7px; + font-size: 14px; + line-height: 25px; +} +.select-box li.selected, +.select-box li.selected:hover { + background-color: #fcf8e3; + color: #c09853; +} +.select-box li:hover { + background-color: #cecece; + color: white; +} +/* fixes for bootstrap */ +.caret { + margin-bottom: 7px; +} +.btn-group { + text-align: left; +} +.btn-toolbar { + margin: 0; +} +.modal-footer { + text-align: left; +} +.modal p { + font-size: 14px; +} +.modal-body > textarea { + width: 515px; + margin-bottom: 0px; +} diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less index 69cdede0..f015ae0c 100644 --- a/askbot/skins/default/media/style/style.less +++ b/askbot/skins/default/media/style/style.less @@ -154,6 +154,7 @@ h1 { padding: 0px 0 5px 0px; } + /* ----- Extra space above for messages ----- */ body.user-messages { @@ -467,24 +468,15 @@ body.anon { #askButton{ /* check blocks/secondary_header.html and widgets/ask_button.html*/ - background: url(../images/bigbutton.png) repeat-x bottom; line-height:44px; - text-align:center; - width:200px; - height:42px; - font-size:23px; - color:@button-label; - margin-top:7px; + margin-top:6px; float:right; text-transform:uppercase; - .rounded-corners(5px); - .box-shadow(1px, 1px, 2px, #636363) + .button-style(200px, 42px, 23px); } #askButton:hover{ - text-decoration:none; - background: url(../images/bigbutton.png) repeat-x top; - .text-shadow(0px, 1px, 0px, #c6d9dd) + .button-style-hover; } /* ----- Content layout, check two_column_body.html or one_column_body.html ----- */ @@ -537,6 +529,7 @@ body.anon { padding-right:10px; margin-bottom:10px; font-family:@main-font; + width:190px; } h3{ color:#4a757f; @@ -606,26 +599,21 @@ body.anon { #ignoredTagAdd, #subscribedTagAdd, #ab-tag-search-add { - background:url(../images/small-button-blue.png) repeat-x top; border:0; - color:@button-label; font-weight:bold; - font-size:12px; - width:30px; - height:27px; margin-top:-2px; - cursor:pointer; + .button-style(30px, 27px, 14px); .rounded-corners(4px); - .text-shadow(0px,1px,0px,#E6F6FA); - .box-shadow(1px, 1px, 2px, #808080); + } + #interestingTagAdd:hover, + #ignoredTagAdd:hover, + #ab-tag-search-add:hover { + .button-style-hover; } #ab-tag-search-add { width: 47px; margin-left: 3px; } - #interestingTagAdd:hover, #ignoredTagAdd:hover, #subscribedTag:hover { - background:url(../images/small-button-blue.png) repeat-x bottom; - } } img.gravatar { @@ -635,28 +623,17 @@ body.anon { /* widgets for question template */ a.followed, a.follow{ - background: url(../images/medium-button.png) top repeat-x; - height:34px; line-height:34px; - text-align:center; border:0; - font-family:@main-font; - color:@button-label; font-weight:normal; - font-size:21px; margin-top:3px; - display:block; - width:120px; - text-decoration:none; - .rounded-corners(4px); - .box-shadow(1px, 1px, 2px, #636363); + .button-style(120px,34px,21px); .center; } a.followed:hover, a.follow:hover{ - text-decoration:none; - background: url(../images/medium-button.png) bottom repeat-x; + .button-style-hover; .text-shadow(0px, 1px, 0px, #c6d9dd); } @@ -1159,7 +1136,7 @@ ul#related-tags { ul.tags li { float:left; display: block; - margin: 0 8px 0 0; + margin: 0 8px 8px 0; padding: 0; height:20px; } @@ -1239,7 +1216,9 @@ ul#related-tags li { color: #1A1A1A; } -.users-page h1, .tags-page h1 { +.users-page h1, +.tags-page h1, +.groups-page h1 { float: left; } @@ -1377,24 +1356,16 @@ ul#related-tags li { .ask-page input.submit, .edit-question-page input.submit { float: left; - background: url(../images/medium-button.png) top repeat-x; - height:34px; - border:0; - font-family:@main-font; - color:@button-label; font-weight:normal; - font-size:21px; margin-top:3px; - .rounded-corners(4px); - .box-shadow(1px, 1px, 2px, #636363); + .button-style(160px,34px,21px); margin-right:7px; } #fmanswer input.submit:hover, .ask-page input.submit:hover, .edit-question-page input.submit:hover{ - text-decoration:none; - background: url(../images/medium-button.png) bottom repeat-x; + .button-style-hover; .text-shadow(0px, 1px, 0px, #c6d9dd) } @@ -1841,24 +1812,14 @@ ul#related-tags li { width: 100px; } button{ - background:url(../images/small-button-blue.png) repeat-x top; - border:0; - color:@button-label; - font-family:@body-font; - font-size:13px; - width:100px; - font-weight:bold; - height:27px; line-height:25px; margin-bottom:5px; - cursor:pointer; - .rounded-corners(4px); - .text-shadow(0px,1px,0px,#E6F6FA); - .box-shadow(1px, 1px, 2px, #808080); + .button-style(100px, 27px, 12px); + font-family:@body-font; + font-weight:bold; } button:hover{ - background: url(../images/small-button-blue.png) bottom repeat-x; - .text-shadow(0px, 1px, 0px, #c6d9dd); + .button-style-hover; } .counter { display: inline-block; @@ -2003,6 +1964,10 @@ ul#related-tags li { text-align: center; padding-top: 2px; margin:10px 10px 0px 3px; + /* smalls IE fixes */ + *margin:0; + *height:210px; + *width:30px; } .vote-buttons IMG { @@ -2199,23 +2164,13 @@ ul#related-tags li { font-size:14px; } input.submit{ - background:url(../images/small-button-blue.png) repeat-x top; - border:0; - color:@button-label; - font-weight:bold; - font-size:13px; - font-family:@body-font; - height:26px; + font-weight:normal; margin:5px 0px; - width:100px; - cursor:pointer; - .rounded-corners(4px); - .text-shadow(0px,1px,0px,#E6F6FA); - .box-shadow(1px, 1px, 2px, #808080); + .button-style(100px,26px,15px); + font-family:@body-font; } input.submit:hover{ - background:url(../images/small-button-blue.png) repeat-x bottom; - text-decoration:none; + .button-style-hover; } .cancel{ background:url(../images/small-button-cancel.png) repeat-x top !important; @@ -2242,25 +2197,19 @@ ul#related-tags li { width:200px; } .submit-b{ - background:url(../images/small-button-blue.png) repeat-x top; - border:0; - color:@button-label; - font-weight:bold; - font-size:13px; + .button-style(100px,24px,15px); font-family:@body-font; - height:24px; - margin-top:-2px; - padding-left:10px; + font-weight:bold; padding-right:10px; - cursor:pointer; - .rounded-corners(4px); - .text-shadow(0px,1px,0px,#E6F6FA); - .box-shadow(1px, 1px, 2px, #808080) + border:0; } + .submit-b:hover{ - background:url(../images/small-button-blue.png) repeat-x bottom; + .button-style-hover; } } + + .openid-input { background: url(../images/openid.gif) no-repeat; padding-left: 15px; @@ -2404,23 +2353,14 @@ a:hover.medal { .follow-toggle,.submit { border:0 !important; - color:@button-label; font-weight:bold; - font-size:12px; - height:26px; line-height:26px; margin-top:-2px; - font-size:15px; - cursor:pointer; - font-family:@main-font; - background:url(../images/small-button-blue.png) repeat-x top; - .rounded-corners(4px); - .text-shadow(0px,1px,0px,#E6F6FA); - .box-shadow(1px, 1px, 2px, #808080) + .button-style(100px,26px,14px); } .follow-toggle:hover, .submit:hover { - background:url(../images/small-button-blue.png) repeat-x bottom; + .button-style-hover; text-decoration:none !important; } @@ -3487,19 +3427,14 @@ img.group-logo { #groups-list { margin-left: 0px; - li { - display: inline; - list-style-type: none; - list-style-position: inside; - float: left; - text-align: center; + .group-name { + padding-right: 20px; } - .group-logo, .group-name { - display: block; + td { + padding-bottom: 5px; } } - #reject-edit-modal { input, textarea { width: 514px; diff --git a/askbot/skins/default/templates/badges.html b/askbot/skins/default/templates/badges.html index 668de4f8..ce76e76b 100644 --- a/askbot/skins/default/templates/badges.html +++ b/askbot/skins/default/templates/badges.html @@ -38,7 +38,7 @@ badges? Please, give us your <a href='{{feedback_faq_url}}'>feedback</a> <a style="cursor:default;" title="{% trans %}gold badge: the highest honor and is very rare{% endtrans %}" class="medal"><span class="badge1">●</span> {% trans %}gold{% endtrans %}</a> </p> <p> - {% trans %}Gold badge is the highest award in this community. To obtain it have to show + {% trans %}Gold badge is the highest award in this community. To obtain it you have to show profound knowledge and ability in addition to your active participation.{% endtrans %} </p> <p> @@ -48,7 +48,7 @@ profound knowledge and ability in addition to your active participation.{% endtr class="medal"><span class="badge2">●</span> {% trans %}silver{% endtrans %}</a> </p> <p> - {% trans %}msgid "silver badge: occasionally awarded for the very high quality contributions{% endtrans %} + {% trans %}silver badge: occasionally awarded for the very high quality contributions{% endtrans %} </p> <p> <a style="cursor:default;" title="{% trans %}bronze badge: often given as a special honor{% endtrans %}" class="medal"> diff --git a/askbot/skins/default/templates/feedback_email.txt b/askbot/skins/default/templates/email/feedback_email.txt index a729066a..a729066a 100644 --- a/askbot/skins/default/templates/feedback_email.txt +++ b/askbot/skins/default/templates/email/feedback_email.txt diff --git a/askbot/skins/default/templates/email/footer.html b/askbot/skins/default/templates/email/footer.html new file mode 100644 index 00000000..eda1269d --- /dev/null +++ b/askbot/skins/default/templates/email/footer.html @@ -0,0 +1 @@ +<p>{% trans %}Sincerely,<br>{{ site_name }} Administrator{% endtrans %}</p> diff --git a/askbot/skins/default/templates/email/re_welcome_lamson_on.html b/askbot/skins/default/templates/email/re_welcome_lamson_on.html new file mode 100644 index 00000000..412fede8 --- /dev/null +++ b/askbot/skins/default/templates/email/re_welcome_lamson_on.html @@ -0,0 +1,7 @@ +<p style="font-size:16px;font-weight:bold;"> + {% trans %}Great, you are ready to use {{ site_name }}!{% endtrans %} +</p> +<p>{% trans %}You can post questions by emailing them at {{ ask_address }}.{% endtrans %}</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> +{% include "email/footer.html" %} diff --git a/askbot/skins/default/templates/reply_by_email_error.html b/askbot/skins/default/templates/email/reply_by_email_error.html index 53648184..53648184 100644 --- a/askbot/skins/default/templates/reply_by_email_error.html +++ b/askbot/skins/default/templates/email/reply_by_email_error.html diff --git a/askbot/skins/default/templates/email/welcome_lamson_on.html b/askbot/skins/default/templates/email/welcome_lamson_on.html new file mode 100644 index 00000000..8bed898c --- /dev/null +++ b/askbot/skins/default/templates/email/welcome_lamson_on.html @@ -0,0 +1,11 @@ +{# site_name - is short name of the site, email_code - address portion +of the reply email used for this message, we scan to the last appearance +of the email code to detect the response signature that will appear under #} +<p style="font-size:16px;font-weight:bold;"> + {% trans %}Welcome to {{ site_name }}!{% endtrans %} +</p> +<p> + {% trans %}Please send a <em>blank reply</em> to this message. This helps us determine your email signature. Until we receive the response from you, you will not be able ask or answer questions on {{ site_name }} by email.{% endtrans %} +</p> +{% include "email/footer.html" %} +<p style="color:#aaa;font-size:8px">{{ email_code }}</p>{# important #} diff --git a/askbot/skins/default/templates/groups.html b/askbot/skins/default/templates/groups.html index eda0c3ff..2499ac9f 100644 --- a/askbot/skins/default/templates/groups.html +++ b/askbot/skins/default/templates/groups.html @@ -1,18 +1,48 @@ {% import "macros.html" as macros %} -{% extends 'one_column_body.html' %} +{% extends 'two_column_body.html' %} {% block title %}{% trans %}Groups{% endtrans %}{% endblock %} {% block content %} - <h1 class="section-title">{% trans %}Groups{% endtrans %}</h1> - {% if can_edit %} + <div id="content-header"> + <h1 class="section-title">{% trans %}Groups{% endtrans %}</h1> + {% if request.user.is_authenticated() %} + <div class="tabBar"> + <div class="tabsC"> + <a id="all-groups" class="first{% if tab_name=="all-groups" %} on{% endif %}" + title="{% trans %}All groups{% endtrans %}" + href="{% url groups %}?sort=all-groups" + ><span>{% trans %}all groups{% endtrans %}</span></a> + <a id="my-groups" {% if tab_name=="my-groups" %}class="on"{% endif %} + title="{% trans %}My groups{% endtrans %}" + href="{% url groups %}?sort=my-groups" + ><span>{% trans %}my groups{% endtrans %}</span></a> + </div> + </div> + {% endif %} + <div class="clearfix"></div> + </div> + {% if user_can_add_groups %} <p id="group-add-tip"> {% trans %}Tip: to create a new group - please go to some user profile and add the new group there. That user will be the first member of the group{% endtrans %} </p> {% endif %} - <ul id="groups-list"> + <table id="groups-list"> {% for group in groups %} - <li> - {{ macros.user_group(group) }} - </li> + <tr> + {{ macros.user_group(group, groups_membership_info[group.id]) }} + </tr> {% endfor %} - </ul> + </table> +{% endblock %} +{% block endjs %} + <script type='text/javascript' src='{{"/js/jquery.validate.min.js"|media}}'></script> + <script src='{{"/js/post.js"|media}}' type='text/javascript'></script> + {% if request.user.is_authenticated() %} + <script type="text/javascript"> + askbot['urls']['join_or_leave_group'] = '{% url join_or_leave_group %}'; + $.each($('.group-join-btn'), function(idx, elem){ + var group_join_btn = new GroupJoinButton(); + group_join_btn.decorate($(elem)); + }); + </script> + {% endif %} {% endblock %} diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html index 4bae1e45..6ba15373 100644 --- a/askbot/skins/default/templates/macros.html +++ b/askbot/skins/default/templates/macros.html @@ -9,7 +9,7 @@ {%- macro follow_toggle(follow, name, alias, id) -%} {# follow - boolean; name - object type name; alias - e.g. users name; id - object id #} <div - class="follow-toggle" + class="follow-toggle follow-user-toggle" id="follow-{{ name }}-{{ id }}" > {% if follow %} @@ -221,22 +221,50 @@ poor design of the data or methods on data objects #} </ul> {%- endmacro -%} -{%- macro user_group(group) -%} - {% if group.group_profile.logo_url %} - <a href="{% url users_by_group group.id, group.name|replace('-', ' ')|slugify %}"> - <img class="group-logo" src="{{group.group_profile.logo_url}}" alt='{% trans name=group.name|escape %}logo for user group "{{name}}"{% endtrans %}' /> - </a> - {% endif %} - <div class="group-name"> - <a +{%- macro user_group(group, membership_info) -%} + <td> + <a class="group-name" href="{% url users_by_group group.id, group.name|replace('-', ' ')|slugify %}" >{{ group.name|escape }}</a> - </div> - <!--div id="group-{{group.id}}-description"> + </td> + <td> + <span class="group-description"> {% if group.tag_wiki %} - {{ group.tag_wiki.html }} + {{ group.tag_wiki.summary }} + {% endif %} + </span> + </td> + <td> + {% if membership_info %} + {{ group_join_button( + group_id = group.id, + can_join = membership_info['can_join'], + is_member = membership_info['is_member'] + ) + }} + {% endif %} + </td> +{%- endmacro -%} + +{%- macro group_join_button(group_id = None, can_join = False, is_member = False) -%} + {% if can_join or is_member %} + <button + class="group-join-btn follow-toggle {% if is_member %}on on-state{% endif %}" + data-group-id="{{group_id}}" + data-off-prompt-text="{% trans %}Leave this group{% endtrans %}" + data-on-prompt-text="{% trans %}Join this group{% endtrans %}" + data-on-state-text="{% trans %}You are a member{% endtrans %}" + data-off-state-text="{% trans %}Join this group{% endtrans %}" + > + {% if is_member %} + {% trans %}You are a member{% endtrans %} + {% else %} + {% if can_join %} + {% trans %}Join this group{% endtrans %} + {% endif %} + {% endif %} + </button> {% endif %} - </div--> {%- endmacro -%} {# todo: remove the extra content argument to make its usage more explicit #} diff --git a/askbot/skins/default/templates/main_page/tab_bar.html b/askbot/skins/default/templates/main_page/tab_bar.html index 8b666155..17ab810e 100644 --- a/askbot/skins/default/templates/main_page/tab_bar.html +++ b/askbot/skins/default/templates/main_page/tab_bar.html @@ -3,9 +3,9 @@ {% cache 0 "scope_sort_tabs" search_tags request.user author_name scope sort query context.page language_code %} <a class="rss" {% if feed_url %} - href="{{settings.APP_URL}}{{feed_url}}" + href="{{feed_url}}" {% else %} - href="{{settings.APP_URL}}/feeds/rss/" + href="/feeds/rss/" {% endif %} title="{% trans %}subscribe to the questions feed{% endtrans %}" >{% trans %}RSS{% endtrans %} diff --git a/askbot/skins/default/templates/meta/fonts.html b/askbot/skins/default/templates/meta/fonts.html new file mode 100644 index 00000000..f55d567c --- /dev/null +++ b/askbot/skins/default/templates/meta/fonts.html @@ -0,0 +1,20 @@ +<style type="text/css"> +@font-face { + font-family: 'Yanone Kaffeesatz'; + font-style: normal; + font-weight: 400; + src: url('{{"/images/YanoneKaffeesatz-Regular.ttf"|media}}'); +} +@font-face { + font-family: 'Yanone Kaffeesatz'; + font-style: normal; + font-weight: 700; + src: url('{{"/images/YanoneKaffeesatz-Bold.ttf"|media}}'); +} +@font-face { + font-family: 'Yanone Kaffeesatz'; + font-style: normal; + font-weight: 300; + src: url('{{"/images/YanoneKaffeesatz-Light.ttf"|media}}'); +} +</style> diff --git a/askbot/skins/default/templates/meta/html_head_stylesheets.html b/askbot/skins/default/templates/meta/html_head_stylesheets.html index 14f3c106..0d2ba463 100644 --- a/askbot/skins/default/templates/meta/html_head_stylesheets.html +++ b/askbot/skins/default/templates/meta/html_head_stylesheets.html @@ -4,7 +4,11 @@ <link href="{{"/style/style.less"|media }}" rel="stylesheet/less" type="text/css" /> <script type="text/javascript" src="{{"/js/less.min.js"|media}}"></script> {% endif %} -<link href='http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700' rel='stylesheet' type='text/css'> +{% if settings.USE_LOCAL_FONTS %} + {% include "meta/fonts.html" %} +{% else %} + <link href='http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:300,400,700' rel='stylesheet' type='text/css'> +{% endif %} {{ skin.get_extra_css_link() }} {% if settings.USE_CUSTOM_CSS %} <link diff --git a/askbot/skins/default/templates/question/question_card.html b/askbot/skins/default/templates/question/question_card.html index 08f7ccee..dd52ea0f 100644 --- a/askbot/skins/default/templates/question/question_card.html +++ b/askbot/skins/default/templates/question/question_card.html @@ -29,4 +29,4 @@ </div> </div> -<div class="clean"></div> + diff --git a/askbot/skins/default/templates/question/subscribe_by_email_prompt.html b/askbot/skins/default/templates/question/subscribe_by_email_prompt.html index a9158143..6a77601c 100644 --- a/askbot/skins/default/templates/question/subscribe_by_email_prompt.html +++ b/askbot/skins/default/templates/question/subscribe_by_email_prompt.html @@ -8,7 +8,6 @@ {% else %} <p> {{ answer.email_notify }} - <label>{% trans %}once you sign in you will be able to subscribe for any updates here{% endtrans %}</label> <label>{% trans %}<span class='strong'>Here</span> (once you log in) you will be able to sign up for the periodic email updates about this question.{% endtrans %}</label> </p> {% endif %} diff --git a/askbot/skins/default/templates/user_profile/user.html b/askbot/skins/default/templates/user_profile/user.html index 15e0622a..bb293b9b 100644 --- a/askbot/skins/default/templates/user_profile/user.html +++ b/askbot/skins/default/templates/user_profile/user.html @@ -30,6 +30,8 @@ <script type='text/javascript' src='{{"/js/jquery.form.js"|media}}'></script> {% endif %} <script type="text/javascript" src='{{"/js/user.js"|media}}'></script> + <script type='text/javascript' src='{{"/js/jquery.validate.min.js"|media}}'></script> + <script type="text/javascript" src='{{"/js/post.js"|media}}'></script> {% block userjs %} {% endblock %} {% endblock %} diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html index 43b7f4fa..b125589c 100644 --- a/askbot/skins/default/templates/user_profile/user_stats.html +++ b/askbot/skins/default/templates/user_profile/user_stats.html @@ -12,15 +12,13 @@ username = view_user.username %}{{username}}'s groups{% endtrans %} </h2> - <ul id="groups-list"> - {% if user_groups %} + <table id="groups-list"> {% for group in user_groups %} - <li> - {{ macros.user_group(group) }} - </li> + <tr> + {{ macros.user_group(group, groups_membership_info[group.id]) }} + </tr> {% endfor %} - {% endif %} - </ul> + </table> <div class="clearfix"></div> <a id="add-group">{% trans %}add group{% endtrans %}</a> </div> @@ -156,8 +154,13 @@ {% block endjs %} {{ super() }} <script type="text/javascript"> + askbot['urls']['join_or_leave_group'] = '{% url join_or_leave_group %}'; $(document).ready(function(){ setup_badge_details_toggle(); + $.each($('.group-join-btn'), function(idx, elem){ + var group_join_btn = new GroupJoinButton(); + group_join_btn.decorate($(elem)); + }); }); </script> {% endblock %} diff --git a/askbot/skins/default/templates/users.html b/askbot/skins/default/templates/users.html index b7a16be1..a6346fc3 100644 --- a/askbot/skins/default/templates/users.html +++ b/askbot/skins/default/templates/users.html @@ -95,45 +95,45 @@ {% else %} var codeFriendlyMarkdown = false; {% endif %} - $().ready(function(){ - {% if group and request.user.is_authenticated() %} - var group_join_btn = new GroupJoinButton({{ group.id }}); + {% if group and request.user.is_authenticated() %} + $().ready(function(){ + var group_join_btn = new GroupJoinButton(); group_join_btn.decorate($('.group-join-btn')); - {% endif %} - //setup WMD editor - if (askbot['data']['userIsAdminOrMod'] === true){ - //todo: this is kind of Attacklab.init ... should not be here - Attacklab.wmd = function(){ - Attacklab.loadEnv = function(){ - var mergeEnv = function(env){ - if(!env){ - return; - } - - for(var key in env){ - Attacklab.wmd_env[key] = env[key]; - } + //setup WMD editor + if (askbot['data']['userIsAdminOrMod'] === true){ + //todo: this is kind of Attacklab.init ... should not be here + Attacklab.wmd = function(){ + Attacklab.loadEnv = function(){ + var mergeEnv = function(env){ + if(!env){ + return; + } + + for(var key in env){ + Attacklab.wmd_env[key] = env[key]; + } + }; + + mergeEnv(Attacklab.wmd_defaults); + mergeEnv(Attacklab.account_options); + mergeEnv(top["wmd_options"]); + Attacklab.full = true; + + var defaultButtons = "bold italic link blockquote code image ol ul heading hr"; + Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons; }; - - mergeEnv(Attacklab.wmd_defaults); - mergeEnv(Attacklab.account_options); - mergeEnv(top["wmd_options"]); - Attacklab.full = true; - - var defaultButtons = "bold italic link blockquote code image ol ul heading hr"; - Attacklab.wmd_env.buttons = Attacklab.wmd_env.buttons || defaultButtons; + Attacklab.loadEnv(); }; - Attacklab.loadEnv(); - }; - Attacklab.wmd(); - Attacklab.wmdBase(); - var group_editor = new UserGroupProfileEditor(); - group_editor.decorate($('#group-wiki-{{group.id}}')); - } - Hilite.exact = false; - Hilite.elementid = "main-body"; - Hilite.debug_referrer = location.href; - }); + Attacklab.wmd(); + Attacklab.wmdBase(); + var group_editor = new UserGroupProfileEditor(); + group_editor.decorate($('#group-wiki-{{group.id}}')); + } + Hilite.exact = false; + Hilite.elementid = "main-body"; + Hilite.debug_referrer = location.href; + }); + {% endif %} </script> {% endblock %} <!-- end users.html --> diff --git a/askbot/skins/default/templates/widgets/group_info.html b/askbot/skins/default/templates/widgets/group_info.html index 601930af..5d3a4c7f 100644 --- a/askbot/skins/default/templates/widgets/group_info.html +++ b/askbot/skins/default/templates/widgets/group_info.html @@ -1,3 +1,4 @@ +{% import "macros.html" as macros %} <div id="group-wiki-{{group.id}}" class="box group-wiki"> <h2>{% trans %}Group info{% endtrans %}</h2> <img class="group-logo" @@ -13,23 +14,12 @@ {% endif %} </div> <div class="clearfix"></div> - {% if user_can_join_group or user_is_group_member %} - <button - class="group-join-btn follow-toggle {% if user_is_group_member %}on on-state{% endif %}" - data-off-prompt-text="{% trans %}Leave this group{% endtrans %}" - data-on-prompt-text="{% trans %}Join this group{% endtrans %}" - data-on-state-text="{% trans %}You are a member{% endtrans %}" - data-off-state-text="{% trans %}Join this group{% endtrans %}" - > - {% if user_is_group_member %} - {% trans %}You are a member{% endtrans %} - {% else %} - {% if user_can_join_group %} - {% trans %}Join this group{% endtrans %} - {% endif %} - {% endif %} - </button> - {% endif %} + {{ macros.group_join_button( + group_id = group.id, + can_join = user_can_join_group, + is_member = user_is_group_member + ) + }} {% if request.user.is_authenticated() and request.user.is_administrator() %} <div class="controls"> <a class="edit-description" diff --git a/askbot/skins/utils.py b/askbot/skins/utils.py index 9f67d5c9..0c0dba9c 100644 --- a/askbot/skins/utils.py +++ b/askbot/skins/utils.py @@ -3,7 +3,7 @@ the lookup resolution process for templates and media works as follows: * look up item in selected skin * if not found look in 'default' -* raise an exception +* raise an exception """ import os import logging @@ -56,7 +56,7 @@ def get_available_skins(selected=None): #re-insert default as a last item skins['default'] = default_dir - skins['common'] = common_dir + skins['common'] = common_dir return skins @@ -71,7 +71,7 @@ def get_path_to_skin(skin): return skin_dirs.get(skin, None) def get_skin_choices(): - """returns a tuple for use as a set of + """returns a tuple for use as a set of choices in the form""" available_skins = get_available_skins().keys() available_skins.remove('common') @@ -88,7 +88,7 @@ def resolve_skin_for_media(media=None, preferred_skin = None): def get_media_url(url, ignore_missing = False): """returns url prefixed with the skin name - of the first skin that contains the file + of the first skin that contains the file directories are searched in this order: askbot_settings.ASKBOT_DEFAULT_SKIN, then 'default', then 'commmon' if file is not found - returns None @@ -158,7 +158,7 @@ def get_media_url(url, ignore_missing = False): url = django_settings.STATIC_URL + use_skin + '/media/' + url url = os.path.normpath(url).replace('\\', '/') - + if resource_revision: url += '?v=%d' % resource_revision @@ -176,7 +176,7 @@ def update_media_revision(skin = None): if skin in get_skin_choices(): skin_path = get_path_to_skin(skin) else: - raise MediaNotFound('Skin %s not found' % skin) + raise MediaNotFound('Skin %s not found' % skin) else: skin = 'default' skin_path = get_path_to_skin(askbot_settings.ASKBOT_DEFAULT_SKIN) @@ -195,6 +195,5 @@ def update_media_revision(skin = None): if current_hash != askbot_settings.MEDIA_RESOURCE_REVISION_HASH: askbot_settings.update('MEDIA_RESOURCE_REVISION', resource_revision + 1) - askbot_settings.update('MEDIA_RESOURCE_REVISION_HASH', current_hash) + askbot_settings.update('MEDIA_RESOURCE_REVISION_HASH', current_hash) logging.debug('MEDIA_RESOURCE_REVISION changed') - askbot_settings.MEDIA_RESOURCE_REVISION diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py index 9678fdf1..a5b0c940 100644 --- a/askbot/startup_procedures.py +++ b/askbot/startup_procedures.py @@ -11,7 +11,8 @@ import sys import os import re import askbot -from django.db import transaction +import south +from django.db import transaction, connection from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured from askbot.utils.loading import load_module @@ -166,7 +167,7 @@ def try_import(module_name, pypi_package_name, short_message = False): try: load_module(module_name) except ImportError, error: - message = 'Error: ' + unicode(error) + message = 'Error: ' + unicode(error) message += '\n\nPlease run: >pip install %s' % pypi_package_name if short_message == False: message += '\n\nTo install all the dependencies at once, type:' @@ -306,10 +307,10 @@ class SettingsTester(object): ) if len(self.messages) != 0: raise AskbotConfigError( - '\n\nTime to do some maintenance of your settings.py:\n\n* ' + + '\n\nTime to do some maintenance of your settings.py:\n\n* ' + '\n\n* '.join(self.messages) ) - + def test_staticfiles(): """tests configuration of the staticfiles app""" errors = list() @@ -350,7 +351,7 @@ def test_staticfiles(): if static_url is None or str(static_url).strip() == '': errors.append( 'Add STATIC_URL setting to your settings.py file. ' - 'The setting must be a url at which static files ' + 'The setting must be a url at which static files ' 'are accessible.' ) url = urlparse(static_url).path @@ -407,7 +408,7 @@ def test_staticfiles(): ' python manage.py collectstatic\n' ) - + print_errors(errors) if django_settings.STATICFILES_STORAGE == \ 'django.contrib.staticfiles.storage.StaticFilesStorage': @@ -479,8 +480,6 @@ def test_avatar(): '-e git+git://github.com/ericflo/django-avatar.git#egg=avatar', short_message = True ) - - def run_startup_tests(): """function that runs diff --git a/askbot/tests/reply_by_email_tests.py b/askbot/tests/reply_by_email_tests.py index a1272b0d..e46d6b3d 100644 --- a/askbot/tests/reply_by_email_tests.py +++ b/askbot/tests/reply_by_email_tests.py @@ -1,6 +1,6 @@ from django.utils.translation import ugettext as _ from askbot.models import ReplyAddress -from askbot.lamson_handlers import PROCESS +from askbot.lamson_handlers import PROCESS, get_parts from askbot import const @@ -21,7 +21,7 @@ class MockPart(object): self.body = body self.content_encoding = {'Content-Type':('text/plain',)} -class MockMessage(object): +class MockMessage(dict): def __init__(self, body, from_email): self._body = body @@ -61,7 +61,10 @@ class EmailProcessingTests(AskbotTestCase): self.comment = self.post_comment(user = self.u2, parent_post = self.answer) def test_process_correct_answer_comment(self): - addr = ReplyAddress.objects.create_new( self.answer, self.u1).address + addr = ReplyAddress.objects.create_new( + post = self.answer, + user = self.u1 + ).address reply_separator = const.REPLY_SEPARATOR_TEMPLATE % { 'user_action': 'john did something', 'instruction': 'reply above this line' @@ -71,7 +74,8 @@ class EmailProcessingTests(AskbotTestCase): "wrote something \n\n%s\nlorem ipsum " % (reply_separator), "user1@domain.com" ) - PROCESS(msg, addr, '') + msg['Subject'] = 'test subject' + PROCESS(msg, address = addr) self.assertEquals(self.answer.comments.count(), 2) self.assertEquals(self.answer.comments.all().order_by('-pk')[0].text.strip(), "This is a test reply") @@ -101,20 +105,29 @@ class ReplyAddressModelTests(AskbotTestCase): def test_address_creation(self): self.assertEquals(ReplyAddress.objects.all().count(), 0) - result = ReplyAddress.objects.create_new( self.answer, self.u1) + result = ReplyAddress.objects.create_new( + post = self.answer, + user = self.u1 + ) self.assertTrue(len(result.address) >= 12 and len(result.address) <= 25) self.assertEquals(ReplyAddress.objects.all().count(), 1) def test_create_answer_reply(self): - result = ReplyAddress.objects.create_new( self.answer, self.u1) + result = ReplyAddress.objects.create_new( + post = self.answer, + user = self.u1 + ) post = result.create_reply(TEST_EMAIL_PARTS) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) self.assertEquals(self.answer.comments.count(), 2) def test_create_comment_reply(self): - result = ReplyAddress.objects.create_new( self.comment, self.u1) + result = ReplyAddress.objects.create_new( + post = self.comment, + user = self.u1 + ) post = result.create_reply(TEST_EMAIL_PARTS) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) @@ -122,13 +135,19 @@ class ReplyAddressModelTests(AskbotTestCase): def test_create_question_comment_reply(self): - result = ReplyAddress.objects.create_new( self.question, self.u3) + result = ReplyAddress.objects.create_new( + post = self.question, + user = self.u3 + ) post = result.create_reply(TEST_EMAIL_PARTS) self.assertEquals(post.post_type, "comment") self.assertEquals(post.text, TEST_CONTENT) def test_create_question_answer_reply(self): - result = ReplyAddress.objects.create_new( self.question, self.u3) + result = ReplyAddress.objects.create_new( + post = self.question, + user = self.u3 + ) post = result.create_reply(TEST_LONG_EMAIL_PARTS) self.assertEquals(post.post_type, "answer") self.assertEquals(post.text, TEST_LONG_CONTENT) diff --git a/askbot/utils/mail.py b/askbot/utils/mail.py index aa4df320..4f653e6b 100644 --- a/askbot/utils/mail.py +++ b/askbot/utils/mail.py @@ -144,21 +144,29 @@ def mail_moderators( if raise_on_failure == True: raise exceptions.EmailNotSent(unicode(error)) -ASK_BY_EMAIL_USAGE = _( -"""<p>To ask by email, please:</p> -<ul> - <li>Format the subject line as: [Tag1; Tag2] Question title</li> - <li>Type details of your question into the email body</li> -</ul> -<p>Note that tags may consist of more than one word, and tags -may be separated by a semicolon or a comma</p> -""" +INSTRUCTIONS_PREAMBLE = _('<p>To ask by email, please:</p>') +QUESTION_TITLE_INSTRUCTION = _( + '<li>Type title in the subject line</li>' +) +QUESTION_DETAILS_INSTRUCTION = _( + '<li>Type details of your question into the email body</li>' +) +OPTIONAL_TAGS_INSTRUCTION = _( +"""<li>The beginning of the subject line can contain tags, +<em>enclosed in the square brackets</em> like so: [Tag1; Tag2]</li>""" +) +REQUIRED_TAGS_INSTRUCTION = _( +"""<li>In the beginning of the subject add at least one tag +<em>enclosed in the brackets</em> like so: [Tag1; Tag2].</li>""" +) +TAGS_INSTRUCTION_FOOTNOTE = _( +"""<p>Note that a tag may consist of more than one word, to separate +the tags, use a semicolon or a comma, for example, [One tag; Other tag]</p>""" ) def bounce_email(email, subject, reason = None, body_text = None): """sends a bounce email at address ``email``, with the subject line ``subject``, accepts several reasons for the bounce: - * ``'problem_posting'``, ``unknown_user`` and ``permission_denied`` * ``body_text`` in an optional parameter that allows to append extra text to the message @@ -168,7 +176,28 @@ def bounce_email(email, subject, reason = None, body_text = None): '<p>Sorry, there was an error posting your question ' 'please contact the %(site)s administrator</p>' ) % {'site': askbot_settings.APP_SHORT_NAME} - error_message = string_concat(error_message, ASK_BY_EMAIL_USAGE) + + if askbot_settings.TAGS_ARE_REQUIRED: + error_message = string_concat( + INSTRUCTIONS_PREAMBLE, + '<ul>', + QUESTION_TITLE_INSTRUCTION, + REQUIRED_TAGS_INSTRUCTION, + QUESTION_DETAILS_INSTRUCTION, + '</ul>', + TAGS_INSTRUCTION_FOOTNOTE + ) + else: + error_message = string_concat( + INSTRUCTIONS_PREAMBLE, + '<ul>', + QUESTION_TITLE_INSTRUCTION, + QUESTION_DETAILS_INSTRUCTION, + OPTIONAL_TAGS_INSTRUCTION, + '</ul>', + TAGS_INSTRUCTION_FOOTNOTE + ) + elif reason == 'unknown_user': error_message = _( '<p>Sorry, in order to post questions on %(site)s ' @@ -271,6 +300,10 @@ def process_emailed_question(from_address, subject, parts, tags = None): user = User.objects.get( email__iexact = email_address ) + + if user.email_isvalid == False: + raise PermissionDenied('Lacking email signature') + tagnames = form.cleaned_data['tagnames'] title = form.cleaned_data['title'] body_text = form.cleaned_data['body_text'] @@ -279,10 +312,15 @@ def process_emailed_question(from_address, subject, parts, tags = None): if tags: tagnames += ' ' + ' '.join(tags) + stripped_body_text = user.strip_email_signature(body_text) + if stripped_body_text == body_text and user.email_signature: + #todo: send an email asking to update the signature + raise ValueError('email signature changed') + user.post_question( title = title, tags = tagnames.strip(), - body_text = body_text, + body_text = stripped_body_text, by_email = True, email_address = from_address ) diff --git a/askbot/views/meta.py b/askbot/views/meta.py index 3e5a0b35..5f4c70ac 100644 --- a/askbot/views/meta.py +++ b/askbot/views/meta.py @@ -87,7 +87,7 @@ def feedback(request): data['email'] = form.cleaned_data.get('email',None) data['message'] = form.cleaned_data['message'] data['name'] = form.cleaned_data.get('name',None) - template = get_template('feedback_email.txt', request) + template = get_template('email/feedback_email.txt', request) message = template.render(RequestContext(request, data)) mail_moderators(_('Q&A forum feedback'), message) msg = _('Thanks for the feedback!') diff --git a/askbot/views/readers.py b/askbot/views/readers.py index 3259cddd..8f19a802 100644 --- a/askbot/views/readers.py +++ b/askbot/views/readers.py @@ -68,6 +68,7 @@ def questions(request, **kwargs): List of Questions, Tagged questions, and Unanswered questions. matching search query or user selection """ + #before = datetime.datetime.now() if request.method != 'GET': return HttpResponseNotAllowed(['GET']) @@ -311,7 +312,7 @@ def question(request, id):#refactor - long subroutine. display question body, an """ #process url parameters #todo: fix inheritance of sort method from questions - before = datetime.datetime.now() + #before = datetime.datetime.now() default_sort_method = request.session.get('questions_sort_method', 'votes') form = ShowQuestionForm(request.GET, default_sort_method) form.full_clean()#always valid @@ -551,9 +552,7 @@ def question(request, id):#refactor - long subroutine. display question body, an 'show_comment_position': show_comment_position, } - result = render_into_skin('question.html', data, request) - #print datetime.datetime.now() - before - return result + return render_into_skin('question.html', data, request) def revisions(request, id, post_type = None): assert post_type in ('question', 'answer') diff --git a/askbot/views/users.py b/askbot/views/users.py index 10f643f5..a5860b14 100644 --- a/askbot/views/users.py +++ b/askbot/views/users.py @@ -13,7 +13,7 @@ import datetime import logging import operator -from django.db.models import Count, Q +from django.db.models import Count from django.conf import settings as django_settings from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator, EmptyPage, InvalidPage @@ -421,6 +421,13 @@ def user_stats(request, user, context): badges = badges_dict.items() badges.sort(key=operator.itemgetter(1), reverse=True) + user_groups = models.Tag.group_tags.get_for_user(user = user) + + if request.user == user: + groups_membership_info = user.get_groups_membership_info(user_groups) + else: + groups_membership_info = collections.defaultdict() + data = { 'active_tab':'users', 'page_class': 'user-profile-page', @@ -442,7 +449,8 @@ def user_stats(request, user, context): 'votes_total_per_day': votes_total, 'user_tags' : user_tags, - 'user_groups': models.Tag.group_tags.get_for_user(user = user), + 'user_groups': user_groups, + 'groups_membership_info': groups_membership_info, 'badges': badges, 'total_badges' : len(badges), } @@ -688,7 +696,7 @@ def user_responses(request, user, context): #6) sort responses by time filtered_response_list.sort(lambda x,y: cmp(y['timestamp'], x['timestamp'])) - reject_reasons = models.PostFlagReason.objects.all().order_by('title'); + reject_reasons = models.PostFlagReason.objects.all().order_by('title') data = { 'active_tab':'users', 'page_class': 'user-profile-page', @@ -914,12 +922,38 @@ def groups(request, id = None, slug = None): """ if askbot_settings.GROUPS_ENABLED == False: raise Http404 - groups = models.Tag.group_tags.get_all() - can_edit = request.user.is_authenticated() and \ + + #6 lines of input cleaning code + if request.user.is_authenticated(): + scope = request.GET.get('sort', 'all-groups') + if scope not in ('all-groups', 'my-groups'): + scope = 'all-groups' + else: + scope = 'all-groups' + + if scope == 'all-groups': + groups = models.Tag.group_tags.get_all() + else: + groups = models.Tag.group_tags.get_for_user( + user = request.user + ) + + groups = groups.select_related('group_profile') + + user_can_add_groups = request.user.is_authenticated() and \ request.user.is_administrator_or_moderator() + + groups_membership_info = collections.defaultdict() + if request.user.is_authenticated(): + #collect group memberhship information + groups_membership_info = request.user.get_groups_membership_info(groups) + data = { 'groups': groups, - 'can_edit': can_edit, - 'active_tab': 'users' + 'groups_membership_info': groups_membership_info, + 'user_can_add_groups': user_can_add_groups, + 'active_tab': 'groups',#todo vars active_tab and tab_name are too similar + 'tab_name': scope, + 'page_class': 'groups-page' } return render_into_skin('groups.html', data, request) |