From ad2e22b999b3b795f60e0f95abcaf3b339567294 Mon Sep 17 00:00:00 2001 From: Evgeny Fadeev Date: Sun, 24 Jan 2010 19:53:24 -0500 Subject: recaptcha for conventional registration\n\ simpler email subscription form at registration\n\ fixed urls in rss feed\n\ added experimental remote password login api (cleartext password for remote site entered locally)\n\ included example for Mediawiki Authentication plugin\n\ very simple message to everyone management command --- INSTALL | 9 + cnprog.wsgi | 5 +- cron/send_email_alerts | 6 +- django_authopenid/external_login.py | 103 ------- django_authopenid/forms.py | 185 ++---------- django_authopenid/urls.py | 33 ++- django_authopenid/util.py | 16 +- django_authopenid/views.py | 103 +++---- dos2unix.sh | 2 + drop-auth.sql | 8 + forum/feed.py | 6 +- forum/forms.py | 48 +++- forum/management/commands/message_to_everyone.py | 12 + forum/management/commands/multi_award_badges.py | 2 +- forum/management/commands/send_email_alerts.py | 7 +- forum/management/commands/subscribe_everyone.py | 2 +- forum/models.py | 4 +- forum/sitemap.py | 3 + forum/urls.py | 2 +- forum/views.py | 17 +- locale/en/LC_MESSAGES/django.mo | Bin 26686 -> 26619 bytes locale/en/LC_MESSAGES/django.po | 2 +- mediawiki/PHPSerialize.py | 149 ++++++++++ mediawiki/PHPUnserialize.py | 187 ++++++++++++ mediawiki/README | 106 +++++++ mediawiki/UserRegister.alias.php | 7 + mediawiki/UserRegister.body.php | 14 + mediawiki/UserRegister.i18n.php | 6 + mediawiki/UserRegister.php | 24 ++ mediawiki/WsgiInjectableSpecialPage.php | 80 ++++++ mediawiki/__init__.py | 0 mediawiki/api.py | 138 +++++++++ mediawiki/auth.py | 59 ++++ mediawiki/forms.py | 164 +++++++++++ mediawiki/junk.py | 2 + mediawiki/middleware.py | 57 ++++ mediawiki/models.py | 312 +++++++++++++++++++++ mediawiki/templatetags/__init__.py | 0 mediawiki/templatetags/mediawikitags.py | 62 ++++ mediawiki/views.py | 192 +++++++++++++ middleware/anon_user.py | 4 +- middleware/cancel.py | 2 +- settings.py | 42 ++- settings_local.py.dist | 12 + sql_scripts/100108_upgrade_ef.sql | 4 + sql_scripts/badges.sql | 37 +++ templates/about.html | 23 +- templates/authopenid/complete.html | 14 +- .../authopenid/external_legacy_login_info.html | 2 +- templates/authopenid/signup.html | 14 +- templates/badge.html | 2 +- templates/badges.html | 4 +- templates/base.html | 4 +- templates/base_content.html | 6 +- templates/content/images/logo.png | Bin 1902 -> 0 bytes .../content/jquery-openid/images/local-login.png | Bin 2522 -> 0 bytes templates/content/jquery-openid/jquery.openid.js | 2 +- templates/content/js/com.cnprog.admin.js | 2 +- templates/content/js/com.cnprog.editor.js | 2 +- templates/content/js/com.cnprog.i18n.js | 2 + templates/content/js/com.cnprog.post.js | 63 +++-- templates/content/js/com.cnprog.tag_selector.js | 45 +-- templates/content/js/com.cnprog.utils.js | 51 ++-- templates/content/js/mediawiki-login.js | 29 ++ templates/content/style/mediawiki-login.css | 63 +++++ templates/content/style/style.css | 22 +- templates/footer.html | 23 +- templates/header.html | 6 - templates/mediawiki/mediawiki_signup.html | 9 + templates/mediawiki/mediawiki_signup_content.html | 110 ++++++++ templates/mediawiki/thanks_for_joining.html | 76 +++++ templates/mediawiki/welcome_email.txt | 28 ++ templates/mediawiki/welcome_professor_email.txt | 19 ++ templates/notarobot.html | 15 + templates/tag_selector.html | 2 +- urls.py | 2 +- utils/forms.py | 151 ++++++++++ 77 files changed, 2531 insertions(+), 495 deletions(-) delete mode 100644 django_authopenid/external_login.py create mode 100644 drop-auth.sql create mode 100644 forum/management/commands/message_to_everyone.py create mode 100644 mediawiki/PHPSerialize.py create mode 100644 mediawiki/PHPUnserialize.py create mode 100644 mediawiki/README create mode 100644 mediawiki/UserRegister.alias.php create mode 100644 mediawiki/UserRegister.body.php create mode 100644 mediawiki/UserRegister.i18n.php create mode 100644 mediawiki/UserRegister.php create mode 100644 mediawiki/WsgiInjectableSpecialPage.php create mode 100644 mediawiki/__init__.py create mode 100644 mediawiki/api.py create mode 100644 mediawiki/auth.py create mode 100644 mediawiki/forms.py create mode 100644 mediawiki/junk.py create mode 100644 mediawiki/middleware.py create mode 100644 mediawiki/models.py create mode 100644 mediawiki/templatetags/__init__.py create mode 100644 mediawiki/templatetags/mediawikitags.py create mode 100644 mediawiki/views.py create mode 100644 sql_scripts/100108_upgrade_ef.sql create mode 100644 sql_scripts/badges.sql delete mode 100644 templates/content/images/logo.png delete mode 100644 templates/content/jquery-openid/images/local-login.png create mode 100644 templates/content/js/mediawiki-login.js create mode 100644 templates/content/style/mediawiki-login.css create mode 100644 templates/mediawiki/mediawiki_signup.html create mode 100644 templates/mediawiki/mediawiki_signup_content.html create mode 100644 templates/mediawiki/thanks_for_joining.html create mode 100644 templates/mediawiki/welcome_email.txt create mode 100644 templates/mediawiki/welcome_professor_email.txt create mode 100644 templates/notarobot.html create mode 100644 utils/forms.py diff --git a/INSTALL b/INSTALL index 612cd371..72cc76bf 100644 --- a/INSTALL +++ b/INSTALL @@ -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/external_login.py b/django_authopenid/external_login.py deleted file mode 100644 index bd49c009..00000000 --- a/django_authopenid/external_login.py +++ /dev/null @@ -1,103 +0,0 @@ -#this file contains stub functions that can be extended to support -#connect legacy login with external site -import settings -from django_authopenid.models import ExternalLoginData -import httplib -import urllib -import Cookie -import cookielib -from django import forms -import xml.dom.minidom as xml -import logging - -def login(request,user): - """performs the additional external login operation - """ - pass - -def set_login_cookies(response,user): - #should be unique value by design - try: - eld = ExternalLoginData.objects.get(user=user) - - data = eld.external_session_data - dom = xml.parseString(data) - login_response = dom.getElementsByTagName('login')[0] - userid = login_response.getAttribute('lguserid') - username = login_response.getAttribute('lgusername') - token = login_response.getAttribute('lgtoken') - prefix = login_response.getAttribute('cookieprefix').decode('utf-8') - sessionid = login_response.getAttribute('sessionid') - - c = {} - c[prefix + 'UserName'] = username - c[prefix + 'UserID'] = userid - c[prefix + 'Token'] = token - c[prefix + '_session'] = sessionid - - #custom code that copies cookies from external site - #not sure how to set paths and domain of cookies here - for key in c: - if c[key]: - response.set_cookie(str(key),value=str(c[key])) - except ExternalLoginData.DoesNotExist: - #this must be an OpenID login - pass - -#function to perform external logout, if needed -def logout(request): - pass - -#should raise User.DoesNotExist or pass -def clean_username(username): - return username - -def check_password(username,password): - """connects to external site and submits username/password pair - return True or False depending on correctness of login - saves remote unique id and remote session data in table ExternalLoginData - may raise forms.ValidationError - """ - host = settings.EXTERNAL_LEGACY_LOGIN_HOST - port = settings.EXTERNAL_LEGACY_LOGIN_PORT - ext_site = httplib.HTTPConnection(host,port) - - #custom code. this one does authentication through - #MediaWiki API - params = urllib.urlencode({'action':'login','format':'xml', - 'lgname':username,'lgpassword':password}) - headers = {"Content-type": "application/x-www-form-urlencoded", - 'User-Agent':"User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7", - "Accept": "text/xml"} - ext_site.request("POST","/wiki/api.php",params,headers) - response = ext_site.getresponse() - if response.status != 200: - raise forms.ValidationError('error ' + response.status + ' ' + response.reason) - data = response.read().strip() - ext_site.close() - - dom = xml.parseString(data) - login = dom.getElementsByTagName('login')[0] - result = login.getAttribute('result') - - if result == 'Success': - username = login.getAttribute('lgusername') - try: - eld = ExternalLoginData.objects.get(external_username=username) - except ExternalLoginData.DoesNotExist: - eld = ExternalLoginData() - eld.external_username = username - eld.external_session_data = data - eld.save() - return True - else: - error = login.getAttribute('details') - raise forms.ValidationError(error) - return False - -def createuser(username,email,password): - pass - -#retrieve email address -def get_email(username,password): - return '' 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\d+)//*' % _('question/'), app.question, name='question'), + url(r'^%s(?P\d+)/' % _('question/'), app.question, name='question'), url(r'^%s$' % _('tags/'), app.tags, name='tags'), url(r'^%s(?P[^/]+)/$' % _('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 index 16f3554d..ef3007a0 100644 Binary files a/locale/en/LC_MESSAGES/django.mo and b/locale/en/LC_MESSAGES/django.mo differ 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 '__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. + + + 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 + + Order deny,allow + Allow from all + + 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 + + + Options Includes + + php_admin_flag engine on + php_admin_flag safe_mode off + + + 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 + + + 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 @@ + 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 @@ +'/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 @@ + '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 @@ + '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 @@ +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(""); + #$wgOut->addHTML(""); #print this only for debugging + } +} diff --git a/mediawiki/__init__.py b/mediawiki/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/mediawiki/api.py b/mediawiki/api.py new file mode 100644 index 00000000..912de041 --- /dev/null +++ b/mediawiki/api.py @@ -0,0 +1,138 @@ +#this file contains stub functions that can be extended to support +#connect legacy login with external site +from django.conf import settings +from django_authopenid.models import ExternalLoginData +import httplib +import urllib +import Cookie +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 + """ + pass + +def set_login_cookies(response,user): + #should be unique value by design + try: + eld = ExternalLoginData.objects.get(user=user) + + data = eld.external_session_data + dom = xml.parseString(data) + login_response = dom.getElementsByTagName('login')[0] + userid = login_response.getAttribute('lguserid') + username = login_response.getAttribute('lgusername') + token = login_response.getAttribute('lgtoken') + prefix = login_response.getAttribute('cookieprefix').decode('utf-8') + sessionid = login_response.getAttribute('sessionid') + + c = {} + c[prefix + 'UserName'] = username + c[prefix + 'UserID'] = userid + 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]),\ + domain=domain) + for c in response.cookies.values(): + logging.debug(c.output()) + except ExternalLoginData.DoesNotExist: + #this must be an OpenID login + pass + +#function to perform external logout, if needed +def logout(request): + pass + +#should raise User.DoesNotExist or pass +def clean_username(username): + return username + +def check_password(username,password): + """connects to external site and submits username/password pair + return True or False depending on correctness of login + saves remote unique id and remote session data in table ExternalLoginData + may raise forms.ValidationError + """ + host = settings.EXTERNAL_LEGACY_LOGIN_HOST + 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', + 'lgname':username,'lgpassword':password}) + headers = {"Content-type": "application/x-www-form-urlencoded", + 'User-Agent':"User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7", + "Accept": "text/xml"} + ext_site.request("POST","/wiki/api.php",params,headers) + response = ext_site.getresponse() + if response.status != 200: + raise forms.ValidationError('error ' + response.status + ' ' + response.reason) + data = response.read().strip() + ext_site.close() + + print data + + dom = xml.parseString(data) + login = dom.getElementsByTagName('login')[0] + result = login.getAttribute('result') + + if result == 'Success': + username = login.getAttribute('lgusername') + try: + eld = ExternalLoginData.objects.get(external_username=username) + except ExternalLoginData.DoesNotExist: + eld = ExternalLoginData() + eld.external_username = username + eld.external_session_data = data + eld.save() + return True + else: + error = login.getAttribute('details') + raise forms.ValidationError(error) + return False + +def createuser(username,email,password): + pass + +#retrieve email address +def get_email(username,password): + 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?
' + '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 %(real_name)s is somehow already taken on the forum.
' + '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.
' + '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 '%s' % (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 += ' - no email!' + 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 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 = ('' \ + + '') \ + % (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 @@
-

{{settings.APP_SHORT_NAME}} is a collaboratively edited question and answer site created with - OSQA: The Open Source Q&A System.

+

Please customize file templates/about.html

Here you can ask and answer questions, comment and vote for the questions of others and their answers. Both questions and answers can be revised and improved. Questions can be tagged with - the relevant keywords to simplify future access and organize the accumulated material.

+ the relevant keywords to simplify future access and organize the accumulated material. +

-

This OSQA site is moderated by its members, hopefully - including yourself! +

This Q&A site is moderated by its members, hopefully - including yourself! Moderation rights are gradually assigned to the site users based on the accumulated "reputation" 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.

- -

No points are necessary to ask or answer the questions - so please - join - us!

- -

If you would like to find out more about this site - please see frequently - asked questions.

+ These points (very) roughly reflect the level of trust of the community. +

+

No points are necessary to ask or answer the questions - so please - + join us! +

+

+ If you would like to find out more about this site - please see frequently asked questions. +

{% endblock %} 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 }} -

