diff options
Diffstat (limited to 'forum/deps')
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>™', + 'google':'Google™', + '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 Binary files differnew file mode 100644 index 00000000..e176bc53 --- /dev/null +++ b/forum/deps/livesettings/locale/de/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..c2bc0b94 --- /dev/null +++ b/forum/deps/livesettings/locale/en/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..dd872edd --- /dev/null +++ b/forum/deps/livesettings/locale/fr/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..04270a04 --- /dev/null +++ b/forum/deps/livesettings/locale/he/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..05c50952 --- /dev/null +++ b/forum/deps/livesettings/locale/it/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..e0738605 --- /dev/null +++ b/forum/deps/livesettings/locale/ko/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..f45e49ed --- /dev/null +++ b/forum/deps/livesettings/locale/pl/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..a8bfb8b2 --- /dev/null +++ b/forum/deps/livesettings/locale/pt_BR/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..42e6074a --- /dev/null +++ b/forum/deps/livesettings/locale/ru/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..caed0ab9 --- /dev/null +++ b/forum/deps/livesettings/locale/sv/LC_MESSAGES/django.mo 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 Binary files differnew file mode 100644 index 00000000..d56ad423 --- /dev/null +++ b/forum/deps/livesettings/locale/tr/LC_MESSAGES/django.mo 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> › + {% 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> › + {% 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)) |