summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--askbot/__init__.py1
-rw-r--r--askbot/conf/__init__.py8
-rw-r--r--askbot/conf/email.py8
-rw-r--r--askbot/conf/ldap.py15
-rw-r--r--askbot/conf/user_settings.py19
-rw-r--r--askbot/const/__init__.py10
-rw-r--r--askbot/context.py2
-rw-r--r--askbot/db0
-rw-r--r--askbot/deps/django_authopenid/backends.py196
-rw-r--r--askbot/deps/django_authopenid/ldap_auth.py208
-rw-r--r--askbot/deps/django_authopenid/views.py139
-rw-r--r--askbot/doc/source/optional-modules.rst44
-rw-r--r--askbot/forms.py37
-rw-r--r--askbot/management/commands/askbot_add_test_content.py14
-rw-r--r--askbot/migrations/0133_apply_global_group_to_posts_and_users.py12
-rw-r--r--askbot/models/__init__.py13
-rw-r--r--askbot/models/post.py3
-rw-r--r--askbot/models/question.py32
-rw-r--r--askbot/setup_templates/settings.py26
-rw-r--r--askbot/setup_templates/settings.py.mustache27
-rw-r--r--askbot/setup_templates/tinymce_sample_config.py26
-rw-r--r--askbot/skins/common/media/js/editor.js111
-rw-r--r--askbot/skins/common/media/js/jquery.ajaxfileupload.js37
-rw-r--r--askbot/skins/common/media/js/post.js46
-rw-r--r--askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js111
-rw-r--r--askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en.js3
-rw-r--r--askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en_dlg.js3
-rw-r--r--askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js111
-rw-r--r--askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en.js3
-rw-r--r--askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en_dlg.js3
-rw-r--r--askbot/skins/common/media/js/utils.js359
-rw-r--r--askbot/skins/common/media/js/wmd/wmd.js27
-rw-r--r--askbot/skins/common/templates/authopenid/complete.html10
-rw-r--r--askbot/skins/common/templates/authopenid/signin.html42
-rw-r--r--askbot/skins/common/templates/authopenid/signup_with_password.html9
-rw-r--r--askbot/skins/common/templates/widgets/edit_post.html5
-rw-r--r--askbot/skins/default/media/bootstrap/css/bootstrap.css142
-rw-r--r--askbot/skins/default/media/images/attachment.pngbin0 -> 3142 bytes
-rw-r--r--askbot/skins/default/media/style/style.less108
-rw-r--r--askbot/skins/default/templates/ask.html1
-rw-r--r--askbot/skins/default/templates/ask_by_widget.html16
-rw-r--r--askbot/skins/default/templates/ask_widget_complete.html8
-rw-r--r--askbot/skins/default/templates/base.html4
-rw-r--r--askbot/skins/default/templates/groups.html20
-rw-r--r--askbot/skins/default/templates/macros.html19
-rw-r--r--askbot/skins/default/templates/meta/bottom_scripts.html4
-rw-r--r--askbot/skins/default/templates/meta/html_head_javascript.html11
-rw-r--r--askbot/skins/default/templates/meta/html_head_stylesheets.html3
-rw-r--r--askbot/skins/default/templates/meta/tinymce.html35
-rw-r--r--askbot/skins/default/templates/question.html5
-rw-r--r--askbot/skins/default/templates/question/new_answer_form.html1
-rw-r--r--askbot/skins/default/templates/user_profile/user_inbox.html1
-rw-r--r--askbot/skins/default/templates/user_profile/user_info.html16
-rw-r--r--askbot/skins/default/templates/user_profile/user_stats.html17
-rw-r--r--askbot/skins/default/templates/users.html19
-rw-r--r--askbot/skins/default/templates/widgets/answer_edit_tips.html46
-rw-r--r--askbot/skins/default/templates/widgets/markdown_help.html42
-rw-r--r--askbot/skins/default/templates/widgets/question_edit_tips.html47
-rw-r--r--askbot/skins/default/templates/widgets/question_summary.html5
-rw-r--r--askbot/skins/default/templates/widgets/tag_editor.html6
-rw-r--r--askbot/startup_procedures.py62
-rw-r--r--askbot/tests/__init__.py1
-rw-r--r--askbot/tests/badge_tests.py2
-rw-r--r--askbot/tests/utils_tests.py17
-rw-r--r--askbot/tests/widget_tests.py22
-rw-r--r--askbot/urls.py1
-rw-r--r--askbot/utils/forms.py12
-rw-r--r--askbot/utils/url_utils.py33
-rw-r--r--askbot/views/readers.py23
-rw-r--r--askbot/views/users.py17
-rw-r--r--askbot/views/widgets.py6
-rw-r--r--askbot/views/writers.py5
-rw-r--r--askbot_requirements.txt1
73 files changed, 1716 insertions, 782 deletions
diff --git a/askbot/__init__.py b/askbot/__init__.py
index 8e4f20ab..c79d19f9 100644
--- a/askbot/__init__.py
+++ b/askbot/__init__.py
@@ -32,6 +32,7 @@ REQUIREMENTS = {
'pystache': 'pystache==0.3.1',
'lamson': 'Lamson',
'pytz': 'pytz',
+ 'tinymce': 'django-tinymce',
}
#necessary for interoperability of django and coffin
diff --git a/askbot/conf/__init__.py b/askbot/conf/__init__.py
index a90c2ccc..3e80877c 100644
--- a/askbot/conf/__init__.py
+++ b/askbot/conf/__init__.py
@@ -36,3 +36,11 @@ def should_show_sort_by_relevance():
questions by search relevance
"""
return ('postgresql_psycopg2' in askbot.get_database_engine_name())
+
+def get_tag_display_filter_strategy_choices():
+ from askbot import const
+ from askbot.conf import settings as askbot_settings
+ if askbot_settings.SUBSCRIBED_TAG_SELECTOR_ENABLED:
+ return const.TAG_DISPLAY_FILTER_STRATEGY_CHOICES
+ else:
+ return const.TAG_DISPLAY_FILTER_STRATEGY_MINIMAL_CHOICES
diff --git a/askbot/conf/email.py b/askbot/conf/email.py
index 1b81fa96..9330e638 100644
--- a/askbot/conf/email.py
+++ b/askbot/conf/email.py
@@ -66,7 +66,7 @@ settings.register(
livesettings.StringValue(
EMAIL,
'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ASK',
- default='w',
+ default='i',
choices=const.NOTIFICATION_DELIVERY_SCHEDULE_CHOICES,
description=_('Default notification frequency questions asked by the user'),
help_text=_(
@@ -80,7 +80,7 @@ settings.register(
livesettings.StringValue(
EMAIL,
'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_ANS',
- default='w',
+ default='d',
choices=const.NOTIFICATION_DELIVERY_SCHEDULE_CHOICES,
description=_('Default notification frequency questions answered by the user'),
help_text=_(
@@ -94,7 +94,7 @@ settings.register(
livesettings.StringValue(
EMAIL,
'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_Q_SEL',
- default='w',
+ default='i',
choices=const.NOTIFICATION_DELIVERY_SCHEDULE_CHOICES,
description=_('Default notification frequency questions individually \
selected by the user'),
@@ -109,7 +109,7 @@ settings.register(
livesettings.StringValue(
EMAIL,
'DEFAULT_NOTIFICATION_DELIVERY_SCHEDULE_M_AND_C',
- default='w',
+ default='i',
choices=const.NOTIFICATION_DELIVERY_SCHEDULE_CHOICES,
description=_('Default notification frequency for mentions \
and comments'),
diff --git a/askbot/conf/ldap.py b/askbot/conf/ldap.py
index ae916313..5c37adb1 100644
--- a/askbot/conf/ldap.py
+++ b/askbot/conf/ldap.py
@@ -19,6 +19,20 @@ settings.register(
)
)
+settings.register(
+ livesettings.BooleanValue(
+ LDAP_SETTINGS,
+ 'LDAP_AUTOCREATE_USERS',
+ description = _('Automatically create user accounts when possible'),
+ default = False,
+ help_text = _(
+ 'Potentially reduces number of steps in the registration process '
+ 'but can expose personal information, e.g. when LDAP login name is '
+ 'the same as email address or real name.'
+ )
+ )
+)
+
LDAP_PROTOCOL_VERSION_CHOICES = (
('3', _('Version 3')),
('2', _('Version 2 (insecure and deprecated)!!!'))
@@ -66,6 +80,7 @@ settings.register(
LDAP_SETTINGS,
'LDAP_BASE_DN',
description=_('Base DN (distinguished name)'),
+ default = '',
help_text = _(
'Usually base DN mirrors domain name of your organization, '
'e.g. "dn=example,dn=com" when your site url is "example.com".'
diff --git a/askbot/conf/user_settings.py b/askbot/conf/user_settings.py
index a1d5a55c..adbdd5ff 100644
--- a/askbot/conf/user_settings.py
+++ b/askbot/conf/user_settings.py
@@ -45,6 +45,16 @@ settings.register(
settings.register(
livesettings.BooleanValue(
USER_SETTINGS,
+ 'AUTOFILL_USER_DATA',
+ default = True,
+ description = _('Auto-fill user name, email, etc on registration'),
+ help_text = _('Implemented only for LDAP logins at this point')
+ )
+)
+
+settings.register(
+ livesettings.BooleanValue(
+ USER_SETTINGS,
'EDITABLE_EMAIL',
default = True,
description = _('Allow users change own email addresses')
@@ -54,6 +64,15 @@ settings.register(
settings.register(
livesettings.BooleanValue(
USER_SETTINGS,
+ 'ALLOW_EMAIL_ADDRESS_IN_USERNAME',
+ default=True,
+ description=_('Allow email address in user name')
+ )
+)
+
+settings.register(
+ livesettings.BooleanValue(
+ USER_SETTINGS,
'ALLOW_ACCOUNT_RECOVERY_BY_EMAIL',
default = True,
description = _('Allow account recovery by email')
diff --git a/askbot/const/__init__.py b/askbot/const/__init__.py
index 037ee379..dfb6995a 100644
--- a/askbot/const/__init__.py
+++ b/askbot/const/__init__.py
@@ -305,11 +305,17 @@ POST_STATUS = {
INCLUDE_ALL = 0
EXCLUDE_IGNORED = 1
INCLUDE_INTERESTING = 2
-TAG_DISPLAY_FILTER_STRATEGY_CHOICES = (
+INCLUDE_SUBSCRIBED = 3
+TAG_DISPLAY_FILTER_STRATEGY_MINIMAL_CHOICES = (
(INCLUDE_ALL, _('show all tags')),
(EXCLUDE_IGNORED, _('exclude ignored tags')),
- (INCLUDE_INTERESTING, _('only interesting tags')),
+ (INCLUDE_INTERESTING, _('only interesting tags'))
)
+TAG_DISPLAY_FILTER_STRATEGY_CHOICES = \
+ TAG_DISPLAY_FILTER_STRATEGY_MINIMAL_CHOICES + \
+ ((INCLUDE_SUBSCRIBED, _('only subscribed tags')),)
+
+
TAG_EMAIL_FILTER_STRATEGY_CHOICES = (
(INCLUDE_ALL, _('email for all tags')),
(EXCLUDE_IGNORED, _('exclude ignored tags')),
diff --git a/askbot/context.py b/askbot/context.py
index 5db008ff..402183ea 100644
--- a/askbot/context.py
+++ b/askbot/context.py
@@ -25,6 +25,8 @@ def application_settings(request):
return {}
my_settings = askbot_settings.as_dict()
my_settings['LANGUAGE_CODE'] = getattr(request, 'LANGUAGE_CODE', settings.LANGUAGE_CODE)
+ my_settings['ALLOWED_UPLOAD_FILE_TYPES'] = \
+ settings.ASKBOT_ALLOWED_UPLOAD_FILE_TYPES
my_settings['ASKBOT_URL'] = settings.ASKBOT_URL
my_settings['STATIC_URL'] = settings.STATIC_URL
my_settings['ASKBOT_CSS_DEVEL'] = getattr(
diff --git a/askbot/db b/askbot/db
deleted file mode 100644
index e69de29b..00000000
--- a/askbot/db
+++ /dev/null
diff --git a/askbot/deps/django_authopenid/backends.py b/askbot/deps/django_authopenid/backends.py
index 5ff49c1b..f41427b9 100644
--- a/askbot/deps/django_authopenid/backends.py
+++ b/askbot/deps/django_authopenid/backends.py
@@ -10,169 +10,12 @@ from django.conf import settings as django_settings
from django.utils.translation import ugettext as _
from askbot.deps.django_authopenid.models import UserAssociation
from askbot.deps.django_authopenid import util
+from askbot.deps.django_authopenid.ldap_auth import ldap_authenticate
+from askbot.deps.django_authopenid.ldap_auth import ldap_create_user
from askbot.conf import settings as askbot_settings
from askbot.models.signals import user_registered
-log = logging.getLogger('configuration')
-
-def split_name(full_name, name_format):
- bits = full_name.strip().split()
- if len(bits) == 1:
- bits.push('')
- elif len(bits) == 0:
- bits = ['', '']
-
- if name_format == 'first,last':
- return bits[0], bits[1]
- elif name_format == 'last,first':
- return bits[1], bits[0]
- else:
- raise ValueError('Unexpected value of name_format')
-
-
-def ldap_authenticate(username, password):
- """
- Authenticate using ldap
-
- python-ldap must be installed
- http://pypi.python.org/pypi/python-ldap/2.4.6
- """
- import ldap
- user_information = None
- try:
- ldap_session = ldap.initialize(askbot_settings.LDAP_URL)
-
- #set protocol version
- if askbot_settings.LDAP_PROTOCOL_VERSION == '2':
- ldap_session.protocol_version = ldap.VERSION2
- elif askbot_settings.LDAP_PROTOCOL_VERSION == '3':
- ldap_session.protocol_version = ldap.VERSION3
- else:
- raise NotImplementedError('unsupported version of ldap protocol')
-
- ldap.set_option(ldap.OPT_REFERRALS, 0)
-
- #set extra ldap options, if given
- if hasattr(django_settings, 'LDAP_EXTRA_OPTIONS'):
- options = django_settings.LDAP_EXTRA_OPTIONS
- for key, value in options:
- if key.startswith('OPT_'):
- ldap_key = getattr(ldap, key)
- ldap.set_option(ldap_key, value)
- else:
- raise ValueError('Invalid LDAP option %s' % key)
-
- #add optional "master" LDAP authentication, if required
- master_username = getattr(django_settings, 'LDAP_USER', None)
- master_password = getattr(django_settings, 'LDAP_PASSWORD', None)
-
- login_name_field = askbot_settings.LDAP_LOGIN_NAME_FIELD
- base_dn = askbot_settings.LDAP_BASE_DN
- login_template = login_name_field + '=%s,' + base_dn
- encoding = askbot_settings.LDAP_ENCODING
-
- if master_username and master_password:
- login_dn = login_template % master_username
- ldap_session.simple_bind_s(
- login_dn.encode(encoding),
- master_password.encode(encoding)
- )
-
- user_filter = askbot_settings.LDAP_USER_FILTER_TEMPLATE % (
- askbot_settings.LDAP_LOGIN_NAME_FIELD,
- username
- )
-
- email_field = askbot_settings.LDAP_EMAIL_FIELD
-
- get_attrs = [
- email_field.encode(encoding),
- login_name_field.encode(encoding)
- #str(askbot_settings.LDAP_USERID_FIELD)
- #todo: here we have a chance to get more data from LDAP
- #maybe a point for some plugin
- ]
-
- common_name_field = askbot_settings.LDAP_COMMON_NAME_FIELD.strip()
- given_name_field = askbot_settings.LDAP_GIVEN_NAME_FIELD.strip()
- surname_field = askbot_settings.LDAP_SURNAME_FIELD.strip()
-
- if given_name_field and surname_field:
- get_attrs.append(given_name_field.encode(encoding))
- get_attrs.append(surname_field.encode(encoding))
- elif common_name_field:
- get_attrs.append(common_name_field.encode(encoding))
-
- # search ldap directory for user
- user_search_result = ldap_session.search_s(
- askbot_settings.LDAP_BASE_DN.encode(encoding),
- ldap.SCOPE_SUBTREE,
- user_filter.encode(encoding),
- get_attrs
- )
- if user_search_result: # User found in LDAP Directory
- user_dn = user_search_result[0][0]
- user_information = user_search_result[0][1]
- ldap_session.simple_bind_s(user_dn, password.encode(encoding)) #raises INVALID_CREDENTIALS
- ldap_session.unbind_s()
-
- exact_username = user_information[login_name_field][0]
- email = user_information[email_field][0]
-
- if given_name_field and surname_field:
- last_name = user_information[surname_field][0]
- first_name = user_information[given_name_field][0]
- elif surname_field:
- common_name_format = askbot_settings.LDAP_COMMON_NAME_FIELD_FORMAT
- common_name = user_information[common_name_field][0]
- first_name, last_name = split_name(common_name, common_name_format)
-
- try:
- user = User.objects.get(username__exact=exact_username)
- # always update user profile to synchronize with ldap server
- user.set_password(password)
- #user.first_name = first_name
- #user.last_name = last_name
- user.email = email
- user.save()
- except User.DoesNotExist:
- # create new user in local db
- user = User()
- user.username = exact_username
- user.set_password(password)#copy password from LDAP locally
- #user.first_name = first_name
- #user.last_name = last_name
- user.email = email
- user.is_staff = False
- 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
- else:
- # Maybe a user created internally (django admin user)
- try:
- user = User.objects.get(username__exact=username)
- if user.check_password(password):
- return user
- else:
- return None
- except User.DoesNotExist:
- return None
-
- except ldap.INVALID_CREDENTIALS, e:
- return None # Will fail login on return of None
- except ldap.LDAPError, e:
- log.error("LDAPError Exception")
- log.exception(e)
- return None
- except Exception, e:
- log.error("Unexpected Exception Occurred")
- log.exception(e)
- return None
-
+LOG = logging.getLogger(__name__)
class AuthBackend(object):
"""Authenticator's authentication backend class
@@ -182,6 +25,8 @@ class AuthBackend(object):
the reason there is only one class - for simplicity of
adding this application to a django project - users only need
to extend the AUTHENTICATION_BACKENDS with a single line
+
+ todo: it is not good to have one giant do all 'authenticate' function
"""
def authenticate(
@@ -221,7 +66,7 @@ class AuthBackend(object):
except User.DoesNotExist:
return None
except User.MultipleObjectsReturned:
- logging.critical(
+ LOG.critical(
('have more than one user with email %s ' +
'he/she will not be able to authenticate with ' +
'the email address in the place of user name') % email_address
@@ -322,7 +167,34 @@ class AuthBackend(object):
return None
elif method == 'ldap':
- user = ldap_authenticate(username, password)
+ user_info = ldap_authenticate(username, password)
+ if user_info['success'] == False:
+ # Maybe a user created internally (django admin user)
+ try:
+ user = User.objects.get(username__exact=username)
+ if user.check_password(password):
+ return user
+ else:
+ return None
+ except User.DoesNotExist:
+ return None
+ else:
+ #load user by association or maybe auto-create one
+ ldap_username = user_info['ldap_username']
+ try:
+ #todo: provider_name is hardcoded - possible conflict
+ assoc = UserAssociation.objects.get(
+ openid_url = ldap_username + '@ldap',
+ provider_name = 'ldap'
+ )
+ user = assoc.user
+ except UserAssociation.DoesNotExist:
+ #email address is required
+ if 'email' in user_info and askbot_settings.LDAP_AUTOCREATE_USERS:
+ assoc = ldap_create_user(user_info)
+ user = assoc.user
+ else:
+ return None
elif method == 'wordpress_site':
try:
diff --git a/askbot/deps/django_authopenid/ldap_auth.py b/askbot/deps/django_authopenid/ldap_auth.py
new file mode 100644
index 00000000..5e650714
--- /dev/null
+++ b/askbot/deps/django_authopenid/ldap_auth.py
@@ -0,0 +1,208 @@
+import logging
+from django.conf import settings as django_settings
+from django.contrib.auth.models import User
+from django.forms import EmailField, ValidationError
+from askbot.conf import settings as askbot_settings
+from askbot.deps.django_authopenid.models import UserAssociation
+from askbot.models.signals import user_registered
+from askbot.utils.loading import load_module
+
+LOG = logging.getLogger(__name__)
+
+def split_name(full_name, name_format):
+ """splits full name into first and last,
+ according to the order given in the name_format parameter"""
+ bits = full_name.strip().split()
+ if len(bits) == 1:
+ bits.push('')
+ elif len(bits) == 0:
+ bits = ['', '']
+
+ if name_format == 'first,last':
+ return bits[0], bits[1]
+ elif name_format == 'last,first':
+ return bits[1], bits[0]
+ else:
+ raise ValueError('Unexpected value of name_format')
+
+
+def ldap_authenticate_default(username, password):
+ """
+ Authenticate using ldap.
+ LDAP parameter setup is described in
+ askbot/doc/source/optional-modules.rst
+ See section about LDAP.
+
+ returns a dict with keys:
+
+ * first_name
+ * last_name
+ * ldap_username
+ * email (optional only if there is valid email)
+ * success - boolean, True if authentication succeeded
+
+ python-ldap must be installed
+ http://pypi.python.org/pypi/python-ldap/2.4.6
+
+ NOTE: if you are planning to implement a custom
+ LDAP authenticate function (python path to which can
+ be provided via setting `ASKBOT_LDAP_AUTHENTICATE`
+ setting in the settings.py file) - implement
+ the function just like this - accepting user name
+ and password and returning dict with the same values.
+ The returned dictionary can contain additional values
+ that you might find useful.
+ """
+ import ldap
+ user_information = None
+ user_info = {}#the return value
+ try:
+ ldap_session = ldap.initialize(askbot_settings.LDAP_URL)
+
+ #set protocol version
+ if askbot_settings.LDAP_PROTOCOL_VERSION == '2':
+ ldap_session.protocol_version = ldap.VERSION2
+ elif askbot_settings.LDAP_PROTOCOL_VERSION == '3':
+ ldap_session.protocol_version = ldap.VERSION3
+ else:
+ raise NotImplementedError('unsupported version of ldap protocol')
+
+ ldap.set_option(ldap.OPT_REFERRALS, 0)
+
+ #set extra ldap options, if given
+ if hasattr(django_settings, 'LDAP_EXTRA_OPTIONS'):
+ options = django_settings.LDAP_EXTRA_OPTIONS
+ for key, value in options:
+ if key.startswith('OPT_'):
+ ldap_key = getattr(ldap, key)
+ ldap.set_option(ldap_key, value)
+ else:
+ raise ValueError('Invalid LDAP option %s' % key)
+
+ #add optional "master" LDAP authentication, if required
+ master_username = getattr(django_settings, 'LDAP_LOGIN_DN', None)
+ master_password = getattr(django_settings, 'LDAP_PASSWORD', None)
+
+ login_name_field = askbot_settings.LDAP_LOGIN_NAME_FIELD
+ base_dn = askbot_settings.LDAP_BASE_DN
+ login_template = login_name_field + '=%s,' + base_dn
+ encoding = askbot_settings.LDAP_ENCODING
+
+ if master_username and master_password:
+ ldap_session.simple_bind_s(
+ master_username.encode(encoding),
+ master_password.encode(encoding)
+ )
+
+ user_filter = askbot_settings.LDAP_USER_FILTER_TEMPLATE % (
+ askbot_settings.LDAP_LOGIN_NAME_FIELD,
+ username
+ )
+
+ email_field = askbot_settings.LDAP_EMAIL_FIELD
+
+ get_attrs = [
+ email_field.encode(encoding),
+ login_name_field.encode(encoding)
+ #str(askbot_settings.LDAP_USERID_FIELD)
+ #todo: here we have a chance to get more data from LDAP
+ #maybe a point for some plugin
+ ]
+
+ common_name_field = askbot_settings.LDAP_COMMON_NAME_FIELD.strip()
+ given_name_field = askbot_settings.LDAP_GIVEN_NAME_FIELD.strip()
+ surname_field = askbot_settings.LDAP_SURNAME_FIELD.strip()
+
+ if given_name_field and surname_field:
+ get_attrs.append(given_name_field.encode(encoding))
+ get_attrs.append(surname_field.encode(encoding))
+ elif common_name_field:
+ get_attrs.append(common_name_field.encode(encoding))
+
+ # search ldap directory for user
+ user_search_result = ldap_session.search_s(
+ askbot_settings.LDAP_BASE_DN.encode(encoding),
+ ldap.SCOPE_SUBTREE,
+ user_filter.encode(encoding),
+ get_attrs
+ )
+ if user_search_result: # User found in LDAP Directory
+ user_dn = user_search_result[0][0]
+ user_information = user_search_result[0][1]
+ ldap_session.simple_bind_s(user_dn, password.encode(encoding)) #raises INVALID_CREDENTIALS
+ ldap_session.unbind_s()
+
+ if given_name_field and surname_field:
+ last_name = user_information.get(surname_field, [''])[0]
+ first_name = user_information.get(given_name_field, [''])[0]
+ elif surname_field:
+ common_name_format = askbot_settings.LDAP_COMMON_NAME_FIELD_FORMAT
+ common_name = user_information.get(common_name_field, [''])[0]
+ first_name, last_name = split_name(common_name, common_name_format)
+
+ user_info = {
+ 'first_name': first_name,
+ 'last_name': last_name,
+ 'ldap_username': user_information[login_name_field][0],
+ 'success': True
+ }
+
+ try:
+ email = user_information.get(email_field, [''])[0]
+ user_info['email'] = EmailField().clean(email)
+ except ValidationError:
+ pass
+ else:
+ user_info['success'] = False
+
+ except ldap.INVALID_CREDENTIALS, e:
+ user_info['success'] = False
+ except ldap.LDAPError, e:
+ LOG.error("LDAPError Exception")
+ LOG.exception(e)
+ user_info['success'] = False
+ except Exception, e:
+ LOG.error("Unexpected Exception Occurred")
+ LOG.exception(e)
+ user_info['success'] = False
+
+ return user_info
+
+
+def ldap_create_user_default(user_info):
+ """takes the result returned by the :func:`ldap_authenticate`
+
+ and returns a :class:`UserAssociation` object
+ """
+ # create new user in local db
+ user = User()
+ user.username = user_info['ldap_username']
+ user.set_unusable_password()
+ user.first_name = user_info['first_name']
+ user.last_name = user_info['last_name']
+ user.email = user_info['email']
+ user.is_staff = False
+ user.is_superuser = False
+ user.is_active = True
+ user.save()
+ user_registered.send(None, user = user)
+ LOG.info('Created New User : [{0}]'.format(user_info['ldap_username']))
+
+ assoc = UserAssociation()
+ assoc.user = user
+ assoc.openid_url = user_info['ldap_username'] + '@ldap'
+ assoc.provider_name = 'ldap'
+ assoc.save()
+ return assoc
+
+LDAP_AUTH_FUNC_PATH = getattr(django_settings, 'LDAP_AUTHENTICATE_FUNCTION', None)
+if LDAP_AUTH_FUNC_PATH:
+ ldap_authenticate = load_module(LDAP_AUTH_FUNC_PATH)
+else:
+ ldap_authenticate = ldap_authenticate_default
+
+LDAP_CREATE_FUNC_PATH = getattr(django_settings, 'LDAP_CREATE_USER_FUNCTION', None)
+if LDAP_CREATE_FUNC_PATH:
+ ldap_create_user = load_module(LDAP_CREATE_FUNC_PATH)
+else:
+ ldap_create_user = ldap_create_user_default
diff --git a/askbot/deps/django_authopenid/views.py b/askbot/deps/django_authopenid/views.py
index 786a34ca..a5dcee57 100644
--- a/askbot/deps/django_authopenid/views.py
+++ b/askbot/deps/django_authopenid/views.py
@@ -34,12 +34,13 @@ import datetime
from django.http import HttpResponseRedirect, get_host, Http404
from django.http import HttpResponse
from django.template import RequestContext, Context
-from django.conf import settings
+from django.conf import settings as django_settings
from askbot.conf import settings as askbot_settings
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.contrib.auth import authenticate
from django.core.urlresolvers import reverse
+from django.forms.util import ErrorList
from django.views.decorators import csrf
from django.utils.encoding import smart_unicode
from django.utils.html import escape
@@ -48,6 +49,9 @@ 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 askbot.deps.django_authopenid.ldap_auth import ldap_create_user
+from askbot.deps.django_authopenid.ldap_auth import ldap_authenticate
+from askbot.utils.loading import load_module
from urlparse import urlparse
from openid.consumer.consumer import Consumer, \
@@ -82,7 +86,7 @@ from askbot.models.signals import user_logged_in, user_registered
def create_authenticated_user_account(
username=None, email=None, password=None,
- user_identifier=None, login_provider_name=None, subscribe=False
+ user_identifier=None, login_provider_name=None
):
"""creates a user account, user association with
the login method and the the default email subscriptions
@@ -104,9 +108,7 @@ def create_authenticated_user_account(
last_used_timestamp = datetime.datetime.now()
).save()
- subscribe_form = askbot_forms.SimpleEmailSubscribeForm(
- {'subscribe': subscribe}
- )
+ subscribe_form = askbot_forms.SimpleEmailSubscribeForm({'subscribe': 'y'})
subscribe_form.full_clean()
logging.debug('saving email feed settings')
subscribe_form.save(user)
@@ -129,7 +131,6 @@ def cleanup_post_register_session(request):
'login_provider_name',
'username',
'email',
- 'subscribe',
'password',
'validation_code'
)
@@ -192,10 +193,10 @@ def ask_openid(
on_failure = on_failure or signin_failure
trust_root = getattr(
- settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/'
+ django_settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/'
)
if xri.identifierScheme(openid_url) == 'XRI' and getattr(
- settings, 'OPENID_DISALLOW_INAMES', False
+ django_settings, 'OPENID_DISALLOW_INAMES', False
):
msg = _("i-names are not supported")
logging.debug('openid failed because i-names are not supported')
@@ -336,14 +337,13 @@ def signin(request, template_name='authopenid/signin.html'):
"""
logging.debug('in signin view')
on_failure = signin_failure
- email_feeds_form = askbot_forms.SimpleEmailSubscribeForm()
#we need a special priority on where to redirect on successful login
#here:
#1) url parameter "next" - if explicitly set
#2) url from django setting LOGIN_REDIRECT_URL
#3) home page of the forum
- login_redirect_url = getattr(settings, 'LOGIN_REDIRECT_URL', None)
+ login_redirect_url = getattr(django_settings, 'LOGIN_REDIRECT_URL', None)
next_url = get_next_url(request, default = login_redirect_url)
logging.debug('next url is %s' % next_url)
@@ -370,18 +370,57 @@ def signin(request, template_name='authopenid/signin.html'):
assert(password_action == 'login')
username = login_form.cleaned_data['username']
password = login_form.cleaned_data['password']
- # will be None if authentication fails
+
user = authenticate(
username=username,
password=password,
method = 'ldap'
)
- if user is not None:
+
+ if user:
login(request, user)
return HttpResponseRedirect(next_url)
else:
- user.message_set.create(_('incorrect user name or password'))
- return HttpResponseRedirect(request.path)
+ #try to login again via LDAP
+ user_info = ldap_authenticate(username, password)
+ if user_info['success']:
+ if askbot_settings.LDAP_AUTOCREATE_USERS:
+ #create new user or
+ user = ldap_create_user(user_info).user
+ user = authenticate(method='force', user_id=user.id)
+ assert(user is not None)
+ login(request, user)
+ return HttpResponseRedirect(next_url)
+ else:
+ #continue with proper registration
+ ldap_username = user_info['ldap_username']
+ request.session['email'] = user_info['email']
+ request.session['ldap_user_info'] = user_info
+ if askbot_settings.AUTOFILL_USER_DATA:
+ request.session['username'] = ldap_username
+ request.session['first_name'] = \
+ user_info['first_name']
+ request.session['last_name'] = \
+ user_info['last_name']
+ return finalize_generic_signin(
+ request,
+ login_provider_name = 'ldap',
+ user_identifier = ldap_username + '@ldap',
+ redirect_url = next_url
+ )
+ else:
+ auth_fail_func_path = getattr(
+ django_settings,
+ 'LDAP_AUTHENTICATE_FAILURE_FUNCTION',
+ None
+ )
+
+ if auth_fail_func_path:
+ auth_fail_func = load_module(auth_fail_func_path)
+ auth_fail_func(user_info, login_form)
+ else:
+ login_form.set_password_login_error()
+ #return HttpResponseRedirect(request.path)
else:
if password_action == 'login':
user = authenticate(
@@ -800,22 +839,21 @@ def finalize_generic_signin(
{'provider': login_provider_name}
request.user.message_set.create(message = msg)
return HttpResponseRedirect(redirect_url)
+ elif user:
+ #login branch
+ login(request, user)
+ logging.debug('login success')
+ return HttpResponseRedirect(redirect_url)
else:
- if user is None:
- #need to register
- request.method = 'GET'#this is not a good thing to do
- #but necessary at the moment to reuse the register()
- #method
- return register(
- request,
- login_provider_name=login_provider_name,
- user_identifier=user_identifier
- )
- else:
- #login branch
- login(request, user)
- logging.debug('login success')
- return HttpResponseRedirect(redirect_url)
+ #need to register
+ request.method = 'GET'#this is not a good thing to do
+ #but necessary at the moment to reuse the register()
+ #method
+ return register(
+ request,
+ login_provider_name=login_provider_name,
+ user_identifier=user_identifier
+ )
@not_authenticated
@csrf.csrf_protect
@@ -826,6 +864,9 @@ def register(request, login_provider_name=None, user_identifier=None):
in which case request.method must ge 'GET'
and login_provider_name and user_identifier arguments must not be None
+ user_identifier will be stored in the UserAssociation as openid_url
+ login_provider_name - as provider_name
+
this function may need to be refactored to simplify the usage pattern
template : authopenid/complete.html
@@ -847,7 +888,6 @@ def register(request, login_provider_name=None, user_identifier=None):
'email': request.session.get('email', ''),
}
)
- email_feeds_form = askbot_forms.SimpleEmailSubscribeForm()
if request.method == 'GET':
assert(login_provider_name is not None)
@@ -870,26 +910,27 @@ def register(request, login_provider_name=None, user_identifier=None):
logging.debug('trying to create new account associated with openid')
register_form = forms.OpenidRegisterForm(request.POST)
- email_feeds_form = askbot_forms.SimpleEmailSubscribeForm(request.POST)
if not register_form.is_valid():
logging.debug('OpenidRegisterForm is INVALID')
- elif not email_feeds_form.is_valid():
- logging.debug('SimpleEmailSubscribeForm is INVALID')
else:
- logging.debug('OpenidRegisterForm and SimpleEmailSubscribeForm are valid')
-
username = register_form.cleaned_data['username']
email = register_form.cleaned_data['email']
- subscribe = email_feeds_form.cleaned_data['subscribe']
- if askbot_settings.REQUIRE_VALID_EMAIL_FOR == 'nothing':
+ if 'ldap_user_info' in request.session:
+ user_info = request.session['ldap_user_info']
+ user = ldap_create_user(user_info).user
+ del request.session['ldap_user_info']
+ login(request, user)
+ cleanup_post_register_session(request)
+ return HttpResponseRedirect(next_url)
+
+ elif askbot_settings.REQUIRE_VALID_EMAIL_FOR == 'nothing':
user = create_authenticated_user_account(
username=username,
email=email,
user_identifier=user_identifier,
login_provider_name=login_provider_name,
- subscribe=subscribe
)
login(request, user)
cleanup_post_register_session(request)
@@ -897,7 +938,6 @@ def register(request, login_provider_name=None, user_identifier=None):
else:
request.session['username'] = username
request.session['email'] = email
- request.session['subscribe'] = subscribe
key = util.generate_random_key()
email = request.session['email']
send_email_key(email, key, handler_url_name='verify_email_and_register')
@@ -921,7 +961,7 @@ def register(request, login_provider_name=None, user_identifier=None):
logging.debug('printing authopenid/complete.html output')
data = {
'openid_register_form': register_form,
- 'email_feeds_form': email_feeds_form,
+ 'default_form_action': django_settings.LOGIN_URL,
'provider':mark_safe(provider_logo),
'username': username,
'email': email,
@@ -957,7 +997,6 @@ def verify_email_and_register(request):
username = request.session['username']
email = request.session['email']
password = request.session.get('password', None)
- subscribe = request.session['subscribe']
user_identifier = request.session.get('user_identifier', None)
login_provider_name = request.session.get('login_provider_name', None)
if password:
@@ -965,7 +1004,6 @@ def verify_email_and_register(request):
username=username,
email=email,
password=password,
- subscribe=subscribe
)
elif user_identifier and login_provider_name:
user = create_authenticated_user_account(
@@ -973,7 +1011,6 @@ def verify_email_and_register(request):
email=email,
user_identifier=user_identifier,
login_provider_name=login_provider_name,
- subscribe=subscribe
)
else:
raise NotImplementedError()
@@ -1014,7 +1051,6 @@ def signup_with_password(request):
logging.debug('request method was %s' % request.method)
if request.method == 'POST':
form = RegisterForm(request.POST)
- email_feeds_form = askbot_forms.SimpleEmailSubscribeForm(request.POST)
#validation outside if to remember form values
logging.debug('validating classic register form')
@@ -1023,25 +1059,19 @@ def signup_with_password(request):
logging.debug('classic register form validated')
else:
logging.debug('classic register form is not valid')
- form2_is_valid = email_feeds_form.is_valid()
- if form2_is_valid:
- logging.debug('email feeds form validated')
- else:
- logging.debug('email feeds form is not valid')
- if form1_is_valid and form2_is_valid:
+
+ if form1_is_valid:
logging.debug('both forms are valid')
next = form.cleaned_data['next']
username = form.cleaned_data['username']
password = form.cleaned_data['password1']
email = form.cleaned_data['email']
- subscribe = email_feeds_form.cleaned_data['subscribe']
if askbot_settings.REQUIRE_VALID_EMAIL_FOR == 'nothing':
user = create_authenticated_user_account(
username=username,
email=email,
password=password,
- subscribe=subscribe
)
login(request, user)
cleanup_post_register_session(request)
@@ -1050,7 +1080,6 @@ def signup_with_password(request):
request.session['username'] = username
request.session['email'] = email
request.session['password'] = password
- request.session['subscribe'] = subscribe
#todo: generate a key and save it in the session
key = util.generate_random_key()
email = request.session['email']
@@ -1072,7 +1101,6 @@ def signup_with_password(request):
'login_provider': provider_name
}
)
- email_feeds_form = askbot_forms.SimpleEmailSubscribeForm()
logging.debug('printing legacy signup form')
major_login_providers = util.get_enabled_major_login_providers()
@@ -1081,7 +1109,6 @@ def signup_with_password(request):
context_data = {
'form': form,
'page_class': 'openid-signin',
- 'email_feeds_form': email_feeds_form,
'major_login_providers': major_login_providers.values(),
'minor_login_providers': minor_login_providers.values(),
'login_form': login_form
@@ -1150,7 +1177,7 @@ def send_email_key(email, key, handler_url_name='user_account_recover'):
}
template = get_template('authopenid/email_validation.txt')
message = template.render(data)
- send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [email])
+ send_mail(subject, message, django_settings.DEFAULT_FROM_EMAIL, [email])
def send_user_new_email_key(user):
user.email_key = util.generate_random_key()
diff --git a/askbot/doc/source/optional-modules.rst b/askbot/doc/source/optional-modules.rst
index 54043c1e..c8f2dba3 100644
--- a/askbot/doc/source/optional-modules.rst
+++ b/askbot/doc/source/optional-modules.rst
@@ -96,22 +96,56 @@ section "LDAP settings"
The parameters are (note that some have pre-set defaults that might work for you)::
-* enable/disable LDAP for password login
+* in Login Provider Settings select "enable local login"
+ - this makes login/password form available
+* enable/disable LDAP for password login -
+ must check that, to connect the login/password form to LDAP flow
+* create accounts automatically or not (``LDAP_AUTOCREATE_USERS``)
* protocol version (``LDAP_PROTOCOL_VERSION``) (version 2 is insecure and deprecated)
* ldap url (``LDAP_URL``)
* base distinguished name, 'dn' in LDAP parlance (``LDAP_BASEDN``)
* user id field name (``LDAP_USERID_FIELD``)
* email field name (``LDAP_EMAIL_FIELD``)
* user name filter template (``LDAP_USERNAME_FILTER_TEMPLATE``)
-* user name filter template - must have two string placeholders.
+ must have two string placeholders.
+* given (first) name field (``LDAP_GIVEN_NAME_FIELD``)
+* surname (last name) field (``LDAP_SURNAME_FIELD``)
+* common name field (``LDAP_COMMON_NAME_FIELD``)
+ either given and surname should be used or common name.
+ All three are not necessary - either first two or common.
+ These fields are used to extract users first and last names.
+* Format of common name (``LDAP_COMMON_NAME_FIELD_FORMAT``)
+ values can be only 'first,last' or 'last,first' - used to
+ extract last and first names from common name
There are three more optional parameters that must go to the ``settings.py`` file::
-* ``LDAP_USER``
+* ``LDAP_LOGIN_DN``
* ``LDAP_PASSWORD``
* ``LDAP_EXTRA_OPTIONS``, a list of two-item tuples - of names and values of
the options. Option names must be upper case strings all starting with ``OPT_``
as described in the `python ldap library documentation <http://www.python-ldap.org/doc/html/ldap.html#options>`_. An often used option is (`OPT_REFERRALS`, 0).
+* ``LDAP_AUTHENTICATE_FUNCTION`` - dotted python path to optional function that
+ can override the default `ldap_authenticate` function. This function allows to
+ completely customize the LDAP login procedure.
+ To see what is expected of this function (input parameters and the return value) -
+ look at the end of the doc string at
+ `askbot.deps.django_authopenid.ldap_auth.ldap_authenticate_default`.
+ One use case for the custom function is determining to which group
+ a user might belong or check any additional access rules that might be
+ stored in your LDAP directory. Another use case - is the case when
+ the default procedure just does not work for you.
+* ``LDAP_AUTHENICATE_FAILURE_FUNCTION`` - python dotted path to an additional function
+ that may be called after a unsuccessful authentication.
+ This function can be used to set custom error messages to the login form.
+ The function should take two parameters (in the following order): user_info, login_form.
+ user_info - is the same dictionary
+ that is returned by the `ldap_authenticate` function.
+* ``LDAP_CREATE_USER_FUNCTION`` - python dotted path to function that will create
+ the ldap user, should actually return a user association object, like
+ ``askbot.deps.django_authopenid.ldap_auth.ldap_create_user_default``.
+ Function takes return value of the ldap authenticate function as a sole parameter.
+
Use these when you have the "directory master passsword" -
for a specific user who can access the rest of the directory,
@@ -124,6 +158,10 @@ you might need to :ref:`debug <debugging>` the settings.
The function to look at is `askbot.deps.django_authopenid.backends.ldap_authenticate`.
If you have problems with LDAP please contact us at support@askbot.com.
+The easiest way to debug - insert ``import pdb; pdb.set_trace()`` line into function
+`askbot.deps.django_authopenid.backends.ldap_authenticate`,
+start the ``runserver`` and step through.
+
Uploaded avatars
================
diff --git a/askbot/forms.py b/askbot/forms.py
index fe344c07..0011210d 100644
--- a/askbot/forms.py
+++ b/askbot/forms.py
@@ -14,6 +14,8 @@ from askbot.utils.forms import NextUrlField, UserNameField
from askbot.mail import extract_first_email_address
from recaptcha_works.fields import RecaptchaField
from askbot.conf import settings as askbot_settings
+from askbot.conf import get_tag_display_filter_strategy_choices
+from tinymce.widgets import TinyMCE
import logging
@@ -272,9 +274,13 @@ class EditorField(forms.CharField):
def __init__(self, *args, **kwargs):
super(EditorField, self).__init__(*args, **kwargs)
- self.required = kwargs.get('required', True)
- self.widget = forms.Textarea(attrs={'id': 'editor'})
- self.label = _('content')
+ self.required = True
+ widget_attrs = {'id': 'editor'}
+ if askbot_settings.EDITOR_TYPE == 'markdown':
+ self.widget = forms.Textarea(attrs=widget_attrs)
+ elif askbot_settings.EDITOR_TYPE == 'tinymce':
+ self.widget = TinyMCE(attrs=widget_attrs)
+ self.label = _('content')
self.help_text = u''
self.initial = ''
@@ -779,7 +785,7 @@ class PostAsSomeoneForm(forms.Form):
'Can create new accounts.'
),
required=False,
- widget=forms.TextInput(attrs={'class': 'tipped-input'})
+ widget=forms.TextInput()
)
post_author_email = forms.CharField(
initial=_('Email address:'),
@@ -853,7 +859,6 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
settings forbids anonymous asking
"""
title = TitleField()
- text = QuestionEditorField()
tags = TagNamesField()
wiki = WikiField()
group_id = forms.IntegerField(required = False, widget = forms.HiddenInput)
@@ -872,6 +877,8 @@ class AskForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, *args, **kwargs):
super(AskForm, self).__init__(*args, **kwargs)
+ #it's important that this field is set up dynamically
+ self.fields['text'] = QuestionEditorField()
#hide ask_anonymously field
if askbot_settings.ALLOW_ASK_ANONYMOUSLY is False:
self.hide_field('ask_anonymously')
@@ -1007,6 +1014,7 @@ class AnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
def __init__(self, *args, **kwargs):
super(AnswerForm, self).__init__(*args, **kwargs)
+ self.fields['text'] = AnswerEditorField()
self.fields['email_notify'].widget.attrs['id'] = \
'question-subscribe-updates'
@@ -1072,10 +1080,8 @@ class RevisionForm(forms.Form):
self.fields['revision'].choices = rev_choices
self.fields['revision'].initial = latest_revision.revision
-
class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
title = TitleField()
- text = QuestionEditorField()
tags = TagNamesField()
summary = SummaryField()
wiki = WikiField()
@@ -1096,6 +1102,8 @@ class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
self.user = kwargs['user']#preserve for superclass
revision = kwargs.pop('revision')
super(EditQuestionForm, self).__init__(*args, **kwargs)
+ #it is important to add this field dynamically
+ self.fields['text'] = QuestionEditorField()
self.fields['title'].initial = revision.title
self.fields['text'].initial = revision.text
self.fields['tags'].initial = revision.tagnames
@@ -1191,15 +1199,15 @@ class EditQuestionForm(PostAsSomeoneForm, PostPrivatelyForm):
self.cleaned_data['stay_anonymous'] = stay_anonymous
return self.cleaned_data
-
class EditAnswerForm(PostAsSomeoneForm, PostPrivatelyForm):
- text = AnswerEditorField()
summary = SummaryField()
wiki = WikiField()
def __init__(self, answer, revision, *args, **kwargs):
self.answer = answer
super(EditAnswerForm, self).__init__(*args, **kwargs)
+ #it is important to add this field dynamically
+ self.fields['text'] = AnswerEditorField()
self.fields['text'].initial = revision.text
self.fields['wiki'].initial = answer.wiki
@@ -1325,11 +1333,14 @@ class EditUserForm(forms.Form):
class TagFilterSelectionForm(forms.ModelForm):
email_tag_filter_strategy = forms.ChoiceField(
- choices=const.TAG_DISPLAY_FILTER_STRATEGY_CHOICES,
- initial=const.EXCLUDE_IGNORED,
- label=_('Choose email tag filter'),
- widget=forms.RadioSelect
+ initial = const.EXCLUDE_IGNORED,
+ label = _('Choose email tag filter'),
+ widget = forms.RadioSelect
)
+ def __init__(self, *args, **kwargs):
+ super(TagFilterSelectionForm, self).__init__(*args, **kwargs)
+ choices = get_tag_display_filter_strategy_choices()
+ self.fields['email_tag_filter_strategy'].choices = choices
class Meta:
model = User
diff --git a/askbot/management/commands/askbot_add_test_content.py b/askbot/management/commands/askbot_add_test_content.py
index 17d3ffd0..ca250339 100644
--- a/askbot/management/commands/askbot_add_test_content.py
+++ b/askbot/management/commands/askbot_add_test_content.py
@@ -14,13 +14,14 @@ NUM_COMMENTS = 20
# karma. This can be calculated dynamically - max of MIN_REP_TO_... settings
INITIAL_REPUTATION = 500
-BAD_STUFF = ''#"<script>alert('hohoho')</script>"
+BAD_STUFF = "<script>alert('hohoho')</script>"
# Defining template inputs.
USERNAME_TEMPLATE = BAD_STUFF + "test_user_%s"
PASSWORD_TEMPLATE = "test_password_%s"
EMAIL_TEMPLATE = "test_user_%s@askbot.org"
-TITLE_TEMPLATE = "Test question title No.%s" + BAD_STUFF
+TITLE_TEMPLATE = "Question No.%s" + BAD_STUFF
+LONG_TITLE_TEMPLATE = TITLE_TEMPLATE + 'a lot more text a lot more text a lot more text '*5
TAGS_TEMPLATE = [BAD_STUFF + "tag-%s-0", BAD_STUFF + "tag-%s-1"] # len(TAGS_TEMPLATE) tags per question
CONTENT_TEMPLATE = BAD_STUFF + """Lorem lean startup ipsum product market fit customer
@@ -99,11 +100,18 @@ class Command(NoArgsCommand):
tags = " ".join([t%user.id for t in TAGS_TEMPLATE])
if i < NUM_QUESTIONS/2:
tags += ' one-tag'
+
+ if i % 2 == 0:
+ question_template = TITLE_TEMPLATE
+ else:
+ question_template = LONG_TITLE_TEMPLATE
+
active_question = user.post_question(
- title = TITLE_TEMPLATE % user.id,
+ title = question_template % user.id,
body_text = CONTENT_TEMPLATE,
tags = tags,
)
+
self.print_if_verbose("Created Question '%s' with tags: '%s'" % (
active_question.thread.title, tags,)
)
diff --git a/askbot/migrations/0133_apply_global_group_to_posts_and_users.py b/askbot/migrations/0133_apply_global_group_to_posts_and_users.py
index 733c4c73..8c14b55d 100644
--- a/askbot/migrations/0133_apply_global_group_to_posts_and_users.py
+++ b/askbot/migrations/0133_apply_global_group_to_posts_and_users.py
@@ -82,7 +82,8 @@ class Migration(DataMigration):
thread.groups.add(group)
done_count += 1
- print 'Added global group to %d threads.\n' % done_count
+ if items.count():
+ print 'Added global group to %d threads.\n' % done_count
post_types = ('question', 'answer')
posts = orm['askbot.Post'].objects.filter(post_type__in=post_types)
@@ -93,7 +94,8 @@ class Migration(DataMigration):
post.groups.add(group)
done_count += 1
- print 'Added global group to %d posts.\n' % done_count
+ if posts.count():
+ print 'Added global group to %d posts.\n' % done_count
comments = orm['askbot.Post'].objects.filter(post_type='comment')
message = 'Copying group information from answers ' +\
@@ -105,7 +107,8 @@ class Migration(DataMigration):
comment.groups.add(*parent_post_groups)
done_count += 1
- print 'Added global group to %d comments.\n' % done_count
+ if comments.count():
+ print 'Added global group to %d comments.\n' % done_count
users = orm['auth.User'].objects.all()
message = 'Adding all users to the global group'
@@ -117,7 +120,8 @@ class Migration(DataMigration):
membership.save()
done_count += 1
- print 'Added global group to %d users.' % done_count
+ if users.count():
+ print 'Added global group to %d users.' % done_count
def backwards(self, orm):
diff --git a/askbot/models/__init__.py b/askbot/models/__init__.py
index 40a71603..549f3277 100644
--- a/askbot/models/__init__.py
+++ b/askbot/models/__init__.py
@@ -2218,6 +2218,18 @@ def user_get_foreign_groups(self):
user_group_ids = self.get_groups().values_list('id', flat = True)
return get_groups().exclude(id__in = user_group_ids)
+def user_get_primary_group(self):
+ """a temporary function - returns ether None or
+ first non-personal non-everyone group
+ works only for one real private group per-person
+ """
+ groups = self.get_groups(private=True)
+ for group in groups:
+ if group.name.startswith('_internal_'):
+ continue
+ return group
+ return None
+
def user_can_make_group_private_posts(self):
"""simplest implementation: user belongs to at least one group"""
return self.get_groups(private=True).count() > 0
@@ -2650,6 +2662,7 @@ User.add_to_class('get_marked_tag_names', user_get_marked_tag_names)
User.add_to_class('get_groups', user_get_groups)
User.add_to_class('get_foreign_groups', user_get_foreign_groups)
User.add_to_class('get_personal_group', user_get_personal_group)
+User.add_to_class('get_primary_group', user_get_primary_group)
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)
diff --git a/askbot/models/post.py b/askbot/models/post.py
index 8d2d05a4..4ae2286e 100644
--- a/askbot/models/post.py
+++ b/askbot/models/post.py
@@ -957,9 +957,8 @@ class Post(models.Model):
user_set_getter(
tag_selections__in = tag_selections
).filter(
+ email_tag_filter_strategy = email_tag_filter_strategy,
notification_subscriptions__in = subscription_records
- ).filter(
- email_tag_filter_strategy = email_tag_filter_strategy
)
)
diff --git a/askbot/models/question.py b/askbot/models/question.py
index bff4bc11..e8ee7a7b 100644
--- a/askbot/models/question.py
+++ b/askbot/models/question.py
@@ -309,11 +309,13 @@ class ThreadManager(BaseQuerySetManager):
user_selections__user = request_user,
user_selections__reason = 'bad'
)
+ subscribed_tags = Tag.objects.none()
if askbot_settings.SUBSCRIBED_TAG_SELECTOR_ENABLED:
- meta_data['subscribed_tag_names'] = Tag.objects.filter(
+ subscribed_tags = Tag.objects.filter(
user_selections__user = request_user,
user_selections__reason = 'subscribed'
- ).values_list('name', flat = True)
+ )
+ meta_data['subscribed_tag_names'] = [tag.name for tag in subscribed_tags]
meta_data['interesting_tag_names'] = [tag.name for tag in interesting_tags]
meta_data['ignored_tag_names'] = [tag.name for tag in ignored_tags]
@@ -336,6 +338,10 @@ class ThreadManager(BaseQuerySetManager):
extra_ignored_tags = Tag.objects.get_by_wildcards(ignored_wildcards)
qs = qs.exclude(tags__in = extra_ignored_tags)
+ if request_user.display_tag_filter_strategy == const.INCLUDE_SUBSCRIBED \
+ and subscribed_tags:
+ qs = qs.filter(tags__in = subscribed_tags)
+
if askbot_settings.USE_WILDCARD_TAGS:
meta_data['interesting_tag_names'].extend(request_user.interesting_tags.split())
meta_data['ignored_tag_names'].extend(request_user.ignored_tags.split())
@@ -497,12 +503,28 @@ class Thread(models.Model):
else:
return self.get_answers(user).count()
- def get_sharing_info(self):
- """returns shared with user and group count"""
+ def get_sharing_info(self, visitor=None):
+ """returns a dictionary with abbreviated thread sharing info:
+ * users - up to a certain number of users, excluding the visitor
+ * groups - up to a certain number of groups
+ * more_users_count - remaining count of shared-with users
+ * more_groups_count - remaining count of shared-with groups
+ """
+ shared_users = self.get_users_shared_with(
+ max_count=2,#"visitor" is implicit
+ exclude_user=visitor
+ )
groups = self.groups
ugroups = groups.filter(name__startswith='_internal_')
ggroups = groups.exclude(name__startswith='_internal_')
- return ugroups.count(), ggroups.count()
+
+ sharing_info = {
+ 'users': shared_users,
+ 'groups': self.get_groups_shared_with(max_count=3),
+ 'more_users_count': max(0, ugroups.count() - 3),
+ 'more_groups_count': max(0, ggroups.count() - 3)
+ }
+ return sharing_info
def get_users_shared_with(self, max_count=None, exclude_user=None):
"""returns query set of users with whom
diff --git a/askbot/setup_templates/settings.py b/askbot/setup_templates/settings.py
index 632c4e70..71f49505 100644
--- a/askbot/setup_templates/settings.py
+++ b/askbot/setup_templates/settings.py
@@ -232,3 +232,29 @@ STATICFILES_DIRS = ( os.path.join(ASKBOT_ROOT, 'skins'),)
RECAPTCHA_USE_SSL = True
+TINYMCE_COMPRESSOR = True
+TINYMCE_SPELLCHECKER = False
+TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'common/media/js/tinymce/')
+TINYMCE_URL = STATIC_URL + 'common/media/js/tinymce/'
+TINYMCE_DEFAULT_CONFIG = {
+ 'plugins': 'askbot_imageuploader,askbot_attachment',
+ 'theme': 'advanced',
+ 'content_css': STATIC_URL + 'default/media/style/tinymce/content.css',
+ 'force_br_newlines': True,
+ 'force_p_newlines': False,
+ 'forced_root_block': '',
+ 'mode' : 'textareas',
+ 'oninit': "function(){ tinyMCE.activeEditor.setContent(askbot['data']['editorContent'] || ''); }",
+ 'plugins': 'askbot_imageuploader,askbot_attachment',
+ 'theme_advanced_toolbar_location' : 'top',
+ 'theme_advanced_toolbar_align': 'left',
+ 'theme_advanced_buttons1': 'bold,italic,underline,|,bullist,numlist,|,undo,redo,|,link,unlink,askbot_imageuploader,askbot_attachment',
+ 'theme_advanced_buttons2': '',
+ 'theme_advanced_buttons3' : '',
+ 'theme_advanced_path': False,
+ 'theme_advanced_resizing': True,
+ 'theme_advanced_resize_horizontal': False,
+ 'theme_advanced_statusbar_location': 'bottom',
+ 'width': '723',
+ 'height': '250'
+}
diff --git a/askbot/setup_templates/settings.py.mustache b/askbot/setup_templates/settings.py.mustache
index 18ac214d..e9d9245e 100644
--- a/askbot/setup_templates/settings.py.mustache
+++ b/askbot/setup_templates/settings.py.mustache
@@ -232,3 +232,30 @@ STATIC_ROOT = os.path.join(PROJECT_ROOT, "static")
STATICFILES_DIRS = (os.path.join(ASKBOT_ROOT, 'skins'),)
RECAPTCHA_USE_SSL = True
+
+TINYMCE_COMPRESSOR = True
+TINYMCE_SPELLCHECKER = False
+TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'common/media/js/tinymce/')
+TINYMCE_URL = STATIC_URL + 'common/media/js/tinymce/'
+TINYMCE_DEFAULT_CONFIG = {
+ 'plugins': 'askbot_imageuploader,askbot_attachment',
+ 'theme': 'advanced',
+ 'content_css': STATIC_URL + 'default/media/style/tinymce/content.css',
+ 'force_br_newlines': True,
+ 'force_p_newlines': False,
+ 'forced_root_block': '',
+ 'mode' : 'textareas',
+ 'oninit': "function(){ tinyMCE.activeEditor.setContent(askbot['data']['editorContent'] || ''); }",
+ 'plugins': 'askbot_imageuploader,askbot_attachment',
+ 'theme_advanced_toolbar_location' : 'top',
+ 'theme_advanced_toolbar_align': 'left',
+ 'theme_advanced_buttons1': 'bold,italic,underline,|,bullist,numlist,|,undo,redo,|,link,unlink,askbot_imageuploader,askbot_attachment',
+ 'theme_advanced_buttons2': '',
+ 'theme_advanced_buttons3' : '',
+ 'theme_advanced_path': False,
+ 'theme_advanced_resizing': True,
+ 'theme_advanced_resize_horizontal': False,
+ 'theme_advanced_statusbar_location': 'bottom',
+ 'width': '723',
+ 'height': '250'
+}
diff --git a/askbot/setup_templates/tinymce_sample_config.py b/askbot/setup_templates/tinymce_sample_config.py
new file mode 100644
index 00000000..c75170b0
--- /dev/null
+++ b/askbot/setup_templates/tinymce_sample_config.py
@@ -0,0 +1,26 @@
+TINYMCE_COMPRESSOR = True
+TINYMCE_SPELLCHECKER = False
+TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, 'common/media/js/tinymce/')
+TINYMCE_URL = STATIC_URL + 'common/media/js/tinymce/'
+TINYMCE_DEFAULT_CONFIG = {
+ 'plugins': 'askbot_imageuploader,askbot_attachment',
+ 'theme': 'advanced',
+ 'content_css': STATIC_URL + 'default/media/style/tinymce/content.css',
+ 'force_br_newlines': True,
+ 'force_p_newlines': False,
+ 'forced_root_block': '',
+ 'mode' : 'textareas',
+ 'oninit': "function(){ tinyMCE.activeEditor.setContent(askbot['data']['editorContent'] || ''); }",
+ 'plugins': 'askbot_imageuploader,askbot_attachment',
+ 'theme_advanced_toolbar_location' : 'top',
+ 'theme_advanced_toolbar_align': 'left',
+ 'theme_advanced_buttons1': 'bold,italic,underline,|,bullist,numlist,|,undo,redo,|,link,unlink,askbot_imageuploader,askbot_attachment',
+ 'theme_advanced_buttons2': '',
+ 'theme_advanced_buttons3' : '',
+ 'theme_advanced_path': False,
+ 'theme_advanced_resizing': True,
+ 'theme_advanced_resize_horizontal': False,
+ 'theme_advanced_statusbar_location': 'bottom',
+ 'width': '723',
+ 'height': '250'
+}
diff --git a/askbot/skins/common/media/js/editor.js b/askbot/skins/common/media/js/editor.js
index ae4f5aea..c6f1c873 100644
--- a/askbot/skins/common/media/js/editor.js
+++ b/askbot/skins/common/media/js/editor.js
@@ -16,65 +16,66 @@ Ajax upload
*/jQuery.extend({createUploadIframe:function(d,b){var a="jUploadFrame"+d;if(window.ActiveXObject){var c=document.createElement('<iframe id="'+a+'" name="'+a+'" />');if(typeof b=="boolean"){c.src="javascript:false"}else{if(typeof b=="string"){c.src=b}}}else{var c=document.createElement("iframe");c.id=a;c.name=a}c.style.position="absolute";c.style.top="-1000px";c.style.left="-1000px";document.body.appendChild(c);return c},createUploadForm:function(g,b){var e="jUploadForm"+g;var a="jUploadFile"+g;var d=$('<form action="" method="POST" name="'+e+'" id="'+e+'" enctype="multipart/form-data"></form>');var c=$("#"+b);var f=$(c).clone();$(c).attr("id",a);$(c).before(f);$(c).appendTo(d);$(d).css("position","absolute");$(d).css("top","-1200px");$(d).css("left","-1200px");$(d).appendTo("body");return d},ajaxFileUpload:function(k){k=jQuery.extend({},jQuery.ajaxSettings,k);var a=new Date().getTime();var b=jQuery.createUploadForm(a,k.fileElementId);var i=jQuery.createUploadIframe(a,k.secureuri);var h="jUploadFrame"+a;var j="jUploadForm"+a;if(k.global&&!jQuery.active++){jQuery.event.trigger("ajaxStart")}var c=false;var f={};if(k.global){jQuery.event.trigger("ajaxSend",[f,k])}var d=function(l){var p=document.getElementById(h);try{if(p.contentWindow){f.responseText=p.contentWindow.document.body?p.contentWindow.document.body.innerText:null;f.responseXML=p.contentWindow.document.XMLDocument?p.contentWindow.document.XMLDocument:p.contentWindow.document}else{if(p.contentDocument){f.responseText=p.contentDocument.document.body?p.contentDocument.document.body.textContent||document.body.innerText:null;f.responseXML=p.contentDocument.document.XMLDocument?p.contentDocument.document.XMLDocument:p.contentDocument.document}}}catch(o){jQuery.handleError(k,f,null,o)}if(f||l=="timeout"){c=true;var m;try{m=l!="timeout"?"success":"error";if(m!="error"){var n=jQuery.uploadHttpData(f,k.dataType);if(k.success){k.success(n,m)}if(k.global){jQuery.event.trigger("ajaxSuccess",[f,k])}}else{jQuery.handleError(k,f,m)}}catch(o){m="error";jQuery.handleError(k,f,m,o)}if(k.global){jQuery.event.trigger("ajaxComplete",[f,k])}if(k.global&&!--jQuery.active){jQuery.event.trigger("ajaxStop")}if(k.complete){k.complete(f,m)}jQuery(p).unbind();setTimeout(function(){try{$(p).remove();$(b).remove()}catch(q){jQuery.handleError(k,f,null,q)}},100);f=null}};if(k.timeout>0){setTimeout(function(){if(!c){d("timeout")}},k.timeout)}try{var b=$("#"+j);$(b).attr("action",k.url);$(b).attr("method","POST");$(b).attr("target",h);if(b.encoding){b.encoding="multipart/form-data"}else{b.enctype="multipart/form-data"}$(b).submit()}catch(g){jQuery.handleError(k,f,null,g)}if(window.attachEvent){document.getElementById(h).attachEvent("onload",d)}else{document.getElementById(h).addEventListener("load",d,false)}return{abort:function(){}}},uploadHttpData:function(r,type){var data=!type;data=type=="xml"||data?r.responseXML:r.responseText;if(type=="script"){jQuery.globalEval(data)}if(type=="json"){eval("data = "+data)}if(type=="html"){jQuery("<div>").html(data).evalScripts()}return data}});
/**
* Upload call. Used only once in the wmd file upload
- * this is "tightly coupled" with the wmd file uploader
- * @param {Object} jquery object imageUrl - where the
- * uploaded url must be inserted on successful upload
- * @param {Function} handler that is run upon change
- * of the file upload field
+ * this is used in the wmd file uploader and the
+ * askbots image and attachment upload plugins
+ * @todo refactor this code to "new style"
*/
-function ajaxFileUpload(imageUrl, startUploadHandler)
-{
- $("#loading").ajaxStart(function(){
- $(this).show();
- }).ajaxComplete(function(){
- $(this).hide();
- });
+function ajaxFileUpload(options) {
- $("#upload").ajaxStart(function(){
- $(this).hide();
- }).ajaxComplete(function(){
- $(this).show();
- });
+ var spinner = options['spinner'];
+ var uploadInputId = options['uploadInputId'];
+ var urlInput = $(options['urlInput']);
+ var startUploadHandler = options['startUploadHandler'];
- $.ajaxFileUpload
- (
- {
- url: askbot['urls']['upload'],
- secureuri:false,
- fileElementId:'file-upload',
- dataType: 'xml',
- 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);
- if (startUploadHandler){
- /* re-install this as the upload extension
- * will remove the handler to prevent double uploading */
- $('#file-upload').change(startUploadHandler);
- }
- }else{
- imageUrl.attr('value', fileURL);
- }
+ spinner.ajaxStart(function(){
+ $(this).show();
+ }).ajaxComplete(function(){
+ $(this).hide();
+ });
- },
- error: function (data, status, e)
- {
- alert(e);
- if (startUploadHandler){
- /* re-install this as the upload extension
- * will remove the handler to prevent double uploading */
- $('#file-upload').change(startUploadHandler);
- }
- }
- }
- )
+ /* important!!! upload input must be loaded by id
+ * because ajaxFileUpload monkey-patches the upload form */
+ $('#' + uploadInputId).ajaxStart(function(){
+ $(this).hide();
+ }).ajaxComplete(function(){
+ $(this).show();
+ });
+ //var localFilePath = upload_input.val();
+ $.ajaxFileUpload({
+ url: askbot['urls']['upload'],
+ secureuri: false,
+ fileElementId: uploadInputId,
+ dataType: 'xml',
+ 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);
+ } else {
+ urlInput.attr('value', fileURL);
+ }
+
+ /* re-install this as the upload extension
+ * will remove the handler to prevent double uploading
+ * this hack is a manipulation around the
+ * ajaxFileUpload jQuery plugin. */
+ $('#' + uploadInputId).unbind('change').change(startUploadHandler);
+ },
+ error: function (data, status, e) {
+ alert(e);
+ if (startUploadHandler){
+ /* re-install this as the upload extension
+ * will remove the handler to prevent double uploading */
+ $('#' + uploadInputId).unbind('change').change(startUploadHandler);
+ }
+ }
+ });
return false;
};
diff --git a/askbot/skins/common/media/js/jquery.ajaxfileupload.js b/askbot/skins/common/media/js/jquery.ajaxfileupload.js
index 75292776..23759c2e 100644
--- a/askbot/skins/common/media/js/jquery.ajaxfileupload.js
+++ b/askbot/skins/common/media/js/jquery.ajaxfileupload.js
@@ -23,8 +23,7 @@ jQuery.extend({
document.body.appendChild(io);
return io;
},
- createUploadForm: function(id, fileElementId)
- {
+ createUploadForm: function(id, fileElementId) {
//create form
var formId = 'jUploadForm' + id;
var fileId = 'jUploadFile' + id;
@@ -62,8 +61,7 @@ jQuery.extend({
if ( s.global )
jQuery.event.trigger("ajaxSend", [xml, s]);
// Wait for a response to come back
- var uploadCallback = function(isTimeout)
- {
+ var uploadCallback = function(isTimeout) {
var io = document.getElementById(frameId);
try {
if(io.contentWindow){
@@ -81,12 +79,10 @@ jQuery.extend({
io.contentDocument.document.XMLDocument : io.contentDocument.document;
}
}
- catch(e)
- {
+ catch(e) {
jQuery.handleError(s, xml, null, e);
}
- if ( xml || isTimeout == "timeout")
- {
+ if ( xml || isTimeout == "timeout") {
requestDone = true;
var status;
try {
@@ -125,16 +121,15 @@ jQuery.extend({
jQuery(io).unbind();
- setTimeout(function()
- { try
- {
+ setTimeout(function() {
+ try {
$(io).remove();
$(form).remove();
- } catch(e) {
- jQuery.handleError(s, xml, null, e);
- }
- }, 100)
+ } catch(e) {
+ jQuery.handleError(s, xml, null, e);
+ }
+ }, 100);
xml = null;
}
}
@@ -145,25 +140,21 @@ jQuery.extend({
if( !requestDone ) uploadCallback( "timeout" );
}, s.timeout);
}
- try
- {
+ try {
// var io = $('#' + frameId);
var form = $('#' + formId);
$(form).attr('action', s.url);
$(form).attr('method', 'POST');
$(form).attr('target', frameId);
- if(form.encoding)
- {
+ if(form.encoding) {
form.encoding = 'multipart/form-data';
}
- else
- {
+ else {
form.enctype = 'multipart/form-data';
}
$(form).submit();
- } catch(e)
- {
+ } catch(e) {
jQuery.handleError(s, xml, null, e);
}
if(window.attachEvent){
diff --git a/askbot/skins/common/media/js/post.js b/askbot/skins/common/media/js/post.js
index 59074e1b..7739e716 100644
--- a/askbot/skins/common/media/js/post.js
+++ b/askbot/skins/common/media/js/post.js
@@ -308,7 +308,7 @@ DraftPost.prototype.getSaveHandler = function() {
return function(save_synchronously) {
if (me.shouldSave()) {
$.ajax({
- type: 'GET',
+ type: 'POST',
cache: false,
dataType: 'json',
async: save_synchronously ? false : true,
@@ -780,7 +780,9 @@ var Vote = function(){
url: askbot['urls']['vote_url_template'].replace('{{QuestionID}}', questionId),
data: { "type": voteType, "postId": postId },
error: handleFail,
- success: function(data){callback(object, voteType, data);}
+ success: function(data) {
+ callback(object, voteType, data);
+ }
});
};
@@ -801,8 +803,8 @@ var Vote = function(){
$("#"+commentLinkIdPrefix+postId).removeClass("comment-link-accepted");
}
else if(data.success == "1"){
- var answers = ('div[id^="'+answerContainerIdPrefix +"']");
- $(answers).removeClass("accepted-answer");
+ var answers = ('div[id^="'+answerContainerIdPrefix +'"]');
+ $(answers).removeClass('accepted-answer');
var commentLinks = ('div[id^="'+answerContainerIdPrefix +'"] div[id^="'+ commentLinkIdPrefix +'"]');
$(commentLinks).removeClass("comment-link-accepted");
@@ -2141,7 +2143,7 @@ QASwapper.prototype.startSwapping = function(){
*/
var WMD = function(){
WrappedElement.call(this);
- this._markdown = undefined;
+ this._text = undefined;
this._enabled_buttons = 'bold italic link blockquote code ' +
'image attachment ol ul heading hr';
this._is_previewer_enabled = true;
@@ -2196,14 +2198,14 @@ WMD.prototype.createDom = function(){
}
};
-WMD.prototype.setMarkdown = function(text){
+WMD.prototype.setText = function(text){
this._markdown = text;
if (this._textarea){
this._textarea.val(text);
}
};
-WMD.prototype.getMarkdown = function(){
+WMD.prototype.getText = function(){
return this._textarea.val();
};
@@ -2295,7 +2297,7 @@ TagWikiEditor.prototype.startActivatingEditor = function(){
cache: false,
success: function(data){
me.backupContent();
- editor.setMarkdown(data);
+ editor.setText(data);
me.setContent(editor.getElement());
me.setState('edit');
if (me.isEditorLoaded() === false){
@@ -2308,7 +2310,7 @@ TagWikiEditor.prototype.startActivatingEditor = function(){
TagWikiEditor.prototype.saveData = function(){
var me = this;
- var text = this._editor.getMarkdown();
+ var text = this._editor.getText();
$.ajax({
type: 'POST',
dataType: 'json',
@@ -2358,7 +2360,11 @@ TagWikiEditor.prototype.decorate = function(element){
this._tag_id = element.attr('id').split('-').pop();
var me = this;
- var editor = new WMD();
+ if (askbot['settings']['editorType'] === 'markdown') {
+ var editor = new WMD();
+ } else {
+ var editor = new TinyMCEWrapper();
+ }
if (this._enabled_editor_buttons){
editor.setEnabledButtons(this._enabled_editor_buttons);
}
@@ -2706,7 +2712,7 @@ TagEditor.prototype.addTag = function(tag_name) {
TagEditor.prototype.immediateClearErrorMessage = function() {
this._error_alert.html('');
this._error_alert.show();
- this._element.css('margin-top', '18px');//todo: the margin thing is a hack
+ //this._element.css('margin-top', '18px');//todo: the margin thing is a hack
}
TagEditor.prototype.clearErrorMessage = function(fade) {
@@ -2727,7 +2733,7 @@ TagEditor.prototype.setErrorMessage = function(text) {
this._error_alert.hide();
this._error_alert.fadeIn(100);
}
- this._element.css('margin-top', '0');//todo: remove this hack
+ //this._element.css('margin-top', '0');//todo: remove this hack
};
TagEditor.prototype.getAddTagHandler = function() {
@@ -2879,7 +2885,7 @@ TagEditor.prototype.decorate = function(element) {
this._element = element;
this._hidden_tags_input = element.find('input[name="tags"]');//this one is hidden
this._tags_container = element.find('ul.tags');
- this._error_alert = $('.tag-editor-error-alert');
+ this._error_alert = $('.tag-editor-error-alert > span');
var me = this;
this._tags_container.children().each(function(idx, elem){
@@ -3282,6 +3288,7 @@ CategoryAdder.prototype.createDom = function() {
var input = this.makeElement('input');
this._input = input;
input.addClass('add-category');
+ input.attr('name', 'add_category');
this._element.append(input);
//add save category button
var save_button = this.makeElement('button');
@@ -3876,8 +3883,6 @@ $(document).ready(function() {
var proxyUserNameInput = $('#id_post_author_username');
var proxyUserEmailInput = $('#id_post_author_email');
if (proxyUserNameInput.length === 1) {
- var tip = new TippedInput();
- tip.decorate(proxyUserNameInput);
var userSelectHandler = function(data) {
proxyUserEmailInput.val(data['data'][0]);
@@ -3886,6 +3891,7 @@ $(document).ready(function() {
var fakeUserAc = new AutoCompleter({
url: '/get-users-info/',//askbot['urls']['get_users_info'],
preloadData: true,
+ promptText: gettext('User name:'),
minChars: 1,
useCache: true,
matchInside: true,
@@ -3893,11 +3899,13 @@ $(document).ready(function() {
delay: 10,
onItemSelect: userSelectHandler
});
+
fakeUserAc.decorate(proxyUserNameInput);
- }
- if (proxyUserEmailInput.length === 1) {
- var tip = new TippedInput();
- tip.decorate(proxyUserEmailInput);
+ if (proxyUserEmailInput.length === 1) {
+ var tip = new TippedInput();
+ tip.decorate(proxyUserEmailInput);
+ }
+
}
//if groups are enabled - activate share functions
var groupsInput = $('#share_group_name');
diff --git a/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js b/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js
new file mode 100644
index 00000000..b1d9a918
--- /dev/null
+++ b/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/editor_plugin.js
@@ -0,0 +1,111 @@
+/**
+ * askbot_attachment.js
+ *
+ * Copyright 2012, Askbot SpA
+ * Released under License.
+ *
+ * License: http://tinymce.moxiecode.com/license
+ * Contributing: http://tinymce.moxiecode.com/contributing
+ */
+
+(function() {
+ var insertIntoDom = function(url, description) {
+ var sel = tinyMCE.activeEditor.selection;
+
+ var content = '<a href="' + url;
+ if (description) {
+ content = content + '" title="' + description;
+ }
+ content = content + '">see attachment</a>';
+
+ sel.setContent(content);
+ };
+
+ var modalMenuHeadline = gettext('Insert a file');
+
+ var createDialog = function() {
+ var dialog = new FileUploadDialog();
+ dialog.setHeadingText(modalMenuHeadline);
+ dialog.setPostUploadHandler(insertIntoDom);
+ dialog.setInputId('askbot_attachment_input');
+ dialog.setUrlInputTooltip(gettext('Or paste file url here'));
+ $(document).append(dialog.getElement());
+ return dialog;
+ };
+
+ var dialog = undefined;
+
+ var getDialog = function() {
+ if (dialog === undefined) {
+ dialog = createDialog();
+ }
+ return dialog;
+ };
+
+ // Load plugin specific language pack
+ tinymce.PluginManager.requireLangPack('askbot_attachment');
+
+ tinymce.create('tinymce.plugins.AskbotAttachmentPlugin', {
+ /**
+ * Initializes the plugin, this will be executed after the plugin has been created.
+ * This call is done before the editor instance has finished it's initialization so use the onInit event
+ * of the editor instance to intercept that event.
+ *
+ * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
+ * @param {string} url Absolute URL to where the plugin is located.
+ */
+ init : function(ed, url) {
+ // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceAskbotAttachment');
+ ed.addCommand('mceAskbotAttachment', function() {
+ //start file uploader modal menu
+ var dialog = getDialog();
+ dialog.show();
+ });
+
+ // Register askbot_attachment button
+ ed.addButton('askbot_attachment', {
+ title : gettext('Insert a file'),
+ cmd : 'mceAskbotAttachment'
+ //image : url + '/img/askbot_leuploader.gif'
+ });
+
+ // Add a node change handler, selects the button in the UI when a image is selected
+ ed.onNodeChange.add(function(ed, cm, n) {
+ cm.setActive('askbot_attachment', n.nodeName == 'IMG');
+ });
+ },
+
+ /**
+ * Creates control instances based in the incomming name. This method is normally not
+ * needed since the addButton method of the tinymce.Editor class is a more easy way of adding buttons
+ * but you sometimes need to create more complex controls like listboxes, split buttons etc then this
+ * method can be used to create those.
+ *
+ * @param {String} n Name of the control to create.
+ * @param {tinymce.ControlManager} cm Control manager to use inorder to create new control.
+ * @return {tinymce.ui.Control} New control instance or null if no control was created.
+ */
+ createControl : function(n, cm) {
+ return null;
+ },
+
+ /**
+ * Returns information about the plugin as a name/value array.
+ * The current keys are longname, author, authorurl, infourl and version.
+ *
+ * @return {Object} Name/value array containing information about the plugin.
+ */
+ getInfo : function() {
+ return {
+ longname : 'AskbotAttachment plugin',
+ author : 'Askbot SpA, Chile',
+ authorurl : 'http://askbot.com',
+ infourl : 'http://github.com/ASKBOT/askbot-devel/',
+ version : '0.1'
+ };
+ }
+ });
+
+ // Register plugin
+ tinymce.PluginManager.add('askbot_attachment', tinymce.plugins.AskbotAttachmentPlugin);
+})();
diff --git a/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en.js b/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en.js
new file mode 100644
index 00000000..d38ad8ea
--- /dev/null
+++ b/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en.js
@@ -0,0 +1,3 @@
+tinyMCE.addI18n('en.askbot_imageupload',{
+ desc : 'Upload an image'
+});
diff --git a/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en_dlg.js b/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en_dlg.js
new file mode 100644
index 00000000..5cd400c1
--- /dev/null
+++ b/askbot/skins/common/media/js/tinymce/plugins/askbot_attachment/langs/en_dlg.js
@@ -0,0 +1,3 @@
+tinyMCE.addI18n('en.askbot_imageupload_dlg',{
+ title : 'Upload an image'
+});
diff --git a/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js b/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js
new file mode 100644
index 00000000..f23d89db
--- /dev/null
+++ b/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/editor_plugin.js
@@ -0,0 +1,111 @@
+/**
+ * askbot_imageuploader.js
+ *
+ * Copyright 2012, Askbot SpA
+ * Released under License.
+ *
+ * License: http://tinymce.moxiecode.com/license
+ * Contributing: http://tinymce.moxiecode.com/contributing
+ */
+
+(function() {
+ var insertIntoDom = function(url, description) {
+ var sel = tinyMCE.activeEditor.selection;
+
+ var content = '<img src="' + url;
+ if (description) {
+ content = content + '" alt="' + description;
+ }
+ content = content + '"/>';
+
+ sel.setContent(content);
+ };
+
+ var modalMenuHeadline = gettext('Upload an image');
+
+ var createDialog = function() {
+ var dialog = new FileUploadDialog();
+ dialog.setHeadingText(modalMenuHeadline);
+ dialog.setPostUploadHandler(insertIntoDom);
+ dialog.setUrlInputTooltip('Or paste image url here');
+ dialog.setInputId('askbot_imageuploader_input');
+ $(document).append(dialog.getElement());
+ return dialog;
+ };
+
+ var dialog = undefined;
+
+ var getDialog = function() {
+ if (dialog === undefined) {
+ dialog = createDialog();
+ }
+ return dialog;
+ };
+
+ // Load plugin specific language pack
+ tinymce.PluginManager.requireLangPack('askbot_imageuploader');
+
+ tinymce.create('tinymce.plugins.AskbotImageUploaderPlugin', {
+ /**
+ * Initializes the plugin, this will be executed after the plugin has been created.
+ * This call is done before the editor instance has finished it's initialization so use the onInit event
+ * of the editor instance to intercept that event.
+ *
+ * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
+ * @param {string} url Absolute URL to where the plugin is located.
+ */
+ init : function(ed, url) {
+ // Register the command so that it can be invoked by using tinyMCE.activeEditor.execCommand('mceAskbotImageUploader');
+ ed.addCommand('mceAskbotImageUploader', function() {
+ //start file uploader modal menu
+ var dialog = getDialog();
+ dialog.show();
+ });
+
+ // Register askbot_imageuploader button
+ ed.addButton('askbot_imageuploader', {
+ title : gettext('Insert image'),
+ cmd : 'mceAskbotImageUploader'
+ //image : url + '/img/askbot_leuploader.gif'
+ });
+
+ // Add a node change handler, selects the button in the UI when a image is selected
+ ed.onNodeChange.add(function(ed, cm, n) {
+ cm.setActive('askbot_imageuploader', n.nodeName == 'IMG');
+ });
+ },
+
+ /**
+ * Creates control instances based in the incomming name. This method is normally not
+ * needed since the addButton method of the tinymce.Editor class is a more easy way of adding buttons
+ * but you sometimes need to create more complex controls like listboxes, split buttons etc then this
+ * method can be used to create those.
+ *
+ * @param {String} n Name of the control to create.
+ * @param {tinymce.ControlManager} cm Control manager to use inorder to create new control.
+ * @return {tinymce.ui.Control} New control instance or null if no control was created.
+ */
+ createControl : function(n, cm) {
+ return null;
+ },
+
+ /**
+ * Returns information about the plugin as a name/value array.
+ * The current keys are longname, author, authorurl, infourl and version.
+ *
+ * @return {Object} Name/value array containing information about the plugin.
+ */
+ getInfo : function() {
+ return {
+ longname : 'AskbotImageUploader plugin',
+ author : 'Askbot SpA, Chile',
+ authorurl : 'http://askbot.com',
+ infourl : 'http://github.com/ASKBOT/askbot-devel/',
+ version : '0.1'
+ };
+ }
+ });
+
+ // Register plugin
+ tinymce.PluginManager.add('askbot_imageuploader', tinymce.plugins.AskbotImageUploaderPlugin);
+})();
diff --git a/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en.js b/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en.js
new file mode 100644
index 00000000..d38ad8ea
--- /dev/null
+++ b/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en.js
@@ -0,0 +1,3 @@
+tinyMCE.addI18n('en.askbot_imageupload',{
+ desc : 'Upload an image'
+});
diff --git a/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en_dlg.js b/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en_dlg.js
new file mode 100644
index 00000000..5cd400c1
--- /dev/null
+++ b/askbot/skins/common/media/js/tinymce/plugins/askbot_imageuploader/langs/en_dlg.js
@@ -0,0 +1,3 @@
+tinyMCE.addI18n('en.askbot_imageupload_dlg',{
+ title : 'Upload an image'
+});
diff --git a/askbot/skins/common/media/js/utils.js b/askbot/skins/common/media/js/utils.js
index b9c80b97..244960ce 100644
--- a/askbot/skins/common/media/js/utils.js
+++ b/askbot/skins/common/media/js/utils.js
@@ -249,20 +249,65 @@ var inherits = function(childCtor, parentCtor) {
childCtor.prototype.constructor = childCtor;
};
-/* wrapper around jQuery object */
+/** wrapper around jQuery object
+ * @constructor
+ * the top level "class" for other elements
+ * I.e. all other things must inherit this class.
+ * For an example of the inheritance pattern,
+ * please see the "TippedInput" below.
+ */
var WrappedElement = function(){
this._element = null;
this._in_document = false;
};
+/* note that we do not call inherits() here
+ * See TippedInput as an example of a subclass
+ */
+
+/**
+ * notice that we use ObjCls.prototype.someMethod = function()
+ * notation - as we use Javascript's prototypal inheritance
+ * explicitly. The point of this is to be able to eventually
+ * use the Closure Compiler
+ */
WrappedElement.prototype.setElement = function(element){
this._element = element;
};
+
+/**
+ * this function must be overridden for any object
+ * what will use "DOM generation" pattern
+ *
+ * Inside this function two things can happen:
+ * 1) dom structure creation
+ * 2) event handlers attached to the dom structure
+ */
WrappedElement.prototype.createDom = function(){
+ /* inside at the very least you must assign
+ * a jQuery object to a parameter called _element
+ */
this._element = $('<div></div>');
};
+
+/**
+ * @param {object} element, a jQuery object wrapping a single
+ * DOM element.
+ *
+ * This function must be overridden in the subclasses
+ * that are used in the "decoration" pattern
+ */
WrappedElement.prototype.decorate = function(element){
this._element = element;
};
+
+/**
+ * This method should not be overridden
+ * Normally you call this method to generate the dom
+ * structure, if applicable, or just obtain the
+ * jQuery object encapsulating the dom.
+ *
+ * @return {object} jQuery
+ */
WrappedElement.prototype.getElement = function(){
if (this._element === null){
this.createDom();
@@ -278,10 +323,23 @@ WrappedElement.prototype.enterDocument = function(){
WrappedElement.prototype.hasElement = function(){
return (this._element !== null);
};
+/**
+ * A utility method, returning a new jQuery object for
+ * some HTML tag
+ *
+ * Example:
+ * var ageInput = this.makeElement('input');
+ */
WrappedElement.prototype.makeElement = function(html_tag){
//makes jQuery element with tags
return $('<' + html_tag + '></' + html_tag + '>');
};
+/**
+ * Removes object's DOM element from the DOM tree
+ * should be overridden to remove the event handlers
+ * and properly destroy the dom structure
+ * as well as any other included sub-elements
+ */
WrappedElement.prototype.dispose = function(){
this._element.remove();
this._in_document = false;
@@ -320,18 +378,47 @@ Widget.prototype.makeButton = function(label, handler) {
* perhaps empty text, the instruction is restored.
* When instruction is shown, class "blank" is present
* in the input/textare element.
+ *
+ * For the usage examples - search for "new TippedInput"
+ * there is at least one example for both - decoration and
+ * the "dom creation" patterns.
+ *
+ * Also - in the FileUploadDialog the TippedInput is used
+ * as a sub-element - the widget composition use case.
*/
var TippedInput = function(){
+ /* the call below is part 1 of the inheritance pattern */
WrappedElement.call(this);
this._instruction = null;
+ this._attrs = {};
+ //this._is_one_shot = false;//if true on starting typing effect is gone
};
inherits(TippedInput, WrappedElement);
+/* the line above is part 2 of the inheritance pattern
+ see definition of the function "inherits" for more details
+*/
+
+/* Below are all the custom methods of the
+ TippedInput class, as well as some required functions
+*/
TippedInput.prototype.reset = function(){
$(this._element).val(this._instruction);
$(this._element).addClass('blank');
};
+/*TippedInput.prototype.setIsOneShot = function(boolValue) {
+ this._is_one_shot = boolValue;
+};*/
+
+TippedInput.prototype.setInstruction = function(text) {
+ this._instruction = text;
+};
+
+TippedInput.prototype.setAttr = function(key, value) {
+ this._attrs[key] = value;
+};
+
TippedInput.prototype.isBlank = function(){
return this.getVal() === this._instruction;
};
@@ -350,13 +437,48 @@ TippedInput.prototype.setVal = function(value){
}
}
};
+/**
+ * Creates the DOM of tipped input from scratch
+ * Notice that there is also a "decorate" method.
+ * At least one - createDom or decorate is required,
+ * depending on the usage.
+ */
+TippedInput.prototype.createDom = function() {
+ this._element = this.makeElement('input');
+ var element = this._element;
+ element.val(this._instruction);
+
+ //here we re-use the decorate call (next method)
+ //to avoid copy-pasting code
+ this.decorate(element);
+};
+/**
+ * Attaches the TippedInput behavior to
+ * a pre-existing <input> element
+ *
+ * decorate() method normally does not create
+ * new dom elements, but it might add some missing elements,
+ * if necessary.
+ *
+ * for example the decorate might be composing inside
+ * a more complex widget, in which case other elements
+ * can be added via a "composition" pattern, or
+ * just "naked dom elements" added to the current widget's element
+ *
+ */
TippedInput.prototype.decorate = function(element){
- this._element = element;
+ this._element = element;//mandatory line
+
+ //part 1 - initialize some values and create dom
+ element.attr(this._attrs);
+
var instruction_text = this.getVal();
this._instruction = instruction_text;
this.reset();
var me = this;
+
+ //part 2 - attach event handlers
$(element).focus(function(){
if (me.isBlank()){
$(element)
@@ -372,7 +494,7 @@ TippedInput.prototype.decorate = function(element){
.addClass('blank');
}
});
- makeKeyHandler(13, function(){
+ makeKeyHandler(27, function(){
$(element).blur();
});
};
@@ -695,8 +817,163 @@ ModalDialog.prototype.createDom = function() {
};
/**
+ * @constructor
+ */
+var FileUploadDialog = function() {
+ ModalDialog.call(this);
+ self._post_upload_handler = undefined;
+};
+inherits(FileUploadDialog, ModalDialog);
+
+FileUploadDialog.prototype.setPostUploadHandler = function(handler) {
+ this._post_upload_handler = handler;
+};
+
+FileUploadDialog.prototype.runPostUploadHandler = function(url, descr) {
+ this._post_upload_handler(url, descr);
+};
+
+FileUploadDialog.prototype.setInputId = function(id) {
+ this._input_id = id;
+};
+
+FileUploadDialog.prototype.getInputId = function() {
+ return this._input_id;
+};
+
+FileUploadDialog.prototype.setUrlInputTooltip = function(text) {
+ this._url_input_tooltip = text;
+};
+
+FileUploadDialog.prototype.getUrl = function() {
+ var url_input = this._url_input;
+ if (url_input.isBlank() === false) {
+ return url_input.getVal();
+ }
+ return '';
+};
+
+//disable description for now
+//FileUploadDialog.prototype.getDescription = function() {
+// return this._description_input.getVal();
+//};
+
+FileUploadDialog.prototype.resetInputs = function() {
+ this._url_input.reset();
+ //this._description_input.reset();
+ this._upload_input.val('');
+};
+
+FileUploadDialog.prototype.show = function() {
+ //hack around the ajaxFileUpload plugin
+ FileUploadDialog.superClass_.show.call(this);
+ var upload_input = this._upload_input;
+ upload_input.unbind('change');
+ //todo: fix this - make event handler reinstall work
+ upload_input.change(this.getStartUploadHandler());
+};
+
+FileUploadDialog.prototype.getStartUploadHandler = function(){
+ /* startUploadHandler is passed in to re-install the event handler
+ * which is removed by the ajaxFileUpload jQuery extension
+ */
+ var spinner = this._spinner;
+ var uploadInputId = this.getInputId();
+ var urlInput = this._url_input;
+ var handler = function() {
+ var options = {
+ 'spinner': spinner,
+ 'uploadInputId': uploadInputId,
+ 'urlInput': urlInput.getElement(),
+ 'startUploadHandler': handler//pass in itself
+ };
+ return ajaxFileUpload(options);
+ };
+ return handler;
+};
+
+FileUploadDialog.prototype.createDom = function() {
+
+ var superClass = FileUploadDialog.superClass_;
+
+ var me = this;
+ superClass.setAcceptHandler.call(this, function(){
+ var url = $.trim(me.getUrl());
+ //var description = me.getDescription();
+ //@todo: have url cleaning code here
+ if (url.length > 0) {
+ me.runPostUploadHandler(url);//, description);
+ me.resetInputs();
+ }
+ me.hide();
+ });
+ superClass.setRejectHandler.call(this, function(){
+ me.resetInputs();
+ me.hide();
+ });
+ superClass.createDom.call(this);
+
+ var form = this.makeElement('form');
+ form.css('margin-bottom', 0);
+ this.prependContent(form);
+
+ // File upload button
+ var upload_input = this.makeElement('input');
+ upload_input.attr({
+ id: this._input_id,
+ type: 'file',
+ name: 'file-upload',
+ //size: 26???
+ });
+ form.append(upload_input);
+ this._upload_input = upload_input;
+ form.append($('<br/>'));
+
+ // The url input text box
+ var url_input = new TippedInput();
+ url_input.setInstruction(this._url_input_tooltip || gettext('Or paste file url here'));
+ var url_input_element = url_input.getElement();
+ url_input_element.css({
+ 'width': '200px',
+ 'display': 'none'
+ });
+ form.append(url_input_element);
+ //form.append($('<br/>'));
+ this._url_input = url_input;
+
+ var label = this.makeElement('label');
+ label.attr('for', this._input_id);
+
+ var types = askbot['settings']['allowedUploadFileTypes'];
+ types = types.join(', ');
+ label.html(gettext('Allowed file types are:') + ' ' + types + '.');
+ form.append(label);
+ form.append($('<br/>'));
+
+ /* //Description input box
+ var descr_input = new TippedInput();
+ descr_input.setInstruction(gettext('Describe the image here'));
+ this.makeElement('input');
+ form.append(descr_input.getElement());
+ form.append($('<br/>'));
+ this._description_input = descr_input;
+ */
+ var spinner = this.makeElement('img');
+ spinner.attr('src', mediaUrl('media/images/indicator.gif'));
+ spinner.css('display', 'none');
+ form.append(spinner);
+ this._spinner = spinner;
+
+ upload_input.change(this.getStartUploadHandler());
+};
+
+/**
* attaches a modal menu with a text editor
* to a link. The modal menu is from bootstrap.js
+ * todo: this should probably be a subclass of ModalDialog,
+ * triggered by a link click, then a whole bunch of methods
+ * would be simply inherited from the modal dialog:
+ * clearMessages, etc.
*/
var TextPropertyEditor = function(){
WrappedElement.call(this);
@@ -720,90 +997,48 @@ TextPropertyEditor.prototype.makeEditor = function(){
if (this._editor) {
return this._editor;
}
- var editor = this.makeElement('div')
- .addClass('modal');
+ var editor = new ModalDialog();
this._editor = editor;
+ editor.setHeadingText(this.getWidgetData()['editor_heading']);
- var header = this.makeElement('div')
- .addClass('modal-header');
- editor.append(header);
-
- var close_link = this.makeElement('div')
- .addClass('close')
- .attr('data-dismiss', 'modal')
- .html('x');
- header.append(close_link);
-
- var title = this.makeElement('h3')
- .html(this.getWidgetData()['editor_heading']);
- header.append(title);
-
- var body = this.makeElement('div')
- .addClass('modal-body');
- editor.append(body);
-
- var textarea = this.makeElement('textarea')
- .addClass('tipped-input blank')
- .val(this.getWidgetData()['help_text']);
- body.append(textarea);
+ //create main content for the editor
+ var textarea = this.makeElement('textarea');
+ textarea.addClass('tipped-input blank');
+ textarea.val(this.getWidgetData()['help_text']);
var tipped_input = new TippedInput();
tipped_input.decorate(textarea);
this._text_input = tipped_input;
- var footer = this.makeElement('div')
- .addClass('modal-footer');
- editor.append(footer);
+ editor.setContent(textarea);
+ //body.append(textarea);
- var save_btn = this.makeElement('button')
- .addClass('btn btn-primary')
- .html(gettext('Save'));
- footer.append(save_btn);
-
- var cancel_btn = this.makeElement('button')
- .addClass('btn cancel')
- .html(gettext('Cancel'));
- footer.append(cancel_btn);
+ editor.setAcceptButtonText(gettext('Save'));
+ editor.setRejectButtonText(gettext('Cancel'));
var me = this;
- setupButtonEventHandlers(save_btn, function(){
+ editor.setAcceptHandler(function(){
me.saveData();
});
- setupButtonEventHandlers(cancel_btn, function(){
- editor.modal('hide');
- });
- editor.modal('hide');
- $(document).append(editor);
+ $(document).append(editor.getElement());
return editor;
};
TextPropertyEditor.prototype.openEditor = function(){
- this._editor.modal('show');
+ this._editor.show();
};
TextPropertyEditor.prototype.clearMessages = function(){
- this._editor.find('.alert').remove();
-};
-
-TextPropertyEditor.prototype.getAlert = function(){
- var box = new AlertBox();
- var modal_body = this._editor.find('.modal-body');
- modal_body.prepend(box.getElement());
- return box;
+ this._editor.clearMessages()
};
TextPropertyEditor.prototype.showAlert = function(text){
- this.clearMessages();
- var box = this.getAlert();
- box.setText(text);
- return box;
+ this._editor.setMessage(text, 'alert');
};
TextPropertyEditor.prototype.showError = function(text){
- var box = this.showAlert(text);
- box.setError(true);
- return box;
+ this._editor.setMessage(text, 'error');
};
TextPropertyEditor.prototype.setText = function(text){
@@ -815,7 +1050,7 @@ TextPropertyEditor.prototype.getText = function(){
};
TextPropertyEditor.prototype.hideDialog = function(){
- this._editor.modal('hide');
+ this._editor.hide();
};
TextPropertyEditor.prototype.startOpeningEditor = function(){
diff --git a/askbot/skins/common/media/js/wmd/wmd.js b/askbot/skins/common/media/js/wmd/wmd.js
index 98af264f..60d6d868 100644
--- a/askbot/skins/common/media/js/wmd/wmd.js
+++ b/askbot/skins/common/media/js/wmd/wmd.js
@@ -357,9 +357,26 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){
upload_input.attr('id', 'file-upload');
upload_input.attr('size', 26);
+ var spinner = $('<img />');
+ spinner.attr('id', 'loading');
+ spinner.attr('src', mediaUrl("media/images/indicator.gif"));
+ spinner.css('display', 'none');
+
var startUploadHandler = function(){
- localUploadFileName = $(this).val();
- return ajaxFileUpload($('#image-url'), startUploadHandler);
+ localUploadFileName = $(this).val();//this is a local var
+ /*
+ * startUploadHandler is passed into the ajaxFileUpload
+ * in order to re-install the onchange handler
+ * because the jquery extension ajaxFileUpload removes the handler
+ */
+ var options = {
+ spinner: spinner,
+ uploadInputId: 'file-upload',
+ urlInput: $(input),
+ startUploadHandler: startUploadHandler
+ };
+ return ajaxFileUpload(options);
+ //$('#image-url'), startUploadHandler);
};
upload_input.change(startUploadHandler);
@@ -367,12 +384,8 @@ util.prompt = function(text, defaultInputText, makeLinkMarkdown, dialogType){
upload_container.append(upload_input);
upload_container.append($('<br/>'));
- var spinner = $('<img />');
- spinner.attr('id', 'loading');
- spinner.attr('src', mediaUrl("media/images/indicator.gif"));
- spinner.css('display', 'none');
-
upload_container.append(spinner);
+
upload_container.css('padding', '5px');
$(form).append(upload_container);
}
diff --git a/askbot/skins/common/templates/authopenid/complete.html b/askbot/skins/common/templates/authopenid/complete.html
index 6408c8b4..c9afedee 100644
--- a/askbot/skins/common/templates/authopenid/complete.html
+++ b/askbot/skins/common/templates/authopenid/complete.html
@@ -14,7 +14,6 @@ parameters:
* username (same as screen name or username in the models, and nickname in openid sreg)
* openid_register_form
* openid_verify_form - not clear what this form is supposed to do, not used for legacy
-* email_feeds_form forum.forms.SimpleEmailSubscribeForm
* openid_username_exists
#}
{% block head %}{% endblock %}
@@ -34,7 +33,7 @@ parameters:
{% elif login_type=='facebook' %}
<form name="fregister" action="" method="POST">{% csrf_token %}
{% else %}
- <form name="fregister" action="{{ settings.LOGIN_URL }}" method="POST">{% csrf_token %}
+ <form name="fregister" action="{{ default_form_action }}" method="POST">{% csrf_token %}
{% endif %}
{{ openid_register_form.next }}
<div class="form-row-vertical">
@@ -57,13 +56,6 @@ anyone, must be valid</i>)
{% endif %}
{{ openid_register_form.email }}
</div>
- <p>{% trans %}<strong>Receive forum updates by email</strong>{% endtrans %}</p>
- <div class='simple-subscribe-options'>
- {{email_feeds_form.subscribe}}
- {% if email_feeds_form.errors %}
- <p class="error">{% trans %}please select one of the options above{% endtrans %}</p>
- {% endif %}
- </div>
<div class="submit-row"><input type="submit" class="submit" name="bnewaccount" value="{% trans %}Signup{% endtrans %}"/></div>
</form>
</div>
diff --git a/askbot/skins/common/templates/authopenid/signin.html b/askbot/skins/common/templates/authopenid/signin.html
index f9c0cfea..c5a5c47f 100644
--- a/askbot/skins/common/templates/authopenid/signin.html
+++ b/askbot/skins/common/templates/authopenid/signin.html
@@ -24,25 +24,25 @@
{% endtrans %}
</div>
{% endif %}
+ {% if not (view_subtype == 'default' and have_buttons) %}
<p id='login-intro'>
- {% if view_subtype == 'default' and have_buttons %}
- {% trans %}Choose your favorite service below to sign in using secure OpenID or similar technology. Your external service password always stays confidential and you don't have to rememeber or create another one.{% endtrans %}
- {% elif view_subtype == 'add_openid' and have_buttons %}
- {% if existing_login_methods %}
- {% trans %}It's a good idea to make sure that your existing login methods still work, or add a new one. Please click any of the icons below to check/change or add new login methods.{% endtrans %}
- {% else %}
- {% trans %}Please add a more permanent login method by clicking one of the icons below, to avoid logging in via email each time.{% endtrans %}
- {% endif %}
- {% elif view_subtype == 'change_openid' and have_buttons %}
- {% if existing_login_methods %}
- {% trans %}Click on one of the icons below to add a new login method or re-validate an existing one.{% endtrans %}
- {% else %}
- {% trans %}You don't have a method to log in right now, please add one or more by clicking any of the icons below.{% endtrans %}
+ {% if view_subtype == 'add_openid' and have_buttons %}
+ {% if existing_login_methods %}
+ {% trans %}It's a good idea to make sure that your existing login methods still work, or add a new one. Please click any of the icons below to check/change or add new login methods.{% endtrans %}
+ {% else %}
+ {% trans %}Please add a more permanent login method by clicking one of the icons below, to avoid logging in via email each time.{% endtrans %}
+ {% endif %}
+ {% elif view_subtype == 'change_openid' and have_buttons %}
+ {% if existing_login_methods %}
+ {% trans %}Click on one of the icons below to add a new login method or re-validate an existing one.{% endtrans %}
+ {% else %}
+ {% trans %}You don't have a method to log in right now, please add one or more by clicking any of the icons below.{% endtrans %}
+ {% endif %}
+ {% elif view_subtype == 'email_sent' %}
+ {% trans %}Please check your email and visit the enclosed link to re-connect to your account{% endtrans %}
{% endif %}
- {% elif view_subtype == 'email_sent' %}
- {% trans %}Please check your email and visit the enclosed link to re-connect to your account{% endtrans %}
- {% endif %}
</p>
+ {% endif %}
{% if openid_error_message %}
<p class="warning">{{ openid_error_message }}</p>
{% endif %}
@@ -97,10 +97,14 @@
{% if have_buttons %}
<p class="hint">{% trans %}(or select another login method above){% endtrans %}</p>
{% endif %}
- {% if login_form.password_login_failed %}
- <p class="error">{% trans %}Login failed, please try again{% endtrans %}</p>
- {% endif %}
<table class="login">
+ {% if login_form.password_login_failed %}
+ <tr>
+ <td colspan="2">
+ <p class="error">{% trans %}Login failed, please try again{% endtrans %}</p>
+ </td>
+ </tr>
+ {% endif %}
<tr>
<td><label for="id_username">{% trans %}Login or email{% endtrans %}</label></td>
<td>{{login_form.username}}</td>
diff --git a/askbot/skins/common/templates/authopenid/signup_with_password.html b/askbot/skins/common/templates/authopenid/signup_with_password.html
index e65cd518..e5a8f633 100644
--- a/askbot/skins/common/templates/authopenid/signup_with_password.html
+++ b/askbot/skins/common/templates/authopenid/signup_with_password.html
@@ -37,15 +37,6 @@ your login details with anyone and having to remember yet another password.{% en
<li><label for="password1_id">{{form.password1.label}}</label>{{form.password1}}{{form.password1.errors}}</li>
<li><label for="password2_id">{{form.password2.label}}</label>{{form.password2}}{{form.password2.errors}}</li>
</ul>
- <p style="margin-top: 10px">
- {% trans %}<strong>Receive periodic updates by email</strong>{% endtrans %}
- </p>
- <div class='simple-subscribe-options'>
- {{email_feeds_form.subscribe}}
- {% if email_feeds_form.errors %}
- <p class="error">{% trans %}please select one of the options above{% endtrans %}</p>
- {% endif %}
- </div>
{% if settings.USE_RECAPTCHA %}
<p class="signup_p">{% trans %}Please read and type in the two words below to help us prevent automated account creation.{% endtrans %}</p>
{{form.recaptcha}}
diff --git a/askbot/skins/common/templates/widgets/edit_post.html b/askbot/skins/common/templates/widgets/edit_post.html
index 2c8af580..89d7f6f3 100644
--- a/askbot/skins/common/templates/widgets/edit_post.html
+++ b/askbot/skins/common/templates/widgets/edit_post.html
@@ -13,7 +13,10 @@
{{ post_form.text }}{# this element is resizable and will be wrapped by js #}
</div>
{% else %}
- <div class="wmd-container"><textarea name="text"></textarea></div>
+ <div class="wmd-container">
+ {{ post_form.media }}
+ {{ post_form.text }}
+ </div>
<script type="text/javascript">
{% if post_html %}
askbot['data']['editorContent'] = '{{ post_html|escapejs }}';
diff --git a/askbot/skins/default/media/bootstrap/css/bootstrap.css b/askbot/skins/default/media/bootstrap/css/bootstrap.css
index 898e657a..e6190005 100644
--- a/askbot/skins/default/media/bootstrap/css/bootstrap.css
+++ b/askbot/skins/default/media/bootstrap/css/bootstrap.css
@@ -1,5 +1,9 @@
/*!
* Bootstrap v2.0.2
+ * This file was modified for Askbot
+ * some styles were deleted, others added at the bottom
+ * of this file. Also some fixes to bootstrap are added
+ * at the bottom of askbot's style.less.
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
@@ -92,14 +96,6 @@ img {
vertical-align: middle;
}
button,
-input,
-select,
-textarea {
- margin: 0;
- font-size: 100%;
- vertical-align: middle;
-}
-button,
input {
*overflow: visible;
line-height: normal;
@@ -130,14 +126,6 @@ textarea {
overflow: auto;
vertical-align: top;
}
-body {
- margin: 0;
- font-family: Arial, sans-serif;
- font-size: 13px;
- line-height: 18px;
- color: #333333;
- background-color: #ffffff;
-}
a {
color: #0088cc;
text-decoration: none;
@@ -894,25 +882,10 @@ legend small {
font-size: 13.5px;
color: #999999;
}
-label,
-input,
-button,
-select,
-textarea {
- font-size: 13px;
- font-weight: normal;
- line-height: 18px;
-}
-input,
-button,
-select,
-textarea {
- font-family: Arial, sans-serif;
-}
label {
- display: block;
+ /*display: block;
margin-bottom: 5px;
- color: #333333;
+ color: #333333;*/
}
input,
textarea,
@@ -928,25 +901,6 @@ label textarea,
label select {
display: block;
}
-input[type="image"],
-input[type="checkbox"],
-input[type="radio"] {
- width: auto;
- height: auto;
- padding: 0;
- margin: 3px 0;
- *margin-top: 0;
- /* IE7 */
-
- line-height: normal;
- cursor: pointer;
- -webkit-border-radius: 0;
- -moz-border-radius: 0;
- border-radius: 0;
- border: 0 \9;
- /* IE9 and down */
-
-}
input[type="image"] {
border: 0;
}
@@ -999,26 +953,6 @@ textarea {
input[type="hidden"] {
display: none;
}
-.radio,
-.checkbox {
- padding-left: 18px;
-}
-.radio input[type="radio"],
-.checkbox input[type="checkbox"] {
- float: left;
- margin-left: -18px;
-}
-.controls > .radio:first-child,
-.controls > .checkbox:first-child {
- padding-top: 5px;
-}
-.radio.inline,
-.checkbox.inline {
- display: inline-block;
- padding-top: 5px;
- margin-bottom: 0;
- vertical-align: middle;
-}
.radio.inline + .radio.inline,
.checkbox.inline + .checkbox.inline {
margin-left: 10px;
@@ -1074,18 +1008,6 @@ select:focus {
.input-xxlarge {
width: 530px;
}
-input[class*="span"],
-select[class*="span"],
-textarea[class*="span"],
-.uneditable-input {
- float: none;
- margin-left: 0;
-}
-input,
-textarea,
-.uneditable-input {
- margin-left: 0;
-}
input.span12, textarea.span12, .uneditable-input.span12 {
width: 930px;
}
@@ -1237,15 +1159,6 @@ select:focus:required:invalid:focus {
.form-actions:after {
clear: both;
}
-.uneditable-input {
- display: block;
- background-color: #ffffff;
- border-color: #eee;
- -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
- -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
- cursor: not-allowed;
-}
:-moz-placeholder {
color: #999999;
}
@@ -1273,17 +1186,6 @@ select:focus:required:invalid:focus {
.input-append {
margin-bottom: 5px;
}
-.input-prepend input,
-.input-append input,
-.input-prepend select,
-.input-append select,
-.input-prepend .uneditable-input,
-.input-append .uneditable-input {
- *margin-left: 0;
- -webkit-border-radius: 0 3px 3px 0;
- -moz-border-radius: 0 3px 3px 0;
- border-radius: 0 3px 3px 0;
-}
.input-prepend input:focus,
.input-append input:focus,
.input-prepend select:focus,
@@ -4280,11 +4182,6 @@ a.thumbnail:hover {
.row-fluid > .span1 {
width: 5.801104972%;
}
- input,
- textarea,
- .uneditable-input {
- margin-left: 0;
- }
input.span12, textarea.span12, .uneditable-input.span12 {
width: 714px;
}
@@ -4633,3 +4530,30 @@ a.thumbnail:hover {
margin-left: 30px;
}
}
+
+/* Modifications for askbot */
+.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;
+}
+.modal-backdrop {
+ z-index: 200000;
+}
+.modal {
+ z-index: 200001;
+}
diff --git a/askbot/skins/default/media/images/attachment.png b/askbot/skins/default/media/images/attachment.png
new file mode 100644
index 00000000..1cb253dc
--- /dev/null
+++ b/askbot/skins/default/media/images/attachment.png
Binary files differ
diff --git a/askbot/skins/default/media/style/style.less b/askbot/skins/default/media/style/style.less
index 90aef77e..a03780c3 100644
--- a/askbot/skins/default/media/style/style.less
+++ b/askbot/skins/default/media/style/style.less
@@ -40,7 +40,8 @@ input, select {
}
input[type="text"].prompt,
-input[type="password"].prompt {
+input[type="password"].prompt,
+input.tipped-input.blank {
font-style: italic;
color: @info-text;
}
@@ -1414,7 +1415,7 @@ ul#related-tags li {
div#question-list {
border-bottom:#f0f0ec 1px solid;
float: none;
- a{
+ a {
line-height:30px;
}
@@ -1423,12 +1424,12 @@ ul#related-tags li {
div#question-list h2 {
font-size: 13px;
padding-bottom: 0;
- color:@link;
- border-top:#f0f0ec 1px solid;
- border-left:#f0f0ec 1px solid;
- height:30px;
- line-height:30px;
- font-weight:normal;
+ color: @link;
+ border-top: #f0f0ec 1px solid;
+ border-left: #f0f0ec 1px solid;
+ min-height: 30px;
+ line-height: 30px;
+ font-weight: normal;
}
div#question-list span {
@@ -1474,6 +1475,7 @@ ul#related-tags li {
}
table.proxy-user-info {
border-spacing: 0px;
+ width: 100%;
.form-item {
float: left;
@@ -1525,6 +1527,44 @@ ul#related-tags li {
font-size:14px;
}
+.groups-input,
+.users-input {
+ width:152px;
+ padding-left:5px;
+ border:#c9c9b5 1px solid;
+ height:25px;
+ font-size: 14px;
+}
+
+.add-groups,
+.add-users {
+ border:0;
+ font-weight:bold;
+ margin-top:-2px;
+ .button-style(27px, 14px);
+ .rounded-corners(4px);
+}
+
+.add-everyone-group {
+ text-align: center;
+ margin: auto;
+ display: block;
+ padding: 0 10px;
+}
+
+.add-groups:hover {
+ .button-style-hover;
+}
+
+#id_user,
+#id_user_author {
+ border:#cce6ec 3px solid;
+ height:25px;
+ padding-left:5px;
+ width:395px;
+ font-size:14px;
+}
+
.title-desc {
color: @info-text;
font-size: 13px;
@@ -2202,6 +2242,7 @@ ul#related-tags li {
.answer{
.vote-buttons {
float:left;
+ margin-top: 10px;
}
}
.accepted-answer {
@@ -2932,10 +2973,6 @@ table.ab-subscr-form {
width: 45em;
}
-table.ab-tag-filter-form {
- width: 45em;
-}
-
.submit-row {
line-height: 30px;
padding-top: 10px;
@@ -2948,10 +2985,17 @@ table.ab-tag-filter-form {
color: red;
}
-.error {
+.error,
+.openid-signin p.error {
color: darkred;
margin: 0;
- font-size: 10px;
+ font-size: 12px;
+ font-weight: bold;
+ text-align: left;
+}
+
+.openid-signin p.error {
+ text-align: center;
}
label.retag-error {
@@ -3431,10 +3475,15 @@ p.signup_p {
margin: 20px 0px 0px 0px;
}
-.simple-subscribe-options ul {
- list-style: none;
- list-style-position: outside;
- margin: 0;
+.simple-subscribe-options {
+ ul {
+ list-style: none;
+ list-style-position: outside;
+ margin: 0;
+ }
+ input {
+ display: inline;
+ }
}
/* a workaround to set link colors correctly */
@@ -3634,6 +3683,12 @@ body.anon.lang-es {
background: #b32f2f;
}
+.question-page .post-update-info a.primary-group-name,
+a.primary-group-name {
+ color: #990E08;
+ font-weight: bold;
+}
+
.users-page {
.wmd-prompt-dialog {
background: #ccc;
@@ -3673,6 +3728,15 @@ img.group-logo {
}
}
+.groups-page #groups-list {
+ th, td {
+ padding-right: 20px;
+ }
+ th {
+ font-weight: bold;
+ }
+}
+
#reject-edit-modal {
input, textarea {
width: 514px;
@@ -3787,12 +3851,16 @@ textarea.tipped-input {
height: 13px;
}
}
- input.new-tags-input {
- border-style: none;
+ input.new-tags-input,
+ input.new-tags-input:focus {
+ border: none;
font-size: 15px;
font-color: @info-text;
line-height: 16px;
margin-top: 9px;
+ -webkit-box-shadow: none;/* undo bootstrap glow */
+ -moz-box-shadow: none;
+ box-shadow: none;
}
}
diff --git a/askbot/skins/default/templates/ask.html b/askbot/skins/default/templates/ask.html
index 4daece9e..5f072577 100644
--- a/askbot/skins/default/templates/ask.html
+++ b/askbot/skins/default/templates/ask.html
@@ -20,6 +20,7 @@
<script type='text/javascript' src='{{"/js/wmd/showdown.js"|media}}'></script>
<script type='text/javascript' src='{{"/js/wmd/wmd.js"|media}}'></script>
{% else %}
+ <script type='text/javascript' src='{{"/js/wmd/showdown.js"|media}}'></script>
{% include "meta/tinymce.html" %}
{% endif %}
<script type='text/javascript'>
diff --git a/askbot/skins/default/templates/ask_by_widget.html b/askbot/skins/default/templates/ask_by_widget.html
new file mode 100644
index 00000000..f700f83a
--- /dev/null
+++ b/askbot/skins/default/templates/ask_by_widget.html
@@ -0,0 +1,16 @@
+{% extends "widget_base.html" %}
+{% block forestyle %}
+{%endblock%}
+
+{%block body%}
+Enter your question
+<form action="." method="POST" accept-charset="utf-8">
+ {% csrf_token %}
+ {{form.title}}
+ {% if form.ask_anonymously %}
+ {{form.ask_anonymously}}
+ {%endif%}
+ <input type="submit" value="Ask your question" />
+</form>
+{{form.errors}}
+{%endblock%}
diff --git a/askbot/skins/default/templates/ask_widget_complete.html b/askbot/skins/default/templates/ask_widget_complete.html
new file mode 100644
index 00000000..82fe570c
--- /dev/null
+++ b/askbot/skins/default/templates/ask_widget_complete.html
@@ -0,0 +1,8 @@
+{% extends "widget_base.html" %}
+{% block forestyle %}
+{%endblock%}
+
+{%block body%}
+<a href="{{question_url}}" >Question posted</a>
+{%endblock%}
+
diff --git a/askbot/skins/default/templates/base.html b/askbot/skins/default/templates/base.html
index da771a41..eaf2261d 100644
--- a/askbot/skins/default/templates/base.html
+++ b/askbot/skins/default/templates/base.html
@@ -1,7 +1,5 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<!-- template base.html -->
<html xmlns="http://www.w3.org/1999/xhtml">
- {% spaceless %}
<head>
<title>{% block title %}{% endblock %} - {{ settings.APP_TITLE|escape }}</title>
{% include "meta/html_head_meta.html" %}
@@ -15,7 +13,6 @@
{{ settings.CUSTOM_HTML_HEAD }}
{% endif %}
</head>
- {% 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" %}
@@ -52,4 +49,3 @@
</script>
</body>
</html>
-<!-- end template base.html -->
diff --git a/askbot/skins/default/templates/groups.html b/askbot/skins/default/templates/groups.html
index 2499ac9f..9c7dac3c 100644
--- a/askbot/skins/default/templates/groups.html
+++ b/askbot/skins/default/templates/groups.html
@@ -26,11 +26,21 @@
</p>
{% endif %}
<table id="groups-list">
- {% for group in groups %}
- <tr>
- {{ macros.user_group(group, groups_membership_info[group.id]) }}
- </tr>
- {% endfor %}
+ <thead>
+ <th>{% trans %}Group{% endtrans %}</th>
+ <th>{% trans %}Number of members{% endtrans %}</th>
+ <th>{% trans %}Description{% endtrans %}</th>
+ </thead>
+ <tbody>
+ {% for group in groups %}
+ <tr>
+ {{ macros.user_group(
+ group, groups_membership_info[group.id], show_count=True
+ )
+ }}
+ </tr>
+ {% endfor %}
+ </tbody>
</table>
{% endblock %}
{% block endjs %}
diff --git a/askbot/skins/default/templates/macros.html b/askbot/skins/default/templates/macros.html
index 6199aebb..122d90c8 100644
--- a/askbot/skins/default/templates/macros.html
+++ b/askbot/skins/default/templates/macros.html
@@ -57,6 +57,7 @@
{{ user_score_and_badge_summary(user, karma_mode = karma_mode, badges_mode = badges_mode) }}<br/>
{{ user_website_link(user) }}
{% endif %}
+ {{ user_primary_group(user) }}
{%- endmacro -%}
{%- macro post_last_updater_and_creator_info(
@@ -212,19 +213,21 @@ poor design of the data or methods on data objects #}
>{{ group.name|escape }}</a>
{%- endmacro -%}
-{%- macro user_group(group, membership_info) -%}
+{%- macro user_group(group, membership_info, show_count=False) -%}
<td>
{{ user_group_link(group) }}
</td>
+ {% if show_count %}
+ <td>{{ group.users_count }}</td>
+ {% endif %}
<td>
<span class="group-description">
{% if group.tag_wiki %}
{{ group.tag_wiki.summary }}
{% endif %}
</span>
- </td>
- <td>
{% if membership_info %}
+ <br/>
{{ group_join_button(
group_id = group.id,
can_join = membership_info['can_join'],
@@ -235,6 +238,16 @@ poor design of the data or methods on data objects #}
</td>
{%- endmacro -%}
+{%- macro user_primary_group(user) -%}
+ {% set group=user.get_primary_group() %}
+ {% if group %}
+ <span class="primary-group-name"><a
+ class="primary-group-name"
+ href="{% url users_by_group group.id, group.name|replace('-', ' ')|slugify %}"
+ >{{ group.name|replace('-', ' ')|escape }}</a></span>
+ {% endif %}
+{%- endmacro -%}
+
{%- macro group_join_button(group_id = None, can_join = False, is_member = False) -%}
{% if can_join %}
<button
diff --git a/askbot/skins/default/templates/meta/bottom_scripts.html b/askbot/skins/default/templates/meta/bottom_scripts.html
index 2a7c85a5..ea763b76 100644
--- a/askbot/skins/default/templates/meta/bottom_scripts.html
+++ b/askbot/skins/default/templates/meta/bottom_scripts.html
@@ -28,9 +28,9 @@
src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"
{% endif %}
></script>
+<script type="text/javascript" src='{{"/bootstrap/js/bootstrap.js"|media}}'></script>
<!-- History.js -->
<script type='text/javascript' src="{{"/js/jquery.history.js"|media }}"></script>
-<script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
<script type='text/javascript' src="{{"/js/utils.js"|media }}"></script>
{% if settings.ENABLE_MATHJAX %}
<script type='text/javascript' src="{{settings.MATHJAX_BASE_URL}}/MathJax.js">
@@ -42,6 +42,7 @@
</script>
{% endif %}
<script type="text/javascript">
+ /*<![CDATA[*/
$(document).ready(function(){
{% if active_tab == 'questions' %}
if (Modernizr.history) {
@@ -80,6 +81,7 @@
notify.show();
{% endif %}
$('abbr.timeago').timeago();
+ /*]]>*/
</script>
{% if settings.USE_CUSTOM_JS %}
<script
diff --git a/askbot/skins/default/templates/meta/html_head_javascript.html b/askbot/skins/default/templates/meta/html_head_javascript.html
index 881244ed..09362baa 100644
--- a/askbot/skins/default/templates/meta/html_head_javascript.html
+++ b/askbot/skins/default/templates/meta/html_head_javascript.html
@@ -1,4 +1,5 @@
-<script src="{{"/js/modernizr.custom.js"|media }}"></script>
+<script type="text/javascript" src="{{"/js/modernizr.custom.js"|media }}"></script>
+<script type="text/javascript" src="{% url django.views.i18n.javascript_catalog %}"></script>
<script type="text/javascript">
var askbot = {};
askbot['data'] = {};
@@ -20,6 +21,14 @@
{% endif %}
askbot['urls'] = {};
askbot['settings'] = {};
+ askbot['settings']['editorType'] = '{{ settings.EDITOR_TYPE }}';
+ {% if settings.ALLOWED_UPLOAD_FILE_TYPES %}
+ askbot['settings']['allowedUploadFileTypes'] = [
+ "{{ settings.ALLOWED_UPLOAD_FILE_TYPES|join('", "')|replace('.','') }}"
+ ];
+ {% else %}
+ askbot['settings']['allowedUploadFileTypes'] = [];
+ {% endif %}
askbot['messages'] = {};
</script>
{# avoid adding javascript here so that pages load faster #}
diff --git a/askbot/skins/default/templates/meta/html_head_stylesheets.html b/askbot/skins/default/templates/meta/html_head_stylesheets.html
index d97b0bff..cadc69e0 100644
--- a/askbot/skins/default/templates/meta/html_head_stylesheets.html
+++ b/askbot/skins/default/templates/meta/html_head_stylesheets.html
@@ -8,10 +8,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="{{'/bootstrap/css/bootstrap.css'|media}}" rel="stylesheet" type="text/css" />
{% if settings.USE_LOCAL_FONTS %}
{% include "meta/fonts.html" %}
{% else %}
- <link href='http://fonts.googleapis.com/css?family=Open+Sans+Condensed:400,700&subset=latin,cyrillic-ext,latin-ext' rel='stylesheet' type='text/css'>
+ <link href='http://fonts.googleapis.com/css?family=Open+Sans+Condensed:400,700&amp;subset=latin,cyrillic-ext,latin-ext' rel='stylesheet' type='text/css' />
{% endif %}
{{ skin.get_extra_css_link() }}
{% if settings.USE_CUSTOM_CSS %}
diff --git a/askbot/skins/default/templates/meta/tinymce.html b/askbot/skins/default/templates/meta/tinymce.html
index 0f12d960..d6b1b7b9 100644
--- a/askbot/skins/default/templates/meta/tinymce.html
+++ b/askbot/skins/default/templates/meta/tinymce.html
@@ -1,31 +1,3 @@
-<script type="text/javascript" src="{{ 'js/tinymce/tiny_mce.js'|media }}" ></script>
-<script type="text/javascript" >
- tinyMCE.init({
- content_css: '{{ "style/tinymce/content.css"|media }}',
- //editor_css: '{{ "style/tinymce/ui.css"|media }}',
- force_br_newlines: true,
- force_p_newlines: false,
- forced_root_block: '',
- mode : 'textareas',
- oninit: function(){
- tinyMCE.activeEditor.setContent(askbot['data']['editorContent'] || '');
- },
- theme : 'advanced',
- theme_advanced_toolbar_location : 'top',
- theme_advanced_toolbar_align: 'left',
- theme_advanced_buttons1 : 'bold,italic,underline,|,bullist,numlist,|,undo,redo,|,link,unlink,image',
- theme_advanced_buttons2 : '',
- theme_advanced_buttons3 : '',
- theme_advanced_path: false,
- theme_advanced_resizing: true,
- theme_advanced_resize_horizontal: false,
- theme_advanced_statusbar_location: 'bottom',
- width: '723',
- height: '250'
- //theme_advanced_font_sizes: "10px,12px,13px,14px,16px,18px,20px",
- //font_size_style_values : "10px,12px,13px,14px,16px,18px,20px",
- });
-</script>
<style type="text/css">
.defaultSkin table.mceLayout,
.defaultSkin table.mceLayout tr.mceFirst td {
@@ -38,4 +10,11 @@
height: 5px;
background: #fff;
}
+ .defaultSkin span.mce_askbot_imageuploader {
+ background-position: -380px 0px;
+ }
+ .defaultSkin span.mce_askbot_attachment {
+ background-image: url({{ '/images/attachment.png'|media }});
+ background-position: 0px 0px;
+ }
</style>
diff --git a/askbot/skins/default/templates/question.html b/askbot/skins/default/templates/question.html
index 7c3ef372..28de61e7 100644
--- a/askbot/skins/default/templates/question.html
+++ b/askbot/skins/default/templates/question.html
@@ -12,6 +12,7 @@
{% endblock %}
{% block forejs %}
<script type="text/javascript">
+ /*<![CDATA[*/
//below is pure cross-browser javascript, no jQuery
(function(){
var data = askbot['data'];
@@ -92,6 +93,9 @@
}
function render_add_comment_button(post_id, extra_comment_count){
var can_add = false;
+ if (data['user_posts'] === undefined) {
+ return;
+ }
{% if user_can_post_comment %}
can_add = true;
{% else %}
@@ -158,6 +162,7 @@
askbot['functions']['renderAddCommentButton'] = render_add_comment_button;
askbot['functions']['renderAddAnswerButton'] = render_add_answer_button;
})();
+ /*]]>*/
</script>
{% endblock %}
{% block content %}
diff --git a/askbot/skins/default/templates/question/new_answer_form.html b/askbot/skins/default/templates/question/new_answer_form.html
index 9c0258f7..76772abf 100644
--- a/askbot/skins/default/templates/question/new_answer_form.html
+++ b/askbot/skins/default/templates/question/new_answer_form.html
@@ -1,6 +1,5 @@
<form
id="fmanswer"
- {% if user == question.author %}style="display:none"{% endif %}
action="{% url answer question.id %}"
method="post"
>{% csrf_token %}
diff --git a/askbot/skins/default/templates/user_profile/user_inbox.html b/askbot/skins/default/templates/user_profile/user_inbox.html
index 635cac32..9f3461f1 100644
--- a/askbot/skins/default/templates/user_profile/user_inbox.html
+++ b/askbot/skins/default/templates/user_profile/user_inbox.html
@@ -103,6 +103,5 @@ inbox_section - forum|flags
setup_inbox();
});
</script>
- <script type="text/javascript" src="{{'/bootstrap/js/bootstrap.js'|media}}" />
<!-- end user_responses.html -->
{% endblock %}
diff --git a/askbot/skins/default/templates/user_profile/user_info.html b/askbot/skins/default/templates/user_profile/user_info.html
index ad460dbc..89f06321 100644
--- a/askbot/skins/default/templates/user_profile/user_info.html
+++ b/askbot/skins/default/templates/user_profile/user_info.html
@@ -56,6 +56,22 @@
<td><b>{{view_user.real_name}}</b></td>
</tr>
{% endif %}
+ {% if settings.GROUPS_ENABLED %}
+ <tr>
+ <td>{% if user_groups %}{% trans %}groups{% endtrans %}{% endif %}</td>
+ <td>
+ <table id="groups-list">
+ {% for group in user_groups %}
+ <tr>
+ {{ macros.user_group(group, groups_membership_info[group.id]) }}
+ </tr>
+ {% endfor %}
+ </table>
+ <div class="clearfix"></div>
+ <a id="add-group">{% trans %}add group{% endtrans %}</a>
+ </td>
+ </div>
+ {% endif %}
<tr>
<td>{% trans %}member since{% endtrans %}</td>
<td><strong>{{ macros.timeago(view_user.date_joined) }}</strong></td>
diff --git a/askbot/skins/default/templates/user_profile/user_stats.html b/askbot/skins/default/templates/user_profile/user_stats.html
index 2ccc277f..dc3d97e0 100644
--- a/askbot/skins/default/templates/user_profile/user_stats.html
+++ b/askbot/skins/default/templates/user_profile/user_stats.html
@@ -7,23 +7,6 @@
{% endblock %}
{% block usercontent %}
{% include "user_profile/user_info.html" %}
- {% if settings.GROUPS_ENABLED %}
- <div id="user-groups">
- <h2>{% trans
- username = view_user.username|escape
- %}{{username}}'s groups{% endtrans %}
- </h2>
- <table id="groups-list">
- {% for group in user_groups %}
- <tr>
- {{ macros.user_group(group, groups_membership_info[group.id]) }}
- </tr>
- {% endfor %}
- </table>
- <div class="clearfix"></div>
- <a id="add-group">{% trans %}add group{% endtrans %}</a>
- </div>
- {% endif %}
<a name="questions"></a>
{% spaceless %}
<h2>{% trans counter=question_count %}<span class="count">{{counter}}</span> Question{% pluralize %}<span class="count">{{counter}}</span> Questions{% endtrans %}</h2>
diff --git a/askbot/skins/default/templates/users.html b/askbot/skins/default/templates/users.html
index 96598b1f..183381d4 100644
--- a/askbot/skins/default/templates/users.html
+++ b/askbot/skins/default/templates/users.html
@@ -2,11 +2,6 @@
{% import "macros.html" as macros %}
<!-- users.html -->
{% block title %}{% spaceless %}{% trans %}Users{% endtrans %}{% endspaceless %}{% endblock %}
-{% block before_css %}
- {% if group and request.user.is_authenticated() and request.user.is_administrator() %}
- <link href="{{'/bootstrap/css/bootstrap.css'|media}}" rel="stylesheet" type="text/css" />
- {% endif %}
-{% endblock %}
{% block forestyle %}
<link rel="stylesheet" type="text/css" href="{{"/js/wmd/wmd.css"|media}}" />
{% endblock %}
@@ -21,7 +16,18 @@
</h1>
<div class="tabBar">
<div class="tabsA">
- <span class="label">{% trans %}Sort by &raquo;{% endtrans %}</span>
+ {% if settings.GROUPS_ENABLED and user_groups %}
+ <span class="label">{% trans %}Select/Sort by &raquo;{% endtrans %}</span>
+ {% for a_group in user_groups %}
+ <a
+ href="{% url users_by_group group_id=a_group.id, group_slug=a_group.name|slugify %}"
+ {% if group.name == a_group.name %}class="on"{% endif %}
+ title="{% trans name=a_group.name|escape %}people in group {{name}}{% endtrans %}"
+ ><span>{{ a_group.name|replace('-',' ')|escape }}</span></a>
+ {% endfor %}
+ {% else %}
+ <span class="label">{% trans %}Sort by &raquo;{% endtrans %}</span>
+ {% endif %}
{% if settings.KARMA_MODE == 'public' %}
<a
id="sort_reputation"
@@ -84,7 +90,6 @@
askbot['urls']['delete_group_logo_url'] = '{% url delete_group_logo %}';
askbot['urls']['join_or_leave_group'] = '{% url join_or_leave_group %}';
</script>
- <script type="text/javascript" src='{{"/bootstrap/js/bootstrap.js"|media}}'></script>
<script type='text/javascript' src='{{"/js/editor.js"|media}}'></script>
<script type='text/javascript' src='{{"/js/wmd/showdown.js"|media}}'></script>
<script type='text/javascript' src='{{"/js/wmd/wmd.js"|media}}'></script>
diff --git a/askbot/skins/default/templates/widgets/answer_edit_tips.html b/askbot/skins/default/templates/widgets/answer_edit_tips.html
index 1c2cdc60..2bb5b256 100644
--- a/askbot/skins/default/templates/widgets/answer_edit_tips.html
+++ b/askbot/skins/default/templates/widgets/answer_edit_tips.html
@@ -22,46 +22,6 @@
</p>
</div>
</div>
-
-<div class="box">
- <h2>{% trans %}Markdown basics{% endtrans %}</h2>
- <ul>
- {% if settings.MARKUP_CODE_FRIENDLY or settings.ENABLE_MATHJAX %}
- <li>
- {% trans %}*italic*{% endtrans %}
- </li>
- <li>
- {% trans %}**bold**{% endtrans %}
- </li>
- {% else %}
- <li>
- {% trans %}*italic* or _italic_{% endtrans %}
- </li>
- <li>
- {% trans %}**bold** or __bold__{% endtrans %}
- </li>
- {% endif %}
- <li>
- <b>{% trans %}link{% endtrans %}</b>:[{% trans %}text{% endtrans %}](http://url.com/ "{% trans %}title{% endtrans %}")
-
- </li>
- <li>
- <b>{% trans %}image{% endtrans %}</b>:![alt {% trans %}text{% endtrans %}](/path/img.jpg "{% trans %}title{% endtrans %}")
-
- </li>
- <li>
- {% trans %}numbered list:{% endtrans %}
- 1. Foo
- 2. Bar
- </li>
- <li>
- {% trans %}basic HTML tags are also supported{% endtrans %}
- </li>
- </ul>
- <p class='info-box-follow-up-links'>
-<!-- will be change to a popup windows
- <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank">{% trans %}learn more about Markdown{% endtrans %} »</a>
--->
- </p>
-</div>
-<!-- end template answer_edit_tips.html -->
+{% if settings.EDITOR_TYPE == 'markdown' %}
+ {% include "widgets/markdown_help.html" %}
+{% endif %}
diff --git a/askbot/skins/default/templates/widgets/markdown_help.html b/askbot/skins/default/templates/widgets/markdown_help.html
new file mode 100644
index 00000000..9816fe26
--- /dev/null
+++ b/askbot/skins/default/templates/widgets/markdown_help.html
@@ -0,0 +1,42 @@
+<div class="box">
+ <h2>{% trans %}Markdown basics{% endtrans %}</h2>
+ <ul>
+ {% if settings.MARKUP_CODE_FRIENDLY or settings.ENABLE_MATHJAX %}
+ <li>
+ {% trans %}*italic*{% endtrans %}
+ </li>
+ <li>
+ {% trans %}**bold**{% endtrans %}
+ </li>
+ {% else %}
+ <li>
+ {% trans %}*italic* or _italic_{% endtrans %}
+ </li>
+ <li>
+ {% trans %}**bold** or __bold__{% endtrans %}
+ </li>
+ {% endif %}
+ <li>
+ <b>{% trans %}link{% endtrans %}</b>:[{% trans %}text{% endtrans %}](http://url.com/ "{% trans %}title{% endtrans %}")
+
+ </li>
+ <li>
+ <b>{% trans %}image{% endtrans %}</b>:![alt {% trans %}text{% endtrans %}](/path/img.jpg "{% trans %}title{% endtrans %}")
+
+ </li>
+ <li>
+ {% trans %}numbered list:{% endtrans %}
+ 1. Foo
+ 2. Bar
+ </li>
+ <li>
+ {% trans %}basic HTML tags are also supported{% endtrans %}
+ </li>
+ </ul>
+ <p class='info-box-follow-up-links'>
+<!-- will be change to a popup windows
+ <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank">{% trans %}learn more about Markdown{% endtrans %} »</a>
+-->
+ </p>
+</div>
+<!-- end template answer_edit_tips.html -->
diff --git a/askbot/skins/default/templates/widgets/question_edit_tips.html b/askbot/skins/default/templates/widgets/question_edit_tips.html
index 842aa491..f60304c1 100644
--- a/askbot/skins/default/templates/widgets/question_edit_tips.html
+++ b/askbot/skins/default/templates/widgets/question_edit_tips.html
@@ -17,47 +17,6 @@
-->
</p>
</div>
-
-<div id="markdownHelp"class="box">
- <h2>{% trans %}Markdown basics{% endtrans %}</h2>
- <ul>
- {% if settings.MARKDUP_CODE_FRIENDLY or settings.ENABLE_MATHJAX %}
- <li>
- {% trans %}*italic*{% endtrans %}
- </li>
- <li>
- {% trans %}**bold**{% endtrans %}
- </li>
- {% else %}
- <li>
- {% trans %}*italic* or _italic_{% endtrans %}
- </li>
- <li>
- {% trans %}**bold** or __bold__{% endtrans %}
- </li>
- {% endif %}
- <li>
- <b>{% trans %}link{% endtrans %}</b>:[{% trans %}text{% endtrans %}](http://url.com/ "{% trans %}title{% endtrans %}")
-
- </li>
-
- <li>
- <b>{% trans %}image{% endtrans %}</b>:![alt {% trans %}text{% endtrans %}](/path/img.jpg "{% trans %}title{% endtrans %}")
-
- </li>
- <li>
- {% trans %}numbered list:{% endtrans %}
- 1. Foo
- 2. Bar
- </li>
- <li>
- {% trans %}basic HTML tags are also supported{% endtrans %}
- </li>
- </ul>
- <p class='info-box-follow-up-links'>
-<!-- will be change to a popup windows
- <a href="http://en.wikipedia.org/wiki/Markdown" target="_blank">{% trans %}learn more about Markdown{% endtrans %} »</a>
--->
- </p>
-</div>
-<!-- end question_edit_tips.html -->
+{% if settings.EDITOR_TYPE == 'markdown' %}
+ {% include "/widgets/markdown_help.html" %}
+{% endif %}
diff --git a/askbot/skins/default/templates/widgets/question_summary.html b/askbot/skins/default/templates/widgets/question_summary.html
index 3c4a9b39..78aa2f0c 100644
--- a/askbot/skins/default/templates/widgets/question_summary.html
+++ b/askbot/skins/default/templates/widgets/question_summary.html
@@ -1,4 +1,4 @@
-{% from "macros.html" import user_country_flag, tag_list_widget, timeago %}
+{% from "macros.html" import user_country_flag, tag_list_widget, timeago, user_primary_group %}
<div class="short-summary{% if extra_class %} {{extra_class}}{% endif %}" id="question-{{question.id}}">
<div class="counts">
<div class="views
@@ -50,9 +50,10 @@
<a href="{% url user_profile thread.last_activity_by.id, thread.last_activity_by.username|slugify %}">{{thread.last_activity_by.username|escape}}</a> {{ user_country_flag(thread.last_activity_by) }}
{#{user_score_and_badge_summary(thread.last_activity_by)}#}
{% endif %}
+ {% if thread.last_activity_by.get_primary_group() %}-{% endif %}
+ {{ user_primary_group(thread.last_activity_by) }}
</div>
</div>
<h2><a title="{{question.summary|escape}}" href="{{ question.get_absolute_url(thread=thread) }}">{{thread.get_title(question)|escape}}</a></h2>
{{ tag_list_widget(thread.get_tag_names(), search_state=search_state) }}
</div>
-
diff --git a/askbot/skins/default/templates/widgets/tag_editor.html b/askbot/skins/default/templates/widgets/tag_editor.html
index 53a73130..8f3fa8cd 100644
--- a/askbot/skins/default/templates/widgets/tag_editor.html
+++ b/askbot/skins/default/templates/widgets/tag_editor.html
@@ -1,6 +1,9 @@
{% import "macros.html" as macros %}
<p style="margin:-18px 0 0 42px; color: brown; font-size: 13px; font-style: italic"
- class="tag-editor-error-alert"></p>
+ class="tag-editor-error-alert"
+>
+ <span for="id_tags" generated="true" class="form-error" style="float: left;"></span>
+</p>
<div class="tag-editor" style="margin-top: 18px">{# there's a hack with the margin in js as well #}
{{ macros.tag_list_widget(
tag_names,
@@ -11,6 +14,7 @@
<input
class="new-tags-input"
type="text"
+ name="new_tags_input"
/>
<div class="clearfix"></div>
<input
diff --git a/askbot/startup_procedures.py b/askbot/startup_procedures.py
index 0fec6d5f..33c33da4 100644
--- a/askbot/startup_procedures.py
+++ b/askbot/startup_procedures.py
@@ -18,6 +18,7 @@ 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 askbot.utils.url_utils import urls_equal
from urlparse import urlparse
PREAMBLE = """\n
@@ -512,6 +513,66 @@ def test_custom_user_profile_tab():
footer = 'Please carefully read about adding a custom user profile tab.'
print_errors(errors, header = header, footer = footer)
+def get_tinymce_sample_config():
+ """returns the sample configuration for TinyMCE
+ as string"""
+ askbot_root = askbot.get_install_directory()
+ file_path = os.path.join(
+ askbot_root, 'setup_templates', 'tinymce_sample_config.py'
+ )
+ config_file = open(file_path, 'r')
+ sample_config = config_file.read()
+ config_file.close()
+ return sample_config
+
+def test_tinymce():
+ """tests the tinymce editor setup"""
+ errors = list()
+ if 'tinymce' not in django_settings.INSTALLED_APPS:
+ errors.append("add 'tinymce', to the INSTALLED_APPS")
+
+ required_attrs = (
+ 'TINYMCE_COMPRESSOR',
+ 'TINYMCE_JS_ROOT',
+ 'TINYMCE_URL',
+ 'TINYMCE_DEFAULT_CONFIG'
+ )
+
+ missing_attrs = list()
+ for attr in required_attrs:
+ if not hasattr(django_settings, attr):
+ missing_attrs.append(attr)
+
+ if missing_attrs:
+ errors.append('add missing settings: %s' % ', '.join(missing_attrs))
+
+ #check compressor setting
+ compressor_on = getattr(django_settings, 'TINYMCE_COMPRESSOR', False)
+ if compressor_on is False:
+ errors.append('add line: TINYMCE_COMPRESSOR = True')
+
+ #check js root setting
+ js_root = getattr(django_settings, 'TINYMCE_JS_ROOT', '')
+ relative_js_path = 'common/media/js/tinymce/'
+ expected_js_root = os.path.join(django_settings.STATIC_ROOT, relative_js_path)
+ if os.path.normpath(js_root) != os.path.normpath(expected_js_root):
+ js_root_template = "add line: TINYMCE_JS_ROOT = os.path.join(STATIC_ROOT, '%s')"
+ errors.append(js_root_template % relative_js_path)
+
+ #check url setting
+ url = getattr(django_settings, 'TINYMCE_URL', '')
+ expected_url = django_settings.STATIC_URL + relative_js_path
+ if urls_equal(url, expected_url) is False:
+ js_url_template = "add line: TINYMCE_URL = STATIC_URL + '%s'"
+ errors.append(js_url_template % relative_js_path)
+
+ if errors:
+ header = 'Please add the tynymce editor configuration ' + \
+ 'to your settings.py file.'
+ footer = 'You might want to use this sample configuration ' + \
+ 'as template:\n\n' + get_tinymce_sample_config()
+ print_errors(errors, header=header, footer=footer)
+
def run_startup_tests():
"""function that runs
all startup tests, mainly checking settings config so far
@@ -526,6 +587,7 @@ def run_startup_tests():
test_middleware()
test_celery()
#test_csrf_cookie_domain()
+ test_tinymce()
test_staticfiles()
test_avatar()
settings_tester = SettingsTester({
diff --git a/askbot/tests/__init__.py b/askbot/tests/__init__.py
index 056320f5..6e43940e 100644
--- a/askbot/tests/__init__.py
+++ b/askbot/tests/__init__.py
@@ -18,3 +18,4 @@ from askbot.tests.reply_by_email_tests import *
from askbot.tests.widget_tests import *
from askbot.tests.category_tree_tests import CategoryTreeTests
from askbot.tests.user_model_tests import UserModelTests
+from askbot.tests.utils_tests import *
diff --git a/askbot/tests/badge_tests.py b/askbot/tests/badge_tests.py
index b66eadcc..0ed4b343 100644
--- a/askbot/tests/badge_tests.py
+++ b/askbot/tests/badge_tests.py
@@ -318,7 +318,7 @@ class BadgeTests(AskbotTestCase):
def test_guru_badge1(self):
self.assert_guru_badge_works('upvote_answer')
- def test_guru_badge1(self):
+ def test_guru_badge2(self):
self.assert_guru_badge_works('accept_best_answer')
def test_necromancer_badge(self):
diff --git a/askbot/tests/utils_tests.py b/askbot/tests/utils_tests.py
new file mode 100644
index 00000000..7f252b69
--- /dev/null
+++ b/askbot/tests/utils_tests.py
@@ -0,0 +1,17 @@
+from django.test import TestCase
+from askbot.utils.url_utils import urls_equal
+
+class UrlUtilsTests(TestCase):
+
+ def tests_urls_equal(self):
+ e = urls_equal
+ self.assertTrue(e('', ''))
+ self.assertTrue(e('', '/', True))
+ self.assertTrue(e('http://cnn.com', 'http://cnn.com/', True))
+
+ self.assertFalse(e('https://cnn.com', 'http://cnn.com'))
+ self.assertFalse(e('http://cnn.com:80', 'http://cnn.com:8000'))
+
+ self.assertTrue(e('http://cnn.com/path', 'http://cnn.com/path/', True))
+ self.assertFalse(e('http://cnn.com/path', 'http://cnn.com/path/'))
+
diff --git a/askbot/tests/widget_tests.py b/askbot/tests/widget_tests.py
index d8aabfa5..98c5a8aa 100644
--- a/askbot/tests/widget_tests.py
+++ b/askbot/tests/widget_tests.py
@@ -47,8 +47,10 @@ class WidgetViewsTests(AskbotTestCase):
session = self.client.session
session['widget_question'] = widget_question_data
session.save()
- response = self.client.get(reverse('ask_by_widget', args=(self.widget.id, )),
- {'action': 'post-after-login'})
+ response = self.client.get(
+ reverse('ask_by_widget', args=(self.widget.id, )),
+ {'action': 'post-after-login'}
+ )
self.assertFalse('widget_question' in self.client.session)
self.assertEquals(response.status_code, 302)
#verify posting question
@@ -141,18 +143,22 @@ class QuestionWidgetViewsTests(AskbotTestCase):
tagnames='test')
#we post 6 questions!
- titles = ('test question 1', 'this is a test',
- 'without the magic word', 'test test test',
- 'test just another test', 'no magic word',
- 'test another', 'I can no believe is a test')
+ titles = (
+ 'test question 1', 'this is a test',
+ 'without the magic word', 'test test test',
+ 'test just another test', 'no magic word',
+ 'test another', 'I can no believe is a test'
+ )
tagnames = 'test foo bar'
for title in titles:
self.post_question(title=title, tags=tagnames)
def test_valid_response(self):
- filter_params = {'title__icontains': self.widget.search_query,
- 'tags__name__in': self.widget.tagnames.split(' ')}
+ filter_params = {
+ 'title__icontains': self.widget.search_query,
+ 'tags__name__in': self.widget.tagnames.split(' ')
+ }
threads = models.Thread.objects.filter(**filter_params)[:5]
diff --git a/askbot/urls.py b/askbot/urls.py
index df3a910a..62ae8b54 100644
--- a/askbot/urls.py
+++ b/askbot/urls.py
@@ -70,7 +70,6 @@ urlpatterns = patterns('',
views.readers.questions,
name='questions'
),
-
# END main page urls
url(
r'^api/get_questions/',
diff --git a/askbot/utils/forms.py b/askbot/utils/forms.py
index 319e9b9d..f607e62b 100644
--- a/askbot/utils/forms.py
+++ b/askbot/utils/forms.py
@@ -74,6 +74,7 @@ class UserNameField(StrippedNonEmptyCharField):
'multiple-taken': _('sorry, we have a serious error - user name is taken by several users'),
'invalid': _('user name can only consist of letters, empty space and underscore'),
'meaningless': _('please use at least some alphabetic characters in the user name'),
+ 'noemail': _('symbol "@" is not allowed')
}
if 'error_messages' in kw:
error_messages.update(kw['error_messages'])
@@ -104,7 +105,16 @@ class UserNameField(StrippedNonEmptyCharField):
except forms.ValidationError:
raise forms.ValidationError(self.error_messages['required'])
- username_regex = re.compile(const.USERNAME_REGEX_STRING, re.UNICODE)
+ username_re_string = const.USERNAME_REGEX_STRING
+ #attention: here we check @ symbol in two places: input and the regex
+ if askbot_settings.ALLOW_EMAIL_ADDRESS_IN_USERNAME is False:
+ if '@' in username:
+ raise forms.ValidationError(self.error_messages['noemail'])
+
+ username_re_string = username_re_string.replace('@', '')
+
+ username_regex = re.compile(username_re_string, re.UNICODE)
+
if self.required and not username_regex.search(username):
raise forms.ValidationError(self.error_messages['invalid'])
if username in self.RESERVED_NAMES:
diff --git a/askbot/utils/url_utils.py b/askbot/utils/url_utils.py
index 6027d096..c58239c5 100644
--- a/askbot/utils/url_utils.py
+++ b/askbot/utils/url_utils.py
@@ -1,3 +1,4 @@
+import os
import urlparse
from django.core.urlresolvers import reverse
from django.conf import settings
@@ -13,6 +14,38 @@ def strip_path(url):
)
)
+def append_trailing_slash(urlpath):
+ """if path is empty - returns slash
+ if not and path does not end with the slash
+ appends it
+ """
+ if urlpath == '':
+ return '/'
+ elif not urlpath.endswith('/'):
+ return urlpath + '/'
+ return urlpath
+
+def urls_equal(url1, url2, ignore_trailing_slash=False):
+ """True, if urls are equal"""
+ purl1 = urlparse.urlparse(url1)
+ purl2 = urlparse.urlparse(url2)
+ if purl1.scheme != purl2.scheme:
+ return False
+
+ if purl1.netloc != purl2.netloc:
+ return False
+
+ if ignore_trailing_slash is True:
+ normfunc = append_trailing_slash
+ else:
+ normfunc = lambda v: v
+
+ if normfunc(purl1.path) != normfunc(purl2.path):
+ return False
+
+ #test remaining items in the parsed url
+ return purl1[3:] == purl2[3:]
+
def get_login_url():
"""returns internal login url if
django_authopenid is used, or
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index c7a4fc26..faa6f3ba 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -30,6 +30,7 @@ import askbot
from askbot import exceptions
from askbot.utils.diff import textDiff as htmldiff
from askbot.forms import AnswerForm, ShowQuestionForm
+from askbot import conf
from askbot import models
from askbot import schedules
from askbot.models.tag import Tag
@@ -39,7 +40,6 @@ from askbot.utils.html import sanitize_html
from askbot.utils.decorators import anonymous_forbidden, ajax_only, get_only
from askbot.search.state_manager import SearchState, DummySearchState
from askbot.templatetags import extra_tags
-import askbot.conf
from askbot.conf import settings as askbot_settings
from askbot.skins.loaders import render_into_skin, get_template #jinja2 template loading enviroment
from askbot.views import context
@@ -211,14 +211,14 @@ def questions(request, **kwargs):
'questions_count' : paginator.count,
'reset_method_count': reset_method_count,
'scope': search_state.scope,
- 'show_sort_by_relevance': askbot.conf.should_show_sort_by_relevance(),
+ 'show_sort_by_relevance': conf.should_show_sort_by_relevance(),
'search_tags' : search_state.tags,
'sort': search_state.sort,
'tab_id' : search_state.sort,
'tags' : related_tags,
'tag_list_type' : tag_list_type,
'font_size' : extra_tags.get_tag_font_size(related_tags),
- 'display_tag_filter_strategy_choices': const.TAG_DISPLAY_FILTER_STRATEGY_CHOICES,
+ 'display_tag_filter_strategy_choices': conf.get_tag_display_filter_strategy_choices(),
'email_tag_filter_strategy_choices': const.TAG_EMAIL_FILTER_STRATEGY_CHOICES,
'update_avatar_data': schedules.should_update_avatar_data(request),
'query_string': search_state.query_string(),
@@ -565,19 +565,6 @@ def question(request, id):#refactor - long subroutine. display question body, an
previous_answer = answer
break
- #shared with ...
- users_count, groups_count = thread.get_sharing_info()
- shared_users = thread.get_users_shared_with(
- max_count=2,#"visitor" is implicit
- exclude_user=request.user
- )
- sharing_info = {
- 'users': shared_users,
- 'groups': thread.get_groups_shared_with(max_count=3),
- 'more_users_count': max(0, users_count - 3),
- 'more_groups_count': max(0, groups_count - 3)
- }
-
data = {
'is_cacheable': False,#is_cacheable, #temporary, until invalidation fix
'long_time': const.LONG_TIME,#"forever" caching
@@ -599,11 +586,13 @@ def question(request, id):#refactor - long subroutine. display question body, an
'similar_threads' : thread.get_similar_threads(),
'language_code': translation.get_language(),
'paginator_context' : paginator_context,
- 'sharing_info': sharing_info,
'show_post': show_post,
'show_comment': show_comment,
'show_comment_position': show_comment_position,
}
+ #shared with ...
+ if askbot_settings.GROUPS_ENABLED:
+ data['sharing_info'] = thread.get_sharing_info()
data.update(context.get_for_tag_editor())
diff --git a/askbot/views/users.py b/askbot/views/users.py
index e06b3092..c5b95390 100644
--- a/askbot/views/users.py
+++ b/askbot/views/users.py
@@ -38,8 +38,8 @@ from askbot import models
from askbot import exceptions
from askbot.models.badges import award_badges_signal
from askbot.models.tag import get_global_group
+from askbot.models.tag import get_groups
from askbot.skins.loaders import render_into_skin
-from askbot.templatetags import extra_tags
from askbot.search.state_manager import SearchState
from askbot.utils import url_utils
from askbot.utils.loading import load_module
@@ -163,6 +163,16 @@ def show_users(request, by_group=False, group_id=None, group_slug=None):
'base_url' : base_url
}
paginator_context = functions.setup_paginator(paginator_data) #
+
+ if askbot_settings.GROUPS_ENABLED:
+ #todo: cleanup this branched code after groups are migrated to auth_group
+ user_groups = get_groups().exclude(name__startswith='_internal_')
+ if len(user_groups) <= 1:
+ assert(user_groups[0].name == askbot_settings.GLOBAL_GROUP_NAME)
+ user_groups = None
+ else:
+ user_groups = None
+
data = {
'active_tab': 'users',
'page_class': 'users-page',
@@ -173,7 +183,8 @@ def show_users(request, by_group=False, group_id=None, group_slug=None):
'paginator_context' : paginator_context,
'group_email_moderation_enabled': group_email_moderation_enabled,
'user_can_join_group': user_can_join_group,
- 'user_is_group_member': user_is_group_member
+ 'user_is_group_member': user_is_group_member,
+ 'user_groups': user_groups
}
return render_into_skin('users.html', data, request)
@@ -1007,7 +1018,7 @@ def groups(request, id = None, slug = None):
)
groups = groups.exclude(name__startswith='_internal_')
-
+ groups = groups.annotate(users_count=Count('user_memberships'))
groups = groups.select_related('group_profile')
user_can_add_groups = request.user.is_authenticated() and \
diff --git a/askbot/views/widgets.py b/askbot/views/widgets.py
index 98fb4643..9bd5e669 100644
--- a/askbot/views/widgets.py
+++ b/askbot/views/widgets.py
@@ -97,8 +97,10 @@ def ask_widget(request, widget_id):
return redirect('ask_by_widget_complete')
else:
request.session['widget_question'] = data_dict
- next_url = '%s?next=%s' % (reverse('widget_signin'),
- reverse('ask_by_widget', args=(widget.id,)))
+ next_url = '%s?next=%s' % (
+ reverse('widget_signin'),
+ reverse('ask_by_widget', args=(widget.id,))
+ )
return redirect(next_url)
else:
if 'widget_question' in request.session and \
diff --git a/askbot/views/writers.py b/askbot/views/writers.py
index 04396cff..a9f1c4a0 100644
--- a/askbot/views/writers.py
+++ b/askbot/views/writers.py
@@ -70,9 +70,8 @@ def upload(request):#ajax upload file to a question or answer
file_name_prefix = request.POST.get('file_name_prefix', '')
if file_name_prefix not in ('', 'group_logo_'):
raise exceptions.PermissionDenied('invalid upload file name prefix')
-
- # check file type
- f = request.FILES['file-upload']
+
+ f = request.FILES['file-upload']#take first file
#todo: extension checking should be replaced with mimetype checking
#and this must be part of the form validation
file_extension = os.path.splitext(f.name)[1].lower()
diff --git a/askbot_requirements.txt b/askbot_requirements.txt
index 7b619c36..1e851490 100644
--- a/askbot_requirements.txt
+++ b/askbot_requirements.txt
@@ -19,3 +19,4 @@ django-recaptcha-works
python-openid
pystache==0.3.1
pytz
+django-tinymce