{% trans "receive updates motivational blurb" %}

- {% include "edit_user_email_feeds_form.html" %} -

{% trans "Tag filter tool will be your right panel, once you log in." %}

+

{% trans "receive updates motivational blurb" %}

+
+ {{email_feeds_form.subscribe}} +
+

{% trans "Tag filter tool will be your right panel, once you log in." %}

@@ -108,7 +110,9 @@ parameters:

{{ form2.username }}

{{ form2.password }}

(Optional) receive updates by email - only sent when there are any.

- {% include "edit_user_email_feeds_form.html" %} +
+ {{email_feeds_form.subscribe}} +
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 %}
-{% 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 %}
{% 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 @@

{% trans "Traditional signup info" %}

    - {{form.as_ul}} +
  • {{form.username}}{{form.username.errors}}
  • +
  • {{form.email}}{{form.email.errors}}
  • +
  • {{form.password1}}{{form.password1.errors}}
  • +
  • {{form.password2}}{{form.password2.errors}}
-

{% trans "receive updates motivational blurb" %}

- {% include "edit_user_email_feeds_form.html" %} + + +
+ {{email_feeds_form.subscribe}} +
+ + {{form.recaptcha}} 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 @@
{% for award in awards %} -

{{ award.name }} {% get_score_badge_by_details award.rep award.gold award.silver award.bronze %}

+

{{ award.name }} {% get_score_badge_by_details award.rep award.gold award.silver award.bronze %}

{% endfor %}
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 %} -
+
 {{ badge.name }} × {{ badge.awarded_count|intcomma }}
