diff options
Diffstat (limited to 'forum')
66 files changed, 5333 insertions, 24 deletions
diff --git a/forum/conf/email.py b/forum/conf/email.py index 0144f35e..2245cdce 100644 --- a/forum/conf/email.py +++ b/forum/conf/email.py @@ -2,8 +2,8 @@ Email related settings """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, IntegerValue, BooleanValue -from livesettings import StringValue +from forum.deps.livesettings import ConfigurationGroup, IntegerValue, BooleanValue +from forum.deps.livesettings import StringValue from django.utils.translation import ugettext as _ from forum import const diff --git a/forum/conf/external_keys.py b/forum/conf/external_keys.py index f43e1120..60111fdb 100644 --- a/forum/conf/external_keys.py +++ b/forum/conf/external_keys.py @@ -2,7 +2,7 @@ External service key settings """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, StringValue +from forum.deps.livesettings import ConfigurationGroup, StringValue from django.utils.translation import ugettext as _ from django.conf import settings as django_settings diff --git a/forum/conf/flatpages.py b/forum/conf/flatpages.py index eb6b646a..14398431 100644 --- a/forum/conf/flatpages.py +++ b/forum/conf/flatpages.py @@ -2,7 +2,7 @@ Q&A forum flatpages (about, etc.) """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, LongStringValue +from forum.deps.livesettings import ConfigurationGroup, LongStringValue from django.utils.translation import ugettext as _ FLATPAGES = ConfigurationGroup( diff --git a/forum/conf/forum_data_rules.py b/forum/conf/forum_data_rules.py index e452ea7b..f9c4afda 100644 --- a/forum/conf/forum_data_rules.py +++ b/forum/conf/forum_data_rules.py @@ -2,8 +2,8 @@ Settings for forum data display and entry """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, BooleanValue, IntegerValue -from livesettings import StringValue +from forum.deps.livesettings import ConfigurationGroup, BooleanValue, IntegerValue +from forum.deps.livesettings import StringValue from django.utils.translation import ugettext as _ from forum import const @@ -39,7 +39,7 @@ settings.register( ) ) -#todo: looks like there is a bug in livesettings +#todo: looks like there is a bug in forum.deps.livesettings #that does not allow Integer values with defaults and choices settings.register( StringValue( diff --git a/forum/conf/minimum_reputation.py b/forum/conf/minimum_reputation.py index a83d94fd..ecb7f4ad 100644 --- a/forum/conf/minimum_reputation.py +++ b/forum/conf/minimum_reputation.py @@ -3,7 +3,7 @@ Settings for minimum reputation required for a variety of actions on the askbot forum """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, IntegerValue +from forum.deps.livesettings import ConfigurationGroup, IntegerValue from django.utils.translation import ugettext as _ MIN_REP = ConfigurationGroup( diff --git a/forum/conf/reputation_changes.py b/forum/conf/reputation_changes.py index cef177f5..2f0bca65 100644 --- a/forum/conf/reputation_changes.py +++ b/forum/conf/reputation_changes.py @@ -4,7 +4,7 @@ user in response to various actions by the same users or others """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, IntegerValue +from forum.deps.livesettings import ConfigurationGroup, IntegerValue from django.utils.translation import ugettext as _ REP_CHANGES = ConfigurationGroup( diff --git a/forum/conf/settings_wrapper.py b/forum/conf/settings_wrapper.py index b3645333..305c0fd3 100644 --- a/forum/conf/settings_wrapper.py +++ b/forum/conf/settings_wrapper.py @@ -1,5 +1,5 @@ """ -Definition of a Singleton wrapper class for livesettings +Definition of a Singleton wrapper class for forum.deps.livesettings with interface similar to django.conf.settings that is each setting has unique key and is accessible via dotted lookup. @@ -11,16 +11,16 @@ from forum.conf import settings as forum_settings forum_settings.BLAH NOTE that at the moment there is distinction between settings -(django settings) and forum_settings (livesettings) +(django settings) and forum_settings (forum.deps.livesettings) -the value will be taken from livesettings database or cache +the value will be taken from forum.deps.livesettings database or cache note that during compilation phase database is not accessible for the most part, so actual values are reliably available only at run time -livesettings is a module developed for satchmo project +forum.deps.livesettings is a module developed for satchmo project """ -from livesettings import SortedDotDict, config_register +from forum.deps.livesettings import SortedDotDict, config_register class ConfigSettings(object): """A very simple Singleton wrapper for settings @@ -41,13 +41,13 @@ class ConfigSettings(object): """value lookup returns the actual value of setting not the object - this way only very minimal modifications will be required in code to convert an app - depending on django.conf.settings to livesettings + depending on django.conf.settings to forum.deps.livesettings """ return getattr(self.__instance, key).value def register(self, value): """registers the setting - value must be a subclass of livesettings.Value + value must be a subclass of forum.deps.livesettings.Value """ key = value.key group_key = value.group.key diff --git a/forum/conf/site_settings.py b/forum/conf/site_settings.py index 05d0c1c8..a4f864b8 100644 --- a/forum/conf/site_settings.py +++ b/forum/conf/site_settings.py @@ -3,7 +3,7 @@ Q&A website settings - title, desctiption, basic urls keywords """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, StringValue +from forum.deps.livesettings import ConfigurationGroup, StringValue from django.utils.translation import ugettext as _ from django.utils.html import escape from forum import const diff --git a/forum/conf/skin_counter_settings.py b/forum/conf/skin_counter_settings.py index 5a9d177f..e5998433 100644 --- a/forum/conf/skin_counter_settings.py +++ b/forum/conf/skin_counter_settings.py @@ -2,7 +2,7 @@ Skin settings to color view, vote and answer counters """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, IntegerValue, StringValue +from forum.deps.livesettings import ConfigurationGroup, IntegerValue, StringValue from django.utils.translation import ugettext as _ from forum.deps.grapefruit import Color diff --git a/forum/conf/skin_general_settings.py b/forum/conf/skin_general_settings.py index 748510e7..3c74a593 100644 --- a/forum/conf/skin_general_settings.py +++ b/forum/conf/skin_general_settings.py @@ -2,7 +2,7 @@ General skin settings """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, StringValue, IntegerValue +from forum.deps.livesettings import ConfigurationGroup, StringValue, IntegerValue from django.utils.translation import ugettext as _ from forum.skins.utils import get_skin_choices diff --git a/forum/conf/user_settings.py b/forum/conf/user_settings.py index 760f921f..a1c09d65 100644 --- a/forum/conf/user_settings.py +++ b/forum/conf/user_settings.py @@ -2,7 +2,7 @@ User policy settings """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, BooleanValue, IntegerValue +from forum.deps.livesettings import ConfigurationGroup, BooleanValue, IntegerValue from django.utils.translation import ugettext as _ USER_SETTINGS = ConfigurationGroup( diff --git a/forum/conf/vote_rules.py b/forum/conf/vote_rules.py index f249ef53..04332bb7 100644 --- a/forum/conf/vote_rules.py +++ b/forum/conf/vote_rules.py @@ -5,7 +5,7 @@ and offensive flags. For example number of times a person can vote each day, etc. """ from forum.conf.settings_wrapper import settings -from livesettings import ConfigurationGroup, IntegerValue +from forum.deps.livesettings import ConfigurationGroup, IntegerValue from django.utils.translation import ugettext as _ VOTE_RULES = ConfigurationGroup( 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)) diff --git a/forum/importers/stackexchange/management/commands/load_stackexchange.py b/forum/importers/stackexchange/management/commands/load_stackexchange.py index 5145035e..2c73c5a6 100644 --- a/forum/importers/stackexchange/management/commands/load_stackexchange.py +++ b/forum/importers/stackexchange/management/commands/load_stackexchange.py @@ -8,7 +8,7 @@ import forum.importers.stackexchange.parse_models as se_parser from xml.etree import ElementTree as et from django.db import models import forum.models as askbot -import django_authopenid.models as askbot_openid +import forum.deps.django_authopenid.models as askbot_openid import forum.importers.stackexchange.models as se from forum.forms import EditUserEmailFeedsForm from forum.conf import settings as forum_settings diff --git a/forum/settings.py b/forum/settings.py index 6e31634f..95e9bf81 100644 --- a/forum/settings.py +++ b/forum/settings.py @@ -1,6 +1,6 @@ #todo: this file is currently not in use import os -from livesettings import ConfigurationGroup, IntegerValue, config_register +from forum.deps.livesettings import ConfigurationGroup, IntegerValue, config_register INSTALLED_APPS = ['forum'] diff --git a/forum/urls.py b/forum/urls.py index 1864c608..b7637f54 100644 --- a/forum/urls.py +++ b/forum/urls.py @@ -197,7 +197,7 @@ urlpatterns = patterns('', url( r'^%s$' % _('upload/'), app.writers.upload, name='upload'), url(r'^%s$' % _('search/'), app.readers.search, name='search'), url(r'^%s$' % _('feedback/'), app.meta.feedback, name='feedback'), - (r'^%s' % _('account/'), include('django_authopenid.urls')), + (r'^%s' % _('account/'), include('forum.deps.django_authopenid.urls')), (r'^i18n/', include('django.conf.urls.i18n')), url(r'^feeds/rss/$', RssLastestQuestionsFeed, name="latest_questions_feed"), ) |