diff options
76 files changed, 2431 insertions, 395 deletions
@@ -47,6 +47,15 @@ http://github.com/dcramer/django-sphinx/tree/master/djangosphinx 8. sphinx search engine (optional, works together with djangosphinx) http://sphinxsearch.com/downloads.html +9. recaptcha_django +http://code.google.com/p/recaptcha-django/ + +10. python recaptcha module +http://code.google.com/p/recaptcha/ +Notice that you will need to register with recaptcha.net and receive +recaptcha public and private keys that need to be saved in your +settings_local.py file + NOTES: django_authopenid is included into CNPROG code and is significantly modified. http://code.google.com/p/django-authopenid/ no need to install this library diff --git a/cnprog.wsgi b/cnprog.wsgi index bd3745ee..a1147de0 100644 --- a/cnprog.wsgi +++ b/cnprog.wsgi @@ -1,8 +1,7 @@ -#example wsgi setup script import os import sys -sys.path.append('/path/above_forum') -sys.path.append('/path/above_forum/forum_dir') +sys.path.append('/path/above/forum') +sys.path.append('/path/above/forum/forum_dir') os.environ['DJANGO_SETTINGS_MODULE'] = 'forum_dir.settings' import django.core.handlers.wsgi application = django.core.handlers.wsgi.WSGIHandler() diff --git a/cron/send_email_alerts b/cron/send_email_alerts index e9e433be..6358b599 100644 --- a/cron/send_email_alerts +++ b/cron/send_email_alerts @@ -1,4 +1,4 @@ -PYTHONPATH=/dir/just/above_forum +PYTHONPATH=/path/to/dir/above/forum export PYTHONPATH -APP_ROOT=$PYTHONPATH/CNPROG -/usr/local/bin/python $APP_ROOT/manage.py send_email_alerts >> $APP_ROOT/log/django.lanai.log +APP_ROOT=$PYTHONPATH/nmr-forum2 +/path/to/python $APP_ROOT/manage.py send_email_alerts diff --git a/django_authopenid/forms.py b/django_authopenid/forms.py index 6781401e..2fe6db74 100644 --- a/django_authopenid/forms.py +++ b/django_authopenid/forms.py @@ -35,11 +35,12 @@ 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 -import external_login import types import re from django.utils.safestring import mark_safe - +from recaptcha_django import ReCaptchaField +from utils.forms import NextUrlField, UserNameField, UserEmailField, SetPasswordForm +EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP() # needed for some linux distributions like debian try: @@ -47,7 +48,7 @@ try: except ImportError: from yadis import xri -from django_authopenid.util import clean_next +from utils.forms import clean_next from django_authopenid.models import ExternalLoginData __all__ = ['OpenidSigninForm', 'ClassicLoginForm', 'OpenidVerifyForm', @@ -55,102 +56,6 @@ __all__ = ['OpenidSigninForm', 'ClassicLoginForm', 'OpenidVerifyForm', 'ChangeEmailForm', 'EmailPasswordForm', 'DeleteForm', 'ChangeOpenidForm'] -class NextUrlField(forms.CharField): - def __init__(self): - super(NextUrlField,self).__init__(max_length = 255,widget = forms.HiddenInput(),required = False) - def clean(self,value): - return clean_next(value) - -attrs_dict = { 'class': 'required login' } - -class UserNameField(forms.CharField): - username_re = re.compile(r'^[\w ]+$') - RESERVED_NAMES = (u'fuck', u'shit', u'ass', u'sex', u'add', - u'edit', u'save', u'delete', u'manage', u'update', 'remove', 'new') - def __init__(self,must_exist=False,skip_clean=False,label=_('choose a username'),**kw): - self.must_exist = must_exist - self.skip_clean = skip_clean - super(UserNameField,self).__init__(max_length=30, - widget=forms.TextInput(attrs=attrs_dict), - label=label, - error_messages={'required':_('user name is required'), - 'taken':_('sorry, this name is taken, please choose another'), - 'forbidden':_('sorry, this name is not allowed, please choose another'), - 'missing':_('sorry, there is no user with this name'), - 'multiple-taken':_('sorry, we have a serious error - user name is taken by several users'), - 'invalid':_('user name can only consist of letters, empty space and underscore'), - }, - **kw - ) - - def clean(self,username): - """ validate username """ - username = super(UserNameField,self).clean(username.strip()) - if self.skip_clean == True: - return username - if hasattr(self, 'user_instance'): - if username == self.user_instance.username: - return username - if not username_re.search(username): - raise forms.ValidationError(self.error_messages['invalid']) - if username in self.RESERVED_NAMES: - raise forms.ValidationError(self.error_messages['forbidden']) - try: - user = User.objects.get( - username__exact = username - ) - if user: - if self.must_exist: - return username - else: - raise forms.ValidationError(self.error_messages['taken']) - except User.DoesNotExist: - if self.must_exist: - raise forms.ValidationError(self.error_messages['missing']) - else: - return username - except User.MultipleObjectsReturned: - raise forms.ValidationError(self.error_messages['multiple-taken']) - -class UserEmailField(forms.EmailField): - def __init__(self,skip_clean=False,**kw): - self.skip_clean = skip_clean - super(UserEmailField,self).__init__(widget=forms.TextInput(attrs=dict(attrs_dict, - maxlength=200)), label=mark_safe(_('your email address')), - error_messages={'required':_('email address is required'), - 'invalid':_('please enter a valid email address'), - 'taken':_('this email is already used by someone else, please choose another'), - }, - **kw - ) - - def clean(self,email): - """ validate if email exist in database - from legacy register - return: raise error if it exist """ - email = super(UserEmailField,self).clean(email.strip()) - if self.skip_clean: - return email - if settings.EMAIL_UNIQUE == True: - try: - user = User.objects.get(email = email) - raise forms.ValidationError(self.error_messsages['taken']) - except User.DoesNotExist: - return email - except User.MultipleObjectsReturned: - raise forms.ValidationError(self.error_messages['taken']) - else: - return email - -def clean_nonempty_field_method(self,field): - value = None - if field in self.cleaned_data: - value = str(self.cleaned_data[field]).strip() - if value == '': - value = None - self.cleaned_data[field] = value - return value - class OpenidSigninForm(forms.Form): """ signin form """ openid_url = forms.CharField(max_length=255, widget=forms.widgets.TextInput(attrs={'class': 'openid-login-input', 'size':80})) @@ -171,7 +76,8 @@ class ClassicLoginForm(forms.Form): next = NextUrlField() username = UserNameField(required=False,skip_clean=True) password = forms.CharField(max_length=128, - widget=forms.widgets.PasswordInput(attrs=attrs_dict), required=False) + widget=forms.widgets.PasswordInput(attrs={'class':'required login'}), + required=False) def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None): @@ -179,13 +85,21 @@ class ClassicLoginForm(forms.Form): prefix, initial) self.user_cache = None - clean_nonempty_field = clean_nonempty_field_method + def _clean_nonempty_field(self,field): + value = None + if field in self.cleaned_data: + value = str(self.cleaned_data[field]).strip() + if value == '': + value = None + self.cleaned_data[field] = value + return value + def clean_username(self): - return self.clean_nonempty_field('username') + return self._clean_nonempty_field('username') def clean_password(self): - return self.clean_nonempty_field('password') + return self._clean_nonempty_field('password') def clean(self): """ @@ -208,7 +122,7 @@ class ClassicLoginForm(forms.Form): if settings.USE_EXTERNAL_LEGACY_LOGIN == True: pw_ok = False try: - pw_ok = external_login.check_password(username,password) + pw_ok = EXTERNAL_LOGIN_APP.api.check_password(username,password) except forms.ValidationError, e: error_list.extend(e.messages) if pw_ok: @@ -274,7 +188,7 @@ class OpenidVerifyForm(forms.Form): next = NextUrlField() username = UserNameField(must_exist=True) password = forms.CharField(max_length=128, - widget=forms.widgets.PasswordInput(attrs=attrs_dict)) + widget=forms.widgets.PasswordInput(attrs={'class':'required login'})) def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None): @@ -302,53 +216,19 @@ class OpenidVerifyForm(forms.Form): """ get authenticated user """ return self.user_cache - -attrs_dict = { 'class': 'required' } -username_re = re.compile(r'^[\w ]+$') - -class ClassicRegisterForm(forms.Form): +class ClassicRegisterForm(SetPasswordForm): """ legacy registration form """ next = NextUrlField() username = UserNameField() email = UserEmailField() - password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), - label=_('choose password'), - error_messages={'required':_('password is required')}, - ) - password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), - label=mark_safe(_('retype password')), - error_messages={'required':_('please, retype your password'), - 'nomatch':_('sorry, entered passwords did not match, please try again')}, - required=False - ) - - def clean_password2(self): - """ - Validates that the two password inputs match. - - """ - self.cleaned_data['password2'] = self.cleaned_data.get('password2','') - if self.cleaned_data['password2'] == '': - del self.cleaned_data['password2'] - raise forms.ValidationError(self.fields['password2'].error_messages['required']) - if 'password1' in self.cleaned_data \ - and self.cleaned_data['password1'] == \ - self.cleaned_data['password2']: - return self.cleaned_data['password2'] - else: - del self.cleaned_data['password2'] - del self.cleaned_data['password1'] - raise forms.ValidationError(self.fields['password2'].error_messages['nomatch']) - -class ChangePasswordForm(forms.Form): + #fields password1 and password2 are inherited + recaptcha = ReCaptchaField() + +class ChangePasswordForm(SetPasswordForm): """ change password form """ - oldpw = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), + oldpw = forms.CharField(widget=forms.PasswordInput(attrs={'class':'required'}), label=mark_safe(_('Current password'))) - password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), - label=mark_safe(_('New password'))) - password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), - label=mark_safe(_('Retype new password'))) def __init__(self, data=None, user=None, *args, **kwargs): if user is None: @@ -362,17 +242,6 @@ class ChangePasswordForm(forms.Form): raise forms.ValidationError(_("Old password is incorrect. \ Please enter the correct password.")) return self.cleaned_data['oldpw'] - - def clean_password2(self): - """ - Validates that the two password inputs match. - """ - if 'password1' in self.cleaned_data and \ - 'password2' in self.cleaned_data and \ - self.cleaned_data['password1'] == self.cleaned_data['password2']: - return self.cleaned_data['password2'] - raise forms.ValidationError(_("new passwords do not match")) - class ChangeEmailForm(forms.Form): """ change email form """ @@ -416,8 +285,8 @@ class ChangeopenidForm(forms.Form): class DeleteForm(forms.Form): """ confirm form to delete an account """ - confirm = forms.CharField(widget=forms.CheckboxInput(attrs=attrs_dict)) - password = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + confirm = forms.CharField(widget=forms.CheckboxInput(attrs={'class':'required'})) + password = forms.CharField(widget=forms.PasswordInput(attrs={'class':'required'})) def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, user=None): diff --git a/django_authopenid/urls.py b/django_authopenid/urls.py index 112cbbe1..e1986d19 100755 --- a/django_authopenid/urls.py +++ b/django_authopenid/urls.py @@ -1,7 +1,21 @@ # -*- coding: utf-8 -*- from django.conf.urls.defaults import patterns, url from django.utils.translation import ugettext as _ +from django.conf import settings +#print 'stuff to import %s' % settings.EXTERNAL_LOGIN_APP.__name__ + '.views' +#try: +# settings.EXTERNAL_LOGIN_APP = __import__('mediawiki.views') +#print 'stuff to import %s' % settings.EXTERNAL_LOGIN_APP.__name__ + '.views' +#try: +# print 'imported fine' +# print settings.EXTERNAL_LOGIN_APP.__dict__.keys() +#except: +# print 'dammit!' +#from mediawiki.views import signup_view +#settings.EXTERNAL_LOGIN_APP.views.signup_view() + +#print settings.EXTERNAL_LOGIN_APP.__dict__.keys() urlpatterns = patterns('django_authopenid.views', # yadis rdf url(r'^yadis.xrdf$', 'xrdf', name='yadis_xrdf'), @@ -12,7 +26,6 @@ urlpatterns = patterns('django_authopenid.views', url(r'^%s$' % _('signout/'), 'signout', name='user_signout'), url(r'^%s%s$' % (_('signin/'), _('complete/')), 'complete_signin', name='user_complete_signin'), - url('^%s$' % _('external-login/'),'external_legacy_login_info', name='user_external_legacy_login_issues'), url(r'^%s$' % _('register/'), 'register', name='user_register'), url(r'^%s$' % _('signup/'), 'signup', name='user_signup'), #disable current sendpw function @@ -29,3 +42,21 @@ urlpatterns = patterns('django_authopenid.views', url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), url(r'^%s$' % _('delete/'), 'delete', name='user_delete'), ) + +#todo move these out of this file completely +if settings.USE_EXTERNAL_LEGACY_LOGIN: + from forum.forms import NotARobotForm + EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP() + urlpatterns += patterns('', + url('^%s$' % _('external-login/forgot-password/'),\ + 'django_authopenid.views.external_legacy_login_info', \ + name='user_external_legacy_login_issues'), + url('^%s$' % _('external-login/signup/'), \ + EXTERNAL_LOGIN_APP.views.signup,\ + name='user_external_legacy_login_signup'), +# url('^%s$' % _('external-login/signup/'), \ +# EXTERNAL_LOGIN_APP.forms.RegisterFormWizard( \ +# [EXTERNAL_LOGIN_APP.forms.RegisterForm, \ +# NotARobotForm]),\ +# name='user_external_legacy_login_signup'), + ) diff --git a/django_authopenid/util.py b/django_authopenid/util.py index edb6808e..165756e0 100644 --- a/django_authopenid/util.py +++ b/django_authopenid/util.py @@ -6,7 +6,6 @@ import openid.store from django.db.models.query import Q from django.conf import settings -from django.http import str_to_unicode from django.core.urlresolvers import reverse # needed for some linux distributions like debian @@ -16,25 +15,12 @@ except: from yadis import xri import time, base64, hashlib, operator -import urllib +from utils.forms import clean_next, get_next_url from models import Association, Nonce __all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next'] -DEFAULT_NEXT = '/' + getattr(settings, 'FORUM_SCRIPT_ALIAS') -def clean_next(next): - if next is None: - return DEFAULT_NEXT - next = str_to_unicode(urllib.unquote(next), 'utf-8') - next = next.strip() - if next.startswith('/'): - return next - return DEFAULT_NEXT - -def get_next_url(request): - return clean_next(request.REQUEST.get('next')) - class OpenID: def __init__(self, openid_, issued, attrs=None, sreg_=None): self.openid = openid_ diff --git a/django_authopenid/views.py b/django_authopenid/views.py index d087d215..b09dea7c 100755 --- a/django_authopenid/views.py +++ b/django_authopenid/views.py @@ -32,7 +32,7 @@ from django.http import HttpResponseRedirect, get_host, Http404, \ HttpResponseServerError -from django.shortcuts import render_to_response as render +from django.shortcuts import render_to_response from django.template import RequestContext, loader, Context from django.conf import settings from django.contrib.auth.models import User @@ -61,23 +61,23 @@ import re import urllib -from forum.forms import EditUserEmailFeedsForm -from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, get_next_url +from forum.forms import SimpleEmailSubscribeForm +from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response from django_authopenid.models import UserAssociation, UserPasswordQueue, ExternalLoginData from django_authopenid.forms import OpenidSigninForm, ClassicLoginForm, OpenidRegisterForm, \ OpenidVerifyForm, ClassicRegisterForm, ChangePasswordForm, ChangeEmailForm, \ ChangeopenidForm, DeleteForm, EmailPasswordForm -import external_login import logging +from utils.forms import get_next_url + +EXTERNAL_LOGIN_APP = settings.LOAD_EXTERNAL_LOGIN_APP() def login(request,user): from django.contrib.auth import login as _login from forum.models import user_logged_in #custom signal - print 'in login call' - if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - external_login.login(request,user) + EXTERNAL_LOGIN_APP.api.login(request,user) #1) get old session key session_key = request.session.session_key @@ -90,7 +90,7 @@ def logout(request): from django.contrib.auth import logout as _logout#for login I've added wrapper below - called login _logout(request) if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - external_login.logout(request) + EXTERNAL_LOGIN_APP.api.logout(request) def get_url_host(request): if request.is_secure(): @@ -158,7 +158,7 @@ def default_on_success(request, identity_url, openid_response): def default_on_failure(request, message): """ default failure action on signin """ - return render('openid_failure.html', { + return render_to_response('openid_failure.html', { 'message': message }) @@ -184,7 +184,7 @@ def signin(request,newquestion=False,newanswer=False): """ request.encoding = 'UTF-8' on_failure = signin_failure - email_feeds_form = EditUserEmailFeedsForm() + email_feeds_form = SimpleEmailSubscribeForm() next = get_next_url(request) form_signin = OpenidSigninForm(initial={'next':next}) form_auth = ClassicLoginForm(initial={'next':next}) @@ -206,35 +206,31 @@ def signin(request,newquestion=False,newanswer=False): request.session['external_username'] = username request.session['external_password'] = password - #2) see if username clashes with some existing user + #2) try to extract user email and nickname from external service + email = EXTERNAL_LOGIN_APP.api.get_email(username,password) + screen_name = EXTERNAL_LOGIN_APP.api.get_screen_name(username,password) + + #3) see if username clashes with some existing user #if so, we have to prompt the user to pick a different name - username_taken = User.is_username_taken(username) - #try: - # User.objects.get(username=username) - # username_taken = True - #except User.DoesNotExist: - # username_taken = False - - #3) try to extract user email from external service - email = external_login.get_email(username,password) - - email_feeds_form = EditUserEmailFeedsForm() - form_data = {'username':username,'email':email,'next':next} + username_taken = User.is_username_taken(screen_name) + + email_feeds_form = SimpleEmailSubscribeForm() + form_data = {'username':screen_name,'email':email,'next':next} form = OpenidRegisterForm(initial=form_data) - template_data = {'form1':form,'username':username,\ + template_data = {'form1':form,'username':screen_name,\ 'email_feeds_form':email_feeds_form,\ 'provider':mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME),\ 'login_type':'legacy',\ 'gravatar_faq_url':reverse('faq') + '#gravatar',\ 'external_login_name_is_taken':username_taken} - return render('authopenid/complete.html',template_data,\ + return render_to_response('authopenid/complete.html',template_data,\ context_instance=RequestContext(request)) else: #user existed, external password is ok user = form_auth.get_user() login(request,user) response = HttpResponseRedirect(get_next_url(request)) - external_login.set_login_cookies(response,user) + EXTERNAL_LOGIN_APP.api.set_login_cookies(response,user) return response else: #regular password authentication @@ -246,7 +242,7 @@ def signin(request,newquestion=False,newanswer=False): #register externally logged in password user with a new local account if settings.USE_EXTERNAL_LEGACY_LOGIN == True: form = OpenidRegisterForm(request.POST) - email_feeds_form = EditUserEmailFeedsForm(request.POST) + email_feeds_form = SimpleEmailSubscribeForm(request.POST) form1_is_valid = form.is_valid() form2_is_valid = email_feeds_form.is_valid() if form1_is_valid and form2_is_valid: @@ -254,10 +250,10 @@ def signin(request,newquestion=False,newanswer=False): username = form.cleaned_data['username'] password = request.session.get('external_password',None) email = form.cleaned_data['email'] - print 'got email addr %s' % email if password and username: User.objects.create_user(username,email,password) user = authenticate(username=username,password=password) + EXTERNAL_LOGIN_APP.api.connect_local_user_to_external_user(user,username,password) external_username = request.session['external_username'] eld = ExternalLoginData.objects.get(external_username=external_username) eld.user = user @@ -266,7 +262,9 @@ def signin(request,newquestion=False,newanswer=False): email_feeds_form.save(user) del request.session['external_username'] del request.session['external_password'] - return HttpResponseRedirect(reverse('index')) + response = HttpResponseRedirect(reverse('index')) + EXTERNAL_LOGIN_APP.api.set_login_cookies(response, user) + return response else: if password: del request.session['external_username'] @@ -281,7 +279,7 @@ def signin(request,newquestion=False,newanswer=False): 'email_feeds_form':email_feeds_form,'provider':provider,\ 'gravatar_faq_url':reverse('faq') + '#gravatar',\ 'external_login_name_is_taken':username_taken} - return render('authopenid/complete.html',data, + return render_to_response('authopenid/complete.html',data, context_instance=RequestContext(request)) else: raise Http404 @@ -319,7 +317,7 @@ def signin(request,newquestion=False,newanswer=False): if len(alist) > 0: answer = alist[0] - return render('authopenid/signin.html', { + return render_to_response('authopenid/signin.html', { 'question':question, 'answer':answer, 'form1': form_auth, @@ -400,14 +398,14 @@ def register(request): 'next': next, 'username': nickname, }) - email_feeds_form = EditUserEmailFeedsForm() + email_feeds_form = SimpleEmailSubscribeForm() user_ = None is_redirect = False if request.POST: if 'bnewaccount' in request.POST.keys(): form1 = OpenidRegisterForm(request.POST) - email_feeds_form = EditUserEmailFeedsForm(request.POST) + email_feeds_form = SimpleEmailSubscribeForm(request.POST) if form1.is_valid() and email_feeds_form.is_valid(): next = form1.cleaned_data['next'] is_redirect = True @@ -469,7 +467,7 @@ def register(request): else: provider_logo = providers[provider_name] - return render('authopenid/complete.html', { + return render_to_response('authopenid/complete.html', { 'form1': form1, 'form2': form2, 'email_feeds_form': email_feeds_form, @@ -490,7 +488,7 @@ def signin_failure(request, message): form_signin = OpenidSigninForm(initial={'next': next}) form_auth = ClassicLoginForm(initial={'next': next}) - return render('authopenid/signin.html', { + return render_to_response('authopenid/signin.html', { 'msg': message, 'form1': form_auth, 'form2': form_signin, @@ -506,11 +504,11 @@ def signup(request): templates: authopenid/signup.html, authopenid/confirm_email.txt """ if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) + return HttpResponseRedirect(reverse('user_external_legacy_login_signup')) next = get_next_url(request) if request.POST: form = ClassicRegisterForm(request.POST) - email_feeds_form = EditUserEmailFeedsForm(request.POST) + email_feeds_form = SimpleEmailSubscribeForm(request.POST) #validation outside if to remember form values form1_is_valid = form.is_valid() @@ -523,7 +521,7 @@ def signup(request): user_ = User.objects.create_user( username,email,password ) if settings.USE_EXTERNAL_LEGACY_LOGIN == True: - external_login.create_user(username,email,password) + EXTERNAL_LOGIN_APP.api.create_user(username,email,password) user_.backend = "django.contrib.auth.backends.ModelBackend" login(request, user_) @@ -545,8 +543,8 @@ def signup(request): return HttpResponseRedirect(next) else: form = ClassicRegisterForm(initial={'next':next}) - email_feeds_form = EditUserEmailFeedsForm() - return render('authopenid/signup.html', { + email_feeds_form = SimpleEmailSubscribeForm() + return render_to_response('authopenid/signup.html', { 'form': form, 'email_feeds_form': email_feeds_form }, context_instance=RequestContext(request)) @@ -571,7 +569,7 @@ def xrdf(request): return_to = [ "%s%s" % (url_host, reverse('user_complete_signin')) ] - return render('authopenid/yadis.xrdf', { + return render_to_response('authopenid/yadis.xrdf', { 'return_to': return_to }, context_instance=RequestContext(request)) @@ -599,7 +597,7 @@ def account_settings(request): is_openid = False - return render('authopenid/settings.html', { + return render_to_response('authopenid/settings.html', { 'msg': msg, 'is_openid': is_openid }, context_instance=RequestContext(request)) @@ -633,7 +631,7 @@ def changepw(request): else: form = ChangePasswordForm(user=user_) - return render('authopenid/changepw.html', {'form': form }, + return render_to_response('authopenid/changepw.html', {'form': form }, context_instance=RequestContext(request)) def find_email_validation_messages(user): @@ -697,7 +695,7 @@ def send_email_key(request): if settings.EMAIL_VALIDATION != 'off': if request.user.email_isvalid: - return render('authopenid/changeemail.html', + return render_to_response('authopenid/changeemail.html', { 'email': request.user.email, 'action_type': 'key_not_sent', 'change_link': reverse('user_changeemail')}, @@ -712,7 +710,7 @@ def send_email_key(request): #internal server view used as return value by other views def validation_email_sent(request): - return render('authopenid/changeemail.html', + return render_to_response('authopenid/changeemail.html', { 'email': request.user.email, 'change_email_url': reverse('user_changeemail'), 'action_type': 'validate', }, @@ -730,7 +728,7 @@ def verifyemail(request,id=None,key=None): user.email_isvalid = True clear_email_validation_message(user) user.save() - return render('authopenid/changeemail.html', { + return render_to_response('authopenid/changeemail.html', { 'action_type': 'validation_complete', }, context_instance=RequestContext(request)) raise Http404 @@ -773,7 +771,7 @@ def changeemail(request, action='change'): form = ChangeEmailForm(initial={'email': user_.email}, user=user_) - output = render('authopenid/changeemail.html', { + output = render_to_response('authopenid/changeemail.html', { 'form': form, 'email': user_.email, 'action_type': action, @@ -857,7 +855,7 @@ def changeopenid(request): changeopenid_failure, redirect_to) form = ChangeopenidForm(initial={'openid_url': openid_url }, user=user_) - return render('authopenid/changeopenid.html', { + return render_to_response('authopenid/changeopenid.html', { 'form': form, 'has_openid': has_openid, 'msg': msg @@ -934,7 +932,7 @@ def delete(request): form = DeleteForm(user=user_) msg = request.GET.get('msg','') - return render('authopenid/delete.html', { + return render_to_response('authopenid/delete.html', { 'form': form, 'msg': msg, }, context_instance=RequestContext(request)) @@ -969,7 +967,10 @@ def deleteopenid_failure(request, message): return HttpResponseRedirect(redirect_to) def external_legacy_login_info(request): - return render('authopenid/external_legacy_login_info.html', context_instance=RequestContext(request)) + feedback_url = reverse('feedback') + return render_to_response('authopenid/external_legacy_login_info.html', + {'feedback_url':feedback_url}, + context_instance=RequestContext(request)) def sendpw(request): """ @@ -1019,7 +1020,7 @@ def sendpw(request): else: form = EmailPasswordForm() - return render('authopenid/sendpw.html', { + return render_to_response('authopenid/sendpw.html', { 'form': form, 'msg': msg }, context_instance=RequestContext(request)) diff --git a/dos2unix.sh b/dos2unix.sh index 96a51c9d..2864426a 100644 --- a/dos2unix.sh +++ b/dos2unix.sh @@ -1,3 +1,5 @@ +#please take care not to dos2unix anything in your .git directory +#because that will probably break your repo dos2unix `find . -name '*.py'` dos2unix `find . -name '*.po'` dos2unix `find . -name '*.js'` diff --git a/drop-auth.sql b/drop-auth.sql new file mode 100644 index 00000000..bc17dce3 --- /dev/null +++ b/drop-auth.sql @@ -0,0 +1,8 @@ +drop table auth_group; +drop table auth_group_permissions; +drop table auth_message; +drop table auth_permission; +drop table auth_user; +drop table auth_user_groups; +drop table auth_user_user_permissions; + diff --git a/forum/feed.py b/forum/feed.py index ad1d5cbd..e4b929e9 100644 --- a/forum/feed.py +++ b/forum/feed.py @@ -13,16 +13,16 @@ from django.contrib.syndication.feeds import Feed, FeedDoesNotExist from django.utils.translation import ugettext as _ from models import Question -import settings +from django.conf import settings class RssLastestQuestionsFeed(Feed): title = settings.APP_TITLE + _(' - ')+ _('latest questions') - link = settings.APP_URL + '/' + _('question/') + link = settings.APP_URL #+ '/' + _('question/') description = settings.APP_DESCRIPTION #ttl = 10 copyright = settings.APP_COPYRIGHT def item_link(self, item): - return self.link + '%s/' % item.id + return self.link + item.get_absolute_url() def item_author_name(self, item): return item.author.username diff --git a/forum/forms.py b/forum/forms.py index 2d2021b5..42becc11 100644 --- a/forum/forms.py +++ b/forum/forms.py @@ -4,8 +4,10 @@ from django import forms from models import * from const import * from django.utils.translation import ugettext as _ -from django_authopenid.forms import NextUrlField, UserNameField -import settings +from utils.forms import NextUrlField, UserNameField +from recaptcha_django import ReCaptchaField +from django.conf import settings +import logging class TitleField(forms.CharField): def __init__(self, *args, **kwargs): @@ -109,6 +111,9 @@ class ModerateUserForm(forms.ModelForm): model = User fields = ('is_approved',) +class NotARobotForm(forms.Form): + recaptcha = ReCaptchaField() + class FeedbackForm(forms.Form): name = forms.CharField(label=_('Your name:'), required=False) email = forms.EmailField(label=_('Email (not shared with anyone):'), required=False) @@ -204,8 +209,8 @@ class EditUserForm(forms.Form): def __init__(self, user, *args, **kwargs): super(EditUserForm, self).__init__(*args, **kwargs) - #self.fields['username'].initial = user.username - #self.fields['username'].user_instance = user + self.fields['username'].initial = user.username + self.fields['username'].user_instance = user self.fields['email'].initial = user.email self.fields['realname'].initial = user.real_name self.fields['website'].initial = user.website @@ -299,14 +304,24 @@ class EditUserEmailFeedsForm(forms.Form): self.initial = self.NO_EMAIL_INITIAL return self - def save(self,user): + def save(self,user,save_unbound=False): + """ + with save_unbound==True will bypass form validation and save initial values + """ changed = False for form_field, feed_type in self.FORM_TO_MODEL_MAP.items(): s, created = EmailFeedSetting.objects.get_or_create(subscriber=user,\ feed_type=feed_type) - new_value = self.cleaned_data[form_field] + if save_unbound: + #just save initial values instead + if form_field in self.initial: + new_value = self.initial[form_field] + else: + new_value = self.fields[form_field].initial + else: + new_value = self.cleaned_data[form_field] if s.frequency != new_value: - s.frequency = self.cleaned_data[form_field] + s.frequency = new_value s.save() changed = True else: @@ -316,3 +331,22 @@ class EditUserEmailFeedsForm(forms.Form): feed_type = ContentType.objects.get_for_model(Question) user.followed_questions.clear() return changed + + +class SimpleEmailSubscribeForm(forms.Form): + SIMPLE_SUBSCRIBE_CHOICES = ( + ('y',_('okay, let\'s try!')), + ('n',_('no OSQA community email please, thanks')) + ) + subscribe = forms.ChoiceField(widget=forms.widgets.RadioSelect(), \ + error_messages={'required':_('please choose one of the options above')}, + choices=SIMPLE_SUBSCRIBE_CHOICES) + + def save(self,user=None): + EFF = EditUserEmailFeedsForm + if self.cleaned_data['subscribe'] == 'y': + email_settings_form = EFF() + logging.debug('%s wants to subscribe' % user.username) + else: + email_settings_form = EFF(initial=EFF.NO_EMAIL_INITIAL) + email_settings_form.save(user,save_unbound=True) diff --git a/forum/management/commands/message_to_everyone.py b/forum/management/commands/message_to_everyone.py new file mode 100644 index 00000000..c020c178 --- /dev/null +++ b/forum/management/commands/message_to_everyone.py @@ -0,0 +1,12 @@ +from django.core.management.base import NoArgsCommand +from django.contrib.auth.models import User +import sys + +class Command(NoArgsCommand): + def handle_noargs(self, **options): + msg = None + if msg == None: + print 'to run this command, please first edit the file %s' % __file__ + sys.exit(1) + for u in User.objects.all(): + u.message_set.create(message = msg % u.username) diff --git a/forum/management/commands/multi_award_badges.py b/forum/management/commands/multi_award_badges.py index c6dbc250..6b330cf9 100644 --- a/forum/management/commands/multi_award_badges.py +++ b/forum/management/commands/multi_award_badges.py @@ -345,4 +345,4 @@ class Command(BaseCommand): award = Award(user=user, badge=badge, content_type=content_type, object_id=object_id) award.save() finally: - cursor.close()
\ No newline at end of file + cursor.close() diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py index f5974e6b..62f13d69 100644 --- a/forum/management/commands/send_email_alerts.py +++ b/forum/management/commands/send_email_alerts.py @@ -7,7 +7,7 @@ from django.core.mail import EmailMessage from django.utils.translation import ugettext as _ from django.utils.translation import ungettext import datetime -import settings +from django.conf import settings import logging from utils.odict import OrderedDict @@ -58,10 +58,10 @@ class Command(NoArgsCommand): q_ans.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) + ignored_tags = Tag.objects.filter(user_selections__reason='bad',user_selections__user=user) q_all = Q_set.exclude( tags__in=ignored_tags ) else: - selected_tags = Tag.objects.filter(user_selections___reason='good',user_selections__user=user) + selected_tags = Tag.objects.filter(user_selections__reason='good',user_selections__user=user) q_all = Q_set.filter( tags__in=selected_tags ) q_all.cutoff_time = cutoff_time #build list in this order @@ -154,6 +154,7 @@ class Command(NoArgsCommand): if num_q > 0: url_prefix = 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 a question', '%(name)s, this is an update message header for %(num)d questions',num_q) \ % {'num':num_q, 'name':user.username} diff --git a/forum/management/commands/subscribe_everyone.py b/forum/management/commands/subscribe_everyone.py index f5cbf8eb..c79528f3 100644 --- a/forum/management/commands/subscribe_everyone.py +++ b/forum/management/commands/subscribe_everyone.py @@ -6,7 +6,7 @@ from django.core.mail import EmailMessage from django.utils.translation import ugettext as _ from django.utils.translation import ungettext import datetime -import settings +from django.conf import settings class Command(NoArgsCommand): def handle_noargs(self,**options): diff --git a/forum/models.py b/forum/models.py index c3b89ce9..a2988be4 100644 --- a/forum/models.py +++ b/forum/models.py @@ -15,7 +15,7 @@ from django.utils.translation import ugettext as _ from django.utils.safestring import mark_safe from django.contrib.sitemaps import ping_google import django.dispatch -import settings +from django.conf import settings import logging if settings.USE_SPHINX_SEARCH == True: @@ -831,7 +831,7 @@ def notify_award_message(instance, created, **kwargs): """ if created: user = instance.user - user.message_set.create(message=u"%s" % instance.badge.name) + user.message_set.create(message=u"Congratulations, you have received a badge '%s'" % instance.badge.name) def record_answer_accepted(instance, created, **kwargs): """ diff --git a/forum/sitemap.py b/forum/sitemap.py index dc97a009..c0c60b5e 100644 --- a/forum/sitemap.py +++ b/forum/sitemap.py @@ -9,3 +9,6 @@ class QuestionsSitemap(Sitemap): def lastmod(self, obj): return obj.last_activity_at + + def location(self, obj): + return obj.get_absolute_url() diff --git a/forum/urls.py b/forum/urls.py index f7d6eba5..42746d44 100644 --- a/forum/urls.py +++ b/forum/urls.py @@ -54,7 +54,7 @@ urlpatterns = patterns('', app.delete_comment, kwargs={'commented_object_type':'answer'}, \ name='delete_answer_comment'), \ #place general question item in the end of other operations - url(r'^%s(?P<id>\d+)//*' % _('question/'), app.question, name='question'), + url(r'^%s(?P<id>\d+)/' % _('question/'), app.question, name='question'), url(r'^%s$' % _('tags/'), app.tags, name='tags'), url(r'^%s(?P<tag>[^/]+)/$' % _('tags/'), app.tag, name='tag_questions'), diff --git a/forum/views.py b/forum/views.py index c4514912..bad37693 100644 --- a/forum/views.py +++ b/forum/views.py @@ -33,7 +33,7 @@ from forum.auth import * from forum.const import * from forum.user import * from forum import auth -from django_authopenid.util import get_next_url +from utils.forms import get_next_url # used in index page INDEX_PAGE_SIZE = 20 @@ -436,6 +436,21 @@ def question(request, id): logging.debug('view_id=' + str(view_id)) question = get_object_or_404(Question, id=id) + try: + pattern = r'/%s%s%d/([\w-]+)' % (settings.FORUM_SCRIPT_ALIAS,_('question/'), question.id) + path_re = re.compile(pattern) + logging.debug(pattern) + logging.debug(request.path) + m = path_re.match(request.path) + if m: + slug = m.group(1) + logging.debug('have slug %s' % slug) + assert(slug == slugify(question.title)) + else: + logging.debug('no match!') + except: + return HttpResponseRedirect(question.get_absolute_url()) + if question.deleted and not can_view_deleted_post(request.user, question): raise Http404 answer_form = AnswerForm(question,request.user) diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo Binary files differindex 16f3554d..ef3007a0 100644 --- a/locale/en/LC_MESSAGES/django.mo +++ b/locale/en/LC_MESSAGES/django.mo diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po index 15385964..ee40fa36 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -175,7 +175,7 @@ msgstr "" #: django_authopenid/urls.py:15 msgid "external-login/" -msgstr "using-nmr-wiki-login-and-password/" +msgstr "" #: django_authopenid/urls.py:16 msgid "register/" diff --git a/mediawiki/PHPSerialize.py b/mediawiki/PHPSerialize.py new file mode 100644 index 00000000..d25b71bd --- /dev/null +++ b/mediawiki/PHPSerialize.py @@ -0,0 +1,149 @@ +import types, string + +""" +Serialize class for the PHP serialization format. + +@version v0.4 BETA +@author Scott Hurring; scott at hurring dot com +@copyright Copyright (c) 2005 Scott Hurring +@license http://opensource.org/licenses/gpl-license.php GNU Public License +$Id: PHPSerialize.py,v 1.1 2006/01/08 21:53:19 shurring Exp $ + +Most recent version can be found at: +http://hurring.com/code/python/phpserialize/ + +Usage: +# Create an instance of the serialize engine +s = PHPSerialize() +# serialize some python data into a string +serialized_string = s.serialize(string) +# encode a session list (php's session_encode) +serialized_string = s.session_encode(list) + +See README.txt for more information. +""" + +class PHPSerialize(object): + """ + Class to serialize data using the PHP Serialize format. + + Usage: + serialized_string = PHPSerialize().serialize(data) + serialized_string = PHPSerialize().session_encode(list) + """ + + def __init__(self): + pass + + def session_encode(self, session): + """Thanks to Ken Restivo for suggesting the addition + of session_encode + """ + out = "" + for (k,v) in session.items(): + out = out + "%s|%s" % (k, self.serialize(v)) + return out + + def serialize(self, data): + return self.serialize_value(data) + + def is_int(self, data): + """ + Determine if a string var looks like an integer + TODO: Make this do what PHP does, instead of a hack + """ + try: + int(data) + return True + except: + return False + + def serialize_key(self, data): + """ + Serialize a key, which follows different rules than when + serializing values. Many thanks to Todd DeLuca for pointing + out that keys are serialized differently than values! + + From http://us2.php.net/manual/en/language.types.array.php + A key may be either an integer or a string. + If a key is the standard representation of an integer, it will be + interpreted as such (i.e. "8" will be interpreted as int 8, + while "08" will be interpreted as "08"). + Floats in key are truncated to integer. + """ + # Integer, Long, Float, Boolean => integer + if type(data) is types.IntType or type(data) is types.LongType \ + or type(data) is types.FloatType or type(data) is types.BooleanType: + return "i:%s;" % int(data) + + # String => string or String => int (if string looks like int) + elif type(data) is types.StringType: + if self.is_int(data): + return "i:%s;" % int(data) + else: + return "s:%i:\"%s\";" % (len(data), data); + + # None / NULL => empty string + elif type(data) is types.NoneType: + return "s:0:\"\";" + + # I dont know how to serialize this + else: + raise Exception("Unknown / Unhandled key type (%s)!" % type(data)) + + + def serialize_value(self, data): + """ + Serialize a value. + """ + + # Integer => integer + if type(data) is types.IntType: + return "i:%s;" % data + + # Float, Long => double + elif type(data) is types.FloatType or type(data) is types.LongType: + return "d:%s;" % data + + # String => string or String => int (if string looks like int) + # Thanks to Todd DeLuca for noticing that PHP strings that + # look like integers are serialized as ints by PHP + elif type(data) is types.StringType: + if self.is_int(data): + return "i:%s;" % int(data) + else: + return "s:%i:\"%s\";" % (len(data), data); + + # None / NULL + elif type(data) is types.NoneType: + return "N;"; + + # Tuple and List => array + # The 'a' array type is the only kind of list supported by PHP. + # array keys are automagically numbered up from 0 + elif type(data) is types.ListType or type(data) is types.TupleType: + i = 0 + out = [] + # All arrays must have keys + for k in data: + out.append(self.serialize_key(i)) + out.append(self.serialize_value(k)) + i += 1 + return "a:%i:{%s}" % (len(data), "".join(out)) + + # Dict => array + # Dict is the Python analogy of a PHP array + elif type(data) is types.DictType: + out = [] + for k in data: + out.append(self.serialize_key(k)) + out.append(self.serialize_value(data[k])) + return "a:%i:{%s}" % (len(data), "".join(out)) + + # Boolean => bool + elif type(data) is types.BooleanType: + return "b:%i;" % (data == 1) + + # I dont know how to serialize this + else: + raise Exception("Unknown / Unhandled data type (%s)!" % type(data)) diff --git a/mediawiki/PHPUnserialize.py b/mediawiki/PHPUnserialize.py new file mode 100644 index 00000000..b59c869c --- /dev/null +++ b/mediawiki/PHPUnserialize.py @@ -0,0 +1,187 @@ +import types, string, re + +""" +Unserialize class for the PHP serialization format. + +@version v0.4 BETA +@author Scott Hurring; scott at hurring dot com +@copyright Copyright (c) 2005 Scott Hurring +@license http://opensource.org/licenses/gpl-license.php GNU Public License +$Id: PHPUnserialize.py,v 1.1 2006/01/08 21:53:19 shurring Exp $ + +Most recent version can be found at: +http://hurring.com/code/python/phpserialize/ + +Usage: +# Create an instance of the unserialize engine +u = PHPUnserialize() +# unserialize some string into python data +data = u.unserialize(serialized_string) + +Please see README.txt for more information. +""" + +class PHPUnserialize(object): + """ + Class to unserialize something from the PHP Serialize format. + + Usage: + u = PHPUnserialize() + data = u.unserialize(serialized_string) + """ + + def __init__(self): + pass + + def session_decode(self, data): + """Thanks to Ken Restivo for suggesting the addition + of session_encode + """ + session = {} + while len(data) > 0: + m = re.match('^(\w+)\|', data) + if m: + key = m.group(1) + offset = len(key)+1 + (dtype, dataoffset, value) = self._unserialize(data, offset) + offset = offset + dataoffset + data = data[offset:] + session[key] = value + else: + # No more stuff to decode + return session + + return session + + def unserialize(self, data): + return self._unserialize(data, 0)[2] + + def _unserialize(self, data, offset=0): + """ + Find the next token and unserialize it. + Recurse on array. + + offset = raw offset from start of data + + return (type, offset, value) + """ + + buf = [] + dtype = string.lower(data[offset:offset+1]) + + #print "# dtype =", dtype + + # 't:' = 2 chars + dataoffset = offset + 2 + typeconvert = lambda x : x + chars = datalength = 0 + + # int => Integer + if dtype == 'i': + typeconvert = lambda x : int(x) + (chars, readdata) = self.read_until(data, dataoffset, ';') + # +1 for end semicolon + dataoffset += chars + 1 + + # bool => Boolean + elif dtype == 'b': + typeconvert = lambda x : (int(x) == 1) + (chars, readdata) = self.read_until(data, dataoffset, ';') + # +1 for end semicolon + dataoffset += chars + 1 + + # double => Floating Point + elif dtype == 'd': + typeconvert = lambda x : float(x) + (chars, readdata) = self.read_until(data, dataoffset, ';') + # +1 for end semicolon + dataoffset += chars + 1 + + # n => None + elif dtype == 'n': + readdata = None + + # s => String + elif dtype == 's': + (chars, stringlength) = self.read_until(data, dataoffset, ':') + # +2 for colons around length field + dataoffset += chars + 2 + + # +1 for start quote + (chars, readdata) = self.read_chars(data, dataoffset+1, int(stringlength)) + # +2 for endquote semicolon + dataoffset += chars + 2 + + if chars != int(stringlength) != int(readdata): + raise Exception("String length mismatch") + + # array => Dict + # If you originally serialized a Tuple or List, it will + # be unserialized as a Dict. PHP doesn't have tuples or lists, + # only arrays - so everything has to get converted into an array + # when serializing and the original type of the array is lost + elif dtype == 'a': + readdata = {} + + # How many keys does this list have? + (chars, keys) = self.read_until(data, dataoffset, ':') + # +2 for colons around length field + dataoffset += chars + 2 + + # Loop through and fetch this number of key/value pairs + for i in range(0, int(keys)): + # Read the key + (ktype, kchars, key) = self._unserialize(data, dataoffset) + dataoffset += kchars + #print "Key(%i) = (%s, %i, %s) %i" % (i, ktype, kchars, key, dataoffset) + + # Read value of the key + (vtype, vchars, value) = self._unserialize(data, dataoffset) + dataoffset += vchars + #print "Value(%i) = (%s, %i, %s) %i" % (i, vtype, vchars, value, dataoffset) + + # Set the list element + readdata[key] = value + + # +1 for end semicolon + dataoffset += 1 + #chars = int(dataoffset) - start + + # I don't know how to unserialize this + else: + raise Exception("Unknown / Unhandled data type (%s)!" % dtype) + + + return (dtype, dataoffset-offset, typeconvert(readdata)) + + def read_until(self, data, offset, stopchar): + """ + Read from data[offset] until you encounter some char 'stopchar'. + """ + buf = [] + char = data[offset:offset+1] + i = 2 + while char != stopchar: + # Consumed all the characters and havent found ';' + if i+offset > len(data): + raise Exception("Invalid") + buf.append(char) + char = data[offset+(i-1):offset+i] + i += 1 + + # (chars_read, data) + return (len(buf), "".join(buf)) + + def read_chars(self, data, offset, length): + """ + Read 'length' number of chars from data[offset]. + """ + buf = [] + # Account for the starting quote char + #offset += 1 + for i in range(0, length): + char = data[offset+(i-1):offset+i] + buf.append(char) + + # (chars_read, data) + return (len(buf), "".join(buf)) diff --git a/mediawiki/README b/mediawiki/README new file mode 100644 index 00000000..b664a8a7 --- /dev/null +++ b/mediawiki/README @@ -0,0 +1,106 @@ +This is a rough example of integrated mediawiki authentication +originally written to work on a customized MW (some tables are different from standard) +so to adapt this to your case you'll most likely need to tweak this + +Also keep in mind that probably a better solution would be to create a single signon site. + +Author: evgeny.fadeev@gmail.com (Evgeny) + +==Minimal directions== + +1) Add the following to your settings_local.py (with relevant modifications): + +USE_EXTERNAL_LEGACY_LOGIN = True #enable external legacy login +EXTERNAL_LEGACY_LOGIN_AUTHENTICATION_BACKEND = 'mediawiki.auth.IncludeVirtualAuthenticationBackend' +EXTERNAL_LEGACY_LOGIN_AUTHENTICATION_MIDDLEWARE = 'mediawiki.middleware.IncludeVirtualAuthenticationMiddleware' +EXTERNAL_LEGACY_LOGIN_MODULE = 'mediawiki' #current module +EXTERNAL_LEGACY_LOGIN_HOST = 'yoursite.org' #wiki domain +EXTERNAL_LEGACY_LOGIN_PORT = 80 #port, probably 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = 'My Wiki' #html allowed +MEDIAWIKI_URL="http://yoursite.org/wiki/index.php" +MEDIAWIKI_SALT_PASSWORD=True #or False - depending on your LocalSettings.php +MEDIAWIKI_INDEX_PHP_URL='/wiki/index.php' +MEDIAWIKI_COOKIE_DOMAIN='.yoursite.org' #for cross subdomain login +MEDIAWIKI_SESSION_COOKIE_NAME = '' # probably '<dbname>_<dbtblprefix>_session' +MEDIAWIKI_PHP_SESSION_PREFIX='sess_' #depends on your setup +MEDIAWIKI_PHP_SESSION_PATH='/var/lib/php/session' +SESSION_COOKIE_DOMAIN = '.yoursite.org' #use this notation for cross-subdomain login + +2) Configure apache to access forum "backend" via the wiki domain. +Example configuration is in the end of the doc. + +3) Install two wiki extensions WsgiInjectableSpecialPage, UserRegister - +the usual MW way. You might want to disable traditional wiki registration. + +4) grep files for 'yourwiki' and change that to your own taste - there are some +hardcoded urls + +5) Templates are in templates/mediawiki - you probably will want to customize them, +form js media: templates/content/js/mediawiki-login.js +form css media: templates/content/style/mediawiki-login.css + +==Requirements== +wiki and forum must live in the same mysql database for registration to work, +however login will work even if this is not the case + +you must own both wiki and forum or there must be good trust relationship +between the owners - because password is shared + +==Notes on how external login currently works.== +password and login are entered in the login form. +these are checked against mw api + +password is saved in the auth_user table (the django way) +so if you at some point set USE_EXTERNAL_LEGACY_LOGIN = False +wiki passwords and logins will still work on the forum + +login action is partially synchronized btw wiki and forum (from forum to wiki, +but not the opposite way yet) + +when users first register - either on wiki or forum they are logged in on both + +on registration they receive a greeting email - you will want to customize messages + +technically, on the wiki registration form is injected via apache SSI +- using include virtual call + +there is a possibility for cross-site scripting attack if wiki session is stolen + +==Apache setup example for the wiki== +This assumes that wiki and forum facing the user are on different subdomains. +Also this setup is just an example - you may do better :). +Forum setup in apache is described in main osqa INSTALL document - that's extra. + +<VirtualHost your.ip:port> + ServerAdmin admin@yourwiki.org + DocumentRoot /path/to/wiki/root #dir containing wiki directory + ServerName yourwiki.org + AddOutputFilter INCLUDES .php + Alias /backend/content/ /path/to/forum/templates/content/ + AliasMatch (content\/style\/[^/]*\.css) /path/to/forum/templates/$1 + AliasMatch (content\/.*) /path/to/forum/templates/$1 + <Directory /path/to/forum/templates/content> + Order deny,allow + Allow from all + </Directory> + WSGIDaemonProcess my-forum-wiki-side #use daemon mode so to avoid potential timezone messups + WSGIProcessGroup my-forum-wiki-side + WSGIScriptAlias /backend /path/to/forum/cnprog.wsgi + CustomLog /var/log/httpd/yourwiki/access_log common + ErrorLog /var/log/httpd/yourwiki/error_log + LogLevel debug + DirectoryIndex index.php index.html +</VirtualHost> +<Directory /path/to/wiki/root> + Options Includes + <IfModule sapi_apache2.c> + php_admin_flag engine on + php_admin_flag safe_mode off + </IfModule> + <IfModule mod_php5.c> + php_admin_flag engine on + php_admin_flag safe_mode off + php_admin_value open_basedir "/path/to/wiki/root:.:/tmp" #tmp used for sessions + </IfModule> +</Directory> + diff --git a/mediawiki/UserRegister.alias.php b/mediawiki/UserRegister.alias.php new file mode 100644 index 00000000..0d5c1523 --- /dev/null +++ b/mediawiki/UserRegister.alias.php @@ -0,0 +1,7 @@ +<?php +$messages = array(); + +/* *** English *** */ +$messages['en'] = array( + 'User Register' => array('User Register'), +); diff --git a/mediawiki/UserRegister.body.php b/mediawiki/UserRegister.body.php new file mode 100644 index 00000000..f8c953a9 --- /dev/null +++ b/mediawiki/UserRegister.body.php @@ -0,0 +1,14 @@ +<?php +class UserRegister extends WsgiInjectableSpecialPage { + function __construct() { + parent::__construct( 'UserRegister', + '/backend/account/nmr-wiki/signup/', + array(0=>'/backend/content/style/mediawiki-login.css'), + array( + 0=>'/backend/content/js/jquery-1.2.6.min.js', + 1=>'/backend/content/js/mediawiki-login.js' + ) + ); + } +} + diff --git a/mediawiki/UserRegister.i18n.php b/mediawiki/UserRegister.i18n.php new file mode 100644 index 00000000..2f8d41d0 --- /dev/null +++ b/mediawiki/UserRegister.i18n.php @@ -0,0 +1,6 @@ +<?php +$messages = array(); + +$messages['en'] = array( + 'userregister' => 'Join the Wiki', +); diff --git a/mediawiki/UserRegister.php b/mediawiki/UserRegister.php new file mode 100644 index 00000000..cff0e69d --- /dev/null +++ b/mediawiki/UserRegister.php @@ -0,0 +1,24 @@ +<?php +# Alert the user that this is not a valid entry point to MediaWiki if they try to access the special pages file directly. +if (!defined('MEDIAWIKI')) { + echo <<<EOT +Not a valid entry point. +EOT; + exit( 1 ); +} + +$wgExtensionCredits['specialpage'][] = array( + 'name' => 'User Registration', + 'author' => 'Evgeny Fadeev', + 'url' => 'none', + 'description' => 'Creates new user account for the Wiki and Q&A forum', + 'descriptionmsg' => 'people-page-desc', + 'version' => '0.0.0', +); + +$dir = dirname(__FILE__) . '/'; + +$wgAutoloadClasses['UserRegister'] = $dir . 'UserRegister.body.php'; # Tell MediaWiki to load the extension body. +$wgExtensionMessagesFiles['UserRegister'] = $dir . 'UserRegister.i18n.php'; +$wgExtensionAliasesFiles['UserRegister'] = $dir . 'UserRegister.alias.php'; +$wgSpecialPages['UserRegister'] = 'UserRegister'; # Let MediaWiki know about your new special page. diff --git a/mediawiki/WsgiInjectableSpecialPage.php b/mediawiki/WsgiInjectableSpecialPage.php new file mode 100644 index 00000000..d23f22e8 --- /dev/null +++ b/mediawiki/WsgiInjectableSpecialPage.php @@ -0,0 +1,80 @@ +<?php + +class WsgiInjectableSpecialPage extends SpecialPage { + var $default_wsgi_command; + function __construct($page_name,$default_wsgi_command,$css='',$scripts='',$wsgi_prefix=''){ + parent::__construct($page_name); + wfLoadExtensionMessages($page_name); + $this->default_wsgi_command = $default_wsgi_command; + $this->css = $css; + $this->scripts = $scripts; + $this->wsgi_prefix = $wsgi_prefix; + } + function execute($par){ + global $wgWsgiScriptPath, $wgRequest, $wgOut, $wgHeader; + $wgWsgiScriptPath = ''; + if ($this->wsgi_prefix != ''){ + $wsgi_call = $this->wsgi_prefix; + } + else { + $wsgi_call = $wgWsgiScriptPath; + } + + $this->setHeaders(); + + if ($this->css != ''){ + if (is_array($this->css)){ + foreach($this->css as $css){ + $wgHeader->addCSS($css); + } + } + else{ + $wgHeader->addCSS($this->css); + } + } + if ($this->scripts != ''){ + if (is_array($this->scripts)){ + foreach($this->scripts as $script){ + $wgHeader->addScript($script); + } + } + else{ + $wgHeader->addScript($this->css); + } + } + + #add command + if (!is_null($wgRequest->getVal('command'))){ + $wsgi_call .= $wgRequest->getVal('command'); + } + else { + #why is this not working? + $wsgi_call .= $this->default_wsgi_command; + } + #add session key + $session_name = ini_get('session.name');#session_name(); + $session = ''; + if (array_key_exists($session_name, $_COOKIE)){ + $session = $_COOKIE[$session_name]; + } + $wsgi_call .= "?session=${session}"; + + #add posted request variables + if ($wgRequest->wasPosted()){ + $data = $wgRequest->data; + foreach ($data as $key => $value){ + if ($key != 'title'){ + $wsgi_call .= "&${key}=${value}"; + } + } + $wsgi_call .= '&was_posted=true'; + } + else { + $wsgi_call .= '&was_posted=false'; + } + + #print the include statement called as GET request + $wgOut->addHTML("<!--#include virtual=\"${wsgi_call}\"-->"); + #$wgOut->addHTML("<!-- ${wsgi_call} -->"); #print this only for debugging + } +} diff --git a/mediawiki/__init__.py b/mediawiki/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/mediawiki/__init__.py diff --git a/django_authopenid/external_login.py b/mediawiki/api.py index bd49c009..912de041 100644 --- a/django_authopenid/external_login.py +++ b/mediawiki/api.py @@ -1,6 +1,6 @@ #this file contains stub functions that can be extended to support #connect legacy login with external site -import settings +from django.conf import settings from django_authopenid.models import ExternalLoginData import httplib import urllib @@ -9,6 +9,7 @@ import cookielib from django import forms import xml.dom.minidom as xml import logging +from models import User as MWUser def login(request,user): """performs the additional external login operation @@ -35,11 +36,18 @@ def set_login_cookies(response,user): c[prefix + 'Token'] = token c[prefix + '_session'] = sessionid + logging.debug('have cookies ' + str(c)) + #custom code that copies cookies from external site #not sure how to set paths and domain of cookies here + domain = settings.MEDIAWIKI_COOKIE_DOMAIN for key in c: if c[key]: - response.set_cookie(str(key),value=str(c[key])) + response.set_cookie(str(key),\ + value=str(c[key]),\ + domain=domain) + for c in response.cookies.values(): + logging.debug(c.output()) except ExternalLoginData.DoesNotExist: #this must be an OpenID login pass @@ -62,6 +70,8 @@ def check_password(username,password): port = settings.EXTERNAL_LEGACY_LOGIN_PORT ext_site = httplib.HTTPConnection(host,port) + print 'connected to %s:%s' % (str(host),str(port)) + #custom code. this one does authentication through #MediaWiki API params = urllib.urlencode({'action':'login','format':'xml', @@ -76,6 +86,8 @@ def check_password(username,password): data = response.read().strip() ext_site.close() + print data + dom = xml.parseString(data) login = dom.getElementsByTagName('login')[0] result = login.getAttribute('result') @@ -100,4 +112,27 @@ def createuser(username,email,password): #retrieve email address def get_email(username,password): - return '' + try: + u = MWUser.objects.get(user_name=username) + return u.user_email + except MWUser.DoesNotExist: + return '' + +#try to get full name from mediawiki +def get_screen_name(username,password): + try: + u = MWUser.objects.get(user_name=username) + full_name = u' '.join((u.user_first_name, u.user_last_name)).strip() + if full_name != u'': + return full_name + else: + return username + except MWUser.DoesNotExist: + return username + +def connect_local_user_to_external_user(user, login, password): + try: + u = MWUser.objects.get(user_name=login) + user.mediawiki_user = u + except MWUser.DoesNotExist: + pass diff --git a/mediawiki/auth.py b/mediawiki/auth.py new file mode 100644 index 00000000..ee367dc1 --- /dev/null +++ b/mediawiki/auth.py @@ -0,0 +1,59 @@ +from mediawiki.models import User as MWUser +from django.contrib.auth.models import User +from django_authopenid.models import ExternalLoginData +from django.conf import settings +import logging +from PHPUnserialize import PHPUnserialize +import os + +class php(object): + @staticmethod + def get_session_data(session): + prefix = settings.MEDIAWIKI_PHP_SESSION_PREFIX + path = settings.MEDIAWIKI_PHP_SESSION_PATH + file = os.path.join(path,prefix + session) + #file may not exist + data = open(file).read() + u = PHPUnserialize() + return u.session_decode(data) + +class IncludeVirtualAuthenticationBackend(object): + def authenticate(self,token=None): + logging.debug('authenticating session %s' % token) + try: + php_session = php.get_session_data(token) + #todo: report technical errors to root + except: + #Fail condition 1. Session data cannot be retrieved + logging.debug('session %s cannot be retrieved' % str(token)) + return None + try: + name = php_session['wsUserName'] + id = php_session['wsUserID'] + except: + #Fail condition 2. Data misses keys + logging.debug('missing data in session table') + return None + try: + logging.debug('trying to find user %s id=%s in the MW database' % (name,id)) + wu = MWUser.objects.get(user_name=name,user_id=id) + except MWUser.DoesNotExist: + #Fail condition 3. User does not match session data + logging.debug('could not find wiki user who owns session') + return None + try: + logging.debug('trying to get external login data for mw user %s' % name) + eld = ExternalLoginData.objects.get(external_username=name) + #update session data and save? + return eld.user #may be none! + except ExternalLoginData.DoesNotExist: + #Fail condition 4. no external login data - user never logged in through django + #using the wiki login and password + logging.debug('no association found for MW user %s with django user' % name) + return None + + def get_user(self, user_id): + try: + return User.objects.get(pk=user_id) + except User.DoesNotExist: + return None diff --git a/mediawiki/forms.py b/mediawiki/forms.py new file mode 100644 index 00000000..bac39b5a --- /dev/null +++ b/mediawiki/forms.py @@ -0,0 +1,164 @@ +from utils.forms import NextUrlField, UserNameField, UserEmailField, SetPasswordForm +from django import forms +from django.forms import ValidationError +from models import User as MWUser +from models import TITLE_CHOICES +from django.contrib.auth.models import User +from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe +from django.contrib.formtools.wizard import FormWizard +from forum.forms import EditUserEmailFeedsForm, SimpleEmailSubscribeForm +from django.forms import ValidationError +from recaptcha_django import ReCaptchaField +from utils.forms import StrippedNonEmptyCharField +from forum.templatetags import extra_tags as forum_extra_tags + +#make better translations in your language django.po +EMAIL_FEED_CHOICES = ( + ('y',_('okay, let\'s try!')), + ('n',_('no OSQA community email please, thanks')) +) + +wiki_account_taken_msg = _('Wiki site already has this account, if it is yours perhaps you can ' + 'just try to log in with it?<br/>' + 'Otherwise, please pick another login name.') + +class RegisterForm(SetPasswordForm, SimpleEmailSubscribeForm): + login_name = UserNameField(label=_('Login name'), \ + db_model=MWUser, \ + db_field='user_name', \ + error_messages={ \ + 'required':_('Please enter login name above, it is required for the Wiki site'), \ + 'taken': mark_safe(wiki_account_taken_msg) \ + } + ) + next = NextUrlField() + email = UserEmailField() + screen_name = UserNameField(label=mark_safe(_('Please type your nickname below')), \ + skip_clean=True, \ + required=False) + first_name = StrippedNonEmptyCharField(max_length=255,label=mark_safe(_('First name')), + error_messages={'required':_('First name is required')} + ) + last_name = StrippedNonEmptyCharField(max_length=255,label=_('Last name'), + error_messages={'required':_('Last name is required')} + ) + #cannot be just "title" because there would be a conflict with "title" field used for MW!!! + user_title = forms.ChoiceField(choices=TITLE_CHOICES, label=_('Title (optional)')) + use_separate_screen_name = forms.BooleanField( + label=mark_safe(_('I prefer (or have to) to use a separate forum screen name')), + required=False, + ) + #subscribe = forms.ChoiceField(widget=forms.widgets.RadioSelect, \ + # error_messages={'required':_('please choose one of the options above')}, + # choices= EMAIL_FEED_CHOICES) + recaptcha = ReCaptchaField() + + class Media: + css={'all':(forum_extra_tags.href('/content/style/mediawiki-login.css'),),} + js=(forum_extra_tags.href('/content/js/mediawiki-login.js'),) + + def add_screen_name_error(self, err): + if 'screen_name' in self.cleaned_data: + del self.cleaned_data['screen_name'] + error_list = self._errors.get('screen_name',forms.util.ErrorList([])) + if isinstance(err, forms.util.ErrorList): + error_list.extend(err) + else: + error_list.append(err) + self._errors['screen_name'] = error_list + + def clean(self): + #this method cleans screen_name and use_separate_screen_name + screen_name = self.cleaned_data.get('screen_name', '') + + if 'use_separate_screen_name' in self.cleaned_data \ + and self.cleaned_data['use_separate_screen_name']: + if screen_name == '': + msg = _('please enter an alternative screen name or uncheck the box above') + self.add_screen_name_error(msg) + else: + try: + screen_name = self.fields['screen_name'].clean(screen_name) + self.final_clean_screen_name(screen_name) + except ValidationError, e: + self.add_screen_name_error(e) + else: + if screen_name != '': + self.add_screen_name_error(_('sorry, to use alternative screen name, please confirm it by checking the box above')) + else: + #build screen name from first and last names + first = self.cleaned_data.get('first_name',None) + last = self.cleaned_data.get('last_name',None) + if first and last: + screen_name = u'%s %s' % (first,last) + self.final_clean_screen_name(screen_name) + return self.cleaned_data + + def final_clean_screen_name(self,name): + try: + u = User.objects.get(username=name) + msg = _('Screen name <strong>%(real_name)s</strong> is somehow already taken on the forum.<br/>' + 'Unfortunately you will have to pick a separate screen name, but of course ' + 'there is no need to change the first name and last name entries.<br/>' + 'Please send us your feedback if you feel there might be a mistake. ' + 'Sorry for the inconvenience.')\ + % {'real_name':name} + self.add_screen_name_error(mark_safe(msg)) + except: + self.cleaned_data['screen_name'] = name + + #overridden validation for UserNameField + def clean_login_name(self): + try: + MWUser.objects.get(user_name=self.cleaned_data['login_name']) + del self.cleaned_data['login_name'] + raise ValidationError(_('sorry this login name is already taken, please try another')) + except: + return self.cleaned_data['login_name'] + +class RegisterFormWizard(FormWizard): + def done(self, request, form_list): + data = form_list[0].cleaned_data + login_name = data['login_name'] + password = data['password'] + first_name = data['first_name'] + last_name = data['last_name'] + screen_name = data['screen_name'] + email = data['email'] + subscribe = data['subscribe'] + next = data['next'] + + #register mediawiki user + mwu = MWUser( + user_name=login_name, + user_password=password, + user_first_name = first_name, + user_last_name = last_name, + user_email = email + ) + mwu.save() + + #register local user + User.objects.create_user(screen_name, email, password) + u = authenticate(username=screen_name, password=password) + u.mediawiki_user = mwu + u.save() + + #save email feed settings + EFF = EditUserEmailFeedsForm + if subscribe == 'y': + email_settings_form = EFF() + else: + email_settings_form = EFF(initial=EFF.NO_EMAIL_INITIAL) + email_settings_form.save(u) + + #create welcome message + u.message_set.create(message=_('Welcome to Q&A forum!')) + return HttpResponseRedirect(next) + + def get_template(self, step): + if step == 0: + return 'mediawiki/mediawiki_signup.html' + elif step == 1: + return 'notarobot.html' diff --git a/mediawiki/junk.py b/mediawiki/junk.py new file mode 100644 index 00000000..e67d492a --- /dev/null +++ b/mediawiki/junk.py @@ -0,0 +1,2 @@ +def junk(): + pass diff --git a/mediawiki/middleware.py b/mediawiki/middleware.py new file mode 100644 index 00000000..a46f486a --- /dev/null +++ b/mediawiki/middleware.py @@ -0,0 +1,57 @@ +from django.contrib import auth +from django.core.exceptions import ImproperlyConfigured +from django.conf import settings +import logging +import traceback +import sys + +class IncludeVirtualAuthenticationMiddleware(object): + def process_request(self,request): + """in this type of authentication the mw session token is passed via + "session" request parameter and authentication happens on every + request + """ + logging.debug('trying include virtual milldeware') + if not hasattr(request,'user'): + raise ImproperlyConfigured( + "The include virtual mediawiki authentication middleware requires the" + " authentication middleware to be installed. Edit your" + " MIDDLEWARE_CLASSES setting to insert" + " 'django.contrib.auth.middleware.AuthenticationMiddleware'" + " before the IncludeVirtualAuthenticationMiddleware class." + ) + + session = None + request.is_include_virtual = False + if request.is_ajax(): + logging.debug('have ajax request') + cookie_name = settings.MEDIAWIKI_SESSION_COOKIE_NAME + if cookie_name in request.COOKIES: + session = request.COOKIES[cookie_name] + logging.debug('ajax call has session %s' % session) + else: + logging.debug('dont have cookie') + else: + if request.REQUEST.has_key('session'): + session = request.REQUEST['session'] + request.is_include_virtual = True + logging.debug('I am virtual') + if request.REQUEST.get('was_posted','false') == 'true': + data = request.GET.copy() + data['recaptcha_ip_field'] = request.META['REMOTE_ADDR'] + request.GET = data + logging.debug('REQUEST is now %s' % str(request.GET)) + user = auth.authenticate(token=session) #authenticate every time + if user: + request.user = user + auth.login(request,user) + #else I probably need to forbid access + #raise ImproperlyConfigured( + # "The include virtual mediawiki authentication middleware requires the" + # "'session' request parameter set in the including document" + #) + + def process_exception(self,request,exception): + exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() + logging.debug('\n'.join(traceback.format_tb(exceptionTraceback))) + logging.debug('have exception %s %s' % (exceptionType,exceptionValue)) diff --git a/mediawiki/models.py b/mediawiki/models.py new file mode 100644 index 00000000..e37aec32 --- /dev/null +++ b/mediawiki/models.py @@ -0,0 +1,312 @@ +# This is an auto-generated Django model module. +# You'll have to do the following manually to clean this up: +# * Rearrange models' order +# * Make sure each model has one field with primary_key=True +# Feel free to rename the models, but don't rename db_table values or field names. +# +# Also note: You'll have to insert the output of 'django-admin.py sqlcustom [appname]' +# into your database. + +table_prefix = u'nmrwiki' +from django.db import models +import re +from django.conf import settings +import logging +from django.contrib.auth.models import User as DjangoUser +from django.utils.translation import ugettext as _ +import hashlib +import time +import random + +MW_TS = '%Y%m%d%H%M%S' + +TITLE_CHOICES = ( + ('none',_('----')), + ('prof',_('Prof.')), + ('dr',_('Dr.')), +) + +class User(models.Model): + user_id = models.IntegerField(primary_key=True,db_column='user_id') + user_name = models.CharField(max_length=765) + user_real_name = models.CharField(max_length=765) + user_password = models.TextField() + user_newpassword = models.TextField() + user_newpass_time = models.CharField(max_length=14, blank=True) + user_email = models.TextField() + user_options = models.TextField() + user_touched = models.CharField(max_length=14) + user_token = models.CharField(max_length=32) + user_email_authenticated = models.CharField(max_length=14, blank=True) + user_email_token = models.CharField(max_length=32, blank=True) + user_email_token_expires = models.CharField(max_length=14, blank=True) + user_registration = models.CharField(max_length=14, blank=True) + user_editcount = models.IntegerField(null=True, blank=True) + user_last_name = models.CharField(max_length=765, blank=True) + user_first_name = models.CharField(max_length=765, blank=True) + user_reason_to_join = models.CharField(max_length=765, blank=True) + user_title = models.CharField(max_length=16, blank=True, choices=TITLE_CHOICES) + class Meta: + db_table = table_prefix + u'user' + managed = False + + def set_default_options(self): + default_options = { + 'quickbar':1, + 'underline':2, + 'cols':80, + 'rows':25, + 'searchlimit':20, + 'contextlines':5, + 'contextchars':50, + 'skin':'false', + 'math':1, + 'rcdays':7, + 'rclimit':50, + 'wllimit':250, + 'highlightbroken':1, + 'stubthreshold':0, + 'previewontop':1, + 'editsection':1, + 'editsectiononrightclick':0, + 'showtoc':1, + 'showtoolbar':1, + 'date':'default', + 'imagesize':2, + 'thumbsize':2, + 'rememberpassword':0, + 'enotifwatchlistpages':0, + 'enotifusertalkpages':1, + 'enotifminoredits':0, + 'enotifrevealaddr':0, + 'shownumberswatching':1, + 'fancysig':0, + 'externaleditor':0, + 'externaldiff':0, + 'showjumplinks':1, + 'numberheadings':0, + 'uselivepreview':0, + 'watchlistdays':3.0, + 'usenewrc':1, + } + self.user_options = '\n'.join( + map(lambda opt: '%s=%s' % (opt[0], str(opt[1])), + default_options.items()) + ) + + def set_password_and_token(self,password): + p = hashlib.md5(password).hexdigest() + if hasattr(settings,'MEDIAWIKI_SALT_PASSWORD') and settings.MEDIAWIKI_SALT_PASSWORD == True: + p = hashlib.md5('%d-%s' % (self.user_id, p)).hexdigest() + self.user_password = p + self.user_token = hashlib.md5(p + str(time.time())).hexdigest() + + def get_name(self): + if self.user_real_name: + if re.search(r'\S',self.user_real_name): + return self.user_real_name + return self.user_name + ' (nickname)' + + def get_html(self): + return '<a href="%s">%s</a>' % (self.get_absolute_url(),self.get_name()) + + def get_absolute_url(self): + url = settings.MEDIAWIKI_URL + '?title=User:' + self.user_name + return url + +class UserProfile(models.Model): + nup_user_id = models.ForeignKey(User,primary_key=True,db_column='nup_user_id') + nup_about = models.CharField(max_length=765, blank=True) + nup_position_title = models.CharField(max_length=765, blank=True) + nup_position_type = models.CharField(max_length=765, blank=True) + nup_employer_division = models.CharField(max_length=765, blank=True) + nup_employer_company = models.CharField(max_length=765, blank=True) + nup_employer_type = models.CharField(max_length=765, blank=True) + nup_employment_status = models.CharField(max_length=45, blank=True) + nup_profession = models.CharField(max_length=765, blank=True) + nup_city = models.CharField(max_length=765, blank=True) + nup_state = models.CharField(max_length=765, blank=True) + nup_country = models.CharField(max_length=765, blank=True) + nup_lattitude = models.FloatField(null=True, blank=True) + nup_longitude = models.FloatField(null=True, blank=True) + nup_hiring = models.IntegerField(null=True, blank=True) + nup_hunting = models.IntegerField(null=True, blank=True) + nup_education = models.TextField(blank=True) + nup_websites = models.TextField(blank=True) + nup_interests = models.TextField(blank=True) + nup_job_ad = models.TextField(blank=True) + nup_job_ad_title = models.CharField(max_length=765, blank=True) + nup_job_ad_active = models.IntegerField(null=True, blank=True) + nup_expertise = models.TextField(blank=True) + nup_is_approved = models.BooleanField() + class Meta: + db_table = table_prefix + u'new_user_profile' + managed = False + +class RecentChanges(models.Model): + rc_id = models.AutoField(primary_key=True, db_column='rc_id') + rc_timestamp = models.CharField(max_length=14) + rc_cur_time = models.CharField(max_length=14) + rc_user = models.ForeignKey(User, db_column='rc_user') + rc_user_text = models.CharField(max_length=765) + rc_namespace = models.IntegerField() + rc_title = models.CharField(max_length=765) + rc_comment = models.CharField(max_length=765) + rc_minor = models.IntegerField() + rc_bot = models.IntegerField() + rc_new = models.IntegerField() + rc_cur_id = models.IntegerField() + rc_this_oldid = models.IntegerField() + rc_last_oldid = models.IntegerField() + rc_type = models.IntegerField() + rc_moved_to_ns = models.IntegerField() + rc_moved_to_title = models.CharField(max_length=765) + rc_patrolled = models.IntegerField() + rc_ip = models.CharField(max_length=40) + rc_old_len = models.IntegerField(null=True, blank=True) + rc_new_len = models.IntegerField(null=True, blank=True) + rc_deleted = models.IntegerField() + rc_logid = models.ForeignKey('Logging', db_column='rc_logid') + rc_log_type = models.CharField(max_length=255, blank=True) + rc_log_action = models.CharField(max_length=255, blank=True) + rc_params = models.TextField(blank=True) + class Meta: + db_table = table_prefix + u'recentchanges' + managed = False + +class Logging(models.Model): + log_id = models.AutoField(primary_key=True) + log_type = models.CharField(max_length=10) + log_action = models.CharField(max_length=10) + log_timestamp = models.CharField(max_length=14) + log_user = models.ForeignKey(User,db_column='log_user') + log_namespace = models.IntegerField() + log_title = models.CharField(max_length=765) + log_comment = models.CharField(max_length=765) + log_params = models.TextField() + log_deleted = models.IntegerField() + class Meta: + db_table = table_prefix + u'logging' + managed = False + + def show_in_recent_changes(self, ip=None, rc_minor=False): + #to call this method self object must already exist in DB + if self.log_type == 'newusers' and self.log_action=='create': + rc = RecentChanges( + rc_ip=ip, + rc_minor=int(rc_minor), + rc_deleted=0, + rc_bot=0, + rc_new=0, + rc_moved_to_title='', + rc_moved_to_ns=0, + rc_this_oldid=0, + rc_last_oldid=0, + rc_patrolled=1, + rc_old_len=None, + rc_new_len=None, + rc_logid=self, + rc_user=self.log_user, + rc_user_text=self.log_user.user_name, + rc_log_type=self.log_type, + rc_log_action=self.log_action, + rc_timestamp = self.log_timestamp, + rc_cur_time = self.log_timestamp, + rc_title='Log/newusers', + rc_namespace=-1, #-1 special, 2 is User namespace + rc_params=self.log_params, + rc_comment=_('Welcome new user!'), + rc_type=3,#MW RCLOG constant from Defines.php + rc_cur_id=0, + ) + rc.save() + else: + raise NotImplementedError() + + +class Page(models.Model): + page_id = models.AutoField(primary_key=True) + page_namespace = models.IntegerField(unique=True) + page_title = models.CharField(max_length=765) + page_restrictions = models.TextField() + page_counter = models.IntegerField() + page_is_redirect = models.IntegerField() + page_is_new = models.IntegerField() + page_random = models.FloatField() + page_touched = models.CharField(max_length=14) + page_latest = models.IntegerField() + page_len = models.IntegerField() + class Meta: + db_table = table_prefix + u'page' + managed = False + def save(self): + raise Exception('WikiUser table is read-only in this application') + +class PageLinks(models.Model): + pl_from = models.ForeignKey(Page) + pl_namespace = models.IntegerField() + pl_title = models.CharField(max_length=765) + class Meta: + db_table = table_prefix + u'pagelinks' + managed = False + def save(self): + raise Exception('WikiUser table is read-only in this application') + +class Revision(models.Model): + rev_id = models.IntegerField(unique=True) + rev_page = models.IntegerField() + rev_text_id = models.IntegerField() + rev_comment = models.TextField() + rev_user = models.IntegerField() + rev_user_text = models.CharField(max_length=765) + rev_timestamp = models.CharField(max_length=14) + rev_minor_edit = models.IntegerField() + rev_deleted = models.IntegerField() + rev_len = models.IntegerField(null=True, blank=True) + rev_parent_id = models.IntegerField(null=True, blank=True) + class Meta: + db_table = table_prefix + u'revision' + managed = False + +class Text(models.Model): + old_id = models.IntegerField(primary_key=True) + old_text = models.TextField() + old_flags = models.TextField() + class Meta: + db_table = table_prefix + u'text' + managed = False + +#nmrwiki_stats table may be of interest + +class UserGroups(models.Model): + ug_user = models.ForeignKey(User,primary_key=True) + ug_group = models.CharField(max_length=16) + class Meta: + db_table = table_prefix + u'user_groups' + managed = False + +def user_get_absolute_url(user): + return user.mediawiki_user.get_absolute_url() + +def user_get_html(user): + return user.mediawiki_user.get_html() + +def user_has_valid_email(user): + if user.mediawiki_user.user_email_authenticated: + return True + else: + return False + +def user_get_description_for_admin(user): + out = user.get_html() + ' (%s)' % user.username + if user.has_valid_email(): + out += ' - has valid email' + else: + out += ' - <em>no email!</em>' + return out + +DjangoUser.add_to_class('mediawiki_user',models.ForeignKey(User, null=True)) +DjangoUser.add_to_class('get_wiki_profile_url',user_get_absolute_url) +DjangoUser.add_to_class('get_wiki_profile_url_html',user_get_html) +DjangoUser.add_to_class('get_description_for_admin',user_get_description_for_admin) +DjangoUser.add_to_class('has_valid_wiki_email',user_has_valid_email) diff --git a/mediawiki/templatetags/__init__.py b/mediawiki/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/mediawiki/templatetags/__init__.py diff --git a/mediawiki/templatetags/mediawikitags.py b/mediawiki/templatetags/mediawikitags.py new file mode 100644 index 00000000..b21789f4 --- /dev/null +++ b/mediawiki/templatetags/mediawikitags.py @@ -0,0 +1,62 @@ +from django import template +from django.template.defaultfilters import stringfilter +from django.conf import settings +import logging + +register = template.Library() + +#template tags +class MWPluginFormActionNode(template.Node): + def __init__(self, wiki_page, form_action): + self.form_action = ''.join(form_action[1:-1]) + self.wiki_page = ''.join(wiki_page[1:-1]) + def render(self, context): + out = ('<input type="hidden" name="title" value="%s"/>' \ + + '<input type="hidden" name="command" value="%s"/>') \ + % (self.wiki_page, self.form_action) + return out + +def curry_up_to_two_argument_tag(TagNodeClass): + def do_the_action_func(parser,token): + args = token.split_contents() + if len(args) > 3: + tagname = token.contents.split()[0] + raise template.TemplateSyntaxError, \ + '%s tag requires two arguments or less' % tagname + if len(args) > 1: + argument1 = ''.join(args[1][1:-1]) + else: + argument1 = None + if len(args) == 3: + argument2 = ''.join(args[2][1:-1]) + else: + argument2 = None + return TagNodeClass(argument1, argument2) + return do_the_action_func + +def do_mw_plugin_form_action(parser,token): + args = token.split_contents() + if len(args) != 3: + tagname = token.contents.split()[0] + raise template.TemplateSyntaxError, \ + '%s tag requires two arguments' % tagname + return MWPluginFormActionNode(args[1],args[2]) + +class MediaWikiPluginUrlNode(template.Node): + """will return either wiki url, a particular page url + or a page with command argument to be interpreted by the plugin + """ + def __init__(self,wiki_page=None,url=None): + self.url = url + self.wiki_page = wiki_page + def render(self,context): + title_token = '?title=%s' % self.wiki_page + cmd_token = '&command=%s' % self.url + if self.wiki_page == None: + return settings.MEDIAWIKI_URL + if self.url == None: + return settings.MEDIAWIKI_URL + title_token + return settings.MEDIAWIKI_URL + title_token + cmd_token + +register.tag('mw_plugin_form_action',do_mw_plugin_form_action) +register.tag('mw_plugin_url',curry_up_to_two_argument_tag(MediaWikiPluginUrlNode)) diff --git a/mediawiki/views.py b/mediawiki/views.py new file mode 100644 index 00000000..012d6f42 --- /dev/null +++ b/mediawiki/views.py @@ -0,0 +1,192 @@ +#this file contains stub functions that can be extended to support +#connect legacy login with external site +#from django import forms +import time +from models import User as MWUser +from models import Logging +from models import MW_TS +import api +from django.shortcuts import render_to_response +from django.utils.translation import ugettext as _ +from django.template import RequestContext +from django.http import HttpResponseRedirect +from forms import RegisterForm +from forum.forms import SimpleEmailSubscribeForm +from forum.models import Question +from django.contrib.auth.models import User +from django.contrib.auth import authenticate, login +from django.http import HttpResponseRedirect +from django.db import transaction +from django_authopenid.models import ExternalLoginData +from django_authopenid.views import not_authenticated +from django.template import loader +from django.core.mail import send_mail +from django.conf import settings +from django.utils.safestring import mark_safe +import hashlib +import random + +#not a view, but uses request and templates +def send_welcome_email(request, wiki_user, django_user): + random.seed() + confirmation_token = '%032x' % random.getrandbits(128) + wiki_user.user_email_token = hashlib.md5(confirmation_token).hexdigest() + wiki_user.user_email_token_expires = time.strftime(MW_TS,(time.gmtime(time.time() + 7*24*60*60))) + wiki_user.save() + + link = 'http://' + settings.EXTERNAL_LEGACY_LOGIN_HOST \ + + settings.MEDIAWIKI_INDEX_PHP_URL \ + + '?title=Special:Confirmemail/' \ + + confirmation_token + + pw_link = 'http://' + settings.EXTERNAL_LEGACY_LOGIN_HOST \ + + settings.MEDIAWIKI_INDEX_PHP_URL \ + + '?title=Password_recovery' + + if wiki_user.user_title == 'prof': + template_name = 'mediawiki/welcome_professor_email.txt' + else: + template_name = 'mediawiki/welcome_email.txt' + t = loader.get_template(template_name) + + data = { + 'email_confirmation_url':mark_safe(link), + 'admin_email':settings.DEFAULT_FROM_EMAIL, + 'first_name':wiki_user.user_first_name, + 'last_name':wiki_user.user_last_name, + 'login_name':wiki_user.user_name, + 'title':wiki_user.user_title, + 'user_email':wiki_user.user_email, + 'forum_screen_name':django_user.username, + 'password_recovery_url':mark_safe(pw_link), + } + body = t.render(RequestContext(request,data)) + if wiki_user.user_title in ('prof','dr'): + subject = _('%(title)s %(last_name)s, welcome to the OSQA online community!') \ + % {'title':wiki_user.get_user_title_display(),'last_name':wiki_user.user_last_name } + else: + subject = _('%(first_name)s, welcome to the OSQA online community!') \ + % {'first_name':wiki_user.user_first_name} + from_email = settings.DEFAULT_FROM_EMAIL + send_mail(subject,body,from_email,[wiki_user.user_email]) + +@transaction.commit_manually +def signup(request): + #this view works through forum and mediawiki (using apache include virtual injection) + if request.is_include_virtual and request.REQUEST.get('was_posted','false')=='true': + POST_INCLUDE_VIRTUAL = True + POST_DATA = request.GET + else: + POST_INCLUDE_VIRTUAL = False + if request.method == 'POST': + POST_DATA = request.POST + else: + POST_DATA = None + + if POST_DATA: + form = RegisterForm(POST_DATA) + if form.is_valid(): + data = form.cleaned_data + login_name = data['login_name'] + password = data['password'] + first_name = data['first_name'] + last_name = data['last_name'] + screen_name = data['screen_name'] + user_title = data['user_title'] + email = data['email'] + next = data['next'] + + #register mediawiki user + user_real_name = u'%s %s' % (first_name,last_name) + mwu = MWUser( + user_name=login_name, + user_first_name = first_name, + user_last_name = last_name, + user_title = user_title, + user_email = email, + user_real_name=user_real_name + ) + mwu.set_default_options() + mwu.save() + #password may need user id so reload it + mwu = MWUser.objects.get(user_name = login_name) + mwu.set_password_and_token(password) + mwu.save() + + #create log message + mwu_creation_log = Logging( + log_type='newusers', + log_action='create', + log_timestamp=time.strftime(MW_TS), + log_params=str(mwu.user_id), + log_namespace=2, + log_user=mwu, + log_deleted=0, + ) + mwu_creation_log.save() + mwu_creation_log.show_in_recent_changes(ip=request.META['REMOTE_ADDR']) + print 'creation log saved' + + #register local user + User.objects.create_user(screen_name, email, password) + u = authenticate(username=screen_name, password=password) + login(request,u) + u.mediawiki_user = mwu + u.save() + + #save email feed settings + subscribe = SimpleEmailSubscribeForm(POST_DATA) + if subscribe.is_valid(): + subscribe.save(user=u) + + #save external login data + eld = ExternalLoginData(external_username=login_name, user=u) + eld.save() + + transaction.commit()#commit so that user becomes visible on the wiki side + + #check password through API and load MW HTTP header session data + api.check_password(login_name,password) + + print 'wiki login worked' + + #create welcome message on the forum + u.message_set.create(message=_('Welcome to the OSQA community!')) + print 'about to send confirmation email' + send_welcome_email(request, mwu, u) + + if POST_INCLUDE_VIRTUAL: + questions = Question.objects.exclude(deleted=True, closed=True, answer_accepted=True) + questions = questions.order_by('-last_activity_at')[:5] + response = render_to_response('mediawiki/thanks_for_joining.html', \ + { + 'wiki_user':mwu, + 'user':u, + 'questions':questions, + }, + context_instance = RequestContext(request)) + api.set_login_cookies(response, u) + #call session middleware now to get the django login cookies + from django.contrib.sessions.middleware import SessionMiddleware + sm = SessionMiddleware() + response = sm.process_response(request,response) + cookies = response.cookies + for c in cookies.values(): + response.write(c.js_output()) + else: + response = HttpResponseRedirect(next) + api.set_login_cookies(response, u) + + #set cookies so that user is logged in in the wiki too + transaction.commit() + return response + else: + form = RegisterForm() + + transaction.commit() + if request.is_include_virtual: + template_name = 'mediawiki/mediawiki_signup_content.html' + else: + template_name = 'mediawiki/mediawiki_signup.html' + return render_to_response(template_name,{'form':form},\ + context_instance=RequestContext(request)) diff --git a/middleware/anon_user.py b/middleware/anon_user.py index 8422d89b..fa2686f0 100644 --- a/middleware/anon_user.py +++ b/middleware/anon_user.py @@ -1,8 +1,8 @@ from django.http import HttpResponseRedirect -from django_authopenid.util import get_next_url +from utils.forms import get_next_url from django.utils.translation import ugettext as _ from user_messages import create_message, get_and_delete_messages -import settings +from django.conf import settings import logging class AnonymousMessageManager(object): diff --git a/middleware/cancel.py b/middleware/cancel.py index f03ff35e..51e1b253 100644 --- a/middleware/cancel.py +++ b/middleware/cancel.py @@ -1,5 +1,5 @@ from django.http import HttpResponseRedirect -from django_authopenid.util import get_next_url +from utils.forms import get_next_url import logging class CancelActionMiddleware(object): def process_view(self, request, view_func, view_args, view_kwargs): diff --git a/settings.py b/settings.py index e2e97cb1..e45d4780 100755 --- a/settings.py +++ b/settings.py @@ -14,19 +14,22 @@ TEMPLATE_LOADERS = ( # 'django.template.loaders.eggs.load_template_source', ) -MIDDLEWARE_CLASSES = ( - 'django.middleware.gzip.GZipMiddleware', +MIDDLEWARE_CLASSES = [ + #'django.middleware.gzip.GZipMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', #'django.middleware.locale.LocaleMiddleware', + #'django.middleware.cache.UpdateCacheMiddleware', 'django.middleware.common.CommonMiddleware', + #'django.middleware.cache.FetchFromCacheMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.middleware.transaction.TransactionMiddleware', #'django.middleware.sqlprint.SqlPrintingMiddleware', 'middleware.anon_user.ConnectToSessionMessagesMiddleware', 'middleware.pagesize.QuestionsPageSizeMiddleware', 'middleware.cancel.CancelActionMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware', -) + 'recaptcha_django.middleware.ReCaptchaMiddleware', + 'django.middleware.transaction.TransactionMiddleware', +] TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', @@ -52,7 +55,10 @@ ALLOW_FILE_TYPES = ('.jpg', '.jpeg', '.gif', '.bmp', '.png', '.tiff') # unit byte ALLOW_MAX_FILE_SIZE = 1024 * 1024 -INSTALLED_APPS = ( +# User settings +from settings_local import * + +INSTALLED_APPS = [ 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', @@ -62,11 +68,27 @@ INSTALLED_APPS = ( 'django.contrib.sitemaps', 'forum', 'django_authopenid', - #'djangosphinx', 'debug_toolbar' , 'user_messages', - 'fbconnect', -) +] -# User settings -from settings_local import * +AUTHENTICATION_BACKENDS = ['django.contrib.auth.backends.ModelBackend',] + +if check_local_setting('USE_SPHINX_SEARCH',True): + INSTALLED_APPS.append('djangosphinx') + +if check_local_setting('USE_FB_CONNECT',True): + INSTALLED_APPS.append('fbconnect') + +#load optional plugin module for external password login +if 'USE_EXTERNAL_LEGACY_LOGIN' in locals() and USE_EXTERNAL_LEGACY_LOGIN: + INSTALLED_APPS.append(EXTERNAL_LEGACY_LOGIN_MODULE) + + if 'EXTERNAL_LEGACY_LOGIN_AUTHENTICATION_BACKEND' in locals(): + AUTHENTICATION_BACKENDS.append(EXTERNAL_LEGACY_LOGIN_AUTHENTICATION_BACKEND) + if 'EXTERNAL_LEGACY_LOGIN_AUTHENTICATION_MIDDLEWARE' in locals(): + MIDDLEWARE_CLASSES.append(EXTERNAL_LEGACY_LOGIN_AUTHENTICATION_MIDDLEWARE) + def LOAD_EXTERNAL_LOGIN_APP(): + return __import__(EXTERNAL_LEGACY_LOGIN_MODULE, [], [], ['api','forms','views']) +else: + LOAD_EXTERNAL_LOGIN_APP = lambda: None diff --git a/settings_local.py.dist b/settings_local.py.dist index 443b73d1..f271a0b0 100755 --- a/settings_local.py.dist +++ b/settings_local.py.dist @@ -2,6 +2,13 @@ import os.path from django.utils.translation import ugettext as _ +def check_local_setting(name, value): + local_vars = locals() + if name in local_vars and local_var[name] == value: + return True + else: + return False + SITE_SRC_ROOT = os.path.dirname(__file__) LOG_FILENAME = 'django.osqa.log' @@ -87,6 +94,11 @@ SPHINX_SEARCH_INDICES=('osqa',) #a tuple of index names remember about a comma a SPHINX_SERVER='localhost' SPHINX_PORT=3312 +#please get these at recaptcha.net +RECAPTCHA_PRIVATE_KEY='...' +RECAPTCHA_PUBLIC_KEY='...' + #Facebook settings +USE_FB_CONNECT=True FB_API_KEY='' #your api key from facebook FB_SECRET='' #your application secret diff --git a/sql_scripts/100108_upgrade_ef.sql b/sql_scripts/100108_upgrade_ef.sql new file mode 100644 index 00000000..1c9a5c1c --- /dev/null +++ b/sql_scripts/100108_upgrade_ef.sql @@ -0,0 +1,4 @@ +alter table auth_user add column hide_ignored_questions tinyint(1) not NULL; +update auth_user set hide_ignored_questions=0; +alter table auth_user add column tag_filter_setting varchar(16) not NULL; +update auth_user set tag_filter_setting='ignored'; diff --git a/sql_scripts/badges.sql b/sql_scripts/badges.sql new file mode 100644 index 00000000..f47e067a --- /dev/null +++ b/sql_scripts/badges.sql @@ -0,0 +1,37 @@ +INSERT INTO `badge` ( `id`, `name`, `type`, `slug`, `description`, `multiple`, `awarded_count`) VALUES +(1, 'Disciplined', 3, 'disciplined', 'Deleted own post with score of 3 or higher', 1, 0), +(2, 'Peer Pressure', 3, 'peer-pressure', 'Deleted own post with score of -3 or lower', 1, 0), +(3, 'Nice answer', 3, 'nice-answer', 'Answer voted up 10 times', 1, 0), +(4, 'Nice Question', 3, 'nice-question', 'Question voted up 10 times', 1, 0), +(5, 'Pundit', 3, 'pundit', 'Left 10 comments with score of 10 or more', 0, 0), +(6, 'Popular Question', 3, 'popular-question', 'Asked a question with 1,000 views', 1, 0), +(7, 'Citizen patrol', 3, 'citizen-patrol', 'First flagged post', 0, 0), +(8, 'Cleanup', 3, 'cleanup', 'First rollback', 0, 0), +(9, 'Critic', 3, 'critic', 'First down vote', 0, 0), +(10, 'Editor', 3, 'editor', 'First edit', 0, 0), +(11, 'Organizer', 3, 'organizer', 'First retag', 0, 0), +(12, 'Scholar', 3, 'scholar', 'First accepted answer on your own question', 0, 0), +(13, 'Student', 3, 'student', 'Asked first question with at least one up vote', 0, 0), +(14, 'Supporter', 3, 'supporter', 'First up vote', 0, 0), +(15, 'Teacher', 3, 'teacher', 'Answered first question with at least one up vote', 0, 0), +(16, 'Autobiographer', 3, 'autobiographer', 'Completed all user profile fields', 0, 0), +(17, 'Self-Learner', 3, 'self-learner', 'Answered your own question with at least 3 up votes', 1, 0), +(18, 'Great Answer', 1, 'great-answer', 'Answer voted up 100 times', 1, 0), +(19, 'Great Question', 1, 'great-question', 'Question voted up 100 times', 1, 0), +(20, 'Stellar Question', 1, 'stellar-question', 'Question favorited by 100 users', 1, 0), +(21, 'Famous question', 1, 'famous-question', 'Asked a question with 10,000 views', 1, 0), +(22, 'Alpha', 2, 'alpha', 'Actively participated in the private alpha', 0, 0), +(23, 'Good Answer', 2, 'good-answer', 'Answer voted up 25 times', 1, 0), +(24, 'Good Question', 2, 'good-question', 'Question voted up 25 times', 1, 0), +(25, 'Favorite Question', 2, 'favorite-question', 'Question favorited by 25 users', 1, 0), +(26, 'Civic duty', 2, 'civic-duty', 'Voted 300 times', 0, 0), +(27, 'Strunk & White', 2, 'strunk-and-white', 'Edited 100 entries', 0, 0), +(28, 'Generalist', 2, 'generalist', 'Active in many different tags', 0, 0), +(29, 'Expert', 2, 'export', 'Very active in one tag', 0, 0), +(30, 'Yearling', 2, 'yearling', 'Active member for a year', 0, 0), +(31, 'Notable Question', 2, 'notable-question', 'Asked a question with 2,500 views', 1, 0), +(32, 'Enlightened', 2, 'enlightened', 'First answer was accepted with at least 10 up votes', 0, 0), +(33, 'Beta', 2, 'beta', 'Actively participated in the private beta', 0, 0), +(34, 'Guru', 2, 'guru', 'Accepted answer and voted up 40 times', 1, 0), +(35, 'Necromancer', 2, 'necromancer', 'Answered a question more than 60 days later with at least 5 votes', 1, 0), +(36, 'Taxonomist', 2, 'taxonomist', 'Created a tag used by 50 questions', 1, 0); diff --git a/templates/about.html b/templates/about.html index ec4b6a73..66dcc3fd 100644 --- a/templates/about.html +++ b/templates/about.html @@ -12,24 +12,25 @@ </div> <div class="content"> - <p><strong>{{settings.APP_SHORT_NAME}}</strong> is a collaboratively edited question and answer site created with - <a href="http://osqa.net">OSQA: The Open Source Q&A System</a>.</p> + <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> + the relevant keywords to simplify future access and organize the accumulated material. + </p> - <p>This OSQA site is moderated by its members, hopefully - including yourself! + <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> + 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> </div> {% endblock %} <!-- end template about.html --> diff --git a/templates/authopenid/complete.html b/templates/authopenid/complete.html index 1606cfc5..c967e8e2 100644 --- a/templates/authopenid/complete.html +++ b/templates/authopenid/complete.html @@ -11,7 +11,7 @@ parameters: * username (same as screen name or username in the models, and nickname in openid sreg) * form1 - OpenidRegisterForm * form2 - OpenidVerifyForm not clear what this form is supposed to do, not used for legacy -* email_feeds_form forum.forms.EditUserEmailFeedsForm +* email_feeds_form forum.forms.SimpleEmailSubscribeForm * openid_username_exists {% endcomment %} {% load i18n %} @@ -92,9 +92,11 @@ parameters: {% endif %} {{ form1.email }} </div> - <p class='nomargin'>{% trans "receive updates motivational blurb" %}</p> - {% include "edit_user_email_feeds_form.html" %} - <p class='nomargin'>{% trans "Tag filter tool will be your right panel, once you log in." %}</p> + <p>{% trans "receive updates motivational blurb" %}</p> + <div class='simple-subscribe-options'> + {{email_feeds_form.subscribe}} + </div> + <p class='space-above'>{% trans "Tag filter tool will be your right panel, once you log in." %}</p> <div class="submit-row"><input type="submit" class="submit" name="bnewaccount" value="{% trans "create account" %}"/></div> </form> </div> @@ -108,7 +110,9 @@ parameters: <div class="form-row"><label for="id_username">{% trans "user name" %}</label><br/>{{ form2.username }}</div> <div class="form-row"><label for="id_passwordl">{% trans "password" %}</label><br/>{{ form2.password }}</div> <p><span class='big strong'>(Optional) receive updates by email</span> - only sent when there are any.</p> - {% include "edit_user_email_feeds_form.html" %} + <div class='simple-subscribe-options'> + {{email_feeds_form.subscribe}} + </div> <!--todo double check translation from chinese 确认 = "Register" --> <div class="submit-row"> <input type="submit" class="submit" name="bverify" value="{% trans "Register" %}"/> diff --git a/templates/authopenid/external_legacy_login_info.html b/templates/authopenid/external_legacy_login_info.html index c200b29d..3318499c 100644 --- a/templates/authopenid/external_legacy_login_info.html +++ b/templates/authopenid/external_legacy_login_info.html @@ -9,7 +9,7 @@ {% spaceless %} <div class="message"> <!--add info about your external login site here--> -{% trans "how to login with password through external login website" %} +{% blocktrans %}how to login with password through external login website or use {{feedback_url}}{% endblocktrans %} </div> {% endspaceless %} {% endblock %} diff --git a/templates/authopenid/signup.html b/templates/authopenid/signup.html index 45dfb51b..d800eaf9 100644 --- a/templates/authopenid/signup.html +++ b/templates/authopenid/signup.html @@ -10,10 +10,18 @@ <p class="message">{% trans "Traditional signup info" %}</p> <form action="{% url user_signup %}" method="post" accept-charset="utf-8"> <ul class="form-horizontal-rows"> - {{form.as_ul}} + <li><label for="usename_id">{{form.username.label}}</label>{{form.username}}{{form.username.errors}}</li> + <li><label for="email_id">{{form.email.label}}</label>{{form.email}}{{form.email.errors}}</li> + <li><label for="password1_id">{{form.password1.label}}</label>{{form.password1}}{{form.password1.errors}}</li> + <li><label for="password2_id">{{form.password2.label}}</label>{{form.password2}}{{form.password2.errors}}</li> </ul> - <p style="margin:10px 0px 0px 3px;">{% trans "receive updates motivational blurb" %}</p> - {% include "edit_user_email_feeds_form.html" %} + <p class="signup_p">{% trans "receive updates motivational blurb" %}</p> + <p class="signup_p">{% trans "Please select your preferred email update schedule for the following groups of questions:" %}</p> + <div class='simple-subscribe-options'> + {{email_feeds_form.subscribe}} + </div> + <p class="signup_p">{% trans "Please read and type in the two words below to help us prevent automated account creation." %}</p> + {{form.recaptcha}} <div class="submit-row"><input type="submit" class="submit" value="{% trans "Create Account" %}" /> <strong>{% trans "or" %} <a href="{% url user_signin %}">{% trans "return to OpenID login" %}</a></strong></div> diff --git a/templates/badge.html b/templates/badge.html index 73cba4ba..af6aa2a2 100644 --- a/templates/badge.html +++ b/templates/badge.html @@ -28,7 +28,7 @@ </div> <div id="award-list" style="clear:both;margin-left:20px;line-height:25px;"> {% for award in awards %} - <p style="width:185px;float:left"><a href="{% url users %}{{ award.id }}/{{ award.name }}">{{ award.name }}</a> {% get_score_badge_by_details award.rep award.gold award.silver award.bronze %}</p> + <p style="width:180px;float:left"><a href="{% url users %}{{ award.id }}/{{ award.name }}">{{ award.name }}</a> {% get_score_badge_by_details award.rep award.gold award.silver award.bronze %}</p> {% endfor %} </div> diff --git a/templates/badges.html b/templates/badges.html index 1902a3b0..8de93df5 100644 --- a/templates/badges.html +++ b/templates/badges.html @@ -33,10 +33,10 @@ {% endifequal %} {% endfor %} </div> - <div style="float:left;width:180px;"> + <div style="float:left;width:230px;"> <a href="{{badge.get_absolute_url}}" title="{{ badge.get_type_display }} : {{ badge.description }}" class="medal"><span class="badge{{ badge.type }}">●</span> {{ badge.name }}</a><strong> × {{ badge.awarded_count|intcomma }}</strong> </div> - <p style="float:left;width:350px;"> + <p style="float:left;margin-top:8px;"> {{ badge.description }} </p> </div> diff --git a/templates/base.html b/templates/base.html index a33512cc..17a32ef2 100755 --- a/templates/base.html +++ b/templates/base.html @@ -11,7 +11,7 @@ {% endspaceless %} <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> {% if settings.GOOGLE_SITEMAP_CODE %} - <meta name="verify-v1" content="{{settings.GOOGLE_SITEMAP_CODE}}" /> + <meta name="google-site-verification" content="{{settings.GOOGLE_SITEMAP_CODE}}" /> {% endif %} <link rel="shortcut icon" href="{% href "/content/images/favicon.ico" %}" /> <link href="{% href "/content/style/style.css" %}" rel="stylesheet" type="text/css" /> @@ -46,7 +46,7 @@ body { margin-top:2.4em; } </style> <script type="text/javascript"> - $().ready(function() { + $(document).ready(function() { $('#validate_email_alert').click(function(){notify.close(true)}) notify.show(); }); diff --git a/templates/base_content.html b/templates/base_content.html index 78e5fe38..eacdc6d0 100644 --- a/templates/base_content.html +++ b/templates/base_content.html @@ -7,7 +7,7 @@ <title>{% block title %}{% endblock %} - {{ settings.APP_TITLE }}</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> {% if settings.GOOGLE_SITEMAP_CODE %} - <meta name="verify-v1" content="{{ settings.GOOGLE_SITEMAP_CODE }}" /> + <meta name="google-site-verification" content="{{ settings.GOOGLE_SITEMAP_CODE }}" /> {% endif %} <link rel="shortcut icon" href="{% href "/content/images/favicon.ico" %}" /> <link href="{% href "/content/style/style.css" %}" rel="stylesheet" type="text/css" /> @@ -48,7 +48,7 @@ body { margin-top:2.4em; } </style> <script type="text/javascript"> - $().ready(function() { + $(document).ready(function() { var element = $('#validate_email_alert') element.click(function(){notify.close(true);setTimeout(function(){},1000)}) notify.show(); @@ -64,7 +64,7 @@ {% autoescape off %} {% if user_messages %} {% for message in user_messages %} - <p class="darkred">{{ message }}<p> + <p class="darkred">{{ message }}</p> {% endfor %} {% endif %} {% endautoescape %} diff --git a/templates/content/images/logo.png b/templates/content/images/logo.png Binary files differdeleted file mode 100644 index b53732e3..00000000 --- a/templates/content/images/logo.png +++ /dev/null diff --git a/templates/content/jquery-openid/images/local-login.png b/templates/content/jquery-openid/images/local-login.png Binary files differdeleted file mode 100644 index 258cedac..00000000 --- a/templates/content/jquery-openid/images/local-login.png +++ /dev/null diff --git a/templates/content/jquery-openid/jquery.openid.js b/templates/content/jquery-openid/jquery.openid.js index 763af2c6..8d1cd204 100644 --- a/templates/content/jquery-openid/jquery.openid.js +++ b/templates/content/jquery-openid/jquery.openid.js @@ -53,7 +53,7 @@ $.fn.openid = function() { $localfs.fadeOut('slow'); $idfs.fadeOut('slow'); $id.val($this.find("li.highlight span").text()); - setTimeout(function(){$('#bsignin').click()},1000); + setTimeout(function(){$('#bsignin').click();},1000); return false; }; diff --git a/templates/content/js/com.cnprog.admin.js b/templates/content/js/com.cnprog.admin.js index cb1c1b15..39dff48c 100644 --- a/templates/content/js/com.cnprog.admin.js +++ b/templates/content/js/com.cnprog.admin.js @@ -1,4 +1,4 @@ -$().ready( function(){ +$(document).ready( function(){ var options = { success: function(a,b){$('.admin #action_status').html($.i18n._('changes saved'));}, dataType:'json', diff --git a/templates/content/js/com.cnprog.editor.js b/templates/content/js/com.cnprog.editor.js index 6cfa2c74..18cc5166 100644 --- a/templates/content/js/com.cnprog.editor.js +++ b/templates/content/js/com.cnprog.editor.js @@ -65,4 +65,4 @@ function ajaxFileUpload(imageUrl) ) return false; -}
\ No newline at end of file +} diff --git a/templates/content/js/com.cnprog.i18n.js b/templates/content/js/com.cnprog.i18n.js index 7562628b..da9bf396 100644 --- a/templates/content/js/com.cnprog.i18n.js +++ b/templates/content/js/com.cnprog.i18n.js @@ -1,3 +1,5 @@ + +//var i18nLang; var i18nZh = { 'insufficient privilege':'用户权限不在操作范围', 'cannot pick own answer as best':'不能设置自己的回答为最佳答案', diff --git a/templates/content/js/com.cnprog.post.js b/templates/content/js/com.cnprog.post.js index a073d20f..668c80fe 100644 --- a/templates/content/js/com.cnprog.post.js +++ b/templates/content/js/com.cnprog.post.js @@ -61,7 +61,7 @@ var Vote = function(){ var pleaseSeeFAQ = $.i18n._('please see') + "<a href='" + scriptUrl + $.i18n._("faq/") + "'>faq</a>"; - var favoriteAnonymousMessage = $.i18n._('anonymous users cannot select favorite questions') + var favoriteAnonymousMessage = $.i18n._('anonymous users cannot select favorite questions'); var voteAnonymousMessage = $.i18n._('anonymous users cannot vote') + pleaseLogin; var upVoteRequiredScoreMessage = $.i18n._('>15 points requried to upvote') + pleaseSeeFAQ; var downVoteRequiredScoreMessage = $.i18n._('>100 points required to downvote') + pleaseSeeFAQ; @@ -242,7 +242,8 @@ var Vote = function(){ url: scriptUrl + $.i18n._("questions/") + questionId + "/" + $.i18n._("vote/"), data: { "type": voteType, "postId": postId }, error: handleFail, - success: function(data){callback(object, voteType, data)}}); + success: function(data){callback(object, voteType, data);} + }); }; var handleFail = function(xhr, msg){ @@ -287,8 +288,9 @@ var Vote = function(){ object.attr("src", scriptUrl + "content/images/vote-favorite-off.png"); var fav = getFavoriteNumber(); fav.removeClass("my-favorite-number"); - if(data.count == 0) + if(data.count === 0){ data.count = ''; + } fav.text(data.count); } else if(data.success == "1"){ @@ -456,9 +458,12 @@ var Vote = function(){ submit($(object), voteType, callback_remove); } } - } + }; } (); +var questionComments = createComments('question'); +var answerComments = createComments('answer'); +var commentsFactory = {'question' : questionComments, 'answer' : answerComments}; // site comments function createComments(type) { @@ -482,12 +487,12 @@ function createComments(type) { $(jDiv).css('background','none'); $(jDiv).css('padding-left',0); if (canPostComments(id)) { - if (jDiv.find("#" + formId).length == 0) { + if (jDiv.find("#" + formId).length === 0) { var form = '<form id="' + formId + '" class="post-comments"><div>'; form += '<textarea name="comment" cols="60" rows="5" maxlength="300" onblur="'+ objectType +'Comments.updateTextCounter(this)" '; form += 'onfocus="' + objectType + 'Comments.updateTextCounter(this)" onkeyup="'+ objectType +'Comments.updateTextCounter(this)"></textarea>'; - form += '<input type="submit" value="' - + $.i18n._('add comment') + '" /><br><span class="text-counter"></span>'; + form += '<input type="submit" value="' + + $.i18n._('add comment') + '" /><br><span class="text-counter"></span>'; form += '<span class="form-error"></span></div></form>'; jDiv.append(form); @@ -499,20 +504,20 @@ function createComments(type) { } else { var divId = "comments-rep-needed-" + objectType + '-' + id; - if (jDiv.find("#" + divId).length == 0) { - jDiv.append('<p id="' + divId + '" class="comment">' - + $.i18n._('to comment, need') + ' ' + - + repNeededForComments + ' ' + $.i18n._('community karma points') - + '<a href="' + scriptUrl + $.i18n._('faq/') + '" class="comment-user">' - + $.i18n._('please see') + 'faq</a></span></p>'); + if (jDiv.find("#" + divId).length === 0) { + jDiv.append('<p id="' + divId + '" class="comment">' + + $.i18n._('to comment, need') + ' ' + + repNeededForComments + ' ' + $.i18n._('community karma points') + + '<a href="' + scriptUrl + $.i18n._('faq/') + '" class="comment-user">' + + $.i18n._('please see') + 'faq</a></span></p>'); } } }; var getComments = function(id, jDiv) { //appendLoaderImg(id); - $.getJSON(scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/") - , function(json) { showComments(id, json); }); + $.getJSON(scriptUrl + objectType + "s/" + id + "/" + $.i18n._("comments/"), + function(json) { showComments(id, json); }); }; var showComments = function(id, json) { @@ -523,8 +528,9 @@ function createComments(type) { jDiv.children().remove(); removeLoader(); if (json && json.length > 0) { - for (var i = 0; i < json.length; i++) + for (var i = 0; i < json.length; i++){ renderComment(jDiv, json[i]); + } jDiv.children().show(); } }; @@ -535,14 +541,14 @@ function createComments(type) { var img = scriptUrl + "content/images/close-small.png"; var imgHover = scriptUrl + "content/images/close-small-hover.png"; html += '<img class="delete-icon" onclick="' + objectType + 'Comments.deleteComment($(this), ' + post_id + ', \'' + delete_url + '\')" src="' + img; - html += '" onmouseover="$(this).attr(\'src\', \'' + imgHover + '\')" onmouseout="$(this).attr(\'src\', \'' + img + html += '" onmouseover="$(this).attr(\'src\', \'' + imgHover + '\')" onmouseout="$(this).attr(\'src\', \'' + img; html += '\')" title="' + $.i18n._('delete this comment') + '" />'; return html; } else{ return ''; } - } + }; // {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null} var renderComment = function(jDiv, json) { @@ -604,8 +610,8 @@ function createComments(type) { $(this).children().each( function(i){ var comment_id = $(this).attr('id').replace('comment-',''); - var delete_url = scriptUrl + objectType + 's/' + post_id + '/' - + $.i18n._('comments/') + comment_id + '/' + $.i18n._('delete/'); + var delete_url = scriptUrl + objectType + 's/' + post_id + '/' + + $.i18n._('comments/') + comment_id + '/' + $.i18n._('delete/'); var html = $(this).html(); var CommentsClass; if (objectType == 'question'){ @@ -638,14 +644,14 @@ function createComments(type) { jDiv.show(); var link = $('#comments-link-' + objectType + '-' + id); - if (canPostComments(id)) link.parent().find("textarea").get(0).focus(); + if (canPostComments(id)) { link.parent().find("textarea").get(0).focus(); } link.remove(); }, hide: function(id) { var jDiv = jDivInit(id); var len = jDiv.children("div.comments").children().length; - var anchorText = len == 0 ? $.i18n._('add a comment') : $.i18n._('comments') + ' (<b>' + len + "</b>)"; + var anchorText = len === 0 ? $.i18n._('add a comment') : $.i18n._('comments') + ' (<b>' + len + "</b>)"; jDiv.hide(); jDiv.siblings("a").unbind("click").click(function() { commentsFactory[objectType].show(id); }).html(anchorText); @@ -666,23 +672,18 @@ function createComments(type) { var length = textarea.value ? textarea.value.length : 0; var color = length > 270 ? "#f00" : length > 200 ? "#f60" : "#999"; var jSpan = $(textarea).siblings("span.text-counter"); - jSpan.html($.i18n._('can write') - + (300 - length) + ' ' - + $.i18n._('characters')).css("color", color); + jSpan.html($.i18n._('can write') + + (300 - length) + ' ' + + $.i18n._('characters')).css("color", color); } }; } -var questionComments = createComments('question'); -var answerComments = createComments('answer'); - -$().ready(function() { +$(document).ready(function() { questionComments.init(); answerComments.init(); }); -var commentsFactory = {'question' : questionComments, 'answer' : answerComments}; - /* Prettify http://www.apache.org/licenses/LICENSE-2.0 diff --git a/templates/content/js/com.cnprog.tag_selector.js b/templates/content/js/com.cnprog.tag_selector.js index f6c16c9c..06aefcfc 100644 --- a/templates/content/js/com.cnprog.tag_selector.js +++ b/templates/content/js/com.cnprog.tag_selector.js @@ -1,7 +1,8 @@ +//var scriptUrl, interestingTags, ignoredTags, tags, $; function pickedTags(){ var sendAjax = function(tagname, reason, action, callback){ - url = scriptUrl; + var url = scriptUrl; if (action == 'add'){ url += $.i18n._('mark-tag/'); if (reason == 'good'){ @@ -16,15 +17,15 @@ function pickedTags(){ } url = url + tagname + '/'; - call_settings = { + var call_settings = { type:'POST', url:url - } - if (callback != false){ - call_settings['success'] = callback; + }; + if (callback !== false){ + call_settings.success = callback; } $.ajax(call_settings); - } + }; var unpickTag = function(from_target ,tagname, reason, send_ajax){ @@ -32,7 +33,7 @@ function pickedTags(){ var deleteTagLocally = function(){ from_target[tagname].remove(); delete from_target[tagname]; - } + }; if (send_ajax){ sendAjax(tagname,reason,'remove',deleteTagLocally); } @@ -40,7 +41,7 @@ function pickedTags(){ deleteTagLocally(); } - } + }; var setupTagDeleteEvents = function(obj,tag_store,tagname,reason,send_ajax){ obj.unbind('mouseover').bind('mouseover', function(){ @@ -52,12 +53,13 @@ function pickedTags(){ obj.click( function(){ unpickTag(tag_store,tagname,reason,send_ajax); }); - } + }; var handlePickedTag = function(obj,reason){ var tagname = $.trim($(obj).prev().attr('value')); - to_target = interestingTags; - from_target = ignoredTags; + var to_target = interestingTags; + var from_target = ignoredTags; + var to_tag_container; if (reason == 'bad'){ to_target = ignoredTags; from_target = interestingTags; @@ -78,13 +80,13 @@ function pickedTags(){ //send ajax request to pick this tag sendAjax(tagname,reason,'add',function(){ - new_tag = $('<span></span>'); + var new_tag = $('<span></span>'); new_tag.addClass('deletable-tag'); - tag_link = $('<a></a>'); + var tag_link = $('<a></a>'); tag_link.attr('rel','tag'); tag_link.attr('href', scriptUrl + $.i18n._('tags/') + tagname); tag_link.html(tagname); - del_link = $('<img></img>'); + var del_link = $('<img></img>'); del_link.addClass('delete-icon'); del_link.attr('src', scriptUrl + 'content/images/close-small-dark.png'); @@ -97,7 +99,7 @@ function pickedTags(){ to_target[tagname] = new_tag; }); } - } + }; var collectPickedTags = function(){ var good_prefix = 'interesting-tag-'; @@ -108,7 +110,8 @@ function pickedTags(){ ignoredTags = {}; $('.deletable-tag').each( function(i,item){ - item_id = $(item).attr('id') + var item_id = $(item).attr('id'); + var tag_name, tag_store; if (good_re.test(item_id)){ tag_name = item_id.replace(good_prefix,''); tag_store = interestingTags; @@ -123,10 +126,10 @@ function pickedTags(){ return; } tag_store[tag_name] = $(item); - setupTagDeleteEvents($(item).find('img'),tag_store,tag_name,reason,true) + setupTagDeleteEvents($(item).find('img'),tag_store,tag_name,reason,true); } ); - } + }; var setupHideIgnoredQuestionsControl = function(){ $('#hideIgnoredTagsCb').unbind('click').click(function(){ @@ -138,7 +141,7 @@ function pickedTags(){ data: {command:'toggle-ignored-questions'} }); }); - } + }; return { init: function(){ collectPickedTags(); @@ -157,8 +160,8 @@ function pickedTags(){ } }); - $("#interestingTagAdd").click(function(){handlePickedTag(this,'good')}); - $("#ignoredTagAdd").click(function(){handlePickedTag(this,'bad')}); + $("#interestingTagAdd").click(function(){handlePickedTag(this,'good');}); + $("#ignoredTagAdd").click(function(){handlePickedTag(this,'bad');}); } }; } diff --git a/templates/content/js/com.cnprog.utils.js b/templates/content/js/com.cnprog.utils.js index b19b6773..4c3aafba 100644 --- a/templates/content/js/com.cnprog.utils.js +++ b/templates/content/js/com.cnprog.utils.js @@ -1,6 +1,7 @@ +//var $, scriptUrl; var showMessage = function(object, msg) { - var div = $('<div class="vote-notification"><h3>' + msg + '</h3>(' - + $.i18n._('click to close') + ')</div>'); + var div = $('<div class="vote-notification"><h3>' + msg + '</h3>(' + + $.i18n._('click to close') + ')</div>'); div.click(function(event) { $(".vote-notification").fadeOut("fast", function() { $(this).remove(); }); @@ -35,18 +36,30 @@ var notify = function() { } (); function appendLoader(containerSelector) { - $(containerSelector).append('<img class="ajax-loader" ' - +'src="' + scriptUrl + 'content/images/indicator.gif" title="' - +$.i18n._('loading...') - +'" alt="' - +$.i18n._('loading...') - +'" />'); + $(containerSelector).append('<img class="ajax-loader" ' + + 'src="' + scriptUrl + 'content/images/indicator.gif" title="' + + $.i18n._('loading...') + + '" alt="' + + $.i18n._('loading...') + + '" />'); } function removeLoader() { $("img.ajax-loader").remove(); } +function setSubmitButtonDisabled(formSelector, isDisabled) { + $(formSelector).find("input[type='submit']").attr("disabled", isDisabled ? "true" : ""); +} + +function enableSubmitButton(formSelector) { + setSubmitButtonDisabled(formSelector, false); +} + +function disableSubmitButton(formSelector) { + setSubmitButtonDisabled(formSelector, true); +} + function setupFormValidation(formSelector, validationRules, validationMessages, onSubmitCallback) { enableSubmitButton(formSelector); $(formSelector).validate({ @@ -56,7 +69,7 @@ function setupFormValidation(formSelector, validationRules, validationMessages, errorClass: "form-error", errorPlacement: function(error, element) { var span = element.next().find("span.form-error"); - if (span.length == 0) { + if (span.length === 0) { span = element.parent().find("span.form-error"); } span.replaceWith(error); @@ -64,24 +77,16 @@ function setupFormValidation(formSelector, validationRules, validationMessages, submitHandler: function(form) { disableSubmitButton(formSelector); - if (onSubmitCallback) + if (onSubmitCallback){ onSubmitCallback(); - else + } + else{ form.submit(); + } } }); } -function enableSubmitButton(formSelector) { - setSubmitButtonDisabled(formSelector, false); -} -function disableSubmitButton(formSelector) { - setSubmitButtonDisabled(formSelector, true); -} -function setSubmitButtonDisabled(formSelector, isDisabled) { - $(formSelector).find("input[type='submit']").attr("disabled", isDisabled ? "true" : ""); -} - var CPValidator = function(){ return { getQuestionFormRules : function(){ @@ -108,11 +113,11 @@ var CPValidator = function(){ }, text: { required: " " + $.i18n._('content cannot be empty'), - minlength: jQuery.format(' ' + $.i18n._('content minchars')) + minlength: $.format(' ' + $.i18n._('content minchars')) }, title: { required: " " + $.i18n._('please enter title'), - minlength: jQuery.format(' ' + $.i18n._('title minchars')) + minlength: $.format(' ' + $.i18n._('title minchars')) } }; } diff --git a/templates/content/js/mediawiki-login.js b/templates/content/js/mediawiki-login.js new file mode 100644 index 00000000..f1805f88 --- /dev/null +++ b/templates/content/js/mediawiki-login.js @@ -0,0 +1,29 @@ +function toggleScreenNameInput(par1,par2){ + if ($(this).is(':checked')){ + $('.optional-screen-name').show(); + } + else { + $('#id_screen_name').val(''); + $('.optional-screen-name').hide(); + } +} + +function toggleScreenNameErrorMessage(e){ + var screen_name = $('#id_screen_name').val(); + if (screen_name != ''){ + $('.screen-name-error').hide(); + } + else{ + $('.screen-name-error').show(); + } +} + +$(document).ready( function(){ + var screen_name = $('#id_screen_name').val(); + var use_screen_name = $('#id_use_separate_screen_name').is(':checked'); + if (screen_name == '' && !use_screen_name){ + $('.optional-screen-name').hide(); + } + $('#id_use_separate_screen_name').unbind('click').click(toggleScreenNameInput); + $('#id_screen_name').unbind('keyup').keyup(toggleScreenNameErrorMessage); +}); diff --git a/templates/content/style/mediawiki-login.css b/templates/content/style/mediawiki-login.css new file mode 100644 index 00000000..58813c7c --- /dev/null +++ b/templates/content/style/mediawiki-login.css @@ -0,0 +1,63 @@ +#mediawiki-login legend { + font-weight:bold; + font-size:14px; +} + +#mediawiki-login fieldset { + border:none; +} + +#mediawiki-login ul { + list-style: none; + list-style-position: outside; + padding: 0px; + margin: 10px 0 0 0; +} + +#mediawiki-login p { + margin:0; +} + +#mediawiki-login div.login-information label { + width: 180px; + display: inline-block; +} + +#mediawiki-login legend { + padding: 0px; +} + +#mediawiki-login h2 { + margin:10px 0 0 0; + padding:3px 0 3px 0; + border:none; + font-family:sans-serif; + font-size:16.8px; + font-weight:bold; +} + +#mediawiki-login tr { + vertical-align: top; +} + +#id_screen_name { + margin-left:25px; +} + +#mediawiki-login input { + height: 20px; +} + +#mediawiki-login input.submit { + margin-top:5px; + margin-left:5px; + display:block; + clear:both; + font-weight: bold; + font-size:14.4px; + height:33px; + /*padding: 4px 6px 4px 6px;*/ + text-align: center; + border: 1px solid #777777; + background: #D4D0C8; +} diff --git a/templates/content/style/style.css b/templates/content/style/style.css index 47b4dc00..aba67eee 100644 --- a/templates/content/style/style.css +++ b/templates/content/style/style.css @@ -686,7 +686,12 @@ table.form-as-table th { /*.form-row li label { display: inline }*/ -.submit-row{line-height:30px;padding-top:10px;} +.submit-row{ + line-height:30px; + padding-top:10px; + display: block; + clear: both; +} .errors{line-height:20px;color:red;} .error{ color:darkred; @@ -1158,7 +1163,7 @@ ul.bulleta li {background:url(../images/bullet_green.gif) no-repeat 0px 2px; pad .message p { margin-bottom:0px; } -.message p.space-above { +p.space-above { margin-top:10px; } @@ -1446,3 +1451,16 @@ ul.form-horizontal-rows li input { #hideIgnoredTagsCb { margin: 0 2px 0 1px; } +#recaptcha_widget_div { + width:318px; + float:left; + clear:both; +} +p.signup_p { + margin: 20px 0px 0px 0px; +} +.simple-subscribe-options ul { + list-style:none; + list-style-position:outside; + margin:0; +} diff --git a/templates/footer.html b/templates/footer.html index 9d19b41e..66feff8a 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -7,8 +7,6 @@ <div class="footerLinks" > <a href="{% url about %}">{% trans "about" %}</a><span class="link-separator"> |</span> <a href="{% url faq %}">{% trans "faq" %}</a><span class="link-separator"> |</span> - <!--<a href="{{ blog_url }}">{% trans "blog" %}</a><span class="link-separator"> |</span>--> - <!--<a href="{{ webmaster_email }}">{% trans "contact us" %}</a><span class="link-separator"> |</span>--> <a href="{% url privacy %}">{% trans "privacy policy" %}</a><span class="link-separator"> |</span> {% spaceless %} <a href= @@ -25,7 +23,6 @@ <p> <a href="http://github.com/cnprog/CNPROG/network" target="_blank"> powered by cnprog platform - <!--<img src="{% href "/content/images/djangomade124x25_grey.gif" %}" border="0" alt="Made with Django." title="Made with Django." >--> </a> </p> </div> @@ -36,14 +33,16 @@ </div> </div> <!-- 页面底部结束: --> - <script type="text/javascript"> - var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); - document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); - </script> - <script type="text/javascript"> - try { - var pageTracker = _gat._getTracker('{{ settings.GOOGLE_ANALYTICS_KEY }}'); - pageTracker._trackPageview(); - } catch(err) {} + {% if settings.GOOGLE_ANALYTICS_KEY %} + <script type="text/javascript"> + var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); + document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); </script> + <script type="text/javascript"> + try { + var pageTracker = _gat._getTracker('{{ settings.GOOGLE_ANALYTICS_KEY }}'); + pageTracker._trackPageview(); + } catch(err) {} + </script> + {% endif %} <!-- end template footer.html --> diff --git a/templates/header.html b/templates/header.html index 42074763..466659de 100644 --- a/templates/header.html +++ b/templates/header.html @@ -32,12 +32,6 @@ {% endif %} <a id="nav_badges" href="{% url badges %}">{% trans "badges" %}</a> <a id="nav_unanswered" href="{% url unanswered %}">{% trans "unanswered questions" %}</a> - - {% comment %}<!-- i think this needs to be removed -e.f. --> - {% if request.user.is_authenticated %} - <a id="nav_profile" href="{% url user %}{{ request.user.id }}/{{ request.user.username }}/">{% trans "my profile" %}</a> - {% endif %} - {% endcomment %} <div class="focus"> <a id="nav_ask" href="{% url ask %}" class="special">{% trans "ask a question" %}</a> </div> diff --git a/templates/mediawiki/mediawiki_signup.html b/templates/mediawiki/mediawiki_signup.html new file mode 100644 index 00000000..d1ecb96c --- /dev/null +++ b/templates/mediawiki/mediawiki_signup.html @@ -0,0 +1,9 @@ +{% extends "base_content.html" %} +{% load i18n %} +{% block forejs %} + {{form.media}} +{% endblock %} +{% block title %}{% spaceless %}{% trans "MediaWiki User Registration" %}{% endspaceless %}{% endblock %} +{% block content %} +{% include "mediawiki/mediawiki_signup_content.html" %} +{% endblock %} diff --git a/templates/mediawiki/mediawiki_signup_content.html b/templates/mediawiki/mediawiki_signup_content.html new file mode 100644 index 00000000..a09de107 --- /dev/null +++ b/templates/mediawiki/mediawiki_signup_content.html @@ -0,0 +1,110 @@ +{% load smart_if %} +{% load i18n %} +{% if request.is_include_virtual == False %} +<div class="headNormal"> + {% trans "MediaWiki User Registration" %} +</div> +{% endif %} +<div id="mediawiki-login"> +{% if request.is_include_virtual %} +<form name="fregister" action="/wiki/index.php" accept-charset="UTF-8" method="post"> + <input type="hidden" name="command" value="/backend/account/yourwiki/signup/"/> + <input type="hidden" name="title" value="Special:UserRegister"/> +{% else %} +<form name="fregister" action="." accept-charset="UTF-8" method="post"> +{% endif %} + {% with form as f %} + {{ f.next }} + <h2>{% trans "Basic information" %}</h2> + <div class="login-information"> + <p> + <label for="id_login_name">{{f.login_name.label}}</label> + {{f.login_name}} + </p> + {% if f.login_name.errors %} + <p class="error">{{ f.login_name.errors|join:", " }}</p> + {% endif %} + <p> + <label for="id_password1">{{f.password1.label}}</label> + {{f.password1}} + {% if f.password1.errors %} + <span class="error">{{ f.password1.errors|join:", " }}</span> + {% endif %} + </p> + <p> + <label for="id_password2">{{f.password2.label}}</label> + {{f.password2}} + {% if f.password2.errors %} + <span class="error">{{ f.password2.errors|join:", " }}</span> + {% endif %} + </p> + <p> + <label for="id_email">{{f.email.label}}</label> + {{f.email}} + {% if f.email.errors %} + <span class="error">{{ f.email.errors|join:", " }}</span> + {% endif %} + </p> + </div> + <h2>{% trans "Your Name" %}</h2> + <p>{% trans "<strong>1) Real name</strong> - required for the Wiki, but not shown on the forum by default" %}</p> + <table> + <tr> + <td><label for="id_first_name">{{f.first_name.label}}</label></td> + <td><label for="id_screen_name">{{f.last_name.label}}</label></td> + <td><label for="id_user_title">{{f.user_title.label}}</label></td> + </tr> + <tr> + <td> + {{f.first_name}} + {% if f.first_name.errors %} + <p class="error">{{ f.first_name.errors|join:", " }}</p> + {% endif %} + </td> + <td> + {{f.last_name}} + {% if f.last_name.errors %} + <p class="error">{{ f.last_name.errors|join:", " }}</p> + {% endif %} + </td> + <td> + {{f.user_title}} + {% if f.user_title.errors %} + <p class="error">{{ f.user_title.errors|join:", " }}</p> + {% endif %} + </td> + </tr> + </table> + <p>{% trans "<strong>2) Forum screen name</strong>" %}</p> + <p>{% trans "Just skip this to use your full name at the forum, otherwise please check below" %}</p> + <p> + {{f.use_separate_screen_name}} + <label for="id_use_separate_screen_name"> + {{f.use_separate_screen_name.label}} + </label> + </p> + <p class="optional-screen-name"> + {{f.screen_name}} + </p> + {% if f.screen_name.errors %} + <p class="error screen-name-error">{{ f.screen_name.errors|join:", " }}</p> + {% endif %} + <p class="optional-screen-name">{% trans "Please remember that forum screen name is not your login name.<br/>Screen name allows you stay anonymous at the forum - you can change it at any time too. Login name cannot be changed." %}</p> + <h2>{% trans "Update subscription" %}</h2> + <p>{% trans "receive updates motivational blurb" %}</p> + {{f.subscribe}} + {% if f.subscribe.errors %} + <p class="error">{{ f.subscribe.errors|join:", " }}</p> + {% endif %} + <h2>{% trans "Almost there..." %}</h2> + <p>{% trans "recaptcha explained" %}</p> + <p>{{f.recaptcha.errors|join:", "}} + <div style="clear:both; display:block;">{{f.recaptcha}}</div> + {% endwith %} + <input type="submit" value="{% trans "Create account" %}" class="submit"/> + {% comment %}<!-- this stuff was used for the wizard that fails with recaptcha so commented out--> + <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" /> + {{ previous_fields|safe }} + {% endcomment %} + </form> +</div> diff --git a/templates/mediawiki/thanks_for_joining.html b/templates/mediawiki/thanks_for_joining.html new file mode 100644 index 00000000..9695ba05 --- /dev/null +++ b/templates/mediawiki/thanks_for_joining.html @@ -0,0 +1,76 @@ +{% spaceless %} +{% load smart_if %} +{% with wiki_user.user_name as user_name %} +<script type="text/javascript"> +$wgUserName = '{{user_name}}'; +$(document).ready(function(){ +$('#p-personal div').html('\ +<ul>\ +<li id="pt-0"><a href="/wiki/index.php?title=User:{{user_name}}" class="new">{{user_name}}</a></li>\ +<li id="pt-1"><a href="/wiki/index.php?title=UserWiki:{{user_name}}" class="new">my page</a></li>\ +<li id="pt-mytalk"><a href="/wiki/index.php?title=User_talk:{{user_name}}" title="My talk page [n]" accesskey="n" class="new">My talk</a></li>\ +<li id="pt-preferences"><a href="/wiki/index.php?title=Special:Preferences" title="My preferences">My preferences</a></li>\ +<li id="pt-watchlist"><a href="/wiki/index.php?title=Special:Watchlist" title="The list of pages you\'re monitoring for changes [l]" accesskey="l">My watchlist</a></li>\ +<li id="pt-mycontris"><a href="/wiki/index.php?title=Special:Contributions/{{user_name}}" title="List of my contributions [y]" accesskey="y">My contributions</a></li>\ +<li id="pt-logout"><a href="/wiki/index.php?title=Special:Userlogout&returnto=Special:UserRegister" title="Log out">Log out</a></li>\ +</ul>\ +'); +}); +</script> +{% endwith %} +{% if wiki_user.user_title == 'prof' %} +<script type="text/javascript"> + var documentTitle = 'Professor {{wiki_user.user_last_name}}, Welcome to Wiki!'; +</script> +{% else %} + {% if wiki_user.title == 'dr' %} + <script type="text/javascript"> + var documentTitle = 'Dr. {{wiki_user.user_last_name}}, Welcome to Wiki!'; + </script> + {% else %} + <script type="text/javascript"> + var documentTitle = '{{wiki_user.user_first_name}}, Welcome to Wiki!'; + </script> + {% endif %} +{% endif %} +<script type="text/javascript"> + $(document).ready( function(){ + document.title = documentTitle; + $('h1.firstHeading').html(documentTitle); + }); +</script> +{% if wiki_user.user_title == 'prof' %} +<p>Dear Professor {{wiki_user.user_last_name}}, +{% else %} + {% if wiki_user.title == 'dr' %} + <p>Dear Dr. {{wiki_user.user_last_name}}, + {% else %} + <p>Dear {{wiki_user.user_first_name}}, + {% endif %} +{% endif %} +thanks joining Wiki!</p> +<p><strong>Could you help our community right now?</strong><br/> +Please answer some of the questions from our Q&A forum:</p> +<ul> + {% for q in questions %} + <li><a href="http://yourwiki.org/question/{{q.id}}/{{q.title|slugify}}">{{q.title}}</a></li> + {% endfor %} +</ul> +<p>Your answers will be <strong>indispensable</strong>.<br/> +Please feel free to ask something too! Hopefully you will like this forum and the wiki and invite your coworkers and friends to join. +</p> +<p>Might you consider <strong>sharing some of the digital documentation and pulse sequences</strong> that +perhaps had accumulated in your lab?<br/> It's very easy to upload +files to the wiki as it is to edit the pages directly. +</p> +<p>Best wishes,<br/> +Wiki Server Admin. +</p> +<p>P.S. An email with the confirmation code has been sent to <strong>{{wiki_user.user_email}}</strong>. +Please follow the included link to confirm your email address. +{% if wiki_user.user_title == 'prof' %} +<br/> +Also, you are always welcome to <strong>advertise open positions</strong> in your laboratory on the wiki. </p> +{% endif %} +</p> +{% endspaceless %} diff --git a/templates/mediawiki/welcome_email.txt b/templates/mediawiki/welcome_email.txt new file mode 100644 index 00000000..c282d9e5 --- /dev/null +++ b/templates/mediawiki/welcome_email.txt @@ -0,0 +1,28 @@ +{% spaceless %} +{% load i18n %} +{% load smart_if %} +{% if title == 'prof' %} +{% blocktrans %}Dear Professor {{last_name}},{% endblocktrans %} +{% endif %} +{% if title == 'dr' %} +{% blocktrans %}Dear Dr. {{last_name}},{% endblocktrans %} +{% endif %} +{% if title == 'none' %} +{% blocktrans %}Dear {{first_name}},{% endblocktrans %} +{% endif %} +{% endspaceless %} + +{% trans "Thank you for joining OSQA online community!" %} + +{% trans "A very brief introduction to OSQA community follows this technical information, included for your record:" %} +{% blocktrans %}* please visit {{email_confirmation_url}} to confirm your email for the OSQA wiki +* your OSQA login name is {{login_name}}, email address {{user_email}}. +* password recovery information can be always found here: {{password_recovery_url}}{% endblocktrans %} + +{% trans "A brief introduction to the OSQA online community for the new user." %} + +{% blocktrans %}Sincerely, +Adminstrator of the OSQA website.{% endblocktrans %} + +{% blocktrans %}P.S. If you believe that this message was sent in error please tell us +about it by email at {{admin_email}}.{% endblocktrans %} diff --git a/templates/mediawiki/welcome_professor_email.txt b/templates/mediawiki/welcome_professor_email.txt new file mode 100644 index 00000000..6b05889d --- /dev/null +++ b/templates/mediawiki/welcome_professor_email.txt @@ -0,0 +1,19 @@ +{% spaceless %} +{% load i18n %} +{% blocktrans %}Dear Professor {{last_name}},{% endblocktrans %} +{% endspaceless %} + +{% trans "Thanks a lot for joining OSQA online community!" %} + +{% trans "A very brief introduction to OSQA community follows this technical information, included for your record:" %} +{% blocktrans %}* please visit {{email_confirmation_url}} to confirm your email for the OSQA wiki +* your OSQA login name is {{login_name}}, email address {{user_email}}. +* password recovery information can be always found here: {{password_recovery_url}}{% endblocktrans %} + +{% trans "A brief introduction to the OSQA online community for the new professor user." %} + +{% blocktrans %}Sincerely, +Adminstrator of the OSQA website.{% endblocktrans %} + +{% blocktrans %}P.S. If you believe that this message was sent in error please tell us +about it by email at {{admin_email}}.{% endblocktrans %} diff --git a/templates/notarobot.html b/templates/notarobot.html new file mode 100644 index 00000000..698c5696 --- /dev/null +++ b/templates/notarobot.html @@ -0,0 +1,15 @@ +{% extends "base_content.html" %} +{% load i18n %} +{% block title %}{% spaceless %}{% trans "Please prove that you are a Human Being" %}{% endspaceless %}{% endblock %} +{% block content %} +{% comment %} this form is set up to be used in wizards {% endcomment %} +<form name="notarobot" action="." method="POST"> + <div> + {{form}} + </div> + <input type="submit" value="{% trans "I am a Human Being" %}" class="submit" style="float:left"/> + <input type="hidden" name="{{ step_field }}" value="{{ step0 }}" /> + {{ previous_fields|safe }} + </form> +</form> +{% endblock %} diff --git a/templates/tag_selector.html b/templates/tag_selector.html index 6edc5cc8..94d23f3c 100644 --- a/templates/tag_selector.html +++ b/templates/tag_selector.html @@ -37,6 +37,6 @@ <input id="ignoredTagAdd" type="submit" value="{% trans "Add" %}"/> <p id="hideIgnoredTagsControl"> <input id="hideIgnoredTagsCb" type="checkbox" {% if request.user.hide_ignored_questions %}checked="checked"{% endif %} /> - <label id="hideIgnoredTagsLabel" for="hideIgnoredTags">{% trans "keep ingored questions hidden" %}</label> + <label id="hideIgnoredTagsLabel" for="hideIgnoredTagsCb">{% trans "keep ingored questions hidden" %}</label> <p> </div> @@ -1,6 +1,6 @@ from django.conf.urls.defaults import * from django.utils.translation import ugettext as _ -import settings +from django.conf import settings from django.contrib import admin admin.autodiscover() diff --git a/utils/forms.py b/utils/forms.py new file mode 100644 index 00000000..c54056ca --- /dev/null +++ b/utils/forms.py @@ -0,0 +1,151 @@ +from django import forms +import re +from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe +from django.conf import settings +from django.http import str_to_unicode +from django.contrib.auth.models import User +import urllib + +DEFAULT_NEXT = '/' + getattr(settings, 'FORUM_SCRIPT_ALIAS') +def clean_next(next): + if next is None: + return DEFAULT_NEXT + next = str_to_unicode(urllib.unquote(next), 'utf-8') + next = next.strip() + if next.startswith('/'): + return next + return DEFAULT_NEXT + +def get_next_url(request): + return clean_next(request.REQUEST.get('next')) + +class StrippedNonEmptyCharField(forms.CharField): + def clean(self,value): + value = value.strip() + if self.required and value == '': + raise forms.ValidationError(_('this field is required')) + return value + +class NextUrlField(forms.CharField): + def __init__(self): + super(NextUrlField,self).__init__(max_length = 255,widget = forms.HiddenInput(),required = False) + def clean(self,value): + return clean_next(value) + +login_form_widget_attrs = { 'class': 'required login' } +username_re = re.compile(r'^[\w ]+$') + +class UserNameField(StrippedNonEmptyCharField): + RESERVED_NAMES = (u'fuck', u'shit', u'ass', u'sex', u'add', + u'edit', u'save', u'delete', u'manage', u'update', 'remove', 'new') + def __init__(self,db_model=User, db_field='username', must_exist=False,skip_clean=False,label=_('choose a username'),**kw): + self.must_exist = must_exist + self.skip_clean = skip_clean + self.db_model = db_model + self.db_field = db_field + error_messages={'required':_('user name is required'), + 'taken':_('sorry, this name is taken, please choose another'), + 'forbidden':_('sorry, this name is not allowed, please choose another'), + 'missing':_('sorry, there is no user with this name'), + 'multiple-taken':_('sorry, we have a serious error - user name is taken by several users'), + 'invalid':_('user name can only consist of letters, empty space and underscore'), + } + if 'error_messages' in kw: + error_messages.update(kw['error_messages']) + del kw['error_messages'] + super(UserNameField,self).__init__(max_length=30, + widget=forms.TextInput(attrs=login_form_widget_attrs), + label=label, + error_messages=error_messages, + **kw + ) + + def clean(self,username): + """ validate username """ + if self.skip_clean == True: + return username + if hasattr(self, 'user_instance') and isinstance(self.user_instance, User): + if username == self.user_instance.username: + return username + try: + username = super(UserNameField, self).clean(username) + except forms.ValidationError: + raise forms.ValidationError(self.error_messages['required']) + if self.required and not username_re.search(username): + raise forms.ValidationError(self.error_messages['invalid']) + if username in self.RESERVED_NAMES: + raise forms.ValidationError(self.error_messages['forbidden']) + try: + user = self.db_model.objects.get( + **{'%s' % self.db_field : username} + ) + if user: + if self.must_exist: + return username + else: + raise forms.ValidationError(self.error_messages['taken']) + except self.db_model.DoesNotExist: + if self.must_exist: + raise forms.ValidationError(self.error_messages['missing']) + else: + return username + except self.db_model.MultipleObjectsReturned: + raise forms.ValidationError(self.error_messages['multiple-taken']) + +class UserEmailField(forms.EmailField): + def __init__(self,skip_clean=False,**kw): + self.skip_clean = skip_clean + super(UserEmailField,self).__init__(widget=forms.TextInput(attrs=dict(login_form_widget_attrs, + maxlength=200)), label=mark_safe(_('your email address')), + error_messages={'required':_('email address is required'), + 'invalid':_('please enter a valid email address'), + 'taken':_('this email is already used by someone else, please choose another'), + }, + **kw + ) + + def clean(self,email): + """ validate if email exist in database + from legacy register + return: raise error if it exist """ + email = super(UserEmailField,self).clean(email.strip()) + if self.skip_clean: + return email + if settings.EMAIL_UNIQUE == True: + try: + user = User.objects.get(email = email) + raise forms.ValidationError(self.error_messsages['taken']) + except User.DoesNotExist: + return email + except User.MultipleObjectsReturned: + raise forms.ValidationError(self.error_messages['taken']) + else: + return email + +class SetPasswordForm(forms.Form): + password1 = forms.CharField(widget=forms.PasswordInput(attrs=login_form_widget_attrs), + label=_('choose password'), + error_messages={'required':_('password is required')}, + ) + password2 = forms.CharField(widget=forms.PasswordInput(attrs=login_form_widget_attrs), + label=mark_safe(_('retype password')), + error_messages={'required':_('please, retype your password'), + 'nomatch':_('sorry, entered passwords did not match, please try again')}, + ) + def clean_password2(self): + """ + Validates that the two password inputs match. + + """ + if 'password1' in self.cleaned_data: + if self.cleaned_data['password1'] == self.cleaned_data['password2']: + self.password = self.cleaned_data['password2'] + self.cleaned_data['password'] = self.cleaned_data['password2'] + return self.cleaned_data['password2'] + else: + del self.cleaned_data['password2'] + raise forms.ValidationError(self.fields['password2'].error_messages['nomatch']) + else: + return self.cleaned_data['password2'] + |