-

+

{{ badge.description }}

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 %} {% if settings.GOOGLE_SITEMAP_CODE %} - + {% endif %} @@ -46,7 +46,7 @@ body { margin-top:2.4em; } - + + {% endif %} 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 %} {% trans "badges" %} {% trans "unanswered questions" %} - - {% comment %} - {% if request.user.is_authenticated %} - {% trans "my profile" %} - {% endif %} - {% endcomment %} 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 %} +
+ {% trans "MediaWiki User Registration" %} +
+{% endif %} +
+{% if request.is_include_virtual %} + + + +{% else %} + +{% endif %} + {% with form as f %} + {{ f.next }} +

{% trans "Basic information" %}

+ +

{% trans "Your Name" %}

+

{% trans "1) Real name - required for the Wiki, but not shown on the forum by default" %}

+ + + + + + + + + + + +
+ {{f.first_name}} + {% if f.first_name.errors %} +

{{ f.first_name.errors|join:", " }}

+ {% endif %} +
+ {{f.last_name}} + {% if f.last_name.errors %} +

{{ f.last_name.errors|join:", " }}

+ {% endif %} +
+ {{f.user_title}} + {% if f.user_title.errors %} +

{{ f.user_title.errors|join:", " }}

+ {% endif %} +
+

{% trans "2) Forum screen name" %}

