summaryrefslogtreecommitdiffstats
path: root/forum/deps
diff options
context:
space:
mode:
Diffstat (limited to 'forum/deps')
-rw-r--r--forum/deps/django_authopenid/README5
-rw-r--r--forum/deps/django_authopenid/__init__.py40
-rw-r--r--forum/deps/django_authopenid/admin.py9
-rw-r--r--forum/deps/django_authopenid/forms.py322
-rw-r--r--forum/deps/django_authopenid/middleware.py28
-rw-r--r--forum/deps/django_authopenid/mimeparse.py160
-rw-r--r--forum/deps/django_authopenid/models.py80
-rw-r--r--forum/deps/django_authopenid/urls.py62
-rw-r--r--forum/deps/django_authopenid/util.py135
-rw-r--r--forum/deps/django_authopenid/views.py1192
-rw-r--r--forum/deps/livesettings/README4
-rw-r--r--forum/deps/livesettings/__init__.py16
-rw-r--r--forum/deps/livesettings/forms.py38
-rw-r--r--forum/deps/livesettings/functions.py247
-rw-r--r--forum/deps/livesettings/locale/de/LC_MESSAGES/django.mobin0 -> 706 bytes
-rw-r--r--forum/deps/livesettings/locale/de/LC_MESSAGES/django.po101
-rw-r--r--forum/deps/livesettings/locale/en/LC_MESSAGES/django.mobin0 -> 367 bytes
-rw-r--r--forum/deps/livesettings/locale/en/LC_MESSAGES/django.po100
-rw-r--r--forum/deps/livesettings/locale/es/LC_MESSAGES/django.po0
-rw-r--r--forum/deps/livesettings/locale/fr/LC_MESSAGES/django.mobin0 -> 1621 bytes
-rw-r--r--forum/deps/livesettings/locale/fr/LC_MESSAGES/django.po113
-rw-r--r--forum/deps/livesettings/locale/he/LC_MESSAGES/django.mobin0 -> 1655 bytes
-rw-r--r--forum/deps/livesettings/locale/he/LC_MESSAGES/django.po98
-rw-r--r--forum/deps/livesettings/locale/it/LC_MESSAGES/django.mobin0 -> 1582 bytes
-rw-r--r--forum/deps/livesettings/locale/it/LC_MESSAGES/django.po106
-rw-r--r--forum/deps/livesettings/locale/ko/LC_MESSAGES/django.mobin0 -> 1128 bytes
-rw-r--r--forum/deps/livesettings/locale/ko/LC_MESSAGES/django.po100
-rw-r--r--forum/deps/livesettings/locale/pl/LC_MESSAGES/django.mobin0 -> 1470 bytes
-rw-r--r--forum/deps/livesettings/locale/pl/LC_MESSAGES/django.po97
-rw-r--r--forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.mobin0 -> 1208 bytes
-rw-r--r--forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.po100
-rw-r--r--forum/deps/livesettings/locale/ru/LC_MESSAGES/django.mobin0 -> 1178 bytes
-rw-r--r--forum/deps/livesettings/locale/ru/LC_MESSAGES/django.po85
-rw-r--r--forum/deps/livesettings/locale/sv/LC_MESSAGES/django.mobin0 -> 1481 bytes
-rw-r--r--forum/deps/livesettings/locale/sv/LC_MESSAGES/django.po92
-rw-r--r--forum/deps/livesettings/locale/tr/LC_MESSAGES/django.mobin0 -> 1242 bytes
-rw-r--r--forum/deps/livesettings/locale/tr/LC_MESSAGES/django.po102
-rw-r--r--forum/deps/livesettings/models.py170
-rw-r--r--forum/deps/livesettings/overrides.py55
-rw-r--r--forum/deps/livesettings/signals.py3
-rw-r--r--forum/deps/livesettings/templates/livesettings/_admin_site_views.html15
-rw-r--r--forum/deps/livesettings/templates/livesettings/group_settings.html81
-rw-r--r--forum/deps/livesettings/templates/livesettings/site_settings.html101
-rw-r--r--forum/deps/livesettings/templates/livesettings/text.txt1
-rw-r--r--forum/deps/livesettings/templatetags/__init__.py0
-rw-r--r--forum/deps/livesettings/templatetags/config_tags.py91
-rw-r--r--forum/deps/livesettings/tests.py545
-rw-r--r--forum/deps/livesettings/urls.py7
-rw-r--r--forum/deps/livesettings/utils.py87
-rw-r--r--forum/deps/livesettings/values.py628
-rw-r--r--forum/deps/livesettings/views.py93
51 files changed, 5309 insertions, 0 deletions
diff --git a/forum/deps/django_authopenid/README b/forum/deps/django_authopenid/README
new file mode 100644
index 00000000..67c33d60
--- /dev/null
+++ b/forum/deps/django_authopenid/README
@@ -0,0 +1,5 @@
+this is a forked version of django-authopenid module
+specifically for askbot forum project.
+
+most likely it is not useful for anything else and
+in fact will be phased out in askbot as well
diff --git a/forum/deps/django_authopenid/__init__.py b/forum/deps/django_authopenid/__init__.py
new file mode 100644
index 00000000..ff040ed7
--- /dev/null
+++ b/forum/deps/django_authopenid/__init__.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2007, 2008, Benoît Chesneau
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# * notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# * notice, this list of conditions and the following disclaimer in the
+# * documentation and/or other materials provided with the
+# * distribution. Neither the name of the <ORGANIZATION> nor the names
+# * of its contributors may be used to endorse or promote products
+# * derived from this software without specific prior written
+# * permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""
+Django authentification application to *with openid using django auth contrib/.
+
+This application allow a user to connect to you website with :
+ * legacy account : username/password
+ * openid url
+"""
+
+__version__ = "0.9.4"
diff --git a/forum/deps/django_authopenid/admin.py b/forum/deps/django_authopenid/admin.py
new file mode 100644
index 00000000..fa6fcbb9
--- /dev/null
+++ b/forum/deps/django_authopenid/admin.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from django.contrib import admin
+from forum.deps.django_authopenid.models import UserAssociation
+
+
+class UserAssociationAdmin(admin.ModelAdmin):
+ """User association admin class"""
+admin.site.register(UserAssociation, UserAssociationAdmin) \ No newline at end of file
diff --git a/forum/deps/django_authopenid/forms.py b/forum/deps/django_authopenid/forms.py
new file mode 100644
index 00000000..dd66591e
--- /dev/null
+++ b/forum/deps/django_authopenid/forms.py
@@ -0,0 +1,322 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2007, 2008, Benoît Chesneau
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# * notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# * notice, this list of conditions and the following disclaimer in the
+# * documentation and/or other materials provided with the
+# * distribution. Neither the name of the <ORGANIZATION> nor the names
+# * of its contributors may be used to endorse or promote products
+# * derived from this software without specific prior written
+# * permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+from django import forms
+from django.contrib.auth.models import User
+from django.contrib.auth import authenticate
+from django.utils.translation import ugettext as _
+from django.conf import settings
+from forum.conf import settings as forum_settings
+import types
+import re
+from django.utils.safestring import mark_safe
+from recaptcha_django import ReCaptchaField
+from forum.utils.forms import NextUrlField, UserNameField, UserEmailField, SetPasswordForm
+EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP()
+
+# needed for some linux distributions like debian
+try:
+ from openid.yadis import xri
+except ImportError:
+ from yadis import xri
+
+from forum.utils.forms import clean_next
+from forum.deps.django_authopenid.models import ExternalLoginData
+
+__all__ = ['OpenidSigninForm', 'ClassicLoginForm', 'OpenidVerifyForm',
+ 'OpenidRegisterForm', 'ClassicRegisterForm', 'ChangePasswordForm',
+ 'ChangeEmailForm', 'EmailPasswordForm', 'DeleteForm',
+ 'ChangeOpenidForm']
+
+class OpenidSigninForm(forms.Form):
+ """ signin form """
+ openid_url = forms.CharField(max_length=255, widget=forms.widgets.TextInput(attrs={'class': 'openid-login-input', 'size':80}))
+ next = NextUrlField()
+
+ def clean_openid_url(self):
+ """ test if openid is accepted """
+ if 'openid_url' in self.cleaned_data:
+ openid_url = self.cleaned_data['openid_url']
+ if xri.identifierScheme(openid_url) == 'XRI' and getattr(
+ settings, 'OPENID_DISALLOW_INAMES', False
+ ):
+ raise forms.ValidationError(_('i-names are not supported'))
+ return self.cleaned_data['openid_url']
+
+class ClassicLoginForm(forms.Form):
+ """ legacy account signin form """
+ next = NextUrlField()
+ username = UserNameField(required=False,skip_clean=True)
+ password = forms.CharField(max_length=128,
+ widget=forms.widgets.PasswordInput(attrs={'class':'required login'}),
+ required=False)
+
+ def __init__(self, data=None, files=None, auto_id='id_%s',
+ prefix=None, initial=None):
+ super(ClassicLoginForm, self).__init__(data, files, auto_id,
+ prefix, initial)
+ self.user_cache = None
+
+ def _clean_nonempty_field(self,field):
+ value = None
+ if field in self.cleaned_data:
+ value = str(self.cleaned_data[field]).strip()
+ if value == '':
+ value = None
+ self.cleaned_data[field] = value
+ return value
+
+ def clean_username(self):
+ return self._clean_nonempty_field('username')
+
+ def clean_password(self):
+ return self._clean_nonempty_field('password')
+
+ def clean(self):
+ """
+ this clean function actually cleans username and password
+
+ test if password is valid for this username
+ this is really the "authenticate" function
+ also openid_auth is not an authentication backend
+ since it's written in a way that does not comply with
+ the Django convention
+ """
+
+ error_list = []
+ username = self.cleaned_data['username']
+ password = self.cleaned_data['password']
+
+ self.user_cache = None
+ if username and password:
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ pw_ok = False
+ try:
+ pw_ok = EXTERNAL_LOGIN_APP.api.check_password(username,password)
+ except forms.ValidationError, e:
+ error_list.extend(e.messages)
+ if pw_ok:
+ external_user = ExternalLoginData.objects.get(external_username=username)
+ if external_user.user == None:
+ return self.cleaned_data
+ user = external_user.user
+ openid_logins = user.userassociation_set.all()
+
+ if len(openid_logins) > 0:
+ msg1 = _('Account with this name already exists on the forum')
+ msg2 = _('can\'t have two logins to the same account yet, sorry.')
+ error_list.append(msg1)
+ error_list.append(msg2)
+ self._errors['__all__'] = forms.util.ErrorList(error_list)
+ return self.cleaned_data
+ else:
+ #synchronize password with external login
+ user.set_password(password)
+ user.save()
+ #this auth will always succeed
+ self.user_cache = authenticate(username=user.username,\
+ password=password)
+ else:
+ #keep self.user_cache == None
+ #nothing to do, error message will be set below
+ pass
+ else:
+ self.user_cache = authenticate(username=username, password=password)
+
+ if self.user_cache is None:
+ del self.cleaned_data['username']
+ del self.cleaned_data['password']
+ error_list.insert(0,(_("Please enter valid username and password "
+ "(both are case-sensitive).")))
+ elif self.user_cache.is_active == False:
+ error_list.append(_("This account is inactive."))
+ if len(error_list) > 0:
+ error_list.insert(0,_('Login failed.'))
+ elif password == None and username == None:
+ error_list.append(_('Please enter username and password'))
+ elif password == None:
+ error_list.append(_('Please enter your password'))
+ elif username == None:
+ error_list.append(_('Please enter user name'))
+ if len(error_list) > 0:
+ self._errors['__all__'] = forms.util.ErrorList(error_list)
+ return self.cleaned_data
+
+ def get_user(self):
+ """ get authenticated user """
+ return self.user_cache
+
+
+class OpenidRegisterForm(forms.Form):
+ """ openid signin form """
+ next = NextUrlField()
+ username = UserNameField()
+ email = UserEmailField()
+
+class OpenidVerifyForm(forms.Form):
+ """ openid verify form (associate an openid with an account) """
+ next = NextUrlField()
+ username = UserNameField(must_exist=True)
+ password = forms.CharField(max_length=128,
+ widget=forms.widgets.PasswordInput(attrs={'class':'required login'}))
+
+ def __init__(self, data=None, files=None, auto_id='id_%s',
+ prefix=None, initial=None):
+ super(OpenidVerifyForm, self).__init__(data, files, auto_id,
+ prefix, initial)
+ self.user_cache = None
+
+ def clean_password(self):
+ """ test if password is valid for this user """
+ if 'username' in self.cleaned_data and \
+ 'password' in self.cleaned_data:
+ self.user_cache = authenticate(
+ username = self.cleaned_data['username'],
+ password = self.cleaned_data['password']
+ )
+ if self.user_cache is None:
+ raise forms.ValidationError(_("Please enter a valid \
+ username and password. Note that both fields are \
+ case-sensitive."))
+ elif self.user_cache.is_active == False:
+ raise forms.ValidationError(_("This account is inactive."))
+ return self.cleaned_data['password']
+
+ def get_user(self):
+ """ get authenticated user """
+ return self.user_cache
+
+class ClassicRegisterForm(SetPasswordForm):
+ """ legacy registration form """
+
+ next = NextUrlField()
+ username = UserNameField()
+ email = UserEmailField()
+ #fields password1 and password2 are inherited
+ recaptcha = ReCaptchaField()
+
+class ChangePasswordForm(SetPasswordForm):
+ """ change password form """
+ oldpw = forms.CharField(widget=forms.PasswordInput(attrs={'class':'required'}),
+ label=mark_safe(_('Current password')))
+
+ def __init__(self, data=None, user=None, *args, **kwargs):
+ if user is None:
+ raise TypeError("Keyword argument 'user' must be supplied")
+ super(ChangePasswordForm, self).__init__(data, *args, **kwargs)
+ self.user = user
+
+ def clean_oldpw(self):
+ """ test old password """
+ if not self.user.check_password(self.cleaned_data['oldpw']):
+ raise forms.ValidationError(_("Old password is incorrect. \
+ Please enter the correct password."))
+ return self.cleaned_data['oldpw']
+
+class ChangeEmailForm(forms.Form):
+ """ change email form """
+ email = UserEmailField(skip_clean=True)
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, \
+ initial=None, user=None):
+ super(ChangeEmailForm, self).__init__(data, files, auto_id,
+ prefix, initial)
+ self.user = user
+
+ def clean_email(self):
+ """ check if email don't exist """
+ if 'email' in self.cleaned_data:
+ if forum_settings.EMAIL_UNIQUE == True:
+ try:
+ user = User.objects.get(email = self.cleaned_data['email'])
+ if self.user and self.user == user:
+ return self.cleaned_data['email']
+ except User.DoesNotExist:
+ return self.cleaned_data['email']
+ except User.MultipleObjectsReturned:
+ raise forms.ValidationError(u'There is already more than one \
+ account registered with that e-mail address. Please try \
+ another.')
+ raise forms.ValidationError(u'This email is already registered \
+ in our database. Please choose another.')
+ else:
+ return self.cleaned_data['email']
+
+class ChangeopenidForm(forms.Form):
+ """ change openid form """
+ openid_url = forms.CharField(max_length=255,
+ widget=forms.TextInput(attrs={'class': "required" }))
+
+ def __init__(self, data=None, user=None, *args, **kwargs):
+ if user is None:
+ raise TypeError("Keyword argument 'user' must be supplied")
+ super(ChangeopenidForm, self).__init__(data, *args, **kwargs)
+ self.user = user
+
+class DeleteForm(forms.Form):
+ """ confirm form to delete an account """
+ confirm = forms.CharField(widget=forms.CheckboxInput(attrs={'class':'required'}))
+ password = forms.CharField(widget=forms.PasswordInput(attrs={'class':'required'}))
+
+ def __init__(self, data=None, files=None, auto_id='id_%s',
+ prefix=None, initial=None, user=None):
+ super(DeleteForm, self).__init__(data, files, auto_id, prefix, initial)
+ self.test_openid = False
+ self.user = user
+
+ def clean_password(self):
+ """ check if we have to test a legacy account or not """
+ if 'password' in self.cleaned_data:
+ if not self.user.check_password(self.cleaned_data['password']):
+ self.test_openid = True
+ return self.cleaned_data['password']
+
+
+class EmailPasswordForm(forms.Form):
+ """ send new password form """
+ username = UserNameField(skip_clean=True,label=mark_safe(_('Your user name (<i>required</i>)')))
+
+ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
+ initial=None):
+ super(EmailPasswordForm, self).__init__(data, files, auto_id,
+ prefix, initial)
+ self.user_cache = None
+
+ def clean_username(self):
+ """ get user for this username """
+ if 'username' in self.cleaned_data:
+ try:
+ self.user_cache = User.objects.get(
+ username = self.cleaned_data['username'])
+ except:
+ raise forms.ValidationError(_("Incorrect username."))
+ return self.cleaned_data['username']
diff --git a/forum/deps/django_authopenid/middleware.py b/forum/deps/django_authopenid/middleware.py
new file mode 100644
index 00000000..d3da56da
--- /dev/null
+++ b/forum/deps/django_authopenid/middleware.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+from forum.deps.django_authopenid import mimeparse
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.conf import settings
+import logging
+
+__all__ = ["OpenIDMiddleware"]
+
+class OpenIDMiddleware(object):
+ """
+ Populate request.openid. This comes either from cookie or from
+ session, depending on the presence of OPENID_USE_SESSIONS.
+ """
+ def process_request(self, request):
+ request.openid = request.session.get('openid', None)
+ logging.debug('openid in session is: %s' % str(request.openid))
+
+ def process_response(self, request, response):
+ if response.status_code != 200 or len(response.content) < 200:
+ return response
+ path = request.get_full_path()
+ if path == "/" and request.META.has_key('HTTP_ACCEPT') and \
+ mimeparse.best_match(['text/html', 'application/xrds+xml'],
+ request.META['HTTP_ACCEPT']) == 'application/xrds+xml':
+ logging.debug('redirecting to yadis_xrdf:%s' % reverse('yadis_xrdf'))
+ return HttpResponseRedirect(reverse('yadis_xrdf'))
+ return response
diff --git a/forum/deps/django_authopenid/mimeparse.py b/forum/deps/django_authopenid/mimeparse.py
new file mode 100644
index 00000000..ab02eab0
--- /dev/null
+++ b/forum/deps/django_authopenid/mimeparse.py
@@ -0,0 +1,160 @@
+"""MIME-Type Parser
+
+This module provides basic functions for handling mime-types. It can handle
+matching mime-types against a list of media-ranges. See section 14.1 of
+the HTTP specification [RFC 2616] for a complete explaination.
+
+ http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
+
+Contents:
+ - parse_mime_type(): Parses a mime-type into it's component parts.
+ - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q' quality parameter.
+ - quality(): Determines the quality ('q') of a mime-type when compared against a list of media-ranges.
+ - quality_parsed(): Just like quality() except the second parameter must be pre-parsed.
+ - best_match(): Choose the mime-type with the highest quality ('q') from a list of candidates.
+"""
+
+__version__ = "0.1.1"
+__author__ = 'Joe Gregorio'
+__email__ = "joe@bitworking.org"
+__credits__ = ""
+
+def parse_mime_type(mime_type):
+ """Carves up a mime_type and returns a tuple of the
+ (type, subtype, params) where 'params' is a dictionary
+ of all the parameters for the media range.
+ For example, the media range 'application/xhtml;q=0.5' would
+ get parsed into:
+
+ ('application', 'xhtml', {'q', '0.5'})
+ """
+ parts = mime_type.split(";")
+ params = dict([tuple([s.strip() for s in param.split("=")])\
+ for param in parts[1:] ])
+ (type, subtype) = parts[0].split("/")
+ return (type.strip(), subtype.strip(), params)
+
+def parse_media_range(range):
+ """Carves up a media range and returns a tuple of the
+ (type, subtype, params) where 'params' is a dictionary
+ of all the parameters for the media range.
+ For example, the media range 'application/*;q=0.5' would
+ get parsed into:
+
+ ('application', '*', {'q', '0.5'})
+
+ In addition this function also guarantees that there
+ is a value for 'q' in the params dictionary, filling it
+ in with a proper default if necessary.
+ """
+ (type, subtype, params) = parse_mime_type(range)
+ if not params.has_key('q') or not params['q'] or \
+ not float(params['q']) or float(params['q']) > 1\
+ or float(params['q']) < 0:
+ params['q'] = '1'
+ return (type, subtype, params)
+
+def quality_parsed(mime_type, parsed_ranges):
+ """Find the best match for a given mime_type against
+ a list of media_ranges that have already been
+ parsed by parse_media_range(). Returns the
+ 'q' quality parameter of the best match, 0 if no
+ match was found. This function bahaves the same as quality()
+ except that 'parsed_ranges' must be a list of
+ parsed media ranges. """
+ best_fitness = -1
+ best_match = ""
+ best_fit_q = 0
+ (target_type, target_subtype, target_params) =\
+ parse_media_range(mime_type)
+ for (type, subtype, params) in parsed_ranges:
+ param_matches = reduce(lambda x, y: x+y, [1 for (key, value) in \
+ target_params.iteritems() if key != 'q' and \
+ params.has_key(key) and value == params[key]], 0)
+ if (type == target_type or type == '*' or target_type == '*') and \
+ (subtype == target_subtype or subtype == '*' or target_subtype == '*'):
+ fitness = (type == target_type) and 100 or 0
+ fitness += (subtype == target_subtype) and 10 or 0
+ fitness += param_matches
+ if fitness > best_fitness:
+ best_fitness = fitness
+ best_fit_q = params['q']
+
+ return float(best_fit_q)
+
+def quality(mime_type, ranges):
+ """Returns the quality 'q' of a mime_type when compared
+ against the media-ranges in ranges. For example:
+
+ >>> quality('text/html','text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
+ 0.7
+
+ """
+ parsed_ranges = [parse_media_range(r) for r in ranges.split(",")]
+ return quality_parsed(mime_type, parsed_ranges)
+
+def best_match(supported, header):
+ """Takes a list of supported mime-types and finds the best
+ match for all the media-ranges listed in header. The value of
+ header must be a string that conforms to the format of the
+ HTTP Accept: header. The value of 'supported' is a list of
+ mime-types.
+
+ >>> best_match(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1')
+ 'text/xml'
+ """
+ parsed_header = [parse_media_range(r) for r in header.split(",")]
+ weighted_matches = [(quality_parsed(mime_type, parsed_header), mime_type)\
+ for mime_type in supported]
+ weighted_matches.sort()
+ return weighted_matches[-1][0] and weighted_matches[-1][1] or ''
+
+if __name__ == "__main__":
+ import unittest
+
+ class TestMimeParsing(unittest.TestCase):
+
+ def test_parse_media_range(self):
+ self.assert_(('application', 'xml', {'q': '1'}) == parse_media_range('application/xml;q=1'))
+ self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml'))
+ self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml;q='))
+ self.assertEqual(('application', 'xml', {'q': '1'}), parse_media_range('application/xml ; q='))
+ self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=1;b=other'))
+ self.assertEqual(('application', 'xml', {'q': '1', 'b': 'other'}), parse_media_range('application/xml ; q=2;b=other'))
+
+ def test_rfc_2616_example(self):
+ accept = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5"
+ self.assertEqual(1, quality("text/html;level=1", accept))
+ self.assertEqual(0.7, quality("text/html", accept))
+ self.assertEqual(0.3, quality("text/plain", accept))
+ self.assertEqual(0.5, quality("image/jpeg", accept))
+ self.assertEqual(0.4, quality("text/html;level=2", accept))
+ self.assertEqual(0.7, quality("text/html;level=3", accept))
+
+ def test_best_match(self):
+ mime_types_supported = ['application/xbel+xml', 'application/xml']
+ # direct match
+ self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml'), 'application/xbel+xml')
+ # direct match with a q parameter
+ self.assertEqual(best_match(mime_types_supported, 'application/xbel+xml; q=1'), 'application/xbel+xml')
+ # direct match of our second choice with a q parameter
+ self.assertEqual(best_match(mime_types_supported, 'application/xml; q=1'), 'application/xml')
+ # match using a subtype wildcard
+ self.assertEqual(best_match(mime_types_supported, 'application/*; q=1'), 'application/xml')
+ # match using a type wildcard
+ self.assertEqual(best_match(mime_types_supported, '*/*'), 'application/xml')
+
+ mime_types_supported = ['application/xbel+xml', 'text/xml']
+ # match using a type versus a lower weighted subtype
+ self.assertEqual(best_match(mime_types_supported, 'text/*;q=0.5,*/*; q=0.1'), 'text/xml')
+ # fail to match anything
+ self.assertEqual(best_match(mime_types_supported, 'text/html,application/atom+xml; q=0.9'), '')
+
+ def test_support_wildcards(self):
+ mime_types_supported = ['image/*', 'application/xml']
+ # match using a type wildcard
+ self.assertEqual(best_match(mime_types_supported, 'image/png'), 'image/*')
+ # match using a wildcard for both requested and supported
+ self.assertEqual(best_match(mime_types_supported, 'image/*'), 'image/*')
+
+ unittest.main() \ No newline at end of file
diff --git a/forum/deps/django_authopenid/models.py b/forum/deps/django_authopenid/models.py
new file mode 100644
index 00000000..a12c2fec
--- /dev/null
+++ b/forum/deps/django_authopenid/models.py
@@ -0,0 +1,80 @@
+# -*- coding: utf-8 -*-
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.db import models
+
+import hashlib, random, sys, os, time
+
+__all__ = ['Nonce', 'Association', 'UserAssociation',
+ 'UserPasswordQueueManager', 'UserPasswordQueue']
+
+class Nonce(models.Model):
+ """ openid nonce """
+ server_url = models.CharField(max_length=255)
+ timestamp = models.IntegerField()
+ salt = models.CharField(max_length=40)
+
+ def __unicode__(self):
+ return u"Nonce: %s" % self.id
+
+
+class Association(models.Model):
+ """ association openid url and lifetime """
+ server_url = models.TextField(max_length=2047)
+ handle = models.CharField(max_length=255)
+ secret = models.TextField(max_length=255) # Stored base64 encoded
+ issued = models.IntegerField()
+ lifetime = models.IntegerField()
+ assoc_type = models.TextField(max_length=64)
+
+ def __unicode__(self):
+ return u"Association: %s, %s" % (self.server_url, self.handle)
+
+class UserAssociation(models.Model):
+ """
+ model to manage association between openid and user
+ """
+ openid_url = models.CharField(blank=False, max_length=255)
+ user = models.ForeignKey(User, unique=True)
+
+ def __unicode__(self):
+ return "Openid %s with user %s" % (self.openid_url, self.user)
+
+class UserPasswordQueueManager(models.Manager):
+ """ manager for UserPasswordQueue object """
+ def get_new_confirm_key(self):
+ "Returns key that isn't being used."
+ # The random module is seeded when this Apache child is created.
+ # Use SECRET_KEY as added salt.
+ while 1:
+ confirm_key = hashlib.md5("%s%s%s%s" % (
+ random.randint(0, sys.maxint - 1), os.getpid(),
+ time.time(), settings.SECRET_KEY)).hexdigest()
+ try:
+ self.get(confirm_key=confirm_key)
+ except self.model.DoesNotExist:
+ break
+ return confirm_key
+
+
+class UserPasswordQueue(models.Model):
+ """
+ model for new password queue.
+ """
+ user = models.ForeignKey(User, unique=True)
+ new_password = models.CharField(max_length=30)
+ confirm_key = models.CharField(max_length=40)
+
+ objects = UserPasswordQueueManager()
+
+ def __unicode__(self):
+ return self.user.username
+
+class ExternalLoginData(models.Model):
+ """this class was added by Evgeny to associate
+ external authentication user with django user
+ probably it just does not belong here... (EF)
+ """
+ external_username = models.CharField(max_length=40, unique=True, null=False)
+ external_session_data = models.TextField()
+ user = models.ForeignKey(User, null=True)
diff --git a/forum/deps/django_authopenid/urls.py b/forum/deps/django_authopenid/urls.py
new file mode 100644
index 00000000..97032b28
--- /dev/null
+++ b/forum/deps/django_authopenid/urls.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+from django.conf.urls.defaults import patterns, url
+from django.utils.translation import ugettext as _
+from django.conf import settings
+
+#print 'stuff to import %s' % settings.EXTERNAL_LOGIN_APP.__name__ + '.views'
+#try:
+# settings.EXTERNAL_LOGIN_APP = __import__('mediawiki.views')
+#print 'stuff to import %s' % settings.EXTERNAL_LOGIN_APP.__name__ + '.views'
+#try:
+# print 'imported fine'
+# print settings.EXTERNAL_LOGIN_APP.__dict__.keys()
+#except:
+# print 'dammit!'
+#from mediawiki.views import signup_view
+#settings.EXTERNAL_LOGIN_APP.views.signup_view()
+
+#print settings.EXTERNAL_LOGIN_APP.__dict__.keys()
+urlpatterns = patterns('forum.deps.django_authopenid.views',
+ # yadis rdf
+ url(r'^yadis.xrdf$', 'xrdf', name='yadis_xrdf'),
+ # manage account registration
+ url(r'^%s$' % _('signin/'), 'signin', name='user_signin'),
+ url(r'^%s%s$' % (_('signin/'),_('newquestion/')), 'signin', kwargs = {'newquestion':True}, name='user_signin_new_question'),
+ url(r'^%s%s$' % (_('signin/'),_('newanswer/')), 'signin', kwargs = {'newanswer':True}, name='user_signin_new_answer'),
+ url(r'^%s$' % _('signout/'), 'signout', name='user_signout'),
+ url(r'^%s%s$' % (_('signin/'), _('complete/')), 'complete_signin',
+ name='user_complete_signin'),
+ url(r'^%s$' % _('register/'), 'register', name='user_register'),
+ url(r'^%s$' % _('signup/'), 'signup', name='user_signup'),
+ #disable current sendpw function
+ url(r'^%s$' % _('sendpw/'), 'sendpw', name='user_sendpw'),
+ url(r'^%s%s$' % (_('password/'), _('confirm/')), 'confirmchangepw', name='user_confirmchangepw'),
+
+ # manage account settings
+ url(r'^$', 'account_settings', name='user_account_settings'),
+ url(r'^%s$' % _('password/'), 'changepw', name='user_changepw'),
+ url(r'^%s%s$' % (_('email/'),_('validate/')), 'changeemail', name='user_validateemail',kwargs = {'action':'validate'}),
+ url(r'^%s%s$' % (_('email/'), _('change/')), 'changeemail', name='user_changeemail'),
+ url(r'^%s%s$' % (_('email/'), _('sendkey/')), 'send_email_key', name='send_email_key'),
+ url(r'^%s%s(?P<id>\d+)/(?P<key>[\dabcdef]{32})/$' % (_('email/'), _('verify/')), 'verifyemail', name='user_verifyemail'),
+ url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'),
+ url(r'^%s$' % _('delete/'), 'delete', name='user_delete'),
+)
+
+#todo move these out of this file completely
+if settings.USE_EXTERNAL_LEGACY_LOGIN:
+ from forum.forms import NotARobotForm
+ EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP()
+ urlpatterns += patterns('',
+ url('^%s$' % _('external-login/forgot-password/'),\
+ 'forum.deps.django_authopenid.views.external_legacy_login_info', \
+ name='user_external_legacy_login_issues'),
+ url('^%s$' % _('external-login/signup/'), \
+ EXTERNAL_LOGIN_APP.views.signup,\
+ name='user_external_legacy_login_signup'),
+# url('^%s$' % _('external-login/signup/'), \
+# EXTERNAL_LOGIN_APP.forms.RegisterFormWizard( \
+# [EXTERNAL_LOGIN_APP.forms.RegisterForm, \
+# NotARobotForm]),\
+# name='user_external_legacy_login_signup'),
+ )
diff --git a/forum/deps/django_authopenid/util.py b/forum/deps/django_authopenid/util.py
new file mode 100644
index 00000000..cd2c2e2c
--- /dev/null
+++ b/forum/deps/django_authopenid/util.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+from openid.store.interface import OpenIDStore
+from openid.association import Association as OIDAssociation
+from openid.extensions import sreg
+import openid.store
+
+from django.db.models.query import Q
+from django.conf import settings
+from django.core.urlresolvers import reverse
+
+# needed for some linux distributions like debian
+try:
+ from openid.yadis import xri
+except:
+ from yadis import xri
+
+import time, base64, hashlib, operator, logging
+from forum.utils.forms import clean_next, get_next_url
+
+from models import Association, Nonce
+
+__all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next']
+
+class OpenID:
+ def __init__(self, openid_, issued, attrs=None, sreg_=None):
+ logging.debug('init janrain openid object')
+ self.openid = openid_
+ self.issued = issued
+ self.attrs = attrs or {}
+ self.sreg = sreg_ or {}
+ self.is_iname = (xri.identifierScheme(openid_) == 'XRI')
+
+ def __repr__(self):
+ return '<OpenID: %s>' % self.openid
+
+ def __str__(self):
+ return self.openid
+
+class DjangoOpenIDStore(OpenIDStore):
+ def __init__(self):
+ self.max_nonce_age = 6 * 60 * 60 # Six hours
+
+ def storeAssociation(self, server_url, association):
+ assoc = Association(
+ server_url = server_url,
+ handle = association.handle,
+ secret = base64.encodestring(association.secret),
+ issued = association.issued,
+ lifetime = association.issued,
+ assoc_type = association.assoc_type
+ )
+ assoc.save()
+
+ def getAssociation(self, server_url, handle=None):
+ assocs = []
+ if handle is not None:
+ assocs = Association.objects.filter(
+ server_url = server_url, handle = handle
+ )
+ else:
+ assocs = Association.objects.filter(
+ server_url = server_url
+ )
+ if not assocs:
+ return None
+ associations = []
+ for assoc in assocs:
+ association = OIDAssociation(
+ assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
+ assoc.lifetime, assoc.assoc_type
+ )
+ if association.getExpiresIn() == 0:
+ self.removeAssociation(server_url, assoc.handle)
+ else:
+ associations.append((association.issued, association))
+ if not associations:
+ return None
+ return associations[-1][1]
+
+ def removeAssociation(self, server_url, handle):
+ assocs = list(Association.objects.filter(
+ server_url = server_url, handle = handle
+ ))
+ assocs_exist = len(assocs) > 0
+ for assoc in assocs:
+ assoc.delete()
+ return assocs_exist
+
+ def useNonce(self, server_url, timestamp, salt):
+ if abs(timestamp - time.time()) > openid.store.nonce.SKEW:
+ return False
+
+ query = [
+ Q(server_url__exact=server_url),
+ Q(timestamp__exact=timestamp),
+ Q(salt__exact=salt),
+ ]
+ try:
+ ononce = Nonce.objects.get(reduce(operator.and_, query))
+ except Nonce.DoesNotExist:
+ ononce = Nonce(
+ server_url=server_url,
+ timestamp=timestamp,
+ salt=salt
+ )
+ ononce.save()
+ return True
+
+ ononce.delete()
+
+ return False
+
+ def cleanupNonce(self):
+ Nonce.objects.filter(timestamp<int(time.time()) - nonce.SKEW).delete()
+
+ def cleanupAssociations(self):
+ Association.objects.extra(where=['issued + lifetimeint<(%s)' % time.time()]).delete()
+
+ def getAuthKey(self):
+ # Use first AUTH_KEY_LEN characters of md5 hash of SECRET_KEY
+ return hashlib.md5(settings.SECRET_KEY).hexdigest()[:self.AUTH_KEY_LEN]
+
+ def isDumb(self):
+ return False
+
+def from_openid_response(openid_response):
+ """ return openid object from response """
+ issued = int(time.time())
+ sreg_resp = sreg.SRegResponse.fromSuccessResponse(openid_response) \
+ or []
+
+ return OpenID(
+ openid_response.identity_url, issued, openid_response.signed_fields,
+ dict(sreg_resp)
+ )
diff --git a/forum/deps/django_authopenid/views.py b/forum/deps/django_authopenid/views.py
new file mode 100644
index 00000000..52809f01
--- /dev/null
+++ b/forum/deps/django_authopenid/views.py
@@ -0,0 +1,1192 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2007, 2008, Benoît Chesneau
+# Copyright (c) 2007 Simon Willison, original work on django-openid
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# * notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# * notice, this list of conditions and the following disclaimer in the
+# * documentation and/or other materials provided with the
+# * distribution. Neither the name of the <ORGANIZATION> nor the names
+# * of its contributors may be used to endorse or promote products
+# * derived from this software without specific prior written
+# * permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+# OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from django.http import HttpResponseRedirect, get_host, Http404, \
+ HttpResponseServerError
+from django.shortcuts import render_to_response
+from django.template import RequestContext, loader, Context
+from django.conf import settings
+from forum.conf import settings as forum_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.utils.encoding import smart_unicode
+from django.utils.html import escape
+from django.utils.translation import ugettext as _
+from django.utils.http import urlquote_plus
+from django.utils.safestring import mark_safe
+from django.core.mail import send_mail
+from django.views.defaults import server_error
+
+from openid.consumer.consumer import Consumer, \
+ SUCCESS, CANCEL, FAILURE, SETUP_NEEDED
+from openid.consumer.discover import DiscoveryFailure
+from openid.extensions import sreg
+# needed for some linux distributions like debian
+try:
+ from openid.yadis import xri
+except ImportError:
+ from yadis import xri
+
+import re
+import urllib
+
+from forum.forms import SimpleEmailSubscribeForm
+from forum.deps.django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response
+from forum.deps.django_authopenid.models import UserAssociation, UserPasswordQueue, ExternalLoginData
+from forum.deps.django_authopenid.forms import OpenidSigninForm, ClassicLoginForm, OpenidRegisterForm, \
+ OpenidVerifyForm, ClassicRegisterForm, ChangePasswordForm, ChangeEmailForm, \
+ ChangeopenidForm, DeleteForm, EmailPasswordForm
+import logging
+from forum.utils.forms import get_next_url
+
+EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP()
+
+#todo: decouple from forum
+def login(request,user):
+ from django.contrib.auth import login as _login
+ from forum.models import signals
+
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ EXTERNAL_LOGIN_APP.api.login(request,user)
+
+ #1) get old session key
+ session_key = request.session.session_key
+ #2) get old search state
+ search_state = None
+ if 'search_state' in request.session:
+ search_state = request.session['search_state']
+
+ #3) login and get new session key
+ _login(request,user)
+ #4) transfer search_state to new session if found
+ if search_state:
+ search_state.set_logged_in()
+ request.session['search_state'] = search_state
+ #5) send signal with old session key as argument
+ logging.debug('logged in user %s with session key %s' % (user.username, session_key))
+ #todo: move to auth app
+ signals.user_logged_in.send(user=user,session_key=session_key,sender=None)
+
+#todo: uncouple this from forum
+def logout(request):
+ from django.contrib.auth import logout as _logout#for login I've added wrapper below - called login
+ if 'search_state' in request.session:
+ request.session['search_state'].set_logged_out()
+ request.session.modified = True
+ _logout(request)
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ EXTERNAL_LOGIN_APP.api.logout(request)
+
+def get_url_host(request):
+ if request.is_secure():
+ protocol = 'https'
+ else:
+ protocol = 'http'
+ host = escape(get_host(request))
+ return '%s://%s' % (protocol, host)
+
+def get_full_url(request):
+ return get_url_host(request) + request.get_full_path()
+
+def ask_openid(request, openid_url, redirect_to, on_failure=None,
+ sreg_request=None):
+ """ basic function to ask openid and return response """
+ request.encoding = 'UTF-8'
+ on_failure = on_failure or signin_failure
+
+ trust_root = getattr(
+ settings, 'OPENID_TRUST_ROOT', get_url_host(request) + '/'
+ )
+ if xri.identifierScheme(openid_url) == 'XRI' and getattr(
+ settings, 'OPENID_DISALLOW_INAMES', False
+ ):
+ msg = _("i-names are not supported")
+ logging.debug('openid failed becaise i-names are not supported')
+ return on_failure(request, msg)
+ consumer = Consumer(request.session, DjangoOpenIDStore())
+ try:
+ auth_request = consumer.begin(openid_url)
+ except DiscoveryFailure:
+ msg = _(u"OpenID %(openid_url)s is invalid" % {'openid_url':openid_url})
+ logging.debug(msg)
+ return on_failure(request, msg)
+
+ logging.debug('openid seemed to work')
+ if sreg_request:
+ logging.debug('adding sreg_request - wtf it is?')
+ auth_request.addExtension(sreg_request)
+ redirect_url = auth_request.redirectURL(trust_root, redirect_to)
+ logging.debug('redirecting to %s' % redirect_url)
+ return HttpResponseRedirect(redirect_url)
+
+def complete(request, on_success=None, on_failure=None, return_to=None):
+ """ complete openid signin """
+ on_success = on_success or default_on_success
+ on_failure = on_failure or default_on_failure
+
+ logging.debug('in forum.deps.django_authopenid.complete')
+
+ consumer = Consumer(request.session, DjangoOpenIDStore())
+ # make sure params are encoded in utf8
+ params = dict((k,smart_unicode(v)) for k, v in request.GET.items())
+ openid_response = consumer.complete(params, return_to)
+
+ if openid_response.status == SUCCESS:
+ logging.debug('SUCCESS')
+ return on_success(request, openid_response.identity_url,
+ openid_response)
+ elif openid_response.status == CANCEL:
+ logging.debug('CANCEL')
+ return on_failure(request, 'The request was canceled')
+ elif openid_response.status == FAILURE:
+ logging.debug('FAILURE')
+ return on_failure(request, openid_response.message)
+ elif openid_response.status == SETUP_NEEDED:
+ logging.debug('SETUP NEEDED')
+ return on_failure(request, 'Setup needed')
+ else:
+ logging.debug('BAD OPENID STATUS')
+ assert False, "Bad openid status: %s" % openid_response.status
+
+def default_on_success(request, identity_url, openid_response):
+ """ default action on openid signin success """
+ logging.debug('')
+ request.session['openid'] = from_openid_response(openid_response)
+ logging.debug('performing default action on openid success %s' % get_next_url(request))
+ return HttpResponseRedirect(get_next_url(request))
+
+def default_on_failure(request, message):
+ """ default failure action on signin """
+ logging.debug('default openid failure action')
+ return render_to_response('openid_failure.html', {
+ 'message': message
+ })
+
+
+def not_authenticated(func):
+ """ decorator that redirect user to next page if
+ he is already logged."""
+ def decorated(request, *args, **kwargs):
+ if request.user.is_authenticated():
+ return HttpResponseRedirect(get_next_url(request))
+ return func(request, *args, **kwargs)
+ return decorated
+
+@not_authenticated
+def signin(request,newquestion=False,newanswer=False):
+ """
+ signin page. It manages the legacy authentification (user/password)
+ and openid authentification
+
+ url: /signin/
+
+ template : authopenid/signin.htm
+ """
+ logging.debug('in signin view')
+ request.encoding = 'UTF-8'
+ on_failure = signin_failure
+ email_feeds_form = SimpleEmailSubscribeForm()
+ next = get_next_url(request)
+ form_signin = OpenidSigninForm(initial={'next':next})
+ form_auth = ClassicLoginForm(initial={'next':next})
+
+ if request.method == 'POST':
+ #'blogin' - password login
+ if 'blogin' in request.POST.keys():
+ logging.debug('processing classic login form submission')
+ form_auth = ClassicLoginForm(request.POST)
+ if form_auth.is_valid():
+ #have login and password and need to login through external website
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ username = form_auth.cleaned_data['username']
+ password = form_auth.cleaned_data['password']
+ next = form_auth.cleaned_data['next']
+ if form_auth.get_user() == None:
+ #need to create internal user
+
+ #1) save login and password temporarily in session
+ request.session['external_username'] = username
+ request.session['external_password'] = password
+
+ #2) try to extract user email and nickname from external service
+ email = EXTERNAL_LOGIN_APP.api.get_email(username,password)
+ screen_name = EXTERNAL_LOGIN_APP.api.get_screen_name(username,password)
+
+ #3) see if username clashes with some existing user
+ #if so, we have to prompt the user to pick a different name
+ username_taken = User.is_username_taken(screen_name)
+
+ email_feeds_form = SimpleEmailSubscribeForm()
+ form_data = {'username':screen_name,'email':email,'next':next}
+ form = OpenidRegisterForm(initial=form_data)
+ template_data = {'form1':form,'username':screen_name,\
+ 'email_feeds_form':email_feeds_form,\
+ 'provider':mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME),\
+ 'login_type':'legacy',\
+ 'gravatar_faq_url':reverse('faq') + '#gravatar',\
+ 'external_login_name_is_taken':username_taken}
+ return render_to_response('authopenid/complete.html',template_data,\
+ context_instance=RequestContext(request))
+ else:
+ #user existed, external password is ok
+ user = form_auth.get_user()
+ login(request,user)
+ response = HttpResponseRedirect(get_next_url(request))
+ EXTERNAL_LOGIN_APP.api.set_login_cookies(response,user)
+ return response
+ else:
+ #regular password authentication
+ user = form_auth.get_user()
+ login(request, user)
+ return HttpResponseRedirect(get_next_url(request))
+
+ elif 'bnewaccount' in request.POST.keys():
+ logging.debug('processing classic (login/password) create account form submission')
+ #register externally logged in password user with a new local account
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ form = OpenidRegisterForm(request.POST)
+ email_feeds_form = SimpleEmailSubscribeForm(request.POST)
+ form1_is_valid = form.is_valid()
+ form2_is_valid = email_feeds_form.is_valid()
+ if form1_is_valid and form2_is_valid:
+ #create the user
+ username = form.cleaned_data['username']
+ password = request.session.get('external_password',None)
+ email = form.cleaned_data['email']
+ if password and username:
+ User.objects.create_user(username,email,password)
+ user = authenticate(username=username,password=password)
+ EXTERNAL_LOGIN_APP.api.connect_local_user_to_external_user(user,username,password)
+ external_username = request.session['external_username']
+ eld = ExternalLoginData.objects.get(external_username=external_username)
+ eld.user = user
+ eld.save()
+ login(request,user)
+ email_feeds_form.save(user)
+ del request.session['external_username']
+ del request.session['external_password']
+ response = HttpResponseRedirect(reverse('index'))
+ EXTERNAL_LOGIN_APP.api.set_login_cookies(response, user)
+ return response
+ else:
+ if password:
+ del request.session['external_username']
+ if username:
+ del request.session['external_password']
+ return HttpResponseServerError()
+ else:
+ username = request.POST.get('username',None)
+ provider = mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME)
+ username_taken = User.is_username_taken(username)
+ data = {'login_type':'legacy','form1':form,'username':username,\
+ 'email_feeds_form':email_feeds_form,'provider':provider,\
+ 'gravatar_faq_url':reverse('faq') + '#gravatar',\
+ 'external_login_name_is_taken':username_taken}
+ return render_to_response('authopenid/complete.html',data,
+ context_instance=RequestContext(request))
+ else:
+ raise Http404
+
+ elif 'bsignin' in request.POST.keys() or 'openid_username' in request.POST.keys():
+ logging.debug('processing signin with openid submission')
+ form_signin = OpenidSigninForm(request.POST)
+ if form_signin.is_valid():
+ logging.debug('OpenidSigninForm is valid')
+ next = form_signin.cleaned_data['next']
+ sreg_req = sreg.SRegRequest(optional=['nickname', 'email'])
+ redirect_to = "%s%s?%s" % (
+ get_url_host(request),
+ reverse('user_complete_signin'),
+ urllib.urlencode({'next':next})
+ )
+ return ask_openid(request,
+ form_signin.cleaned_data['openid_url'],
+ redirect_to,
+ on_failure=signin_failure,
+ sreg_request=sreg_req)
+ else:
+ logging.debug('OpenidSigninForm is NOT valid! -> redisplay login view')
+
+ #if request is GET
+ if request.method == 'GET':
+ logging.debug('request method was GET')
+ question = None
+ if newquestion == True:
+ from forum.models import AnonymousQuestion as AQ
+ session_key = request.session.session_key
+ logging.debug('retrieving anonymously posted question associated with session %s' % session_key)
+ qlist = AQ.objects.filter(session_key=session_key).order_by('-added_at')
+ if len(qlist) > 0:
+ question = qlist[0]
+ answer = None
+ if newanswer == True:
+ from forum.models import AnonymousAnswer as AA
+ session_key = request.session.session_key
+ logging.debug('retrieving posted answer associated with session %s' % session_key)
+ alist = AA.objects.filter(session_key=session_key).order_by('-added_at')
+ if len(alist) > 0:
+ answer = alist[0]
+
+ logging.debug('showing signin view')
+ return render_to_response('authopenid/signin.html', {
+ 'question':question,
+ 'answer':answer,
+ 'form1': form_auth,
+ 'form2': form_signin,
+ 'msg': request.GET.get('msg',''),
+ 'sendpw_url': reverse('user_sendpw'),
+ 'fb_api_key': forum_settings.FB_API_KEY,
+ }, context_instance=RequestContext(request))
+
+def complete_signin(request):
+ """ in case of complete signin with openid """
+ logging.debug('')#blank log just for the trace
+ return complete(request, signin_success, signin_failure,
+ get_url_host(request) + reverse('user_complete_signin'))
+
+def signin_success(request, identity_url, openid_response):
+ """
+ openid signin success.
+
+ If the openid is already registered, the user is redirected to
+ url set par next or in settings with OPENID_REDIRECT_NEXT variable.
+ If none of these urls are set user is redirectd to /.
+
+ if openid isn't registered user is redirected to register page.
+ """
+
+ logging.debug('')
+ openid_ = from_openid_response(openid_response) #create janrain OpenID object
+ request.session['openid'] = openid_
+ try:
+ logging.debug('trying to get user associated with this openid...')
+ rel = UserAssociation.objects.get(openid_url__exact = str(openid_))
+ logging.debug('success')
+ except:
+ logging.debug('failed --> try to register brand new user')
+ # try to register this new user
+ return register(request)
+ user_ = rel.user
+ if user_.is_active:
+ user_.backend = "django.contrib.auth.backends.ModelBackend"
+ logging.debug('user is active --> attached django auth ModelBackend --> calling login')
+ login(request, user_)
+ logging.debug('success')
+ else:
+ logging.debug('user is inactive, do not log them in')
+ logging.debug('redirecting to %s' % get_next_url(request))
+ return HttpResponseRedirect(get_next_url(request))
+
+def is_association_exist(openid_url):
+ """ test if an openid is already in database """
+ is_exist = True
+ try:
+ uassoc = UserAssociation.objects.get(openid_url__exact = openid_url)
+ except:
+ is_exist = False
+ logging.debug(str(is_exist))
+ return is_exist
+
+@not_authenticated
+def register(request):
+ """
+ register an openid.
+
+ If user is already a member he can associate its openid with
+ its account.
+
+ A new account could also be created and automaticaly associated
+ to the openid.
+
+ url : /complete/
+
+ template : authopenid/complete.html
+ """
+
+ logging.debug('')
+ openid_ = request.session.get('openid', None)
+ next = get_next_url(request)
+ if not openid_:
+ logging.debug('oops, no openid in session --> go back to signin')
+ return HttpResponseRedirect(reverse('user_signin') + '?next=%s' % next)
+
+ nickname = openid_.sreg.get('nickname', '')
+ email = openid_.sreg.get('email', '')
+ form1 = OpenidRegisterForm(initial={
+ 'next': next,
+ 'username': nickname,
+ 'email': email,
+ })
+ form2 = OpenidVerifyForm(initial={
+ 'next': next,
+ 'username': nickname,
+ })
+ email_feeds_form = SimpleEmailSubscribeForm()
+
+ user_ = None
+ is_redirect = False
+ logging.debug('request method is %s' % request.method)
+ if request.method == 'POST':
+ if 'bnewaccount' in request.POST.keys():
+ logging.debug('trying to create new account associated with openid')
+ form1 = OpenidRegisterForm(request.POST)
+ email_feeds_form = SimpleEmailSubscribeForm(request.POST)
+ if not form1.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')
+ next = form1.cleaned_data['next']
+ is_redirect = True
+ logging.debug('creatng new django user %s ...' % form1.cleaned_data['username'])
+ tmp_pwd = User.objects.make_random_password()
+ user_ = User.objects.create_user(form1.cleaned_data['username'],
+ form1.cleaned_data['email'], tmp_pwd)
+
+ user_.set_unusable_password()
+ # make association with openid
+ logging.debug('creating new openid user association %s <--> %s' \
+ % (user_.username, str(openid_)))
+ uassoc = UserAssociation(openid_url=str(openid_), user_id=user_.id)
+ uassoc.save()
+
+ # login
+ user_.backend = "django.contrib.auth.backends.ModelBackend"
+ logging.debug('logging the user in')
+ login(request, user_)
+ logging.debug('saving email feed settings')
+ email_feeds_form.save(user_)
+ elif 'bverify' in request.POST.keys():
+ logging.debug('processing OpenidVerify form')
+ form2 = OpenidVerifyForm(request.POST)
+ if form2.is_valid():
+ logging.debug('form is valid')
+ is_redirect = True
+ next = form2.cleaned_data['next']
+ user_ = form2.get_user()
+ logging.debug('creating new openid user association %s <--> %s' \
+ % (user_.username, str(openid_)))
+ uassoc = UserAssociation(openid_url=str(openid_),
+ user_id=user_.id)
+ uassoc.save()
+ logging.debug('logging the user in')
+ login(request, user_)
+
+ #check if we need to post a question that was added anonymously
+ #this needs to be a function call becase this is also done
+ #if user just logged in and did not need to create the new account
+
+ if user_ != None:
+ if forum_settings.EMAIL_VALIDATION == True:
+ logging.debug('sending email validation')
+ send_new_email_key(user_,nomessage=True)
+ output = validation_email_sent(request)
+ set_email_validation_message(user_) #message set after generating view
+ return output
+ if user_.is_authenticated():
+ logging.debug('success, send user to main page')
+ return HttpResponseRedirect(reverse('index'))
+ else:
+ logging.debug('have really strange error')
+ raise Exception('openid login failed')#should not ever get here
+
+ openid_str = str(openid_)
+ bits = openid_str.split('/')
+ base_url = bits[2] #assume this is base url
+ url_bits = base_url.split('.')
+ provider_name = url_bits[-2].lower()
+
+ providers = {'yahoo':'<font color="purple">Yahoo!</font>',
+ 'flickr':'<font color="#0063dc">flick</font><font color="#ff0084">r</font>&trade;',
+ 'google':'Google&trade;',
+ 'aol':'<font color="#31658e">AOL</font>',
+ 'myopenid':'MyOpenID',
+ }
+ if provider_name not in providers:
+ provider_logo = provider_name
+ logging.error('openid provider named "%s" has no pretty customized logo' % provider_name)
+ else:
+ provider_logo = providers[provider_name]
+
+ logging.debug('printing authopenid/complete.html output')
+ return render_to_response('authopenid/complete.html', {
+ 'form1': form1,
+ 'form2': form2,
+ 'email_feeds_form': email_feeds_form,
+ 'provider':mark_safe(provider_logo),
+ 'username': nickname,
+ 'email': email,
+ 'login_type':'openid',
+ 'gravatar_faq_url':reverse('faq') + '#gravatar',
+ }, context_instance=RequestContext(request))
+
+def signin_failure(request, message):
+ """
+ falure with openid signin. Go back to signin page.
+
+ template : "authopenid/signin.html"
+ """
+ logging.debug('')
+ next = get_next_url(request)
+ form_signin = OpenidSigninForm(initial={'next': next})
+ form_auth = ClassicLoginForm(initial={'next': next})
+
+ return render_to_response('authopenid/signin.html', {
+ 'msg': message,
+ 'form1': form_auth,
+ 'form2': form_signin,
+ }, context_instance=RequestContext(request))
+
+@not_authenticated
+def signup(request):
+ """
+ signup page. Create a legacy account
+
+ url : /signup/"
+
+ templates: authopenid/signup.html, authopenid/confirm_email.txt
+ """
+ logging.debug('')
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ logging.debug('handling external legacy login registration')
+ return HttpResponseRedirect(reverse('user_external_legacy_login_signup'))
+ next = get_next_url(request)
+ logging.debug('request method was %s' % request.method)
+ if request.method == 'POST':
+ form = ClassicRegisterForm(request.POST)
+ email_feeds_form = SimpleEmailSubscribeForm(request.POST)
+
+ #validation outside if to remember form values
+ logging.debug('validating classic register form')
+ form1_is_valid = form.is_valid()
+ logging.debug('classic register form validated')
+ form2_is_valid = email_feeds_form.is_valid()
+ logging.debug('email feeds form validated')
+ if form1_is_valid and form2_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']
+
+ user_ = User.objects.create_user( username,email,password )
+ logging.debug('new user %s created' % username)
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ EXTERNAL_LOGIN_APP.api.create_user(username,email,password)
+
+ user_.backend = "django.contrib.auth.backends.ModelBackend"
+ login(request, user_)
+ logging.debug('new user logged in')
+ email_feeds_form.save(user_)
+ logging.debug('email feeds form saved')
+
+ # send email
+ subject = _("Welcome email subject line")
+ message_template = loader.get_template(
+ 'authopenid/confirm_email.txt'
+ )
+ message_context = Context({
+ 'signup_url': forum_settings.APP_URL + reverse('user_signin'),
+ 'username': username,
+ 'password': password,
+ })
+ message = message_template.render(message_context)
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
+ [user_.email])
+ logging.debug('new user with login and password created, confirmation email sent!')
+ return HttpResponseRedirect(next)
+ else:
+ logging.debug('create classic account forms were invalid')
+ else:
+ form = ClassicRegisterForm(initial={'next':next})
+ email_feeds_form = SimpleEmailSubscribeForm()
+ logging.debug('printing legacy signup form')
+ return render_to_response('authopenid/signup.html', {
+ 'form': form,
+ 'email_feeds_form': email_feeds_form
+ }, context_instance=RequestContext(request))
+ #what if request is not posted?
+
+@login_required
+def signout(request):
+ """
+ signout from the website. Remove openid from session and kill it.
+
+ url : /signout/"
+ """
+ logging.debug('')
+ try:
+ logging.debug('deleting openid session var')
+ del request.session['openid']
+ except KeyError:
+ logging.debug('failed')
+ pass
+ logout(request)
+ logging.debug('user logged out')
+ return HttpResponseRedirect(get_next_url(request))
+
+def xrdf(request):
+ url_host = get_url_host(request)
+ logging.debug('what does this do??')
+ return_to = [
+ "%s%s" % (url_host, reverse('user_complete_signin'))
+ ]
+ return render_to_response('authopenid/yadis.xrdf', {
+ 'return_to': return_to
+ }, context_instance=RequestContext(request))
+
+@login_required
+def account_settings(request):
+ """
+ index pages to changes some basic account settings :
+ - change password
+ - change email
+ - associate a new openid
+ - delete account
+
+ url : /
+
+ template : authopenid/settings.html
+ """
+ logging.debug('')
+ msg = request.GET.get('msg', '')
+ is_openid = True
+
+ try:
+ uassoc = UserAssociation.objects.get(
+ user__username__exact=request.user.username
+ )
+ except:
+ is_openid = False
+
+
+ return render_to_response('authopenid/settings.html', {
+ 'msg': msg,
+ 'is_openid': is_openid
+ }, context_instance=RequestContext(request))
+
+@login_required
+def changepw(request):
+ """
+ change password view.
+
+ url : /changepw/
+ template: authopenid/changepw.html
+ """
+ logging.debug('')
+ user_ = request.user
+
+ if user_.has_usable_password():
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ return HttpResponseRedirect(reverse('user_external_legacy_login_issues'))
+ else:
+ raise Http404
+
+ if request.POST:
+ form = ChangePasswordForm(request.POST, user=user_)
+ if form.is_valid():
+ user_.set_password(form.cleaned_data['password1'])
+ user_.save()
+ msg = _("Password changed.")
+ redirect = "%s?msg=%s" % (
+ reverse('user_account_settings'),
+ urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+ else:
+ form = ChangePasswordForm(user=user_)
+
+ return render_to_response('authopenid/changepw.html', {'form': form },
+ context_instance=RequestContext(request))
+
+def find_email_validation_messages(user):
+ msg_text = _('your email needs to be validated see %(details_url)s') \
+ % {'details_url':reverse('faq') + '#validate'}
+ return user.message_set.filter(message__exact=msg_text)
+
+def set_email_validation_message(user):
+ messages = find_email_validation_messages(user)
+ msg_text = _('your email needs to be validated see %(details_url)s') \
+ % {'details_url':reverse('faq') + '#validate'}
+ if len(messages) == 0:
+ user.message_set.create(message=msg_text)
+
+def clear_email_validation_message(user):
+ messages = find_email_validation_messages(user)
+ messages.delete()
+
+def set_new_email(user, new_email, nomessage=False):
+ if new_email != user.email:
+ user.email = new_email
+ user.email_isvalid = False
+ user.save()
+ if forum_settings.EMAIL_VALIDATION == True:
+ send_new_email_key(user,nomessage=nomessage)
+
+def _send_email_key(user):
+ """private function. sends email containing validation key
+ to user's email address
+ """
+ subject = _("Email verification subject line")
+ message_template = loader.get_template('authopenid/email_validation.txt')
+ import settings
+ message_context = Context({
+ 'validation_link': forum_settings.APP_URL + reverse('user_verifyemail', kwargs={'id':user.id,'key':user.email_key})
+ })
+ message = message_template.render(message_context)
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email])
+
+def send_new_email_key(user,nomessage=False):
+ import random
+ random.seed()
+ user.email_key = '%032x' % random.getrandbits(128)
+ user.save()
+ _send_email_key(user)
+ if nomessage==False:
+ set_email_validation_message(user)
+
+@login_required
+def send_email_key(request):
+ """
+ url = /email/sendkey/
+
+ view that is shown right after sending email key
+ email sending is called internally
+
+ raises 404 if email validation is off
+ if current email is valid shows 'key_not_sent' view of
+ authopenid/changeemail.html template
+ """
+
+ if forum_settings.EMAIL_VALIDATION == True:
+ if request.user.email_isvalid:
+ return render_to_response('authopenid/changeemail.html',
+ { 'email': request.user.email,
+ 'action_type': 'key_not_sent',
+ 'change_link': reverse('user_changeemail')},
+ context_instance=RequestContext(request)
+ )
+ else:
+ send_new_email_key(request.user)
+ return validation_email_sent(request)
+ else:
+ raise Http404
+
+
+#internal server view used as return value by other views
+def validation_email_sent(request):
+ logging.debug('')
+ return render_to_response('authopenid/changeemail.html',
+ { 'email': request.user.email,
+ 'change_email_url': reverse('user_changeemail'),
+ 'action_type': 'validate', },
+ context_instance=RequestContext(request))
+
+def verifyemail(request,id=None,key=None):
+ """
+ view that is shown when user clicks email validation link
+ url = /email/verify/{{user.id}}/{{user.email_key}}/
+ """
+ logging.debug('')
+ if forum.settings.EMAIL_VALIDATION == True:
+ user = User.objects.get(id=id)
+ if user:
+ if user.email_key == key:
+ user.email_isvalid = True
+ clear_email_validation_message(user)
+ user.save()
+ return render_to_response('authopenid/changeemail.html', {
+ 'action_type': 'validation_complete',
+ }, context_instance=RequestContext(request))
+ else:
+ logging.error('hmm, no user found for email validation message - foul play?')
+ raise Http404
+
+@login_required
+def changeemail(request, action='change'):
+ """
+ changeemail view. requires openid with request type GET
+
+ url: /email/*
+
+ template : authopenid/changeemail.html
+ """
+ logging.debug('')
+ msg = request.GET.get('msg', None)
+ extension_args = {}
+ user_ = request.user
+
+ if request.POST:
+ if 'cancel' in request.POST:
+ msg = _('your email was not changed')
+ request.user.message_set.create(message=msg)
+ return HttpResponseRedirect(get_next_url(request))
+ form = ChangeEmailForm(request.POST, user=user_)
+ if form.is_valid():
+ new_email = form.cleaned_data['email']
+ if new_email != user_.email:
+ if forum_settings.EMAIL_VALIDATION == True:
+ action = 'validate'
+ else:
+ action = 'done_novalidate'
+ set_new_email(user_, new_email,nomessage=True)
+ else:
+ action = 'keep'
+
+ elif not request.POST and 'openid.mode' in request.GET:
+ redirect_to = get_url_host(request) + reverse('user_changeemail')
+ return complete(request, emailopenid_success,
+ emailopenid_failure, redirect_to)
+ else:
+ form = ChangeEmailForm(initial={'email': user_.email},
+ user=user_)
+
+ output = render_to_response('authopenid/changeemail.html', {
+ 'form': form,
+ 'email': user_.email,
+ 'action_type': action,
+ 'gravatar_faq_url': reverse('faq') + '#gravatar',
+ 'change_email_url': reverse('user_changeemail'),
+ 'msg': msg
+ }, context_instance=RequestContext(request))
+
+ if action == 'validate':
+ set_email_validation_message(user_)
+
+ return output
+
+def emailopenid_success(request, identity_url, openid_response):
+ logging.debug('')
+ openid_ = from_openid_response(openid_response)
+
+ user_ = request.user
+ try:
+ uassoc = UserAssociation.objects.get(
+ openid_url__exact=identity_url
+ )
+ except:
+ return emailopenid_failure(request,
+ _("No OpenID %s found associated in our database" % identity_url))
+
+ if uassoc.user.username != request.user.username:
+ return emailopenid_failure(request,
+ _("The OpenID %s isn't associated to current user logged in" %
+ identity_url))
+
+ new_email = request.session.get('new_email', '')
+ if new_email:
+ user_.email = new_email
+ user_.save()
+ del request.session['new_email']
+ msg = _("Email Changed.")
+
+ redirect = "%s?msg=%s" % (reverse('user_account_settings'),
+ urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+
+
+def emailopenid_failure(request, message):
+ logging.debug('')
+ redirect_to = "%s?msg=%s" % (
+ reverse('user_changeemail'), urlquote_plus(message))
+ return HttpResponseRedirect(redirect_to)
+
+@login_required
+def changeopenid(request):
+ """
+ change openid view. Allow user to change openid
+ associated to its username.
+
+ url : /changeopenid/
+
+ template: authopenid/changeopenid.html
+ """
+ logging.error('change openid view - never tested it yet!!!')
+
+ extension_args = {}
+ openid_url = ''
+ has_openid = True
+ msg = request.GET.get('msg', '')
+
+ user_ = request.user
+
+ try:
+ uopenid = UserAssociation.objects.get(user=user_)
+ openid_url = uopenid.openid_url
+ except:
+ has_openid = False
+
+ redirect_to = get_url_host(request) + reverse('user_changeopenid')
+ if request.POST and has_openid:
+ form = ChangeopenidForm(request.POST, user=user_)
+ if form.is_valid():
+ return ask_openid(request, form.cleaned_data['openid_url'],
+ redirect_to, on_failure=changeopenid_failure)
+ elif not request.POST and has_openid:
+ if 'openid.mode' in request.GET:
+ return complete(request, changeopenid_success,
+ changeopenid_failure, redirect_to)
+
+ form = ChangeopenidForm(initial={'openid_url': openid_url }, user=user_)
+ return render_to_response('authopenid/changeopenid.html', {
+ 'form': form,
+ 'has_openid': has_openid,
+ 'msg': msg
+ }, context_instance=RequestContext(request))
+
+def changeopenid_success(request, identity_url, openid_response):
+ logging.error('never tested this worflow')
+ openid_ = from_openid_response(openid_response)
+ is_exist = True
+ try:
+ uassoc = UserAssociation.objects.get(openid_url__exact=identity_url)
+ except:
+ is_exist = False
+
+ if not is_exist:
+ try:
+ uassoc = UserAssociation.objects.get(
+ user__username__exact=request.user.username
+ )
+ uassoc.openid_url = identity_url
+ uassoc.save()
+ except:
+ uassoc = UserAssociation(user=request.user,
+ openid_url=identity_url)
+ uassoc.save()
+ elif uassoc.user.username != request.user.username:
+ return changeopenid_failure(request,
+ _('This OpenID is already associated with another account.'))
+
+ request.session['openids'] = []
+ request.session['openids'].append(openid_)
+
+ msg = _("OpenID %s is now associated with your account." % identity_url)
+ redirect = "%s?msg=%s" % (
+ reverse('user_account_settings'),
+ urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+
+
+def changeopenid_failure(request, message):
+ logging.error('never tested this workflow')
+ redirect_to = "%s?msg=%s" % (
+ reverse('user_changeopenid'),
+ urlquote_plus(message))
+ return HttpResponseRedirect(redirect_to)
+
+@login_required
+def delete(request):
+ """
+ delete view. Allow user to delete its account. Password/openid are required to
+ confirm it. He should also check the confirm checkbox.
+
+ url : /delete
+
+ template : authopenid/delete.html
+ """
+ logging.error('deleting account - never tested this')
+
+ extension_args = {}
+
+ user_ = request.user
+
+ redirect_to = get_url_host(request) + reverse('user_delete')
+ if request.POST:
+ form = DeleteForm(request.POST, user=user_)
+ if form.is_valid():
+ if not form.test_openid:
+ user_.delete()
+ return signout(request)
+ else:
+ return ask_openid(request, form.cleaned_data['password'],
+ redirect_to, on_failure=deleteopenid_failure)
+ elif not request.POST and 'openid.mode' in request.GET:
+ return complete(request, deleteopenid_success, deleteopenid_failure,
+ redirect_to)
+
+ form = DeleteForm(user=user_)
+
+ msg = request.GET.get('msg','')
+ return render_to_response('authopenid/delete.html', {
+ 'form': form,
+ 'msg': msg,
+ }, context_instance=RequestContext(request))
+
+def deleteopenid_success(request, identity_url, openid_response):
+ logging.error('never tested this')
+ openid_ = from_openid_response(openid_response)
+
+ user_ = request.user
+ try:
+ uassoc = UserAssociation.objects.get(
+ openid_url__exact=identity_url
+ )
+ except:
+ return deleteopenid_failure(request,
+ _("No OpenID %s found associated in our database" % identity_url))
+
+ if uassoc.user.username == user_.username:
+ user_.delete()
+ return signout(request)
+ else:
+ return deleteopenid_failure(request,
+ _("The OpenID %s isn't associated to current user logged in" %
+ identity_url))
+
+ msg = _("Account deleted.")
+ redirect = reverse('index') + u"/?msg=%s" % (urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+
+
+def deleteopenid_failure(request, message):
+ logging.error('never tested this')
+ redirect_to = "%s?msg=%s" % (reverse('user_delete'), urlquote_plus(message))
+ return HttpResponseRedirect(redirect_to)
+
+def external_legacy_login_info(request):
+ logging.debug('maybe this view does not belong in this library')
+ feedback_url = reverse('feedback')
+ return render_to_response('authopenid/external_legacy_login_info.html',
+ {'feedback_url':feedback_url},
+ context_instance=RequestContext(request))
+
+def sendpw(request):
+ """
+ send a new password to the user. It return a mail with
+ a new pasword and a confirm link in. To activate the
+ new password, the user should click on confirm link.
+
+ url : /sendpw/
+
+ templates : authopenid/sendpw_email.txt, authopenid/sendpw.html
+ """
+ logging.debug('')
+ if settings.USE_EXTERNAL_LEGACY_LOGIN == True:
+ logging.debug('delegating to view dealing with external password recovery')
+ return HttpResponseRedirect(reverse('user_external_legacy_login_issues'))
+
+ msg = request.GET.get('msg','')
+ logging.debug('request method is %s' % request.method)
+ if request.method == 'POST':
+ form = EmailPasswordForm(request.POST)
+ if form.is_valid():
+ logging.debug('EmailPasswordForm is valid')
+ new_pw = User.objects.make_random_password()
+ confirm_key = UserPasswordQueue.objects.get_new_confirm_key()
+ try:
+ uqueue = UserPasswordQueue.objects.get(
+ user=form.user_cache
+ )
+ except:
+ uqueue = UserPasswordQueue(
+ user=form.user_cache
+ )
+ uqueue.new_password = new_pw
+ uqueue.confirm_key = confirm_key
+ uqueue.save()
+ # send email
+ subject = _("Request for new password")
+ message_template = loader.get_template(
+ 'authopenid/sendpw_email.txt')
+ key_link = forum_settings.APP_URL + reverse('user_confirmchangepw') + '?key=' + confirm_key
+ logging.debug('emailing new password for %s' % form.user_cache.username)
+ message_context = Context({
+ 'site_url': forum_settings.APP_URL + reverse('index'),
+ 'key_link': key_link,
+ 'username': form.user_cache.username,
+ 'password': new_pw,
+ })
+ message = message_template.render(message_context)
+ send_mail(subject, message, settings.DEFAULT_FROM_EMAIL,
+ [form.user_cache.email])
+ msg = _("A new password and the activation link were sent to your email address.")
+ else:
+ form = EmailPasswordForm()
+
+ logging.debug('showing reset password form')
+ return render_to_response('authopenid/sendpw.html', {
+ 'form': form,
+ 'msg': msg
+ }, context_instance=RequestContext(request))
+
+def confirmchangepw(request):
+ """
+ view to set new password when the user click on confirm link
+ in its mail. Basically it check if the confirm key exist, then
+ replace old password with new password and remove confirm
+ ley from the queue. Then it redirect the user to signin
+ page.
+
+ url : /sendpw/confirm/?key
+
+ """
+ logging.debug('')
+ confirm_key = request.GET.get('key', '')
+ if not confirm_key:
+ logging.error('someone called confirm password without a key!')
+ return HttpResponseRedirect(reverse('index'))
+
+ try:
+ uqueue = UserPasswordQueue.objects.get(
+ confirm_key__exact=confirm_key
+ )
+ except:
+ msg = _("Could not change password. Confirmation key '%s'\
+ is not registered." % confirm_key)
+ logging.error(msg)
+ redirect = "%s?msg=%s" % (
+ reverse('user_sendpw'), urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+
+ try:
+ user_ = User.objects.get(id=uqueue.user.id)
+ except:
+ msg = _("Can not change password. User don't exist anymore \
+ in our database.")
+ logging.error(msg)
+ redirect = "%s?msg=%s" % (reverse('user_sendpw'),
+ urlquote_plus(msg))
+ return HttpResponseRedirect(redirect)
+
+ user_.set_password(uqueue.new_password)
+ user_.save()
+ uqueue.delete()
+ msg = _("Password changed for %s. You may now sign in." %
+ user_.username)
+ logging.debug(msg)
+ redirect = "%s?msg=%s" % (reverse('user_signin'),
+ urlquote_plus(msg))
+
+ return HttpResponseRedirect(redirect)
diff --git a/forum/deps/livesettings/README b/forum/deps/livesettings/README
new file mode 100644
index 00000000..6fe70cc5
--- /dev/null
+++ b/forum/deps/livesettings/README
@@ -0,0 +1,4 @@
+this is very slightly forked version of django-livesettings
+for use in the askbot forum project
+
+will attempt to re-merge into the original django-livesettings
diff --git a/forum/deps/livesettings/__init__.py b/forum/deps/livesettings/__init__.py
new file mode 100644
index 00000000..49aaacc9
--- /dev/null
+++ b/forum/deps/livesettings/__init__.py
@@ -0,0 +1,16 @@
+"""Database persistent administrative settings with defaults.
+
+This code is a large fork of the excellent "dbsettings" code found at
+http://code.google.com/p/django-values/
+
+The items set here are intended to be changeable during runtime, and do not require a
+programmer to test or install.
+
+Appropriate: Your google code for adwords.
+Inappropriate: The keyedcache timeout for the store.
+
+"""
+
+from functions import *
+from models import *
+from values import * \ No newline at end of file
diff --git a/forum/deps/livesettings/forms.py b/forum/deps/livesettings/forms.py
new file mode 100644
index 00000000..f8d40895
--- /dev/null
+++ b/forum/deps/livesettings/forms.py
@@ -0,0 +1,38 @@
+from django import forms
+from forum.deps.livesettings import *
+import logging
+
+log = logging.getLogger('configuration')
+
+class SettingsEditor(forms.Form):
+ "Base editor, from which customized forms are created"
+
+ def __init__(self, *args, **kwargs):
+ settings = kwargs.pop('settings')
+ super(SettingsEditor, self).__init__(*args, **kwargs)
+ flattened = []
+ groups = []
+ for setting in settings:
+ if isinstance(setting, ConfigurationGroup):
+ for s in setting:
+ flattened.append(s)
+ else:
+ flattened.append(setting)
+
+ for setting in flattened:
+ # Add the field to the customized field list
+ kw = {
+ 'label': setting.description,
+ 'help_text': setting.help_text,
+ # Provide current setting values for initializing the form
+ 'initial': setting.editor_value
+ }
+ field = setting.make_field(**kw)
+
+ k = '%s__%s' % (setting.group.key, setting.key)
+ self.fields[k] = field
+ if not setting.group in groups:
+ groups.append(setting.group)
+ #log.debug("Added field: %s = %s" % (k, str(field)))
+
+ self.groups = groups \ No newline at end of file
diff --git a/forum/deps/livesettings/functions.py b/forum/deps/livesettings/functions.py
new file mode 100644
index 00000000..4c2073ea
--- /dev/null
+++ b/forum/deps/livesettings/functions.py
@@ -0,0 +1,247 @@
+from django.utils.translation import ugettext
+from forum.deps.livesettings import values
+from forum.deps.livesettings.models import SettingNotSet
+from forum.deps.livesettings.utils import is_string_like
+
+import logging
+
+log = logging.getLogger('configuration')
+
+_NOTSET = object()
+
+class ConfigurationSettings(object):
+ """A singleton manager for ConfigurationSettings"""
+
+ class __impl(object):
+ def __init__(self):
+ self.settings = values.SortedDotDict()
+ self.prereg = {}
+
+ def __getitem__(self, key):
+ """Get an element either by ConfigurationGroup object or by its key"""
+ key = self._resolve_key(key)
+ return self.settings.get(key)
+
+ def __getattr__(self, key):
+ """Get an element either by ConfigurationGroup object or by its key"""
+ try:
+ return self[key]
+ except:
+ raise AttributeError, key
+
+ def __iter__(self):
+ for v in self.groups():
+ yield v
+
+ def __len__(self):
+ return len(self.settings)
+
+ def __contains__(self, key):
+ try:
+ key = self._resolve_key(key)
+ return self.settings.has_key(key)
+ except:
+ return False
+
+ def _resolve_key(self, raw):
+ if is_string_like(raw):
+ key = raw
+
+ elif isinstance(raw, values.ConfigurationGroup):
+ key = raw.key
+
+ else:
+ group = self.groups()[raw]
+ key = group.key
+
+ return key
+
+ def get_config(self, group, key):
+ try:
+ if isinstance(group, values.ConfigurationGroup):
+ group = group.key
+
+ cg = self.settings.get(group, None)
+ if not cg:
+ raise SettingNotSet('%s config group does not exist' % group)
+
+ else:
+ return cg[key]
+ except KeyError:
+ raise SettingNotSet('%s.%s' % (group, key))
+
+ def groups(self):
+ """Return ordered list"""
+ return self.settings.values()
+
+ def has_config(self, group, key):
+ if isinstance(group, values.ConfigurationGroup):
+ group = group.key
+
+ cfg = self.settings.get(group, None)
+ if cfg and key in cfg:
+ return True
+ else:
+ return False
+
+ def preregister_choice(self, group, key, choice):
+ """Setup a choice for a group/key which hasn't been instantiated yet."""
+ k = (group, key)
+ if self.prereg.has_key(k):
+ self.prereg[k].append(choice)
+ else:
+ self.prereg[k] = [choice]
+
+ def register(self, value):
+ g = value.group
+ if not isinstance(g, values.ConfigurationGroup):
+ raise ValueError('value.group should be an instance of ConfigurationGroup')
+
+ groupkey = g.key
+ valuekey = value.key
+
+ k = (groupkey, valuekey)
+ if self.prereg.has_key(k):
+ for choice in self.prereg[k]:
+ value.add_choice(choice)
+
+ if not groupkey in self.settings:
+ self.settings[groupkey] = g
+
+ self.settings[groupkey][valuekey] = value
+
+ return value
+
+ __instance = None
+
+ def __init__(self):
+ if ConfigurationSettings.__instance is None:
+ ConfigurationSettings.__instance = ConfigurationSettings.__impl()
+ #ConfigurationSettings.__instance.load_app_configurations()
+
+ self.__dict__['_ConfigurationSettings__instance'] = ConfigurationSettings.__instance
+
+ def __getattr__(self, attr):
+ """ Delegate access to implementation """
+ return getattr(self.__instance, attr)
+
+ def __getitem__(self, key):
+ return self.__instance[key]
+
+ def __len__(self):
+ return len(self.__instance)
+
+ def __setattr__(self, attr, value):
+ """ Delegate access to implementation """
+ return setattr(self.__instance, attr, value)
+
+ def __unicode__(self):
+ return u"ConfigurationSettings: " + unicode(self.groups())
+
+def config_exists(group, key):
+ """Test to see if a setting has been registered"""
+
+ return ConfigurationSettings().has_config(group, key)
+
+def config_get(group, key):
+ """Get a configuration setting"""
+ try:
+ return ConfigurationSettings().get_config(group, key)
+ except SettingNotSet:
+ log.debug('SettingNotSet: %s.%s', group, key)
+ raise
+
+def config_get_group(group):
+ return ConfigurationSettings()[group]
+
+def config_collect_values(group, groupkey, key, unique=True, skip_missing=True):
+ """Look up (group, groupkey) from config, then take the values returned and
+ use them as groups for a second-stage lookup.
+
+ For example:
+
+ config_collect_values(PAYMENT, MODULES, CREDITCHOICES)
+
+ Stage 1: ['PAYMENT_GOOGLE', 'PAYMENT_AUTHORIZENET']
+ Stage 2: config_value('PAYMENT_GOOGLE', 'CREDITCHOICES')
+ + config_value('PAYMENT_AUTHORIZENET', 'CREDITCHOICES')
+ Stage 3: (if unique is true) remove dupes
+ """
+ groups = config_value(group, groupkey)
+
+ ret = []
+ for g in groups:
+ try:
+ ret.append(config_value(g, key))
+ except KeyError, ke:
+ if not skip_missing:
+ raise SettingNotSet('No config %s.%s' % (g, key))
+
+ if unique:
+ out = []
+ for x in ret:
+ if not x in out:
+ out.append(x)
+ ret = out
+
+ return ret
+
+def config_register(value):
+ """Register a value or values.
+
+ Parameters:
+ -A Value
+ """
+ return ConfigurationSettings().register(value)
+
+def config_register_list(*args):
+ for value in args:
+ config_register(value)
+
+def config_value(group, key, default=_NOTSET):
+ """Get a value from the configuration system"""
+ try:
+ return config_get(group, key).value
+ except SettingNotSet:
+ if default != _NOTSET:
+ return default
+ raise
+
+def config_value_safe(group, key, default_value):
+ """Get a config value with a default fallback, safe for use during SyncDB."""
+ raw = default_value
+
+ try:
+ raw = config_value(group, key)
+ except SettingNotSet:
+ pass
+ except ImportError, e:
+ log.warn("Error getting %s.%s, OK if you are in SyncDB.", group, key)
+
+ return raw
+
+
+def config_choice_values(group, key, skip_missing=True, translate=False):
+ """Get pairs of key, label from the setting."""
+ try:
+ cfg = config_get(group, key)
+ choices = cfg.choice_values
+
+ except SettingNotSet:
+ if skip_missing:
+ return []
+ else:
+ raise SettingNotSet('%s.%s' % (group, key))
+
+ if translate:
+ choices = [(k, ugettext(v)) for k, v in choices]
+
+ return choices
+
+def config_add_choice(group, key, choice):
+ """Add a choice to a value"""
+ if config_exists(group, key):
+ cfg = config_get(group, key)
+ cfg.add_choice(choice)
+ else:
+ ConfigurationSettings().preregister_choice(group, key, choice)
diff --git a/forum/deps/livesettings/locale/de/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..e176bc53
--- /dev/null
+++ b/forum/deps/livesettings/locale/de/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/de/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 00000000..1cef701b
--- /dev/null
+++ b/forum/deps/livesettings/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,101 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the Satchmo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-03-22 15:10+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: values.py:88
+msgid "Base Settings"
+msgstr "Basiseinstellungen"
+
+#: values.py:194
+msgid "Default value: \"\""
+msgstr "Standardwert: \"\""
+
+#: values.py:201
+msgid "Default value: "
+msgstr "Standardwert: "
+
+#: values.py:204
+#, python-format
+msgid "Default value: %s"
+msgstr "Standardwert: %s"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:10
+msgid "Home"
+msgstr "Start"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Log out"
+msgstr "Abmelden"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:18
+#, fuzzy
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+msgstr[1] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+#, fuzzy
+msgid "Documentation"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Change password"
+msgstr "Passwort ändern"
+
+#: templates/livesettings/site_settings.html:11
+msgid "Edit Site Settings"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr ""
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:61
+msgid "You don't have permission to edit values."
+msgstr ""
+
+#: templates/livesettings/site_settings.html:34
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr ""
+
diff --git a/forum/deps/livesettings/locale/en/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..c2bc0b94
--- /dev/null
+++ b/forum/deps/livesettings/locale/en/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/en/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 00000000..45eb23a5
--- /dev/null
+++ b/forum/deps/livesettings/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,100 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the Satchmo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-12-31 00:49-0600\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: values.py:88
+msgid "Base Settings"
+msgstr ""
+
+#: values.py:194
+msgid "Default value: \"\""
+msgstr ""
+
+#: values.py:201
+msgid "Default value: "
+msgstr ""
+
+#: values.py:204
+#, python-format
+msgid "Default value: %s"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:10
+msgid "Home"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Log out"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:18
+#, fuzzy
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+msgstr[1] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Documentation"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Change password"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr ""
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:61
+msgid "You don't have permission to edit values."
+msgstr ""
+
+#: templates/livesettings/site_settings.html:11
+msgid "Edit Site Settings"
+msgstr ""
+
+#: templates/livesettings/site_settings.html:34
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr ""
+
diff --git a/forum/deps/livesettings/locale/es/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/forum/deps/livesettings/locale/es/LC_MESSAGES/django.po
diff --git a/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..dd872edd
--- /dev/null
+++ b/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 00000000..90475585
--- /dev/null
+++ b/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,113 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# Jacques Moulin <jacques@tpi.be>, 2008.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-11-02 16:11+0100\n"
+"PO-Revision-Date: 2008-11-02 17:51+0100\n"
+"Last-Translator: Jacques Moulin <jacques@tpi.be>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-Language: French\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: templates/livesettings/group_settings.html.py:10
+#: templates/livesettings/site_settings.html.py:25
+#: templates/livesettings/group_settings.html.py:10
+#: templates/livesettings/site_settings.html.py:25
+msgid "Home"
+msgstr "Accueil"
+
+#: models.py:76
+#: models.py:115
+msgid "Site"
+msgstr "Site"
+
+#: values.py:94
+msgid "Base Settings"
+msgstr "Configuration de base"
+
+#: values.py:200
+msgid "Default value: \"\""
+msgstr "Valeur par défaut: \"\""
+
+#: values.py:207
+msgid "Default value: "
+msgstr "Valeur par défaut:"
+
+#: values.py:210
+#, python-format
+msgid "Default value: %s"
+msgstr "Valeur par défaut: %s"
+
+#: templates/livesettings/group_settings.html.py:7
+#: templates/livesettings/site_settings.html.py:22
+#: templates/livesettings/group_settings.html.py:7
+#: templates/livesettings/site_settings.html.py:22
+msgid "Documentation"
+msgstr "Documentation"
+
+#: templates/livesettings/group_settings.html.py:7
+#: templates/livesettings/site_settings.html.py:22
+#: templates/livesettings/group_settings.html.py:7
+#: templates/livesettings/site_settings.html.py:22
+msgid "Change password"
+msgstr "Modifier le mot de passe"
+
+#: templates/livesettings/group_settings.html.py:7
+#: templates/livesettings/site_settings.html.py:22
+#: templates/livesettings/group_settings.html.py:7
+#: templates/livesettings/site_settings.html.py:22
+msgid "Log out"
+msgstr "Se déconnecter"
+
+#: templates/livesettings/group_settings.html.py:11
+#: templates/livesettings/group_settings.html.py:11
+msgid "Edit Group Settings"
+msgstr "Editer les paramètres de groupe"
+
+#: templates/livesettings/group_settings.html.py:18
+#: templates/livesettings/site_settings.html.py:43
+#: templates/livesettings/group_settings.html.py:18
+#: templates/livesettings/site_settings.html.py:41
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] "Veuillez corriger l'erreur ci-dessous:"
+msgstr[1] "Veuillez corriger les erreurs ci-dessous:"
+
+#: templates/livesettings/group_settings.html.py:24
+#: templates/livesettings/group_settings.html.py:24
+msgid "Settings included in %(name)s."
+msgstr "Paramètres inclus dans %(name)s."
+
+#: templates/livesettings/group_settings.html.py:49
+#: templates/livesettings/site_settings.html.py:89
+#: templates/livesettings/group_settings.html.py:49
+#: templates/livesettings/site_settings.html.py:87
+msgid "You don't have permission to edit values."
+msgstr "Vous n'avez pas le droit d'éditer les valeurs."
+
+#: templates/livesettings/site_settings.html.py:26
+#: templates/livesettings/site_settings.html.py:26
+msgid "Edit Site Settings"
+msgstr "Editer les paramètres du site"
+
+#: templates/livesettings/site_settings.html.py:59
+#: templates/livesettings/site_settings.html.py:58
+msgid "Group settings: %(name)s"
+msgstr "Paramètres du groupe: %(name)s"
+
+#: templates/livesettings/site_settings.html.py:86
+#: templates/livesettings/site_settings.html.py:84
+msgid "Uncollapse all"
+msgstr "Déployer tout"
+
+#: templates/livesettings/_admin_site_views.html.py:5
+msgid "Sites"
+msgstr "Sites"
+
diff --git a/forum/deps/livesettings/locale/he/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/he/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..04270a04
--- /dev/null
+++ b/forum/deps/livesettings/locale/he/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/he/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/he/LC_MESSAGES/django.po
new file mode 100644
index 00000000..362f5612
--- /dev/null
+++ b/forum/deps/livesettings/locale/he/LC_MESSAGES/django.po
@@ -0,0 +1,98 @@
+# translation of Satchmo
+# Copyright (C) 2008 The Satchmo Project
+# This file is distributed under the same license as the Satchmo package.
+#
+# Aviv Greenberg <avivgr@gmail.com>, 2008.
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2009-03-13 23:02+0200\n"
+"PO-Revision-Date: 2009-03-22 07:45\n"
+"Last-Translator: Aviv Greenberg <avivgr@gmail.com>\n"
+"Language-Team: <en@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: KBabel 1.11.4\n"
+"X-Translated-Using: django-rosetta 0.4.0\n"
+
+#: models.py:75 models.py:114
+msgid "Site"
+msgstr "אתר"
+
+#: values.py:96
+msgid "Base Settings"
+msgstr "תצורה בסיסית"
+
+#: values.py:202
+msgid "Default value: \"\""
+msgstr "ברירת מחדל:\"\""
+
+#: values.py:209
+msgid "Default value: "
+msgstr "ברירת מחדל:"
+
+#: values.py:212
+#, python-format
+msgid "Default value: %s"
+msgstr "ברירת מחדל:%s"
+
+#: templates/livesettings/_admin_site_views.html:4
+msgid "Sites"
+msgstr "אתרים"
+
+#: templates/livesettings/group_settings.html:11
+#: templates/livesettings/site_settings.html:23
+msgid "Documentation"
+msgstr "תיעוד"
+
+#: templates/livesettings/group_settings.html:11
+#: templates/livesettings/site_settings.html:23
+msgid "Change password"
+msgstr "שינוי סיסמה"
+
+#: templates/livesettings/group_settings.html:11
+#: templates/livesettings/site_settings.html:23
+msgid "Log out"
+msgstr "יציאה"
+
+#: templates/livesettings/group_settings.html:14
+#: templates/livesettings/site_settings.html:26
+msgid "Home"
+msgstr "דף הבית"
+
+#: templates/livesettings/group_settings.html:15
+msgid "Edit Group Settings"
+msgstr "ערוך הגדרות קבוצה"
+
+#: templates/livesettings/group_settings.html:22
+#: templates/livesettings/site_settings.html:44
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] "נא לתקן את השגיאה המופיעה מתחת."
+msgstr[1] "נא לתקן את השגיאות המופיעות מתחת."
+
+#: templates/livesettings/group_settings.html:28
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "הגדרות כלולות %(name)s"
+
+#: templates/livesettings/group_settings.html:53
+#: templates/livesettings/site_settings.html:90
+msgid "You don't have permission to edit values."
+msgstr "אינך מורשה לערוך ערכים."
+
+#: templates/livesettings/site_settings.html:27
+msgid "Edit Site Settings"
+msgstr "ערוך הגדרות אתר"
+
+#: templates/livesettings/site_settings.html:60
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "הגדרות קבוצה: %(name)s"
+
+#: templates/livesettings/site_settings.html:87
+msgid "Uncollapse all"
+msgstr "הסתר פרטים"
diff --git a/forum/deps/livesettings/locale/it/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..05c50952
--- /dev/null
+++ b/forum/deps/livesettings/locale/it/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/it/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 00000000..66401866
--- /dev/null
+++ b/forum/deps/livesettings/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,106 @@
+# translation of django.po to Italiano
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the PACKAGE package.
+#
+# costantino giuliodori <costantino.giuliodori@gmail.com>, 2007.
+# Alessandro Ronchi <alessandro.ronchi@soasi.com>, 2008.
+msgid ""
+msgstr ""
+"Project-Id-Version: django\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-09-27 09:16-0700\n"
+"PO-Revision-Date: 2008-09-30 13:13+0200\n"
+"Last-Translator: Alessandro Ronchi <alessandro.ronchi@soasi.com>\n"
+"Language-Team: Italiano <it@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: KBabel 1.11.4\n"
+"Plural-Forms: nplurals=2; plural=n > 1\n"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:25
+msgid "Home"
+msgstr "Pagina iniziale"
+
+#: models.py:76
+#: models.py:115
+msgid "Site"
+msgstr "Sito"
+
+#: values.py:94
+msgid "Base Settings"
+msgstr "Impostazioni base"
+
+#: values.py:200
+msgid "Default value: \"\""
+msgstr "Valore di default: \"\""
+
+#: values.py:207
+msgid "Default value: "
+msgstr "Valore di default: "
+
+#: values.py:210
+#, python-format
+msgid "Default value: %s"
+msgstr "Valore di default:%s"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+# translated = "Extra di spedizione"
+msgid "Documentation"
+msgstr "Documentazione"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Change password"
+msgstr "Cambia Password"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Log out"
+msgstr "Esci"
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "Modifica le impostazioni del Gruppo"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:43
+# translated = ""
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] "Correggi l'errore indicato di seguito."
+msgstr[1] "Correggi gli errori indicati di seguito."
+
+#: templates/livesettings/group_settings.html:24
+# translated = "Modificare le impostazioni di gruppo"
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "Impostazioni incluse in %(name)s."
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:89
+# translated = "Impostazioni incluse in% (nome) s."
+msgid "You don't have permission to edit values."
+msgstr "Non hai il permesso di modificare questi valori."
+
+#: templates/livesettings/site_settings.html:26
+# translated = "Non avete il permesso di modificare i valori."
+msgid "Edit Site Settings"
+msgstr "Modifica le impostazioni del sito"
+
+#: templates/livesettings/site_settings.html:59
+# translated = "Modifica impostazioni sito"
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "Impostazioni di gruppo: %(name)s"
+
+#: templates/livesettings/site_settings.html:86
+msgid "Uncollapse all"
+msgstr "Espandi tutti"
+
+#: templates/livesettings/_admin_site_views.html:5
+msgid "Sites"
+msgstr "Siti"
+
diff --git a/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..e0738605
--- /dev/null
+++ b/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.po
new file mode 100644
index 00000000..0dbd2d4d
--- /dev/null
+++ b/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.po
@@ -0,0 +1,100 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the Satchmo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-12-31 00:49-0600\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: values.py:88
+msgid "Base Settings"
+msgstr "기본 세팅"
+
+#: values.py:194
+msgid "Default value: \"\""
+msgstr "기본 값: \"\""
+
+#: values.py:201
+msgid "Default value: "
+msgstr "기본 값: "
+
+#: values.py:204
+#, python-format
+msgid "Default value: %s"
+msgstr "기본 값:%s"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:10
+msgid "Home"
+msgstr "홈"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Log out"
+msgstr "로그 아웃"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:18
+#, fuzzy
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+msgstr[1] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Documentation"
+msgstr "문서"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Change password"
+msgstr "패스워드 변경"
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "그룹설정 수정"
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "%(name)s을 포함한 설정"
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:61
+msgid "You don't have permission to edit values."
+msgstr "이 값을 수정할 권한이 없습니다."
+
+#: templates/livesettings/site_settings.html:11
+msgid "Edit Site Settings"
+msgstr "사이트 설정 수정"
+
+#: templates/livesettings/site_settings.html:34
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "그룹 설정: %(name)s"
+
diff --git a/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..f45e49ed
--- /dev/null
+++ b/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..1e7b4199
--- /dev/null
+++ b/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,97 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-09-03 18:10+0200\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: <jerzyk@jerzyk.com>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:25
+msgid "Home"
+msgstr "Strona startowa"
+
+#: models.py:76
+#: models.py:115
+msgid "Site"
+msgstr "Strona"
+
+#: values.py:93
+msgid "Base Settings"
+msgstr "Ustawienia podstawowe"
+
+#: values.py:199
+msgid "Default value: \"\""
+msgstr "Domyślna wartość: \"\""
+
+#: values.py:206
+msgid "Default value: "
+msgstr "Domyślna wartość: "
+
+#: values.py:209
+#, python-format
+msgid "Default value: %s"
+msgstr "Domyślna wartość: %s"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Documentation"
+msgstr "Dokumentacja"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Change password"
+msgstr "Zmiana hasła"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Log out"
+msgstr "Wyloguj"
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "Edycja Ustawień dla Grupy"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:43
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] "Proszę poprawić poniższy błąd."
+msgstr[1] "Proszę poprawić poniższe błędy."
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "Ustawienia w %(name)s."
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:89
+msgid "You don't have permission to edit values."
+msgstr "Nie masz uprawnień do zmiany tych wartości."
+
+#: templates/livesettings/site_settings.html:26
+msgid "Edit Site Settings"
+msgstr "Edytuj ustawienia serwisu"
+
+#: templates/livesettings/site_settings.html:59
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "Ustawienia grupy: %(name)s"
+
+#: templates/livesettings/site_settings.html:86
+msgid "Uncollapse all"
+msgstr "Rozwiń wszystko"
+
+#: templates/livesettings/_admin_site_views.html:5
+msgid "Sites"
+msgstr "Strony"
+
diff --git a/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..a8bfb8b2
--- /dev/null
+++ b/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 00000000..72d49df7
--- /dev/null
+++ b/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,100 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the PACKAGE package.
+# Terry Laundos Aguiar <terry@s1solucoes.com.br>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-09-05 23:50-0300\n"
+"PO-Revision-Date: 2008-09-05 23:51-0300\n"
+"Last-Translator: Terry Laundos Aguiar <terry@s1solucoes.com.br>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:25
+msgid "Home"
+msgstr "Inicial"
+
+#: models.py:76
+#: models.py:115
+#, fuzzy
+msgid "Site"
+msgstr "Estado"
+
+#: values.py:93
+msgid "Base Settings"
+msgstr "Configurações Iniciais"
+
+#: values.py:199
+msgid "Default value: \"\""
+msgstr "Valor padrão: \"\""
+
+#: values.py:206
+msgid "Default value: "
+msgstr "Valor padrão: "
+
+#: values.py:209
+#, python-format
+msgid "Default value: %s"
+msgstr "Valor padrão: %s"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Documentation"
+msgstr "Documentação"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Change password"
+msgstr "Mudar senha"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Log out"
+msgstr "Deslogar"
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "Editar preferências de grupo"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:43
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "Configurações inclusas no %(name)s."
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:89
+msgid "You don't have permission to edit values."
+msgstr "Você não tem permissão para editar valores."
+
+#: templates/livesettings/site_settings.html:26
+msgid "Edit Site Settings"
+msgstr "Editar configurações do site"
+
+#: templates/livesettings/site_settings.html:59
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "Configurações de grupo: %(name)s"
+
+#: templates/livesettings/site_settings.html:86
+#, fuzzy
+msgid "Uncollapse all"
+msgstr "Desmarcar todos"
+
+#: templates/livesettings/_admin_site_views.html:5
+#, fuzzy
+msgid "Sites"
+msgstr "Notas"
+
diff --git a/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..42e6074a
--- /dev/null
+++ b/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 00000000..a0db054b
--- /dev/null
+++ b/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,85 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the Satchmo package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Satchmo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-12-31 00:49-0600\n"
+"PO-Revision-Date: 2009-03-02 21:52+0300\n"
+"Last-Translator: Данил Семеленов <danil.mail@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language-Team: \n"
+
+#: values.py:88
+msgid "Base Settings"
+msgstr "Основные настройки"
+
+#: values.py:194
+msgid "Default value: \"\""
+msgstr "Значение по умолчанию: \"\""
+
+#: values.py:201
+msgid "Default value: "
+msgstr "Значение по умолчанию: "
+
+#: values.py:204
+#, python-format
+msgid "Default value: %s"
+msgstr "Значение по умолчанию: %s"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:10
+msgid "Home"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Log out"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:18
+#, fuzzy
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] ""
+msgstr[1] ""
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Documentation"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Change password"
+msgstr ""
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "Изменить группу настроек"
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "Настройки включены в %(name)s."
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:61
+msgid "You don't have permission to edit values."
+msgstr "У вас нет разрешения изменять значение."
+
+#: templates/livesettings/site_settings.html:11
+msgid "Edit Site Settings"
+msgstr "Изменить настройки сайта"
+
+#: templates/livesettings/site_settings.html:34
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "Группа настроек: %(name)s"
+
diff --git a/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..caed0ab9
--- /dev/null
+++ b/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.po
new file mode 100644
index 00000000..6b096f6b
--- /dev/null
+++ b/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.po
@@ -0,0 +1,92 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the PACKAGE package.
+# N.L. <kotorinl@yahoo.co.uk>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Satchmo svn\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2008-04-30 23:40+0200\n"
+"PO-Revision-Date: 2008-04-30 23:35+0100\n"
+"Last-Translator: N.L. <kotorinl@yahoo.co.uk>\n"
+"Language-Team: Group\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Language: Swedish\n"
+"X-Poedit-Basepath: ../../../\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-Country: SWEDEN\n"
+
+#: values.py:89
+msgid "Base Settings"
+msgstr "Grundinställningar"
+
+#: values.py:195
+msgid "Default value: \"\""
+msgstr "Förvalt värde: \"\""
+
+#: values.py:202
+msgid "Default value: "
+msgstr "Förvalt värde:"
+
+#: values.py:205
+#, python-format
+msgid "Default value: %s"
+msgstr "Förvalt värde: %s"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:25
+msgid "Home"
+msgstr "Hem"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Log out"
+msgstr "Logga ut"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:41
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] "Var god rätta till felet nedan."
+msgstr[1] "Var god rätta till felen nedan."
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Documentation"
+msgstr "Dokumentation"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:22
+msgid "Change password"
+msgstr "Byt lösenord"
+
+#: templates/livesettings/site_settings.html:26
+msgid "Edit Site Settings"
+msgstr "Ändra sajtinställningar"
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "Redigera gruppinställningar"
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "Inställningar som ingår i %(name)s."
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:87
+msgid "You don't have permission to edit values."
+msgstr "Du har inte tillåtelse att ändra värden."
+
+#: templates/livesettings/site_settings.html:58
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "Gruppinställningar: %(name)s"
+
+#: templates/livesettings/site_settings.html:84
+msgid "Uncollapse all"
+msgstr "Visa alla"
+
diff --git a/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.mo b/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..d56ad423
--- /dev/null
+++ b/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.po b/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.po
new file mode 100644
index 00000000..bb2a1506
--- /dev/null
+++ b/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.po
@@ -0,0 +1,102 @@
+# Satchmo Translation Package
+# Copyright (C) 2008 Satchmo Project
+# This file is distributed under the same license as the Satchmo package.
+# Selin Çuhadar <selincuhadar@gmail.com>, 2008.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: Satchmo\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2007-12-31 00:49-0600\n"
+"PO-Revision-Date: 2008-06-09 18:18+0200\n"
+"Last-Translator: Selin Çuhadar <selincuhadar@gmail.com>\n"
+"Language-Team: Turkish <selincuhadar@gmail.com>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Poedit-Country: TURKEY\n"
+"X-Poedit-SourceCharset: utf-8\n"
+
+#: values.py:88
+msgid "Base Settings"
+msgstr "Temel Ayarlar"
+
+#: values.py:194
+msgid "Default value: \"\""
+msgstr "Geçerli Değer: \"\""
+
+#: values.py:201
+msgid "Default value: "
+msgstr "Geçerli Değer:"
+
+#: values.py:204
+#, python-format
+msgid "Default value: %s"
+msgstr "Geçerli Değer: %s"
+
+#: templates/livesettings/group_settings.html:10
+#: templates/livesettings/site_settings.html:10
+msgid "Home"
+msgstr "Ev"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Log out"
+msgstr "Oturumu kapa"
+
+#: templates/livesettings/group_settings.html:18
+#: templates/livesettings/site_settings.html:18
+#, fuzzy
+msgid "Please correct the error below."
+msgid_plural "Please correct the errors below."
+msgstr[0] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+msgstr[1] ""
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+"#-#-#-#-# django.pot (PACKAGE VERSION) #-#-#-#-#\n"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Documentation"
+msgstr "Dokümentasyon"
+
+#: templates/livesettings/group_settings.html:7
+#: templates/livesettings/site_settings.html:7
+msgid "Change password"
+msgstr "Şifreyi değiştir"
+
+#: templates/livesettings/group_settings.html:11
+msgid "Edit Group Settings"
+msgstr "Grup Ayarlarını Düzenle"
+
+#: templates/livesettings/group_settings.html:24
+#, python-format
+msgid "Settings included in %(name)s."
+msgstr "%(name)s ayarlara dahil edildi."
+
+#: templates/livesettings/group_settings.html:49
+#: templates/livesettings/site_settings.html:61
+msgid "You don't have permission to edit values."
+msgstr "Değerleri düzenlemek için gerekli izniniz yok."
+
+#: templates/livesettings/site_settings.html:11
+msgid "Edit Site Settings"
+msgstr "Site Ayarlarını Düzenle"
+
+#: templates/livesettings/site_settings.html:34
+#, python-format
+msgid "Group settings: %(name)s"
+msgstr "Grup ayarları: %(name)s"
+
diff --git a/forum/deps/livesettings/models.py b/forum/deps/livesettings/models.py
new file mode 100644
index 00000000..5d27033c
--- /dev/null
+++ b/forum/deps/livesettings/models.py
@@ -0,0 +1,170 @@
+from django.conf import settings
+from django.contrib.sites.models import Site
+from django.db import models
+from django.db.models import loading
+from django.utils.translation import ugettext_lazy as _
+from keyedcache import cache_key, cache_get, cache_set, NotCachedError
+from keyedcache.models import CachedObjectMixin
+from forum.deps.livesettings.overrides import get_overrides
+import logging
+
+log = logging.getLogger('configuration.models')
+
+__all__ = ['SettingNotSet', 'Setting', 'LongSetting', 'find_setting']
+
+def _safe_get_siteid(site):
+ if not site:
+ try:
+ site = Site.objects.get_current()
+ siteid = site.id
+ except:
+ siteid = settings.SITE_ID
+ else:
+ siteid = site.id
+ return siteid
+
+def find_setting(group, key, site=None):
+ """Get a setting or longsetting by group and key, cache and return it."""
+
+ siteid = _safe_get_siteid(site)
+ setting = None
+
+ use_db, overrides = get_overrides(siteid)
+ ck = cache_key('Setting', siteid, group, key)
+
+ if use_db:
+ try:
+ setting = cache_get(ck)
+
+ except NotCachedError, nce:
+ if loading.app_cache_ready():
+ try:
+ setting = Setting.objects.get(site__id__exact=siteid, key__exact=key, group__exact=group)
+
+ except Setting.DoesNotExist:
+ # maybe it is a "long setting"
+ try:
+ setting = LongSetting.objects.get(site__id__exact=siteid, key__exact=key, group__exact=group)
+
+ except LongSetting.DoesNotExist:
+ pass
+
+ cache_set(ck, value=setting)
+
+ else:
+ grp = overrides.get(group, None)
+ if grp and grp.has_key(key):
+ val = grp[key]
+ setting = ImmutableSetting(key=key, group=group, value=val)
+ log.debug('Returning overridden: %s', setting)
+
+ if not setting:
+ raise SettingNotSet(key, cachekey=ck)
+
+ return setting
+
+class SettingNotSet(Exception):
+ def __init__(self, k, cachekey=None):
+ self.key = k
+ self.cachekey = cachekey
+ self.args = [self.key, self.cachekey]
+
+class SettingManager(models.Manager):
+ def get_query_set(self):
+ all = super(SettingManager, self).get_query_set()
+ siteid = _safe_get_siteid(None)
+ return all.filter(site__id__exact=siteid)
+
+
+class ImmutableSetting(object):
+
+ def __init__(self, group="", key="", value="", site=1):
+ self.site = site
+ self.group = group
+ self.key = key
+ self.value = value
+
+ def cache_key(self, *args, **kwargs):
+ return cache_key('OverrideSetting', self.site, self.group, self.key)
+
+ def delete(self):
+ pass
+
+ def save(self, *args, **kwargs):
+ pass
+
+ def __repr__(self):
+ return "ImmutableSetting: %s.%s=%s" % (self.group, self.key, self.value)
+
+
+class Setting(models.Model, CachedObjectMixin):
+ site = models.ForeignKey(Site, verbose_name=_('Site'))
+ group = models.CharField(max_length=100, blank=False, null=False)
+ key = models.CharField(max_length=100, blank=False, null=False)
+ value = models.CharField(max_length=255, blank=True)
+
+ objects = SettingManager()
+
+ def __nonzero__(self):
+ return self.id is not None
+
+ def cache_key(self, *args, **kwargs):
+ return cache_key('Setting', self.site, self.group, self.key)
+
+ def delete(self):
+ self.cache_delete()
+ super(Setting, self).delete()
+
+ def save(self, force_insert=False, force_update=False):
+ try:
+ site = self.site
+ except Site.DoesNotExist:
+ self.site = Site.objects.get_current()
+
+ super(Setting, self).save(force_insert=force_insert, force_update=force_update)
+
+ self.cache_set()
+
+ class Meta:
+ unique_together = ('site', 'group', 'key')
+
+
+class LongSettingManager(models.Manager):
+ def get_query_set(self):
+ all = super(LongSettingManager, self).get_query_set()
+ siteid = _safe_get_siteid(None)
+ return all.filter(site__id__exact=siteid)
+
+class LongSetting(models.Model, CachedObjectMixin):
+ """A Setting which can handle more than 255 characters"""
+ site = models.ForeignKey(Site, verbose_name=_('Site'))
+ group = models.CharField(max_length=100, blank=False, null=False)
+ key = models.CharField(max_length=100, blank=False, null=False)
+ value = models.TextField(blank=True)
+
+ objects = LongSettingManager()
+
+ def __nonzero__(self):
+ return self.id is not None
+
+ def cache_key(self, *args, **kwargs):
+ # note same cache pattern as Setting. This is so we can look up in one check.
+ # they can't overlap anyway, so this is moderately safe. At the worst, the
+ # Setting will override a LongSetting.
+ return cache_key('Setting', self.site, self.group, self.key)
+
+ def delete(self):
+ self.cache_delete()
+ super(LongSetting, self).delete()
+
+ def save(self, force_insert=False, force_update=False):
+ try:
+ site = self.site
+ except Site.DoesNotExist:
+ self.site = Site.objects.get_current()
+ super(LongSetting, self).save(force_insert=force_insert, force_update=force_update)
+ self.cache_set()
+
+ class Meta:
+ unique_together = ('site', 'group', 'key')
+
diff --git a/forum/deps/livesettings/overrides.py b/forum/deps/livesettings/overrides.py
new file mode 100644
index 00000000..58c1079b
--- /dev/null
+++ b/forum/deps/livesettings/overrides.py
@@ -0,0 +1,55 @@
+"""Allows forum.deps.livesettings to be "locked down" and no longer use the settings page or the database
+for settings retrieval.
+"""
+
+from django.conf import settings as djangosettings
+from django.contrib.sites.models import Site
+import logging
+
+__all__ = ['get_overrides']
+
+def _safe_get_siteid(site):
+ if not site:
+ try:
+ site = Site.objects.get_current()
+ siteid = site.id
+ except:
+ siteid = djangosettings.SITE_ID
+ else:
+ siteid = site.id
+ return siteid
+
+def get_overrides(siteid=-1):
+ """Check to see if forum.deps.livesettings is allowed to use the database. If not, then
+ it will only use the values in the dictionary, LIVESETTINGS_OPTIONS[SITEID]['SETTINGS'],
+ this allows 'lockdown' of a live site.
+
+ The LIVESETTINGS dict must be formatted as follows::
+
+ LIVESETTINGS_OPTIONS = {
+ 1 : {
+ 'DB' : [True/False],
+ SETTINGS = {
+ 'GROUPKEY' : {'KEY', val, 'KEY2', val},
+ 'GROUPKEY2' : {'KEY', val, 'KEY2', val},
+ }
+ }
+ }
+
+ In the settings dict above, the "val" entries must exactly match the format
+ stored in the database for a setting. Do not use a literal True or an integer,
+ it needs to be the string representation of them.
+
+ Returns a tuple (DB_ALLOWED, SETTINGS)
+ """
+ overrides = (True, {})
+ if hasattr(djangosettings, 'LIVESETTINGS_OPTIONS'):
+ if siteid == -1:
+ siteid = _safe_get_siteid(None)
+
+ opts = djangosettings.LIVESETTINGS_OPTIONS
+ if opts.has_key(siteid):
+ opts = opts[siteid]
+ overrides = (opts.get('DB', True), opts['SETTINGS'])
+
+ return overrides
diff --git a/forum/deps/livesettings/signals.py b/forum/deps/livesettings/signals.py
new file mode 100644
index 00000000..ddea31f5
--- /dev/null
+++ b/forum/deps/livesettings/signals.py
@@ -0,0 +1,3 @@
+import django.dispatch
+
+configuration_value_changed = django.dispatch.Signal()
diff --git a/forum/deps/livesettings/templates/livesettings/_admin_site_views.html b/forum/deps/livesettings/templates/livesettings/_admin_site_views.html
new file mode 100644
index 00000000..17d08f58
--- /dev/null
+++ b/forum/deps/livesettings/templates/livesettings/_admin_site_views.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+<div id="content-related">
+ <div class="module" id="sites-module">
+ <h2 class="module-title">{% trans 'Sites' %}</h2>
+ <div class="module-content">
+ <ul>
+ {% for label, link in links %}
+ <li>
+ <a href="{{ link }}">{{ label }}</a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+</div>
diff --git a/forum/deps/livesettings/templates/livesettings/group_settings.html b/forum/deps/livesettings/templates/livesettings/group_settings.html
new file mode 100644
index 00000000..e56f764f
--- /dev/null
+++ b/forum/deps/livesettings/templates/livesettings/group_settings.html
@@ -0,0 +1,81 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_modify config_tags %}
+{% block extrastyle %}
+{{ block.super }}
+<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/base.css" />
+{% endblock %}
+
+{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/forms.css{% endblock %}
+{% block coltype %}colMS{% endblock %}
+{% block bodyclass %}dashboard{% endblock %}
+{% block userlinks %}<a href="/admin/doc/">{% trans 'Documentation' %}</a> / <a href="/admin/password_change/">{% trans 'Change password' %}</a> / <a href="/admin/logout/">{% trans 'Log out' %}</a>{% endblock %}
+{% block breadcrumbs %}{% if not is_popup %}
+<div class="breadcrumbs">
+ <a href="/admin/">{% trans "Home" %}</a> &rsaquo;
+ {% trans "Edit Group Settings" %}
+</div>
+{% endif %}{% endblock %}
+{% block content %}
+<div id="content-main">
+{% if form.errors %}
+ <p class="errornote">
+ {% blocktrans count form.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+ </p>
+{% endif %}
+{% if form.fields %}
+<form method="post">
+ <div class="module">
+ <table summary="{% filter capfirst %}{% blocktrans with group.name as name %}Settings included in {{ name }}.{% endblocktrans %}{% endfilter %}" width="100%">
+ {% for field in form %}
+ {% if field.is_hidden %}
+ <!-- skip hidden field {{field.key}} -->
+ {% else %}
+ {% if field.errors %}
+ <tr class="error">
+ <td colspan="2">{{ field.errors }}</td>
+ </tr>
+ {% endif %}
+ <tr{% if field.errors %} class="error"{% endif %}>
+ <td style="width: 50%;">
+ {{ field.label_tag }}
+ {% if field.help_text %}
+ <p class="help">{{ field.help_text|safe }}</p>
+ {% endif %}
+ {% if field.field.default_text %}
+ <p class="help">{{ field.field.default_text|safe }}</p>
+ {% endif %}
+ </td>
+ <td>{{ field }}</td>
+ </tr>
+ {% endif %}
+ {% endfor %}
+ </table>
+ {% for field in form %}
+ {% if field.is_hidden %}
+ {{field}}
+ {% endif %}
+ {% endfor %}
+ </div>
+ <input type="submit" value="Save" class="default" />
+</form>
+{% else %}
+ <p>{% trans "You don't have permission to edit values." %}</p>
+{% endif %}
+</div>
+{% if all_groups %}
+<div id="content-related">
+ <div class="module">
+ <h2>{% trans "Setting groups" %}</h2>
+ <ul>
+ {% for g in all_groups %}
+ {% ifequal g.key group.key %}
+ <li><b>{{g.name}}</b></li>
+ {% else %}
+ <li><a href="{% url group_settings g.key %}">{{g.name}}</a></li>
+ {% endifequal %}
+ {% endfor %}
+ </ul>
+ </div>
+</div>
+{% endif %}
+{% endblock %}
diff --git a/forum/deps/livesettings/templates/livesettings/site_settings.html b/forum/deps/livesettings/templates/livesettings/site_settings.html
new file mode 100644
index 00000000..35333778
--- /dev/null
+++ b/forum/deps/livesettings/templates/livesettings/site_settings.html
@@ -0,0 +1,101 @@
+{% extends "admin/base_site.html" %}
+{% load i18n admin_modify config_tags %}
+
+{% block stylesheet %}{% load adminmedia %}{% admin_media_prefix %}css/forms.css{% endblock %}
+{% block extrahead %}
+<script type="text/javascript" src="{% url admin:jsi18n %}"></script>
+<script type="text/javascript" src="{% admin_media_prefix %}js/core.js"></script>
+<script type="text/javascript" src="{% admin_media_prefix %}js/admin/CollapsedFieldsets.js"></script>
+{% endblock %}
+{% block extrastyle %}
+{{ block.super }}
+<link rel="stylesheet" type="text/css" href="{% load adminmedia %}{% admin_media_prefix %}css/base.css" />
+<style type="text/css">
+ul.fieldref { margin: 0; padding: 0; font-size: 9px; }
+ul.fieldref li { float: left; margin: 0 10px 0 0; list-style: none; }
+fieldset.collapsed h2 { display: block !important; }
+fieldset.collapsed h2 a { display: inline !important; }
+div.fieldcontainer { float: left; margin-right: 0; }
+</style>
+{% endblock %}
+{% block coltype %}colMS{% endblock %}
+{% block bodyclass %}dashboard{% endblock %}
+{% block userlinks %}<a href="/admin/doc/">{% trans 'Documentation' %}</a> / <a href="/admin/password_change/">{% trans 'Change password' %}</a> / <a href="/admin/logout/">{% trans 'Log out' %}</a>{% endblock %}
+{% block breadcrumbs %}{% if not is_popup %}
+<div class="breadcrumbs">
+ <a href="/admin/">{% trans "Home" %}</a> &rsaquo;
+ {% trans "Edit Site Settings" %}
+</div>
+{% endif %}{% endblock %}
+{% block content %}
+{% comment %}
+<div class="fieldcontainer">
+<ul class="fieldref">
+{% for group in form.groups %}
+ <li><a onclick="javascript:CollapsedFieldsets.show({{ forloop.counter0 }});" href="#{{ group.key }}">{{ group.name }}</a></li>
+{% endfor %}
+</ul>
+</div>
+{% endcomment %}
+<span style="clear: both;" />
+<div id="content-main">
+{% if not use_db %}
+ <p>{% trans "Livesettings are disabled for this site." %}</p>
+ <p>{% trans "All configuration options must be edited in the site settings.py file" %}</p>
+ </div>
+ {% admin_site_views 'satchmo_site_settings' %}
+{% else %}
+ {% if form.errors %}
+ <p class="errornote">
+ {% blocktrans count form.errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %}
+ </p>
+ {% endif %}
+ {% if form.fields %}
+ <form method="post">
+ {% for field in form %}
+ {% if field.is_hidden %}
+ {{ field }}
+ {% else %}
+ {% ifchanged field.field.group %}{% with field.field.group as group %}
+ {% if not forloop.first %}
+ </table>
+ </fieldset>
+ {% endif %}
+ <fieldset class="module collapse">
+ <h2 id="{{ group.key }}">{{ group.name }}</h2>
+ <table summary="{% blocktrans with group.name as name %}Group settings: {{ name }}{% endblocktrans %}" style="width: 100%">
+ {% endwith %}{% endifchanged %}
+
+ {% if field.errors %}
+ <tr class="error">
+ <td colspan="2">{{ field.errors }}</td>
+ </tr>
+ {% endif %}
+ <tr{% if field.errors %} class="error"{% endif %}>
+ <td style="width: 50%;">
+ {{ field.label_tag }}
+ {% if field.help_text %}
+ <p class="help">{{ field.help_text|break_at:40|safe }}</p>
+ {% endif %}
+ {% if field.field.default_text %}
+ <p class="help">{{ field.field.default_text|break_at:40}}</p>
+ {% endif %}
+ </td>
+ <td>{{ field }}</td>
+ </tr>
+ {% endif %}
+ {% endfor %}
+ </table>
+ </div>
+ {% admin_site_views 'satchmo_site_settings' %}
+ <br class="clear:both;" />
+ <input type="submit" value="Save" class="default" />
+ <p><a onclick="javascript:CollapsedFieldsets.uncollapse_all(); return false;" href="#">{% trans 'Uncollapse all' %}</a></p>
+ <p><a href="{% url settings_export %}">Export</a></p>
+ </form>
+ {% else %}
+ <p>{% trans "You don't have permission to edit values." %}</p>
+ {% endif %}
+{% endif %}
+</div>
+{% endblock %}
diff --git a/forum/deps/livesettings/templates/livesettings/text.txt b/forum/deps/livesettings/templates/livesettings/text.txt
new file mode 100644
index 00000000..d57a57e3
--- /dev/null
+++ b/forum/deps/livesettings/templates/livesettings/text.txt
@@ -0,0 +1 @@
+{{ text|safe }} \ No newline at end of file
diff --git a/forum/deps/livesettings/templatetags/__init__.py b/forum/deps/livesettings/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/forum/deps/livesettings/templatetags/__init__.py
diff --git a/forum/deps/livesettings/templatetags/config_tags.py b/forum/deps/livesettings/templatetags/config_tags.py
new file mode 100644
index 00000000..140014ba
--- /dev/null
+++ b/forum/deps/livesettings/templatetags/config_tags.py
@@ -0,0 +1,91 @@
+from django import template
+from django.contrib.sites.models import Site
+from django.core import urlresolvers
+from forum.deps.livesettings import config_value
+from forum.deps.livesettings.utils import url_join
+import logging
+
+log = logging.getLogger('configuration.config_tags')
+
+register = template.Library()
+
+def force_space(value, chars=40):
+ """Forces spaces every `chars` in value"""
+
+ chars = int(chars)
+ if len(value) < chars:
+ return value
+ else:
+ out = []
+ start = 0
+ end = 0
+ looping = True
+
+ while looping:
+ start = end
+ end += chars
+ out.append(value[start:end])
+ looping = end < len(value)
+
+ return ' '.join(out)
+
+def break_at(value, chars=40):
+ """Force spaces into long lines which don't have spaces"""
+ #todo: EF - lazy patch
+ return value
+
+ chars = int(chars)
+ value = unicode(value)
+ if len(value) < chars:
+ return value
+ else:
+ out = []
+ line = value.split(' ')
+ for word in line:
+ if len(word) > chars:
+ out.append(force_space(word, chars))
+ else:
+ out.append(word)
+
+ return " ".join(out)
+
+register.filter('break_at', break_at)
+
+def config_boolean(option):
+ """Looks up the configuration option, returning true or false."""
+ args = option.split('.')
+ try:
+ val = config_value(*args)
+ except:
+ log.warn('config_boolean tag: Tried to look up config setting "%s", got SettingNotSet, returning False', option)
+ val = False
+ if val:
+ return "true"
+ else:
+ return ""
+
+register.filter('config_boolean', config_boolean)
+
+def admin_site_views(view):
+ """Returns a formatted list of sites, rendering for view, if any"""
+
+ if view:
+ path = urlresolvers.reverse(view)
+ else:
+ path = None
+
+ links = []
+ for site in Site.objects.all():
+ paths = ["http://", site.domain]
+ if path:
+ paths.append(path)
+
+ links.append((site.name, url_join(paths)))
+
+ ret = {
+ 'links' : links,
+ }
+ return ret
+
+
+register.inclusion_tag('forum.deps.livesettings/_admin_site_views.html')(admin_site_views)
diff --git a/forum/deps/livesettings/tests.py b/forum/deps/livesettings/tests.py
new file mode 100644
index 00000000..f604af99
--- /dev/null
+++ b/forum/deps/livesettings/tests.py
@@ -0,0 +1,545 @@
+from django.conf import settings as djangosettings
+from django.test import TestCase
+import keyedcache
+from forum.deps.livesettings import *
+import logging
+log = logging.getLogger('test');
+
+class ConfigurationFunctionTest(TestCase):
+
+ def testSetSingleConfigItem(self):
+ value = IntegerValue(BASE_GROUP, 'SingleItem')
+ config_register(value)
+ self.assert_(config_exists(BASE_GROUP, 'SingleItem'))
+
+ def testSetTwoConfigItems(self):
+ s = [IntegerValue(BASE_GROUP, 'testTwoA'), StringValue(BASE_GROUP, 'testTwoB')]
+ config_register_list(*s)
+
+ self.assert_(config_exists(BASE_GROUP, 'testTwoA'))
+ self.assert_(config_exists(BASE_GROUP, 'testTwoB'))
+
+ def testSetGroup(self):
+ g1 = ConfigurationGroup('test1','test1')
+ value = IntegerValue(g1, 'SingleGroupedItem')
+ config_register(value)
+ self.assertFalse(config_exists(BASE_GROUP, 'SingleGroupedItem'))
+ self.assert_(config_exists(g1, 'SingleGroupedItem'))
+
+
+class ConfigurationTestSettings(TestCase):
+
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+ g = ConfigurationGroup('test2','test2')
+ self.g = g
+ config_register(StringValue(g, 's1'))
+ config_register(IntegerValue(g, 's2', default=10))
+ config_register(IntegerValue(g, 's3', default=10))
+
+ def testSetSetting(self):
+ c = config_get('test2', 's1')
+ c.update('test')
+
+ self.assertEqual(c.value, 'test')
+ self.assertEqual(c.setting.value, 'test')
+
+ def testSettingDefault(self):
+ c = config_get('test2', 's2')
+ self.assertEqual(c.value, 10)
+
+ def testSetAndReset(self):
+ """Test setting one value and then updating"""
+ c = config_get('test2', 's1')
+ c.update('test1')
+
+ self.assertEqual(c.value, 'test1')
+
+ # should be true, since it is an update
+ self.assert_(c.update('test2'))
+ self.assertEqual(c.value, 'test2')
+
+ def testTwice(self):
+ """Config items should respond False to duplicate requests to update."""
+
+ c = config_get('test2', 's1')
+ c.update('test1')
+
+ self.assertFalse(c.update('test1'))
+
+
+ def testDeletesDefault(self):
+ c = config_get('test2', 's3')
+ # false because it isn't saving a default value
+ self.assertFalse(c.update(10))
+
+ self.assert_(c.update(20))
+ self.assertEqual(c.value, 20)
+ try:
+ s = c.setting
+ except SettingNotSet:
+ self.fail("Should have a setting now")
+
+ # now delete and go back to no setting by setting the default
+ self.assert_(c.update(10))
+ self.assertEqual(c.value, 10)
+
+ try:
+ s = c.setting
+ self.fail('Should throw SettingNotSet')
+ except SettingNotSet:
+ pass
+
+
+class ConfigTestDotAccess(TestCase):
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g = ConfigurationGroup('test3','test3')
+ self.g = g
+ c1 = config_register(BooleanValue(g, 's1', default=True))
+ c2 = config_register(IntegerValue(g, 's2', default=10))
+ c2.update(100)
+
+ def testDotAccess(self):
+ self.assert_(ConfigurationSettings().test3.s1.value)
+ self.assertEqual(ConfigurationSettings().test3.s2.value, 100)
+
+ def testSettingProperty(self):
+ c = config_get('test3','s2')
+ s = c.setting
+ self.assert_(s.value, 100)
+
+ def testDictValues(self):
+ d = self.g.dict_values()
+ self.assertEqual(d, {'s1': True, 's2' : 100})
+
+class ConfigTestModuleValue(TestCase):
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g = ConfigurationGroup('modules','module test')
+ self.g = g
+ self.c = config_register(ModuleValue(g, 'test'))
+
+ # def testModule(self):
+ # c = config_get('modules', 'test')
+ # c.update('satchmo_store')
+
+ # self.assert_(hasattr(self.c.value, 'get_version'))
+
+class ConfigTestSortOrder(TestCase):
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g1 = ConfigurationGroup('group1', 'Group 1', ordering=-1001)
+ g2 = ConfigurationGroup('group2', 'Group 2', ordering=-1002)
+ g3 = ConfigurationGroup('group3', 'Group 3', ordering=-1003)
+
+ self.g1 = g1
+ self.g2 = g2
+ self.g3 = g3
+
+ self.g1c1 = config_register(IntegerValue(g1, 'c1'))
+ self.g1c2 = config_register(IntegerValue(g1, 'c2'))
+ self.g1c3 = config_register(IntegerValue(g1, 'c3'))
+
+ self.g2c1 = config_register(IntegerValue(g2, 'c1'))
+ self.g2c2 = config_register(IntegerValue(g2, 'c2'))
+ self.g2c3 = config_register(IntegerValue(g2, 'c3'))
+
+ self.g3c1 = config_register(IntegerValue(g3, 'c1'))
+ self.g3c2 = config_register(IntegerValue(g3, 'c2'))
+ self.g3c3 = config_register(IntegerValue(g3, 'c3'))
+
+ def testGroupOrdering(self):
+ mgr = ConfigurationSettings()
+ self.assertEqual(mgr[2].key, self.g1.key)
+ self.assertEqual(mgr[1].key, self.g2.key)
+ self.assertEqual(mgr[0].key, self.g3.key)
+
+
+class TestMultipleValues(TestCase):
+
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g1 = ConfigurationGroup('m1', 'Multiple Group 1', ordering=1000)
+ self.g1 = g1
+
+ self.g1c1 = config_register(MultipleStringValue(g1,
+ 'c1',
+ choices=((1,'one'),(2,'two'),(3,'three'))))
+
+ def testSave(self):
+
+ c = config_get('m1','c1')
+ c.update([1,2])
+ self.assertEqual(c.value, [1,2])
+
+ def testAddChoice(self):
+
+ config_add_choice('m1','c1',(4, 'four'))
+ c = config_get('m1','c1')
+ self.assertEqual(c.choices, ((1,'one'),(2,'two'),(3,'three'),(4,'four')))
+
+ def testChoiceValues(self):
+ self.g1c1.update([1,2])
+
+ self.assertEqual(self.g1c1.value, [1,2])
+ self.assertEqual(self.g1c1.choice_values, [(1, 'one'),(2, 'two')])
+
+ choices = config_choice_values('m1', 'c1')
+ self.assertEqual(choices, [(1, 'one'),(2, 'two')])
+
+class TestMultipleValuesWithDefault(TestCase):
+
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g1 = ConfigurationGroup('mv2', 'Multiple Group 2', ordering=1000)
+ self.g1 = g1
+
+ self.g1c1 = config_register(MultipleStringValue(g1,
+ 'c1',
+ choices=((1,'one'),(2,'two'),(3,'three')),
+ default=[1,2]))
+
+ def testDefault(self):
+
+ c = config_get('mv2','c1')
+ self.assertEqual(c.value, [1,2])
+
+ c.update([1,2,3])
+ self.assertEqual(c.value, [1,2,3])
+
+class ConfigTestChoices(TestCase):
+
+ def testAddPreregisteredChoice(self):
+ """Test that we can register choices before the config is actually set up."""
+ config_add_choice('ctg1', 'c1', ('a', 'Item A'))
+ config_add_choice('ctg1', 'c1', ('b', 'Item B'))
+ config_add_choice('ctg1', 'c1', ('c', 'Item C'))
+
+ g1 = ConfigurationGroup('ctg1', 'Choice 1', ordering=1000)
+ config_register(StringValue(g1, 'c1'))
+
+ c = config_get('ctg1','c1')
+
+ self.assertEqual(c.choices, [('a','Item A'), ('b','Item B'), ('c','Item C')])
+
+
+class ConfigTestRequires(TestCase):
+
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g1 = ConfigurationGroup('req1', 'Requirements 1', ordering=1000)
+
+ self.g1 = g1
+
+ bool1 = config_register(BooleanValue(g1, 'bool1', default=False, ordering=1))
+ bool2 = config_register(BooleanValue(g1, 'bool2', ordering=2))
+
+ self.g1c1 = config_register(IntegerValue(g1, 'c1', requires=bool1, ordering=3))
+
+ self.g1c2 = config_register(IntegerValue(g1, 'c2', requires=bool2, ordering=4))
+ self.g1c3 = config_register(IntegerValue(g1, 'c3', ordering=5))
+
+ bool2.update(True)
+
+ def testSimpleRequires(self):
+
+ v = config_value('req1', 'bool2')
+ self.assertTrue(v)
+
+ keys = [cfg.key for cfg in self.g1]
+ self.assertEqual(keys, ['bool1', 'bool2', 'c2','c3'])
+
+ c = config_get('req1','bool1')
+ c.update(True)
+
+ keys = [cfg.key for cfg in self.g1]
+ self.assertEqual(keys, ['bool1', 'bool2', 'c1', 'c2', 'c3'])
+
+class ConfigTestRequiresChoices(TestCase):
+
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g1 = ConfigurationGroup('req2', 'Requirements 2', ordering=1000)
+
+ self.g1 = g1
+
+ choices1 = config_register(MultipleStringValue(BASE_GROUP, 'rc1', ordering=1))
+
+ self.g1c1 = config_register(IntegerValue(g1, 'c1', requires=choices1, ordering=3))
+ self.g1c2 = config_register(IntegerValue(g1, 'c2', requires=choices1, ordering=4))
+ self.g1c3 = config_register(IntegerValue(g1, 'c3', ordering=5))
+
+ choices1.update('c1')
+
+ g2 = ConfigurationGroup('req3', 'Requirements 3', ordering=1000)
+
+ self.g2 = g2
+
+ choices2 = config_register(StringValue(BASE_GROUP, 'choices2', ordering=1))
+
+ self.g2c1 = config_register(IntegerValue(g2, 'c1', requires=choices2, ordering=3))
+ self.g2c2 = config_register(IntegerValue(g2, 'c2', requires=choices2, ordering=4))
+ self.g2c3 = config_register(IntegerValue(g2, 'c3', requires=choices2, ordering=5))
+
+ choices2.update('c1')
+
+ def testSimpleRequiresChoices(self):
+
+ v = config_value('BASE', 'rc1')
+ self.assertEquals(v, ['c1'])
+
+ g = config_get_group('req2')
+ keys = [cfg.key for cfg in g]
+ self.assertEqual(keys, ['c1','c3'])
+
+ c = config_get('BASE', 'rc1')
+ c.update(['c1','c2'])
+
+ g = config_get_group('req2')
+ keys = [cfg.key for cfg in g]
+ self.assertEqual(keys, ['c1', 'c2', 'c3'])
+
+ def testRequiresSingleValue(self):
+ v = config_value('BASE', 'choices2')
+ self.assertEquals(v, 'c1')
+
+ keys = [cfg.key for cfg in self.g2]
+ self.assertEqual(keys, ['c1'])
+
+ c = config_get('BASE', 'choices2')
+ c.update('c2')
+
+ keys = [cfg.key for cfg in self.g2]
+ self.assertEqual(keys, ['c2'])
+
+class ConfigTestRequiresValue(TestCase):
+
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ g1 = ConfigurationGroup('reqval', 'Requirements 3', ordering=1000)
+
+ self.g1 = g1
+
+ choices1 = config_register(MultipleStringValue(BASE_GROUP, 'valchoices', ordering=1))
+
+ self.g1c1 = config_register(IntegerValue(g1, 'c1', requires=choices1, requiresvalue='foo', ordering=3))
+ self.g1c2 = config_register(IntegerValue(g1, 'c2', requires=choices1, requiresvalue='bar', ordering=4))
+ self.g1c3 = config_register(IntegerValue(g1, 'c3', ordering=5))
+
+ choices1.update('foo')
+
+ g2 = ConfigurationGroup('reqval2', 'Requirements 4', ordering=1000)
+
+ self.g2 = g2
+
+ choices2 = config_register(StringValue(BASE_GROUP, 'valchoices2', ordering=1,
+ choices=(('a','test a'),('b', 'test b'),('c', 'test c'))))
+
+ self.g2c1 = config_register(IntegerValue(g2, 'c1', requires=choices2, requiresvalue='a', ordering=3))
+ self.g2c2 = config_register(IntegerValue(g2, 'c2', requires=choices2, requiresvalue='b', ordering=4))
+ self.g2c3 = config_register(IntegerValue(g2, 'c3', requires=choices2, requiresvalue='c', ordering=5))
+
+ choices2.update('a')
+
+ def testRequiresValue(self):
+ v = config_value('BASE', 'valchoices')
+ self.assertEquals(v, ['foo'])
+
+ g = config_get_group('reqval')
+
+ keys = [cfg.key for cfg in g]
+ self.assertEqual(keys, ['c1','c3'])
+
+ c = config_get('BASE', 'valchoices')
+ c.update(['foo','bar'])
+
+ g = config_get_group('reqval')
+ keys = [cfg.key for cfg in g]
+ self.assertEqual(keys, ['c1', 'c2', 'c3'])
+
+ def testRequiresSingleValue(self):
+ v = config_value('BASE', 'valchoices2')
+ self.assertEquals(v, 'a')
+
+ keys = [cfg.key for cfg in self.g2]
+ self.assertEqual(keys, ['c1'])
+
+ c = config_get('BASE', 'valchoices2')
+ c.update('b')
+
+ keys = [cfg.key for cfg in self.g2]
+ self.assertEqual(keys, ['c2'])
+
+class ConfigTestGroupRequires(TestCase):
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ choices1 = config_register(MultipleStringValue(BASE_GROUP, 'groupchoice', ordering=1))
+ choices2 = config_register(MultipleStringValue(BASE_GROUP, 'groupchoice2', ordering=1))
+
+ g1 = ConfigurationGroup('groupreq', 'Requirements 4', ordering=1000, requires=choices1)
+ self.g1 = g1
+
+ self.g1c1 = config_register(IntegerValue(g1, 'c1', ordering=3))
+ self.g1c2 = config_register(IntegerValue(g1, 'c2', requires=choices2, requiresvalue='bar', ordering=4))
+ self.g1c3 = config_register(IntegerValue(g1, 'c3', ordering=5))
+
+ def testRequiresValue(self):
+ c = config_get('BASE', 'groupchoice')
+ self.assertEquals(c.value, [])
+
+ keys = [cfg.key for cfg in self.g1]
+ self.assertEqual(keys, [])
+
+ c2 = config_get('BASE', 'groupchoice2')
+ c2.update('bar')
+
+ keys = [cfg.key for cfg in self.g1]
+ self.assertEqual(keys, ['c2'])
+
+ c.update(['groupreq'])
+
+ keys = [cfg.key for cfg in self.g1]
+ self.assertEqual(keys, ['c1', 'c2', 'c3'])
+
+class ConfigCollectGroup(TestCase):
+ def setUp(self):
+ keyedcache.cache_delete()
+ choices = config_register(MultipleStringValue(BASE_GROUP, 'collect', ordering=1))
+ self.choices = choices
+
+ g1 = ConfigurationGroup('coll1', 'Collection 1')
+ g2 = ConfigurationGroup('coll2', 'Collection 2')
+ g3 = ConfigurationGroup('coll3', 'Collection 3')
+
+ g1c1 = config_register(StringValue(g1, 'test'))
+ g1c2 = config_register(StringValue(g1, 'test1'))
+ g2c1 = config_register(StringValue(g2, 'test'))
+ g3c1 = config_register(StringValue(g3, 'test'))
+
+ g1c1.update('set a')
+ g1c2.update('set b')
+ g2c1.update('set a')
+ g3c1.update('set d')
+
+ choices.update(['coll1','coll3'])
+
+ def testCollectSimple(self):
+ v = config_collect_values('BASE', 'collect', 'test')
+
+ self.assertEqual(v, ['set a', 'set d'])
+
+ def testCollectUnique(self):
+ self.choices.update(['coll1','coll2','coll3'])
+
+ v = config_collect_values('BASE', 'collect', 'test', unique=False)
+
+ self.assertEqual(v, ['set a', 'set a', 'set d'])
+
+ v = config_collect_values('BASE', 'collect', 'test', unique=True)
+
+ self.assertEqual(v, ['set a', 'set d'])
+
+class LongSettingTest(TestCase):
+ def setUp(self):
+ keyedcache.cache_delete()
+ wide = config_register(LongStringValue(BASE_GROUP, 'LONG', ordering=1, default="woot"))
+ self.wide = wide
+ self.wide.update('*' * 1000)
+
+ def testLongStorage(self):
+ w = config_value('BASE', 'LONG')
+ self.assertEqual(len(w), 1000)
+ self.assertEqual(w, '*'*1000)
+
+ def testShortInLong(self):
+ self.wide.update("test")
+ w = config_value('BASE', 'LONG')
+ self.assertEqual(len(w), 4)
+ self.assertEqual(w, 'test')
+
+ def testDelete(self):
+ remember = self.wide.setting.id
+ self.wide.update('woot')
+
+ try:
+ q = LongSetting.objects.get(pk = remember)
+ self.fail("Should be deletec")
+ except LongSetting.DoesNotExist:
+ pass
+
+class OverrideTest(TestCase):
+ """Test settings overrides"""
+ def setUp(self):
+ # clear out cache from previous runs
+ keyedcache.cache_delete()
+
+ djangosettings.LIVESETTINGS_OPTIONS = {
+ 1 : {
+ 'DB' : False,
+ 'SETTINGS' : {
+ 'overgroup' : {
+ 's2' : '100',
+ 'choices' : '["one","two","three"]'
+ }
+ }
+ }
+ }
+
+ g = ConfigurationGroup('overgroup','Override Group')
+ self.g = g
+ config_register(StringValue(g, 's1'))
+ config_register(IntegerValue(g, 's2', default=10))
+ config_register(IntegerValue(g, 's3', default=10))
+ config_register(MultipleStringValue(g, 'choices'))
+
+ def tearDown(self):
+ djangosettings.LIVESETTINGS_OPTIONS = {}
+
+ def testOverriddenSetting(self):
+ """Accessing an overridden setting should give the override value."""
+ c = config_get('overgroup', 's2')
+ self.assertEquals(c.value, 100)
+
+ def testCantChangeSetting(self):
+ """When overridden, setting a value should not work, should get the overridden value"""
+ c = config_get('overgroup', 's2')
+ c.update(1)
+
+ c = config_get('overgroup', 's2')
+ self.assertEquals(c.value, 100)
+
+ def testNotOverriddenSetting(self):
+ """Settings which are not overridden should return their defaults"""
+ c = config_get('overgroup', 's3')
+
+ self.assertEquals(c.value, 10)
+
+ def testOverriddenListSetting(self):
+ """Make sure lists work when overridden"""
+
+ c = config_get('overgroup', 'choices')
+ v = c.value
+ self.assertEqual(len(v), 3)
+ self.assertEqual(v[0], "one")
+ self.assertEqual(v[1], "two")
+ self.assertEqual(v[2], "three")
diff --git a/forum/deps/livesettings/urls.py b/forum/deps/livesettings/urls.py
new file mode 100644
index 00000000..4d2bf0f2
--- /dev/null
+++ b/forum/deps/livesettings/urls.py
@@ -0,0 +1,7 @@
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('forum.deps.livesettings.views',
+ url(r'^$', 'site_settings', {}, name='site_settings'),
+ url(r'^export/$', 'export_as_python', {}, name='settings_export'),
+ url(r'^(?P<group>[^/]+)/$', 'group_settings', name='group_settings'),
+)
diff --git a/forum/deps/livesettings/utils.py b/forum/deps/livesettings/utils.py
new file mode 100644
index 00000000..c0e0e293
--- /dev/null
+++ b/forum/deps/livesettings/utils.py
@@ -0,0 +1,87 @@
+import sys
+import types
+import os
+
+def can_loop_over(maybe):
+ """Test value to see if it is list like"""
+ try:
+ iter(maybe)
+ except:
+ return 0
+ else:
+ return 1
+
+def is_list_or_tuple(maybe):
+ return isinstance(maybe, (types.TupleType, types.ListType))
+
+
+def is_scalar(maybe):
+ """Test to see value is a string, an int, or some other scalar type"""
+ return is_string_like(maybe) or not can_loop_over(maybe)
+
+def is_string_like(maybe):
+ """Test value to see if it acts like a string"""
+ try:
+ maybe+""
+ except TypeError:
+ return 0
+ else:
+ return 1
+
+
+def flatten_list(sequence, scalarp=is_scalar, result=None):
+ """flatten out a list by putting sublist entries in the main list"""
+ if result is None:
+ result = []
+
+ for item in sequence:
+ if scalarp(item):
+ result.append(item)
+ else:
+ flatten_list(item, scalarp, result)
+
+def load_module(module):
+ """Load a named python module."""
+ try:
+ module = sys.modules[module]
+ except KeyError:
+ __import__(module)
+ module = sys.modules[module]
+ return module
+
+def get_flat_list(sequence):
+ """flatten out a list and return the flat list"""
+ flat = []
+ flatten_list(sequence, result=flat)
+ return flat
+
+def url_join(*args):
+ """Join any arbitrary strings into a forward-slash delimited string.
+ Do not strip leading / from first element, nor trailing / from last element.
+
+ This function can take lists as arguments, flattening them appropriately.
+
+ example:
+ url_join('one','two',['three','four'],'five') => 'one/two/three/four/five'
+ """
+ if len(args) == 0:
+ return ""
+
+ args = get_flat_list(args)
+
+ if len(args) == 1:
+ return str(args[0])
+
+ else:
+ args = [str(arg).replace("\\", "/") for arg in args]
+
+ work = [args[0]]
+ for arg in args[1:]:
+ if arg.startswith("/"):
+ work.append(arg[1:])
+ else:
+ work.append(arg)
+
+ joined = reduce(os.path.join, work)
+
+ return joined.replace("\\", "/")
diff --git a/forum/deps/livesettings/values.py b/forum/deps/livesettings/values.py
new file mode 100644
index 00000000..f00fdc97
--- /dev/null
+++ b/forum/deps/livesettings/values.py
@@ -0,0 +1,628 @@
+"""Taken and modified from the dbsettings project.
+
+http://code.google.com/p/django-values/
+"""
+from decimal import Decimal
+from django import forms
+from django.core.exceptions import ImproperlyConfigured
+from django.utils import simplejson
+from django.utils.datastructures import SortedDict
+from django.utils.encoding import smart_str
+from django.utils.translation import gettext, ugettext_lazy as _
+from forum.deps.livesettings.models import find_setting, LongSetting, Setting, SettingNotSet
+from forum.deps.livesettings.overrides import get_overrides
+from forum.deps.livesettings.utils import load_module, is_string_like, is_list_or_tuple
+import datetime
+import logging
+import signals
+
+__all__ = ['BASE_GROUP', 'ConfigurationGroup', 'Value', 'BooleanValue', 'DecimalValue', 'DurationValue',
+ 'FloatValue', 'IntegerValue', 'ModuleValue', 'PercentValue', 'PositiveIntegerValue', 'SortedDotDict',
+ 'StringValue', 'LongStringValue', 'MultipleStringValue']
+
+_WARN = {}
+
+log = logging.getLogger('configuration')
+
+NOTSET = object()
+
+class SortedDotDict(SortedDict):
+
+ def __getattr__(self, key):
+ try:
+ return self[key]
+ except:
+ raise AttributeError, key
+
+ def __iter__(self):
+ vals = self.values()
+ for k in vals:
+ yield k
+
+ def values(self):
+ vals = super(SortedDotDict, self).values()
+ vals = [v for v in vals if isinstance(v, (ConfigurationGroup, Value))]
+ vals.sort()
+ return vals
+
+class ConfigurationGroup(SortedDotDict):
+ """A simple wrapper for a group of configuration values"""
+ def __init__(self, key, name, *args, **kwargs):
+ """Create a new ConfigurationGroup.
+
+ Arguments:
+ - key
+ - group name - for display to user
+
+ Named Arguments:
+ - ordering: integer, optional, defaults to 1.
+ - requires: See `Value` requires. The default `requires` all member values will have if not overridden.
+ - requiresvalue: See `Values` requires_value. The default `requires_value` if not overridden on the `Value` objects.
+ """
+ self.key = key
+ self.name = name
+ self.ordering = kwargs.pop('ordering', 1)
+ self.requires = kwargs.pop('requires', None)
+ if self.requires:
+ reqval = kwargs.pop('requiresvalue', key)
+ if not is_list_or_tuple(reqval):
+ reqval = (reqval, reqval)
+
+ self.requires_value = reqval[0]
+ self.requires.add_choice(reqval)
+
+ super(ConfigurationGroup, self).__init__(*args, **kwargs)
+
+ def __cmp__(self, other):
+ return cmp((self.ordering, self.name), (other.ordering, other.name))
+
+ def __eq__(self, other):
+ return (type(self) == type(other)
+ and self.ordering == other.ordering
+ and self.name == other.name)
+
+ def __ne__(self, other):
+ return not self == other
+
+ def dict_values(self, load_modules=True):
+ vals = {}
+ keys = super(ConfigurationGroup, self).keys()
+ for key in keys:
+ v = self[key]
+ if isinstance(v, Value):
+ value = v.value
+ else:
+ value = v
+ vals[key] = value
+ return vals
+
+ def values(self):
+ vals = super(ConfigurationGroup, self).values()
+ return [v for v in vals if v.enabled()]
+
+BASE_GROUP = ConfigurationGroup('BASE', _('Base Settings'), ordering=0)
+
+class Value(object):
+
+ creation_counter = 0
+
+ def __init__(self, group, key, **kwargs):
+ """
+ Create a new Value object for configuration.
+
+ Args:
+ - `ConfigurationGroup`
+ - key - a string key
+
+ Named arguments:
+ - `description` - Will be passed to the field for form usage. Should be a translation proxy. Ex: _('example')
+ - `help_text` - Will be passed to the field for form usage.
+ - `choices` - If given, then the form field will use a select box
+ - `ordering` - Defaults to alphabetical by key if not given.
+ - `requires` - If given as a `Value`, then this field will only be rendered if that Value evaluates true (for Boolean requires) or the proper key is in the associated value.
+ - `requiresvalue` - If set, then this field will only be rendered if that value is in the list returned by self.value. Defaults to self.key.
+ - `hidden` - If true, then render a hidden field.
+ - `default` - If given, then this Value will return that default whenever it has no assocated `Setting`.
+ - `update_callback` - if given, then this value will call the callback whenever updated
+ """
+ self.group = group
+ self.key = key
+ self.description = kwargs.get('description', None)
+ self.help_text = kwargs.get('help_text')
+ self.choices = kwargs.get('choices',[])
+ self.ordering = kwargs.pop('ordering', 0)
+ self.hidden = kwargs.pop('hidden', False)
+ self.update_callback = kwargs.pop('update_callback', None)
+ self.requires = kwargs.pop('requires', None)
+ if self.requires:
+ reqval = kwargs.pop('requiresvalue', key)
+ if not is_list_or_tuple(reqval):
+ reqval = (reqval, reqval)
+
+ self.requires_value = reqval[0]
+ self.requires.add_choice(reqval)
+
+ elif group.requires:
+ self.requires = group.requires
+ self.requires_value = group.requires_value
+
+ if kwargs.has_key('default'):
+ self.default = kwargs.pop('default')
+ self.use_default = True
+ else:
+ self.use_default = False
+
+ self.creation_counter = Value.creation_counter
+ Value.creation_counter += 1
+
+ def __cmp__(self, other):
+ return cmp((self.ordering, self.description, self.creation_counter), (other.ordering, other.description, other.creation_counter))
+
+ def __eq__(self, other):
+ if type(self) == type(other):
+ return self.value == other.value
+ else:
+ return self.value == other
+
+ def __iter__(self):
+ return iter(self.value)
+
+ def __unicode__(self):
+ return unicode(self.value)
+
+ def __str__(self):
+ return str(self.value)
+
+ def add_choice(self, choice):
+ """Add a choice if it doesn't already exist."""
+ if not is_list_or_tuple(choice):
+ choice = (choice, choice)
+ skip = False
+ for k, v in self.choices:
+ if k == choice[0]:
+ skip = True
+ break
+ if not skip:
+ self.choices += (choice, )
+
+ def choice_field(self, **kwargs):
+ if self.hidden:
+ kwargs['widget'] = forms.MultipleHiddenInput()
+ return forms.ChoiceField(choices=self.choices, **kwargs)
+
+ def _choice_values(self):
+ choices = self.choices
+ vals = self.value
+ return [x for x in choices if x[0] in vals]
+
+ choice_values = property(fget=_choice_values)
+
+ def copy(self):
+ new_value = self.__class__(self.key)
+ new_value.__dict__ = self.__dict__.copy()
+ return new_value
+
+ def _default_text(self):
+ if not self.use_default:
+ note = ""
+ else:
+ if self.default == "":
+ note = _('Default value: ""')
+
+ elif self.choices:
+ work = []
+ for x in self.choices:
+ if x[0] in self.default:
+ work.append(smart_str(x[1]))
+ note = gettext('Default value: ') + ", ".join(work)
+
+ else:
+ note = _("Default value: %s") % unicode(self.default)
+
+ return note
+
+ default_text = property(fget=_default_text)
+
+ def enabled(self):
+ enabled = False
+ try:
+ if not self.requires:
+ enabled = True
+ else:
+ v = self.requires.value
+ if self.requires.choices:
+ enabled = self.requires_value == v or self.requires_value in v
+ elif v:
+ enabled = True
+ except SettingNotSet:
+ pass
+ return enabled
+
+ def make_field(self, **kwargs):
+ if self.choices:
+ if self.hidden:
+ kwargs['widget'] = forms.MultipleHiddenInput()
+ field = self.choice_field(**kwargs)
+ else:
+ if self.hidden:
+ kwargs['widget'] = forms.HiddenInput()
+ field = self.field(**kwargs)
+
+ field.group = self.group
+ field.default_text = self.default_text
+ return field
+
+ def make_setting(self, db_value):
+ log.debug('new setting %s.%s', self.group.key, self.key)
+ return Setting(group=self.group.key, key=self.key, value=db_value)
+
+ def _setting(self):
+ return find_setting(self.group.key, self.key)
+
+ setting = property(fget = _setting)
+
+ def _value(self):
+ use_db, overrides = get_overrides()
+
+ if not use_db:
+ try:
+ val = overrides[self.group.key][self.key]
+ except KeyError:
+ if self.use_default:
+ val = self.default
+ else:
+ raise SettingNotSet('%s.%s is not in your LIVESETTINGS_OPTIONS' % (self.group.key, self.key))
+
+ else:
+ try:
+ val = self.setting.value
+
+ except SettingNotSet, sns:
+ if self.use_default:
+ val = self.default
+ if overrides:
+ # maybe override the default
+ grp = overrides.get(self.group.key, {})
+ if grp.has_key(self.key):
+ val = grp[self.key]
+ else:
+ val = NOTSET
+
+ except AttributeError, ae:
+ log.error("Attribute error: %s", ae)
+ log.error("%s: Could not get _value of %s", self.key, self.setting)
+ raise(ae)
+
+ except Exception, e:
+ global _WARN
+ log.error(e)
+ if str(e).find("configuration_setting") > -1:
+ if not _WARN.has_key('configuration_setting'):
+ log.warn('Error loading setting %s.%s from table, OK if you are in syncdb', self.group.key, self.key)
+ _WARN['configuration_setting'] = True
+
+ if self.use_default:
+ val = self.default
+ else:
+ raise ImproperlyConfigured("All settings used in startup must have defaults, %s.%s does not", self.group.key, self.key)
+ else:
+ import traceback
+ traceback.print_exc()
+ log.warn("Problem finding settings %s.%s, %s", self.group.key, self.key, e)
+ raise SettingNotSet("Startup error, couldn't load %s.%s" %(self.group.key, self.key))
+ return val
+
+ def update(self, value):
+ use_db, overrides = get_overrides()
+
+ if use_db:
+ current_value = self.value
+
+ new_value = self.to_python(value)
+ if current_value != new_value:
+ if self.update_callback:
+ new_value = apply(self.update_callback, (current_value, new_value))
+
+ db_value = self.get_db_prep_save(new_value)
+
+ try:
+ s = self.setting
+ s.value = db_value
+
+ except SettingNotSet:
+ s = self.make_setting(db_value)
+
+ if self.use_default and self.default == new_value:
+ if s.id:
+ log.info("Deleted setting %s.%s", self.group.key, self.key)
+ s.delete()
+ else:
+ log.info("Updated setting %s.%s = %s", self.group.key, self.key, value)
+ s.save()
+
+ signals.configuration_value_changed.send(self, old_value=current_value, new_value=new_value, setting=self)
+
+ return True
+ else:
+ log.debug('not updating setting %s.%s - forum.deps.livesettings db is disabled',self.group.key, self.key)
+
+ return False
+
+ @property
+ def value(self):
+ val = self._value()
+ return self.to_python(val)
+
+ @property
+ def editor_value(self):
+ val = self._value()
+ return self.to_editor(val)
+
+ # Subclasses should override the following methods where applicable
+
+ def to_python(self, value):
+ "Returns a native Python object suitable for immediate use"
+ if value == NOTSET:
+ value = None
+ return value
+
+ def get_db_prep_save(self, value):
+ "Returns a value suitable for storage into a CharField"
+ if value == NOTSET:
+ value = ""
+ return unicode(value)
+
+ def to_editor(self, value):
+ "Returns a value suitable for display in a form widget"
+ if value == NOTSET:
+ return NOTSET
+ return unicode(value)
+
+###############
+# VALUE TYPES #
+###############
+
+class BooleanValue(Value):
+
+ class field(forms.BooleanField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.BooleanField.__init__(self, *args, **kwargs)
+
+ def add_choice(self, choice):
+ # ignore choice adding for boolean types
+ pass
+
+ def to_python(self, value):
+ if value in (True, 't', 'True', 1, '1'):
+ return True
+ return False
+
+ to_editor = to_python
+
+class DecimalValue(Value):
+ class field(forms.DecimalField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.DecimalField.__init__(self, *args, **kwargs)
+
+ def to_python(self, value):
+ if value==NOTSET:
+ return Decimal("0")
+
+ try:
+ return Decimal(value)
+ except TypeError, te:
+ log.warning("Can't convert %s to Decimal for settings %s.%s", value, self.group.key, self.key)
+ raise TypeError(te)
+
+ def to_editor(self, value):
+ if value == NOTSET:
+ return "0"
+ else:
+ return unicode(value)
+
+# DurationValue has a lot of duplication and ugliness because of issue #2443
+# Until DurationField is sorted out, this has to do some extra work
+class DurationValue(Value):
+
+ class field(forms.CharField):
+ def clean(self, value):
+ try:
+ return datetime.timedelta(seconds=float(value))
+ except (ValueError, TypeError):
+ raise forms.ValidationError('This value must be a real number.')
+ except OverflowError:
+ raise forms.ValidationError('The maximum allowed value is %s' % datetime.timedelta.max)
+
+ def to_python(self, value):
+ if value == NOTSET:
+ value = 0
+ if isinstance(value, datetime.timedelta):
+ return value
+ try:
+ return datetime.timedelta(seconds=float(value))
+ except (ValueError, TypeError):
+ raise forms.ValidationError('This value must be a real number.')
+ except OverflowError:
+ raise forms.ValidationError('The maximum allowed value is %s' % datetime.timedelta.max)
+
+ def get_db_prep_save(self, value):
+ if value == NOTSET:
+ return NOTSET
+ else:
+ return unicode(value.days * 24 * 3600 + value.seconds + float(value.microseconds) / 1000000)
+
+class FloatValue(Value):
+
+ class field(forms.FloatField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.FloatField.__init__(self, *args, **kwargs)
+
+ def to_python(self, value):
+ if value == NOTSET:
+ value = 0
+ return float(value)
+
+ def to_editor(self, value):
+ if value == NOTSET:
+ return "0"
+ else:
+ return unicode(value)
+
+class IntegerValue(Value):
+ class field(forms.IntegerField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.IntegerField.__init__(self, *args, **kwargs)
+
+ def to_python(self, value):
+ if value == NOTSET:
+ value = 0
+ return int(value)
+
+ def to_editor(self, value):
+ if value == NOTSET:
+ return "0"
+ else:
+ return unicode(value)
+
+
+class PercentValue(Value):
+
+ class field(forms.DecimalField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.DecimalField.__init__(self, 100, 0, 5, 2, *args, **kwargs)
+
+ class widget(forms.TextInput):
+ def render(self, *args, **kwargs):
+ # Place a percent sign after a smaller text field
+ attrs = kwargs.pop('attrs', {})
+ attrs['size'] = attrs['max_length'] = 6
+ return forms.TextInput.render(self, attrs=attrs, *args, **kwargs) + '%'
+
+ def to_python(self, value):
+ if value == NOTSET:
+ value = 0
+ return Decimal(value) / 100
+
+ def to_editor(self, value):
+ if value == NOTSET:
+ return "0"
+ else:
+ return unicode(value)
+
+class PositiveIntegerValue(IntegerValue):
+
+ class field(forms.IntegerField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['min_value'] = 0
+ forms.IntegerField.__init__(self, *args, **kwargs)
+
+
+class StringValue(Value):
+
+ class field(forms.CharField):
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.CharField.__init__(self, *args, **kwargs)
+
+ def to_python(self, value):
+ if value == NOTSET:
+ value = ""
+ return unicode(value)
+
+ to_editor = to_python
+
+class LongStringValue(Value):
+
+ class field(forms.CharField):
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ kwargs['widget'] = forms.Textarea()
+ forms.CharField.__init__(self, *args, **kwargs)
+
+ def make_setting(self, db_value):
+ log.debug('new long setting %s.%s', self.group.key, self.key)
+ return LongSetting(group=self.group.key, key=self.key, value=db_value)
+
+ def to_python(self, value):
+ if value == NOTSET:
+ value = ""
+ return unicode(value)
+
+ to_editor = to_python
+
+
+class MultipleStringValue(Value):
+
+ class field(forms.CharField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.CharField.__init__(self, *args, **kwargs)
+
+ def choice_field(self, **kwargs):
+ kwargs['required'] = False
+ return forms.MultipleChoiceField(choices=self.choices, **kwargs)
+
+ def get_db_prep_save(self, value):
+ if is_string_like(value):
+ value = [value]
+ return simplejson.dumps(value)
+
+ def to_python(self, value):
+ if not value or value == NOTSET:
+ return []
+ if is_list_or_tuple(value):
+ return value
+ else:
+ try:
+ return simplejson.loads(value)
+ except:
+ if is_string_like(value):
+ return [value]
+ else:
+ log.warning('Could not decode returning empty list: %s', value)
+ return []
+
+
+ to_editor = to_python
+
+class ModuleValue(Value):
+ """Handles setting modules, storing them as strings in the db."""
+
+ class field(forms.CharField):
+
+ def __init__(self, *args, **kwargs):
+ kwargs['required'] = False
+ forms.CharField.__init__(self, *args, **kwargs)
+
+ def load_module(self, module):
+ """Load a child module"""
+ value = self._value()
+ if value == NOTSET:
+ raise SettingNotSet("%s.%s", self.group.key, self.key)
+ else:
+ return load_module("%s.%s" % (value, module))
+
+ def to_python(self, value):
+ if value == NOTSET:
+ v = {}
+ else:
+ v = load_module(value)
+ return v
+
+ def to_editor(self, value):
+ if value == NOTSET:
+ value = ""
+ return value
+
diff --git a/forum/deps/livesettings/views.py b/forum/deps/livesettings/views.py
new file mode 100644
index 00000000..ae78ca3f
--- /dev/null
+++ b/forum/deps/livesettings/views.py
@@ -0,0 +1,93 @@
+from django.http import HttpResponseRedirect
+from django.core.urlresolvers import reverse
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.contrib.admin.views.decorators import staff_member_required
+from django.views.decorators.cache import never_cache
+from forum.deps.livesettings import ConfigurationSettings, forms
+from forum.deps.livesettings.overrides import get_overrides
+import logging
+
+log = logging.getLogger('configuration.views')
+
+def group_settings(request, group, template='forum.deps.livesettings/group_settings.html'):
+ # Determine what set of settings this editor is used for
+
+ use_db, overrides = get_overrides();
+
+ mgr = ConfigurationSettings()
+
+ settings = mgr[group]
+ title = settings.name
+ log.debug('title: %s', title)
+
+ if use_db:
+ # Create an editor customized for the current user
+ #editor = forms.customized_editor(settings)
+
+ if request.method == 'POST':
+ # Populate the form with user-submitted data
+ data = request.POST.copy()
+ form = forms.SettingsEditor(data, settings=settings)
+ if form.is_valid():
+ form.full_clean()
+ for name, value in form.cleaned_data.items():
+ group, key = name.split('__')
+ cfg = mgr.get_config(group, key)
+ if cfg.update(value):
+
+ # Give user feedback as to which settings were changed
+ request.user.message_set.create(message='Updated %s on %s' % (cfg.key, cfg.group.key))
+
+ return HttpResponseRedirect(request.path)
+ else:
+ # Leave the form populated with current setting values
+ #form = editor()
+ form = forms.SettingsEditor(settings=settings)
+ else:
+ form = None
+
+ return render_to_response(template, {
+ 'all_groups': mgr.groups(),
+ 'title': title,
+ 'group' : settings,
+ 'form': form,
+ 'use_db' : use_db
+ }, context_instance=RequestContext(request))
+group_settings = never_cache(staff_member_required(group_settings))
+
+# Site-wide setting editor is identical, but without a group
+# staff_member_required is implied, since it calls group_settings
+def site_settings(request):
+ mgr = ConfigurationSettings()
+ default_group= mgr.groups()[0].key
+ return HttpResponseRedirect(reverse('group_settings', args=[default_group]))
+ #return group_settings(request, group=None, template='forum.deps.livesettings/site_settings.html')
+
+def export_as_python(request):
+ """Export site settings as a dictionary of dictionaries"""
+
+ from forum.deps.livesettings.models import Setting, LongSetting
+ import pprint
+
+ work = {}
+ both = list(Setting.objects.all())
+ both.extend(list(LongSetting.objects.all()))
+
+ for s in both:
+ if not work.has_key(s.site.id):
+ work[s.site.id] = {}
+ sitesettings = work[s.site.id]
+
+ if not sitesettings.has_key(s.group):
+ sitesettings[s.group] = {}
+ sitegroup = sitesettings[s.group]
+
+ sitegroup[s.key] = s.value
+
+ pp = pprint.PrettyPrinter(indent=4)
+ pretty = pp.pformat(work)
+
+ return render_to_response('forum.deps.livesettings/text.txt', { 'text' : pretty }, mimetype='text/plain')
+
+export_as_python = never_cache(staff_member_required(export_as_python))