diff options
196 files changed, 6133 insertions, 421 deletions
@@ -1,8 +1,8 @@ This is Askbot project - open source Q&A system -Demo site is http://askbot.org/meta +Demo site is http://askbot.org -All documentation is in the directory forum/documentation +All documentation is in the directory forum/doc askbot-devel repository is open to anyone who wants to help develop Askbot - just drop us a note diff --git a/django_authopenid/forms.py b/django_authopenid/forms.py index 2f34986c..2fd3fd7f 100644 --- a/django_authopenid/forms.py +++ b/django_authopenid/forms.py @@ -35,6 +35,7 @@ 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 @@ -254,7 +255,7 @@ class ChangeEmailForm(forms.Form): def clean_email(self): """ check if email don't exist """ if 'email' in self.cleaned_data: - if settings.EMAIL_UNIQUE == True: + if forum_settings.EMAIL_UNIQUE == True: try: user = User.objects.get(email = self.cleaned_data['email']) if self.user and self.user == user: diff --git a/django_authopenid/urls.py b/django_authopenid/urls.py index 65afc45a..65afc45a 100755..100644 --- a/django_authopenid/urls.py +++ b/django_authopenid/urls.py diff --git a/django_authopenid/views.py b/django_authopenid/views.py index 4f7d3efa..688a41fc 100755..100644 --- a/django_authopenid/views.py +++ b/django_authopenid/views.py @@ -35,6 +35,7 @@ from django.http import HttpResponseRedirect, get_host, Http404, \ 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 @@ -364,7 +365,7 @@ def signin(request,newquestion=False,newanswer=False): 'form2': form_signin, 'msg': request.GET.get('msg',''), 'sendpw_url': reverse('user_sendpw'), - 'fb_api_key': settings.FB_API_KEY, + 'fb_api_key': forum_settings.FB_API_KEY, }, context_instance=RequestContext(request)) def complete_signin(request): @@ -507,7 +508,7 @@ def register(request): #if user just logged in and did not need to create the new account if user_ != None: - if settings.EMAIL_VALIDATION == 'on': + if forum_settings.EMAIL_VALIDATION == True: logging.debug('sending email validation') send_new_email_key(user_,nomessage=True) output = validation_email_sent(request) @@ -616,7 +617,7 @@ def signup(request): 'authopenid/confirm_email.txt' ) message_context = Context({ - 'signup_url': settings.APP_URL + reverse('user_signin'), + 'signup_url': forum_settings.APP_URL + reverse('user_signin'), 'username': username, 'password': password, }) @@ -749,7 +750,7 @@ def set_new_email(user, new_email, nomessage=False): user.email = new_email user.email_isvalid = False user.save() - if settings.EMAIL_VALIDATION == 'on': + if forum_settings.EMAIL_VALIDATION == True: send_new_email_key(user,nomessage=nomessage) def _send_email_key(user): @@ -760,7 +761,7 @@ def _send_email_key(user): message_template = loader.get_template('authopenid/email_validation.txt') import settings message_context = Context({ - 'validation_link': settings.APP_URL + reverse('user_verifyemail', kwargs={'id':user.id,'key':user.email_key}) + '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]) @@ -787,7 +788,7 @@ def send_email_key(request): authopenid/changeemail.html template """ - if settings.EMAIL_VALIDATION != 'off': + if forum_settings.EMAIL_VALIDATION == True: if request.user.email_isvalid: return render_to_response('authopenid/changeemail.html', { 'email': request.user.email, @@ -817,7 +818,7 @@ def verifyemail(request,id=None,key=None): url = /email/verify/{{user.id}}/{{user.email_key}}/ """ logging.debug('') - if settings.EMAIL_VALIDATION != 'off': + if forum.settings.EMAIL_VALIDATION == True: user = User.objects.get(id=id) if user: if user.email_key == key: @@ -854,7 +855,7 @@ def changeemail(request, action='change'): if form.is_valid(): new_email = form.cleaned_data['email'] if new_email != user_.email: - if settings.EMAIL_VALIDATION == 'on': + if forum_settings.EMAIL_VALIDATION == True: action = 'validate' else: action = 'done_novalidate' @@ -1118,10 +1119,10 @@ def sendpw(request): subject = _("Request for new password") message_template = loader.get_template( 'authopenid/sendpw_email.txt') - key_link = settings.APP_URL + reverse('user_confirmchangepw') + '?key=' + confirm_key + 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': settings.APP_URL + reverse('index'), + 'site_url': forum_settings.APP_URL + reverse('index'), 'key_link': key_link, 'username': form.user_cache.username, 'password': new_pw, diff --git a/fbconnect/__init__.py b/fbconnect/__init__.py index e69de29b..e69de29b 100755..100644 --- a/fbconnect/__init__.py +++ b/fbconnect/__init__.py diff --git a/fbconnect/fb.py b/fbconnect/fb.py index afcd8210..8d41c3a2 100755..100644 --- a/fbconnect/fb.py +++ b/fbconnect/fb.py @@ -1,4 +1,4 @@ -from django.conf import settings +from forum.conf import settings as forum_settings from time import time from datetime import datetime from urllib import urlopen, urlencode @@ -20,11 +20,11 @@ def generate_sig(values): for key in sorted(values.keys()): keys.append(key) - signature = ''.join(['%s=%s' % (key, values[key]) for key in keys]) + settings.FB_SECRET + signature = ''.join(['%s=%s' % (key, values[key]) for key in keys]) + forum_settings.FB_SECRET return hashlib.md5(signature).hexdigest() def check_cookies_signature(cookies): - API_KEY = settings.FB_API_KEY + API_KEY = forum_settings.FB_API_KEY values = {} @@ -37,10 +37,10 @@ def check_cookies_signature(cookies): def get_user_data(cookies): request_data = { 'method': 'Users.getInfo', - 'api_key': settings.FB_API_KEY, + 'api_key': forum_settings.FB_API_KEY, 'call_id': time(), 'v': '1.0', - 'uids': cookies[settings.FB_API_KEY + '_user'], + 'uids': cookies[forum_settings.FB_API_KEY + '_user'], 'fields': 'name,first_name,last_name', 'format': 'json', } @@ -52,7 +52,7 @@ def get_user_data(cookies): def delete_cookies(response): - API_KEY = settings.FB_API_KEY + API_KEY = forum_settings.FB_API_KEY response.delete_cookie(API_KEY + '_user') response.delete_cookie(API_KEY + '_session_key') @@ -62,7 +62,7 @@ def delete_cookies(response): response.delete_cookie('fbsetting_' + API_KEY) def check_session_expiry(cookies): - return datetime.fromtimestamp(float(cookies[settings.FB_API_KEY+'_expires'])) > datetime.now() + return datetime.fromtimestamp(float(cookies[forum_settings.FB_API_KEY+'_expires'])) > datetime.now() STATES = { 'FIRSTTIMER': 1, @@ -72,7 +72,7 @@ STATES = { } def get_user_state(request): - API_KEY = settings.FB_API_KEY + API_KEY = forum_settings.FB_API_KEY logging.debug('') if API_KEY in request.COOKIES: diff --git a/fbconnect/forms.py b/fbconnect/forms.py index 94f86816..94f86816 100755..100644 --- a/fbconnect/forms.py +++ b/fbconnect/forms.py diff --git a/fbconnect/models.py b/fbconnect/models.py index 2172217d..2172217d 100755..100644 --- a/fbconnect/models.py +++ b/fbconnect/models.py diff --git a/fbconnect/pjson.py b/fbconnect/pjson.py index 273b684e..273b684e 100755..100644 --- a/fbconnect/pjson.py +++ b/fbconnect/pjson.py diff --git a/fbconnect/tests.py b/fbconnect/tests.py index 2247054b..2247054b 100755..100644 --- a/fbconnect/tests.py +++ b/fbconnect/tests.py diff --git a/fbconnect/urls.py b/fbconnect/urls.py index bf2d4364..81b0cb0f 100755..100644 --- a/fbconnect/urls.py +++ b/fbconnect/urls.py @@ -1,13 +1,15 @@ from django.conf.urls.defaults import * from django.utils.translation import ugettext as _ from django.views.generic.simple import direct_to_template -from django.conf import settings from views import signin, register urlpatterns = patterns('', - url(r'^xd_receiver$', direct_to_template, {'template': 'fbconnect/xd_receiver.html',\ - 'extra_context': {'APP_SHORT_NAME':settings.APP_SHORT_NAME}},\ - name='xd_receiver'), + url( + r'^xd_receiver$', + direct_to_template, + {'template': 'fbconnect/xd_receiver.html',}, + name='xd_receiver' + ), url(r'^%s$' % _('signin/'), signin, name="fb_signin"), url(r'^%s%s$' % (_('signin/'), _('newquestion/')), signin, {'newquestion': True}, name="fb_signin_new_question"), diff --git a/fbconnect/views.py b/fbconnect/views.py index 1781f6bf..1781f6bf 100755..100644 --- a/fbconnect/views.py +++ b/fbconnect/views.py diff --git a/forum/__init__.py b/forum/__init__.py index 85cd5d26..85cd5d26 100755..100644 --- a/forum/__init__.py +++ b/forum/__init__.py diff --git a/forum/admin.py b/forum/admin.py index 3afa2241..41b68b9a 100755..100644 --- a/forum/admin.py +++ b/forum/admin.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- - from django.contrib import admin -from models import * +from forum.models import * class AnonymousQuestionAdmin(admin.ModelAdmin): """AnonymousQuestion admin class""" diff --git a/forum/auth.py b/forum/auth.py index 5d6e71c4..6c9998cb 100755..100644 --- a/forum/auth.py +++ b/forum/auth.py @@ -14,49 +14,7 @@ from models import mark_offensive, delete_post_or_answer from const import TYPE_REPUTATION import logging -VOTE_UP = 15 -FLAG_OFFENSIVE = 15 -POST_IMAGES = 15 -LEAVE_COMMENTS = 50 -UPLOAD_FILES = 60 -VOTE_DOWN = 100 -CLOSE_OWN_QUESTIONS = 250 -RETAG_OTHER_QUESTIONS = 500 -REOPEN_OWN_QUESTIONS = 500 -EDIT_COMMUNITY_WIKI_POSTS = 750 -EDIT_OTHER_POSTS = 2000 -DELETE_COMMENTS = 2000 -VIEW_OFFENSIVE_FLAGS = 2000 -DISABLE_URL_NOFOLLOW = 2000 -CLOSE_OTHER_QUESTIONS = 3000 -LOCK_POSTS = 4000 - -VOTE_RULES = { - 'scope_votes_per_user_per_day' : 30, # how many votes of one user has everyday - 'scope_flags_per_user_per_day' : 5, # how many times user can flag posts everyday - 'scope_warn_votes_left' : 10, # start when to warn user how many votes left - 'scope_deny_unvote_days' : 1, # if 1 days passed, user can't cancel votes. - 'scope_flags_invisible_main_page' : 3, # post doesn't show on main page if has more than 3 offensive flags - 'scope_flags_delete_post' : 5, # post will be deleted if it has more than 5 offensive flags -} - -REPUTATION_RULES = { - 'initial_score' : 1, - 'scope_per_day_by_upvotes' : 200, - 'gain_by_upvoted' : 10, - 'gain_by_answer_accepted' : 15, - 'gain_by_accepting_answer' : 2, - 'gain_by_downvote_canceled' : 2, - 'gain_by_canceling_downvote' : 1, - 'lose_by_canceling_accepted_answer' : -2, - 'lose_by_accepted_answer_cancled' : -15, - 'lose_by_downvoted' : -2, - 'lose_by_flagged' : -2, - 'lose_by_downvoting' : -1, - 'lose_by_flagged_lastrevision_3_times': -30, - 'lose_by_flagged_lastrevision_5_times': -100, - 'lose_by_upvote_canceled' : -10, -} +from forum.conf import settings as forum_settings def can_moderate_users(user): return user.is_superuser @@ -64,13 +22,13 @@ def can_moderate_users(user): def can_vote_up(user): """Determines if a User can vote Questions and Answers up.""" return user.is_authenticated() and ( - user.reputation >= VOTE_UP or + user.reputation >= forum_settings.MIN_REP_TO_VOTE_UP or user.is_superuser) def can_flag_offensive(user): """Determines if a User can flag Questions and Answers as offensive.""" return user.is_authenticated() and ( - user.reputation >= FLAG_OFFENSIVE or + user.reputation >= forum_settings.MIN_REP_TO_FLAG_OFFENSIVE or user.is_superuser) def can_add_comments(user,subject): @@ -78,7 +36,7 @@ def can_add_comments(user,subject): if user.is_authenticated(): if user.id == subject.author.id: return True - if user.reputation >= LEAVE_COMMENTS: + if user.reputation >= forum_settings.MIN_REP_TO_LEAVE_COMMENTS: return True if user.is_superuser: return True @@ -89,53 +47,55 @@ def can_add_comments(user,subject): def can_vote_down(user): """Determines if a User can vote Questions and Answers down.""" return user.is_authenticated() and ( - user.reputation >= VOTE_DOWN or + user.reputation >= forum_settings.MIN_REP_TO_VOTE_DOWN or user.is_superuser) def can_retag_questions(user): """Determines if a User can retag Questions.""" return user.is_authenticated() and ( - RETAG_OTHER_QUESTIONS <= user.reputation < EDIT_OTHER_POSTS or + forum_settings.MIN_REP_TO_RETAG_OTHERS_QUESTIONS + <= user.reputation + < forum_settings.MIN_REP_TO_EDIT_OTHERS_POSTS or user.is_superuser) def can_edit_post(user, post): """Determines if a User can edit the given Question or Answer.""" return user.is_authenticated() and ( user.id == post.author_id or - (post.wiki and user.reputation >= EDIT_COMMUNITY_WIKI_POSTS) or - user.reputation >= EDIT_OTHER_POSTS or + (post.wiki and user.reputation >= forum_settings.MIN_REP_TO_EDIT_WIKI) or + user.reputation >= forum_settings.MIN_REP_TO_EDIT_OTHERS_POSTS or user.is_superuser) def can_delete_comment(user, comment): """Determines if a User can delete the given Comment.""" return user.is_authenticated() and ( user.id == comment.user_id or - user.reputation >= DELETE_COMMENTS or + user.reputation >= forum_settings.MIN_REP_TO_DELETE_OTHERS_COMMENTS or user.is_superuser) def can_view_offensive_flags(user): """Determines if a User can view offensive flag counts.""" return user.is_authenticated() and ( - user.reputation >= VIEW_OFFENSIVE_FLAGS or + user.reputation >= forum_settings.MIN_REP_TO_VIEW_OFFENSIVE_FLAGS or user.is_superuser) def can_close_question(user, question): """Determines if a User can close the given Question.""" return user.is_authenticated() and ( (user.id == question.author_id and - user.reputation >= CLOSE_OWN_QUESTIONS) or - user.reputation >= CLOSE_OTHER_QUESTIONS or + user.reputation >= forum_settings.MIN_REP_TO_CLOSE_OWN_QUESTIONS) or + user.reputation >= forum_settings.MIN_REP_TO_CLOSE_OTHERS_QUESTIONS or user.is_superuser) def can_lock_posts(user): """Determines if a User can lock Questions or Answers.""" return user.is_authenticated() and ( - user.reputation >= LOCK_POSTS or + user.reputation >= forum_settings.MIN_REP_TO_LOCK_POSTS or user.is_superuser) def can_follow_url(user): """Determines if the URL link can be followed by Google search engine.""" - return user.reputation >= DISABLE_URL_NOFOLLOW + return user.reputation >= forum_settings.MIN_REP_TO_DISABLE_URL_NOFOLLOW def can_accept_answer(user, question, answer): return (user.is_authenticated() and @@ -146,7 +106,7 @@ def can_accept_answer(user, question, answer): def can_reopen_question(user, question): return (user.is_authenticated() and user.id == question.author_id and - user.reputation >= REOPEN_OWN_QUESTIONS) or user.is_superuser + user.reputation >= forum_settings.MIN_REP_TO_REOPEN_OWN_QUESTIONS) or user.is_superuser def can_delete_post(user, post): if user.is_superuser: @@ -182,7 +142,8 @@ def can_view_user_edit(request_user, target_user): return (request_user.is_authenticated() and request_user == target_user) def can_upload_files(request_user): - return (request_user.is_authenticated() and request_user.reputation >= UPLOAD_FILES) or \ + return (request_user.is_authenticated() and + request_user.reputation >= forum_settings.MIN_REP_TO_UPLOAD_FILES) or \ request_user.is_superuser ########################################### @@ -205,7 +166,7 @@ def onFlaggedItem(item, post, user, timestamp=None): post.save() post.author.reputation = calculate_reputation(post.author.reputation, - int(REPUTATION_RULES['lose_by_flagged'])) + forum_settings.REP_LOSS_FOR_RECEIVING_DOWNVOTE) post.author.save() question = post @@ -213,37 +174,39 @@ def onFlaggedItem(item, post, user, timestamp=None): question = post.question reputation = Repute(user=post.author, - negative=int(REPUTATION_RULES['lose_by_flagged']), + negative=forum_settings.REP_LOSS_FOR_RECEIVING_DOWNVOTE, question=question, reputed_at=timestamp, reputation_type=-4, reputation=post.author.reputation) reputation.save() #todo: These should be updated to work on same revisions. - if post.offensive_flag_count == VOTE_RULES['scope_flags_invisible_main_page'] : + if post.offensive_flag_count == forum_settings.MIN_FLAGS_TO_HIDE_POST: post.author.reputation = calculate_reputation(post.author.reputation, - int(REPUTATION_RULES['lose_by_flagged_lastrevision_3_times'])) + forum_settings.REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION) + post.author.save() reputation = Repute(user=post.author, - negative=int(REPUTATION_RULES['lose_by_flagged_lastrevision_3_times']), - question=question, - reputed_at=timestamp, - reputation_type=-6, - reputation=post.author.reputation) + negative=forum_settings.REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION, + question=question, + reputed_at=timestamp, + reputation_type=-6, + reputation=post.author.reputation) reputation.save() - elif post.offensive_flag_count == VOTE_RULES['scope_flags_delete_post']: + elif post.offensive_flag_count == forum_settings.MIN_FLAGS_TO_DELETE_POST: post.author.reputation = calculate_reputation(post.author.reputation, - int(REPUTATION_RULES['lose_by_flagged_lastrevision_5_times'])) + forum_settings.REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION) + post.author.save() reputation = Repute(user=post.author, - negative=int(REPUTATION_RULES['lose_by_flagged_lastrevision_5_times']), - question=question, - reputed_at=timestamp, - reputation_type=-7, - reputation=post.author.reputation) + negative=forum_settings.REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION, + question=question, + reputed_at=timestamp, + reputation_type=-7, + reputation=post.author.reputation) reputation.save() post.deleted = True @@ -267,11 +230,13 @@ def onAnswerAccept(answer, user, timestamp=None): answer.save() answer.question.save() - answer.author.reputation = calculate_reputation(answer.author.reputation, - int(REPUTATION_RULES['gain_by_answer_accepted'])) + answer.author.reputation = calculate_reputation( + answer.author.reputation, + forum_settings.REP_GAIN_FOR_RECEIVING_ANSWER_ACCEPTANCE + ) answer.author.save() reputation = Repute(user=answer.author, - positive=int(REPUTATION_RULES['gain_by_answer_accepted']), + positive=REP_GAIN_FOR_RECEIVING_ANSWER_ACCEPTANCE, question=answer.question, reputed_at=timestamp, reputation_type=2, @@ -279,10 +244,10 @@ def onAnswerAccept(answer, user, timestamp=None): reputation.save() user.reputation = calculate_reputation(user.reputation, - int(REPUTATION_RULES['gain_by_accepting_answer'])) + forum_settings.REP_GAIN_FOR_ACCEPTING_ANSWER) user.save() reputation = Repute(user=user, - positive=int(REPUTATION_RULES['gain_by_accepting_answer']), + positive=forum_settings.REP_GAIN_FOR_ACCEPTING_ANSWER, question=answer.question, reputed_at=timestamp, reputation_type=3, @@ -300,21 +265,21 @@ def onAnswerAcceptCanceled(answer, user, timestamp=None): answer.question.save() answer.author.reputation = calculate_reputation(answer.author.reputation, - int(REPUTATION_RULES['lose_by_accepted_answer_cancled'])) + forum_settings.REP_LOSS_FOR_RECEIVING_CANCELATION_OF_ANSWER_ACCEPTANCE) answer.author.save() reputation = Repute(user=answer.author, - negative=int(REPUTATION_RULES['lose_by_accepted_answer_cancled']), - question=answer.question, - reputed_at=timestamp, - reputation_type=-2, - reputation=answer.author.reputation) + negative=forum_settings.REP_LOSS_FOR_RECEIVING_CANCELATION_OF_ANSWER_ACCEPTANCE, + question=answer.question, + reputed_at=timestamp, + reputation_type=-2, + reputation=answer.author.reputation) reputation.save() user.reputation = calculate_reputation(user.reputation, - int(REPUTATION_RULES['lose_by_canceling_accepted_answer'])) + forum_settings.REP_LOSS_FOR_CANCELING_ANSWER_ACCEPTANCE) user.save() reputation = Repute(user=user, - negative=int(REPUTATION_RULES['lose_by_canceling_accepted_answer']), + negative=forum_settings.REP_LOSS_FOR_CANCELING_ANSWER_ACCEPTANCE, question=answer.question, reputed_at=timestamp, reputation_type=-1, @@ -334,9 +299,9 @@ def onUpVoted(vote, post, user, timestamp=None): if not post.wiki: author = post.author todays_rep_gain = Repute.objects.get_reputation_by_upvoted_today(author) - if todays_rep_gain < int(REPUTATION_RULES['scope_per_day_by_upvotes']): + if todays_rep_gain < forum_settings.MAX_REP_GAIN_PER_USER_PER_DAY: author.reputation = calculate_reputation(author.reputation, - int(REPUTATION_RULES['gain_by_upvoted'])) + forum_settings.REP_GAIN_FOR_RECEIVING_UPVOTE) author.save() question = post @@ -344,7 +309,7 @@ def onUpVoted(vote, post, user, timestamp=None): question = post.question reputation = Repute(user=author, - positive=int(REPUTATION_RULES['gain_by_upvoted']), + positive=forum_settings.REP_GAIN_FOR_RECEIVING_UPVOTE, question=question, reputed_at=timestamp, reputation_type=1, @@ -366,7 +331,7 @@ def onUpVotedCanceled(vote, post, user, timestamp=None): if not post.wiki: author = post.author author.reputation = calculate_reputation(author.reputation, - int(REPUTATION_RULES['lose_by_upvote_canceled'])) + forum_settings.REP_LOSS_FOR_RECEIVING_UPVOTE_CANCELATION) author.save() question = post @@ -374,7 +339,7 @@ def onUpVotedCanceled(vote, post, user, timestamp=None): question = post.question reputation = Repute(user=author, - negative=int(REPUTATION_RULES['lose_by_upvote_canceled']), + negative=forum_settings.REP_LOSS_FOR_RECEIVING_UPVOTE_CANCELATION, question=question, reputed_at=timestamp, reputation_type=-8, @@ -394,7 +359,7 @@ def onDownVoted(vote, post, user, timestamp=None): if not post.wiki: author = post.author author.reputation = calculate_reputation(author.reputation, - int(REPUTATION_RULES['lose_by_downvoted'])) + forum_settings.REP_LOSS_FOR_DOWNVOTING) author.save() question = post @@ -402,7 +367,7 @@ def onDownVoted(vote, post, user, timestamp=None): question = post.question reputation = Repute(user=author, - negative=int(REPUTATION_RULES['lose_by_downvoted']), + negative=forum_settings.REP_LOSS_FOR_DOWNVOTING, question=question, reputed_at=timestamp, reputation_type=-3, @@ -410,11 +375,11 @@ def onDownVoted(vote, post, user, timestamp=None): reputation.save() user.reputation = calculate_reputation(user.reputation, - int(REPUTATION_RULES['lose_by_downvoting'])) + forum_settings.REP_LOSS_FOR_RECEIVING_DOWNVOTE) user.save() reputation = Repute(user=user, - negative=int(REPUTATION_RULES['lose_by_downvoting']), + negative=forum_settings.REP_LOSS_FOR_RECEIVING_DOWNVOTE, question=question, reputed_at=timestamp, reputation_type=-5, @@ -436,7 +401,7 @@ def onDownVotedCanceled(vote, post, user, timestamp=None): if not post.wiki: author = post.author author.reputation = calculate_reputation(author.reputation, - int(REPUTATION_RULES['gain_by_downvote_canceled'])) + forum_settings.REP_GAIN_FOR_RECEIVING_DOWNVOTE_CANCELATION) author.save() question = post @@ -444,7 +409,7 @@ def onDownVotedCanceled(vote, post, user, timestamp=None): question = post.question reputation = Repute(user=author, - positive=int(REPUTATION_RULES['gain_by_downvote_canceled']), + positive=forum_settings.REP_GAIN_FOR_RECEIVING_DOWNVOTE_CANCELATION, question=question, reputed_at=timestamp, reputation_type=4, @@ -452,11 +417,11 @@ def onDownVotedCanceled(vote, post, user, timestamp=None): reputation.save() user.reputation = calculate_reputation(user.reputation, - int(REPUTATION_RULES['gain_by_canceling_downvote'])) + forum_settings.REP_GAIN_FOR_CANCELING_DOWNVOTE) user.save() reputation = Repute(user=user, - positive=int(REPUTATION_RULES['gain_by_canceling_downvote']), + positive=forum_settings.REP_GAIN_FOR_CANCELING_DOWNVOTE, question=question, reputed_at=timestamp, reputation_type=5, diff --git a/forum/badges/__init__.py b/forum/badges/__init__.py index 8d7cd097..8d7cd097 100755..100644 --- a/forum/badges/__init__.py +++ b/forum/badges/__init__.py diff --git a/forum/badges/base.py b/forum/badges/base.py index 03ef3565..03ef3565 100755..100644 --- a/forum/badges/base.py +++ b/forum/badges/base.py diff --git a/forum/conf/README b/forum/conf/README new file mode 100644 index 00000000..4dd62329 --- /dev/null +++ b/forum/conf/README @@ -0,0 +1,5 @@ +this directory contains +forum site configurations for livesettings + +they need to be imported in models so made this a part of +models module diff --git a/forum/conf/__init__.py b/forum/conf/__init__.py new file mode 100644 index 00000000..54ea2e32 --- /dev/null +++ b/forum/conf/__init__.py @@ -0,0 +1,16 @@ +#import these to compile code and install values +import forum.conf.minimum_reputation +import forum.conf.vote_rules +import forum.conf.reputation_changes +import forum.conf.email +import forum.conf.forum_data_rules +import forum.conf.flatpages +import forum.conf.site_settings +import forum.conf.external_keys +import forum.conf.skin_counter_settings +import forum.conf.skin_general_settings +import forum.conf.user_settings + +#import main settings object +from forum.conf.settings_wrapper import settings + diff --git a/forum/conf/email.py b/forum/conf/email.py new file mode 100644 index 00000000..8b0f2af3 --- /dev/null +++ b/forum/conf/email.py @@ -0,0 +1,51 @@ +""" +Email related settings +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, IntegerValue, BooleanValue +from livesettings import StringValue +from django.utils.translation import ugettext as _ + +EMAIL = ConfigurationGroup( + 'EMAIL', + _('Email and email alert settings'), + ) + +settings.register( + IntegerValue( + EMAIL, + 'MAX_ALERTS_PER_EMAIL', + default=7, + description=_('Maximum number of news entries in an email alert') + ) +) + +settings.register( + BooleanValue( + EMAIL, + 'EMAIL_VALIDATION', + default=False, + hidden=True, + description=_('Require email verification before allowing to post'), + help_text=_('Active email verification is done by sending a verification key in email') + ) +) + +settings.register( + BooleanValue( + EMAIL, + 'EMAIL_UNIQUE', + default=True, + description=_('Allow only one account per email address') + ) +) + +settings.register( + StringValue( + EMAIL, + 'ANONYMOUS_USER_EMAIL', + default='anonymous@askbot.org', + description=_('Fake email for anonymous user'), + help_text=_('Use this setting to control gravatar for email-less user') + ) +) diff --git a/forum/conf/external_keys.py b/forum/conf/external_keys.py new file mode 100644 index 00000000..f43e1120 --- /dev/null +++ b/forum/conf/external_keys.py @@ -0,0 +1,93 @@ +""" +External service key settings +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, StringValue +from django.utils.translation import ugettext as _ +from django.conf import settings as django_settings + +EXTERNAL_KEYS = ConfigurationGroup( + 'EXTERNAL_KEYS', + _('Keys to connect the site with external services like Facebook, etc.') + ) + +settings.register( + StringValue( + EXTERNAL_KEYS, + 'GOOGLE_SITEMAP_CODE', + description=_('Google site verification key'), + help_text=_( + 'This key helps google index your site ' + 'please obtain is at ' + '<a href="%(google_webmasters_tools_url)s">' + 'google webmasters tools site</a>' + ) % {'google_webmasters_tools_url': + 'https://www.google.com/webmasters/tools/home?hl=' \ + + django_settings.LANGUAGE_CODE} + ) +) + +settings.register( + StringValue( + EXTERNAL_KEYS, + 'GOOGLE_ANALYTICS_KEY', + description=_('Google Analytics key'), + help_text=_( + 'Obtain is at <a href="%(ga_site)s">' + 'Google Analytics</a> site, if you ' + 'wish to use Google Analytics to monitor ' + 'your site' + ) % {'ga_site':'http://www.google.com/intl/%s/analytics/' \ + % django_settings.LANGUAGE_CODE } + ) +) + +settings.register( + StringValue( + EXTERNAL_KEYS, + 'RECAPTCHA_PRIVATE_KEY', + description=_('Recaptcha private key') + ' - does not work yet', + hidden=True, + help_text=_( + 'Recaptcha is a tool that helps distinguish ' + 'real people from annoying spam robots. ' + 'Please get this and a public key at ' + 'the <a href="http://recaptcha.net">recaptcha.net</a>' + ) + ) +) + +settings.register( + StringValue( + EXTERNAL_KEYS, + 'RECAPTCHA_PUBLIC_KEY', + hidden=True, + description=_('Recaptcha public key') + ' - does not work yet' + ) +) + +settings.register( + StringValue( + EXTERNAL_KEYS, + 'FB_API_KEY', + description=_('Facebook public API key') + ' - does not work yet', + hidden=True, + help_text=_( + 'Facebook API key and Facebook secret ' + 'allow to use Facebook Connect login method ' + 'at your site. Please obtain these keys ' + 'at <a href="http://www.facebook.com/developers/createapp.php">' + 'facebook create app</a> site' + ) + ) + +) + +settings.register( + StringValue( + EXTERNAL_KEYS, + 'FB_SECRET', + hidden=True, + description=_('Facebook secret key') + ' - does not work yet' + ) +) diff --git a/forum/conf/flatpages.py b/forum/conf/flatpages.py new file mode 100644 index 00000000..eb6b646a --- /dev/null +++ b/forum/conf/flatpages.py @@ -0,0 +1,37 @@ +""" +Q&A forum flatpages (about, etc.) +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, LongStringValue +from django.utils.translation import ugettext as _ + +FLATPAGES = ConfigurationGroup( + 'FLATPAGES', + _('Flatpages - about, privacy policy, etc.') + ) + +settings.register( + LongStringValue( + FLATPAGES, + 'FORUM_ABOUT', + description=_('Text the Q&A forum About page (html format)'), + help_text=\ + _( + 'Save, then <a href="http://validator.w3.org/">' + 'use HTML validator</a> on the "about" page to check your input.' + ) + ) +) + +settings.register( + LongStringValue( + FLATPAGES, + 'FORUM_PRIVACY', + description=_('Text the Q&A forum Privacy Policy (html format)'), + help_text=\ + _( + 'Save, then <a href="http://validator.w3.org/">' + 'use HTML validator</a> on the "privacy" page to check your input.' + ) + ) +) diff --git a/forum/conf/forum_data_rules.py b/forum/conf/forum_data_rules.py new file mode 100644 index 00000000..e452ea7b --- /dev/null +++ b/forum/conf/forum_data_rules.py @@ -0,0 +1,62 @@ +""" +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 django.utils.translation import ugettext as _ +from forum import const + +FORUM_DATA_RULES = ConfigurationGroup( + 'FORUM_DATA_RULES', + _('Settings for forum data entry and display') + ) + +settings.register( + BooleanValue( + FORUM_DATA_RULES, + 'WIKI_ON', + default=True, + description=_('Check to enable community wiki feature') + ) +) + +settings.register( + IntegerValue( + FORUM_DATA_RULES, + 'MAX_TAG_LENGTH', + default=20, + description=_('Maximum length of tag (number of characters)') + ) +) + +settings.register( + IntegerValue( + FORUM_DATA_RULES, + 'MAX_TAGS_PER_POST', + default=5, + description=_('Maximum number of tags per question') + ) +) + +#todo: looks like there is a bug in livesettings +#that does not allow Integer values with defaults and choices +settings.register( + StringValue( + FORUM_DATA_RULES, + 'DEFAULT_QUESTIONS_PAGE_SIZE', + choices=const.PAGE_SIZE_CHOICES, + default='30', + description=_('Number of questions to list by default') + ) +) + +settings.register( + StringValue( + FORUM_DATA_RULES, + 'UNANSWERED_QUESTION_MEANING', + choices=const.UNANSWERED_QUESTION_MEANING_CHOICES, + default='NO_ACCEPTED_ANSWERS', + description=_('What should "unanswered question" mean?') + ) +) diff --git a/forum/conf/minimum_reputation.py b/forum/conf/minimum_reputation.py new file mode 100644 index 00000000..a83d94fd --- /dev/null +++ b/forum/conf/minimum_reputation.py @@ -0,0 +1,148 @@ +""" +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 django.utils.translation import ugettext as _ + +MIN_REP = ConfigurationGroup( + 'MIN_REP', + _('Minimum reputation required to perform actions'), + ordering=0 + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_VOTE_UP', + default=15, + description=_('Upvote') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_VOTE_DOWN', + default=100, + description=_('Downvote') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_FLAG_OFFENSIVE', + default=15, + description=_('Flag offensive') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_LEAVE_COMMENTS', + default=50, + description=_('Leave comments') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_DELETE_OTHERS_COMMENTS', + default=2000, + description=_('Delete comments posted by others') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_UPLOAD_FILES', + default=60, + description=_('Upload files') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_CLOSE_OWN_QUESTIONS', + default=250, + description=_('Close own questions'), + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_RETAG_OTHERS_QUESTIONS', + default=500, + description=_('Retag questions posted by other people') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_REOPEN_OWN_QUESTIONS', + default=500, + description=_('Reopen own questions') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_EDIT_WIKI', + default=750, + description=_('Edit community wiki posts') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_EDIT_OTHERS_POSTS', + default=2000, + description=_('Edit posts authored by other people') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_VIEW_OFFENSIVE_FLAGS', + default=2000, + description=_('View offensive flags') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_DISABLE_URL_NOFOLLOW', + default=2000, + description=_('Disable nofollow directive on links') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_CLOSE_OTHERS_QUESTIONS', + default=2000, + description=_('Close questions asked by others') + ) + ) + +settings.register( + IntegerValue( + MIN_REP, + 'MIN_REP_TO_LOCK_POSTS', + default=4000, + description=_('Lock posts') + ) + ) diff --git a/forum/conf/reputation_changes.py b/forum/conf/reputation_changes.py new file mode 100644 index 00000000..cef177f5 --- /dev/null +++ b/forum/conf/reputation_changes.py @@ -0,0 +1,149 @@ +""" +Settings for reputation changes that apply to +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 django.utils.translation import ugettext as _ + +REP_CHANGES = ConfigurationGroup( + 'REP_CHANGES', + _('Reputaion loss and gain rules'), + ordering=2 + ) + +settings.register( + IntegerValue( + REP_CHANGES, + 'MAX_REP_GAIN_PER_USER_PER_DAY', + default=200, + description=_('Maximum daily reputation gain per user') + ) +) + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_GAIN_FOR_RECEIVING_UPVOTE', + default=10, + description=_('Gain for receiving an upvote') + ) +) + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_GAIN_FOR_RECEIVING_ANSWER_ACCEPTANCE', + default=15, + description=_('Gain for the author of accepted answer') + ) +) + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_GAIN_FOR_ACCEPTING_ANSWER', + default=2, + description=_('Gain for accepting best answer') + ) +) + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_GAIN_FOR_RECEIVING_DOWNVOTE_CANCELATION', + default=2, + description=_('Gain for post owner on canceled downvote') + ) +) + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_GAIN_FOR_CANCELING_DOWNVOTE', + default=1, + description=_('Gain for voter on canceling downvote') + ) +) +#'gain_by_canceling_downvote', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_CANCELING_ANSWER_ACCEPTANCE', + default=-2, + description=_('Loss for voter for canceling of answer acceptance') + ) +) +#'lose_by_canceling_accepted_answer', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_RECEIVING_CANCELATION_OF_ANSWER_ACCEPTANCE', + default=-5, + description=_('Loss for author whose answer was "un-accepted"') + ) +) +#'lose_by_accepted_answer_cancled', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_DOWNVOTING', + default=-2, + description=_('Loss for giving a downvote') + ) +) +#'lose_by_downvoted', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_RECEIVING_FLAG', + default=-2, + description=_('Loss for owner of post that was flagged offensive') + ) +) +#'lose_by_flagged', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_RECEIVING_DOWNVOTE', + default=-1, + description=_('Loss for owner of post that was downvoted') + ) +) +#'lose_by_downvoting', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_RECEIVING_THREE_FLAGS_PER_REVISION', + default=-30, + description=_('Loss for owner of post that was flagged 3 times per same revision') + ) +) +#'lose_by_flagged_lastrevision_3_times', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_RECEIVING_FIVE_FLAGS_PER_REVISION', + default=-100, + description=_('Loss for owner of post that was flagged 5 times per same revision') + ) +) +#'lose_by_flagged_lastrevision_5_times', + +settings.register( + IntegerValue( + REP_CHANGES, + 'REP_LOSS_FOR_RECEIVING_UPVOTE_CANCELATION', + default=-10, + description=_('Loss for post owner when upvote is canceled') + ) +) +#'lose_by_upvote_canceled', diff --git a/forum/conf/settings_wrapper.py b/forum/conf/settings_wrapper.py new file mode 100644 index 00000000..86d41709 --- /dev/null +++ b/forum/conf/settings_wrapper.py @@ -0,0 +1,70 @@ +""" +Definition of a Singleton wrapper class for livesettings +with interface similar to django.conf.settings +that is each setting has unique key and is accessible +via dotted lookup. + +for example to lookup value of setting BLAH you would do + +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) + +the value will be taken from 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 +""" +from livesettings import SortedDotDict, config_register + +class ConfigSettings(object): + """A very simple Singleton wrapper for settings + a limitation is that all settings names using this class + must be distinct, even though they might belong + to different settings groups + """ + __instance = None + + def __init__(self): + """assigns SortedDotDict to self.__instance if not set""" + if ConfigSettings.__instance == None: + ConfigSettings.__instance = SortedDotDict() + self.__dict__['_ConfigSettings__instance'] = ConfigSettings.__instance + self.__ordering_index = {} + + def __getattr__(self, key): + """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 + """ + return getattr(self.__instance, key).value + + def register(self, value): + """registers the setting + value must be a subclass of livesettings.Value + """ + key = value.key + group_key = value.group.key + + ordering = self.__ordering_index.get(group_key, None) + if ordering: + ordering += 1 + value.ordering = ordering + else: + ordering = 1 + value.ordering = ordering + self.__ordering_index[group_key] = ordering + + if key in self.__instance: + raise Exception('setting %s is already registered' % key) + else: + self.__instance[key] = config_register(value) + +#settings instance to be used elsewhere in the project +settings = ConfigSettings() diff --git a/forum/conf/site_settings.py b/forum/conf/site_settings.py new file mode 100644 index 00000000..19445bda --- /dev/null +++ b/forum/conf/site_settings.py @@ -0,0 +1,95 @@ +""" +Q&A website settings - title, desctiption, basic urls +keywords +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, StringValue +from django.utils.translation import ugettext as _ +from django.utils.html import escape +from django.conf import settings as django_settings +from forum import const + +QA_SITE_SETTINGS = ConfigurationGroup( + 'QA_SITE_SETTINGS', + _('Q&A forum website parameters and urls') + ) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'APP_TITLE', + default=u'ASKBOT: Open Source Q&A Forum', + description=_('Site title for the Q&A forum') + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'APP_KEYWORDS', + default=u'ASKBOT,CNPROG,forum,community', + description=_('Comma separated list of Q&A site keywords') + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'APP_COPYRIGHT', + default='Copyright ASKBOT, 2010. Some rights reserved under creative commons license.', + description=_('Copyright message to show in the footer') + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'APP_DESCRIPTION', + default='Open source question and answer forum written in Python and Django', + description=_('Site description for the search engines') + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'APP_SHORT_NAME', + default=_('Askbot'), + hidden=True, + description=_('Short name for your Q&A forum') + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'APP_URL', + default='http://askbot.org', + description=_('Base URL for your Q&A forum, must start with http or https'), + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'GREETING_URL', + default='/' + _('faq/'),#cannot reverse url here, unfortunately, must be absolute also + hidden=True, + description=_('Link shown in the greeting message shown to the anonymous user'), + help_text=_('If you change this url from the default - ' + 'then you will also probably want to adjust translation of ' + 'the following string: ') + '"' + + escape(const.GREETING_FOR_ANONYMOUS_USER + '"' + ' You can find this string in your locale django.po file' + ) + ) +) + +settings.register( + StringValue( + QA_SITE_SETTINGS, + 'FEEDBACK_SITE_URL', + description=_('Feedback site URL'), + help_text=_('If left empty, a simple internal feedback form will be used instead') + ) +) diff --git a/forum/conf/skin_counter_settings.py b/forum/conf/skin_counter_settings.py new file mode 100644 index 00000000..51c7e332 --- /dev/null +++ b/forum/conf/skin_counter_settings.py @@ -0,0 +1,251 @@ +""" +Skin settings to color view, vote and answer counters +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, IntegerValue, StringValue +from django.utils.translation import ugettext as _ +from forum_modules.grapefruit import Color + +SKIN_COUNTER_SETTINGS = ConfigurationGroup( + 'SKIN_COUNTER_SETTINGS', + _('Skin: view, vote and answer counters') + ) + +settings.register( + IntegerValue( + SKIN_COUNTER_SETTINGS, + 'VOTE_COUNTER_EXPECTED_MAXIMUM', + default=3, + description=_('Vote counter value to give "full color"') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VOTE_COUNTER_EMPTY_BG', + default='white', + description=_('Background color for votes = 0'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VOTE_COUNTER_EMPTY_FG', + default='gray', + description=_('Foreground color for votes = 0'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VOTE_COUNTER_MIN_BG', + default='white', + description=_('Background color for votes = 1'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VOTE_COUNTER_MIN_FG', + default='black', + description=_('Foreground color for votes = 1'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VOTE_COUNTER_MAX_BG', + default='#a9d0f5', + description=_('Background color for votes = MAX'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VOTE_COUNTER_MAX_FG', + default=Color.NewFromHtml( + settings.COLORS_VOTE_COUNTER_MAX_BG + ).DarkerColor(0.7).html, + description=_('Foreground color for votes = MAX'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + IntegerValue( + SKIN_COUNTER_SETTINGS, + 'VIEW_COUNTER_EXPECTED_MAXIMUM', + default=100, + description=_('View counter value to give "full color"') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VIEW_COUNTER_EMPTY_BG', + default='gray', + description=_('Background color for views = 0'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VIEW_COUNTER_EMPTY_FG', + default='white', + description=_('Foreground color for views = 0'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VIEW_COUNTER_MIN_BG', + default='#D0F5A9', + description=_('Background color for views = 1'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VIEW_COUNTER_MIN_FG', + default=Color.NewFromHtml( + settings.COLORS_VIEW_COUNTER_MIN_BG + ).DarkerColor(0.6).html, + description=_('Foreground color for views = 1'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VIEW_COUNTER_MAX_BG', + default='#FF8000', + description=_('Background color for views = MAX'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_VIEW_COUNTER_MAX_FG', + default=Color.NewFromHtml( + settings.COLORS_VIEW_COUNTER_MAX_BG + ).DarkerColor(0.7).html, + description=_('Foreground color for views = MAX'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + IntegerValue( + SKIN_COUNTER_SETTINGS, + 'ANSWER_COUNTER_EXPECTED_MAXIMUM', + default=4, + description=_('Answer counter value to give "full color"') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_EMPTY_BG', + default=Color.NewFromHtml('#a40000').Blend( + Color.NewFromHtml('white'),0.8 + ).html, + description=_('Background color for answers = 0'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_EMPTY_FG', + default='yellow', + description=_('Foreground color for answers = 0'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_MIN_BG', + default='#AEB404', + description=_('Background color for answers = 1'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_MIN_FG', + default='white', + description=_('Foreground color for answers = 1'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_MAX_BG', + default=Color.NewFromHtml('#61380B').Blend( + Color.NewFromHtml('white'),0.75 + ).html, + description=_('Background color for answers = MAX'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_MAX_FG', + default='#ffff00', + description=_('Foreground color for answers = MAX'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_ACCEPTED_BG', + default=Color.NewFromHtml('darkgreen').Blend( + Color.NewFromHtml('white'),0.8 + ).html, + description=_('Background color for accepted'), + help_text=_('HTML color name of hex value') + ) +) + +settings.register( + StringValue( + SKIN_COUNTER_SETTINGS, + 'COLORS_ANSWER_COUNTER_ACCEPTED_FG', + default='#D0F5A9', + description=_('Foreground color for accepted answer'), + help_text=_('HTML color name of hex value') + ) +) diff --git a/forum/conf/skin_general_settings.py b/forum/conf/skin_general_settings.py new file mode 100644 index 00000000..166cd603 --- /dev/null +++ b/forum/conf/skin_general_settings.py @@ -0,0 +1,38 @@ +""" +General skin settings +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, StringValue, IntegerValue +from django.utils.translation import ugettext as _ +from forum.skins import get_skin_choices + +GENERAL_SKIN_SETTINGS = ConfigurationGroup( + 'GENERAL_SKIN_SETTINGS', + _('Skin: general settings'), + ) + +settings.register( + StringValue( + GENERAL_SKIN_SETTINGS, + 'ASKBOT_DEFAULT_SKIN', + default='default', + choices=get_skin_choices(), + description=_('Select skin'), + ) +) + +settings.register( + IntegerValue( + GENERAL_SKIN_SETTINGS, + 'MEDIA_RESOURCE_REVISION', + default=1, + description=_('Skin media revision number'), + help_text=_( + 'Increment this number when you change ' + 'image in skin media or stylesheet. ' + 'This helps avoid showing your users ' + 'outdated images from their browser cache.' + ) + ) +) + diff --git a/forum/conf/user_settings.py b/forum/conf/user_settings.py new file mode 100644 index 00000000..760f921f --- /dev/null +++ b/forum/conf/user_settings.py @@ -0,0 +1,30 @@ +""" +User policy settings +""" +from forum.conf.settings_wrapper import settings +from livesettings import ConfigurationGroup, BooleanValue, IntegerValue +from django.utils.translation import ugettext as _ + +USER_SETTINGS = ConfigurationGroup( + 'USER_SETTINGS', + _('User policy settings') + ) + +settings.register( + BooleanValue( + USER_SETTINGS, + 'EDITABLE_SCREEN_NAME', + default=True, + description=_('Allow editing user screen name') + ) +) + +settings.register( + IntegerValue( + USER_SETTINGS, + 'MIN_USERNAME_LENGTH', + hidden=True, + default=1, + description=_('Minimum allowed length for screen name') + ) +) diff --git a/forum/conf/vote_rules.py b/forum/conf/vote_rules.py new file mode 100644 index 00000000..f249ef53 --- /dev/null +++ b/forum/conf/vote_rules.py @@ -0,0 +1,69 @@ +""" +Forum configuration settings detailing rules on votes +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 django.utils.translation import ugettext as _ + +VOTE_RULES = ConfigurationGroup( + 'VOTE_RULES', + _('Limits applicable to votes and moderation flags'), + ordering=1, + ) + +settings.register( + IntegerValue( + VOTE_RULES, + 'MAX_VOTES_PER_USER_PER_DAY', + default=30, + description=_('Number of votes a user can cast per day') + ) +) + +settings.register( + IntegerValue( + VOTE_RULES, + 'MAX_FLAGS_PER_USER_PER_DAY', + default=5, + description=_('Maximum number of flags per user per day') + ) +) + +settings.register( + IntegerValue( + VOTE_RULES, + 'VOTES_LEFT_WARNING_THRESHOLD', + default=5, + description=_('Threshold for warning about remaining daily votes') + ) +) + +settings.register( + IntegerValue( + VOTE_RULES, + 'MAX_DAYS_TO_CANCEL_VOTE', + default=1, + description=_('Number of days to allow canceling votes') + ) +) + +settings.register( + IntegerValue( + VOTE_RULES, + 'MIN_FLAGS_TO_HIDE_POST', + default=3, + description=_('Number of flags required to automatically hide posts') + ) +) + +settings.register( + IntegerValue( + VOTE_RULES, + 'MIN_FLAGS_TO_DELETE_POST', + default=5, + description=_('Number of flags required to automatically delete posts') + ) +) diff --git a/forum/const.py b/forum/const/__init__.py index 07b0291c..50676189 100755..100644 --- a/forum/const.py +++ b/forum/const/__init__.py @@ -54,12 +54,15 @@ POST_SCOPE_LIST = ( ('favorite', _('favorite')), ) DEFAULT_POST_SCOPE = 'all' -DEFAULT_QUESTIONS_PAGE_SIZE = 30 PAGE_SIZE_CHOICES = (('10','10',),('30','30',),('50','50',),) -UNANSWERED_MEANING_LIST = ('NO_ANSWERS','NO_UPVOTED_ANSWERS','NO_ACCEPTED_ANSWERS') -UNANSWERED_MEANING = 'NO_ACCEPTED_ANSWERS' -assert(UNANSWERED_MEANING in UNANSWERED_MEANING_LIST) +UNANSWERED_QUESTION_MEANING_CHOICES = ( + ('NO_ANSWERS', _('Question has no answers')), + ('NO_ACCEPTED_ANSWERS', _('Question has no accepted answers')), +) +#todo: implement this +# ('NO_UPVOTED_ANSWERS',), +#) #todo: #this probably needs to be language-specific @@ -69,8 +72,6 @@ assert(UNANSWERED_MEANING in UNANSWERED_MEANING_LIST) #to do full string match TAG_REGEX = r'^[a-z0-9\+\.\-]+$' TAG_SPLIT_REGEX = r'[ ,]+' -MAX_TAG_LENGTH = 20 #default 20 chars -MAX_TAGS_PER_POST = 5 #no more than five tags TYPE_ACTIVITY_ASK_QUESTION=1 TYPE_ACTIVITY_ANSWER=2 @@ -130,5 +131,7 @@ CONST = { #how to filter questions by tags in email digests? TAG_EMAIL_FILTER_CHOICES = (('ignored', _('exclude ignored tags')),('interesting',_('allow only selected tags'))) -MAX_ALERTS_PER_EMAIL = 7 -USERS_PAGE_SIZE = 28 +USERS_PAGE_SIZE = 28#todo: move it to settings? + +#an exception import * because that file has only strings +from forum.const.message_keys import * diff --git a/forum/const/message_keys.py b/forum/const/message_keys.py new file mode 100644 index 00000000..c52b9353 --- /dev/null +++ b/forum/const/message_keys.py @@ -0,0 +1,19 @@ +""" +This file must hold keys for translatable messages +that are used as variables +it is important that a dummy _() function is used here +this way message key will be pulled into django.po +and can still be used as a variable in python files +""" +_ = lambda v:v + +#NOTE: all strings must be explicitly put into this dictionary, +#because you don't want to import _ from here with import * +__all__ = ['GREETING_FOR_ANONYMOUS_USER',] + +#this variable is shown in settings, because +#the url within is configurable, the default is reverse('faq') +#if user changes url they will have to be able to fix the +#message translation too +GREETING_FOR_ANONYMOUS_USER = \ + _('First time here? Check out the <a href="%s">FAQ</a>!') diff --git a/forum/context.py b/forum/context.py index 043af81d..47fb43f2 100644 --- a/forum/context.py +++ b/forum/context.py @@ -1,22 +1,24 @@ from django.conf import settings +from forum.conf import settings as forum_settings def application_settings(context): my_settings = { - 'APP_TITLE' : settings.APP_TITLE, - 'APP_SHORT_NAME' : settings.APP_SHORT_NAME, - 'APP_URL' : settings.APP_URL, - 'APP_KEYWORDS' : settings.APP_KEYWORDS, - 'APP_DESCRIPTION' : settings.APP_DESCRIPTION, - 'APP_INTRO' : settings.APP_INTRO, - 'EMAIL_VALIDATION': settings.EMAIL_VALIDATION, - 'FEEDBACK_SITE_URL': settings.FEEDBACK_SITE_URL, + 'WIKI_ON':forum_settings.WIKI_ON, + 'APP_TITLE' : forum_settings.APP_TITLE, + 'APP_URL' : forum_settings.APP_URL, + 'APP_KEYWORDS' : forum_settings.APP_KEYWORDS, + 'APP_DESCRIPTION': forum_settings.APP_DESCRIPTION, + 'APP_COPYRIGHT': forum_settings.APP_COPYRIGHT, + 'FEEDBACK_SITE_URL': forum_settings.FEEDBACK_SITE_URL, + 'FORUM_ABOUT': forum_settings.FORUM_ABOUT, + 'FORUM_PRIVACY': forum_settings.FORUM_PRIVACY, + 'GOOGLE_SITEMAP_CODE':forum_settings.GOOGLE_SITEMAP_CODE, + 'GOOGLE_ANALYTICS_KEY':forum_settings.GOOGLE_ANALYTICS_KEY, + 'EMAIL_VALIDATION': forum_settings.EMAIL_VALIDATION, + 'RESOURCE_REVISION':forum_settings.MEDIA_RESOURCE_REVISION, + 'ASKBOT_SKIN':forum_settings.ASKBOT_DEFAULT_SKIN, + 'EDITABLE_SCREEN_NAME':forum_settings.EDITABLE_SCREEN_NAME, 'FORUM_SCRIPT_ALIAS': settings.FORUM_SCRIPT_ALIAS, 'LANGUAGE_CODE': settings.LANGUAGE_CODE, - 'GOOGLE_SITEMAP_CODE':settings.GOOGLE_SITEMAP_CODE, - 'GOOGLE_ANALYTICS_KEY':settings.GOOGLE_ANALYTICS_KEY, - 'WIKI_ON':settings.WIKI_ON, - 'RESOURCE_REVISION':settings.RESOURCE_REVISION, - 'ASKBOT_SKIN':settings.ASKBOT_DEFAULT_SKIN, - 'EDITABLE_SCREEN_NAME':settings.EDITABLE_SCREEN_NAME, } return {'settings':my_settings} diff --git a/forum/documentation/HOW_TO_DEBUG b/forum/doc/HOW_TO_DEBUG index fbbdb1f7..fbbdb1f7 100644 --- a/forum/documentation/HOW_TO_DEBUG +++ b/forum/doc/HOW_TO_DEBUG diff --git a/forum/documentation/INSTALL b/forum/doc/INSTALL index 73714566..73714566 100644 --- a/forum/documentation/INSTALL +++ b/forum/doc/INSTALL diff --git a/forum/documentation/INSTALL.pip b/forum/doc/INSTALL.pip index 2f817ff8..2f817ff8 100644 --- a/forum/documentation/INSTALL.pip +++ b/forum/doc/INSTALL.pip diff --git a/forum/documentation/INSTALL.webfaction b/forum/doc/INSTALL.webfaction index a449ffe6..a449ffe6 100644 --- a/forum/documentation/INSTALL.webfaction +++ b/forum/doc/INSTALL.webfaction diff --git a/forum/documentation/ROADMAP.rst b/forum/doc/ROADMAP.rst index c79e0ae4..c79e0ae4 100644 --- a/forum/documentation/ROADMAP.rst +++ b/forum/doc/ROADMAP.rst diff --git a/forum/documentation/TODO.rst b/forum/doc/TODO.rst index b89013b0..b89013b0 100644 --- a/forum/documentation/TODO.rst +++ b/forum/doc/TODO.rst diff --git a/forum/documentation/UPGRADE b/forum/doc/UPGRADE index 538b75a0..538b75a0 100644 --- a/forum/documentation/UPGRADE +++ b/forum/doc/UPGRADE diff --git a/forum/documentation/WISH_LIST b/forum/doc/WISH_LIST index 2b53662c..2b53662c 100644 --- a/forum/documentation/WISH_LIST +++ b/forum/doc/WISH_LIST diff --git a/forum/documentation/askbot-requirements.txt b/forum/doc/askbot-requirements.txt index 66a37fbe..66a37fbe 100644 --- a/forum/documentation/askbot-requirements.txt +++ b/forum/doc/askbot-requirements.txt diff --git a/forum/documentation/scratch b/forum/doc/scratch index ca4e67e9..948055fa 100644 --- a/forum/documentation/scratch +++ b/forum/doc/scratch @@ -5,3 +5,8 @@ Name duplicates previous WSGI daemon definition different keys - empty space counts for translation keys {% blocktrans %}page number {{num}} {% endblocktrans %} {% blocktrans %}page number {{num}}{% endblocktrans %} + +for admin interface downloaded two packages: +django-keyedcache +django-livesettings +from http://bitbucket.org/bkroeze/ diff --git a/forum/feed.py b/forum/feed.py index e4b929e9..94065120 100755..100644 --- a/forum/feed.py +++ b/forum/feed.py @@ -1,7 +1,7 @@ #!/usr/bin/env python #encoding:utf-8 #------------------------------------------------------------------------------- -# Name: Syndication feed class for subsribtion +# Name: Syndication feed class for subscription # Purpose: # # Author: Mike @@ -13,13 +13,13 @@ from django.contrib.syndication.feeds import Feed, FeedDoesNotExist from django.utils.translation import ugettext as _ from models import Question -from django.conf import settings +from forum.conf import settings as forum_settings class RssLastestQuestionsFeed(Feed): - title = settings.APP_TITLE + _(' - ')+ _('latest questions') - link = settings.APP_URL #+ '/' + _('question/') - description = settings.APP_DESCRIPTION + title = forum_settings.APP_TITLE + _(' - ')+ _('latest questions') + link = forum_settings.APP_URL + description = forum_settings.APP_DESCRIPTION #ttl = 10 - copyright = settings.APP_COPYRIGHT + copyright = forum_settings.APP_COPYRIGHT def item_link(self, item): return self.link + item.get_absolute_url() diff --git a/forum/forms.py b/forum/forms.py index e9781dc9..7e5ae755 100755..100644 --- a/forum/forms.py +++ b/forum/forms.py @@ -11,6 +11,7 @@ from django.contrib.contenttypes.models import ContentType from forum.utils.forms import NextUrlField, UserNameField, SetPasswordForm from recaptcha_django import ReCaptchaField from django.conf import settings +from forum.conf import settings as forum_settings import logging @@ -65,15 +66,16 @@ class TagNamesField(forms.CharField): tag_strings = split_re.split(data) out_tag_list = [] tag_count = len(tag_strings) - if tag_count > const.MAX_TAGS_PER_POST: + if tag_count > forum_settings.MAX_TAGS_PER_POST: + max_tags = forum_settings.MAX_TAGS_PER_POST msg = ungettext( - 'please use %(tag_count)d tag or less',#odd but have to use to pluralize + 'please use %(tag_count)d tag or less', 'please use %(tag_count)d tags or less', - tag_count) % {'tag_count':tag_count} + tag_count) % {'tag_count':max_tags} raise forms.ValidationError(msg) for tag in tag_strings: tag_length = len(tag) - if tag_length > const.MAX_TAG_LENGTH: + if tag_length > forum_settings.MAX_TAG_LENGTH: #singular form is odd in english, but required for pluralization #in other languages msg = ungettext('each tag must be shorter than %(max_chars)d character',#odd but added for completeness @@ -97,7 +99,7 @@ class WikiField(forms.BooleanField): self.label = _('community wiki') self.help_text = _('if you choose community wiki option, the question and answer do not generate points and name of author will not be shown') def clean(self,value): - return value and settings.WIKI_ON + return value and forum_settings.WIKI_ON class EmailNotifyField(forms.BooleanField): def __init__(self, *args, **kwargs): @@ -231,7 +233,7 @@ class AnswerForm(forms.Form): def __init__(self, question, user, *args, **kwargs): super(AnswerForm, self).__init__(*args, **kwargs) self.fields['email_notify'].widget.attrs['id'] = 'question-subscribe-updates'; - if question.wiki and settings.WIKI_ON: + if question.wiki and forum_settings.WIKI_ON: self.fields['wiki'].initial = True if user.is_authenticated(): if user in question.followed_by.all(): @@ -292,7 +294,7 @@ class EditAnswerForm(forms.Form): class EditUserForm(forms.Form): email = forms.EmailField(label=u'Email', help_text=_('this email does not have to be linked to gravatar'), required=True, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) - if settings.EDITABLE_SCREEN_NAME: + if forum_settings.EDITABLE_SCREEN_NAME: username = UserNameField(label=_('Screen name')) realname = forms.CharField(label=_('Real name'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) website = forms.URLField(label=_('Website'), required=False, max_length=255, widget=forms.TextInput(attrs={'size' : 35})) @@ -303,7 +305,7 @@ class EditUserForm(forms.Form): def __init__(self, user, *args, **kwargs): super(EditUserForm, self).__init__(*args, **kwargs) logging.debug('initializing the form') - if settings.EDITABLE_SCREEN_NAME: + if forum_settings.EDITABLE_SCREEN_NAME: self.fields['username'].initial = user.username self.fields['username'].user_instance = user self.fields['email'].initial = user.email @@ -322,7 +324,7 @@ class EditUserForm(forms.Form): """For security reason one unique email in database""" if self.user.email != self.cleaned_data['email']: #todo dry it, there is a similar thing in openidauth - if settings.EMAIL_UNIQUE == True: + if forum_settings.EMAIL_UNIQUE == True: if 'email' in self.cleaned_data: try: user = User.objects.get(email = self.cleaned_data['email']) diff --git a/forum/importers/stackexchange/management/commands/load_stackexchange.py b/forum/importers/stackexchange/management/commands/load_stackexchange.py index ee22e33a..308ac0bb 100644 --- a/forum/importers/stackexchange/management/commands/load_stackexchange.py +++ b/forum/importers/stackexchange/management/commands/load_stackexchange.py @@ -11,7 +11,7 @@ import forum.models as askbot import forum.importers.stackexchange.models as se from forum.forms import EditUserEmailFeedsForm from forum.utils.html import sanitize_html -from django.conf import settings +from forum.conf import settings as forum_settings from django.contrib.auth.models import Message as DjangoMessage from django.utils.translation import ugettext as _ #from markdown2 import Markdown @@ -209,7 +209,7 @@ class X(object):# @classmethod def get_email(cls, email):#todo: fix fringe case - user did not give email! if email is None: - return settings.ANONYMOUS_USER_EMAIL + return forum_settings.ANONYMOUS_USER_EMAIL else: assert(email != '') return email diff --git a/forum/management/__init__.py b/forum/management/__init__.py index b654caaa..b654caaa 100755..100644 --- a/forum/management/__init__.py +++ b/forum/management/__init__.py diff --git a/forum/management/commands/__init__.py b/forum/management/commands/__init__.py index e69de29b..e69de29b 100755..100644 --- a/forum/management/commands/__init__.py +++ b/forum/management/commands/__init__.py diff --git a/forum/management/commands/base_command.py b/forum/management/commands/base_command.py index c073bf7a..c073bf7a 100755..100644 --- a/forum/management/commands/base_command.py +++ b/forum/management/commands/base_command.py diff --git a/forum/management/commands/clean_award_badges.py b/forum/management/commands/clean_award_badges.py index 117e3a5f..117e3a5f 100755..100644 --- a/forum/management/commands/clean_award_badges.py +++ b/forum/management/commands/clean_award_badges.py diff --git a/forum/management/commands/message_to_everyone.py b/forum/management/commands/message_to_everyone.py index c020c178..c020c178 100755..100644 --- a/forum/management/commands/message_to_everyone.py +++ b/forum/management/commands/message_to_everyone.py diff --git a/forum/management/commands/multi_award_badges.py b/forum/management/commands/multi_award_badges.py index 6b330cf9..6b330cf9 100755..100644 --- a/forum/management/commands/multi_award_badges.py +++ b/forum/management/commands/multi_award_badges.py diff --git a/forum/management/commands/once_award_badges.py b/forum/management/commands/once_award_badges.py index 60457373..60457373 100755..100644 --- a/forum/management/commands/once_award_badges.py +++ b/forum/management/commands/once_award_badges.py diff --git a/forum/management/commands/pg_base_command.py b/forum/management/commands/pg_base_command.py index b3167dcf..b3167dcf 100755..100644 --- a/forum/management/commands/pg_base_command.py +++ b/forum/management/commands/pg_base_command.py diff --git a/forum/management/commands/pg_clean_award_badges.py b/forum/management/commands/pg_clean_award_badges.py index b3925a68..b3925a68 100755..100644 --- a/forum/management/commands/pg_clean_award_badges.py +++ b/forum/management/commands/pg_clean_award_badges.py diff --git a/forum/management/commands/pg_multi_award_badges.py b/forum/management/commands/pg_multi_award_badges.py index 75f84bfe..75f84bfe 100755..100644 --- a/forum/management/commands/pg_multi_award_badges.py +++ b/forum/management/commands/pg_multi_award_badges.py diff --git a/forum/management/commands/pg_once_award_badges.py b/forum/management/commands/pg_once_award_badges.py index b2f79363..b2f79363 100755..100644 --- a/forum/management/commands/pg_once_award_badges.py +++ b/forum/management/commands/pg_once_award_badges.py diff --git a/forum/management/commands/sample_command.py b/forum/management/commands/sample_command.py index 55e67235..55e67235 100755..100644 --- a/forum/management/commands/sample_command.py +++ b/forum/management/commands/sample_command.py diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py index 5204a81e..db16f00f 100755..100644 --- a/forum/management/commands/send_email_alerts.py +++ b/forum/management/commands/send_email_alerts.py @@ -1,13 +1,14 @@ from django.core.management.base import NoArgsCommand from django.db import connection from django.db.models import Q, F -from forum.models import * -from forum import const +from forum.models import User, Question, Answer, Tag, QuestionRevision +from forum.models import AnswerRevision, Activity, EmailFeedSetting from django.core.mail import EmailMessage from django.utils.translation import ugettext as _ from django.utils.translation import ungettext import datetime from django.conf import settings +from forum.conf import settings as forum_settings import logging from forum.utils.odict import OrderedDict from django.contrib.contenttypes.models import ContentType @@ -18,7 +19,7 @@ def extend_question_list(src, dst, limit=False): or None dst - is an ordered dictionary """ - if limit and len(dst.keys()) >= const.MAX_ALERTS_PER_EMAIL: + if limit and len(dst.keys()) >= forum_settings.MAX_ALERTS_PER_EMAIL: return if src is None:#is not QuerySet return #will not do anything if subscription of this type is not used @@ -110,16 +111,16 @@ class Command(NoArgsCommand): q_ask_B = Q_set_B.filter(author=user) q_ask_B.cutoff_time = cutoff_time elif feed.feed_type == 'q_ans': - q_ans_A = Q_set_A.filter(answers__author=user)[:const.MAX_ALERTS_PER_EMAIL] + q_ans_A = Q_set_A.filter(answers__author=user)[:forum_settings.MAX_ALERTS_PER_EMAIL] q_ans_A.cutoff_time = cutoff_time - q_ans_B = Q_set_B.filter(answers__author=user)[:const.MAX_ALERTS_PER_EMAIL] + q_ans_B = Q_set_B.filter(answers__author=user)[:forum_settings.MAX_ALERTS_PER_EMAIL] q_ans_B.cutoff_time = cutoff_time elif feed.feed_type == 'q_all': if user.tag_filter_setting == 'ignored': ignored_tags = Tag.objects.filter(user_selections__reason='bad', \ user_selections__user=user) - q_all_A = Q_set_A.exclude( tags__in=ignored_tags )[:const.MAX_ALERTS_PER_EMAIL] - q_all_B = Q_set_B.exclude( tags__in=ignored_tags )[:const.MAX_ALERTS_PER_EMAIL] + q_all_A = Q_set_A.exclude( tags__in=ignored_tags )[:forum_settings.MAX_ALERTS_PER_EMAIL] + q_all_B = Q_set_B.exclude( tags__in=ignored_tags )[:forum_settings.MAX_ALERTS_PER_EMAIL] else: selected_tags = Tag.objects.filter(user_selections__reason='good', \ user_selections__user=user) @@ -232,7 +233,7 @@ class Command(NoArgsCommand): else: num_q += 1 if num_q > 0: - url_prefix = settings.APP_URL + url_prefix = forum_settings.APP_URL subject = _('email update message subject') print 'have %d updated questions for %s' % (num_q, user.username) text = ungettext('%(name)s, this is an update message header for %(num)d question', @@ -246,7 +247,7 @@ class Command(NoArgsCommand): act_list = [] if meta_data['skip']: continue - if items_added >= const.MAX_ALERTS_PER_EMAIL: + if items_added >= forum_settings.MAX_ALERTS_PER_EMAIL: items_unreported = num_q - items_added #may be inaccurate actually, but it's ok else: @@ -261,7 +262,7 @@ class Command(NoArgsCommand): % (url_prefix + q.get_absolute_url(), q.title, act_token) text += '</ul>' text += '<p></p>' - #if len(q_list.keys()) >= const.MAX_ALERTS_PER_EMAIL: + #if len(q_list.keys()) >= forum_settings.MAX_ALERTS_PER_EMAIL: # text += _('There may be more questions updated since ' # 'you have logged in last time as this list is ' # 'abridged for your convinience. Please visit ' diff --git a/forum/management/commands/subscribe_everyone.py b/forum/management/commands/subscribe_everyone.py index c79528f3..c79528f3 100755..100644 --- a/forum/management/commands/subscribe_everyone.py +++ b/forum/management/commands/subscribe_everyone.py diff --git a/forum/middleware/__init__.py b/forum/middleware/__init__.py index e69de29b..e69de29b 100755..100644 --- a/forum/middleware/__init__.py +++ b/forum/middleware/__init__.py diff --git a/forum/middleware/anon_user.py b/forum/middleware/anon_user.py index 866734da..51d35fa7 100755..100644 --- a/forum/middleware/anon_user.py +++ b/forum/middleware/anon_user.py @@ -2,8 +2,9 @@ from django.http import HttpResponseRedirect from forum.utils.forms import get_next_url from django.utils.translation import ugettext as _ from forum.user_messages import create_message, get_and_delete_messages -from django.conf import settings from django.core.urlresolvers import reverse +from forum.conf import settings as forum_settings +from forum import const import logging class AnonymousMessageManager(object): @@ -31,5 +32,5 @@ class ConnectToSessionMessagesMiddleware(object): #also set the first greeting one time per session only if 'greeting_set' not in request.session: request.session['greeting_set'] = True - msg = _('First time here? Check out the <a href="%s">FAQ</a>!') % reverse('faq') + msg = _(const.GREETING_FOR_ANONYMOUS_USER) % forum_settings.GREETING_URL request.user.message_set.create(message=msg) diff --git a/forum/middleware/cancel.py b/forum/middleware/cancel.py index 15a4371d..15a4371d 100755..100644 --- a/forum/middleware/cancel.py +++ b/forum/middleware/cancel.py diff --git a/forum/middleware/pagesize.py b/forum/middleware/pagesize.py index 486193dc..486193dc 100755..100644 --- a/forum/middleware/pagesize.py +++ b/forum/middleware/pagesize.py diff --git a/forum/models/__init__.py b/forum/models/__init__.py index d0d2d4a7..024379db 100755..100644 --- a/forum/models/__init__.py +++ b/forum/models/__init__.py @@ -57,6 +57,8 @@ def user_get_absolute_url(self): User.add_to_class('is_approved', models.BooleanField(default=False)) User.add_to_class('email_isvalid', models.BooleanField(default=False)) User.add_to_class('email_key', models.CharField(max_length=32, null=True)) + +#hardcoded initial reputaion of 1, no setting for this one User.add_to_class('reputation', models.PositiveIntegerField(default=1)) User.add_to_class('gravatar', models.CharField(max_length=32)) @@ -406,8 +408,8 @@ def record_user_full_updated(instance, **kwargs): def post_stored_anonymous_content(sender,user,session_key,signal,*args,**kwargs): aq_list = AnonymousQuestion.objects.filter(session_key = session_key) aa_list = AnonymousAnswer.objects.filter(session_key = session_key) - import settings - if settings.EMAIL_VALIDATION == 'on':#add user to the record + from forum.conf import settings as forum_settings + if forum_settings.EMAIL_VALIDATION == True:#add user to the record for aq in aq_list: aq.author = user aq.save() diff --git a/forum/models/answer.py b/forum/models/answer.py index 3de6cfc4..3de6cfc4 100755..100644 --- a/forum/models/answer.py +++ b/forum/models/answer.py diff --git a/forum/models/base.py b/forum/models/base.py index fcec47b4..fcec47b4 100755..100644 --- a/forum/models/base.py +++ b/forum/models/base.py diff --git a/forum/models/meta.py b/forum/models/meta.py index 114d2130..114d2130 100755..100644 --- a/forum/models/meta.py +++ b/forum/models/meta.py diff --git a/forum/models/question.py b/forum/models/question.py index 4d3154b0..1d387ab7 100755..100644 --- a/forum/models/question.py +++ b/forum/models/question.py @@ -96,16 +96,19 @@ class QuestionManager(models.Manager): params=['%' + search_query + '%'] ) + #have to import this at run time, otherwise there + #a circular import dependency... + from forum.conf import settings as forum_settings if scope_selector: if scope_selector == 'unanswered': - if const.UNANSWERED_MEANING == 'NO_ANSWERS': + if forum_settings.UNANSWERED_QUESTION_MEANING == 'NO_ANSWERS': qs = qs.filter(answer_count=0)#todo: expand for different meanings of this - elif const.UNANSWERED_MEANING == 'NO_ACCEPTED_ANSWERS': + elif forum_settings.UNANSWERED_QUESTION_MEANING == 'NO_ACCEPTED_ANSWERS': qs = qs.filter(answer_accepted=False) - elif const.UNANSWERED_MEANING == 'NO_UPVOTED_ANSWERS': + elif forum_settings.UNANSWERED_QUESTION_MEANING == 'NO_UPVOTED_ANSWERS': raise NotImplementedError() else: - raise Exception('UNANSWERED_MEANING setting is wrong') + raise Exception('UNANSWERED_QUESTION_MEANING setting is wrong') elif scope_selector == 'favorite': qs = qs.filter(favorited_by = request_user) @@ -513,7 +516,7 @@ class Question(Content, DeletableContent): out.append(_('%(people)s commented answers') % {'people':people}) else: out.append(_('%(people)s commented an answer') % {'people':people}) - url = settings.APP_URL + self.get_absolute_url() + url = forum_settings.APP_URL + self.get_absolute_url() retval = '<a href="%s">%s</a>:<br>\n' % (url,self.title) out = map(lambda x: '<li>' + x + '</li>',out) retval += '<ul>' + '\n'.join(out) + '</ul><br>\n' diff --git a/forum/models/repute.py b/forum/models/repute.py index f71be4db..f71be4db 100755..100644 --- a/forum/models/repute.py +++ b/forum/models/repute.py diff --git a/forum/models/tag.py b/forum/models/tag.py index e13baf9b..e13baf9b 100755..100644 --- a/forum/models/tag.py +++ b/forum/models/tag.py diff --git a/forum/models/user.py b/forum/models/user.py index 6d871bf4..6d871bf4 100755..100644 --- a/forum/models/user.py +++ b/forum/models/user.py diff --git a/forum/modules.py b/forum/modules.py index 6c9a9dba..6c9a9dba 100755..100644 --- a/forum/modules.py +++ b/forum/modules.py diff --git a/forum/search/state_manager.py b/forum/search/state_manager.py index 6a38a7a2..86cc5662 100644 --- a/forum/search/state_manager.py +++ b/forum/search/state_manager.py @@ -2,6 +2,7 @@ #that lives in the session and takes care of the state #persistece during the search session from forum import const +from forum.conf import settings as forum_settings import logging ACTIVE_COMMANDS = ( @@ -24,7 +25,7 @@ class SearchState(object): self.tags = None self.author = None self.sort = const.DEFAULT_POST_SORT_METHOD - self.page_size = const.DEFAULT_QUESTIONS_PAGE_SIZE + self.page_size = int(forum_settings.DEFAULT_QUESTIONS_PAGE_SIZE) self.page = 1 self.logged_in = False logging.debug('new search state initialized') diff --git a/forum/settings.py b/forum/settings.py index 04a7c399..6e31634f 100755..100644 --- a/forum/settings.py +++ b/forum/settings.py @@ -1,5 +1,6 @@ +#todo: this file is currently not in use import os - +from livesettings import ConfigurationGroup, IntegerValue, config_register INSTALLED_APPS = ['forum'] @@ -31,7 +32,7 @@ TEMPLATE_DIRS = [ os.path.join(os.path.dirname(__file__),'skins').replace('\\','/'), ] -def setup_settings(settings): +def setup_django_settings(settings): if (hasattr(settings, 'DEBUG') and getattr(settings, 'DEBUG')): try: @@ -48,4 +49,19 @@ def setup_settings(settings): settings.TEMPLATE_CONTEXT_PROCESSORS = set(settings.TEMPLATE_CONTEXT_PROCESSORS) | set(TEMPLATE_CONTEXT_PROCESSORS) settings.TEMPLATE_DIRS = set(settings.TEMPLATE_DIRS) | set(TEMPLATE_DIRS) -
\ No newline at end of file + +class AskbotConfigGroup(ConfigurationGroup): + def __init__(self, key, name, *arg, **kwarg): + super(AskbotConfigGroup, self).__init__(key, name, *arg,**kwarg) + self.item_count = 0 + def new_int_setting(self, key, value, description): + self.item_count += 1 + setting = config_register(IntegerValue( + self, + key, + default=value, + description=description, + ordering=self.item_count + ) + ) + return setting diff --git a/forum/sitemap.py b/forum/sitemap.py index c0c60b5e..c0c60b5e 100755..100644 --- a/forum/sitemap.py +++ b/forum/sitemap.py diff --git a/forum/skins/__init__.py b/forum/skins/__init__.py index 10b6a340..8e5265e8 100755..100644 --- a/forum/skins/__init__.py +++ b/forum/skins/__init__.py @@ -1,13 +1,12 @@ -from django.conf import settings from django.template import loader from django.template.loaders import filesystem from django.http import HttpResponse import os.path +import os import logging #module for skinning askbot -#at this point skin can be changed only in settings file -#via ASKBOT_DEFAULT_SKIN variable +#via ASKBOT_DEFAULT_SKIN configureation variable (not django setting) #note - Django template loaders use method django.utils._os.safe_join #to work on unicode file paths @@ -15,18 +14,32 @@ import logging def load_template_source(name, dirs=None): try: - tname = os.path.join(settings.ASKBOT_DEFAULT_SKIN,'templates',name) + #todo: move this to top after splitting out get_skin_dirs() + from forum.conf import settings as forum_settings + tname = os.path.join(forum_settings.ASKBOT_DEFAULT_SKIN,'templates',name) return filesystem.load_template_source(tname,dirs) except: tname = os.path.join('default','templates',name) return filesystem.load_template_source(tname,dirs) load_template_source.is_usable = True +#todo: move this to skins/utils.py +#then move import forum.conf.settings to top +def get_skin_dirs(): + #todo: handle case of multiple skin directories + d = os.path.dirname + n = os.path.normpath + j = os.path.join + f = os.path.isfile + skin_dirs = [] + skin_dirs.append( n(j(d(d(__file__)), 'skins')) ) + return skin_dirs + def find_media_source(url): """returns url prefixed with the skin name of the first skin that contains the file directories are searched in this order: - settings.ASKBOT_DEFAULT_SKIN, then 'default', then 'commmon' + forum_settings.ASKBOT_DEFAULT_SKIN, then 'default', then 'commmon' if file is not found - returns None and logs an error message """ @@ -35,11 +48,14 @@ def find_media_source(url): n = os.path.normpath j = os.path.join f = os.path.isfile - skins = n(j(d(d(__file__)),'skins')) + #todo: handles case of multiple skin directories + skins = get_skin_dirs()[0] try: - media = os.path.join(skins, settings.ASKBOT_DEFAULT_SKIN, url) + #todo: move this to top after splitting out get_skin_dirs() + from forum.conf import settings as forum_settings + media = os.path.join(skins, forum_settings.ASKBOT_DEFAULT_SKIN, url) assert(f(media)) - use_skin = settings.ASKBOT_DEFAULT_SKIN + use_skin = forum_settings.ASKBOT_DEFAULT_SKIN except: try: media = j(skins, 'default', url) @@ -55,3 +71,20 @@ def find_media_source(url): use_skin = '' return None return use_skin + '/' + url + +def get_skin_choices(): + #todo: expand this to handle custom skin directories + dirs = get_skin_dirs() + default_dir = dirs[0] + items = os.listdir(default_dir) + skin_list = ['default'] + for i in items: + item_path = os.path.join(default_dir,i) + if not os.path.isdir(item_path): + continue + if i == 'common': + continue + if i not in skin_list: + skin_list.append(i) + + return [(i,i) for i in skin_list] diff --git a/forum/skins/default/templates/about.html b/forum/skins/default/templates/about.html index 686141b3..d091725b 100644 --- a/forum/skins/default/templates/about.html +++ b/forum/skins/default/templates/about.html @@ -12,25 +12,7 @@ </div> <div class="content"> - <p class="strong">Please customize file templates/about.html</p> - - <p>Here you can <strong>ask</strong> and <strong>answer</strong> questions, <strong>comment</strong> - and <strong>vote</strong> for the questions of others and their answers. Both questions and answers - <strong>can be revised</strong> and improved. Questions can be <strong>tagged</strong> with - the relevant keywords to simplify future access and organize the accumulated material. - </p> - - <p>This <span class="orange">Q&A</span> site is moderated by its members, hopefully - including yourself! - Moderation rights are gradually assigned to the site users based on the accumulated <strong>"reputation"</strong> - points. These points are added to the users account when others vote for his/her questions or answers. - These points (very) roughly reflect the level of trust of the community. - </p> - <p>No points are necessary to ask or answer the questions - so please - - <strong><a href="{% url user_signin %}">join us!</a></strong> - </p> - <p> - If you would like to find out more about this site - please see <strong><a href="{% url faq %}">frequently asked questions</a></strong>. - </p> + {{settings.FORUM_ABOUT|safe}} </div> {% endblock %} <!-- end template about.html --> diff --git a/forum/skins/default/templates/ask.html b/forum/skins/default/templates/ask.html index 4278f4cb..82828fe9 100644 --- a/forum/skins/default/templates/ask.html +++ b/forum/skins/default/templates/ask.html @@ -69,7 +69,7 @@ <p>{% trans "login to post question info" %}</p> </div> {% else %} - {% ifequal settings.EMAIL_VALIDATION 'on' %} + {% if settings.EMAIL_VALIDATION %} {% if not request.user.email_isvalid %} <div class="message"> {% blocktrans with request.user.email as email %}must have valid {{email}} to post, @@ -77,7 +77,7 @@ {% endblocktrans %} </div> {% endif %} - {% endifequal %} + {% endif %} {% endif %} <div class="form-item"> <label for="id_title" ><strong>{{ form.title.label_tag }}:</strong></label> <span class="form-error"></span><br/> diff --git a/forum/skins/default/templates/ask_form.html b/forum/skins/default/templates/ask_form.html index 25e9fe6c..1bb3866b 100644 --- a/forum/skins/default/templates/ask_form.html +++ b/forum/skins/default/templates/ask_form.html @@ -10,13 +10,13 @@ {% if not request.user.is_authenticated %} <p>{% trans "login to post question info" %}</p> {% else %} - {% ifequal settings.EMAIL_VALIDATION 'on' %} + {% if settings.EMAIL_VALIDATION %} {% if not request.user.email_isvalid %} {% blocktrans with request.user.email as email %}must have valid {{email}} to post, see {{email_validation_faq_url}} {% endblocktrans %} {% endif %} - {% endifequal %} + {% endif %} {% endif %} <input id="id_title" class="questionTitleInput" name="title" value="{% if form.initial.title %}{{form.initial.title}}{% endif %}"/> diff --git a/forum/skins/default/templates/base.html b/forum/skins/default/templates/base.html index a4e4ceed..94d3392d 100644 --- a/forum/skins/default/templates/base.html +++ b/forum/skins/default/templates/base.html @@ -9,8 +9,12 @@ <title>{% block title %}{% endblock %} - {{ settings.APP_TITLE }}</title> {% spaceless %} {% block meta %}{% endblock %} + {% block meta_description %} + <meta name="description" content="{{settings.APP_DESCRIPTION}}" /> + {% endblock %} {% endspaceless %} <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <meta name="keywords" content="{%block keywords%}{%endblock%},{{settings.APP_KEYWORDS}}" /> {% if settings.GOOGLE_SITEMAP_CODE %} <meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" /> {% endif %} diff --git a/forum/skins/default/templates/base_content.html b/forum/skins/default/templates/base_content.html index 284007d8..7b49d9ba 100644 --- a/forum/skins/default/templates/base_content.html +++ b/forum/skins/default/templates/base_content.html @@ -6,6 +6,10 @@ <head> <title>{% block title %}{% endblock %} - {{ settings.APP_TITLE }}</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> + <meta name="keywords" content="{%block keywords%}{%endblock%},{{settings.APP_KEYWORDS}}" /> + {% block meta_description %} + <meta name="description" content="{{settings.APP_DESCRIPTION}}" /> + {% endblock %} {% if settings.GOOGLE_SITEMAP_CODE %} <meta name="google-site-verification" content="{{ settings.GOOGLE_SITEMAP_CODE }}" /> {% endif %} diff --git a/forum/skins/default/templates/faq.html b/forum/skins/default/templates/faq.html index cc790ccc..c944240a 100644 --- a/forum/skins/default/templates/faq.html +++ b/forum/skins/default/templates/faq.html @@ -98,13 +98,13 @@ </table> </div> {% comment %} - {% ifequal settings.EMAIL_VALIDATION 'on' %} + {% if settings.EMAIL_VALIDATION %} <div> <a id='validate'></a><h3 class="subtitle">{% trans "how to validate email title" %}</h3> <!--special case here message must contain paragraphs--> {% blocktrans %}how to validate email info with {{send_email_key_url}} {{gravatar_faq_url}}{% endblocktrans %} </div> - {% endifequal %} + {% endif %} {% endcomment %} <div> <a id='gravatar'></a><h3 class="subtitle">{% trans "what is gravatar" %}</h3> diff --git a/forum/skins/default/templates/fbconnect/xd_receiver.html b/forum/skins/default/templates/fbconnect/xd_receiver.html index a03c61bc..60c02a22 100755 --- a/forum/skins/default/templates/fbconnect/xd_receiver.html +++ b/forum/skins/default/templates/fbconnect/xd_receiver.html @@ -2,7 +2,7 @@ <html xmlns="http://www.w3.org/1999/xhtml" >
{% load i18n %}
<head>
- <title>{% blocktrans %}Connect to {{APP_SHORT_NAME}} with Facebook!{% endblocktrans %}
+ <title>{% blocktrans %}Connect to {{settings.APP_SHORT_NAME}} with Facebook!{% endblocktrans %}
</head>
<body>
<script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/XdCommReceiver.js" type="text/javascript"></script>
diff --git a/forum/skins/default/templates/footer.html b/forum/skins/default/templates/footer.html index 9b7c5d98..a28a1980 100644 --- a/forum/skins/default/templates/footer.html +++ b/forum/skins/default/templates/footer.html @@ -23,7 +23,7 @@ <p> <a href="http://askbot.org" target="_blank"> powered by ASKBOT - </a> + </a><br/>{{settings.APP_COPYRIGHT}} </p> </div> <div id="licenseLogo"> diff --git a/forum/skins/default/templates/header.html b/forum/skins/default/templates/header.html index 0a1a3296..f2eaa3ac 100644 --- a/forum/skins/default/templates/header.html +++ b/forum/skins/default/templates/header.html @@ -2,46 +2,37 @@ {% load extra_tags %} {% load smart_if %} {% load i18n %} - <div id="roof"> - <div id="navBar"> - <div id="top"> - {% if request.user.is_authenticated %} - <a href="{% url user_profile id=request.user.id,slug=request.user.username|slugify %}">{{ request.user.username }}</a> {% get_score_badge request.user %} - <a href="{% url logout %}">{% trans "logout" %}</a> - {% else %} - <a href="{% url user_signin %}">{% trans "login" %}</a> - {% endif %} - <a href="{% url about %}">{% trans "about" %}</a> - <a href="{% url faq %}">{% trans "faq" %}</a> - </div> - <table border="0" cellspacing="0" cellpadding="0"> - <tr> - <td id="logoContainer"> - <div id="logo"> - <a href="{% url questions %}?start_over=true"><img - src="{% media "/media/images/logo.gif" %}" title="{% trans "back to home page" %}" alt="{{settings.APP_TITLE}} logo"/></a> - </div> - </td> - <td id="navTabContainer" valign="bottom" align="left"> - <div class="nav"> - <a id="nav_questions" href="{% url questions %}" >{% trans "questions" %}</a> - <a id="nav_tags" href="{% url tags %}">{% trans "tags" %}</a> - <a id="nav_users" href="{% url users %}">{% trans "users" %}</a> - {% if settings.BOOKS_ON %} - <a id="nav_books" href="{% url books %}">{% trans "books" %}</a> - {% endif %} - <a id="nav_badges" href="{% url badges %}">{% trans "badges" %}</a> - <a id="nav_ask" href="{% url ask %}" class="special">{% trans "ask a question" %}</a> - {% comment %} - <a id="nav_unanswered" href="{% url unanswered %}">{% trans "unanswered questions" %}</a> - <div class="focus"> - <a id="nav_ask" href="{% url ask %}" class="special">{% trans "ask a question" %}</a> - </div> - {% endcomment %} - </div> - </td> - </tr> - </table> - </div> - </div> +<div id="roof"> + <div id="navBar"> + <div id="top"> + {% if request.user.is_authenticated %} + <a href="{% url user_profile id=request.user.id,slug=request.user.username|slugify %}">{{ request.user.username }}</a> {% get_score_badge request.user %} + <a href="{% url logout %}">{% trans "logout" %}</a> + {% else %} + <a href="{% url user_signin %}">{% trans "login" %}</a> + {% endif %} + <a href="{% url about %}">{% trans "about" %}</a> + <a href="{% url faq %}">{% trans "faq" %}</a> + </div> + <table border="0" cellspacing="0" cellpadding="0"> + <tr> + <td id="logoContainer"> + <div id="logo"> + <a href="{% url questions %}?start_over=true"><img + src="{% media "/media/images/logo.gif" %}" title="{% trans "back to home page" %}" alt="{{settings.APP_TITLE}} logo"/></a> + </div> + </td> + <td id="navTabContainer" valign="bottom" align="left"> + <div class="nav"> + <a id="nav_questions" href="{% url questions %}" >{% trans "questions" %}</a> + <a id="nav_tags" href="{% url tags %}">{% trans "tags" %}</a> + <a id="nav_users" href="{% url users %}">{% trans "users" %}</a> + <a id="nav_badges" href="{% url badges %}">{% trans "badges" %}</a> + <a id="nav_ask" href="{% url ask %}" class="special">{% trans "ask a question" %}</a> + </div> + </td> + </tr> + </table> + </div> +</div> <!-- end template header.html --> diff --git a/forum/skins/default/templates/privacy.html b/forum/skins/default/templates/privacy.html index e66086dd..fe074491 100644 --- a/forum/skins/default/templates/privacy.html +++ b/forum/skins/default/templates/privacy.html @@ -11,32 +11,7 @@ {% trans "Privacy policy" %} </div> <div id="main-body" style="width:100%"> - <p> - {% trans "general message about privacy" %} - </p> - - <h3 class="subtitle">{% trans "Site Visitors" %}</h3> - <p> - {% trans "what technical information is collected about visitors" %} - </p> - - <h3 class="subtitle">{% trans "Personal Information" %}</h3> - <p> - {% trans "details on personal information policies" %} - </p> - - <h3 class="subtitle">{% trans "Other Services" %}</h3> - <p> - {% trans "details on sharing data with third parties" %} - </p> - - <h3 class="subtitle">Cookies</h3> - <p> - {% trans "cookie policy details" %} - </p> - <h3 class="subtitle">{% trans "Policy Changes" %}</h3> - <p>{% trans "how privacy policies can be changed" %} - </p> + {{settings.FORUM_PRIVACY|safe}} </div> {% endblock %} <!-- end privacy.html --> diff --git a/forum/skins/default/templates/question.html b/forum/skins/default/templates/question.html index f0619ab8..c75bea84 100644 --- a/forum/skins/default/templates/question.html +++ b/forum/skins/default/templates/question.html @@ -7,9 +7,11 @@ {% load i18n %}
{% load cache %}
{% block title %}{% spaceless %}{{ question.get_question_title }}{% endspaceless %}{% endblock %}
-{% block forejs %}
+{% block meta_description %}
<meta name="description" content="{{question.summary}}" />
- <meta name="keywords" content="{{question.tagname_meta_generator}}" />
+{% endblock %}
+{% block keywords %}{{question.tagname_meta_generator}}{% endblock %}
+{% block forejs %}
<link rel="canonical" href="{{settings.APP_URL}}{{question.get_absolute_url}}" />
{% if not question.closed %}
<script type='text/javascript' src='{% media "/media/js/com.cnprog.editor.js" %}'></script>
diff --git a/forum/templatetags/__init__.py b/forum/templatetags/__init__.py index e69de29b..e69de29b 100755..100644 --- a/forum/templatetags/__init__.py +++ b/forum/templatetags/__init__.py diff --git a/forum/templatetags/extra_filters.py b/forum/templatetags/extra_filters.py index d600c23e..d600c23e 100755..100644 --- a/forum/templatetags/extra_filters.py +++ b/forum/templatetags/extra_filters.py diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 86f2e9df..39a1ded7 100755..100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -13,6 +13,7 @@ from forum.models import Question, Answer, QuestionRevision, AnswerRevision from django.utils.translation import ugettext as _ from django.utils.translation import ungettext from django.conf import settings +from forum.conf import settings as forum_settings from django.template.defaulttags import url as default_url from django.template.defaultfilters import slugify from django.core.urlresolvers import reverse @@ -146,7 +147,7 @@ def post_contributor_info(post,contributor_type='original_author'): return { 'post':post, 'post_type':post_type, - 'wiki_on':settings.WIKI_ON, + 'wiki_on':forum_settings.WIKI_ON, 'contributor_type':contributor_type } @@ -293,7 +294,10 @@ def media(url): url = skins.find_media_source(url) if url: url = '///' + settings.FORUM_SCRIPT_ALIAS + '/m/' + url - return posixpath.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION + return posixpath.normpath(url) + '?v=%d' \ + % forum_settings.MEDIA_RESOURCE_REVISION + else: + return '' #todo: raise exception here? class ItemSeparatorNode(template.Node): def __init__(self,separator): @@ -373,7 +377,7 @@ class BlockMediaUrlNode(template.Node): url = skins.find_media_source(url) url = prefix + url - out = posixpath.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION + out = posixpath.normpath(url) + '?v=%d' % forum_settings.MEDIA_RESOURCE_REVISION return out.replace(' ','') @register.tag(name='blockmedia') @@ -395,7 +399,7 @@ class FullUrlNode(template.Node): self.default_node = default_node def render(self, context): - domain = settings.APP_URL + domain = forum_settings.APP_URL #protocol = getattr(settings, "PROTOCOL", "http") path = self.default_node.render(context) return "%s%s" % (domain, path) @@ -407,7 +411,7 @@ def fullurl(parser, token): @register.simple_tag def fullmedia(url): - domain = settings.APP_URL + domain = forum_settings.APP_URL #protocol = getattr(settings, "PROTOCOL", "http") path = media(url) return "%s%s" % (domain, path) @@ -423,39 +427,39 @@ def question_counter_widget(question): #background and foreground colors for each item (views_fg, views_bg) = colors.get_counter_colors( view_count, - max = settings.VIEW_COUNTER_EXPECTED_MAXIMUM, - zero_bg = settings.COLORS_VIEW_COUNTER_EMPTY_BG, - zero_fg = settings.COLORS_VIEW_COUNTER_EMPTY_FG, - min_bg = settings.COLORS_VIEW_COUNTER_MIN_BG, - min_fg = settings.COLORS_VIEW_COUNTER_MIN_FG, - max_bg = settings.COLORS_VIEW_COUNTER_MAX_BG, - max_fg = settings.COLORS_VIEW_COUNTER_MAX_FG, + max = forum_settings.VIEW_COUNTER_EXPECTED_MAXIMUM, + zero_bg = forum_settings.COLORS_VIEW_COUNTER_EMPTY_BG, + zero_fg = forum_settings.COLORS_VIEW_COUNTER_EMPTY_FG, + min_bg = forum_settings.COLORS_VIEW_COUNTER_MIN_BG, + min_fg = forum_settings.COLORS_VIEW_COUNTER_MIN_FG, + max_bg = forum_settings.COLORS_VIEW_COUNTER_MAX_BG, + max_fg = forum_settings.COLORS_VIEW_COUNTER_MAX_FG, ) (answers_fg, answers_bg) = colors.get_counter_colors( answer_count, - max = settings.ANSWER_COUNTER_EXPECTED_MAXIMUM, - zero_bg = settings.COLORS_ANSWER_COUNTER_EMPTY_BG, - zero_fg = settings.COLORS_ANSWER_COUNTER_EMPTY_FG, - min_bg = settings.COLORS_ANSWER_COUNTER_MIN_BG, - min_fg = settings.COLORS_ANSWER_COUNTER_MIN_FG, - max_bg = settings.COLORS_ANSWER_COUNTER_MAX_BG, - max_fg = settings.COLORS_ANSWER_COUNTER_MAX_FG, + max = forum_settings.ANSWER_COUNTER_EXPECTED_MAXIMUM, + zero_bg = forum_settings.COLORS_ANSWER_COUNTER_EMPTY_BG, + zero_fg = forum_settings.COLORS_ANSWER_COUNTER_EMPTY_FG, + min_bg = forum_settings.COLORS_ANSWER_COUNTER_MIN_BG, + min_fg = forum_settings.COLORS_ANSWER_COUNTER_MIN_FG, + max_bg = forum_settings.COLORS_ANSWER_COUNTER_MAX_BG, + max_fg = forum_settings.COLORS_ANSWER_COUNTER_MAX_FG, ) if answer_accepted: #todo: maybe recalculate the foreground color too - answers_bg = settings.COLORS_ANSWER_COUNTER_ACCEPTED_BG - answers_fg = settings.COLORS_ANSWER_COUNTER_ACCEPTED_FG + answers_bg = forum_settings.COLORS_ANSWER_COUNTER_ACCEPTED_BG + answers_fg = forum_settings.COLORS_ANSWER_COUNTER_ACCEPTED_FG (votes_fg, votes_bg) = colors.get_counter_colors( vote_count, - max = settings.VOTE_COUNTER_EXPECTED_MAXIMUM, - zero_bg = settings.COLORS_VOTE_COUNTER_EMPTY_BG, - zero_fg = settings.COLORS_VOTE_COUNTER_EMPTY_FG, - min_bg = settings.COLORS_VOTE_COUNTER_MIN_BG, - min_fg = settings.COLORS_VOTE_COUNTER_MIN_FG, - max_bg = settings.COLORS_VOTE_COUNTER_MAX_BG, - max_fg = settings.COLORS_VOTE_COUNTER_MAX_FG, + max = forum_settings.VOTE_COUNTER_EXPECTED_MAXIMUM, + zero_bg = forum_settings.COLORS_VOTE_COUNTER_EMPTY_BG, + zero_fg = forum_settings.COLORS_VOTE_COUNTER_EMPTY_FG, + min_bg = forum_settings.COLORS_VOTE_COUNTER_MIN_BG, + min_fg = forum_settings.COLORS_VOTE_COUNTER_MIN_FG, + max_bg = forum_settings.COLORS_VOTE_COUNTER_MAX_BG, + max_fg = forum_settings.COLORS_VOTE_COUNTER_MAX_FG, ) #returns a dictionary with keys like 'votes_bg', etc diff --git a/forum/templatetags/smart_if.py b/forum/templatetags/smart_if.py index ca3b43fe..ca3b43fe 100755..100644 --- a/forum/templatetags/smart_if.py +++ b/forum/templatetags/smart_if.py diff --git a/forum/urls.py b/forum/urls.py index 41ffbcf7..41ffbcf7 100755..100644 --- a/forum/urls.py +++ b/forum/urls.py diff --git a/forum/user_messages/__init__.py b/forum/user_messages/__init__.py index 0136c888..0136c888 100755..100644 --- a/forum/user_messages/__init__.py +++ b/forum/user_messages/__init__.py diff --git a/forum/user_messages/context_processors.py b/forum/user_messages/context_processors.py index 5f7b857c..5f7b857c 100755..100644 --- a/forum/user_messages/context_processors.py +++ b/forum/user_messages/context_processors.py diff --git a/forum/utils/__init__.py b/forum/utils/__init__.py index e69de29b..e69de29b 100755..100644 --- a/forum/utils/__init__.py +++ b/forum/utils/__init__.py diff --git a/forum/utils/cache.py b/forum/utils/cache.py index 6341392e..6341392e 100755..100644 --- a/forum/utils/cache.py +++ b/forum/utils/cache.py diff --git a/forum/utils/decorators.py b/forum/utils/decorators.py index 440e8312..440e8312 100755..100644 --- a/forum/utils/decorators.py +++ b/forum/utils/decorators.py diff --git a/forum/utils/diff.py b/forum/utils/diff.py index d741d788..d741d788 100755..100644 --- a/forum/utils/diff.py +++ b/forum/utils/diff.py diff --git a/forum/utils/email.py b/forum/utils/email.py index dc712572..dc712572 100755..100644 --- a/forum/utils/email.py +++ b/forum/utils/email.py diff --git a/forum/utils/forms.py b/forum/utils/forms.py index d8d04d05..946c1fd9 100755..100644 --- a/forum/utils/forms.py +++ b/forum/utils/forms.py @@ -3,6 +3,7 @@ import re from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe from django.conf import settings +from forum.conf import settings as forum_settings from django.http import str_to_unicode from django.contrib.auth.models import User import logging @@ -121,7 +122,7 @@ class UserEmailField(forms.EmailField): email = super(UserEmailField,self).clean(email.strip()) if self.skip_clean: return email - if settings.EMAIL_UNIQUE == True: + if forum_settings.EMAIL_UNIQUE == True: try: user = User.objects.get(email = email) logging.debug('email taken') diff --git a/forum/utils/html.py b/forum/utils/html.py index 25a74a4a..25a74a4a 100755..100644 --- a/forum/utils/html.py +++ b/forum/utils/html.py diff --git a/forum/utils/lists.py b/forum/utils/lists.py index f69c8f20..f69c8f20 100755..100644 --- a/forum/utils/lists.py +++ b/forum/utils/lists.py diff --git a/forum/utils/odict.py b/forum/utils/odict.py index 2c8391d7..2c8391d7 100755..100644 --- a/forum/utils/odict.py +++ b/forum/utils/odict.py diff --git a/forum/utils/time.py b/forum/utils/time.py index 39e01d0f..39e01d0f 100755..100644 --- a/forum/utils/time.py +++ b/forum/utils/time.py diff --git a/forum/views/__init__.py b/forum/views/__init__.py index 291fee2a..291fee2a 100755..100644 --- a/forum/views/__init__.py +++ b/forum/views/__init__.py diff --git a/forum/views/commands.py b/forum/views/commands.py index 39b34d13..4746f71d 100755..100644 --- a/forum/views/commands.py +++ b/forum/views/commands.py @@ -1,5 +1,7 @@ import datetime +#todo: maybe eliminate usage of django.settings from django.conf import settings +from forum.conf import settings as forum_settings from django.utils import simplejson from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response @@ -138,7 +140,8 @@ def vote(request, id):#todo: pretty incomprehensible view used by various ajax c vote = post.votes.filter(user=request.user)[0] # get latest vote by the current user # unvote should be less than certain time - if (datetime.datetime.now().day - vote.voted_at.day) >= auth.VOTE_RULES['scope_deny_unvote_days']: + if (datetime.datetime.now().day - vote.voted_at.day) \ + >= forum_settings.MAX_DAYS_TO_CANCEL_VOTE: response_data['status'] = 2 else: voted = vote.vote @@ -152,7 +155,8 @@ def vote(request, id):#todo: pretty incomprehensible view used by various ajax c response_data['status'] = 1 response_data['count'] = post.score - elif Vote.objects.get_votes_count_today_from_user(request.user) >= auth.VOTE_RULES['scope_votes_per_user_per_day']: + elif Vote.objects.get_votes_count_today_from_user(request.user)\ + >= forum_settings.MAX_VOTES_PER_USER_PER_DAY: response_data['allowed'] = -3 else: vote = Vote(user=request.user, content_object=post, vote=vote_score, voted_at=datetime.datetime.now()) @@ -163,8 +167,10 @@ def vote(request, id):#todo: pretty incomprehensible view used by various ajax c # downvote auth.onDownVoted(vote, post, request.user) - votes_left = auth.VOTE_RULES['scope_votes_per_user_per_day'] - Vote.objects.get_votes_count_today_from_user(request.user) - if votes_left <= auth.VOTE_RULES['scope_warn_votes_left']: + votes_left = forum_settings.MAX_VOTES_PER_USER_PER_DAY \ + - Vote.objects.get_votes_count_today_from_user(request.user) + if votes_left <= \ + forum_settings.VOTES_LEFT_WARNING_THRESHOLD: response_data['message'] = u'%s votes left' % votes_left response_data['count'] = post.score elif vote_type in ['7', '8']: @@ -174,7 +180,7 @@ def vote(request, id):#todo: pretty incomprehensible view used by various ajax c post_id = request.POST.get('postId') post = get_object_or_404(Answer, id=post_id) - if FlaggedItem.objects.get_flagged_items_count_today(request.user) >= auth.VOTE_RULES['scope_flags_per_user_per_day']: + if FlaggedItem.objects.get_flagged_items_count_today(request.user) >= forum_settings.MAX_FLAGS_PER_USER_PER_DAY: response_data['allowed'] = -3 elif not auth.can_flag_offensive(request.user): response_data['allowed'] = -2 @@ -204,7 +210,9 @@ def vote(request, id):#todo: pretty incomprehensible view used by various ajax c if user.is_authenticated(): if user not in question.followed_by.all(): question.followed_by.add(user) - if settings.EMAIL_VALIDATION == 'on' and user.email_isvalid == False: + if forum_settings.EMAIL_VALIDATION == True \ + and user.email_isvalid == False: + response_data['message'] = \ _('subscription saved, %(email)s needs validation, see %(details_url)s') \ % {'email':user.email,'details_url':reverse('faq') + '#validate'} diff --git a/forum/views/meta.py b/forum/views/meta.py index af5fe6df..af5fe6df 100755..100644 --- a/forum/views/meta.py +++ b/forum/views/meta.py diff --git a/forum/views/readers.py b/forum/views/readers.py index cd3832d3..f75f62a4 100644 --- a/forum/views/readers.py +++ b/forum/views/readers.py @@ -21,14 +21,13 @@ from markdown2 import Markdown from forum.utils.diff import textDiff as htmldiff from forum.forms import * from forum.models import * -from forum.auth import * from forum import const from forum import auth from forum.utils.forms import get_next_url from forum.search.state_manager import SearchState # used in index page -#refactor - move these numbers somewhere? +#todo: - take these out of const or settings INDEX_PAGE_SIZE = 30 INDEX_AWARD_SIZE = 15 INDEX_TAGS_SIZE = 25 diff --git a/forum/views/users.py b/forum/views/users.py index 76e41008..8a7ecaa6 100755..100644 --- a/forum/views/users.py +++ b/forum/views/users.py @@ -19,6 +19,7 @@ from django.contrib.contenttypes.models import ContentType from forum.models import user_updated from forum.const import USERS_PAGE_SIZE from django.conf import settings +from forum.conf import settings as forum_settings question_type = ContentType.objects.get_for_model(Question) answer_type = ContentType.objects.get_for_model(Answer) @@ -108,7 +109,7 @@ def set_new_email(user, new_email, nomessage=False): user.email = new_email user.email_isvalid = False user.save() - #if settings.EMAIL_VALIDATION == 'on': + #if forum_settings.EMAIL_VALIDATION == True: # send_new_email_key(user,nomessage=nomessage) @login_required @@ -123,7 +124,7 @@ def edit_user(request, id): set_new_email(user, new_email) - if settings.EDITABLE_SCREEN_NAME: + if forum_settings.EDITABLE_SCREEN_NAME: user.username = sanitize_html(form.cleaned_data['username']) user.real_name = sanitize_html(form.cleaned_data['realname']) @@ -218,7 +219,7 @@ def user_stats(request, user_id, user_view): up_votes = Vote.objects.get_up_vote_count_from_user(user) down_votes = Vote.objects.get_down_vote_count_from_user(user) votes_today = Vote.objects.get_votes_count_today_from_user(user) - votes_total = auth.VOTE_RULES['scope_votes_per_user_per_day'] + votes_total = forum_settings.MAX_VOTES_PER_USER_PER_DAY question_id_set = set(map(lambda v: v['id'], list(questions))) \ | set(map(lambda v: v['id'], list(answered_questions))) diff --git a/forum/views/writers.py b/forum/views/writers.py index 86831ba3..eddf2a87 100755..100644 --- a/forum/views/writers.py +++ b/forum/views/writers.py @@ -15,7 +15,6 @@ from django.core.exceptions import PermissionDenied from forum.forms import * from forum.models import * -from forum.auth import * from forum.const import * from forum import auth from forum.utils.forms import get_next_url diff --git a/forum_modules/__init__.py b/forum_modules/__init__.py index e69de29b..e69de29b 100755..100644 --- a/forum_modules/__init__.py +++ b/forum_modules/__init__.py diff --git a/forum_modules/authentication/auth.py b/forum_modules/authentication/auth.py index 96025dc1..96025dc1 100755..100644 --- a/forum_modules/authentication/auth.py +++ b/forum_modules/authentication/auth.py diff --git a/forum_modules/books/__init__.py b/forum_modules/books/__init__.py index c51a2bfb..c51a2bfb 100755..100644 --- a/forum_modules/books/__init__.py +++ b/forum_modules/books/__init__.py diff --git a/forum_modules/books/models.py b/forum_modules/books/models.py index a78c0e76..a78c0e76 100755..100644 --- a/forum_modules/books/models.py +++ b/forum_modules/books/models.py diff --git a/forum_modules/books/urls.py b/forum_modules/books/urls.py index bc0811e7..bc0811e7 100755..100644 --- a/forum_modules/books/urls.py +++ b/forum_modules/books/urls.py diff --git a/forum_modules/books/views.py b/forum_modules/books/views.py index d4907e5f..d4907e5f 100755..100644 --- a/forum_modules/books/views.py +++ b/forum_modules/books/views.py diff --git a/forum_modules/robotstxt/__init__.py b/forum_modules/robotstxt/__init__.py index e69de29b..e69de29b 100755..100644 --- a/forum_modules/robotstxt/__init__.py +++ b/forum_modules/robotstxt/__init__.py diff --git a/forum_modules/robotstxt/urls.py b/forum_modules/robotstxt/urls.py index 79a6d84c..79a6d84c 100755..100644 --- a/forum_modules/robotstxt/urls.py +++ b/forum_modules/robotstxt/urls.py diff --git a/keyedcache/__init__.py b/keyedcache/__init__.py new file mode 100644 index 00000000..d7dfe9ec --- /dev/null +++ b/keyedcache/__init__.py @@ -0,0 +1,329 @@ +"""A full cache system written on top of Django's rudimentary one.""" + +from django.conf import settings +from django.core.cache import cache +from django.utils.encoding import smart_str +from django.utils.hashcompat import md5_constructor +from keyedcache.utils import is_string_like, is_list_or_tuple +import cPickle as pickle +import logging +import types + +log = logging.getLogger('keyedcache') + +CACHED_KEYS = {} +CACHE_CALLS = 0 +CACHE_HITS = 0 +KEY_DELIM = "::" +REQUEST_CACHE = {'enabled' : False} +try: + CACHE_PREFIX = settings.CACHE_PREFIX +except AttributeError: + CACHE_PREFIX = str(settings.SITE_ID) + log.warn("No CACHE_PREFIX found in settings, using SITE_ID. Please update your settings to add a CACHE_PREFIX") + +try: + CACHE_TIMEOUT = settings.CACHE_TIMEOUT +except AttributeError: + CACHE_TIMEOUT = 0 + log.warn("No CACHE_TIMEOUT found in settings, so we used 0, disabling the cache system. Please update your settings to add a CACHE_TIMEOUT and avoid this warning.") + +_CACHE_ENABLED = CACHE_TIMEOUT > 0 + +class CacheWrapper(object): + def __init__(self, val, inprocess=False): + self.val = val + self.inprocess = inprocess + + def __str__(self): + return str(self.val) + + def __repr__(self): + return repr(self.val) + + def wrap(cls, obj): + if isinstance(obj, cls): + return obj + else: + return cls(obj) + + wrap = classmethod(wrap) + +class MethodNotFinishedError(Exception): + def __init__(self, f): + self.func = f + + +class NotCachedError(Exception): + def __init__(self, k): + self.key = k + +class CacheNotRespondingError(Exception): + pass + +def cache_delete(*keys, **kwargs): + removed = [] + if cache_enabled(): + global CACHED_KEYS + log.debug('cache_delete') + children = kwargs.pop('children',False) + + if (keys or kwargs): + key = cache_key(*keys, **kwargs) + + if CACHED_KEYS.has_key(key): + del CACHED_KEYS[key] + removed.append(key) + + cache.delete(key) + + if children: + key = key + KEY_DELIM + children = [x for x in CACHED_KEYS.keys() if x.startswith(key)] + for k in children: + del CACHED_KEYS[k] + cache.delete(k) + removed.append(k) + else: + key = "All Keys" + deleteneeded = _cache_flush_all() + + removed = CACHED_KEYS.keys() + + if deleteneeded: + for k in CACHED_KEYS: + cache.delete(k) + + CACHED_KEYS = {} + + if removed: + log.debug("Cache delete: %s", removed) + else: + log.debug("No cached objects to delete for %s", key) + + return removed + + +def cache_delete_function(func): + return cache_delete(['func', func.__name__, func.__module__], children=True) + +def cache_enabled(): + global _CACHE_ENABLED + return _CACHE_ENABLED + +def cache_enable(state=True): + global _CACHE_ENABLED + _CACHE_ENABLED=state + +def _cache_flush_all(): + if is_memcached_backend(): + cache._cache.flush_all() + return False + return True + +def cache_function(length=CACHE_TIMEOUT): + """ + A variant of the snippet posted by Jeff Wheeler at + http://www.djangosnippets.org/snippets/109/ + + Caches a function, using the function and its arguments as the key, and the return + value as the value saved. It passes all arguments on to the function, as + it should. + + The decorator itself takes a length argument, which is the number of + seconds the cache will keep the result around. + + It will put a temp value in the cache while the function is + processing. This should not matter in most cases, but if the app is using + threads, you won't be able to get the previous value, and will need to + wait until the function finishes. If this is not desired behavior, you can + remove the first two lines after the ``else``. + """ + def decorator(func): + def inner_func(*args, **kwargs): + if not cache_enabled(): + value = func(*args, **kwargs) + + else: + try: + value = cache_get('func', func.__name__, func.__module__, args, kwargs) + + except NotCachedError, e: + # This will set a temporary value while ``func`` is being + # processed. When using threads, this is vital, as otherwise + # the function can be called several times before it finishes + # and is put into the cache. + funcwrapper = CacheWrapper(".".join([func.__module__, func.__name__]), inprocess=True) + cache_set(e.key, value=funcwrapper, length=length, skiplog=True) + value = func(*args, **kwargs) + cache_set(e.key, value=value, length=length) + + except MethodNotFinishedError, e: + value = func(*args, **kwargs) + + return value + return inner_func + return decorator + + +def cache_get(*keys, **kwargs): + if kwargs.has_key('default'): + default_value = kwargs.pop('default') + use_default = True + else: + use_default = False + + key = cache_key(keys, **kwargs) + + if not cache_enabled(): + raise NotCachedError(key) + else: + global CACHE_CALLS, CACHE_HITS, REQUEST_CACHE + CACHE_CALLS += 1 + if CACHE_CALLS == 1: + cache_require() + + obj = None + tid = -1 + if REQUEST_CACHE['enabled']: + tid = cache_get_request_uid() + if tid > -1: + try: + obj = REQUEST_CACHE[tid][key] + log.debug('Got from request cache: %s', key) + except KeyError: + pass + + if obj == None: + obj = cache.get(key) + + if obj and isinstance(obj, CacheWrapper): + CACHE_HITS += 1 + CACHED_KEYS[key] = True + log.debug('got cached [%i/%i]: %s', CACHE_CALLS, CACHE_HITS, key) + if obj.inprocess: + raise MethodNotFinishedError(obj.val) + + cache_set_request(key, obj, uid=tid) + + return obj.val + else: + try: + del CACHED_KEYS[key] + except KeyError: + pass + + if use_default: + return default_value + + raise NotCachedError(key) + + +def cache_set(*keys, **kwargs): + """Set an object into the cache.""" + if cache_enabled(): + global CACHED_KEYS, REQUEST_CACHE + obj = kwargs.pop('value') + length = kwargs.pop('length', CACHE_TIMEOUT) + skiplog = kwargs.pop('skiplog', False) + + key = cache_key(keys, **kwargs) + val = CacheWrapper.wrap(obj) + if not skiplog: + log.debug('setting cache: %s', key) + cache.set(key, val, length) + CACHED_KEYS[key] = True + if REQUEST_CACHE['enabled']: + cache_set_request(key, val) + +def _hash_or_string(key): + if is_string_like(key) or isinstance(key, (types.IntType, types.LongType, types.FloatType)): + return smart_str(key) + else: + try: + #if it has a PK, use it. + return str(key._get_pk_val()) + except AttributeError: + return md5_hash(key) + +def cache_contains(*keys, **kwargs): + key = cache_key(keys, **kwargs) + return CACHED_KEYS.has_key(key) + +def cache_key(*keys, **pairs): + """Smart key maker, returns the object itself if a key, else a list + delimited by ':', automatically hashing any non-scalar objects.""" + + if is_string_like(keys): + keys = [keys] + + if is_list_or_tuple(keys): + if len(keys) == 1 and is_list_or_tuple(keys[0]): + keys = keys[0] + else: + keys = [md5_hash(keys)] + + if pairs: + keys = list(keys) + klist = pairs.keys() + klist.sort() + for k in klist: + keys.append(k) + keys.append(pairs[k]) + + key = KEY_DELIM.join([_hash_or_string(x) for x in keys]) + prefix = CACHE_PREFIX + KEY_DELIM + if not key.startswith(prefix): + key = prefix+key + return key.replace(" ", ".") + +def md5_hash(obj): + pickled = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) + return md5_constructor(pickled).hexdigest() + + +def is_memcached_backend(): + try: + return cache._cache.__module__.endswith('memcache') + except AttributeError: + return False + +def cache_require(): + """Error if keyedcache isn't running.""" + if cache_enabled(): + key = cache_key('require_cache') + cache_set(key,value='1') + v = cache_get(key, default = '0') + if v != '1': + raise CacheNotRespondingError() + else: + log.debug("Cache responding OK") + return True + +def cache_clear_request(uid): + """Clears all locally cached elements with that uid""" + global REQUEST_CACHE + try: + del REQUEST_CACHE[uid] + log.debug('cleared request cache: %s', uid) + except KeyError: + pass + +def cache_use_request_caching(): + global REQUEST_CACHE + REQUEST_CACHE['enabled'] = True + +def cache_get_request_uid(): + from threaded_multihost import threadlocals + return threadlocals.get_thread_variable('request_uid', -1) + +def cache_set_request(key, val, uid=None): + if uid == None: + uid = cache_get_request_uid() + + if uid>-1: + global REQUEST_CACHE + if not uid in REQUEST_CACHE: + REQUEST_CACHE[uid] = {key:val} + else: + REQUEST_CACHE[uid][key] = val diff --git a/keyedcache/locale/de/LC_MESSAGES/django.mo b/keyedcache/locale/de/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..c623451a --- /dev/null +++ b/keyedcache/locale/de/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/de/LC_MESSAGES/django.po b/keyedcache/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..fc94969b --- /dev/null +++ b/keyedcache/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,40 @@ +# 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" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "Start" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "Cachestatistik" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "" + diff --git a/keyedcache/locale/en/LC_MESSAGES/django.mo b/keyedcache/locale/en/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..c2bc0b94 --- /dev/null +++ b/keyedcache/locale/en/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/en/LC_MESSAGES/django.po b/keyedcache/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..7c6fdf87 --- /dev/null +++ b/keyedcache/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,40 @@ +# 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" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "" + diff --git a/keyedcache/locale/es/LC_MESSAGES/django.po b/keyedcache/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/keyedcache/locale/es/LC_MESSAGES/django.po diff --git a/keyedcache/locale/fr/LC_MESSAGES/django.mo b/keyedcache/locale/fr/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..43ae9859 --- /dev/null +++ b/keyedcache/locale/fr/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/fr/LC_MESSAGES/django.po b/keyedcache/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..811a3def --- /dev/null +++ b/keyedcache/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,69 @@ +# 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" + +#: views.py:16 +msgid "Yes" +msgstr "Oui" + +#: views.py:17 +msgid "No" +msgstr "Non" + +#: views.py:21 +msgid "Key to delete" +msgstr "Clé à effacer" + +#: views.py:22 +msgid "Include Children?" +msgstr "Inclure les enfants?" + +#: views.py:23 +msgid "Delete all keys?" +msgstr "Effacer toutes les clés?" + +#: templates/keyedcache/delete.html.py:6 +#: templates/keyedcache/stats.html.py:6 +#: templates/keyedcache/view.html.py:6 +#: templates/keyedcache/delete.html.py:6 +#: templates/keyedcache/stats.html.py:6 +#: templates/keyedcache/view.html.py:6 +msgid "Home" +msgstr "Accueil" + +#: templates/keyedcache/delete.html.py:7 +#: templates/keyedcache/view.html.py:7 +#: templates/keyedcache/delete.html.py:7 +#: templates/keyedcache/view.html.py:7 +msgid "Cache" +msgstr "Cache" + +#: templates/keyedcache/delete.html.py:8 +#: templates/keyedcache/delete.html.py:8 +msgid "Cache Delete" +msgstr "Vider le cache" + +#: templates/keyedcache/stats.html.py:7 +#: templates/keyedcache/stats.html.py:7 +msgid "Cache Stats" +msgstr "Statut du cache" + +#: templates/keyedcache/view.html.py:8 +#: templates/keyedcache/view.html.py:8 +msgid "Cache View" +msgstr "Voir le cache" + diff --git a/keyedcache/locale/he/LC_MESSAGES/django.mo b/keyedcache/locale/he/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..8c043f31 --- /dev/null +++ b/keyedcache/locale/he/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/he/LC_MESSAGES/django.po b/keyedcache/locale/he/LC_MESSAGES/django.po new file mode 100644 index 00000000..7354e16a --- /dev/null +++ b/keyedcache/locale/he/LC_MESSAGES/django.po @@ -0,0 +1,60 @@ +# 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:01+0200\n" +"PO-Revision-Date: 2009-03-13 16:04\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" + +#: views.py:16 +msgid "Yes" +msgstr "כן" + +#: views.py:17 +msgid "No" +msgstr "לא" + +#: views.py:21 +msgid "Key to delete" +msgstr "מפתח שיש למחוק" + +#: views.py:22 +msgid "Include Children?" +msgstr "כלול ילדים?" + +#: views.py:23 +msgid "Delete all keys?" +msgstr "מחק את כל המפתחות?" + +#: templates/keyedcache/delete.html:6 templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "בית" + +#: templates/keyedcache/delete.html:7 templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "זכרון מטמון" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "מחק זכרון מטמון" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "סטטיסטיקת זכרון מטמון" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "הצג זכרון מטמון" diff --git a/keyedcache/locale/it/LC_MESSAGES/django.mo b/keyedcache/locale/it/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..5e614a26 --- /dev/null +++ b/keyedcache/locale/it/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/it/LC_MESSAGES/django.po b/keyedcache/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..271ef7be --- /dev/null +++ b/keyedcache/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,68 @@ +# 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" + +#: views.py:16 +msgid "Yes" +msgstr "Si" + +#: views.py:17 +msgid "No" +msgstr "No" + +#: views.py:21 +msgid "Key to delete" +msgstr "Chiave da eliminare" + +#: views.py:22 +# translated = "Slug" +msgid "Include Children?" +msgstr "Includi i figli?" + +#: views.py:23 +msgid "Delete all keys?" +msgstr "Elimina tutte le chiavi?" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "Pagina iniziale" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +# translated = "Prodotto sottotipi" +msgid "Cache" +msgstr "Cache" + +#: templates/keyedcache/delete.html:8 +# translated = "Cache" +msgid "Cache Delete" +msgstr "Elimina Cache" + +#: templates/keyedcache/stats.html:7 +# translated = "Elimina cache" +msgid "Cache Stats" +msgstr "Statistiche Cache" + +#: templates/keyedcache/view.html:8 +# translated = "Statistiche di cache" +msgid "Cache View" +msgstr "Viste Cache" + diff --git a/keyedcache/locale/ko/LC_MESSAGES/django.mo b/keyedcache/locale/ko/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..18d97529 --- /dev/null +++ b/keyedcache/locale/ko/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/ko/LC_MESSAGES/django.po b/keyedcache/locale/ko/LC_MESSAGES/django.po new file mode 100644 index 00000000..0969979a --- /dev/null +++ b/keyedcache/locale/ko/LC_MESSAGES/django.po @@ -0,0 +1,40 @@ +# 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" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "홈" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "캐쉬" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "캐쉬 삭제" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "캐쉬 상태" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "캐쉬 보기" + diff --git a/keyedcache/locale/pl/LC_MESSAGES/django.mo b/keyedcache/locale/pl/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..14c6acd3 --- /dev/null +++ b/keyedcache/locale/pl/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/pl/LC_MESSAGES/django.po b/keyedcache/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 00000000..77974341 --- /dev/null +++ b/keyedcache/locale/pl/LC_MESSAGES/django.po @@ -0,0 +1,60 @@ +# 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" + +#: views.py:16 +msgid "Yes" +msgstr "Tak" + +#: views.py:17 +msgid "No" +msgstr "Nie" + +#: views.py:21 +msgid "Key to delete" +msgstr "Identyfikator do usunięcia" + +#: views.py:22 +msgid "Include Children?" +msgstr "Czy razem z dziećmi?" + +#: views.py:23 +msgid "Delete all keys?" +msgstr "Usunąć wszystkie identyfikatory?" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "Strona startowa" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "Cache" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "Wyczyść pamięć podręczną" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "Statystyka pamięci podręcznej" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "Podgląd pamięci podręcznej" + diff --git a/keyedcache/locale/pt_BR/LC_MESSAGES/django.mo b/keyedcache/locale/pt_BR/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..bb1225bd --- /dev/null +++ b/keyedcache/locale/pt_BR/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/pt_BR/LC_MESSAGES/django.po b/keyedcache/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 00000000..4a9a1842 --- /dev/null +++ b/keyedcache/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,61 @@ +# 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" + +#: views.py:16 +msgid "Yes" +msgstr "Sim" + +#: views.py:17 +msgid "No" +msgstr "Não" + +#: views.py:21 +msgid "Key to delete" +msgstr "Chave para apagar" + +#: views.py:22 +#, fuzzy +msgid "Include Children?" +msgstr "Incluir imagens?" + +#: views.py:23 +msgid "Delete all keys?" +msgstr "Apagar todas as chaves?" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "Inicial" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "Cache" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "Apagar cache" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "Estatísticas de cache" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "Exibição de cache" + diff --git a/keyedcache/locale/ru/LC_MESSAGES/django.mo b/keyedcache/locale/ru/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..1ff29657 --- /dev/null +++ b/keyedcache/locale/ru/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/ru/LC_MESSAGES/django.po b/keyedcache/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 00000000..fbf7be23 --- /dev/null +++ b/keyedcache/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,40 @@ +# 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 15:50+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" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "Кеш" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "Очистка кеша" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "Статистика кеша" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "Просмотр кеша" + diff --git a/keyedcache/locale/sv/LC_MESSAGES/django.mo b/keyedcache/locale/sv/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..c9f2fd84 --- /dev/null +++ b/keyedcache/locale/sv/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/sv/LC_MESSAGES/django.po b/keyedcache/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 00000000..9aa00572 --- /dev/null +++ b/keyedcache/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,44 @@ +# 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" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "Hem" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "Cache" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "Radera Cache" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "Cache-statistik" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "Visa Cache" + diff --git a/keyedcache/locale/tr/LC_MESSAGES/django.mo b/keyedcache/locale/tr/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..60e5be3f --- /dev/null +++ b/keyedcache/locale/tr/LC_MESSAGES/django.mo diff --git a/keyedcache/locale/tr/LC_MESSAGES/django.po b/keyedcache/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 00000000..4dd6869c --- /dev/null +++ b/keyedcache/locale/tr/LC_MESSAGES/django.po @@ -0,0 +1,42 @@ +# 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" + +#: templates/keyedcache/delete.html:6 +#: templates/keyedcache/stats.html:6 +#: templates/keyedcache/view.html:6 +msgid "Home" +msgstr "Ev" + +#: templates/keyedcache/delete.html:7 +#: templates/keyedcache/view.html:7 +msgid "Cache" +msgstr "Cache" + +#: templates/keyedcache/delete.html:8 +msgid "Cache Delete" +msgstr "Cache'yi Sil" + +#: templates/keyedcache/stats.html:7 +msgid "Cache Stats" +msgstr "Cache İstatistikleri" + +#: templates/keyedcache/view.html:8 +msgid "Cache View" +msgstr "Cache'yi Göster" + diff --git a/keyedcache/models.py b/keyedcache/models.py new file mode 100644 index 00000000..dec68463 --- /dev/null +++ b/keyedcache/models.py @@ -0,0 +1,86 @@ +import keyedcache +import logging + +log = logging.getLogger('keyedcache') + +class CachedObjectMixin(object): + """Provides basic object keyedcache for any objects using this as a mixin.""" + + def cache_delete(self, *args, **kwargs): + key = self.cache_key(*args, **kwargs) + log.debug("clearing cache for %s", key) + keyedcache.cache_delete(key, children=True) + + def cache_get(self, *args, **kwargs): + key = self.cache_key(*args, **kwargs) + return keyedcache.cache_get(key) + + def cache_key(self, *args, **kwargs): + keys = [self.__class__.__name__, self] + keys.extend(args) + return keyedcache.cache_key(keys, **kwargs) + + def cache_reset(self): + self.cache_delete() + self.cache_set() + + def cache_set(self, *args, **kwargs): + val = kwargs.pop('value', self) + key = self.cache_key(*args, **kwargs) + keyedcache.cache_set(key, value=val) + + def is_cached(self, *args, **kwargs): + return keyedcache.is_cached(self.cache_key(*args, **kwargs)) + +def find_by_id(cls, groupkey, objectid, raises=False): + """A helper function to look up an object by id""" + ob = None + try: + ob = keyedcache.cache_get(groupkey, objectid) + except keyedcache.NotCachedError, e: + try: + ob = cls.objects.get(pk=objectid) + keyedcache.cache_set(e.key, value=ob) + + except cls.DoesNotExist: + log.debug("No such %s: %s", groupkey, objectid) + if raises: + raise cls.DoesNotExist + + return ob + + +def find_by_key(cls, groupkey, key, raises=False): + """A helper function to look up an object by key""" + ob = None + try: + ob = keyedcache.cache_get(groupkey, key) + except keyedcache.NotCachedError, e: + try: + ob = cls.objects.get(key__exact=key) + keyedcache.cache_set(e.key, value=ob) + + except cls.DoesNotExist: + log.debug("No such %s: %s", groupkey, key) + if raises: + raise + + return ob + +def find_by_slug(cls, groupkey, slug, raises=False): + """A helper function to look up an object by slug""" + ob = None + try: + ob = keyedcache.cache_get(groupkey, slug) + except keyedcache.NotCachedError, e: + try: + ob = cls.objects.get(slug__exact=slug) + keyedcache.cache_set(e.key, value=ob) + + except cls.DoesNotExist: + log.debug("No such %s: %s", groupkey, slug) + if raises: + raise + + return ob + diff --git a/keyedcache/templates/keyedcache/delete.html b/keyedcache/templates/keyedcache/delete.html new file mode 100644 index 00000000..9449a6d3 --- /dev/null +++ b/keyedcache/templates/keyedcache/delete.html @@ -0,0 +1,21 @@ +{% extends "admin/base_site.html" %} +{% load messaging_tags i18n %} + +{% block breadcrumbs %}{% if not is_popup %} +<div class="breadcrumbs"> + <a href="/admin/">{% trans "Home" %}</a> › + <a href="{% url keyedcache_view %}">{% trans "Cache" %}</a> › + {% trans "Cache Delete" %} +</div> +{% endif %}{% endblock %} + +{% block content %} +{% show_messages %} +<p>[<a href="{% url keyedcache_stats %}">Cache Stats</a>] [<a href="{% url keyedcache_view %}">View Cache</a>]</p> +<h1>Delete From Cache</h1> +<form method="POST" action="{% url keyedcache_delete %}"> +{{ form.as_p }} +<input type="submit"/> +</form> + +{% endblock %} diff --git a/keyedcache/templates/keyedcache/stats.html b/keyedcache/templates/keyedcache/stats.html new file mode 100644 index 00000000..0a3f17d6 --- /dev/null +++ b/keyedcache/templates/keyedcache/stats.html @@ -0,0 +1,21 @@ +{% extends "admin/base_site.html" %} +{% load messaging_tags i18n %} + +{% block breadcrumbs %}{% if not is_popup %} +<div class="breadcrumbs"> + <a href="/admin/">{% trans "Home" %}</a> › + {% trans "Cache Stats" %} +</div> +{% endif %}{% endblock %} + +{% block content %} +{% show_messages %} +<p>[<a href="{% url keyedcache_view %}">View Cache</a>] [<a href="{% url keyedcache_delete %}">Delete from Cache</a>] +<h1>Cache Stats</h1> +<p>Backend: {{ cache_backend }} ({% if cache_running %}running{% else %}down{% endif %})</p> +<p>Timeout: {{ cache_time }}</p> +<p>Keys in cache: {{ cache_count }}</p> +<p>Cache Calls: {{ cache_calls }}</p> +<p>Cache Hits: {{ cache_hits }}</p> +<p>Cache Hit Rate: {{ hit_rate }}%</p> +{% endblock %} diff --git a/keyedcache/templates/keyedcache/view.html b/keyedcache/templates/keyedcache/view.html new file mode 100644 index 00000000..e28c5a0e --- /dev/null +++ b/keyedcache/templates/keyedcache/view.html @@ -0,0 +1,18 @@ +{% extends "admin/base_site.html" %} +{% load messaging_tags i18n %} + +{% block breadcrumbs %}{% if not is_popup %} +<div class="breadcrumbs"> + <a href="/admin/">{% trans "Home" %}</a> › + <a href="{% url keyedcache_view %}">{% trans "Cache" %}</a> › + {% trans "Cache View" %} +</div> +{% endif %}{% endblock %} + +{% block content %} +{% show_messages %} +<p>[<a href="{% url keyedcache_stats %}">Cache Stats</a>] [<a href="{% url keyedcache_delete %}">Delete from Cache</a>] +<h1>Cache Keys</h1> +<p style="font-size:82%;">{% for key in cached_keys %}{{ key }}, {% endfor %} +</p> +{% endblock %} diff --git a/keyedcache/tests.py b/keyedcache/tests.py new file mode 100644 index 00000000..8abb8dd3 --- /dev/null +++ b/keyedcache/tests.py @@ -0,0 +1,150 @@ +import keyedcache +import random +from django.test import TestCase +import time + +CACHE_HIT=0 + +def cachetest(a,b,c): + global CACHE_HIT + CACHE_HIT += 1 + r = [random.randrange(0,1000) for x in range(0,3)] + ret = [r, a + r[0], b + r[1], c + r[2]] + return ret + +cachetest = keyedcache.cache_function(2)(cachetest) + +class DecoratorTest(TestCase): + + def testCachePut(self): + d = cachetest(1,2,3) + self.assertEqual(CACHE_HIT,1) + + d2 = cachetest(1,2,3) + self.assertEqual(CACHE_HIT,1) + self.assertEqual(d, d2) + + seeds = d[0] + self.assertEqual(seeds[0] + 1, d[1]) + self.assertEqual(seeds[1] + 2, d[2]) + self.assertEqual(seeds[2] + 3, d[3]) + + time.sleep(3) + d3 = cachetest(1,2,3) + self.assertEqual(CACHE_HIT,2) + self.assertNotEqual(d, d3) + + def testDeleteCachedFunction(self): + orig = cachetest(10,20,30) + keyedcache.cache_delete_function(cachetest) + after = cachetest(10,20,30) + self.assertNotEqual(orig,keyedcache) + +class CachingTest(TestCase): + + def testCacheGetFail(self): + try: + keyedcache.cache_get('x') + self.fail('should have raised NotCachedError') + except keyedcache.NotCachedError: + pass + + def testCacheGetOK(self): + one = [1,2,3,4] + keyedcache.cache_set('ok', value=one, length=2) + two = keyedcache.cache_get('ok') + self.assertEqual(one, two) + + time.sleep(5) + try: + three = keyedcache.cache_get('ok') + self.fail('should have raised NotCachedError, got %s' % three) + except keyedcache.NotCachedError: + pass + + def testCacheGetDefault(self): + chk = keyedcache.cache_get('default',default='-') + self.assertEqual(chk, '-') + + + def testDelete(self): + keyedcache.cache_set('del', value=True) + + for x in range(0,10): + keyedcache.cache_set('del', 'x', x, value=True) + for y in range(0,5): + keyedcache.cache_set('del', 'x', x, 'y', y, value=True) + + # check to make sure all the values are in the cache + self.assert_(keyedcache.cache_get('del', default=False)) + for x in range(0,10): + self.assert_(keyedcache.cache_get('del', 'x', x, default=False)) + for y in range(0,5): + self.assert_(keyedcache.cache_get('del', 'x', x, 'y', y, default=False)) + + # try to delete just one + killed = keyedcache.cache_delete('del','x',1) + self.assertEqual([keyedcache.CACHE_PREFIX + "::del::x::1"], killed) + self.assertFalse(keyedcache.cache_get('del', 'x', 1, default=False)) + + # but the others are still there + self.assert_(keyedcache.cache_get('del', 'x', 2, default=False)) + + # now kill all of del::x::1 + killed = keyedcache.cache_delete('del','x', 1, children=True) + for y in range(0,5): + self.assertFalse(keyedcache.cache_get('del', 'x', 1, 'y', y, default=False)) + + # but del::x::2 and children are there + self.assert_(keyedcache.cache_get('del','x',2,'y',1, default=False)) + + # kill the rest + killed = keyedcache.cache_delete('del', children=True) + self.assertFalse(keyedcache.cache_get('del',default=False)) + for x in range(0,10): + self.assertFalse(keyedcache.cache_get('del', 'x', x, default=False)) + for y in range(0,5): + self.assertFalse(keyedcache.cache_get('del', 'x', x, 'y', y, default=False)) + + +class TestCacheDisable(TestCase): + + def testDisable(self): + keyedcache.cache_set('disabled', value=False) + v = keyedcache.cache_get('disabled') + self.assertEqual(v, False) + + keyedcache.cache_enable(False) + keyedcache.cache_set('disabled', value=True) + try: + keyedcache.cache_get('disabled') + self.fail('should have raised NotCachedError') + except keyedcache.NotCachedError, nce: + key = keyedcache.cache_key('disabled') + self.assertEqual(nce.key, key) + + keyedcache.cache_enable() + v2 = keyedcache.cache_get('disabled') + # should still be False, since the cache was disabled + self.assertEqual(v2, False) + +class TestKeyMaker(TestCase): + + def testSimpleKey(self): + v = keyedcache.cache_key('test') + self.assertEqual(v, keyedcache.CACHE_PREFIX + '::test') + + def testDualKey(self): + v = keyedcache.cache_key('test', 2) + self.assertEqual(v, keyedcache.CACHE_PREFIX + '::test::2') + + def testPairedKey(self): + v = keyedcache.cache_key('test', more='yes') + self.assertEqual(v, keyedcache.CACHE_PREFIX + '::test::more::yes') + + def testPairedDualKey(self): + v = keyedcache.cache_key('test', 3, more='yes') + self.assertEqual(v, keyedcache.CACHE_PREFIX + '::test::3::more::yes') + + + diff --git a/keyedcache/threaded.py b/keyedcache/threaded.py new file mode 100644 index 00000000..997fddbc --- /dev/null +++ b/keyedcache/threaded.py @@ -0,0 +1,32 @@ +"""Causes the keyedcache to also use a first-level cache in memory - this can cut 30-40% of memcached calls. + +To enable, add this to some models.py file in an app:: + + from keyedcache import threaded + threaded.start_listening() + +""" +from threaded_multihost import threadlocals +from django.core.signals import request_started, request_finished +from keyedcache import cache_clear_request, cache_use_request_caching +import random +import logging +log = logging.getLogger('keyedcache.threaded') + +def set_request_uid(sender, *args, **kwargs): + """Puts a unique id into the thread""" + tid = random.randrange(1,10000000) + threadlocals.set_thread_variable('request_uid', tid) + #log.debug('request UID: %s', tid) + +def clear_request_uid(sender, *args, **kwargs): + """Removes the thread cache for this request""" + tid = threadlocals.get_thread_variable('request_uid', -1) + if tid > -1: + cache_clear_request(tid) + +def start_listening(): + log.debug('setting up threaded keyedcache') + cache_use_request_caching() + request_started.connect(set_request_uid) + request_finished.connect(clear_request_uid) diff --git a/keyedcache/urls.py b/keyedcache/urls.py new file mode 100644 index 00000000..1a944043 --- /dev/null +++ b/keyedcache/urls.py @@ -0,0 +1,10 @@ +""" +URLConf for Caching app +""" + +from django.conf.urls.defaults import patterns +urlpatterns = patterns('keyedcache.views', + (r'^$', 'stats_page', {}, 'keyedcache_stats'), + (r'^view/$', 'view_page', {}, 'keyedcache_view'), + (r'^delete/$', 'delete_page', {}, 'keyedcache_delete'), +) diff --git a/keyedcache/utils.py b/keyedcache/utils.py new file mode 100644 index 00000000..29b8fd71 --- /dev/null +++ b/keyedcache/utils.py @@ -0,0 +1,14 @@ +import types + +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 is_list_or_tuple(maybe): + return isinstance(maybe, (types.TupleType, types.ListType)) diff --git a/keyedcache/views.py b/keyedcache/views.py new file mode 100644 index 00000000..9a3c1219 --- /dev/null +++ b/keyedcache/views.py @@ -0,0 +1,103 @@ +from django import forms +from django.conf import settings +from django.contrib.auth.decorators import user_passes_test +from django.http import HttpResponseRedirect +from django.shortcuts import render_to_response +from django.template import RequestContext +from django.utils.translation import ugettext_lazy as _ +import keyedcache +import logging + +log = logging.getLogger('keyedcache.views') + +YN = ( + ('Y', _('Yes')), + ('N', _('No')), + ) + +class CacheDeleteForm(forms.Form): + tag = forms.CharField(label=_('Key to delete'), required=False) + children = forms.ChoiceField(label=_('Include Children?'), choices=YN, initial="Y") + kill_all = forms.ChoiceField(label=_('Delete all keys?'), choices=YN, initial="Y") + + def delete_cache(self): + + data = self.cleaned_data + if data['kill_all'] == "Y": + keyedcache.cache_delete() + result = "Deleted all keys" + elif data['tag']: + keyedcache.cache_delete(data['tag'], children=data['children']) + if data['children'] == "Y": + result = "Deleted %s and children" % data['tag'] + else: + result = "Deleted %s" % data['tag'] + else: + result = "Nothing selected to delete" + + log.debug(result) + return result + +def stats_page(request): + calls = keyedcache.CACHE_CALLS + hits = keyedcache.CACHE_HITS + + if (calls and hits): + rate = float(keyedcache.CACHE_HITS)/keyedcache.CACHE_CALLS*100 + else: + rate = 0 + + try: + running = keyedcache.cache_require() + + except keyedcache.CacheNotRespondingError: + running = False + + ctx = RequestContext(request, { + 'cache_count' : len(keyedcache.CACHED_KEYS), + 'cache_running' : running, + 'cache_time' : settings.CACHE_TIMEOUT, + 'cache_backend' : settings.CACHE_BACKEND, + 'cache_calls' : keyedcache.CACHE_CALLS, + 'cache_hits' : keyedcache.CACHE_HITS, + 'hit_rate' : "%02.1f" % rate + }) + + return render_to_response('keyedcache/stats.html', context_instance=ctx) + +stats_page = user_passes_test(lambda u: u.is_authenticated() and u.is_staff, login_url='/accounts/login/')(stats_page) + +def view_page(request): + keys = keyedcache.CACHED_KEYS.keys() + + keys.sort() + + ctx = RequestContext(request, { + 'cached_keys' : keys, + }) + + return render_to_response('keyedcache/view.html', context_instance=ctx) + +view_page = user_passes_test(lambda u: u.is_authenticated() and u.is_staff, login_url='/accounts/login/')(view_page) + +def delete_page(request): + log.debug("delete_page") + if request.method == "POST": + form = CacheDeleteForm(request.POST) + if form.is_valid(): + log.debug('delete form valid') + results = form.delete_cache() + return HttpResponseRedirect('../') + else: + log.debug("Errors in form: %s", form.errors) + else: + log.debug("new form") + form = CacheDeleteForm() + + ctx = RequestContext(request, { + 'form' : form, + }) + + return render_to_response('keyedcache/delete.html', context_instance=ctx) + +delete_page = user_passes_test(lambda u: u.is_authenticated() and u.is_staff, login_url='/accounts/login/')(delete_page) diff --git a/livesettings/__init__.py b/livesettings/__init__.py new file mode 100644 index 00000000..49aaacc9 --- /dev/null +++ b/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/livesettings/forms.py b/livesettings/forms.py new file mode 100644 index 00000000..b1c5f6f4 --- /dev/null +++ b/livesettings/forms.py @@ -0,0 +1,38 @@ +from django import forms +from 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/livesettings/functions.py b/livesettings/functions.py new file mode 100644 index 00000000..8b919083 --- /dev/null +++ b/livesettings/functions.py @@ -0,0 +1,247 @@ +from django.utils.translation import ugettext +from livesettings import values +from livesettings.models import SettingNotSet +from 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/livesettings/locale/de/LC_MESSAGES/django.mo b/livesettings/locale/de/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..e176bc53 --- /dev/null +++ b/livesettings/locale/de/LC_MESSAGES/django.mo diff --git a/livesettings/locale/de/LC_MESSAGES/django.po b/livesettings/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..1cef701b --- /dev/null +++ b/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/livesettings/locale/en/LC_MESSAGES/django.mo b/livesettings/locale/en/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..c2bc0b94 --- /dev/null +++ b/livesettings/locale/en/LC_MESSAGES/django.mo diff --git a/livesettings/locale/en/LC_MESSAGES/django.po b/livesettings/locale/en/LC_MESSAGES/django.po new file mode 100644 index 00000000..45eb23a5 --- /dev/null +++ b/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/livesettings/locale/es/LC_MESSAGES/django.po b/livesettings/locale/es/LC_MESSAGES/django.po new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/livesettings/locale/es/LC_MESSAGES/django.po diff --git a/livesettings/locale/fr/LC_MESSAGES/django.mo b/livesettings/locale/fr/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..dd872edd --- /dev/null +++ b/livesettings/locale/fr/LC_MESSAGES/django.mo diff --git a/livesettings/locale/fr/LC_MESSAGES/django.po b/livesettings/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..90475585 --- /dev/null +++ b/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/livesettings/locale/he/LC_MESSAGES/django.mo b/livesettings/locale/he/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..04270a04 --- /dev/null +++ b/livesettings/locale/he/LC_MESSAGES/django.mo diff --git a/livesettings/locale/he/LC_MESSAGES/django.po b/livesettings/locale/he/LC_MESSAGES/django.po new file mode 100644 index 00000000..362f5612 --- /dev/null +++ b/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/livesettings/locale/it/LC_MESSAGES/django.mo b/livesettings/locale/it/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..05c50952 --- /dev/null +++ b/livesettings/locale/it/LC_MESSAGES/django.mo diff --git a/livesettings/locale/it/LC_MESSAGES/django.po b/livesettings/locale/it/LC_MESSAGES/django.po new file mode 100644 index 00000000..66401866 --- /dev/null +++ b/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/livesettings/locale/ko/LC_MESSAGES/django.mo b/livesettings/locale/ko/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..e0738605 --- /dev/null +++ b/livesettings/locale/ko/LC_MESSAGES/django.mo diff --git a/livesettings/locale/ko/LC_MESSAGES/django.po b/livesettings/locale/ko/LC_MESSAGES/django.po new file mode 100644 index 00000000..0dbd2d4d --- /dev/null +++ b/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/livesettings/locale/pl/LC_MESSAGES/django.mo b/livesettings/locale/pl/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..f45e49ed --- /dev/null +++ b/livesettings/locale/pl/LC_MESSAGES/django.mo diff --git a/livesettings/locale/pl/LC_MESSAGES/django.po b/livesettings/locale/pl/LC_MESSAGES/django.po new file mode 100644 index 00000000..1e7b4199 --- /dev/null +++ b/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/livesettings/locale/pt_BR/LC_MESSAGES/django.mo b/livesettings/locale/pt_BR/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..a8bfb8b2 --- /dev/null +++ b/livesettings/locale/pt_BR/LC_MESSAGES/django.mo diff --git a/livesettings/locale/pt_BR/LC_MESSAGES/django.po b/livesettings/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 00000000..72d49df7 --- /dev/null +++ b/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/livesettings/locale/ru/LC_MESSAGES/django.mo b/livesettings/locale/ru/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..42e6074a --- /dev/null +++ b/livesettings/locale/ru/LC_MESSAGES/django.mo diff --git a/livesettings/locale/ru/LC_MESSAGES/django.po b/livesettings/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 00000000..a0db054b --- /dev/null +++ b/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/livesettings/locale/sv/LC_MESSAGES/django.mo b/livesettings/locale/sv/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..caed0ab9 --- /dev/null +++ b/livesettings/locale/sv/LC_MESSAGES/django.mo diff --git a/livesettings/locale/sv/LC_MESSAGES/django.po b/livesettings/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 00000000..6b096f6b --- /dev/null +++ b/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/livesettings/locale/tr/LC_MESSAGES/django.mo b/livesettings/locale/tr/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..d56ad423 --- /dev/null +++ b/livesettings/locale/tr/LC_MESSAGES/django.mo diff --git a/livesettings/locale/tr/LC_MESSAGES/django.po b/livesettings/locale/tr/LC_MESSAGES/django.po new file mode 100644 index 00000000..bb2a1506 --- /dev/null +++ b/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/livesettings/models.py b/livesettings/models.py new file mode 100644 index 00000000..43f14648 --- /dev/null +++ b/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 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/livesettings/overrides.py b/livesettings/overrides.py new file mode 100644 index 00000000..5f88d5c5 --- /dev/null +++ b/livesettings/overrides.py @@ -0,0 +1,55 @@ +"""Allows 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 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/livesettings/signals.py b/livesettings/signals.py new file mode 100644 index 00000000..ddea31f5 --- /dev/null +++ b/livesettings/signals.py @@ -0,0 +1,3 @@ +import django.dispatch + +configuration_value_changed = django.dispatch.Signal() diff --git a/livesettings/templates/livesettings/_admin_site_views.html b/livesettings/templates/livesettings/_admin_site_views.html new file mode 100644 index 00000000..17d08f58 --- /dev/null +++ b/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/livesettings/templates/livesettings/group_settings.html b/livesettings/templates/livesettings/group_settings.html new file mode 100644 index 00000000..e56f764f --- /dev/null +++ b/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/livesettings/templates/livesettings/site_settings.html b/livesettings/templates/livesettings/site_settings.html new file mode 100644 index 00000000..35333778 --- /dev/null +++ b/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/livesettings/templates/livesettings/text.txt b/livesettings/templates/livesettings/text.txt new file mode 100644 index 00000000..d57a57e3 --- /dev/null +++ b/livesettings/templates/livesettings/text.txt @@ -0,0 +1 @@ +{{ text|safe }}
\ No newline at end of file diff --git a/livesettings/templatetags/__init__.py b/livesettings/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/livesettings/templatetags/__init__.py diff --git a/livesettings/templatetags/config_tags.py b/livesettings/templatetags/config_tags.py new file mode 100644 index 00000000..bcdded12 --- /dev/null +++ b/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 livesettings import config_value +from 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('livesettings/_admin_site_views.html')(admin_site_views) diff --git a/livesettings/tests.py b/livesettings/tests.py new file mode 100644 index 00000000..2a60bf7e --- /dev/null +++ b/livesettings/tests.py @@ -0,0 +1,545 @@ +from django.conf import settings as djangosettings +from django.test import TestCase +import keyedcache +from 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/livesettings/urls.py b/livesettings/urls.py new file mode 100644 index 00000000..bc10b787 --- /dev/null +++ b/livesettings/urls.py @@ -0,0 +1,7 @@ +from django.conf.urls.defaults import * + +urlpatterns = patterns('livesettings.views', + url(r'^$', 'site_settings', {}, name='satchmo_site_settings'), + url(r'^export/$', 'export_as_python', {}, name='settings_export'), + url(r'^(?P<group>[^/]+)/$', 'group_settings', name='group_settings'), +) diff --git a/livesettings/utils.py b/livesettings/utils.py new file mode 100644 index 00000000..c0e0e293 --- /dev/null +++ b/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/livesettings/values.py b/livesettings/values.py new file mode 100644 index 00000000..db952172 --- /dev/null +++ b/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 livesettings.models import find_setting, LongSetting, Setting, SettingNotSet +from livesettings.overrides import get_overrides +from 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 - 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/livesettings/views.py b/livesettings/views.py new file mode 100644 index 00000000..c2a2ccdc --- /dev/null +++ b/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 livesettings import ConfigurationSettings, forms +from livesettings.overrides import get_overrides +import logging + +log = logging.getLogger('configuration.views') + +def group_settings(request, group, template='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='livesettings/site_settings.html') + +def export_as_python(request): + """Export site settings as a dictionary of dictionaries""" + + from 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('livesettings/text.txt', { 'text' : pretty }, mimetype='text/plain') + +export_as_python = never_cache(staff_member_required(export_as_python)) diff --git a/settings.py b/settings.py index 2acc920e..98555997 100644 --- a/settings.py +++ b/settings.py @@ -11,8 +11,10 @@ SECRET_KEY = '$oo^&_m&qwbib=(_4m_n*zn-d=g#s0he5fx9xonnym#8p6yigm' TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', + + #below is forum stuff for this tuple 'forum.modules.module_templates_loader',#todo: remove this - 'forum.skins.load_template_source', + 'forum.skins.load_template_source',#forum stuff # 'django.template.loaders.eggs.load_template_source', ) @@ -25,6 +27,8 @@ MIDDLEWARE_CLASSES = ( #'django.middleware.cache.FetchFromCacheMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', #'django.middleware.sqlprint.SqlPrintingMiddleware', + + #below is forum stuff for this tuple 'forum.middleware.anon_user.ConnectToSessionMessagesMiddleware', 'forum.middleware.pagesize.QuestionsPageSizeMiddleware', 'forum.middleware.cancel.CancelActionMiddleware', @@ -34,6 +38,7 @@ MIDDLEWARE_CLASSES = ( 'forum.middleware.view_log.ViewLogMiddleware', ) +#all of these are necessary for the forum and absend in default settings.py TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', 'forum.context.application_settings', @@ -45,6 +50,7 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ROOT_URLCONF = 'urls' TEMPLATE_DIRS = ( + #specific to forum os.path.join(os.path.dirname(__file__),'forum','skins').replace('\\','/'), ) @@ -66,6 +72,8 @@ INSTALLED_APPS = ( 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', + + #all of these are needed for the forum 'django.contrib.admin', 'django.contrib.humanize', 'django.contrib.sitemaps', @@ -75,14 +83,13 @@ INSTALLED_APPS = ( 'debug_toolbar' , #'forum.importers.stackexchange', #se loader 'south', + 'livesettings', + 'keyedcache', ) AUTHENTICATION_BACKENDS = ('django.contrib.auth.backends.ModelBackend',) -if USE_FB_CONNECT: - INSTALLED_APPS += ('fbconnect',) - -#load optional plugin module for external password login +#this needs to go if 'USE_EXTERNAL_LEGACY_LOGIN' in locals() and USE_EXTERNAL_LEGACY_LOGIN: INSTALLED_APPS += (EXTERNAL_LEGACY_LOGIN_MODULE,) diff --git a/settings_local.py.dist b/settings_local.py.dist index 137ad6ec..3ae8b512 100755 --- a/settings_local.py.dist +++ b/settings_local.py.dist @@ -1,6 +1,5 @@ # encoding:utf-8 import os.path -from django.utils.translation import ugettext as _ SITE_SRC_ROOT = os.path.dirname(__file__) LOG_FILENAME = 'askbot.log' @@ -18,26 +17,17 @@ ADMINS = (('Forum Admin', 'forum@example.com'),) MANAGERS = ADMINS #DEBUG SETTINGS -DEBUG = False +DEBUG = True TEMPLATE_DEBUG = DEBUG INTERNAL_IPS = ('127.0.0.1',) DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_ENGINE = '' #mysql, etc +DATABASE_ENGINE = 'mysql' #mysql, etc DATABASE_HOST = '' DATABASE_PORT = '' -#set this value to 'dummy://' if you don't want to use cache, or set up your favourite caching mechanism -#see http://docs.djangoproject.com/en/1.1/topics/cache/ for details -#example (set local file system cache in a cache folder in the root of the askbot install): -#CACHE_BACKEND = 'file://%s' % os.path.join(os.path.dirname(__file__),'cache').replace('\\','/') -CACHE_BACKEND = 'dummy://' - -#If you use memcache you may want to uncomment the following line to enable memcached based sessions -#SESSION_ENGINE = 'django.contrib.sessions.backends.cache_db' - #email server settings SERVER_EMAIL = '' DEFAULT_FROM_EMAIL = '' @@ -48,80 +38,32 @@ EMAIL_HOST='askbot.org' EMAIL_PORT='25' EMAIL_USE_TLS=False -#HACK - anonymous user email - for email-less users -ANONYMOUS_USER_EMAIL = 'anonymous@askbot.org' - #LOCALIZATIONS TIME_ZONE = 'America/New_York' -########################### -# -# this will allow running your forum with url like http://site.com/forum -# -# FORUM_SCRIPT_ALIAS = 'forum/' -# +#this will allow running your forum with url like http://site.com/forum +#FORUM_SCRIPT_ALIAS = 'forum/' FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string - #OTHER SETTINGS -APP_TITLE = u'ASKBOT: Open Source Q&A Forum' -APP_SHORT_NAME = u'ASKBOT' -APP_KEYWORDS = u'ASKBOT,CNPROG,forum,community' -APP_DESCRIPTION = u'Ask and answer questions.' -APP_INTRO = u'<p>Ask and answer questions, make the world better!</p>' -APP_COPYRIGHT = 'Copyright ASKBOT, 2009. Some rights reserved under creative commons license.' -LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/') -GREETING_URL = LOGIN_URL #may be url of "faq" page or "about", etc +_ = lambda v:v +LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,_('account/'),_('signin/')) USE_I18N = True LANGUAGE_CODE = 'en' -EMAIL_VALIDATION = 'off' #string - on|off -MIN_USERNAME_LENGTH = 1 -EMAIL_UNIQUE = False -APP_URL = 'http://askbot.org' #used by email notif system and RSS -GOOGLE_SITEMAP_CODE = '' -GOOGLE_ANALYTICS_KEY = '' -WIKI_ON = True -FEEDBACK_SITE_URL = None #None or url -EDITABLE_SCREEN_NAME = False #True or False - can user change screen name? - -DJANGO_VERSION = 1.1 -RESOURCE_REVISION=4 - -#please get these at recaptcha.net -RECAPTCHA_PRIVATE_KEY='...' -RECAPTCHA_PUBLIC_KEY='...' -ASKBOT_DEFAULT_SKIN = 'default' - - -#Facebook settings -USE_FB_CONNECT=False -FB_API_KEY='' #your api key from facebook -FB_SECRET='' #your application secret -USE_EXTERNAL_LEGACY_LOGIN = False #DO NOT USE, and do not delete this line, will be removed later -#counter colors -from forum_modules.grapefruit import Color -VOTE_COUNTER_EXPECTED_MAXIMUM = 5 -COLORS_VOTE_COUNTER_EMPTY_BG = 'white' -COLORS_VOTE_COUNTER_EMPTY_FG = 'gray' -COLORS_VOTE_COUNTER_MIN_BG = 'white' -COLORS_VOTE_COUNTER_MIN_FG = 'black' -COLORS_VOTE_COUNTER_MAX_BG = '#a9d0f5' -COLORS_VOTE_COUNTER_MAX_FG = Color.NewFromHtml(COLORS_VOTE_COUNTER_MAX_BG).DarkerColor(0.7).html -VIEW_COUNTER_EXPECTED_MAXIMUM = 100 -COLORS_VIEW_COUNTER_EMPTY_BG = 'gray' -COLORS_VIEW_COUNTER_EMPTY_FG = 'white' -COLORS_VIEW_COUNTER_MIN_BG = '#D0F5A9' -COLORS_VIEW_COUNTER_MIN_FG = Color.NewFromHtml(COLORS_VIEW_COUNTER_MIN_BG).DarkerColor(0.6).html -COLORS_VIEW_COUNTER_MAX_BG = '#FF8000'#'#F7BE81' -COLORS_VIEW_COUNTER_MAX_FG = Color.NewFromHtml(COLORS_VIEW_COUNTER_MAX_BG).DarkerColor(0.7).html -ANSWER_COUNTER_EXPECTED_MAXIMUM = 4 -COLORS_ANSWER_COUNTER_EMPTY_BG = Color.NewFromHtml('#a40000').Blend(Color.NewFromHtml('white'),0.8).html -COLORS_ANSWER_COUNTER_EMPTY_FG = 'yellow' -COLORS_ANSWER_COUNTER_MIN_BG = '#AEB404'#'#81F7F3'#'#A9D0F5'#'#045FB4' -COLORS_ANSWER_COUNTER_MIN_FG = 'white'#'#81F7F3' -COLORS_ANSWER_COUNTER_MAX_BG = Color.NewFromHtml('#61380B').Blend(Color.NewFromHtml('white'),0.75).html -COLORS_ANSWER_COUNTER_MAX_FG = '#ffff00' -COLORS_ANSWER_COUNTER_ACCEPTED_BG = Color.NewFromHtml('darkgreen').Blend(Color.NewFromHtml('white'),0.8).html -COLORS_ANSWER_COUNTER_ACCEPTED_FG = '#D0F5A9' +#Do not use these - will be phased out +USE_EXTERNAL_LEGACY_LOGIN = False +EXTERNAL_LEGACY_LOGIN_HOST = 'login.askbot.org' +EXTERNAL_LEGACY_LOGIN_PORT = 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = '<span class="orange">ASKBOT</span>' + +#SPHINX SETTINGS +USE_SPHINX_SEARCH = False #if True all SPHINX_* settings are required +#also sphinx search engine and djangosphinxs app must be installed +#sample sphinx configuration file is /sphinx/sphinx.conf +SPHINX_API_VERSION = 0x113 #refer to djangosphinx documentation +SPHINX_SEARCH_INDICES=('askbot',) #a tuple of index names remember about a comma after the +#last item, especially if you have just one :) +SPHINX_SERVER='localhost' +SPHINX_PORT=3312 @@ -1,5 +1,7 @@ -from django.conf.urls.defaults import * -from django.utils.translation import ugettext as _ +""" +main url configuration file for the askbot site +""" +from django.conf.urls.defaults import patterns, include, url from django.conf import settings from django.contrib import admin @@ -8,6 +10,7 @@ admin.autodiscover() urlpatterns = patterns('', (r'^%s' % settings.FORUM_SCRIPT_ALIAS, include('forum.urls')), (r'^admin/', include(admin.site.urls)), + (r'^settings/', include('livesettings.urls')), ) if 'rosetta' in settings.INSTALLED_APPS: |