+

{% trans "Just skip this to use your full name at the forum, otherwise please check below" %}

+

+ {{f.use_separate_screen_name}} + +

+

+ {{f.screen_name}} +

+ {% if f.screen_name.errors %} +

{{ f.screen_name.errors|join:", " }}

+ {% endif %} +

{% trans "Please remember that forum screen name is not your login name.
Screen name allows you stay anonymous at the forum - you can change it at any time too. Login name cannot be changed." %}

+

{% trans "Update subscription" %}

+

{% trans "receive updates motivational blurb" %}

+ {{f.subscribe}} + {% if f.subscribe.errors %} +

{{ f.subscribe.errors|join:", " }}

+ {% endif %} +

{% trans "Almost there..." %}

+

{% trans "recaptcha explained" %}

+

{{f.recaptcha.errors|join:", "}} +

{{f.recaptcha}}
+ {% endwith %} + + {% comment %} + + {{ previous_fields|safe }} + {% endcomment %} + +
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 %} + +{% endwith %} +{% if wiki_user.user_title == 'prof' %} + +{% else %} + {% if wiki_user.title == 'dr' %} + + {% else %} + + {% endif %} +{% endif %} + +{% if wiki_user.user_title == 'prof' %} +

Dear Professor {{wiki_user.user_last_name}}, +{% else %} + {% if wiki_user.title == 'dr' %} +

