diff options
34 files changed, 394 insertions, 129 deletions
@@ -5,6 +5,7 @@ db rebuild-locales.pl /src +/static cache/?? run *.wsgi @@ -15,6 +16,7 @@ settings.py *.iml lint env +/static django django/* nbproject @@ -42,4 +44,4 @@ askbot/skins/common/media/mathjax/ run recaptcha /.ve -/db.sq3
\ No newline at end of file +/db.sq3 diff --git a/askbot/__init__.py b/askbot/__init__.py index 7b12329c..2989d660 100644 --- a/askbot/__init__.py +++ b/askbot/__init__.py @@ -9,7 +9,7 @@ import smtplib import sys import logging -VERSION = (0, 7, 37) +VERSION = (0, 7, 39) #keys are module names used by python imports, #values - the package qualifier to use for pip diff --git a/askbot/context.py b/askbot/context.py index 17ab35bd..41298fc1 100644 --- a/askbot/context.py +++ b/askbot/context.py @@ -2,6 +2,7 @@ from the django settings, all parameters from the askbot livesettings and the application available for the templates """ +import sys from django.conf import settings import askbot from askbot import api @@ -15,8 +16,10 @@ def application_settings(request): my_settings = askbot_settings.as_dict() 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['DEBUG'] = settings.DEBUG + my_settings['USING_RUNSERVER'] = 'runserver' in sys.argv my_settings['ASKBOT_VERSION'] = askbot.get_version() my_settings['LOGIN_URL'] = url_utils.get_login_url() my_settings['LOGOUT_URL'] = url_utils.get_logout_url() diff --git a/askbot/deployment/__init__.py b/askbot/deployment/__init__.py index 6f7a86f6..8832cd01 100644 --- a/askbot/deployment/__init__.py +++ b/askbot/deployment/__init__.py @@ -3,6 +3,7 @@ module for deploying askbot """ import os.path import sys +import django from optparse import OptionParser from askbot.utils import console from askbot.deployment import messages @@ -126,6 +127,12 @@ def deploy_askbot(directory, options): path_utils.create_path(directory) + if django.VERSION[0] == 1 and django.VERSION[1] < 3: + #force people install the django-staticfiles app + context['staticfiles_app'] = '' + else: + context['staticfiles_app'] = "'django.contrib.staticfiles'," + path_utils.deploy_into( directory, new_project = create_new_project, diff --git a/askbot/deps/django_authopenid/util.py b/askbot/deps/django_authopenid/util.py index 4468a6d2..28f6b2dd 100644 --- a/askbot/deps/django_authopenid/util.py +++ b/askbot/deps/django_authopenid/util.py @@ -29,7 +29,7 @@ try: except: from yadis import xri -import time, base64, hashlib, operator, logging +import time, base64, hmac, hashlib, operator, logging from models import Association, Nonce __all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next'] @@ -787,30 +787,54 @@ class FacebookError(Exception): """ pass -def get_facebook_user_id(request): - try: - key = askbot_settings.FACEBOOK_KEY - secret = askbot_settings.FACEBOOK_SECRET +def urlsafe_b64decode(input): + length = len(input) + return base64.urlsafe_b64decode( + input.ljust(length + length % 4, '=') + ) - fb_cookie = request.COOKIES['fbs_%s' % key] - fb_response = dict(cgi.parse_qsl(fb_cookie)) +def parse_signed_facebook_request(signed_request, secret): + """ + Parse signed_request given by Facebook (usually via POST), + decrypt with app secret. - signature = None - payload = '' - for key in sorted(fb_response.keys()): - if key != 'sig': - payload += '%s=%s' % (key, fb_response[key]) + Arguments: + signed_request -- Facebook's signed request given through POST + secret -- Application's app_secret required to decrpyt signed_request - if 'sig' in fb_response: - if md5(payload + secret).hexdigest() != fb_response['sig']: - raise ValueError('signature does not match') - else: - raise ValueError('no signature in facebook response') + slightly edited copy from https://gist.github.com/1190267 + """ + + if "." in signed_request: + esig, payload = signed_request.split(".") + else: + return {} - if 'uid' not in fb_response: - raise ValueError('no user id in facebook response') + sig = urlsafe_b64decode(str(esig)) + data = simplejson.loads(urlsafe_b64decode(str(payload))) - return fb_response['uid'] + if not isinstance(data, dict): + raise ValueError("Pyload is not a json string!") + return {} + + if data["algorithm"].upper() == "HMAC-SHA256": + if hmac.new(str(secret), str(payload), hashlib.sha256).digest() == sig: + return data + else: + raise ValueError("Not HMAC-SHA256 encrypted!") + + return {} + +def get_facebook_user_id(request): + try: + key = askbot_settings.FACEBOOK_KEY + fb_cookie = request.COOKIES['fbsr_%s' % key] + if not fb_cookie: + raise ValueError('cannot access facebook cookie') + + secret = askbot_settings.FACEBOOK_SECRET + response = parse_signed_facebook_request(fb_cookie, secret) + return response['user_id'] except Exception, e: raise FacebookError(e) diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py index 8cb30365..bb0b4986 100644 --- a/askbot/deps/django_authopenid/views.py +++ b/askbot/deps/django_authopenid/views.py @@ -48,6 +48,7 @@ from django.utils.safestring import mark_safe from django.core.mail import send_mail from recaptcha_works.decorators import fix_recaptcha_remote_ip from askbot.skins.loaders import render_into_skin, get_template +from urlparse import urlparse from openid.consumer.consumer import Consumer, \ SUCCESS, CANCEL, FAILURE, SETUP_NEEDED @@ -1084,8 +1085,10 @@ def _send_email_key(user): to user's email address """ subject = _("Recover your %(site)s account") % {'site': askbot_settings.APP_SHORT_NAME} + + url = urlparse(askbot_settings.APP_URL) data = { - 'validation_link': askbot_settings.APP_URL + \ + 'validation_link': url.scheme + '://' + url.netloc + \ reverse( 'user_account_recover', kwargs={'key':user.email_key} diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst index ce18fe11..b4b810d6 100644 --- a/askbot/doc/source/changelog.rst +++ b/askbot/doc/source/changelog.rst @@ -1,6 +1,20 @@ Changes in Askbot ================= +Development version (not released yet) +-------------------------------------- +* Made email recovery link work when askbot is deployed on subdirectory (Evgeny) +* Added tests for the CSRF_COOKIE_DOMAIN setting in the startup_procedures (Evgeny) +* Askbot now respects django's staticfiles app (Radim Řehůřek, Evgeny) + +0.7.39 (Jan 11, 2012) +--------------------- +* restored facebook login after FB changed the procedure (Evgeny) + +0.7.38 (Jan 11, 2012) +--------------------- +* xss vulnerability fix, issue found by Radim Řehůřek (Evgeny) + 0.7.37 (Jan 8, 2012) -------------------- * added basic slugification treatment to question titles with diff --git a/askbot/doc/source/deployment.rst b/askbot/doc/source/deployment.rst index 8baa99c0..1ca7553f 100644 --- a/askbot/doc/source/deployment.rst +++ b/askbot/doc/source/deployment.rst @@ -6,18 +6,30 @@ Deploying Askbot Deploying askbot (assuming that it is already installed) entails: +* collecting static media files * setting correct file access permissions * configuring the webserver to work with your application This document currently explains the configuration under Apache and mod_wsgi_. +Collecting static media files +----------------------------- +Static media must be collected into a single location with a command:: + + python manage.py collectstatic + +There are several options on where to put the static files - the simplest is +a local directory, but it is also possible to use a dedicated static files +storage or a CDN, for more information see django documentation about +serving static files. + Setting up file access permissions ---------------------------------- Webserver process must be able to write to the following locations within your project:: - log/ - askbot/upfiles + log/ + askbot/upfiles If you know user name or the group name under which the webserver runs, you can make those directories writable by setting the permissons @@ -26,11 +38,11 @@ accordingly: For example, if you are using Linux installation of apache webserver running under group name 'apache' you could do the following:: - cd /path/to/django-project - cd .. #go one level up - chown -R yourlogin:apache django-project - chmod -R g+w django-project/askbot/upfiles - chmod -R g+w django-project/log + cd /path/to/django-project + cd .. #go one level up + chown -R yourlogin:apache django-project + chmod -R g+w django-project/askbot/upfiles + chmod -R g+w django-project/log If your account somehow limits you from running such commands - please consult your system administrator. @@ -71,9 +83,8 @@ Settings below are not perfect but may be a good starting point:: #aliases to serve static media directly #will probably need adjustment - Alias /m/ /usr/local/lib/python2.6/site-packages/askbot/skins/ + Alias /static/ /path/to/django-project/static/ Alias /upfiles/ /path/to/django-project/askbot/upfiles/ - Alias /admin/media/ /usr/local/lib/python2.6/site-packages/django/contrib/admin/media/ <DirectoryMatch "/path/to/django-project/askbot/skins/([^/]+)/media"> Order deny,allow Allow from all diff --git a/askbot/management/commands/send_email_alerts.py b/askbot/management/commands/send_email_alerts.py index d7966e83..c17c678e 100644 --- a/askbot/management/commands/send_email_alerts.py +++ b/askbot/management/commands/send_email_alerts.py @@ -453,33 +453,6 @@ class Command(NoArgsCommand): # 'the askbot and see what\'s new!<br>' # ) - text += _( - 'Please visit the askbot and see what\'s new! ' - 'Could you spread the word about it - ' - 'can somebody you know help answering those questions or ' - 'benefit from posting one?' - ) - - feeds = EmailFeedSetting.objects.filter(subscriber = user) - feed_freq = [feed.frequency for feed in feeds] - text += '<p></p>' - if 'd' in feed_freq: - text += _('Your most frequent subscription setting is \'daily\' ' - 'on selected questions. If you are receiving more than one ' - 'email per day' - 'please tell about this issue to the askbot administrator.' - ) - elif 'w' in feed_freq: - text += _('Your most frequent subscription setting is \'weekly\' ' - 'if you are receiving this email more than once a week ' - 'please report this issue to the askbot administrator.' - ) - text += ' ' - text += _( - 'There is a chance that you may be receiving links seen ' - 'before - due to a technicality that will eventually go away. ' - ) - link = url_prefix + reverse( 'user_subscriptions', kwargs = { diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py index ed51d2c4..6d263816 100644 --- a/askbot/setup_templates/settings.py +++ b/askbot/setup_templates/settings.py @@ -3,11 +3,13 @@ import os.path import logging import sys import askbot +import site #this line is added so that we can import pre-packaged askbot dependencies -sys.path.append(os.path.join(os.path.dirname(askbot.__file__), 'deps')) +ASKBOT_ROOT = os.path.abspath(os.path.dirname(askbot.__file__)) +site.addsitedir(os.path.join(ASKBOT_ROOT, 'deps')) -DEBUG = False#set to True to enable debugging +DEBUG = True#set to True to enable debugging TEMPLATE_DEBUG = False#keep false when debugging jinja2 templates INTERNAL_IPS = ('127.0.0.1',) @@ -69,14 +71,16 @@ LANGUAGE_CODE = 'en' # Absolute path to the directory that holds media. # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') -MEDIA_URL = '/upfiles/' +MEDIA_URL = '/upfiles/'#url to uploaded media +STATIC_URL = '/m/'#url to project static files PROJECT_ROOT = os.path.dirname(__file__) +STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')#path to files collected by collectstatic # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/admin/media/' +ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/'#must be this value # Make up some unique string, and don't share it with anybody. SECRET_KEY = 'sdljdfjkldsflsdjkhsjkldgjlsdgfs s ' @@ -149,6 +153,7 @@ INSTALLED_APPS = ( 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', + 'django.contrib.staticfiles', #all of these are needed for the askbot 'django.contrib.admin', @@ -218,3 +223,5 @@ djcelery.setup_loader() CSRF_COOKIE_NAME = 'askbot_csrf' CSRF_COOKIE_DOMAIN = ''#enter domain name here - e.g. example.com + +STATICFILES_DIRS = ( os.path.join(ASKBOT_ROOT, 'skins'),) diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache index 0575266c..19fd3c61 100644 --- a/askbot/setup_templates/settings.py.mustache +++ b/askbot/setup_templates/settings.py.mustache @@ -3,11 +3,13 @@ import os.path import logging import sys import askbot +import site #this line is added so that we can import pre-packaged askbot dependencies -sys.path.append(os.path.join(os.path.dirname(askbot.__file__), 'deps')) +ASKBOT_ROOT = os.path.abspath(os.path.dirname(askbot.__file__)) +site.addsitedir(os.path.join(ASKBOT_ROOT, 'deps')) -DEBUG = False#set to True to enable debugging +DEBUG = True#set to True to enable debugging TEMPLATE_DEBUG = False#keep false when debugging jinja2 templates INTERNAL_IPS = ('127.0.0.1',) @@ -66,17 +68,19 @@ SITE_ID = 1 USE_I18N = True LANGUAGE_CODE = 'en' -# Absolute path to the directory that holds media. +# Absolute path to the directory that holds uploaded media # Example: "/home/media/media.lawrence.com/" MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'askbot', 'upfiles') MEDIA_URL = '/upfiles/' +STATIC_URL = '/m/'#this must be different from MEDIA_URL PROJECT_ROOT = os.path.dirname(__file__) +STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static') # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a # trailing slash. # Examples: "http://foo.com/media/", "/media/". -ADMIN_MEDIA_PREFIX = '/admin/media/' +ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/' # Make up some unique string, and don't share it with anybody. SECRET_KEY = 'sdljdfjkldsflsdjkhsjkldgjlsdgfs s ' @@ -148,6 +152,7 @@ INSTALLED_APPS = ( 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', + {{ staticfiles_app }} #all of these are needed for the askbot 'django.contrib.admin', @@ -218,3 +223,6 @@ DOMAIN_NAME = '{{domain_name}}' CSRF_COOKIE_NAME = '{{domain_name}}_csrf' CSRF_COOKIE_DOMAIN = DOMAIN_NAME + +STATIC_ROOT = os.path.join(PROJECT_ROOT, "static") +STATICFILES_DIRS = (os.path.join(ASKBOT_ROOT, 'skins'),) diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js index 0b9957a0..9e02b5d4 100644 --- a/askbot/skins/common/media/js/utils.js +++ b/askbot/skins/common/media/js/utils.js @@ -1,6 +1,6 @@ //var $, scriptUrl, askbotSkin var mediaUrl = function(resource){ - return scriptUrl + 'm/' + askbotSkin + '/' + resource; + return askbot['settings']['static_url'] + askbotSkin + '/' + resource; }; var cleanUrl = function(url){ diff --git a/askbot/skins/common/templates/authopenid/signin.html b/askbot/skins/common/templates/authopenid/signin.html index 305574f0..7fdbe203 100644 --- a/askbot/skins/common/templates/authopenid/signin.html +++ b/askbot/skins/common/templates/authopenid/signin.html @@ -11,14 +11,14 @@ {% endif %}
{% if answer %}
<div class="message">
- {% trans title=answer.question.thread.title, summary=answer.summary %}
+ {% trans title=answer.question.title|escape, summary=answer.summary|escape %}
Your answer to {{title}} {{summary}} will be posted once you log in
{% endtrans %}
</div>
{% endif %}
{% if question %}
<div class="message">
- {% trans title=question.title, summary=question.summary %}Your question
+ {% trans title=question.title|escape, summary=question.summary|escape %}Your question
{{title}} {{summary}} will be posted once you log in
{% endtrans %}
</div>
diff --git a/askbot/skins/common/templates/debug_header.html b/askbot/skins/common/templates/debug_header.html new file mode 100644 index 00000000..e1230265 --- /dev/null +++ b/askbot/skins/common/templates/debug_header.html @@ -0,0 +1,27 @@ +{% if settings.USING_RUNSERVER %} + {% if settings.DEBUG == False %} + <div> + <p> + You are seeing this message because you are using Django runserver + and DEBUG_MODE is False. Runserver should not be used in production. + </p> + <p> + To serve static media in production - please run: + <pre>python manage.py collectstatic</pre> + </p> + <p> + If you do not see page styling - set DEBUG_MODE = True. + </p> + </div> + {% endif %} +{% else %} + {% if settings.DEBUG == True %} + <div> + <p> + Debug mode is on, do not use it in production. + To turn it off, use DEBUG = False in your + settings.py file. + </p> + </div> + {% endif %} +{% endif %} diff --git a/askbot/skins/default/templates/base.html b/askbot/skins/default/templates/base.html index 8287f5ba..39b89fe8 100644 --- a/askbot/skins/default/templates/base.html +++ b/askbot/skins/default/templates/base.html @@ -16,6 +16,7 @@ {% endspaceless %} <body class="{% block body_class %}{% endblock %}{% if user_messages %} user-messages{% endif %}{% if page_class %} {{page_class}}{% endif %}{% if request.user.is_anonymous() %} anon{% endif %} lang-{{settings.LANGUAGE_CODE}}"> {% include "widgets/system_messages.html" %} + {% include "debug_header.html" %} {% include "custom_header.html" ignore missing %} {% if settings.CUSTOM_HEADER|trim != '' %} <div id="custom-header"> diff --git a/askbot/skins/default/templates/close.html b/askbot/skins/default/templates/close.html index d8160865..bac2b3ee 100644 --- a/askbot/skins/default/templates/close.html +++ b/askbot/skins/default/templates/close.html @@ -4,7 +4,7 @@ {% block content %} <h1>{% trans %}Close question{% endtrans %}</h1> <p>{% trans %}Close the question{% endtrans %}: <a href="{{ question.get_absolute_url() }}"> - <strong>{{ question.get_question_title() }}</strong></a> + <strong>{{ question.get_question_title()|escape }}</strong></a> </p> <form id="fmclose" action="{% url close question.id %}" method="post" >{% csrf_token %} <p> diff --git a/askbot/skins/default/templates/meta/bottom_scripts.html b/askbot/skins/default/templates/meta/bottom_scripts.html index 24a79478..dd5cb202 100644 --- a/askbot/skins/default/templates/meta/bottom_scripts.html +++ b/askbot/skins/default/templates/meta/bottom_scripts.html @@ -28,6 +28,7 @@ askbot['urls']['follow_user'] = '/followit/follow/user/{{'{{'}}userId{{'}}'}}/'; askbot['urls']['unfollow_user'] = '/followit/unfollow/user/{{'{{'}}userId{{'}}'}}/'; askbot['urls']['user_signin'] = '{{ settings.LOGIN_URL }}'; + askbot['settings']['static_url'] = '{{ settings.STATIC_URL }}'; </script> <script type="text/javascript" diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html index 0c4ce4cc..33c0e11f 100644 --- a/askbot/skins/default/templates/question.html +++ b/askbot/skins/default/templates/question.html @@ -1,6 +1,6 @@ {% extends "two_column_body.html" %} <!-- question.html --> -{% block title %}{% spaceless %}{{ thread.get_title(question) }}{% endspaceless %}{% endblock %} +{% block title %}{% spaceless %}{{ question.get_question_title()|escape }}{% endspaceless %}{% endblock %} {% block meta_description %} <meta name="description" content="{{question.summary|striptags|escape}}" /> {% endblock %} diff --git a/askbot/skins/default/templates/question/question_card.html b/askbot/skins/default/templates/question/question_card.html index d71ff69b..ff4ada1d 100644 --- a/askbot/skins/default/templates/question/question_card.html +++ b/askbot/skins/default/templates/question/question_card.html @@ -9,8 +9,7 @@ </div> <div class="question-content"> - <h1><a href="{{ question.get_absolute_url() }}">{{ thread.get_title(question) }}</a></h1> - {# ==== START: question/question_tags.html" #} + <h1><a href="{{ question.get_absolute_url() }}">{{ thread.get_title(question)|escape }}</a></h1> {% include "question/question_tags.html" %} {# ==== END: question/question_tags.html" #} diff --git a/askbot/skins/default/templates/question/sidebar.html b/askbot/skins/default/templates/question/sidebar.html index c2eb3842..bc6a58c9 100644 --- a/askbot/skins/default/templates/question/sidebar.html +++ b/askbot/skins/default/templates/question/sidebar.html @@ -64,7 +64,7 @@ <div class="questions-related"> {% for thread_dict in similar_threads.data() %} <p> - <a href="{{ thread_dict.url }}">{{ thread_dict.title }}</a> + <a href="{{ thread_dict.url }}">{{ thread_dict.title|escape }}</a> </p> {% endfor %} </div> diff --git a/askbot/skins/default/templates/question_retag.html b/askbot/skins/default/templates/question_retag.html index c8981f72..d15813e5 100644 --- a/askbot/skins/default/templates/question_retag.html +++ b/askbot/skins/default/templates/question_retag.html @@ -5,7 +5,7 @@ <h1>{% trans %}Change tags{% endtrans %} [<a href="{{ question.get_absolute_url() }}">{% trans %}back{% endtrans %}</a>]</h1> <form id="fmretag" action="{% url retag_question question.id %}" method="post" >{% csrf_token %} <h2> - {{ question.thread.get_title() }} + {{ question.thread.get_title()|escape }} </h2> <div id="description" class="edit-content-html"> {{ question.html }} diff --git a/askbot/skins/default/templates/question_widget.html b/askbot/skins/default/templates/question_widget.html index 0aee8f05..65ee8b64 100644 --- a/askbot/skins/default/templates/question_widget.html +++ b/askbot/skins/default/templates/question_widget.html @@ -12,7 +12,7 @@ <ul> {% for thread in threads %} <li><a href="{{settings.APP_URL}}{{ thread.get_absolute_url() }}"> - {{ thread.title }}</a></li> + {{ thread.title|escape }}</a></li> {% endfor %} </ul> </div> diff --git a/askbot/skins/default/templates/reopen.html b/askbot/skins/default/templates/reopen.html index 46c86e8b..d1ccc313 100644 --- a/askbot/skins/default/templates/reopen.html +++ b/askbot/skins/default/templates/reopen.html @@ -5,7 +5,7 @@ <h1>{% trans %}Reopen question{% endtrans %}</h1> <p>{% trans %}Title{% endtrans %}: <a href="{{ question.get_absolute_url() }}"> - <span class="big">{{ question.get_question_title() }}</span> + <span class="big">{{ question.get_question_title()|escape }}</span> </a> </p> <p>{% trans %}This question has been closed by diff --git a/askbot/skins/default/templates/revisions.html b/askbot/skins/default/templates/revisions.html index a4e429a4..07b98b5b 100644 --- a/askbot/skins/default/templates/revisions.html +++ b/askbot/skins/default/templates/revisions.html @@ -30,7 +30,7 @@ <td width="200px" style="vertical-align:middle"> {% if revision.summary %} <div class="summary"> - <span>{{ revision.summary }}</span> + <span>{{ revision.summary|escape }}</span> </div> {% endif %} {% if request.user|can_edit_post(post) %} diff --git a/askbot/skins/default/templates/user_profile/user_recent.html b/askbot/skins/default/templates/user_profile/user_recent.html index a8fd4890..bace94d8 100644 --- a/askbot/skins/default/templates/user_profile/user_recent.html +++ b/askbot/skins/default/templates/user_profile/user_recent.html @@ -20,11 +20,11 @@ </a> {% if act.content_object.post_type == 'question' %} {% set question=act.content_object %} - (<a title="{{question.summary|collapse}}" + (<a title="{{question.summary|collapse|escape}}" href="{% url question question.id %}{{question.thread.title|slugify}}">{% trans %}source{% endtrans %}</a>) {% elif act.content_object.post_type == 'answer' %} {% set answer=act.content_object %} - (<a title="{{answer.text|collapse}}" + (<a title="{{answer.text|collapse|escape}}" href="{% url question answer.thread._question_post().id %}{{answer.thread.title|slugify}}#{{answer.id}}">{% trans %}source{% endtrans %}</a>) {% endif %} {% else %} diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html index 0b85f648..793bce77 100644 --- a/askbot/skins/default/templates/user_profile/user_stats.html +++ b/askbot/skins/default/templates/user_profile/user_stats.html @@ -18,7 +18,7 @@ <div class="user-stats-table"> {% for top_answer in top_answers %} <div class="answer-summary"> - <a title="{{ top_answer.summary|collapse }}" + <a title="{{ top_answer.summary|collapse|escape }}" href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{ top_answer.id }}"> <span class="answer-votes {% if top_answer.accepted() %}answered-accepted{% endif %}" title="{% trans answer_score=top_answer.score %}the answer has been voted for {{ answer_score }} times{% endtrans %} {% if top_answer.accepted() %}{% trans %}this answer has been selected as correct{% endtrans %}{%endif%}"> @@ -27,7 +27,7 @@ </a> <div class="answer-link"> {% spaceless %} - <a href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{top_answer.id}}">{{ top_answer.thread.title }}</a> + <a href="{% url question top_answer.thread._question_post().id %}{{ top_answer.thread.title|slugify }}#{{top_answer.id}}">{{ top_answer.thread.title|escape }}</a> {% endspaceless %} {% if top_answer.comment_count > 0 %} <span> @@ -122,6 +122,7 @@ {% endif %} {% endfor %} </ul> +<<<<<<< HEAD {% if loop.index is divisibleby 3 %} </td></tr> <tr><td style="line-height:35px"> @@ -131,6 +132,51 @@ </tr> </table> </div> +======= + </td> + </tr> + </table> + </div> + <a name="badges"></a> + {% spaceless %} + <h2>{% trans counter=total_awards %}<span class="count">{{counter}}</span> Badge{% pluralize %}<span class="count">{{counter}}</span> Badges{% endtrans %}</h2> + {% endspaceless %} + <div class="user-stats-table badges"> + <table> + <tr> + <td style="line-height:35px"> + {% for badge in badges %}{# todo: translate badge name properly #} + <a + href="{{badge.get_absolute_url()}}" + title="{% trans description=badge.description %}{{description}}{% endtrans %}" + class="medal" + ><span class="{{ badge.css_class }}">●</span> {% trans name=badge.name %}{{name}}{% endtrans %} + </a> + <span class="tag-number">× + <span class="badge-context-toggle">{{ badge.award_badge.count()|intcomma }}</span> + </span> + <ul id="badge-context-{{ badge.id }}" class="badge-context-list" style="display:none"> + {% for award in badge.award_badge.filter(user = view_user) %} + {% if award.content_type in (question_type, answer_type) %} + <li> + <a + title="{{ award.content_object.get_snippet()|collapse }}" + href="{{ award.content_object.get_absolute_url() }}" + >{% if award.content_type == answer_type %}{% trans %}Answer to:{% endtrans %}{% endif %} {{ award.content_object.get_origin_post().title|escape }}</a> + </li> + {% endif %} + {% endfor %} + </ul> + {% if loop.index is divisibleby 3 %} + </td></tr> + <tr><td style="line-height:35px"> + {% endif %} + {% endfor %} + </td> + </tr> + </table> + </div> +>>>>>>> master {% endblock %} {% block endjs %} {{ super() }} diff --git a/askbot/skins/default/templates/widgets/ask_form.html b/askbot/skins/default/templates/widgets/ask_form.html index 18196d93..17dc89f5 100644 --- a/askbot/skins/default/templates/widgets/ask_form.html +++ b/askbot/skins/default/templates/widgets/ask_form.html @@ -14,7 +14,7 @@ {% endif %} {% endif %} <input id="id_title" class="questionTitleInput" name="title" autocomplete="off" - value="{% if form.initial.title %}{{form.initial.title}}{% endif %}"/> + value="{% if form.initial.title %}{{form.initial.title|escape}}{% endif %}"/> <span class="form-error">{{ form.title.errors }}</span> </div> <div class="title-desc"> diff --git a/askbot/skins/utils.py b/askbot/skins/utils.py index 50c0d4fb..520fa2f7 100644 --- a/askbot/skins/utils.py +++ b/askbot/skins/utils.py @@ -154,13 +154,8 @@ def get_media_url(url, ignore_missing = False): logging.critical(log_message) return None - url = use_skin + '/media/' + url - url = '///' + django_settings.ASKBOT_URL + 'm/' + url - url = os.path.normpath(url).replace( - '\\', '/' - ).replace( - '///', '/' - ) + url = django_settings.STATIC_URL + use_skin + '/media/' + url + url = os.path.normpath(url).replace('\\', '/') if resource_revision: url += '?v=%d' % resource_revision diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py index 448f8a29..966e98e2 100644 --- a/askbot/startup_procedures.py +++ b/askbot/startup_procedures.py @@ -9,22 +9,58 @@ the main function is run_startup_tests """ import sys import os +import re +import askbot from django.db import transaction from django.conf import settings as django_settings from django.core.exceptions import ImproperlyConfigured from askbot.utils.loading import load_module +from askbot.utils.functions import enumerate_string_list +from urlparse import urlparse PREAMBLE = """\n ************************ * * * Askbot self-test * * * -************************""" +************************\n +""" + +FOOTER = """\n +If necessary, type ^C (Ctrl-C) to stop the program. +""" + +class AskbotConfigError(ImproperlyConfigured): + """Prints an error with a preamble and possibly a footer""" + def __init__(self, error_message): + msg = PREAMBLE + error_message + if sys.__stdin__.isatty(): + #print footer only when askbot is run from the shell + msg += FOOTER + super(AskbotConfigError, self).__init__(msg) def askbot_warning(line): """prints a warning with the nice header, but does not quit""" print >> sys.stderr, PREAMBLE + '\n' + line +def print_errors(error_messages, header = None, footer = None): + """if there is one or more error messages, + raise ``class:AskbotConfigError`` with the human readable + contents of the message + * ``header`` - text to show above messages + * ``footer`` - text to show below messages + """ + if len(error_messages) == 0: return + if len(error_messages) > 1: + error_messages = enumerate_string_list(error_messages) + + message = '' + if header: message += header + '\n' + message += 'Please attend to the following:\n\n' + message += '\n\n'.join(error_messages) + if footer: message += '\n\n' + footer + raise AskbotConfigError(message) + def format_as_text_tuple_entries(items): """prints out as entries or tuple containing strings ready for copy-pasting into say django settings file""" @@ -45,21 +81,21 @@ def test_askbot_url(): pass else: msg = 'setting ASKBOT_URL must be of string or unicode type' - raise ImproperlyConfigured(msg) + raise AskbotConfigError(msg) if url == '/': msg = 'value "/" for ASKBOT_URL is invalid. '+ \ 'Please, either make ASKBOT_URL an empty string ' + \ 'or a non-empty path, ending with "/" but not ' + \ 'starting with "/", for example: "forum/"' - raise ImproperlyConfigured(msg) + raise AskbotConfigError(msg) else: try: assert(url.endswith('/')) except AssertionError: msg = 'if ASKBOT_URL setting is not empty, ' + \ 'it must end with /' - raise ImproperlyConfigured(msg) + raise AskbotConfigError(msg) try: assert(not url.startswith('/')) except AssertionError: @@ -69,7 +105,7 @@ def test_askbot_url(): def test_middleware(): """Checks that all required middleware classes are installed in the django settings.py file. If that is not the - case - raises an ImproperlyConfigured exception. + case - raises an AskbotConfigError exception. """ required_middleware = ( 'django.contrib.sessions.middleware.SessionMiddleware', @@ -95,7 +131,7 @@ to the MIDDLEWARE_CLASSES variable in your site settings.py file. The order the middleware records may be important, please take a look at the example in https://github.com/ASKBOT/askbot-devel/blob/master/askbot/setup_templates/settings.py:\n\n""" middleware_text = format_as_text_tuple_entries(missing_middleware_set) - raise ImproperlyConfigured(PREAMBLE + error_message + middleware_text) + raise AskbotConfigError(error_message + middleware_text) #middleware that was used in the past an now removed @@ -110,7 +146,7 @@ https://github.com/ASKBOT/askbot-devel/blob/master/askbot/setup_templates/settin error_message = """\n\nPlease remove the following middleware entries from the list of MIDDLEWARE_CLASSES in your settings.py - these are not used any more:\n\n""" middleware_text = format_as_text_tuple_entries(remove_middleware_set) - raise ImproperlyConfigured(PREAMBLE + error_message + middleware_text) + raise AskbotConfigError(error_message + middleware_text) def try_import(module_name, pypi_package_name): """tries importing a module and advises to install @@ -123,7 +159,7 @@ def try_import(module_name, pypi_package_name): message += '\n\nTo install all the dependencies at once, type:' message += '\npip install -r askbot_requirements.txt\n' message += '\nType ^C to quit.' - raise ImproperlyConfigured(message) + raise AskbotConfigError(message) def test_modules(): """tests presence of required modules""" @@ -139,7 +175,7 @@ def test_postgres(): import psycopg2 version = psycopg2.__version__.split(' ')[0].split('.') if version == ['2', '4', '2']: - raise ImproperlyConfigured( + raise AskbotConfigError( 'Please install psycopg2 version 2.4.1,\n version 2.4.2 has a bug' ) elif version > ['2', '4', '2']: @@ -169,7 +205,7 @@ def test_template_loader(): loader that used to send a warning""" old_template_loader = 'askbot.skins.loaders.load_template_source' if old_template_loader in django_settings.TEMPLATE_LOADERS: - raise ImproperlyConfigured(PREAMBLE + \ + raise AskbotConfigError( "\nPlease change: \n" "'askbot.skins.loaders.load_template_source', to\n" "'askbot.skins.loaders.filesystem_load_template_source',\n" @@ -187,7 +223,7 @@ def test_celery(): if broker_backend is None: if broker_transport is None: - raise ImproperlyConfigured(PREAMBLE + \ + raise AskbotConfigError( "\nPlease add\n" 'BROKER_TRANSPORT = "djkombu.transport.DatabaseTransport"\n' "or other valid value to your settings.py file" @@ -197,7 +233,7 @@ def test_celery(): return if broker_backend != broker_transport: - raise ImproperlyConfigured(PREAMBLE + \ + raise AskbotConfigError( "\nPlease rename setting BROKER_BACKEND to BROKER_TRANSPORT\n" "in your settings.py file\n" "If you have both in your settings.py - then\n" @@ -205,7 +241,7 @@ def test_celery(): ) if hasattr(django_settings, 'BROKER_BACKEND') and not hasattr(django_settings, 'BROKER_TRANSPORT'): - raise ImproperlyConfigured(PREAMBLE + \ + raise AskbotConfigError( "\nPlease rename setting BROKER_BACKEND to BROKER_TRANSPORT\n" "in your settings.py file" ) @@ -216,7 +252,7 @@ def test_media_url(): media_url = django_settings.MEDIA_URL #todo: add proper url validation to MEDIA_URL setting if not (media_url.startswith('/') or media_url.startswith('http')): - raise ImproperlyConfigured(PREAMBLE + \ + raise AskbotConfigError( "\nMEDIA_URL parameter must be a unique url on the site\n" "and must start with a slash - e.g. /media/ or http(s)://" ) @@ -265,11 +301,129 @@ class SettingsTester(object): **self.requirements[setting_name] ) if len(self.messages) != 0: - raise ImproperlyConfigured( - PREAMBLE + + raise AskbotConfigError( '\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() + import django + django_version = django.VERSION + if django_version[0] == 1 and django_version[1] < 3: + staticfiles_app_name = 'staticfiles' + wrong_staticfiles_app_name = 'django.contrib.staticfiles' + try_import('staticfiles', 'django-staticfiles') + import staticfiles + if staticfiles.__version__[0] != 1: + raise AskbotConfigError( + 'Please use the newest available version of ' + 'django-staticfiles app, type\n' + 'pip install --upgrade django-staticfiles' + ) + if not hasattr(django_settings, 'STATICFILES_STORAGE'): + raise AskbotConfigError( + 'Configure STATICFILES_STORAGE setting as desired, ' + 'a reasonable default is\n' + "STATICFILES_STORAGE = 'staticfiles.storage.StaticFilesStorage'" + ) + else: + staticfiles_app_name = 'django.contrib.staticfiles' + wrong_staticfiles_app_name = 'staticfiles' + + if staticfiles_app_name not in django_settings.INSTALLED_APPS: + errors.append( + 'Add to the INSTALLED_APPS section of your settings.py:\n' + " '%s'," % staticfiles_app_name + ) + if wrong_staticfiles_app_name in django_settings.INSTALLED_APPS: + errors.append( + 'Remove from the INSTALLED_APPS section of your settings.py:\n' + " '%s'," % wrong_staticfiles_app_name + ) + static_url = django_settings.STATIC_URL + 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 ' + 'are accessible.' + ) + url = urlparse(static_url).path + if not (url.startswith('/') and url.endswith('/')): + #a simple check for the url + errors.append( + 'Path in the STATIC_URL must start and end with the /.' + ) + if django_settings.ADMIN_MEDIA_PREFIX != static_url + 'admin/': + errors.append( + 'Set ADMIN_MEDIA_PREFIX as: \n' + " ADMIN_MEDIA_PREFIX = STATIC_URL + 'admin/'" + ) + + askbot_root = os.path.dirname(askbot.__file__) + skin_dir = os.path.abspath(os.path.join(askbot_root, 'skins')) + if skin_dir not in map(os.path.abspath, django_settings.STATICFILES_DIRS): + errors.append( + 'Add to STATICFILES_DIRS list of your settings.py file:\n' + " '%s'," % skin_dir + ) + extra_skins_dir = getattr(django_settings, 'ASKBOT_EXTRA_SKINS_DIR', None) + if extra_skins_dir is not None: + if not os.path.isdir(extra_skins_dir): + errors.append( + 'Directory specified with settning ASKBOT_EXTRA_SKINS_DIR ' + 'must exist and contain your custom skins for askbot.' + ) + if extra_skins_dir not in django_settings.STATICFILES_DIRS: + errors.append( + 'Add ASKBOT_EXTRA_SKINS_DIR to STATICFILES_DIRS entry in ' + 'your settings.py file.\n' + 'NOTE: it might be necessary to move the line with ' + 'ASKBOT_EXTRA_SKINS_DIR just above STATICFILES_DIRS.' + ) + + if errors: + errors.append( + 'Run command (after fixing the above errors)\n' + ' python manage.py collectstatic\n' + ) + print_errors(errors) + if django_settings.DEBUG and django_settings.STATICFILES_STORAGE == \ + 'django.contrib.staticfiles.storage.StaticFilesStorage': + if not os.path.isdir(django_settings.STATIC_ROOT): + askbot_warning( + 'Please run command\n\n' + ' python manage.py collectstatic' + + ) + +def test_csrf_cookie_domain(): + """makes sure that csrf cookie domain setting is acceptable""" + #todo: maybe use the same steps to clean domain name + csrf_cookie_domain = django_settings.CSRF_COOKIE_DOMAIN + if csrf_cookie_domain == 'localhost': + raise ImproperlyConfigured( + PREAMBLE + + '\n\nPlease do not use value "localhost" for the setting ' + 'CSRF_COOKIE_DOMAIN\n' + 'instead use 127.0.0.1, a real IP ' + 'address or domain name.' + '\nThe value must match the network location you type in the ' + 'web browser to reach your site.' + ) + if re.match(r'https?://', csrf_cookie_domain): + raise ImproperlyConfigured( + PREAMBLE + + '\n\nplease remove http(s):// prefix in the CSRF_COOKIE_DOMAIN ' + 'setting' + ) + if ':' in csrf_cookie_domain: + raise ImproperlyConfigured( + PREAMBLE + + '\n\nPlease do not use port number in the CSRF_COOKIE_DOMAIN ' + 'setting' + ) def run_startup_tests(): """function that runs @@ -284,6 +438,8 @@ def run_startup_tests(): #test_postgres() test_middleware() test_celery() + test_csrf_cookie_domain() + test_staticfiles() settings_tester = SettingsTester({ 'CACHE_MIDDLEWARE_ANONYMOUS_ONLY': { 'value': True, @@ -319,7 +475,7 @@ def run(): """runs all the startup procedures""" try: run_startup_tests() - except ImproperlyConfigured, error: + except AskbotConfigError, error: transaction.rollback() print error sys.exit(1) diff --git a/askbot/tests/images/logo.gif b/askbot/tests/images/logo.gif Binary files differindex 8540e12b..8540e12b 100755..100644 --- a/askbot/tests/images/logo.gif +++ b/askbot/tests/images/logo.gif diff --git a/askbot/tests/skin_tests.py b/askbot/tests/skin_tests.py index ecbea77d..5226e6d6 100644 --- a/askbot/tests/skin_tests.py +++ b/askbot/tests/skin_tests.py @@ -41,13 +41,9 @@ class SkinTests(TestCase): def assert_default_logo_in_skin(self, skin_name): url = skin_utils.get_media_url(askbot_settings.SITE_LOGO_URL) self.assertTrue('/' + skin_name + '/' in url) - response = self.client.get(url) - self.assertTrue(response.status_code == 200) def test_default_skin_logo(self): - """make sure that default logo - is where it is expected - """ + """make sure that default logo is where it is expected""" self.assert_default_logo_in_skin('default') def test_switch_to_custom_skin_logo(self): diff --git a/askbot/urls.py b/askbot/urls.py index de7b3026..bcae5c85 100644 --- a/askbot/urls.py +++ b/askbot/urls.py @@ -36,11 +36,6 @@ urlpatterns = patterns('', {'sitemaps': sitemaps}, name='sitemap' ), - url( - r'^m/(?P<skin>[^/]+)/media/(?P<resource>.*)$', - views.meta.media, - name='askbot_media', - ), #no translation for this url!! url(r'import-data/$', views.writers.import_data, name='import_data'), url(r'^%s$' % _('about/'), views.meta.about, name='about'), diff --git a/askbot/utils/functions.py b/askbot/utils/functions.py index f47f0d2a..6042414c 100644 --- a/askbot/utils/functions.py +++ b/askbot/utils/functions.py @@ -11,6 +11,14 @@ def get_from_dict_or_object(source, key): return getattr(source, key) +def enumerate_string_list(strings): + """for a list or a tuple ('one', 'two',) return + a list formatted as ['1) one', '2) two',] + """ + numbered_strings = enumerate(strings, start = 1) + return [ '%d) %s' % item for item in numbered_strings ] + + def is_iterable(thing): if hasattr(thing, '__iter__'): return True diff --git a/askbot/views/meta.py b/askbot/views/meta.py index 6415077a..884ec5e4 100644 --- a/askbot/views/meta.py +++ b/askbot/views/meta.py @@ -136,14 +136,3 @@ def badge(request, id): 'page_class': 'meta', } return render_into_skin('badge.html', data, request) - -def media(request, skin, resource): - """view that serves static media from any skin - uses django static serve view, where document root is - adjusted according to the current skin selection - - in production this views should be by-passed via server configuration - for the better efficiency of serving static files - """ - dir = skins.utils.get_path_to_skin(skin) - return static.serve(request, '/media/' + resource, document_root = dir) |