Dear Dr. {{wiki_user.user_last_name}}, + {% else %} +

Dear {{wiki_user.user_first_name}}, + {% endif %} +{% endif %} +thanks joining Wiki!

+

Could you help our community right now?
+Please answer some of the questions from our Q&A forum:

+
    + {% for q in questions %} +
  • {{q.title}}
  • + {% endfor %} +
+

Your answers will be indispensable.
+Please feel free to ask something too! Hopefully you will like this forum and the wiki and invite your coworkers and friends to join. +

+

Might you consider sharing some of the digital documentation and pulse sequences that +perhaps had accumulated in your lab?
It's very easy to upload +files to the wiki as it is to edit the pages directly. +

+

Best wishes,
+Wiki Server Admin. +

+

P.S. An email with the confirmation code has been sent to {{wiki_user.user_email}}. +Please follow the included link to confirm your email address. +{% if wiki_user.user_title == 'prof' %} +
+Also, you are always welcome to advertise open positions in your laboratory on the wiki.

+{% endif %} +

+{% 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}} +
+ + + {{ previous_fields|safe }} +
+ +{% 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 @@

- +

diff --git a/urls.py b/urls.py index b27193be..3ebc19e8 100644 --- a/urls.py +++ b/urls.py @@ -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'] + -- cgit v1.2.3-1-g7c22