From 7e4f1d542e00b4d3121da6ae5524e95867f2371b Mon Sep 17 00:00:00 2001 From: root Date: Fri, 13 Nov 2009 20:18:55 -0500 Subject: better comments, email subscriptions, corrected view counter, some ie7 issues, wiki optional with settings.WIKI_ON, site can be mounted on arbitrary url prefix, english language improvements, added feedback form, versioned css and js files to force browser cache reload when settings.RESOURCE_REVISION is incremented , other fixes --- cnprog.wsgi | 7 +- context.py | 3 + django_authopenid/external_login.py | 103 + django_authopenid/forms.py | 462 ++--- django_authopenid/models.py | 5 + django_authopenid/urls.py | 15 +- django_authopenid/util.py | 7 +- django_authopenid/views.py | 345 ++-- drop-all-tables.sh | 4 +- drop-all.sql | 39 + forum/auth.py | 92 +- forum/const.py | 2 +- forum/forms.py | 145 +- forum/management/commands/send_email_alerts.py | 139 +- forum/management/commands/subscribe_everyone.py | 31 + forum/managers.py | 11 +- forum/models.py | 141 +- forum/templatetags/extra_filters.py | 9 +- forum/templatetags/extra_tags.py | 126 +- forum/templatetags/smart_if.py | 401 ++++ forum/urls.py | 72 + forum/user.py | 12 +- forum/views.py | 362 ++-- junk.py | 3 + locale/en/LC_MESSAGES/django.mo | Bin 15146 -> 25611 bytes locale/en/LC_MESSAGES/django.po | 2067 ++++++++++++-------- locale/es/LC_MESSAGES/django.mo | Bin 49819 -> 49713 bytes locale/es/LC_MESSAGES/django.po | 497 +++-- middleware/anon_user.py | 26 + middleware/cancel.py | 15 + middleware/pagesize.py | 6 +- migration | 7 + session_messages/.svn/all-wcprops | 23 + session_messages/.svn/dir-prop-base | 6 + session_messages/.svn/entries | 64 + session_messages/.svn/format | 1 + .../.svn/text-base/__init__.py.svn-base | 36 + .../.svn/text-base/context_processors.py.svn-base | 48 + session_messages/.svn/text-base/models.py.svn-base | 3 + session_messages/__init__.py | 37 + session_messages/context_processors.py | 48 + session_messages/models.py | 3 + settings.py | 25 +- settings_local.py.dist | 55 +- sql_scripts/091111_upgrade_evgeny.sql | 1 + tables.sql | 440 +++++ templates/404.html | 2 +- templates/about.html | 25 + templates/answer_edit.html | 13 +- templates/answer_edit_tips.html | 8 +- templates/ask.html | 19 +- templates/authopenid/changeemail.html | 45 +- templates/authopenid/changeopenid.html | 2 +- templates/authopenid/changepw.html | 33 +- templates/authopenid/complete.html | 82 +- templates/authopenid/confirm_email.txt | 2 +- templates/authopenid/delete.html | 3 +- .../authopenid/external_legacy_login_info.html | 16 + templates/authopenid/sendpw.html | 30 +- templates/authopenid/sendpw_email.txt | 11 +- templates/authopenid/settings.html | 2 +- templates/authopenid/signin.html | 70 +- templates/authopenid/signup.html | 59 +- templates/badges.html | 6 +- templates/base.html | 17 +- templates/base_content.html | 17 +- templates/book.html | 6 +- templates/content/images/blue-up-arrow-h18px.png | Bin 0 -> 593 bytes templates/content/images/cnprog_logo_200_56.gif | Bin 2114 -> 0 bytes templates/content/images/gray-up-arrow-h18px.png | Bin 0 -> 383 bytes templates/content/images/logo.gif | Bin 0 -> 2114 bytes templates/content/images/logo.png | Bin 3631 -> 1902 bytes templates/content/jquery-openid/jquery.openid.js | 8 +- templates/content/jquery-openid/openid.css | 36 +- templates/content/js/com.cnprog.admin.js | 13 + templates/content/js/com.cnprog.i18n.js | 31 +- templates/content/js/com.cnprog.post.js | 314 +-- templates/content/js/com.cnprog.utils.js | 7 +- templates/content/js/jquery.form.js | 654 +++++++ templates/content/js/wmd/wmd.js | 2 +- templates/content/style/default.css | 27 +- templates/content/style/jquery.autocomplete.css | 2 +- templates/content/style/style.css | 469 ++++- templates/edit_user_email_feeds_form.html | 4 + templates/faq.html | 6 +- templates/feedback.html | 55 + templates/feedback_email.txt | 19 + templates/footer.html | 52 +- templates/header.html | 28 +- templates/index.html | 60 +- templates/logout.html | 1 - templates/post_contributor_info.html | 55 + templates/question.html | 587 +++--- templates/question_edit.html | 15 +- templates/question_edit_tips.html | 9 +- templates/question_retag.html | 13 +- templates/question_summary_list_roll.html | 55 + templates/questions.html | 45 +- templates/revisions_answer.html | 41 +- templates/revisions_question.html | 41 +- templates/tags.html | 2 +- templates/unanswered.html | 66 +- templates/upfiles/1245715031297631.png | Bin 3863 -> 0 bytes templates/upfiles/12457157052552259.png | Bin 3863 -> 0 bytes templates/user.html | 30 +- templates/user_edit.html | 4 +- templates/user_email_subscriptions.html | 23 + templates/user_favorites.html | 1 - templates/user_info.html | 42 +- templates/user_preferences.html | 24 - templates/user_recent.html | 1 - templates/user_reputation.html | 4 +- templates/user_stats.html | 70 +- templates/user_tabs.html | 6 +- templates/user_votes.html | 4 +- templates/users.html | 8 +- templates/users_questions.html | 34 +- urls.py | 65 +- user_messages/__init__.py | 36 + user_messages/context_processors.py | 52 + user_messages/models.py | 3 + 121 files changed, 6802 insertions(+), 2734 deletions(-) create mode 100644 django_authopenid/external_login.py create mode 100644 drop-all.sql create mode 100644 forum/management/commands/subscribe_everyone.py create mode 100644 forum/templatetags/smart_if.py create mode 100644 forum/urls.py create mode 100644 junk.py create mode 100644 middleware/anon_user.py create mode 100644 middleware/cancel.py create mode 100644 migration create mode 100644 session_messages/.svn/all-wcprops create mode 100644 session_messages/.svn/dir-prop-base create mode 100644 session_messages/.svn/entries create mode 100644 session_messages/.svn/format create mode 100644 session_messages/.svn/text-base/__init__.py.svn-base create mode 100644 session_messages/.svn/text-base/context_processors.py.svn-base create mode 100644 session_messages/.svn/text-base/models.py.svn-base create mode 100644 session_messages/__init__.py create mode 100644 session_messages/context_processors.py create mode 100644 session_messages/models.py create mode 100644 sql_scripts/091111_upgrade_evgeny.sql create mode 100644 tables.sql create mode 100644 templates/authopenid/external_legacy_login_info.html create mode 100644 templates/content/images/blue-up-arrow-h18px.png delete mode 100644 templates/content/images/cnprog_logo_200_56.gif create mode 100644 templates/content/images/gray-up-arrow-h18px.png create mode 100644 templates/content/images/logo.gif create mode 100644 templates/content/js/com.cnprog.admin.js create mode 100644 templates/content/js/jquery.form.js create mode 100644 templates/edit_user_email_feeds_form.html create mode 100644 templates/feedback.html create mode 100644 templates/feedback_email.txt create mode 100644 templates/post_contributor_info.html create mode 100644 templates/question_summary_list_roll.html delete mode 100755 templates/upfiles/1245715031297631.png delete mode 100755 templates/upfiles/12457157052552259.png create mode 100644 templates/user_email_subscriptions.html delete mode 100644 templates/user_preferences.html create mode 100644 user_messages/__init__.py create mode 100644 user_messages/context_processors.py create mode 100644 user_messages/models.py diff --git a/cnprog.wsgi b/cnprog.wsgi index a1bd8039..a3d332f2 100644 --- a/cnprog.wsgi +++ b/cnprog.wsgi @@ -1,8 +1,7 @@ import os import sys - -sys.path.append('/var/www/vhosts') -os.environ['DJANGO_SETTINGS_MODULE'] = 'cnprog.settings' - +sys.path.append('/var/www/vhosts/default/htdocs') +sys.path.append('/var/www/vhosts/default/htdocs/forum') +os.environ['DJANGO_SETTINGS_MODULE'] = 'forum.settings' import django.core.handlers.wsgi application = django.core.handlers.wsgi.WSGIHandler() diff --git a/context.py b/context.py index 3da69f7b..680d1c3c 100644 --- a/context.py +++ b/context.py @@ -11,6 +11,9 @@ def application_settings(context): 'GOOGLE_SITEMAP_CODE':settings.GOOGLE_SITEMAP_CODE, 'GOOGLE_ANALYTICS_KEY':settings.GOOGLE_ANALYTICS_KEY, 'BOOKS_ON':settings.BOOKS_ON, + 'WIKI_ON':settings.WIKI_ON, + 'USE_EXTERNAL_LEGACY_LOGIN':settings.USE_EXTERNAL_LEGACY_LOGIN, + 'RESOURCE_REVISION':settings.RESOURCE_REVISION, } return {'settings':my_settings} diff --git a/django_authopenid/external_login.py b/django_authopenid/external_login.py new file mode 100644 index 00000000..bd49c009 --- /dev/null +++ b/django_authopenid/external_login.py @@ -0,0 +1,103 @@ +#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 690a781f..d4482751 100644 --- a/django_authopenid/forms.py +++ b/django_authopenid/forms.py @@ -35,8 +35,10 @@ 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 # needed for some linux distributions like debian @@ -46,16 +48,110 @@ except ImportError: from yadis import xri from django_authopenid.util import clean_next +from django_authopenid.models import ExternalLoginData + +__all__ = ['OpenidSigninForm', 'ClassicLoginForm', 'OpenidVerifyForm', + 'OpenidRegisterForm', 'ClassicRegisterForm', 'ChangePasswordForm', + 'ChangeEmailForm', 'EmailPasswordForm', 'DeleteForm', + 'ChangeOpenidForm'] -__all__ = ['OpenidSigninForm', 'OpenidAuthForm', 'OpenidVerifyForm', - 'OpenidRegisterForm', 'RegistrationForm', 'ChangepwForm', - 'ChangeemailForm', 'EmailPasswordForm', 'DeleteForm', - 'ChangeOpenidForm', 'ChangeEmailForm', 'ChangepwForm'] +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 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})) - next = forms.CharField(max_length=255, widget=forms.HiddenInput(), required=False) + next = NextUrlField() def clean_openid_url(self): """ test if openid is accepted """ @@ -67,76 +163,98 @@ class OpenidSigninForm(forms.Form): raise forms.ValidationError(_('i-names are not supported')) return self.cleaned_data['openid_url'] - def clean_next(self): - """ validate next """ - if 'next' in self.cleaned_data and self.cleaned_data['next'] != "": - self.cleaned_data['next'] = clean_next(self.cleaned_data['next']) - return self.cleaned_data['next'] - - -attrs_dict = { 'class': 'required login' } -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') - -class OpenidAuthForm(forms.Form): +class ClassicLoginForm(forms.Form): """ legacy account signin form """ - next = forms.CharField(max_length=255, widget=forms.HiddenInput(), - required=False) - username = forms.CharField(max_length=30, - widget=forms.widgets.TextInput(attrs=attrs_dict)) + next = NextUrlField() + username = UserNameField(required=False,skip_clean=True) password = forms.CharField(max_length=128, - widget=forms.widgets.PasswordInput(attrs=attrs_dict)) - + widget=forms.widgets.PasswordInput(attrs=attrs_dict), required=False) + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None): - super(OpenidAuthForm, self).__init__(data, files, auto_id, + super(ClassicLoginForm, self).__init__(data, files, auto_id, prefix, initial) self.user_cache = None - + + clean_nonempty_field = clean_nonempty_field_method + def clean_username(self): - """ validate username and test if it exists.""" - if 'username' in self.cleaned_data and \ - 'openid_url' not in self.cleaned_data: - if not username_re.search(self.cleaned_data['username']): - raise forms.ValidationError(_("Usernames can only contain \ - letters, numbers and underscores")) - try: - user = User.objects.get( - username__exact = self.cleaned_data['username'] - ) - except User.DoesNotExist: - raise forms.ValidationError(_("This username does not exist \ - in our database. Please choose another.")) - except User.MultipleObjectsReturned: - raise forms.ValidationError(u'There is already more than one \ - account registered with that username. Please try \ - another.') - return self.cleaned_data['username'] + return self.clean_nonempty_field('username') def clean_password(self): - """" test if password is valid for this username """ - if 'username' in self.cleaned_data and \ - 'password' in self.cleaned_data: - self.user_cache = authenticate( - username=self.cleaned_data['username'], - password=self.cleaned_data['password'] - ) + return self.clean_nonempty_field('password') + + def clean(self): + """ + this clean function actuall cleans username and password + + test if password is valid for this username + this is really the "authenticate" function + also openid_auth is not an authentication backend + since it's written in a way that does not comply with + the Django convention + """ + + error_list = [] + username = self.cleaned_data['username'] + password = self.cleaned_data['password'] + + self.user_cache = None + if username and password: + + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + pw_ok = False + try: + pw_ok = external_login.check_password(username,password) + except forms.ValidationError, e: + error_list.extend(e.messages) + if pw_ok: + external_user = ExternalLoginData.objects.get(external_username=username) + if external_user.user == None: + return self.cleaned_data + user = external_user.user + openid_logins = user.userassociation_set.all() + + if len(openid_logins) > 0: + msg1 = _('Account with this name already exists on the forum') + msg2 = _('can\'t have two logins to the same account yet, sorry.') + error_list.append(msg1) + error_list.append(msg2) + self._errors['__all__'] = forms.util.ErrorList(error_list) + return self.cleaned_data + else: + #synchronize password with external login + user.set_password(password) + user.save() + #this auth will always succeed + self.user_cache = authenticate(username=user.username,\ + password=password) + else: + #keep self.user_cache == None + #nothing to do, error message will be set below + pass + else: + self.user_cache = authenticate(username=username, password=password) + if self.user_cache is None: - raise forms.ValidationError(_("Please enter a valid \ - username and password. Note that both fields are \ - case-sensitive.")) + del self.cleaned_data['username'] + del self.cleaned_data['password'] + error_list.insert(0,(_("Please enter valid username and password " + "(both are case-sensitive)."))) elif self.user_cache.is_active == False: - raise forms.ValidationError(_("This account is inactive.")) - return self.cleaned_data['password'] + error_list.append(_("This account is inactive.")) + if len(error_list) > 0: + error_list.insert(0,_('Login failed.')) + elif password == None and username == None: + error_list.append(_('Please enter username and password')) + elif password == None: + error_list.append(_('Please enter your password')) + elif username == None: + error_list.append(_('Please enter user name')) + if len(error_list) > 0: + self._errors['__all__'] = forms.util.ErrorList(error_list) + return self.cleaned_data - def clean_next(self): - """ validate next url """ - if 'next' in self.cleaned_data and \ - self.cleaned_data['next'] != "": - self.cleaned_data['next'] = clean_next(self.cleaned_data['next']) - return self.cleaned_data['next'] - def get_user(self): """ get authenticated user """ return self.user_cache @@ -144,56 +262,14 @@ class OpenidAuthForm(forms.Form): class OpenidRegisterForm(forms.Form): """ openid signin form """ - next = forms.CharField(max_length=255, widget=forms.HiddenInput(), - required=False) - username = forms.CharField(max_length=30, - widget=forms.widgets.TextInput(attrs=attrs_dict)) - email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, - maxlength=200)), label=u'Email address') - - def clean_username(self): - """ test if username is valid and exist in database """ - if 'username' in self.cleaned_data: - if not username_re.search(self.cleaned_data['username']): - raise forms.ValidationError(_('invalid user name')) - if self.cleaned_data['username'] in RESERVED_NAMES: - raise forms.ValidationError(_('sorry, this name can not be used, please try another')) - if len(self.cleaned_data['username']) < settings.MIN_USERNAME_LENGTH: - raise forms.ValidationError(_('username too short')) - try: - user = User.objects.get( - username__exact = self.cleaned_data['username'] - ) - except User.DoesNotExist: - return self.cleaned_data['username'] - except User.MultipleObjectsReturned: - raise forms.ValidationError(_('this name is already in use - please try anoter')) - raise forms.ValidationError(_('this name is already in use - please try anoter')) - - def clean_email(self): - """Optionally, for security reason one unique email in database""" - if 'email' in self.cleaned_data: - if settings.EMAIL_UNIQUE == True: - try: - user = User.objects.get(email = self.cleaned_data['email']) - except User.DoesNotExist: - return self.cleaned_data['email'] - except User.MultipleObjectsReturned: - raise forms.ValidationError(u'There is already more than one \ - account registered with that e-mail address. Please try \ - another.') - raise forms.ValidationError(_("This email is already \ - registered in our database. Please choose another.")) - else: - return self.cleaned_data['email'] - #what if not??? - + next = NextUrlField() + username = UserNameField() + email = UserEmailField() + class OpenidVerifyForm(forms.Form): """ openid verify form (associate an openid with an account) """ - next = forms.CharField(max_length=255, widget = forms.HiddenInput(), - required=False) - username = forms.CharField(max_length=30, - widget=forms.widgets.TextInput(attrs=attrs_dict)) + next = NextUrlField() + username = UserNameField(must_exist=True) password = forms.CharField(max_length=128, widget=forms.widgets.PasswordInput(attrs=attrs_dict)) @@ -203,24 +279,6 @@ class OpenidVerifyForm(forms.Form): prefix, initial) self.user_cache = None - def clean_username(self): - """ validate username """ - if 'username' in self.cleaned_data: - if not username_re.search(self.cleaned_data['username']): - raise forms.ValidationError(_('invalid user name')) - try: - user = User.objects.get( - username__exact = self.cleaned_data['username'] - ) - except User.DoesNotExist: - raise forms.ValidationError(_("This username don't exist. \ - Please choose another.")) - except User.MultipleObjectsReturned: - raise forms.ValidationError(u'Somehow, that username is in \ - use for multiple accounts. Please contact us to get this \ - problem resolved.') - return self.cleaned_data['username'] - def clean_password(self): """ test if password is valid for this user """ if 'username' in self.cleaned_data and \ @@ -236,7 +294,7 @@ class OpenidVerifyForm(forms.Form): elif self.user_cache.is_active == False: raise forms.ValidationError(_("This account is inactive.")) return self.cleaned_data['password'] - + def get_user(self): """ get authenticated user """ return self.user_cache @@ -245,88 +303,54 @@ class OpenidVerifyForm(forms.Form): attrs_dict = { 'class': 'required' } username_re = re.compile(r'^[\w ]+$') -class RegistrationForm(forms.Form): +class ClassicRegisterForm(forms.Form): """ legacy registration form """ - next = forms.CharField(max_length=255, widget=forms.HiddenInput(), - required=False) - username = forms.CharField(max_length=30, - widget=forms.TextInput(attrs=attrs_dict), - label=_('choose a username')) - email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, - maxlength=200)), label=_('your email address')) + next = NextUrlField() + username = UserNameField() + email = UserEmailField() password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), - label=_('choose password')) + label=_('choose password'), + error_messages={'required':_('password is required')}, + ) password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), - label=_('retype password')) - - def clean_username(self): - """ - Validates that the username is alphanumeric and is not already - in use. - - """ - if 'username' in self.cleaned_data: - if not username_re.search(self.cleaned_data['username']): - raise forms.ValidationError(u'Usernames can only contain \ - letters, numbers and underscores') - try: - user = User.objects.get( - username__exact = self.cleaned_data['username'] - ) - - except User.DoesNotExist: - return self.cleaned_data['username'] - except User.MultipleObjectsReturned: - raise forms.ValidationError(u'Somehow, that username is in \ - use for multiple accounts. Please contact us to get this \ - problem resolved.') - raise forms.ValidationError(u'This username is already taken. \ - Please choose another.') - - def clean_email(self): - """ validate if email exist in database - :return: raise error if it exist """ - if 'email' in self.cleaned_data: - if settings.EMAIL_UNIQUE == True: - try: - user = User.objects.get(email = self.cleaned_data['email']) - except User.DoesNotExist: - return self.cleaned_data['email'] - except User.MultipleObjectsReturned: - raise forms.ValidationError(u'There is already more than one \ - account registered with that e-mail address. Please try \ - another.') - raise forms.ValidationError(u'This email is already registered \ - in our database. Please choose another.') - else: - return self.cleaned_data['email'] - #what if not? + 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. """ - if 'password1' in self.cleaned_data and \ - 'password2' in self.cleaned_data and \ - self.cleaned_data['password1'] == \ + 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'] - raise forms.ValidationError(u'You must type the same password each \ - time') + else: + del self.cleaned_data['password2'] + del self.cleaned_data['password1'] + raise forms.ValidationError(self.fields['password2'].error_messages['nomatch']) - -class ChangepwForm(forms.Form): +class ChangePasswordForm(forms.Form): """ change password form """ - oldpw = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) - password1 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) - password2 = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + oldpw = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict), + 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: raise TypeError("Keyword argument 'user' must be supplied") - super(ChangepwForm, self).__init__(data, *args, **kwargs) + super(ChangePasswordForm, self).__init__(data, *args, **kwargs) self.user = user def clean_oldpw(self): @@ -347,49 +371,35 @@ class ChangepwForm(forms.Form): raise forms.ValidationError(_("new passwords do not match")) -class ChangeemailForm(forms.Form): +class ChangeEmailForm(forms.Form): """ change email form """ - email = forms.EmailField(widget=forms.TextInput(attrs=dict(attrs_dict, - maxlength=200)), label=u'Email address') - password = forms.CharField(widget=forms.PasswordInput(attrs=attrs_dict)) + email = UserEmailField(skip_clean=True) def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, \ initial=None, user=None): - if user is None: - raise TypeError("Keyword argument 'user' must be supplied") - super(ChangeemailForm, self).__init__(data, files, auto_id, + super(ChangeEmailForm, self).__init__(data, files, auto_id, prefix, initial) - self.test_openid = False self.user = user - - + def clean_email(self): """ check if email don't exist """ if 'email' in self.cleaned_data: if settings.EMAIL_UNIQUE == True: - if self.user.email != self.cleaned_data['email']: - try: - user = User.objects.get(email = self.cleaned_data['email']) - except User.DoesNotExist: + try: + user = User.objects.get(email = self.cleaned_data['email']) + if self.user and self.user == user: return self.cleaned_data['email'] - except User.MultipleObjectsReturned: - raise forms.ValidationError(u'There is already more than one \ - account registered with that e-mail address. Please try \ - another.') - raise forms.ValidationError(u'This email is already registered \ - in our database. Please choose another.') + except User.DoesNotExist: + return self.cleaned_data['email'] + except User.MultipleObjectsReturned: + raise forms.ValidationError(u'There is already more than one \ + account registered with that e-mail address. Please try \ + another.') + raise forms.ValidationError(u'This email is already registered \ + in our database. Please choose another.') else: return self.cleaned_data['email'] - #what if not? - - def clean_password(self): - """ check if we have to test a legacy account or not """ - if 'password' in self.cleaned_data: - if not self.user.check_password(self.cleaned_data['password']): - self.test_openid = True - return self.cleaned_data['password'] - class ChangeopenidForm(forms.Form): """ change openid form """ openid_url = forms.CharField(max_length=255, @@ -422,8 +432,7 @@ class DeleteForm(forms.Form): class EmailPasswordForm(forms.Form): """ send new password form """ - username = forms.CharField(max_length=30, - widget=forms.TextInput(attrs={'class': "required" })) + username = UserNameField(skip_clean=True,label=mark_safe(_('Your user name (required)'))) def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None): @@ -431,7 +440,6 @@ class EmailPasswordForm(forms.Form): prefix, initial) self.user_cache = None - def clean_username(self): """ get user for this username """ if 'username' in self.cleaned_data: diff --git a/django_authopenid/models.py b/django_authopenid/models.py index e6fb8111..7b2e1c02 100644 --- a/django_authopenid/models.py +++ b/django_authopenid/models.py @@ -69,3 +69,8 @@ class UserPasswordQueue(models.Model): def __unicode__(self): return self.user.username + +class ExternalLoginData(models.Model): + external_username = models.CharField(max_length=40, unique=True, null=False) + external_session_data = models.TextField() + user = models.ForeignKey(User, null=True) diff --git a/django_authopenid/urls.py b/django_authopenid/urls.py index f5d7a72f..112cbbe1 100644 --- a/django_authopenid/urls.py +++ b/django_authopenid/urls.py @@ -7,12 +7,13 @@ urlpatterns = patterns('django_authopenid.views', url(r'^yadis.xrdf$', 'xrdf', name='yadis_xrdf'), # manage account registration url(r'^%s$' % _('signin/'), 'signin', name='user_signin'), - url(r'^%s%s$' % (_('signin/'),_('newquestion/')), 'signin', kwargs = {'newquestion':True}), - url(r'^%s%s$' % (_('signin/'),_('newanswer/')), 'signin', kwargs = {'newanswer':True}), + url(r'^%s%s$' % (_('signin/'),_('newquestion/')), 'signin', kwargs = {'newquestion':True}, name='user_signin_new_question'), + url(r'^%s%s$' % (_('signin/'),_('newanswer/')), 'signin', kwargs = {'newanswer':True}, name='user_signin_new_answer'), url(r'^%s$' % _('signout/'), 'signout', name='user_signout'), url(r'^%s%s$' % (_('signin/'), _('complete/')), 'complete_signin', name='user_complete_signin'), - #url(r'^%s$' % _('register/'), 'register', name='user_register'), + url('^%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 url(r'^%s$' % _('sendpw/'), 'sendpw', name='user_sendpw'), @@ -21,8 +22,10 @@ urlpatterns = patterns('django_authopenid.views', # manage account settings url(r'^$', _('account_settings'), name='user_account_settings'), url(r'^%s$' % _('password/'), 'changepw', name='user_changepw'), - url(r'^%s$' % _('email/'), 'changeemail', name='user_changeemail',kwargs = {'action':'change'}), - url(r'^%s%s$' % (_('email/'),_('validate/')), 'changeemail', name='user_changeemail',kwargs = {'action':'validate'}), - #url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), + url(r'^%s%s$' % (_('email/'),_('validate/')), 'changeemail', name='user_validateemail',kwargs = {'action':'validate'}), + url(r'^%s%s$' % (_('email/'), _('change/')), 'changeemail', name='user_changeemail'), + url(r'^%s%s$' % (_('email/'), _('sendkey/')), 'send_email_key', name='send_email_key'), + url(r'^%s%s(?P\d+)/(?P[\dabcdef]{32})/$' % (_('email/'), _('verify/')), 'verifyemail', name='user_verifyemail'), + url(r'^%s$' % _('openid/'), 'changeopenid', name='user_changeopenid'), url(r'^%s$' % _('delete/'), 'delete', name='user_delete'), ) diff --git a/django_authopenid/util.py b/django_authopenid/util.py index 54c1246b..edb6808e 100644 --- a/django_authopenid/util.py +++ b/django_authopenid/util.py @@ -7,7 +7,7 @@ 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 try: @@ -22,7 +22,7 @@ from models import Association, Nonce __all__ = ['OpenID', 'DjangoOpenIDStore', 'from_openid_response', 'clean_next'] -DEFAULT_NEXT = getattr(settings, 'OPENID_REDIRECT_NEXT', '/') +DEFAULT_NEXT = '/' + getattr(settings, 'FORUM_SCRIPT_ALIAS') def clean_next(next): if next is None: return DEFAULT_NEXT @@ -32,6 +32,9 @@ def clean_next(next): 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 65d579f4..feb6b58f 100644 --- a/django_authopenid/views.py +++ b/django_authopenid/views.py @@ -30,19 +30,20 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from django.http import HttpResponseRedirect, get_host, Http404 +from django.http import HttpResponseRedirect, get_host, Http404, \ + HttpResponseServerError from django.shortcuts import render_to_response as render from django.template import RequestContext, loader, Context from django.conf import settings from django.contrib.auth.models import User -from django.contrib.auth import logout #for login I've added wrapper below - called login from django.contrib.auth.decorators import login_required +from django.contrib.auth import authenticate from django.core.urlresolvers import reverse from django.utils.encoding import smart_unicode from django.utils.html import escape from django.utils.translation import ugettext as _ -from django.contrib.sites.models import Site from django.utils.http import urlquote_plus +from django.utils.safestring import mark_safe from django.core.mail import send_mail from django.views.defaults import server_error @@ -60,15 +61,24 @@ import re import urllib -from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, clean_next -from django_authopenid.models import UserAssociation, UserPasswordQueue -from django_authopenid.forms import OpenidSigninForm, OpenidAuthForm, OpenidRegisterForm, \ - OpenidVerifyForm, RegistrationForm, ChangepwForm, ChangeemailForm, \ +from forum.forms import EditUserEmailFeedsForm +from django_authopenid.util import OpenID, DjangoOpenIDStore, from_openid_response, get_next_url +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 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) + #1) get old session key session_key = request.session.session_key #2) login and get new session key @@ -76,6 +86,12 @@ def login(request,user): #3) send signal with old session key as argument user_logged_in.send(user=user,session_key=session_key,sender=None) +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) + def get_url_host(request): if request.is_secure(): protocol = 'https' @@ -138,7 +154,7 @@ def complete(request, on_success=None, on_failure=None, return_to=None): def default_on_success(request, identity_url, openid_response): """ default action on openid signin success """ request.session['openid'] = from_openid_response(openid_response) - return HttpResponseRedirect(clean_next(request.GET.get('next'))) + return HttpResponseRedirect(get_next_url(request)) def default_on_failure(request, message): """ default failure action on signin """ @@ -152,16 +168,15 @@ def not_authenticated(func): he is already logged.""" def decorated(request, *args, **kwargs): if request.user.is_authenticated(): - next = request.GET.get("next", "/") - return HttpResponseRedirect(next) + return HttpResponseRedirect(get_next_url(request)) return func(request, *args, **kwargs) return decorated @not_authenticated def signin(request,newquestion=False,newanswer=False): """ - signin page. It manage the legacy authentification (user/password) - and authentification with openid. + signin page. It manages the legacy authentification (user/password) + and openid authentification url: /signin/ @@ -169,27 +184,112 @@ def signin(request,newquestion=False,newanswer=False): """ request.encoding = 'UTF-8' on_failure = signin_failure - next = clean_next(request.GET.get('next')) - + email_feeds_form = EditUserEmailFeedsForm() + next = get_next_url(request) form_signin = OpenidSigninForm(initial={'next':next}) - form_auth = OpenidAuthForm(initial={'next':next}) + form_auth = ClassicLoginForm(initial={'next':next}) if request.POST: - + #'blogin' - password login if 'blogin' in request.POST.keys(): - # perform normal django authentification - form_auth = OpenidAuthForm(request.POST) + form_auth = ClassicLoginForm(request.POST) if form_auth.is_valid(): - user_ = form_auth.get_user() - login(request, user_) - next = clean_next(form_auth.cleaned_data.get('next')) - print 'next stop is "%s"' % next - return HttpResponseRedirect(next) + #have login and password and need to login through external website + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + username = form_auth.cleaned_data['username'] + password = form_auth.cleaned_data['password'] + next = form_auth.cleaned_data['next'] + if form_auth.get_user() == None: + #need to create internal user + + #1) save login and password temporarily in session + request.session['external_username'] = username + request.session['external_password'] = password + + #2) 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} + form = OpenidRegisterForm(initial=form_data) + template_data = {'form1':form,'username':username,\ + '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,\ + 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) + return response + else: + #regular password authentication + user = form_auth.get_user() + login(request, user) + return HttpResponseRedirect(get_next_url(request)) + + elif 'bnewaccount' in request.POST.keys(): + #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) + form1_is_valid = form.is_valid() + form2_is_valid = email_feeds_form.is_valid() + if form1_is_valid and form2_is_valid: + #create the user + username = form.cleaned_data['username'] + password = request.session.get('external_password',None) + email = form.cleaned_data['email'] + print 'got email addr %s' % email + if password and username: + User.objects.create_user(username,email,password) + user = authenticate(username=username,password=password) + external_username = request.session['external_username'] + eld = ExternalLoginData.objects.get(external_username=external_username) + eld.user = user + eld.save() + login(request,user) + email_feeds_form.save(user) + del request.session['external_username'] + del request.session['external_password'] + return HttpResponseRedirect(reverse('index')) + else: + if password: + del request.session['external_username'] + if username: + del request.session['external_password'] + return HttpResponseServerError() + else: + username = request.POST.get('username',None) + provider = mark_safe(settings.EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME) + username_taken = User.is_username_taken(username) + data = {'login_type':'legacy','form1':form,'username':username,\ + 'email_feeds_form':email_feeds_form,'provider':provider,\ + 'gravatar_faq_url':reverse('faq') + '#gravatar',\ + 'external_login_name_is_taken':username_taken} + return render('authopenid/complete.html',data, + context_instance=RequestContext(request)) + else: + raise Http404 elif 'bsignin' in request.POST.keys() or 'openid_username' in request.POST.keys(): form_signin = OpenidSigninForm(request.POST) if form_signin.is_valid(): - next = clean_next(form_signin.cleaned_data.get('next')) + next = form_signin.cleaned_data['next'] sreg_req = sreg.SRegRequest(optional=['nickname', 'email']) redirect_to = "%s%s?%s" % ( get_url_host(request), @@ -203,6 +303,7 @@ def signin(request,newquestion=False,newanswer=False): sreg_request=sreg_req) + #if request is GET question = None if newquestion == True: from forum.models import AnonymousQuestion as AQ @@ -232,7 +333,6 @@ def complete_signin(request): return complete(request, signin_success, signin_failure, get_url_host(request) + reverse('user_complete_signin')) - def signin_success(request, identity_url, openid_response): """ openid signin success. @@ -256,8 +356,7 @@ def signin_success(request, identity_url, openid_response): user_.backend = "django.contrib.auth.backends.ModelBackend" login(request, user_) - next = clean_next(request.GET.get('next')) - return HttpResponseRedirect(next) + return HttpResponseRedirect(get_next_url(request)) def is_association_exist(openid_url): """ test if an openid is already in database """ @@ -284,15 +383,13 @@ def register(request): template : authopenid/complete.html """ - is_redirect = False - next = clean_next(request.GET.get('next')) openid_ = request.session.get('openid', None) + next = get_next_url(request) if not openid_: - return HttpResponseRedirect(reverse('user_signin') + next) + return HttpResponseRedirect(reverse('user_signin') + '?next=%s' % next) nickname = openid_.sreg.get('nickname', '') email = openid_.sreg.get('email', '') - form1 = OpenidRegisterForm(initial={ 'next': next, 'username': nickname, @@ -302,19 +399,22 @@ def register(request): 'next': next, 'username': nickname, }) + email_feeds_form = EditUserEmailFeedsForm() user_ = None + is_redirect = False if request.POST: - just_completed = False if 'bnewaccount' in request.POST.keys(): form1 = OpenidRegisterForm(request.POST) - if form1.is_valid(): - next = clean_next(form1.cleaned_data.get('next')) + email_feeds_form = EditUserEmailFeedsForm(request.POST) + if form1.is_valid() and email_feeds_form.is_valid(): + next = form1.cleaned_data['next'] is_redirect = True tmp_pwd = User.objects.make_random_password() user_ = User.objects.create_user(form1.cleaned_data['username'], form1.cleaned_data['email'], tmp_pwd) - + + user_.set_unusable_password() # make association with openid uassoc = UserAssociation(openid_url=str(openid_), user_id=user_.id) @@ -323,11 +423,12 @@ def register(request): # login user_.backend = "django.contrib.auth.backends.ModelBackend" login(request, user_) + email_feeds_form.save(user_) elif 'bverify' in request.POST.keys(): form2 = OpenidVerifyForm(request.POST) if form2.is_valid(): is_redirect = True - next = clean_next(form2.cleaned_data.get('next')) + next = form2.cleaned_data['next'] user_ = form2.get_user() uassoc = UserAssociation(openid_url=str(openid_), @@ -339,15 +440,16 @@ def register(request): #this needs to be a function call becase this is also done #if user just logged in and did not need to create the new account - if user_ != None and settings.EMAIL_VALIDATION == 'on': - send_new_email_key(user_,nomessage=True) - output = validation_email_sent(request) - set_email_validation_message(user_) #message set after generating view - return output - elif user_.is_authenticated(): - return HttpResponseRedirect('/') - else: - raise server_error('') + if user_ != None: + if settings.EMAIL_VALIDATION == 'on': + send_new_email_key(user_,nomessage=True) + output = validation_email_sent(request) + set_email_validation_message(user_) #message set after generating view + return output + if user_.is_authenticated(): + return HttpResponseRedirect(reverse('index')) + else: + raise Exception('openid login failed')#should not ever get here openid_str = str(openid_) bits = openid_str.split('/') @@ -369,9 +471,12 @@ def register(request): return render('authopenid/complete.html', { 'form1': form1, 'form2': form2, - 'provider':provider_logo, - 'nickname': nickname, - 'email': email + 'email_feeds_form': email_feeds_form, + 'provider':mark_safe(provider_logo), + 'username': nickname, + 'email': email, + 'login_type':'openid', + 'gravatar_faq_url':reverse('faq') + '#gravatar', }, context_instance=RequestContext(request)) def signin_failure(request, message): @@ -380,9 +485,9 @@ def signin_failure(request, message): template : "authopenid/signin.html" """ - next = clean_next(request.GET.get('next')) + next = get_next_url(request) form_signin = OpenidSigninForm(initial={'next': next}) - form_auth = OpenidAuthForm(initial={'next': next}) + form_auth = ClassicLoginForm(initial={'next': next}) return render('authopenid/signin.html', { 'msg': message, @@ -399,42 +504,52 @@ def signup(request): templates: authopenid/signup.html, authopenid/confirm_email.txt """ - action_signin = reverse('user_signin') - next = clean_next(request.GET.get('next')) - form = RegistrationForm(initial={'next':next}) - form_signin = OpenidSigninForm(initial={'next':next}) - + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) + next = get_next_url(request) if request.POST: - form = RegistrationForm(request.POST) - if form.is_valid(): - next = clean_next(form.cleaned_data.get('next')) - user_ = User.objects.create_user( form.cleaned_data['username'], - form.cleaned_data['email'], form.cleaned_data['password1']) + form = ClassicRegisterForm(request.POST) + email_feeds_form = EditUserEmailFeedsForm(request.POST) + + #validation outside if to remember form values + form1_is_valid = form.is_valid() + form2_is_valid = email_feeds_form.is_valid() + if form1_is_valid and form2_is_valid: + next = form.cleaned_data['next'] + username = form.cleaned_data['username'] + password = form.cleaned_data['password1'] + email = form.cleaned_data['email'] + + user_ = User.objects.create_user( username,email,password ) + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + external_login.create_user(username,email,password) user_.backend = "django.contrib.auth.backends.ModelBackend" login(request, user_) + email_feeds_form.save(user_) # send email - current_domain = Site.objects.get_current().domain - subject = _("Welcome") + subject = _("Welcome email subject line") message_template = loader.get_template( 'authopenid/confirm_email.txt' ) message_context = Context({ - 'site_url': 'http://%s/' % current_domain, - 'username': form.cleaned_data['username'], - 'password': form.cleaned_data['password1'] + 'signup_url': settings.APP_URL + reverse('user_signin'), + 'username': username, + 'password': password, }) message = message_template.render(message_context) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user_.email]) - return HttpResponseRedirect(next) - + else: + form = ClassicRegisterForm(initial={'next':next}) + email_feeds_form = EditUserEmailFeedsForm() return render('authopenid/signup.html', { - 'form': form, - 'form2': form_signin, + 'form': form, + 'email_feeds_form': email_feeds_form }, context_instance=RequestContext(request)) + #what if request is not posted? @login_required def signout(request): @@ -447,10 +562,8 @@ def signout(request): del request.session['openid'] except KeyError: pass - next = clean_next(request.GET.get('next')) logout(request) - - return HttpResponseRedirect(next) + return HttpResponseRedirect(get_next_url(request)) def xrdf(request): url_host = get_url_host(request) @@ -498,11 +611,16 @@ def changepw(request): url : /changepw/ template: authopenid/changepw.html """ - user_ = request.user + + if user_.has_usable_password(): + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) + else: + raise Http404 if request.POST: - form = ChangepwForm(request.POST, user=user_) + form = ChangePasswordForm(request.POST, user=user_) if form.is_valid(): user_.set_password(form.cleaned_data['password1']) user_.save() @@ -512,18 +630,20 @@ def changepw(request): urlquote_plus(msg)) return HttpResponseRedirect(redirect) else: - form = ChangepwForm(user=user_) + form = ChangePasswordForm(user=user_) return render('authopenid/changepw.html', {'form': form }, context_instance=RequestContext(request)) def find_email_validation_messages(user): - msg_text = _('your email needs to be validated') + msg_text = _('your email needs to be validated see %(details_url)s') \ + % {'details_url':reverse('faq') + '#validate'} return user.message_set.filter(message__exact=msg_text) def set_email_validation_message(user): messages = find_email_validation_messages(user) - msg_text = _('your email needs to be validated') + msg_text = _('your email needs to be validated see %(details_url)s') \ + % {'details_url':reverse('faq') + '#validate'} if len(messages) == 0: user.message_set.create(message=msg_text) @@ -543,11 +663,11 @@ def _send_email_key(user): """private function. sends email containing validation key to user's email address """ - subject = _("Welcome") + subject = _("Email verification subject line") message_template = loader.get_template('authopenid/email_validation.txt') import settings message_context = Context({ - 'validation_link': '%s/email/verify/%d/%s/' % (settings.APP_URL ,user.id,user.email_key) + 'validation_link': settings.APP_URL + reverse('user_verifyemail', kwargs={'id':user.id,'key':user.email_key}) }) message = message_template.render(message_context) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email]) @@ -583,7 +703,7 @@ def send_email_key(request): context_instance=RequestContext(request) ) else: - _send_email_key(request.user) + send_new_email_key(request.user) return validation_email_sent(request) else: raise Http404 @@ -592,10 +712,11 @@ 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', - { 'email': request.user.email, 'action_type': 'validate', }, + { 'email': request.user.email, + 'change_email_url': reverse('user_changeemail'), + 'action_type': 'validate', }, context_instance=RequestContext(request)) - def verifyemail(request,id=None,key=None): """ view that is shown when user clicks email validation link @@ -614,53 +735,49 @@ def verifyemail(request,id=None,key=None): raise Http404 @login_required -def changeemail(request): +def changeemail(request, action='change'): """ - changeemail view. It require password or openid to allow change. + changeemail view. requires openid with request type GET url: /email/* template : authopenid/changeemail.html """ - msg = request.GET.get('msg', '') + msg = request.GET.get('msg', None) extension_args = {} user_ = request.user - redirect_to = get_url_host(request) + reverse('user_changeemail') - action = 'change' - if request.POST: - form = ChangeemailForm(request.POST, user=user_) + if 'cancel' in request.POST: + msg = _('your email was not changed') + request.user.message_set.create(message=msg) + return HttpResponseRedirect(get_next_url(request)) + form = ChangeEmailForm(request.POST, user=user_) if form.is_valid(): - if not form.test_openid: - new_email = form.cleaned_data['email'] - if new_email != user_.email: - if settings.EMAIL_VALIDATION == 'on': - action = 'validate' - else: - action = 'done_novalidate' - set_new_email(user_, new_email,nomessage=True) + new_email = form.cleaned_data['email'] + if new_email != user_.email: + if settings.EMAIL_VALIDATION == 'on': + action = 'validate' else: - action = 'keep' + action = 'done_novalidate' + set_new_email(user_, new_email,nomessage=True) else: - #what does this branch do? - return server_error('') - request.session['new_email'] = form.cleaned_data['email'] - return ask_openid(request, form.cleaned_data['password'], - redirect_to, on_failure=emailopenid_failure) + action = 'keep' elif not request.POST and 'openid.mode' in request.GET: + redirect_to = get_url_host(request) + reverse('user_changeemail') return complete(request, emailopenid_success, emailopenid_failure, redirect_to) else: - form = ChangeemailForm(initial={'email': user_.email}, + form = ChangeEmailForm(initial={'email': user_.email}, user=user_) - output = render('authopenid/changeemail.html', { 'form': form, 'email': user_.email, 'action_type': action, + 'gravatar_faq_url': reverse('faq') + '#gravatar', + 'change_email_url': reverse('user_changeemail'), 'msg': msg }, context_instance=RequestContext(request)) @@ -669,7 +786,6 @@ def changeemail(request): return output - def emailopenid_success(request, identity_url, openid_response): openid_ = from_openid_response(openid_response) @@ -843,7 +959,7 @@ def deleteopenid_success(request, identity_url, openid_response): identity_url)) msg = _("Account deleted.") - redirect = "/?msg=%s" % (urlquote_plus(msg)) + redirect = reverse('index') + u"/?msg=%s" % (urlquote_plus(msg)) return HttpResponseRedirect(redirect) @@ -851,6 +967,8 @@ def deleteopenid_failure(request, message): redirect_to = "%s?msg=%s" % (reverse('user_delete'), urlquote_plus(message)) return HttpResponseRedirect(redirect_to) +def external_legacy_login_info(request): + return render('authopenid/external_legacy_login_info.html', context_instance=RequestContext(request)) def sendpw(request): """ @@ -862,6 +980,8 @@ def sendpw(request): templates : authopenid/sendpw_email.txt, authopenid/sendpw.html """ + if settings.USE_EXTERNAL_LEGACY_LOGIN == True: + return HttpResponseRedirect(reverse('user_external_legacy_login_issues')) msg = request.GET.get('msg','') if request.POST: @@ -881,21 +1001,20 @@ def sendpw(request): uqueue.confirm_key = confirm_key uqueue.save() # send email - current_domain = Site.objects.get_current().domain subject = _("Request for new password") message_template = loader.get_template( 'authopenid/sendpw_email.txt') + key_link = settings.APP_URL + reverse('user_confirmchangepw') + '?key=' + confirm_key message_context = Context({ - 'site_url': 'http://%s' % current_domain, - 'confirm_key': confirm_key, + 'site_url': settings.APP_URL + reverse('index'), + 'key_link': key_link, 'username': form.user_cache.username, 'password': new_pw, - 'url_confirm': reverse('user_confirmchangepw'), }) message = message_template.render(message_context) send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [form.user_cache.email]) - msg = _("A new password has been sent to your email address.") + msg = _("A new password and the activation link were sent to your email address.") else: form = EmailPasswordForm() @@ -918,7 +1037,7 @@ def confirmchangepw(request): """ confirm_key = request.GET.get('key', '') if not confirm_key: - return HttpResponseRedirect('/') + return HttpResponseRedirect(reverse('index')) try: uqueue = UserPasswordQueue.objects.get( diff --git a/drop-all-tables.sh b/drop-all-tables.sh index 87783b77..1e55cb1f 100644 --- a/drop-all-tables.sh +++ b/drop-all-tables.sh @@ -1,4 +1,4 @@ -mysql_username='cnprog' -mysql_database='cnprog' +mysql_username='' +mysql_database='' mysqldump -u $mysql_username -p --add-drop-table --no-data $mysql_database | grep ^DROP #| mysql -u[USERNAME] -p[PASSWORD] [DATABASE] diff --git a/drop-all.sql b/drop-all.sql new file mode 100644 index 00000000..52feb337 --- /dev/null +++ b/drop-all.sql @@ -0,0 +1,39 @@ +DROP TABLE IF EXISTS `activity`; +DROP TABLE IF EXISTS `answer`; +DROP TABLE IF EXISTS `answer_revision`; +DROP TABLE IF EXISTS `auth_group`; +DROP TABLE IF EXISTS `auth_group_permissions`; +DROP TABLE IF EXISTS `auth_message`; +DROP TABLE IF EXISTS `auth_permission`; +DROP TABLE IF EXISTS `auth_user`; +DROP TABLE IF EXISTS `auth_user_groups`; +DROP TABLE IF EXISTS `auth_user_user_permissions`; +DROP TABLE IF EXISTS `award`; +DROP TABLE IF EXISTS `badge`; +DROP TABLE IF EXISTS `book`; +DROP TABLE IF EXISTS `book_author_info`; +DROP TABLE IF EXISTS `book_author_rss`; +DROP TABLE IF EXISTS `book_question`; +DROP TABLE IF EXISTS `comment`; +DROP TABLE IF EXISTS `django_admin_log`; +DROP TABLE IF EXISTS `django_authopenid_association`; +DROP TABLE IF EXISTS `django_authopenid_externallogindata`; +DROP TABLE IF EXISTS `django_authopenid_nonce`; +DROP TABLE IF EXISTS `django_authopenid_userassociation`; +DROP TABLE IF EXISTS `django_authopenid_userpasswordqueue`; +DROP TABLE IF EXISTS `django_content_type`; +DROP TABLE IF EXISTS `django_session`; +DROP TABLE IF EXISTS `django_site`; +DROP TABLE IF EXISTS `favorite_question`; +DROP TABLE IF EXISTS `flagged_item`; +DROP TABLE IF EXISTS `forum_anonymousanswer`; +DROP TABLE IF EXISTS `forum_anonymousemail`; +DROP TABLE IF EXISTS `forum_anonymousquestion`; +DROP TABLE IF EXISTS `forum_emailfeed`; +DROP TABLE IF EXISTS `forum_emailfeedsetting`; +DROP TABLE IF EXISTS `question`; +DROP TABLE IF EXISTS `question_revision`; +DROP TABLE IF EXISTS `question_tags`; +DROP TABLE IF EXISTS `repute`; +DROP TABLE IF EXISTS `tag`; +DROP TABLE IF EXISTS `vote`; diff --git a/forum/auth.py b/forum/auth.py index 776746e8..1569482f 100644 --- a/forum/auth.py +++ b/forum/auth.py @@ -6,18 +6,20 @@ and superuser status. """ import datetime from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ from django.db import transaction from models import Repute from models import Question from models import Answer from const import TYPE_REPUTATION +import logging question_type = ContentType.objects.get_for_model(Question) answer_type = ContentType.objects.get_for_model(Answer) VOTE_UP = 15 FLAG_OFFENSIVE = 15 POST_IMAGES = 15 -LEAVE_COMMENTS = 50 +LEAVE_COMMENTS = 50 UPLOAD_FILES = 60 VOTE_DOWN = 100 CLOSE_OWN_QUESTIONS = 250 @@ -58,6 +60,9 @@ REPUTATION_RULES = { 'lose_by_upvote_canceled' : -10, } +def can_moderate_users(user): + return user.is_superuser + def can_vote_up(user): """Determines if a User can vote Questions and Answers up.""" return user.is_authenticated() and ( @@ -70,11 +75,18 @@ def can_flag_offensive(user): user.reputation >= FLAG_OFFENSIVE or user.is_superuser) -def can_add_comments(user): +def can_add_comments(user,subject): """Determines if a User can add comments to Questions and Answers.""" - return user.is_authenticated() and ( - user.reputation >= LEAVE_COMMENTS or - user.is_superuser) + if user.is_authenticated(): + if user.id == subject.author.id: + return True + if user.reputation >= LEAVE_COMMENTS: + return True + if user.is_superuser: + return True + if isinstance(subject,Answer) and subject.question.author.id == user.id: + return True + return False def can_vote_down(user): """Determines if a User can vote Questions and Answers down.""" @@ -139,8 +151,21 @@ def can_reopen_question(user, question): user.reputation >= REOPEN_OWN_QUESTIONS) or user.is_superuser def can_delete_post(user, post): - return (user.is_authenticated() and - user.id == post.author_id) or user.is_superuser + if user.is_superuser: + return True + elif user.is_authenticated() and user == post.author: + if isinstance(post,Answer): + return True + elif isinstance(post,Question): + answers = post.answers.all() + for answer in answers: + if user != answer.author and answer.deleted == False: + return False + return True + else: + return False + else: + return False def can_view_deleted_post(user, post): return user.is_superuser @@ -422,15 +447,20 @@ def onDownVotedCanceled(vote, post, user): def onDeleteCanceled(post, user): post.deleted = False - post.deleted_by = None - post.deleted_at = None + post.deleted_by = None + post.deleted_at = None post.save() - for tag in list(post.tags.all()): - if tag.used_count == 1 and tag.deleted: - tag.deleted = False - tag.deleted_by = None - tag.deleted_at = None - tag.save() + logging.debug('now restoring something') + if isinstance(post,Answer): + logging.debug('updated answer count on undelete, have %d' % post.question.answer_count) + Question.objects.update_answer_count(post.question) + elif isinstance(post,Question): + for tag in list(post.tags.all()): + if tag.used_count == 1 and tag.deleted: + tag.deleted = False + tag.deleted_by = None + tag.deleted_at = None + tag.save() def onDeleted(post, user): post.deleted = True @@ -438,9 +468,31 @@ def onDeleted(post, user): post.deleted_at = datetime.datetime.now() post.save() - for tag in list(post.tags.all()): - if tag.used_count == 1: - tag.deleted = True - tag.deleted_by = user - tag.deleted_at = datetime.datetime.now() + if isinstance(post, Question): + for tag in list(post.tags.all()): + if tag.used_count == 1: + tag.deleted = True + tag.deleted_by = user + tag.deleted_at = datetime.datetime.now() + else: + tag.used_count = tag.used_count - 1 tag.save() + + answers = post.answers.all() + if user == post.author: + if len(answers) > 0: + msg = _('Your question and all of it\'s answers have been deleted') + else: + msg = _('Your question has been deleted') + else: + if len(answers) > 0: + msg = _('The question and all of it\'s answers have been deleted') + else: + msg = _('The question has been deleted') + user.message_set.create(message=msg) + logging.debug('posted a message %s' % msg) + for answer in answers: + onDeleted(answer, user) + elif isinstance(post, Answer): + Question.objects.update_answer_count(post.question) + logging.debug('updated answer count to %d' % post.question.answer_count) diff --git a/forum/const.py b/forum/const.py index f6649cc4..9b9230c0 100644 --- a/forum/const.py +++ b/forum/const.py @@ -6,7 +6,7 @@ For reasons that models, views can't have unicode text in this project, all unic """ CLOSE_REASONS = ( (1, _('duplicate question')), - (2, _('question if off-topic or not relevant')), + (2, _('question is off-topic or not relevant')), (3, _('too subjective and argumentative')), (4, _('is not an answer to the question')), (5, _('the question is answered, right answer was accepted')), diff --git a/forum/forms.py b/forum/forms.py index 59d0d620..5b181d48 100644 --- a/forum/forms.py +++ b/forum/forms.py @@ -4,6 +4,8 @@ from django import forms from models import * from const import * from django.utils.translation import ugettext as _ +from django_authopenid.forms import NextUrlField +import settings class TitleField(forms.CharField): def __init__(self, *args, **kwargs): @@ -47,26 +49,28 @@ class TagNamesField(forms.CharField): self.help_text = _('Tags are short keywords, with no spaces within. Up to five tags can be used.') self.initial = '' - def clean(self, value): - value = super(TagNamesField, self).clean(value) - data = value.strip() - if len(data) < 1: - raise forms.ValidationError(_('tags are required')) - list = data.split(' ') - list_temp = [] - if len(list) > 5: - raise forms.ValidationError(_('please use 5 tags or less')) - for tag in list: - if len(tag) > 20: - raise forms.ValidationError(_('tags must be shorter than 20 characters')) - #take tag regex from settings - tagname_re = re.compile(r'[a-z0-9]+') - if not tagname_re.match(tag): - raise forms.ValidationError(_('please use following characters in tags: letters \'a-z\', numbers, and characters \'.-_#\'')) - # only keep one same tag - if tag not in list_temp and len(tag.strip()) > 0: - list_temp.append(tag) - return u' '.join(list_temp) + def clean(self, value): + value = super(TagNamesField, self).clean(value) + data = value.strip() + if len(data) < 1: + raise forms.ValidationError(_('tags are required')) + + split_re = re.compile(r'[ ,]+') + list = split_re.split(data) + list_temp = [] + if len(list) > 5: + raise forms.ValidationError(_('please use 5 tags or less')) + for tag in list: + if len(tag) > 20: + raise forms.ValidationError(_('tags must be shorter than 20 characters')) + #take tag regex from settings + tagname_re = re.compile(r'[a-z0-9]+') + if not tagname_re.match(tag): + raise forms.ValidationError(_('please use following characters in tags: letters \'a-z\', numbers, and characters \'.-_#\'')) + # only keep one same tag + if tag not in list_temp and len(tag.strip()) > 0: + list_temp.append(tag) + return u' '.join(list_temp) class WikiField(forms.BooleanField): def __init__(self, *args, **kwargs): @@ -74,11 +78,14 @@ class WikiField(forms.BooleanField): self.required = False self.label = _('community wiki') self.help_text = _('if you choose community wiki option, the question and answer do not generate points and name of author will not be shown') + def clean(self,value): + return value and settings.WIKI_ON class EmailNotifyField(forms.BooleanField): def __init__(self, *args, **kwargs): super(EmailNotifyField, self).__init__(*args, **kwargs) self.required = False + self.widget.attrs['class'] = 'nomargin' class SummaryField(forms.CharField): def __init__(self, *args, **kwargs): @@ -89,6 +96,25 @@ class SummaryField(forms.CharField): self.label = _('update summary:') self.help_text = _('enter a brief summary of your revision (e.g. fixed spelling, grammar, improved style, this field is optional)') +class ModerateUserForm(forms.ModelForm): + is_approved = forms.BooleanField(label=_("Automatically accept user's contributions for the email updates"), + required=False) + + def clean_is_approved(self): + if 'is_approved' not in self.cleaned_data: + self.cleaned_data['is_approved'] = False + return self.cleaned_data['is_approved'] + + class Meta: + model = User + fields = ('is_approved',) + +class FeedbackForm(forms.Form): + name = forms.CharField(label=_('Your name:'), required=False) + email = forms.EmailField(label=_('Email (not shared with anyone):'), required=False) + message = forms.CharField(label=_('Your message:'), max_length=800,widget=forms.Textarea(attrs={'cols':60})) + next = NextUrlField() + class AskForm(forms.Form): title = TitleField() text = EditorField() @@ -109,16 +135,12 @@ class AnswerForm(forms.Form): def __init__(self, question, user, *args, **kwargs): super(AnswerForm, self).__init__(*args, **kwargs) self.fields['email_notify'].widget.attrs['id'] = 'question-subscribe-updates'; - if question.wiki: + if question.wiki and settings.WIKI_ON: self.fields['wiki'].initial = True if user.is_authenticated(): - try: - feed = EmailFeed.objects.get(feed_id=question.id, subscriber_id=user.id) - if feed.subscriber == user and feed.content == question: - self.fields['email_notify'].initial = True - return - except EmailFeed.DoesNotExist: - pass + if user in question.followed_by.all(): + self.fields['email_notify'].initial = True + return self.fields['email_notify'].initial = False @@ -207,3 +229,70 @@ class EditUserForm(forms.Form): raise forms.ValidationError(_('this email has already been registered, please use another one')) raise forms.ValidationError(_('this email has already been registered, please use another one')) return self.cleaned_data['email'] + +class EditUserEmailFeedsForm(forms.Form): + WN = (('w',_('weekly')),('n',_('no email'))) + DWN = (('d',_('daily')),('w',_('weekly')),('n',_('no email'))) + FORM_TO_MODEL_MAP = { + 'all_questions':'q_all', + 'asked_by_me':'q_ask', + 'answered_by_me':'q_ans', + 'individually_selected':'q_sel', + } + NO_EMAIL_INITIAL = { + 'all_questions':'n', + 'asked_by_me':'n', + 'answered_by_me':'n', + 'individually_selected':'n', + } + all_questions = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Entire forum'),) + asked_by_me = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Asked by me')) + answered_by_me = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Answered by me')) + individually_selected = forms.ChoiceField(choices=DWN,initial='w', + widget=forms.RadioSelect, + label=_('Individually selected')) + + def set_initial_values(self,user=None): + KEY_MAP = dict([(v,k) for k,v in self.FORM_TO_MODEL_MAP.iteritems()]) + if user != None: + settings = EmailFeedSetting.objects.filter(subscriber=user) + initial_values = {} + for setting in settings: + feed_type = setting.feed_type + form_field = KEY_MAP[feed_type] + frequency = setting.frequency + initial_values[form_field] = frequency + self.initial = initial_values + return self + + def reset(self): + self.cleaned_data['all_questions'] = 'n' + self.cleaned_data['asked_by_me'] = 'n' + self.cleaned_data['answered_by_me'] = 'n' + self.cleaned_data['individually_selected'] = 'n' + self.initial = self.NO_EMAIL_INITIAL + return self + + def save(self,user): + 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 s.frequency != new_value: + s.frequency = self.cleaned_data[form_field] + s.save() + changed = True + else: + if created: + s.save() + if form_field == 'individually_selected': + feed_type = ContentType.objects.get_for_model(Question) + user.followed_questions.clear() + return changed diff --git a/forum/management/commands/send_email_alerts.py b/forum/management/commands/send_email_alerts.py index 3c37aaa3..a25e4343 100644 --- a/forum/management/commands/send_email_alerts.py +++ b/forum/management/commands/send_email_alerts.py @@ -1,9 +1,11 @@ from django.core.management.base import NoArgsCommand from django.db import connection +from django.db.models import Q, F from forum.models import * -import collections from django.core.mail import EmailMessage from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime import settings class Command(NoArgsCommand): @@ -15,27 +17,116 @@ class Command(NoArgsCommand): finally: connection.close() + def get_updated_questions_for_user(self,user): + q_sel = [] + q_ask = [] + q_ans = [] + q_all = [] + now = datetime.datetime.now() + Q_set1 = Question.objects.exclude( + last_activity_by=user, + ).exclude( + last_activity_at__lt=user.date_joined + ).filter( + Q(viewed__who=user,viewed__when__lt=F('last_activity_at')) | \ + ~Q(viewed__who=user) + ).exclude( + deleted=True + ).exclude( + closed=True + ) + user_feeds = EmailFeedSetting.objects.filter(subscriber=user).exclude(frequency='n') + for feed in user_feeds: + cutoff_time = now - EmailFeedSetting.DELTA_TABLE[feed.frequency] + if feed.reported_at == None or feed.reported_at <= cutoff_time: + Q_set = Q_set1.exclude(last_activity_at__gt=cutoff_time) + feed.reported_at = now + feed.save()#may not actually report anything, depending on filters below + if feed.feed_type == 'q_sel': + q_sel = Q_set.filter(followed_by=user) + q_sel.cutoff_time = cutoff_time + elif feed.feed_type == 'q_ask': + q_ask = Q_set.filter(author=user) + q_ask.cutoff_time = cutoff_time + elif feed.feed_type == 'q_ans': + q_ans = Q_set.filter(answers__author=user) + q_ans.cutoff_time = cutoff_time + elif feed.feed_type == 'q_all': + q_all = Q_set + q_all.cutoff_time = cutoff_time + #build list in this order + q_tbl = {} + def extend_question_list(src, dst): + if isinstance(src,list): + return + cutoff_time = src.cutoff_time + for q in src: + if q in dst: + if cutoff_time < dst[q]: + dst[q] = cutoff_time + else: + dst[q] = cutoff_time + + extend_question_list(q_sel, q_tbl) + extend_question_list(q_ask, q_tbl) + extend_question_list(q_ans, q_tbl) + extend_question_list(q_all, q_tbl) + + ctype = ContentType.objects.get_for_model(Question) + out = {} + for q, cutoff_time in q_tbl.items(): + #todo use Activity, but first start keeping more Activity records + #act = Activity.objects.filter(content_type=ctype, object_id=q.id) + #get info on question edits, answer edits, comments + out[q] = {} + q_rev = QuestionRevision.objects.filter(question=q,revised_at__lt=cutoff_time) + q_rev = q_rev.exclude(author=user) + out[q]['q_rev'] = len(q_rev) + if len(q_rev) > 0 and q.added_at == q_rev[0].revised_at: + out[q]['q_rev'] = 0 + out[q]['new_q'] = True + else: + out[q]['new_q'] = False + + new_ans = Answer.objects.filter(question=q,added_at__lt=cutoff_time) + new_ans = new_ans.exclude(author=user) + out[q]['new_ans'] = len(new_ans) + ans_rev = AnswerRevision.objects.filter(answer__question=q,revised_at__lt=cutoff_time) + ans_rev = ans_rev.exclude(author=user) + out[q]['ans_rev'] = len(ans_rev) + return out + + def __act_count(self,string,number,output): + if number > 0: + output.append(_(string) % {'num':number}) + def send_email_alerts(self): - report_time = datetime.datetime.now() - feeds = EmailFeed.objects.all() - user_ctype = ContentType.objects.get_for_model(User) - - #lists of update messages keyed by email address - update_collection = collections.defaultdict(list) - for feed in feeds: - update_summary = feed.get_update_summary() - if update_summary != None: - email = feed.get_email() - update_collection[email].append(update_summary) - feed.reported_at = report_time - feed.save() - - for email, updates in update_collection.items(): - text = '\n'.join(updates) - subject = _('updates from website') - print 'sent %s to %s' % (updates,email) - msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [email]) - msg.content_subtype = 'html' - msg.send() - - + + for user in User.objects.all(): + q_list = self.get_updated_questions_for_user(user) + num_q = len(q_list) + if num_q > 0: + url_prefix = settings.APP_URL + subject = _('email update message subject') + 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} + + text += '
    ' + for q, act in q_list.items(): + act_list = [] + if act['new_q']: + act_list.append(_('new question')) + self.__act_count('%(num)d rev', act['q_rev'],act_list) + self.__act_count('%(num)d ans', act['new_ans'],act_list) + self.__act_count('%(num)d ans rev',act['ans_rev'],act_list) + act_token = ', '.join(act_list) + text += '
  • %s (%s)
  • ' \ + % (url_prefix + q.get_absolute_url(), q.title, act_token) + text += '
' + link = url_prefix + user.get_profile_url() + '?sort=email_subscriptions' + text += _('go to %(link)s to change frequency of email updates or %(email)s administrator') \ + % {'link':link, 'email':settings.ADMINS[0][1]} + msg = EmailMessage(subject, text, settings.DEFAULT_FROM_EMAIL, [user.email]) + msg.content_subtype = 'html' + msg.send() diff --git a/forum/management/commands/subscribe_everyone.py b/forum/management/commands/subscribe_everyone.py new file mode 100644 index 00000000..3f8da9ec --- /dev/null +++ b/forum/management/commands/subscribe_everyone.py @@ -0,0 +1,31 @@ +from django.core.management.base import NoArgsCommand +from django.db import connection +from django.db.models import Q, F +from forum.models import * +from django.core.mail import EmailMessage +from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +import datetime +import settings + +class Command(NoArgsCommand): + def handle_noargs(self,**options): + try: + self.subscribe_everyone() + except Exception, e: + print e + finally: + connection.close() + + def subscribe_everyone(self): + + feed_type_info = EmailFeedSetting.FEED_TYPES + for user in User.objects.all(): + for feed_type in feed_type_info: + try: + feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type = feed_type[0]) + except EmailFeedSetting.DoesNotExist: + feed_setting = EmailFeedSetting(subscriber=user,feed_type=feed_type[0]) + feed_setting.frequency = 'w' + feed_setting.reported_at = None + feed_setting.save() diff --git a/forum/managers.py b/forum/managers.py index 6ae0ed99..795d382e 100644 --- a/forum/managers.py +++ b/forum/managers.py @@ -70,7 +70,7 @@ class QuestionManager(models.Manager): # although we have imported all classes from models on top. from forum.models import Answer self.filter(id=question.id).update( - answer_count=Answer.objects.get_answers_from_question(question).count()) + answer_count=Answer.objects.get_answers_from_question(question).filter(deleted=False).count()) def update_view_count(self, question): """ @@ -93,11 +93,11 @@ class QuestionManager(models.Manager): """ #print datetime.datetime.now() from forum.models import Question - questions = list(Question.objects.filter(tagnames = question.tagnames).all()) + questions = list(Question.objects.filter(tagnames = question.tagnames, deleted=False).all()) tags_list = question.tags.all() for tag in tags_list: - extend_questions = Question.objects.filter(tags__id = tag.id)[:50] + extend_questions = Question.objects.filter(tags__id = tag.id, deleted=False)[:50] for item in extend_questions: if item not in questions and len(questions) < 10: questions.append(item) @@ -110,10 +110,11 @@ class TagManager(models.Manager): 'UPDATE tag ' 'SET used_count = (' 'SELECT COUNT(*) FROM question_tags ' - 'WHERE tag_id = tag.id' + 'INNER JOIN question ON question_id=question.id ' + 'WHERE tag_id = tag.id AND question.deleted=0' ') ' 'WHERE id IN (%s)') - + def get_valid_tags(self, page_size): from forum.models import Tag tags = Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-id")[:page_size] diff --git a/forum/models.py b/forum/models.py index f647ba12..39058bea 100644 --- a/forum/models.py +++ b/forum/models.py @@ -2,7 +2,7 @@ import datetime import hashlib from urllib import quote_plus, urlencode -from django.db import models +from django.db import models, IntegrityError from django.utils.html import strip_tags from django.core.urlresolvers import reverse from django.contrib.auth.models import User @@ -11,33 +11,61 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import slugify from django.db.models.signals import post_delete, post_save, pre_save from django.utils.translation import ugettext as _ +from django.utils.safestring import mark_safe import django.dispatch import settings from forum.managers import * from const import * -class EmailFeed(models.Model): - #subscription key for unsubscribe by visiting emailed link - key = models.CharField(max_length=32) - #generic relation with feed content (i.e. question or tags) - feed_content_type = models.ForeignKey(ContentType,related_name='content_emailfeed') - feed_id = models.PositiveIntegerField() - content = generic.GenericForeignKey('feed_content_type','feed_id') - #generic relation with owner - either nameless email or User - subscriber_content_type = models.ForeignKey(ContentType,related_name='subscriber_emailfeed') - subscriber_id = models.PositiveIntegerField() - subscriber = generic.GenericForeignKey('subscriber_content_type','subscriber_id') - added_at = models.DateTimeField(default=datetime.datetime.now) - reported_at = models.DateTimeField(default=datetime.datetime.now) - - #getter functions rely on implementations of similar functions in content - #of subscriber objects - def get_update_summary(self): - return self.content.get_update_summary(last_reported_at = self.reported_at,recipient_email = self.get_email()) - - def get_email(self): - return self.subscriber.email +def get_object_comments(self): + comments = self.comments.all().order_by('id') + return comments + +def post_get_last_update_info(self): + when = self.added_at + who = self.author + if self.last_edited_at and self.last_edited_at > when: + when = self.last_edited_at + who = self.last_edited_by + comments = self.comments.all() + if len(comments) > 0: + for c in comments: + if c.added_at > when: + when = c.added_at + who = c.user + return when, who + +class EmailFeedSetting(models.Model): + DELTA_TABLE = { + 'w':datetime.timedelta(7), + 'd':datetime.timedelta(1), + 'n':datetime.timedelta(-1), + } + FEED_TYPES = ( + ('q_all',_('Entire forum')), + ('q_ask',_('Questions that I asked')), + ('q_ans',_('Questions that I answered')), + ('q_sel',_('Individually selected questions')), + ) + UPDATE_FREQUENCY = ( + ('w',_('Weekly')), + ('d',_('Daily')), + ('n',_('No email')), + ) + subscriber = models.ForeignKey(User) + feed_type = models.CharField(max_length=16,choices=FEED_TYPES) + frequency = models.CharField(max_length=8,choices=UPDATE_FREQUENCY,default='n') + added_at = models.DateTimeField(auto_now_add=True) + reported_at = models.DateTimeField(null=True) + + def save(self,*args,**kwargs): + type = self.feed_type + subscriber = self.subscriber + similar = self.__class__.objects.filter(feed_type=type,subscriber=subscriber).exclude(pk=self.id) + if len(similar) > 0: + raise IntegrityError('email feed setting already exists') + super(EmailFeedSetting,self).save(*args,**kwargs) class Tag(models.Model): name = models.CharField(max_length=255, unique=True) @@ -45,7 +73,6 @@ class Tag(models.Model): deleted = models.BooleanField(default=False) deleted_at = models.DateTimeField(null=True, blank=True) deleted_by = models.ForeignKey(User, null=True, blank=True, related_name='deleted_tags') - email_feeds = generic.GenericRelation(EmailFeed) # Denormalised data used_count = models.PositiveIntegerField(default=0) @@ -136,6 +163,7 @@ class Question(models.Model): locked = models.BooleanField(default=False) locked_by = models.ForeignKey(User, null=True, blank=True, related_name='locked_questions') locked_at = models.DateTimeField(null=True, blank=True) + followed_by = models.ManyToManyField(User, related_name='followed_questions') # Denormalised data score = models.IntegerField(default=0) vote_up_count = models.IntegerField(default=0) @@ -155,7 +183,6 @@ class Question(models.Model): comments = generic.GenericRelation(Comment) votes = generic.GenericRelation(Vote) flagged_items = generic.GenericRelation(FlaggedItem) - email_feeds = generic.GenericRelation(EmailFeed) objects = QuestionManager() @@ -212,6 +239,22 @@ class Question(models.Model): def get_latest_revision(self): return self.revisions.all()[0] + get_comments = get_object_comments + + def get_last_update_info(self): + + when, who = post_get_last_update_info(self) + + answers = self.answers.all() + if len(answers) > 0: + for a in answers: + a_when, a_who = a.get_last_update_info() + if a_when > when: + when = a_when + who = a_who + + return when, who + def get_update_summary(self,last_reported_at=None,recipient_email=''): edited = False if self.last_edited_at and self.last_edited_at > last_reported_at: @@ -240,7 +283,7 @@ class Question(models.Model): answer_comments.append(comment) #create the report - if edited or comments or new_answers or modified_answers or answer_comments: + if edited or new_answers or modified_answers or answer_comments: out = [] if edited: out.append(_('%(author)s modified the question') % {'author':self.last_edited_by.username}) @@ -274,6 +317,11 @@ class Question(models.Model): class Meta: db_table = u'question' +class QuestionView(models.Model): + question = models.ForeignKey(Question, related_name='viewed') + who = models.ForeignKey(User, related_name='question_views') + when = models.DateTimeField() + class FavoriteQuestion(models.Model): """A favorite Question of a User.""" question = models.ForeignKey(Question) @@ -303,7 +351,8 @@ class QuestionRevision(models.Model): return self.question.title def get_absolute_url(self): - return '/%s%s/%s' % (_('questions/'),self.question.id,_('revisions')) + print 'in QuestionRevision.get_absolute_url()' + return reverse('question_revisions', args=[self.question.id]) def save(self, **kwargs): """Looks up the next available revision number.""" @@ -383,6 +432,9 @@ class Answer(models.Model): objects = AnswerManager() + get_comments = get_object_comments + get_last_update_info = post_get_last_update_info + def get_user_vote(self, user): votes = self.votes.filter(user=user) if votes.count() > 0: @@ -397,7 +449,7 @@ class Answer(models.Model): return self.question.title def get_absolute_url(self): - return '%s%s#%s' % (reverse('question', args=[self.question.id]), self.question.title, self.id) + return '%s%s#%s' % (reverse('question', args=[self.question.id]), slugify(self.question.title), self.id) class Meta: db_table = u'answer' @@ -415,7 +467,7 @@ class AnswerRevision(models.Model): text = models.TextField() def get_absolute_url(self): - return '/%s%s/%s' % (_('answers/'),self.answer.id,_('revisions')) + return reverse('answer_revisions', kwargs={'id':self.answer.id}) def get_question_title(self): return self.answer.question.title @@ -538,7 +590,7 @@ class Book(models.Model): questions = models.ManyToManyField(Question, related_name='book', db_table='book_question') def get_absolute_url(self): - return '%s' % reverse('book', args=[self.short_name]) + return reverse('book', args=[self.short_name]) def __unicode__(self): return self.title @@ -577,7 +629,6 @@ class AnonymousEmail(models.Model): key = models.CharField(max_length=32) email = models.EmailField(null=False,unique=True) isvalid = models.BooleanField(default=False) - feeds = generic.GenericRelation(EmailFeed) # User extend properties QUESTIONS_PER_PAGE_CHOICES = ( @@ -586,11 +637,30 @@ QUESTIONS_PER_PAGE_CHOICES = ( (50, u'50'), ) +def user_is_username_taken(cls,username): + try: + cls.objects.get(username=username) + return True + except cls.MultipleObjectsReturned: + return True + except cls.DoesNotExist: + return False + +def user_get_q_sel_email_feed_frequency(self): + print 'looking for frequency for user %s' % self + try: + feed_setting = EmailFeedSetting.objects.get(subscriber=self,feed_type='q_sel') + except Exception, e: + print 'have error %s' % e.message + raise e + print 'have freq=%s' % feed_setting.frequency + return feed_setting.frequency + +User.add_to_class('is_approved', models.BooleanField(default=False)) User.add_to_class('email_isvalid', models.BooleanField(default=False)) User.add_to_class('email_key', models.CharField(max_length=32, null=True)) User.add_to_class('reputation', models.PositiveIntegerField(default=1)) User.add_to_class('gravatar', models.CharField(max_length=32)) -User.add_to_class('email_feeds', generic.GenericRelation(EmailFeed)) User.add_to_class('favorite_questions', models.ManyToManyField(Question, through=FavoriteQuestion, related_name='favorited_by')) @@ -608,6 +678,8 @@ User.add_to_class('website', models.URLField(max_length=200, blank=True)) User.add_to_class('location', models.CharField(max_length=100, blank=True)) User.add_to_class('date_of_birth', models.DateField(null=True, blank=True)) User.add_to_class('about', models.TextField(blank=True)) +User.add_to_class('is_username_taken',classmethod(user_is_username_taken)) +User.add_to_class('get_q_sel_email_feed_frequency',user_get_q_sel_email_feed_frequency) # custom signal tags_updated = django.dispatch.Signal(providing_args=["question"]) @@ -630,7 +702,14 @@ def delete_messages(self): def get_profile_url(self): """Returns the URL for this User's profile.""" return '%s%s/' % (reverse('user', args=[self.id]), slugify(self.username)) + +def get_profile_link(self): + profile_link = u'%s' % (self.get_profile_url(),self.username) + logging.debug('in get profile link %s' % profile_link) + return mark_safe(profile_link) + User.add_to_class('get_profile_url', get_profile_url) +User.add_to_class('get_profile_link', get_profile_link) User.add_to_class('get_messages', get_messages) User.add_to_class('delete_messages', delete_messages) diff --git a/forum/templatetags/extra_filters.py b/forum/templatetags/extra_filters.py index d8b8e61f..3644fdc3 100644 --- a/forum/templatetags/extra_filters.py +++ b/forum/templatetags/extra_filters.py @@ -1,5 +1,6 @@ from django import template from forum import auth +import logging register = template.Library() @@ -8,6 +9,10 @@ register = template.Library() def collapse(input): return ' '.join(input.split()) +@register.filter +def can_moderate_users(user): + return auth.can_moderate_users(user) + @register.filter def can_vote_up(user): return auth.can_vote_up(user) @@ -17,8 +22,8 @@ def can_flag_offensive(user): return auth.can_flag_offensive(user) @register.filter -def can_add_comments(user): - return auth.can_add_comments(user) +def can_add_comments(user,subject): + return auth.can_add_comments(user,subject) @register.filter def can_vote_down(user): diff --git a/forum/templatetags/extra_tags.py b/forum/templatetags/extra_tags.py index 06a2d27c..8bd0e128 100644 --- a/forum/templatetags/extra_tags.py +++ b/forum/templatetags/extra_tags.py @@ -1,4 +1,5 @@ import time +import os import datetime import math import re @@ -6,13 +7,15 @@ import logging from django import template from django.utils.encoding import smart_unicode from django.utils.safestring import mark_safe -from django.utils.timesince import timesince from forum.const import * +from forum.models import Question, Answer, QuestionRevision, AnswerRevision from django.utils.translation import ugettext as _ +from django.utils.translation import ungettext +from django.conf import settings register = template.Library() -GRAVATAR_TEMPLATE = ('') @@ -115,6 +118,23 @@ def cnprog_pagesize(context): "pagesize" : context["pagesize"], "is_paginated": context["is_paginated"] } + +@register.inclusion_tag("post_contributor_info.html") +def post_contributor_info(post,contributor_type='original_author'): + """contributor_type: original_author|last_updater + """ + if isinstance(post,Question): + post_type = 'question' + elif isinstance(post,Answer): + post_type = 'answer' + elif isinstance(post,AnswerRevision) or isinstance(post,QuestionRevision): + post_type = 'revision' + return { + 'post':post, + 'post_type':post_type, + 'wiki_on':settings.WIKI_ON, + 'contributor_type':contributor_type + } @register.simple_tag def get_score_badge(user): @@ -216,20 +236,31 @@ def convert2tagname_list(question): @register.simple_tag def diff_date(date, limen=2): - current_time = datetime.datetime(*time.localtime()[0:6]) - diff = current_time - date - diff_days = diff.days - if diff_days > limen: - return date + now = datetime.datetime.now()#datetime(*time.localtime()[0:6])#??? + diff = now - date + days = diff.days + hours = int(diff.seconds/3600) + minutes = int(diff.seconds/60) + + if days > 2: + if date.year == now.year: + return date.strftime(_("%b %d at %H:%M")) + else: + return date.strftime(_("%b %d '%y at %H:%M")) + elif days == 2: + return _('2 days ago') + elif days == 1: + return _('yesterday') + elif minutes > 60: + return ungettext('%(hr)d hour ago','%(hr)d hours ago',hours) % {'hr':hours} else: - return timesince(date) + _(' ago') - + return ungettext('%(min)d min ago','%(min)d mins ago',minutes) % {'min':minutes} + @register.simple_tag def get_latest_changed_timestamp(): try: from time import localtime, strftime from os import path - from django.conf import settings root = settings.SITE_SRC_ROOT dir = ( root, @@ -242,3 +273,78 @@ def get_latest_changed_timestamp(): except: timestr = '' return timestr + +@register.simple_tag +def href(url): + url = '///' + settings.FORUM_SCRIPT_ALIAS + '/' + url + return os.path.normpath(url) + '?v=%d' % settings.RESOURCE_REVISION + +class ItemSeparatorNode(template.Node): + def __init__(self,separator): + sep = separator.strip() + if sep[0] == sep[-1] and sep[0] in ('\'','"'): + sep = sep[1:-1] + else: + raise template.TemplateSyntaxError('separator in joinitems tag must be quoted') + self.content = sep + def render(self,context): + return self.content + +class JoinItemListNode(template.Node): + def __init__(self,separator=ItemSeparatorNode("''"), items=()): + self.separator = separator + self.items = items + def render(self,context): + out = [] + empty_re = re.compile(r'^\s*$') + for item in self.items: + bit = item.render(context) + if not empty_re.search(bit): + out.append(bit) + return self.separator.render(context).join(out) + +@register.tag(name="joinitems") +def joinitems(parser,token): + try: + tagname,junk,sep_token = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters") + if junk == 'using': + sep_node = ItemSeparatorNode(sep_token) + else: + raise template.TemplateSyntaxError("joinitems tag requires 'using \"separator html\"' parameters") + nodelist = [] + while True: + nodelist.append(parser.parse(('separator','endjoinitems'))) + next = parser.next_token() + if next.contents == 'endjoinitems': + break + + return JoinItemListNode(separator=sep_node,items=nodelist) + +class BlockResourceNode(template.Node): + def __init__(self,nodelist): + self.items = nodelist + def render(self,context): + out = '///' + settings.FORUM_SCRIPT_ALIAS + if self.items: + out += '/' + for item in self.items: + bit = item.render(context) + out += bit + out = out.replace(' ','') + return os.path.normpath(out) + '?v=%d' % settings.RESOURCE_REVISION + +@register.tag(name='blockresource') +def blockresource(parser,token): + try: + tagname = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError("blockresource tag does not use arguments") + nodelist = [] + while True: + nodelist.append(parser.parse(('endblockresource'))) + next = parser.next_token() + if next.contents == 'endblockresource': + break + return BlockResourceNode(nodelist) diff --git a/forum/templatetags/smart_if.py b/forum/templatetags/smart_if.py new file mode 100644 index 00000000..a8fc1944 --- /dev/null +++ b/forum/templatetags/smart_if.py @@ -0,0 +1,401 @@ +""" +A smarter {% if %} tag for django templates. + +While retaining current Django functionality, it also handles equality, +greater than and less than operators. Some common case examples:: + + {% if articles|length >= 5 %}...{% endif %} + {% if "ifnotequal tag" != "beautiful" %}...{% endif %} +""" +import unittest +from django import template + + +register = template.Library() + + +#============================================================================== +# Calculation objects +#============================================================================== + +class BaseCalc(object): + def __init__(self, var1, var2=None, negate=False): + self.var1 = var1 + self.var2 = var2 + self.negate = negate + + def resolve(self, context): + try: + var1, var2 = self.resolve_vars(context) + outcome = self.calculate(var1, var2) + except: + outcome = False + if self.negate: + return not outcome + return outcome + + def resolve_vars(self, context): + var2 = self.var2 and self.var2.resolve(context) + return self.var1.resolve(context), var2 + + def calculate(self, var1, var2): + raise NotImplementedError() + + +class Or(BaseCalc): + def calculate(self, var1, var2): + return var1 or var2 + + +class And(BaseCalc): + def calculate(self, var1, var2): + return var1 and var2 + + +class Equals(BaseCalc): + def calculate(self, var1, var2): + return var1 == var2 + + +class Greater(BaseCalc): + def calculate(self, var1, var2): + return var1 > var2 + + +class GreaterOrEqual(BaseCalc): + def calculate(self, var1, var2): + return var1 >= var2 + + +class In(BaseCalc): + def calculate(self, var1, var2): + return var1 in var2 + + +#============================================================================== +# Tests +#============================================================================== + +class TestVar(object): + """ + A basic self-resolvable object similar to a Django template variable. Used + to assist with tests. + """ + def __init__(self, value): + self.value = value + + def resolve(self, context): + return self.value + + +class SmartIfTests(unittest.TestCase): + def setUp(self): + self.true = TestVar(True) + self.false = TestVar(False) + self.high = TestVar(9000) + self.low = TestVar(1) + + def assertCalc(self, calc, context=None): + """ + Test a calculation is True, also checking the inverse "negate" case. + """ + context = context or {} + self.assert_(calc.resolve(context)) + calc.negate = not calc.negate + self.assertFalse(calc.resolve(context)) + + def assertCalcFalse(self, calc, context=None): + """ + Test a calculation is False, also checking the inverse "negate" case. + """ + context = context or {} + self.assertFalse(calc.resolve(context)) + calc.negate = not calc.negate + self.assert_(calc.resolve(context)) + + def test_or(self): + self.assertCalc(Or(self.true)) + self.assertCalcFalse(Or(self.false)) + self.assertCalc(Or(self.true, self.true)) + self.assertCalc(Or(self.true, self.false)) + self.assertCalc(Or(self.false, self.true)) + self.assertCalcFalse(Or(self.false, self.false)) + + def test_and(self): + self.assertCalc(And(self.true, self.true)) + self.assertCalcFalse(And(self.true, self.false)) + self.assertCalcFalse(And(self.false, self.true)) + self.assertCalcFalse(And(self.false, self.false)) + + def test_equals(self): + self.assertCalc(Equals(self.low, self.low)) + self.assertCalcFalse(Equals(self.low, self.high)) + + def test_greater(self): + self.assertCalc(Greater(self.high, self.low)) + self.assertCalcFalse(Greater(self.low, self.low)) + self.assertCalcFalse(Greater(self.low, self.high)) + + def test_greater_or_equal(self): + self.assertCalc(GreaterOrEqual(self.high, self.low)) + self.assertCalc(GreaterOrEqual(self.low, self.low)) + self.assertCalcFalse(GreaterOrEqual(self.low, self.high)) + + def test_in(self): + list_ = TestVar([1,2,3]) + invalid_list = TestVar(None) + self.assertCalc(In(self.low, list_)) + self.assertCalcFalse(In(self.low, invalid_list)) + + def test_parse_bits(self): + var = IfParser([True]).parse() + self.assert_(var.resolve({})) + var = IfParser([False]).parse() + self.assertFalse(var.resolve({})) + + var = IfParser([False, 'or', True]).parse() + self.assert_(var.resolve({})) + + var = IfParser([False, 'and', True]).parse() + self.assertFalse(var.resolve({})) + + var = IfParser(['not', False, 'and', 'not', False]).parse() + self.assert_(var.resolve({})) + + var = IfParser(['not', 'not', True]).parse() + self.assert_(var.resolve({})) + + var = IfParser([1, '=', 1]).parse() + self.assert_(var.resolve({})) + + var = IfParser([1, 'not', '=', 1]).parse() + self.assertFalse(var.resolve({})) + + var = IfParser([1, 'not', 'not', '=', 1]).parse() + self.assert_(var.resolve({})) + + var = IfParser([1, '!=', 1]).parse() + self.assertFalse(var.resolve({})) + + var = IfParser([3, '>', 2]).parse() + self.assert_(var.resolve({})) + + var = IfParser([1, '<', 2]).parse() + self.assert_(var.resolve({})) + + var = IfParser([2, 'not', 'in', [2, 3]]).parse() + self.assertFalse(var.resolve({})) + + var = IfParser([1, 'or', 1, '=', 2]).parse() + self.assert_(var.resolve({})) + + def test_boolean(self): + var = IfParser([True, 'and', True, 'and', True]).parse() + self.assert_(var.resolve({})) + var = IfParser([False, 'or', False, 'or', True]).parse() + self.assert_(var.resolve({})) + var = IfParser([True, 'and', False, 'or', True]).parse() + self.assert_(var.resolve({})) + var = IfParser([False, 'or', True, 'and', True]).parse() + self.assert_(var.resolve({})) + + var = IfParser([True, 'and', True, 'and', False]).parse() + self.assertFalse(var.resolve({})) + var = IfParser([False, 'or', False, 'or', False]).parse() + self.assertFalse(var.resolve({})) + var = IfParser([False, 'or', True, 'and', False]).parse() + self.assertFalse(var.resolve({})) + var = IfParser([False, 'and', True, 'or', False]).parse() + self.assertFalse(var.resolve({})) + + def test_invalid(self): + self.assertRaises(ValueError, IfParser(['not']).parse) + self.assertRaises(ValueError, IfParser(['==']).parse) + self.assertRaises(ValueError, IfParser([1, 'in']).parse) + self.assertRaises(ValueError, IfParser([1, '>', 'in']).parse) + self.assertRaises(ValueError, IfParser([1, '==', 'not', 'not']).parse) + self.assertRaises(ValueError, IfParser([1, 2]).parse) + + +OPERATORS = { + '=': (Equals, True), + '==': (Equals, True), + '!=': (Equals, False), + '>': (Greater, True), + '>=': (GreaterOrEqual, True), + '<=': (Greater, False), + '<': (GreaterOrEqual, False), + 'or': (Or, True), + 'and': (And, True), + 'in': (In, True), +} +BOOL_OPERATORS = ('or', 'and') + + +class IfParser(object): + error_class = ValueError + + def __init__(self, tokens): + self.tokens = tokens + + def _get_tokens(self): + return self._tokens + + def _set_tokens(self, tokens): + self._tokens = tokens + self.len = len(tokens) + self.pos = 0 + + tokens = property(_get_tokens, _set_tokens) + + def parse(self): + if self.at_end(): + raise self.error_class('No variables provided.') + var1 = self.get_bool_var() + while not self.at_end(): + op, negate = self.get_operator() + var2 = self.get_bool_var() + var1 = op(var1, var2, negate=negate) + return var1 + + def get_token(self, eof_message=None, lookahead=False): + negate = True + token = None + pos = self.pos + while token is None or token == 'not': + if pos >= self.len: + if eof_message is None: + raise self.error_class() + raise self.error_class(eof_message) + token = self.tokens[pos] + negate = not negate + pos += 1 + if not lookahead: + self.pos = pos + return token, negate + + def at_end(self): + return self.pos >= self.len + + def create_var(self, value): + return TestVar(value) + + def get_bool_var(self): + """ + Returns either a variable by itself or a non-boolean operation (such as + ``x == 0`` or ``x < 0``). + + This is needed to keep correct precedence for boolean operations (i.e. + ``x or x == 0`` should be ``x or (x == 0)``, not ``(x or x) == 0``). + """ + var = self.get_var() + if not self.at_end(): + op_token = self.get_token(lookahead=True)[0] + if isinstance(op_token, basestring) and (op_token not in + BOOL_OPERATORS): + op, negate = self.get_operator() + return op(var, self.get_var(), negate=negate) + return var + + def get_var(self): + token, negate = self.get_token('Reached end of statement, still ' + 'expecting a variable.') + if isinstance(token, basestring) and token in OPERATORS: + raise self.error_class('Expected variable, got operator (%s).' % + token) + var = self.create_var(token) + if negate: + return Or(var, negate=True) + return var + + def get_operator(self): + token, negate = self.get_token('Reached end of statement, still ' + 'expecting an operator.') + if not isinstance(token, basestring) or token not in OPERATORS: + raise self.error_class('%s is not a valid operator.' % token) + if self.at_end(): + raise self.error_class('No variable provided after "%s".' % token) + op, true = OPERATORS[token] + if not true: + negate = not negate + return op, negate + + +#============================================================================== +# Actual templatetag code. +#============================================================================== + +class TemplateIfParser(IfParser): + error_class = template.TemplateSyntaxError + + def __init__(self, parser, *args, **kwargs): + self.template_parser = parser + return super(TemplateIfParser, self).__init__(*args, **kwargs) + + def create_var(self, value): + return self.template_parser.compile_filter(value) + + +class SmartIfNode(template.Node): + def __init__(self, var, nodelist_true, nodelist_false=None): + self.nodelist_true, self.nodelist_false = nodelist_true, nodelist_false + self.var = var + + def render(self, context): + if self.var.resolve(context): + return self.nodelist_true.render(context) + if self.nodelist_false: + return self.nodelist_false.render(context) + return '' + + def __repr__(self): + return "" + + def __iter__(self): + for node in self.nodelist_true: + yield node + if self.nodelist_false: + for node in self.nodelist_false: + yield node + + def get_nodes_by_type(self, nodetype): + nodes = [] + if isinstance(self, nodetype): + nodes.append(self) + nodes.extend(self.nodelist_true.get_nodes_by_type(nodetype)) + if self.nodelist_false: + nodes.extend(self.nodelist_false.get_nodes_by_type(nodetype)) + return nodes + + +@register.tag('if') +def smart_if(parser, token): + """ + A smarter {% if %} tag for django templates. + + While retaining current Django functionality, it also handles equality, + greater than and less than operators. Some common case examples:: + + {% if articles|length >= 5 %}...{% endif %} + {% if "ifnotequal tag" != "beautiful" %}...{% endif %} + + Arguments and operators _must_ have a space between them, so + ``{% if 1>2 %}`` is not a valid smart if tag. + + All supported operators are: ``or``, ``and``, ``in``, ``=`` (or ``==``), + ``!=``, ``>``, ``>=``, ``<`` and ``<=``. + """ + bits = token.split_contents()[1:] + var = TemplateIfParser(parser, bits).parse() + nodelist_true = parser.parse(('else', 'endif')) + token = parser.next_token() + if token.contents == 'else': + nodelist_false = parser.parse(('endif',)) + parser.delete_first_token() + else: + nodelist_false = None + return SmartIfNode(var, nodelist_true, nodelist_false) + + +if __name__ == '__main__': + unittest.main() diff --git a/forum/urls.py b/forum/urls.py new file mode 100644 index 00000000..a08fe716 --- /dev/null +++ b/forum/urls.py @@ -0,0 +1,72 @@ +import os.path +from django.conf.urls.defaults import * +from django.contrib import admin +from forum import views as app +from forum.feed import RssLastestQuestionsFeed +from django.utils.translation import ugettext as _ + +admin.autodiscover() +feeds = { + 'rss': RssLastestQuestionsFeed +} + +APP_PATH = os.path.dirname(os.path.dirname(__file__)) +urlpatterns = patterns('', + url(r'^$', app.index, name='index'), + (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/content/images/favicon.ico'}), + (r'^favicon\.gif$', 'django.views.generic.simple.redirect_to', {'url': '/content/images/favicon.gif'}), + (r'^content/(?P.*)$', 'django.views.static.serve', + {'document_root': os.path.join(APP_PATH, 'templates/content').replace('\\','/')} + ), + (r'^%s(?P.*)$' % _('upfiles/'), 'django.views.static.serve', + {'document_root': os.path.join(APP_PATH, 'templates/upfiles').replace('\\','/')} + ), + (r'^%s/$' % _('signin/'), 'django_authopenid.views.signin'), + url(r'^%s$' % _('about/'), app.about, name='about'), + url(r'^%s$' % _('faq/'), app.faq, name='faq'), + url(r'^%s$' % _('privacy/'), app.privacy, name='privacy'), + url(r'^%s$' % _('logout/'), app.logout, name='logout'), + url(r'^%s(?P\d+)/%s$' % (_('answers/'), _('comments/')), app.answer_comments, name='answer_comments'), + url(r'^%s(?P\d+)/%s$' % (_('answers/'), _('edit/')), app.edit_answer, name='edit_answer'), + url(r'^%s(?P\d+)/%s$' % (_('answers/'), _('revisions/')), app.answer_revisions, name='answer_revisions'), + url(r'^%s$' % _('questions/'), app.questions, name='questions'), + url(r'^%s%s$' % (_('questions/'), _('ask/')), app.ask, name='ask'), + url(r'^%s%s$' % (_('questions/'), _('unanswered/')), app.unanswered, name='unanswered'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('edit/')), app.edit_question, name='edit_question'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('close/')), app.close, name='close'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('reopen/')), app.reopen, name='reopen'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('answer/')), app.answer, name='answer'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('vote/')), app.vote, name='vote'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('revisions/')), app.question_revisions, name='question_revisions'), + url(r'^%s(?P\d+)/%s$' % (_('questions/'), _('comments/')), app.question_comments, name='question_comments'), + + url(r'^%s(?P\d+)/%s(?P\d+)/%s$' % (_('questions/'), _('comments/'),_('delete/')), \ + app.delete_comment, kwargs={'commented_object_type':'question'},\ + name='delete_question_comment'), + + url(r'^%s(?P\d+)/%s(?P\d+)/%s$' % (_('answers/'), _('comments/'),_('delete/')), \ + 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$' % _('tags/'), app.tags, name='tags'), + url(r'^%s(?P[^/]+)/$' % _('tags/'), app.tag), + url(r'^%s$' % _('users/'),app.users, name='users'), + url(r'^%s(?P\d+)/$' % _('moderate-user/'), app.moderate_user, name='moderate_user'), + url(r'^%s(?P\d+)/%s$' % (_('users/'), _('edit/')), app.edit_user, name='edit_user'), + url(r'^%s(?P\d+)//*' % _('users/'), app.user, name='user'), + url(r'^%s$' % _('badges/'),app.badges, name='badges'), + url(r'^%s(?P\d+)//*' % _('badges/'), app.badge, name='badge'), + url(r'^%s%s$' % (_('messages/'), _('markread/')),app.read_message, name='read_message'), + # (r'^admin/doc/' % _('admin/doc'), include('django.contrib.admindocs.urls')), + (r'^%s(.*)' % _('nimda/'), admin.site.root), + url(r'^feeds/(?P.*)/$', 'django.contrib.syndication.views.feed', {'feed_dict': feeds}), + (r'^%s$' % _('upload/'), app.upload), + url(r'^%s$' % _('books/'), app.books, name='books'), + url(r'^%s%s(?P[^/]+)/$' % (_('books/'), _('ask/')), app.ask_book, name='ask_book'), + url(r'^%s(?P[^/]+)/$' % _('books/'), app.book, name='book'), + url(r'^%s$' % _('search/'), app.search, name='search'), + url(r'^%s$' % _('feedback/'), app.feedback, name='feedback'), + (r'^%s' % _('account/'), include('django_authopenid.urls')), + (r'^i18n/', include('django.conf.urls.i18n')), +) diff --git a/forum/user.py b/forum/user.py index 41811db9..40bf6a89 100644 --- a/forum/user.py +++ b/forum/user.py @@ -64,11 +64,11 @@ USER_TEMPLATE_VIEWS = ( data_size = 50 ), UserView( - id = 'preferences', - tab_title = _('preferences'), - tab_description = _('user preference settings'), - page_title = _('profile - user preferences'), - view_name = 'user_preferences', - template_file = 'user_preferences.html' + id = 'email_subscriptions', + tab_title = _('email subscriptions'), + tab_description = _('email subscription settings'), + page_title = _('profile - email subscriptions'), + view_name = 'user_email_subscriptions', + template_file = 'user_email_subscriptions.html' ) ) diff --git a/forum/views.py b/forum/views.py index d663a4cb..e4ccaa16 100644 --- a/forum/views.py +++ b/forum/views.py @@ -7,16 +7,18 @@ from django.conf import settings from django.core.files.storage import default_storage from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.auth.decorators import login_required -from django.http import HttpResponseRedirect, HttpResponse,Http404 +from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404 from django.core.paginator import Paginator, EmptyPage, InvalidPage -from django.template import RequestContext +from django.template import RequestContext, loader from django.utils.html import * from django.utils import simplejson from django.core import serializers +from django.core.mail import mail_admins from django.db import transaction from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _ from django.template.defaultfilters import slugify +from django.core.exceptions import PermissionDenied from utils.html import sanitize_html from markdown2 import Markdown @@ -28,6 +30,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 # used in index page INDEX_PAGE_SIZE = 20 @@ -63,24 +66,28 @@ def _get_tags_cache_json(): tags = simplejson.dumps(tags_list) return tags +def _get_and_remember_questions_sort_method(request, view_dic, default): + if default not in view_dic: + raise Exception('default value must be in view_dic') + + q_sort_method = request.REQUEST.get('sort', None) + if q_sort_method == None: + q_sort_method = request.session.get('questions_sort_method', default) + + if q_sort_method not in view_dic: + q_sort_method = default + request.session['questions_sort_method'] = q_sort_method + return q_sort_method, view_dic[q_sort_method] + def index(request): - view_id = request.GET.get('sort', None) view_dic = { "latest":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score", - "trans": "-last_activity_at" } - try: - orderby = view_dic[view_id] - except KeyError: - view_id = "latest" - orderby = "-last_activity_at" - # group questions by author_id of 28,29 - if view_id == 'trans': - questions = Question.objects.get_translation_questions(orderby, INDEX_PAGE_SIZE) - else: - questions = Question.objects.get_questions_by_pagesize(orderby, INDEX_PAGE_SIZE) + view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest') + + questions = Question.objects.get_questions_by_pagesize(orderby, INDEX_PAGE_SIZE) # RISK - inner join queries questions = questions.select_related() tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE) @@ -98,7 +105,34 @@ def about(request): return render_to_response('about.html', context_instance=RequestContext(request)) def faq(request): - return render_to_response('faq.html', context_instance=RequestContext(request)) + data = { + 'gravatar_faq_url': reverse('faq') + '#gravatar', + 'send_email_key_url': reverse('send_email_key'), + 'ask_question_url': reverse('ask'), + } + return render_to_response('faq.html', data, context_instance=RequestContext(request)) + +def feedback(request): + data = {} + form = None + if request.method == "POST": + form = FeedbackForm(request.POST) + if form.is_valid(): + if not request.user.is_authenticated: + data['email'] = form.cleaned_data.get('email',None) + data['message'] = form.cleaned_data['message'] + data['name'] = form.cleaned_data.get('name',None) + message = render_to_response('feedback_email.txt',data,context_instance=RequestContext(request)) + mail_admins(_('Q&A forum feedback'), message) + msg = _('Thanks for the feedback!') + request.user.message_set.create(message=msg) + return HttpResponseRedirect(get_next_url(request)) + else: + form = FeedbackForm(initial={'next':get_next_url(request)}) + + data['form'] = form + return render_to_response('feedback.html', data, context_instance=RequestContext(request)) +feedback.CANCEL_MESSAGE=_('We look forward to hearing your feedback! Please, give it next time :)') def privacy(request): return render_to_response('privacy.html', context_instance=RequestContext(request)) @@ -122,13 +156,8 @@ def questions(request, tagname=None, unanswered=False): except ValueError: page = 1 - view_id = request.GET.get('sort', None) view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" } - try: - orderby = view_dic[view_id] - except KeyError: - view_id = "latest" - orderby = "-added_at" + view_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest') # check if request is from tagged questions if tagname is not None: @@ -208,16 +237,12 @@ def create_new_answer( question=None, author=None,\ #set notification/delete if email_notify: - try: - EmailFeed.objects.get(feed_id = question.id, subscriber_id = author.id, feed_content_type=question_type) - except EmailFeed.DoesNotExist: - feed = EmailFeed(content = question, subscriber = author) - feed.save() + if author not in question.followed_by.all(): + question.followed_by.add(author) else: #not sure if this is necessary. ajax should take care of this... try: - feed = Email.objects.get(feed_id = question.id, subscriber_id = author.id, feed_content_type=question_type) - feed.delete() + question.followed_by.remove(author) except: pass @@ -267,7 +292,7 @@ def ask(request): if form.is_valid(): added_at = datetime.datetime.now() - title = strip_tags(form.cleaned_data['title']) + title = strip_tags(form.cleaned_data['title'].strip()) wiki = form.cleaned_data['wiki'] tagnames = form.cleaned_data['tags'].strip() text = form.cleaned_data['text'] @@ -302,7 +327,7 @@ def ask(request): ip_addr = request.META['REMOTE_ADDR'], ) question.save() - return HttpResponseRedirect('/%s%s%s' % ( _('account/'),_('signin/'),_('newquestion/'))) + return HttpResponseRedirect(reverse('user_signin_new_question')) else: form = AskForm() @@ -310,6 +335,7 @@ def ask(request): return render_to_response('ask.html', { 'form' : form, 'tags' : tags, + 'email_validation_faq_url':reverse('faq') + '#validate', }, context_instance=RequestContext(request)) def question(request, id): @@ -317,13 +343,26 @@ def question(request, id): page = int(request.GET.get('page', '1')) except ValueError: page = 1 - view_id = request.GET.get('sort', 'votes') + + view_id = request.GET.get('sort', None) view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score" } try: orderby = view_dic[view_id] except KeyError: - view_id = "votes" - orderby = "-score" + qsm = request.session.get('questions_sort_method',None) + if qsm in ('mostvoted','latest'): + logging.debug('loaded from session ' + qsm) + if qsm == 'mostvoted': + view_id = 'votes' + orderby = '-score' + else: + view_id = 'latest' + orderby = '-added_at' + else: + view_id = "votes" + orderby = "-score" + + logging.debug('view_id=' + str(view_id)) question = get_object_or_404(Question, id=id) if question.deleted and not can_view_deleted_post(request.user, question): @@ -349,7 +388,6 @@ def question(request, id): vote_value = 1 user_answer_votes[answer.id] = vote_value - if answers is not None: answers = answers.order_by("-accepted", orderby) @@ -363,8 +401,38 @@ def question(request, id): objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE) page_objects = objects_list.page(page) - # update view count - Question.objects.update_view_count(question) + + #todo: merge view counts per user and per session + #1) view count per session + update_view_count = False + if 'question_view_times' not in request.session: + request.session['question_view_times'] = {} + + last_seen = request.session['question_view_times'].get(question.id,None) + updated_when, updated_who = question.get_last_update_info() + + if updated_who != request.user: + if last_seen: + if last_seen < updated_when: + update_view_count = True + else: + update_view_count = True + + request.session['question_view_times'][question.id] = datetime.datetime.now() + + if update_view_count: + question.view_count += 1 + question.save() + + #2) question view count per user + if request.user.is_authenticated(): + try: + question_view = QuestionView.objects.get(who=request.user, question=question) + except QuestionView.DoesNotExist: + question_view = QuestionView(who=request.user, question=question) + question_view.when = datetime.datetime.now() + question_view.save() + return render_to_response('question.html', { "question" : question, "question_vote" : question_vote, @@ -623,6 +691,7 @@ QUESTION_REVISION_TEMPLATE = ('

%(title)s

\n' def question_revisions(request, id): post = get_object_or_404(Question, id=id) revisions = list(post.revisions.all()) + revisions.reverse() for i, revision in enumerate(revisions): revision.html = QUESTION_REVISION_TEMPLATE % { 'title': revision.title, @@ -631,16 +700,15 @@ def question_revisions(request, id): for tag in revision.tagnames.split(' ')]), } if i > 0: - revisions[i - 1].diff = htmldiff(revision.html, - revisions[i - 1].html) + revisions[i].diff = htmldiff(revisions[i-1].html, revision.html) else: - revisions[i - 1].diff = QUESTION_REVISION_TEMPLATE % { + revisions[i].diff = QUESTION_REVISION_TEMPLATE % { 'title': revisions[0].title, 'html': sanitize_html(markdowner.convert(revisions[0].text)), 'tags': ' '.join(['' % tag for tag in revisions[0].tagnames.split(' ')]), } - revisions[i - 1].summary = None + revisions[i].summary = _('initial version') return render_to_response('revisions_question.html', { 'post': post, 'revisions': revisions, @@ -650,16 +718,16 @@ ANSWER_REVISION_TEMPLATE = ('
%(html)s
') def answer_revisions(request, id): post = get_object_or_404(Answer, id=id) revisions = list(post.revisions.all()) + revisions.reverse() for i, revision in enumerate(revisions): revision.html = ANSWER_REVISION_TEMPLATE % { 'html': sanitize_html(markdowner.convert(revision.text)) } if i > 0: - revisions[i - 1].diff = htmldiff(revision.html, - revisions[i - 1].html) + revisions[i].diff = htmldiff(revisions[i-1].html, revision.html) else: - revisions[i - 1].diff = revisions[i-1].text - revisions[i - 1].summary = None + revisions[i].diff = revisions[i].text + revisions[i].summary = _('initial version') return render_to_response('revisions_answer.html', { 'post': post, 'revisions': revisions, @@ -696,8 +764,7 @@ def answer(request, id): ip_addr = request.META['REMOTE_ADDR'], ) anon.save() - return HttpResponseRedirect('/%s%s%s' % ( _('account/'), - _('signin/'),_('newquestion/'))) + return HttpResponseRedirect(reverse('user_signin_new_answer')) return HttpResponseRedirect(question.get_absolute_url()) @@ -712,7 +779,7 @@ def tags(request): if request.method == "GET": stag = request.GET.get("q", "").strip() - if stag is not None: + if stag != '': objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE) else: if sortby == "name": @@ -738,7 +805,7 @@ def tags(request): 'has_next': tags.has_next(), 'previous': tags.previous_page_number(), 'next': tags.next_page_number(), - 'base_url' : '/%s?sort=%s&' % (_('tags/'), sortby) + 'base_url' : reverse('tags') + '?sort=%s&' % sortby } }, context_instance=RequestContext(request)) @@ -930,7 +997,8 @@ def vote(request, id): if not can_delete_post(request.user, post): response_data['allowed'] = -2 - elif post.deleted: + elif post.deleted == True: + logging.debug('debug restoring post in view') onDeleteCanceled(post, request.user) response_data['status'] = 1 else: @@ -939,13 +1007,19 @@ def vote(request, id): elif vote_type == '11':#subscribe q updates user = request.user if user.is_authenticated(): - try: - EmailFeed.objects.get(feed_id=question.id,subscriber_id=user.id,feed_content_type=question_type) - except EmailFeed.DoesNotExist: - feed = EmailFeed(subscriber=user,content=question) - feed.save() + if user not in question.followed_by.all(): + question.followed_by.add(user) if settings.EMAIL_VALIDATION == 'on' and user.email_isvalid == False: - response_data['message'] = _('subscription saved, %(email)s needs validation') % {'email':user.email} + response_data['message'] = \ + _('subscription saved, %(email)s needs validation, see %(details_url)s') \ + % {'email':user.email,'details_url':reverse('faq') + '#validate'} + feed_setting = EmailFeedSetting.objects.get(subscriber=user,feed_type='q_sel') + if feed_setting.frequency == 'n': + feed_setting.frequency = 'd' + feed_setting.save() + if 'message' in response_data: + response_data['message'] += '
' + response_data['message'] = _('email update frequency has been set to daily') #response_data['status'] = 1 #responst_data['allowed'] = 1 else: @@ -955,12 +1029,8 @@ def vote(request, id): elif vote_type == '12':#unsubscribe q updates user = request.user if user.is_authenticated(): - try: - feed = EmailFeed.objects.get(feed_id=question.id,subscriber_id=user.id) - feed.delete() - except EmailFeed.DoesNotExist: - pass - + if user in question.followed_by.all(): + question.followed_by.remove(user) else: response_data['success'] = 0 response_data['message'] = u'Request mode is not supported. Please try again.' @@ -991,11 +1061,11 @@ def users(request): # default else: objects_list = Paginator(User.objects.all().order_by('-reputation'), USERS_PAGE_SIZE) - base_url = '/%s?sort=%s&' % (_('users/'), sortby) + base_url = reverse('users') + '?sort=%s&' % sortby else: sortby = "reputation" objects_list = Paginator(User.objects.extra(where=['username like %s'], params=['%' + suser + '%']).order_by('-reputation'), USERS_PAGE_SIZE) - base_url = '/%s?name=%s&sort=%s&' % (_('users/'), suser, sortby) + base_url = reverse('users') + '?name=%s&sort=%s&' % (suser, sortby) try: users = objects_list.page(page) @@ -1027,6 +1097,26 @@ def user(request, id): func = getattr(views, user_view.view_name) return func(request, id, user_view) +@login_required +def moderate_user(request, id): + """ajax handler of user moderation + """ + if not auth.can_moderate_users(request.user) or request.method != 'POST': + raise Http404 + if not request.is_ajax(): + return HttpResponseForbidden(mimetype="application/json") + + user = get_object_or_404(User, id=id) + form = ModerateUserForm(request.POST, instance=user) + + if form.is_valid(): + form.save() + logging.debug('data saved') + response = HttpResponse(simplejson.dumps(''), mimetype="application/json") + else: + response = HttpResponseForbidden(mimetype="application/json") + return response + @login_required def edit_user(request, id): user = get_object_or_404(User, id=id) @@ -1058,6 +1148,7 @@ def edit_user(request, id): form = EditUserForm(user) return render_to_response('user_edit.html', { 'form' : form, + 'gravatar_faq_url' : reverse('faq') + '#gravatar', }, context_instance=RequestContext(request)) def user_stats(request, user_id, user_view): @@ -1111,7 +1202,7 @@ def user_stats(request, user_id, user_view): 'comment_count' : 'answer.comment_count' }, tables=['question', 'answer'], - where=['answer.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], + where=['answer.deleted=0 AND question.deleted=0 AND answer.author_id=%s AND answer.question_id=question.id'], params=[user_id], order_by=['-vote_count', '-answer_id'], select_params=[user_id] @@ -1152,7 +1243,13 @@ def user_stats(request, user_id, user_view): from django.db.models import Count awards = awards.annotate(count = Count('badge__id')) + if auth.can_moderate_users(request.user): + moderate_user_form = ModerateUserForm(instance=user) + else: + moderate_user_form = None + return render_to_response(user_view.template_file,{ + 'moderate_user_form': moderate_user_form, "tab_name" : user_view.id, "tab_description" : user_view.tab_description, "page_title" : user_view.page_title, @@ -1184,10 +1281,9 @@ def user_recent(request, user_id, user_view): self.title = title self.summary = summary slug_title = slugify(title) + self.title_link = reverse('question', kwargs={'id':question_id}) + u'%s' % slug_title if int(answer_id) > 0: - self.title_link = u'/%s%s/%s#%s' %(_('questions/'),question_id, slug_title, answer_id) - else: - self.title_link = u'/%s%s/%s' %(_('questions/'),question_id, slug_title) + self.title_link += '#%s' % answer_id class AwardEvent: def __init__(self, time, type, id): @@ -1207,7 +1303,7 @@ def user_recent(request, user_id, user_view): }, tables=['activity', 'question'], where=['activity.content_type_id = %s AND activity.object_id = ' + - 'question.id AND activity.user_id = %s AND activity.activity_type = %s'], + 'question.id AND question.deleted=0 AND activity.user_id = %s AND activity.activity_type = %s'], params=[question_type_id, user_id, TYPE_ACTIVITY_ASK_QUESTION], order_by=['-activity.active_at'] ).values( @@ -1232,7 +1328,8 @@ def user_recent(request, user_id, user_view): }, tables=['activity', 'answer', 'question'], where=['activity.content_type_id = %s AND activity.object_id = answer.id AND ' + - 'answer.question_id=question.id AND activity.user_id=%s AND activity.activity_type=%s'], + 'answer.question_id=question.id AND answer.deleted=0 AND activity.user_id=%s AND '+ + 'activity.activity_type=%s AND question.deleted=0'], params=[answer_type_id, user_id, TYPE_ACTIVITY_ANSWER], order_by=['-activity.active_at'] ).values( @@ -1259,7 +1356,8 @@ def user_recent(request, user_id, user_view): where=['activity.content_type_id = %s AND activity.object_id = comment.id AND '+ 'activity.user_id = comment.user_id AND comment.object_id=question.id AND '+ - 'comment.content_type_id=%s AND activity.user_id = %s AND activity.activity_type=%s'], + 'comment.content_type_id=%s AND activity.user_id = %s AND activity.activity_type=%s AND ' + + 'question.deleted=0'], params=[comment_type_id, question_type_id, user_id, TYPE_ACTIVITY_COMMENT_QUESTION], order_by=['-comment.added_at'] ).values( @@ -1288,7 +1386,8 @@ def user_recent(request, user_id, user_view): where=['activity.content_type_id = %s AND activity.object_id = comment.id AND '+ 'activity.user_id = comment.user_id AND comment.object_id=answer.id AND '+ 'comment.content_type_id=%s AND question.id = answer.question_id AND '+ - 'activity.user_id = %s AND activity.activity_type=%s'], + 'activity.user_id = %s AND activity.activity_type=%s AND '+ + 'answer.deleted=0 AND question.deleted=0'], params=[comment_type_id, answer_type_id, user_id, TYPE_ACTIVITY_COMMENT_ANSWER], order_by=['-comment.added_at'] ).values( @@ -1313,8 +1412,9 @@ def user_recent(request, user_id, user_view): 'activity_type' : 'activity.activity_type', 'summary' : 'question_revision.summary' }, - tables=['activity', 'question_revision'], + tables=['activity', 'question_revision', 'question'], where=['activity.content_type_id = %s AND activity.object_id = question_revision.id AND '+ + 'question_revision.id=question.id AND question.deleted=0 AND '+ 'activity.user_id = question_revision.author_id AND activity.user_id = %s AND '+ 'activity.activity_type=%s'], params=[question_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_QUESTION], @@ -1347,6 +1447,7 @@ def user_recent(request, user_id, user_view): where=['activity.content_type_id = %s AND activity.object_id = answer_revision.id AND '+ 'activity.user_id = answer_revision.author_id AND activity.user_id = %s AND '+ 'answer_revision.answer_id=answer.id AND answer.question_id = question.id AND '+ + 'question.deleted=0 AND answer.deleted=0 AND '+ 'activity.activity_type=%s'], params=[answer_revision_type_id, user_id, TYPE_ACTIVITY_UPDATE_ANSWER], order_by=['-activity.active_at'] @@ -1375,6 +1476,7 @@ def user_recent(request, user_id, user_view): tables=['activity', 'answer', 'question'], where=['activity.content_type_id = %s AND activity.object_id = answer.id AND '+ 'activity.user_id = question.author_id AND activity.user_id = %s AND '+ + 'answer.deleted=0 AND question.deleted=0 AND '+ 'answer.question_id=question.id AND activity.activity_type=%s'], params=[answer_type_id, user_id, TYPE_ACTIVITY_MARK_ANSWER], order_by=['-activity.active_at'] @@ -1427,9 +1529,9 @@ def user_responses(request, user_id, user_view): def __init__(self, type, title, question_id, answer_id, time, username, user_id, content): self.type = type self.title = title - self.titlelink = u'/%s%s/%s#%s' % (_('questions/'), question_id, title, answer_id) + self.titlelink = reverse('questions') + u'%s/%s#%s' % (question_id, title, answer_id) self.time = time - self.userlink = u'/%s%s/%s/' % (_('users/'), user_id, username) + self.userlink = reverse('users') + u'%s/%s/' % (user_id, username) self.username = username self.content = u'%s ...' % strip_tags(content)[:300] @@ -1715,86 +1817,110 @@ def user_favorites(request, user_id, user_view): }, context_instance=RequestContext(request)) -def user_preferences(request, user_id, user_view): +def user_email_subscriptions(request, user_id, user_view): user = get_object_or_404(User, id=user_id) + if request.method == 'POST': + form = EditUserEmailFeedsForm(request.POST) + if form.is_valid(): + if 'save' in request.POST: + saved = form.save(user) + if saved: + action_status = _('changes saved') + elif 'stop_email' in request.POST: + saved = form.reset().save(user) + initial_values = EditUserEmailFeedsForm.NO_EMAIL_INITIAL + form = EditUserEmailFeedsForm(initial=initial_values) + if saved: + action_status = _('email updates canceled') + if not saved: + action_status = None + else: + form = EditUserEmailFeedsForm() + form.set_initial_values(user) + action_status = None return render_to_response(user_view.template_file,{ - "tab_name" : user_view.id, - "tab_description" : user_view.tab_description, - "page_title" : user_view.page_title, - "view_user" : user, + 'tab_name':user_view.id, + 'tab_description':user_view.tab_description, + 'page_title':user_view.page_title, + 'view_user':user, + 'email_feeds_form':form, + 'action_status':action_status, }, context_instance=RequestContext(request)) def question_comments(request, id): question = get_object_or_404(Question, id=id) user = request.user - return __comments(request, question, 'question', user) + return __comments(request, question, 'question') def answer_comments(request, id): answer = get_object_or_404(Answer, id=id) user = request.user - return __comments(request, answer, 'answer', user) + return __comments(request, answer, 'answer') -def __comments(request, obj, type, user): +def __comments(request, obj, type): # only support get comments by ajax now + user = request.user if request.is_ajax(): if request.method == "GET": - return __generate_comments_json(obj, type, user) + response = __generate_comments_json(obj, type, user) elif request.method == "POST": - comment_data = request.POST.get('comment') - comment = Comment(content_object=obj, comment=comment_data, user=request.user) - comment.save() - obj.comment_count = obj.comment_count + 1 - obj.save() - return __generate_comments_json(obj, type, user) + if auth.can_add_comments(user,obj): + comment_data = request.POST.get('comment') + comment = Comment(content_object=obj, comment=comment_data, user=request.user) + comment.save() + obj.comment_count = obj.comment_count + 1 + obj.save() + response = __generate_comments_json(obj, type, user) + else: + response = HttpResponseForbidden(mimetype="application/json") + return response def __generate_comments_json(obj, type, user): - comments = obj.comments.all().order_by('-id') + comments = obj.comments.all().order_by('id') # {"Id":6,"PostId":38589,"CreationDate":"an hour ago","Text":"hello there!","UserDisplayName":"Jarrod Dixon","UserUrl":"/users/3/jarrod-dixon","DeleteUrl":null} json_comments = [] + from forum.templatetags.extra_tags import diff_date for comment in comments: comment_user = comment.user delete_url = "" if user != None and auth.can_delete_comment(user, comment): #/posts/392845/comments/219852/delete - delete_url = "/" + type + "s/%s/comments/%s/delete/" % (obj.id, comment.id) + #todo translate this url + delete_url = reverse(index) + type + "s/%s/comments/%s/delete/" % (obj.id, comment.id) json_comments.append({"id" : comment.id, "object_id" : obj.id, - "add_date" : comment.added_at.strftime('%Y-%m-%d'), + "comment_age" : diff_date(comment.added_at), "text" : comment.comment, "user_display_name" : comment_user.username, - "user_url" : "/%s%s/%s" % (_('users/'), comment_user.id, comment_user.username), + "user_url" : comment_user.get_profile_url(), "delete_url" : delete_url }) data = simplejson.dumps(json_comments) return HttpResponse(data, mimetype="application/json") -def delete_question_comment(request, question_id, comment_id): - if request.is_ajax(): - question = get_object_or_404(Question, id=question_id) - comment = get_object_or_404(Comment, id=comment_id) - - question.comments.remove(comment) - question.comment_count = question.comment_count - 1 - question.save() - user = request.user - return __generate_comments_json(question, 'question', user) +def delete_comment(request, object_id='', comment_id='', commented_object_type=None): + response = None + commented_object = None + if commented_object_type == 'question': + commented_object = Question + elif commented_object_type == 'answer': + commented_object = Answer -def delete_answer_comment(request, answer_id, comment_id): if request.is_ajax(): - answer = get_object_or_404(Answer, id=answer_id) comment = get_object_or_404(Comment, id=comment_id) - - answer.comments.remove(comment) - answer.comment_count = answer.comment_count - 1 - answer.save() - user = request.user - return __generate_comments_json(answer, 'answer', user) + if auth.can_delete_comment(request.user, comment): + obj = get_object_or_404(commented_object, id=object_id) + obj.comments.remove(comment) + obj.comment_count = obj.comment_count - 1 + obj.save() + user = request.user + return __generate_comments_json(obj, commented_object_type, user) + raise PermissionDenied() def logout(request): - url = request.GET.get('next') return render_to_response('logout.html', { - 'next' : url, + 'next' : get_next_url(request), }, context_instance=RequestContext(request)) def badges(request): @@ -1807,6 +1933,7 @@ def badges(request): return render_to_response('badges.html', { 'badges' : badges, 'mybadges' : my_badges, + 'feedback_faq_url' : reverse('feedback'), }, context_instance=RequestContext(request)) def badge(request, id): @@ -1882,7 +2009,7 @@ def upload(request): return HttpResponse(result, mimetype="application/xml") def books(request): - return HttpResponseRedirect("/books/mysql-zhaoyang") + return HttpResponseRedirect(reverse('books') + '/mysql-zhaoyang') def book(request, short_name, unanswered=False): """ @@ -2007,6 +2134,7 @@ def ask_book(request, short_name): return render_to_response('ask.html', { 'form' : form, 'tags' : tags, + 'email_validation_faq_url': reverse('faq') + '#validate', }, context_instance=RequestContext(request)) def search(request): @@ -2022,11 +2150,11 @@ def search(request): except ValueError: page = 1 if keywords is None: - return HttpResponseRedirect('/') + return HttpResponseRedirect(reverse(index)) if search_type == 'tag': - return HttpResponseRedirect('/%s?q=%s&page=%s' % (_('tags/'), keywords.strip(), page)) + return HttpResponseRedirect(reverse('tags') + '?q=%s&page=%s' % (keywords.strip(), page)) elif search_type == "user": - return HttpResponseRedirect('/%s?q=%s&page=%s' % (_('users/'), keywords.strip(), page)) + return HttpResponseRedirect(reverse('users') + '?q=%s&page=%s' % (keywords.strip(), page)) elif search_type == "question": template_file = "questions.html" diff --git a/junk.py b/junk.py new file mode 100644 index 00000000..c6c03d27 --- /dev/null +++ b/junk.py @@ -0,0 +1,3 @@ +import os + +print os.path.normpath('/haha//haha') diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo index cd8de560..502c1075 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 b5e5c549..18eb61b1 100644 --- a/locale/en/LC_MESSAGES/django.po +++ b/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-19 00:41+0000\n" +"POT-Creation-Date: 2009-11-10 19:00-0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -16,339 +16,315 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: settings.py:12 urls.py:25 forum/views.py:305 forum/views.py:699 -msgid "account/" -msgstr "" - -#: settings.py:12 urls.py:26 django_authopenid/urls.py:9 -#: django_authopenid/urls.py:10 django_authopenid/urls.py:11 -#: django_authopenid/urls.py:13 forum/views.py:305 forum/views.py:700 -#: templates/authopenid/confirm_email.txt:10 -msgid "signin/" -msgstr "" - -#: urls.py:22 -msgid "upfiles/" -msgstr "" - -#: urls.py:27 urls.py:28 urls.py:29 django_authopenid/urls.py:26 -#: django_authopenid/urls.py:27 -msgid "email/" -msgstr "" - -#: urls.py:27 -msgid "change/" -msgstr "" - -#: urls.py:28 -msgid "sendkey/" -msgstr "" - -#: urls.py:29 -msgid "verify/" -msgstr "" - -#: urls.py:30 -msgid "about/" -msgstr "" - -#: urls.py:31 -msgid "faq/" -msgstr "" +#: django_authopenid/forms.py:70 +msgid "choose a username" +msgstr "Choose screen name" -#: urls.py:32 -msgid "privacy/" +#: django_authopenid/forms.py:76 +msgid "user name is required" msgstr "" -#: urls.py:33 -msgid "logout/" +#: django_authopenid/forms.py:77 +msgid "sorry, this name is taken, please choose another" msgstr "" -#: urls.py:34 urls.py:35 urls.py:36 urls.py:48 forum/models.py:418 -msgid "answers/" +#: django_authopenid/forms.py:78 +msgid "sorry, this name is not allowed, please choose another" msgstr "" -#: urls.py:34 urls.py:46 -msgid "comments/" +#: django_authopenid/forms.py:79 +msgid "sorry, there is no user with this name" msgstr "" -#: urls.py:35 urls.py:40 urls.py:54 templates/user_info.html:34 -msgid "edit/" +#: django_authopenid/forms.py:80 +msgid "sorry, we have a serious error - user name is taken by several users" msgstr "" -#: urls.py:36 urls.py:45 -msgid "revisions/" +#: django_authopenid/forms.py:81 +msgid "user name can only consist of letters, empty space and underscore" msgstr "" -#: urls.py:37 urls.py:38 urls.py:39 urls.py:40 urls.py:41 urls.py:42 -#: urls.py:43 urls.py:44 urls.py:45 urls.py:46 urls.py:47 forum/feed.py:19 -#: forum/models.py:306 forum/views.py:1188 forum/views.py:1190 -#: forum/views.py:1430 -msgid "questions/" -msgstr "" +#: django_authopenid/forms.py:116 +msgid "your email address" +msgstr "Your email (never shared)" -#: urls.py:38 urls.py:64 -msgid "ask/" +#: django_authopenid/forms.py:117 +msgid "email address is required" msgstr "" -#: urls.py:39 -msgid "unanswered/" +#: django_authopenid/forms.py:118 +msgid "please enter a valid email address" msgstr "" -#: urls.py:41 -msgid "close/" +#: django_authopenid/forms.py:119 +msgid "this email is already used by someone else, please choose another" msgstr "" -#: urls.py:42 -msgid "reopen/" +#: django_authopenid/forms.py:163 django_authopenid/views.py:117 +msgid "i-names are not supported" msgstr "" -#: urls.py:43 -msgid "answer/" +#: django_authopenid/forms.py:239 +msgid "Please enter valid username and password (both are case-sensitive)." msgstr "" -#: urls.py:44 -msgid "vote/" +#: django_authopenid/forms.py:242 django_authopenid/forms.py:292 +msgid "This account is inactive." msgstr "" -#: urls.py:47 urls.py:48 django_authopenid/urls.py:29 -msgid "delete/" +#: django_authopenid/forms.py:244 +msgid "Login failed." msgstr "" -#: urls.py:50 -msgid "question/" +#: django_authopenid/forms.py:246 +msgid "Please enter username and password" msgstr "" -#: urls.py:51 urls.py:52 forum/views.py:741 forum/views.py:2027 -msgid "tags/" +#: django_authopenid/forms.py:248 +msgid "Please enter your password" msgstr "" -#: urls.py:53 urls.py:54 urls.py:55 forum/views.py:994 forum/views.py:998 -#: forum/views.py:1432 forum/views.py:1765 forum/views.py:2029 -msgid "users/" +#: django_authopenid/forms.py:250 +msgid "Please enter user name" msgstr "" -#: urls.py:56 urls.py:57 -msgid "badges/" +#: django_authopenid/forms.py:288 +msgid "" +"Please enter a valid username and password. Note that " +"both fields are case-sensitive." msgstr "" -#: urls.py:58 -msgid "messages/" -msgstr "" +#: django_authopenid/forms.py:310 +msgid "choose password" +msgstr "Password" -#: urls.py:58 -msgid "markread/" +#: django_authopenid/forms.py:311 +msgid "password is required" msgstr "" -#: urls.py:60 -msgid "nimda/" -msgstr "" +#: django_authopenid/forms.py:314 +msgid "retype password" +msgstr "Password (please retype)" -#: urls.py:62 -msgid "upload/" +#: django_authopenid/forms.py:315 +msgid "please, retype your password" msgstr "" -#: urls.py:63 urls.py:64 urls.py:65 -msgid "books/" +#: django_authopenid/forms.py:316 +msgid "sorry, entered passwords did not match, please try again" msgstr "" -#: urls.py:66 -msgid "search/" +#: django_authopenid/forms.py:341 +msgid "Current password" msgstr "" -#: django_authopenid/forms.py:67 django_authopenid/views.py:102 -msgid "i-names are not supported" +#: django_authopenid/forms.py:343 +msgid "New password" msgstr "" -#: django_authopenid/forms.py:102 -msgid "" -"Usernames can only contain letters, numbers and " -"underscores" +#: django_authopenid/forms.py:345 +msgid "Retype new password" msgstr "" -#: django_authopenid/forms.py:109 +#: django_authopenid/forms.py:356 msgid "" -"This username does not exist in our database. Please " -"choose another." +"Old password is incorrect. Please enter the correct " +"password." msgstr "" -#: django_authopenid/forms.py:126 django_authopenid/forms.py:233 -msgid "" -"Please enter a valid username and password. Note that " -"both fields are case-sensitive." +#: django_authopenid/forms.py:368 +msgid "new passwords do not match" msgstr "" -#: django_authopenid/forms.py:130 django_authopenid/forms.py:237 -msgid "This account is inactive." +#: django_authopenid/forms.py:432 +msgid "Your user name (required)" msgstr "" -#: django_authopenid/forms.py:158 django_authopenid/forms.py:210 -msgid "invalid user name" -msgstr "User names can contain letters, underscore and empty space." - -#: django_authopenid/forms.py:160 -msgid "sorry, this name can not be used, please try another" -msgstr "" +#: django_authopenid/forms.py:447 +msgid "Incorrect username." +msgstr "sorry, there is no such user name" -#: django_authopenid/forms.py:162 -msgid "username too short" +#: django_authopenid/urls.py:9 django_authopenid/urls.py:10 +#: django_authopenid/urls.py:11 django_authopenid/urls.py:13 forum/urls.py:24 +msgid "signin/" msgstr "" -#: django_authopenid/forms.py:170 django_authopenid/forms.py:171 -msgid "this name is already in use - please try anoter" +#: django_authopenid/urls.py:10 +msgid "newquestion/" msgstr "" -#: django_authopenid/forms.py:185 -msgid "" -"This email is already registered in our database. " -"Please choose another." +#: django_authopenid/urls.py:11 +msgid "newanswer/" msgstr "" -#: django_authopenid/forms.py:216 -msgid "" -"This username don't exist. Please choose another." +#: django_authopenid/urls.py:12 +msgid "signout/" msgstr "" -#: django_authopenid/forms.py:255 -msgid "choose a username" +#: django_authopenid/urls.py:13 +msgid "complete/" msgstr "" -#: django_authopenid/forms.py:257 templates/authopenid/signup.html:38 -msgid "your email address" -msgstr "" +#: django_authopenid/urls.py:15 +msgid "external-login/" +msgstr "using-nmr-wiki-login-and-password/" -#: django_authopenid/forms.py:259 templates/authopenid/signup.html:39 -msgid "choose password" +#: django_authopenid/urls.py:16 +msgid "register/" msgstr "" -#: django_authopenid/forms.py:261 templates/authopenid/signup.html:40 -msgid "retype password" +#: django_authopenid/urls.py:17 +msgid "signup/" msgstr "" -#: django_authopenid/forms.py:335 -msgid "" -"Old password is incorrect. Please enter the correct " -"password." +#: django_authopenid/urls.py:19 +msgid "sendpw/" msgstr "" -#: django_authopenid/forms.py:347 -msgid "new passwords do not match" +#: django_authopenid/urls.py:20 django_authopenid/urls.py:24 +msgid "password/" msgstr "" -#: django_authopenid/forms.py:442 -msgid "Incorrect username." +#: django_authopenid/urls.py:20 +msgid "confirm/" msgstr "" -#: django_authopenid/urls.py:10 forum/views.py:305 forum/views.py:700 -msgid "newquestion/" +#: django_authopenid/urls.py:23 +msgid "account_settings" msgstr "" -#: django_authopenid/urls.py:11 -msgid "newanswer/" +#: django_authopenid/urls.py:25 django_authopenid/urls.py:26 +#: django_authopenid/urls.py:27 django_authopenid/urls.py:28 +msgid "email/" msgstr "" -#: django_authopenid/urls.py:12 -msgid "signout/" +#: django_authopenid/urls.py:25 +msgid "validate/" msgstr "" -#: django_authopenid/urls.py:13 -msgid "complete/" +#: django_authopenid/urls.py:26 +msgid "change/" msgstr "" -#: django_authopenid/urls.py:15 -msgid "register/" +#: django_authopenid/urls.py:27 +msgid "sendkey/" msgstr "" -#: django_authopenid/urls.py:16 -msgid "signup/" +#: django_authopenid/urls.py:28 +msgid "verify/" msgstr "" -#: django_authopenid/urls.py:18 -msgid "sendpw/" +#: django_authopenid/urls.py:29 +msgid "openid/" msgstr "" -#: django_authopenid/urls.py:27 -msgid "validate/" +#: django_authopenid/urls.py:30 forum/urls.py:43 forum/urls.py:47 +msgid "delete/" msgstr "" -#: django_authopenid/views.py:108 +#: django_authopenid/views.py:123 #, python-format msgid "OpenID %(openid_url)s is invalid" msgstr "" -#: django_authopenid/views.py:418 django_authopenid/views.py:545 -msgid "Welcome" -msgstr "Verification Email from Q&A forum" +#: django_authopenid/views.py:531 +msgid "Welcome email subject line" +msgstr "Welcome to the Q&A forum" -#: django_authopenid/views.py:508 +#: django_authopenid/views.py:626 msgid "Password changed." msgstr "" -#: django_authopenid/views.py:520 django_authopenid/views.py:525 -msgid "your email needs to be validated" +#: django_authopenid/views.py:638 django_authopenid/views.py:644 +#, python-format +msgid "your email needs to be validated see %(details_url)s" msgstr "" "Your email needs to be validated. Please see details here." +"id='validate_email_alert' href='%(details_url)s'>here." + +#: django_authopenid/views.py:665 +msgid "Email verification subject line" +msgstr "Verification Email from Q&A forum" + +#: django_authopenid/views.py:751 +msgid "your email was not changed" +msgstr "" -#: django_authopenid/views.py:682 django_authopenid/views.py:834 +#: django_authopenid/views.py:798 django_authopenid/views.py:950 #, python-format msgid "No OpenID %s found associated in our database" msgstr "" -#: django_authopenid/views.py:686 django_authopenid/views.py:841 +#: django_authopenid/views.py:802 django_authopenid/views.py:957 #, python-format msgid "The OpenID %s isn't associated to current user logged in" msgstr "" -#: django_authopenid/views.py:694 +#: django_authopenid/views.py:810 msgid "Email Changed." msgstr "" -#: django_authopenid/views.py:769 +#: django_authopenid/views.py:885 msgid "This OpenID is already associated with another account." msgstr "" -#: django_authopenid/views.py:774 +#: django_authopenid/views.py:890 #, python-format msgid "OpenID %s is now associated with your account." msgstr "" -#: django_authopenid/views.py:844 +#: django_authopenid/views.py:960 msgid "Account deleted." msgstr "" -#: django_authopenid/views.py:884 +#: django_authopenid/views.py:1003 msgid "Request for new password" msgstr "" -#: django_authopenid/views.py:897 -msgid "A new password has been sent to your email address." +#: django_authopenid/views.py:1016 +msgid "A new password and the activation link were sent to your email address." msgstr "" -#: django_authopenid/views.py:927 +#: django_authopenid/views.py:1046 #, python-format msgid "" "Could not change password. Confirmation key '%s' is not " "registered." msgstr "" -#: django_authopenid/views.py:936 +#: django_authopenid/views.py:1055 msgid "" "Can not change password. User don't exist anymore in our " "database." msgstr "" -#: django_authopenid/views.py:945 +#: django_authopenid/views.py:1064 #, python-format msgid "Password changed for %s. You may now sign in." msgstr "" +#: forum/auth.py:484 +msgid "Your question and all of it's answers have been deleted" +msgstr "" + +#: forum/auth.py:486 +msgid "Your question has been deleted" +msgstr "" + +#: forum/auth.py:489 +msgid "The question and all of it's answers have been deleted" +msgstr "" + +#: forum/auth.py:491 +msgid "The question has been deleted" +msgstr "" + #: forum/const.py:8 msgid "duplicate question" msgstr "" #: forum/const.py:9 -msgid "question if off-topic or not relevant" +msgid "question is off-topic or not relevant" msgstr "" #: forum/const.py:10 @@ -467,151 +443,336 @@ msgstr "" msgid "latest questions" msgstr "" -#: forum/forms.py:14 templates/answer_edit_tips.html:33 -#: templates/answer_edit_tips.html.py:37 templates/question_edit_tips.html:31 -#: templates/question_edit_tips.html:36 +#: forum/feed.py:19 forum/urls.py:32 forum/urls.py:33 forum/urls.py:34 +#: forum/urls.py:35 forum/urls.py:36 forum/urls.py:37 forum/urls.py:38 +#: forum/urls.py:39 forum/urls.py:40 forum/urls.py:41 forum/urls.py:43 +msgid "questions/" +msgstr "" + +#: forum/forms.py:16 templates/answer_edit_tips.html:35 +#: templates/answer_edit_tips.html.py:39 templates/question_edit_tips.html:32 +#: templates/question_edit_tips.html:37 msgid "title" msgstr "" -#: forum/forms.py:15 +#: forum/forms.py:17 msgid "please enter a descriptive title for your question" msgstr "" -#: forum/forms.py:20 +#: forum/forms.py:22 msgid "title must be > 10 characters" msgstr "" -#: forum/forms.py:29 +#: forum/forms.py:31 msgid "content" msgstr "" -#: forum/forms.py:35 +#: forum/forms.py:37 msgid "question content must be > 10 characters" msgstr "" -#: forum/forms.py:45 templates/header.html:30 templates/header.html.py:64 +#: forum/forms.py:47 templates/header.html:29 templates/header.html.py:63 msgid "tags" msgstr "" -#: forum/forms.py:47 +#: forum/forms.py:49 msgid "" "Tags are short keywords, with no spaces within. Up to five tags can be used." msgstr "" -#: forum/forms.py:54 templates/question_retag.html:38 +#: forum/forms.py:56 templates/question_retag.html:39 msgid "tags are required" msgstr "" -#: forum/forms.py:58 +#: forum/forms.py:62 msgid "please use 5 tags or less" msgstr "" -#: forum/forms.py:61 +#: forum/forms.py:65 msgid "tags must be shorter than 20 characters" msgstr "" -#: forum/forms.py:65 +#: forum/forms.py:69 msgid "" "please use following characters in tags: letters 'a-z', numbers, and " "characters '.-_#'" msgstr "" -#: forum/forms.py:75 templates/index.html:57 templates/question.html:209 -#: templates/question.html.py:395 templates/questions.html:58 -#: templates/questions.html.py:70 templates/unanswered.html:48 -#: templates/unanswered.html.py:60 +#: forum/forms.py:79 templates/index.html:59 templates/index.html.py:71 +#: templates/post_contributor_info.html:7 +#: templates/question_summary_list_roll.html:26 +#: templates/question_summary_list_roll.html:38 templates/questions.html:59 +#: templates/questions.html.py:71 templates/unanswered.html:51 +#: templates/unanswered.html.py:63 msgid "community wiki" msgstr "" -#: forum/forms.py:76 +#: forum/forms.py:80 msgid "" "if you choose community wiki option, the question and answer do not generate " "points and name of author will not be shown" msgstr "" -#: forum/forms.py:89 +#: forum/forms.py:96 msgid "update summary:" msgstr "" -#: forum/forms.py:90 +#: forum/forms.py:97 msgid "" "enter a brief summary of your revision (e.g. fixed spelling, grammar, " "improved style, this field is optional)" msgstr "" -#: forum/forms.py:175 +#: forum/forms.py:100 +msgid "Automatically accept user's contributions for the email updates" +msgstr "" + +#: forum/forms.py:113 +msgid "Your name:" +msgstr "" + +#: forum/forms.py:114 +msgid "Email (not shared with anyone):" +msgstr "" + +#: forum/forms.py:115 +msgid "Your message:" +msgstr "" + +#: forum/forms.py:197 msgid "this email does not have to be linked to gravatar" msgstr "" -#: forum/forms.py:176 +#: forum/forms.py:198 msgid "Real name" msgstr "" -#: forum/forms.py:177 +#: forum/forms.py:199 msgid "Website" msgstr "" -#: forum/forms.py:178 +#: forum/forms.py:200 msgid "Location" msgstr "" -#: forum/forms.py:179 +#: forum/forms.py:201 msgid "Date of birth" msgstr "" -#: forum/forms.py:179 +#: forum/forms.py:201 msgid "will not be shown, used to calculate age, format: YYYY-MM-DD" msgstr "" -#: forum/forms.py:180 templates/authopenid/settings.html:21 +#: forum/forms.py:202 templates/authopenid/settings.html:21 msgid "Profile" msgstr "" -#: forum/forms.py:207 forum/forms.py:208 +#: forum/forms.py:229 forum/forms.py:230 msgid "this email has already been registered, please use another one" msgstr "" -#: forum/models.py:246 +#: forum/forms.py:234 forum/forms.py:235 +msgid "weekly" +msgstr "" + +#: forum/forms.py:234 forum/forms.py:235 +msgid "no email" +msgstr "" + +#: forum/forms.py:235 +msgid "daily" +msgstr "" + +#: forum/forms.py:250 forum/models.py:46 +msgid "Entire forum" +msgstr "" + +#: forum/forms.py:253 +msgid "Asked by me" +msgstr "" + +#: forum/forms.py:256 +msgid "Answered by me" +msgstr "" + +#: forum/forms.py:259 +msgid "Individually selected" +msgstr "" + +#: forum/models.py:47 +msgid "Questions that I asked" +msgstr "" + +#: forum/models.py:48 +msgid "Questions that I answered" +msgstr "" + +#: forum/models.py:49 +msgid "Individually selected questions" +msgstr "" + +#: forum/models.py:52 +msgid "Weekly" +msgstr "" + +#: forum/models.py:53 +msgid "Daily" +msgstr "" + +#: forum/models.py:54 +msgid "No email" +msgstr "" + +#: forum/models.py:289 #, python-format msgid "%(author)s modified the question" msgstr "" -#: forum/models.py:250 +#: forum/models.py:293 #, python-format msgid "%(people)s posted %(new_answer_count)s new answers" msgstr "" -#: forum/models.py:255 +#: forum/models.py:298 #, python-format msgid "%(people)s commented the question" msgstr "" -#: forum/models.py:260 +#: forum/models.py:303 #, python-format msgid "%(people)s commented answers" msgstr "" -#: forum/models.py:262 +#: forum/models.py:305 #, python-format msgid "%(people)s commented an answer" msgstr "" -#: forum/models.py:306 forum/models.py:418 -msgid "revisions" -msgstr "" - -#: forum/models.py:441 templates/badges.html:51 +#: forum/models.py:493 templates/badges.html:53 msgid "gold" msgstr "" -#: forum/models.py:442 templates/badges.html:59 +#: forum/models.py:494 templates/badges.html:61 msgid "silver" msgstr "" -#: forum/models.py:443 templates/badges.html:66 +#: forum/models.py:495 templates/badges.html:68 msgid "bronze" msgstr "" +#: forum/urls.py:21 +msgid "upfiles/" +msgstr "" + +#: forum/urls.py:25 +msgid "about/" +msgstr "" + +#: forum/urls.py:26 +msgid "faq/" +msgstr "" + +#: forum/urls.py:27 +msgid "privacy/" +msgstr "" + +#: forum/urls.py:28 +msgid "logout/" +msgstr "" + +#: forum/urls.py:29 forum/urls.py:30 forum/urls.py:31 forum/urls.py:47 +msgid "answers/" +msgstr "" + +#: forum/urls.py:29 forum/urls.py:41 forum/urls.py:43 forum/urls.py:47 +msgid "comments/" +msgstr "" + +#: forum/urls.py:30 forum/urls.py:35 forum/urls.py:56 +#: templates/user_info.html:45 +msgid "edit/" +msgstr "" + +#: forum/urls.py:31 forum/urls.py:40 +msgid "revisions/" +msgstr "" + +#: forum/urls.py:33 forum/urls.py:66 +msgid "ask/" +msgstr "" + +#: forum/urls.py:34 +msgid "unanswered/" +msgstr "" + +#: forum/urls.py:36 +msgid "close/" +msgstr "" + +#: forum/urls.py:37 +msgid "reopen/" +msgstr "" + +#: forum/urls.py:38 +msgid "answer/" +msgstr "" + +#: forum/urls.py:39 +msgid "vote/" +msgstr "" + +#: forum/urls.py:51 +msgid "question/" +msgstr "" + +#: forum/urls.py:52 forum/urls.py:53 +msgid "tags/" +msgstr "" + +#: forum/urls.py:54 forum/urls.py:56 forum/urls.py:57 +msgid "users/" +msgstr "" + +#: forum/urls.py:55 +msgid "moderate-user/" +msgstr "" + +#: forum/urls.py:58 forum/urls.py:59 +msgid "badges/" +msgstr "" + +#: forum/urls.py:60 +msgid "messages/" +msgstr "" + +#: forum/urls.py:60 +msgid "markread/" +msgstr "" + +#: forum/urls.py:62 +msgid "nimda/" +msgstr "" + +#: forum/urls.py:64 +msgid "upload/" +msgstr "" + +#: forum/urls.py:65 forum/urls.py:66 forum/urls.py:67 +msgid "books/" +msgstr "" + +#: forum/urls.py:68 +msgid "search/" +msgstr "" + +#: forum/urls.py:69 +msgid "feedback/" +msgstr "" + +#: forum/urls.py:70 +msgid "account/" +msgstr "" + #: forum/user.py:16 templates/user_tabs.html:7 msgid "overview" msgstr "" @@ -648,17 +809,17 @@ msgstr "" msgid "profile - responses" msgstr "" -#: forum/user.py:42 templates/user_info.html:23 templates/users.html:26 +#: forum/user.py:42 templates/user_info.html:22 templates/users.html:26 msgid "reputation" -msgstr "" +msgstr "karma" #: forum/user.py:43 msgid "user reputation in the community" -msgstr "" +msgstr "user karma" #: forum/user.py:44 msgid "profile - user reputation" -msgstr "" +msgstr "Profile - User's Karma" #: forum/user.py:50 msgid "favorite questions" @@ -684,61 +845,130 @@ msgstr "" msgid "profile - votes" msgstr "" -#: forum/user.py:68 -msgid "preferences" +#: forum/user.py:68 templates/user_tabs.html:28 +msgid "email subscriptions" msgstr "" #: forum/user.py:69 templates/user_tabs.html:27 -msgid "user preference settings" +msgid "email subscription settings" msgstr "" #: forum/user.py:70 -msgid "profile - user preferences" +msgid "profile - email subscriptions" msgstr "" -#: forum/views.py:948 +#: forum/views.py:126 +msgid "Q&A forum feedback" +msgstr "" + +#: forum/views.py:127 +msgid "Thanks for the feedback!" +msgstr "" + +#: forum/views.py:135 +msgid "We look forward to hearing your feedback! Please, give it next time :)" +msgstr "" + +#: forum/views.py:1014 #, python-format -msgid "subscription saved, %(email)s needs validation" +msgid "subscription saved, %(email)s needs validation, see %(details_url)s" msgstr "" "Your subscription is saved, but email address %(email)s needs to be " -"validated, please see more details here" +"validated, please see more details here" -#: forum/views.py:1874 -msgid "uploading images is limited to users with >60 reputation points" +#: forum/views.py:1022 +msgid "email update frequency has been set to daily" msgstr "" -#: forum/views.py:1876 +#: forum/views.py:1826 +msgid "changes saved" +msgstr "" + +#: forum/views.py:1832 +msgid "email updates canceled" +msgstr "" + +#: forum/views.py:1999 +msgid "uploading images is limited to users with >60 reputation points" +msgstr "sorry, file uploading requires karma >60" + +#: forum/views.py:2001 msgid "allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'" msgstr "" -#: forum/views.py:1878 +#: forum/views.py:2003 #, python-format msgid "maximum upload file size is %sK" msgstr "" -#: forum/views.py:1880 +#: forum/views.py:2005 #, python-format msgid "" "Error uploading file. Please contact the site administrator. Thank you. %s" msgstr "" -#: forum/management/commands/send_email_alerts.py:35 -msgid "updates from website" -msgstr "Q&A forum update" +#: forum/management/commands/send_email_alerts.py:71 +msgid "email update message subject" +msgstr "news from Q&A forum" -#: forum/templatetags/extra_tags.py:143 forum/templatetags/extra_tags.py:172 -#: templates/header.html:35 +#: forum/management/commands/send_email_alerts.py:72 +#, python-format +msgid "%(name)s, this is an update message header for a question" +msgid_plural "%(name)s, this is an update message header for %(num)d questions" +msgstr[0] "

Dear %(name)s,

The following question has been updated on the Q&A forum:

" +msgstr[1] "

Dear %(name)s,

The following %(num)d questions have been updated on the Q&A forum:

" + +#: forum/management/commands/send_email_alerts.py:81 +#, python-format +msgid "" +"go to %(link)s to change frequency of email updates or %(email)s " +"administrator" +msgstr "" +"

Please remember that you can always adjust frequency of the " +"email updates or turn them off entirely.
If you believe that this message " +"was sent in an error, please email about it the forum administrator at %(email)" +"s.

" +"

Sincerely,

Your friendly Q&A forum server.

" + +#: forum/templatetags/extra_tags.py:160 forum/templatetags/extra_tags.py:189 +#: templates/header.html:34 msgid "badges" msgstr "" -#: forum/templatetags/extra_tags.py:144 forum/templatetags/extra_tags.py:171 +#: forum/templatetags/extra_tags.py:161 forum/templatetags/extra_tags.py:188 msgid "reputation points" +msgstr "karma" + +#: forum/templatetags/extra_tags.py:244 +msgid "%b %d at %H:%M" +msgstr "" + +#: forum/templatetags/extra_tags.py:246 +msgid "%b %d '%y at %H:%M" +msgstr "" + +#: forum/templatetags/extra_tags.py:248 +msgid "2 days ago" msgstr "" -#: forum/templatetags/extra_tags.py:225 -msgid " ago" +#: forum/templatetags/extra_tags.py:250 +msgid "yesterday" msgstr "" +#: forum/templatetags/extra_tags.py:252 +#, python-format +msgid "%(hr)d hour ago" +msgid_plural "%(hr)d hours ago" +msgstr[0] "" +msgstr[1] "" + +#: forum/templatetags/extra_tags.py:254 +#, python-format +msgid "%(min)d min ago" +msgid_plural "%(min)d mins ago" +msgstr[0] "" +msgstr[1] "" + #: templates/404.html:24 msgid "Sorry, could not find the page you requested." msgstr "" @@ -805,55 +1035,55 @@ msgstr "" msgid "About" msgstr "" -#: templates/answer_edit.html:4 templates/answer_edit.html.py:47 +#: templates/answer_edit.html:5 templates/answer_edit.html.py:48 msgid "Edit answer" msgstr "" -#: templates/answer_edit.html:24 templates/answer_edit.html.py:27 -#: templates/ask.html:25 templates/ask.html.py:28 templates/question.html:43 -#: templates/question.html.py:46 templates/question_edit.html:27 +#: templates/answer_edit.html:25 templates/answer_edit.html.py:28 +#: templates/ask.html:26 templates/ask.html.py:29 templates/question.html:45 +#: templates/question.html.py:48 templates/question_edit.html:28 msgid "hide preview" msgstr "" -#: templates/answer_edit.html:27 templates/ask.html:28 -#: templates/question.html:46 templates/question_edit.html:27 +#: templates/answer_edit.html:28 templates/ask.html:29 +#: templates/question.html:48 templates/question_edit.html:28 msgid "show preview" msgstr "" -#: templates/answer_edit.html:47 templates/question_edit.html:65 -#: templates/question_retag.html:52 templates/revisions_answer.html:36 +#: templates/answer_edit.html:48 templates/question_edit.html:66 +#: templates/question_retag.html:53 templates/revisions_answer.html:36 #: templates/revisions_question.html:36 msgid "back" msgstr "" -#: templates/answer_edit.html:52 templates/question_edit.html:70 -#: templates/revisions_answer.html:47 templates/revisions_question.html:47 +#: templates/answer_edit.html:53 templates/question_edit.html:71 +#: templates/revisions_answer.html:50 templates/revisions_question.html:50 msgid "revision" msgstr "" -#: templates/answer_edit.html:55 templates/question_edit.html:74 +#: templates/answer_edit.html:56 templates/question_edit.html:75 msgid "select revision" msgstr "" -#: templates/answer_edit.html:62 templates/ask.html:94 -#: templates/question.html:467 templates/question_edit.html:91 +#: templates/answer_edit.html:63 templates/ask.html:97 +#: templates/question.html:434 templates/question_edit.html:92 msgid "Toggle the real time Markdown editor preview" msgstr "" -#: templates/answer_edit.html:62 templates/ask.html:94 -#: templates/question.html:467 templates/question_edit.html:91 +#: templates/answer_edit.html:63 templates/ask.html:97 +#: templates/question.html:435 templates/question_edit.html:92 msgid "toggle preview" msgstr "" -#: templates/answer_edit.html:71 templates/question_edit.html:115 -#: templates/question_retag.html:73 +#: templates/answer_edit.html:72 templates/question_edit.html:118 +#: templates/question_retag.html:74 msgid "Save edit" msgstr "" -#: templates/answer_edit.html:72 templates/close.html:29 -#: templates/question_edit.html:116 templates/question_retag.html:74 -#: templates/reopen.html:30 templates/user_edit.html:83 -#: templates/authopenid/changeemail.html:34 +#: templates/answer_edit.html:73 templates/close.html:29 +#: templates/feedback.html:50 templates/question_edit.html:119 +#: templates/question_retag.html:75 templates/reopen.html:30 +#: templates/user_edit.html:83 templates/authopenid/changeemail.html:40 msgid "Cancel" msgstr "" @@ -877,80 +1107,82 @@ msgstr "" msgid "be clear and concise" msgstr "" -#: templates/answer_edit_tips.html:19 templates/question_edit_tips.html:16 +#: templates/answer_edit_tips.html:20 templates/question_edit_tips.html:17 msgid "see frequently asked questions" msgstr "" -#: templates/answer_edit_tips.html:24 templates/question_edit_tips.html:22 +#: templates/answer_edit_tips.html:26 templates/question_edit_tips.html:23 msgid "Markdown tips" msgstr "Markdown basics" -#: templates/answer_edit_tips.html:27 templates/question_edit_tips.html:25 +#: templates/answer_edit_tips.html:29 templates/question_edit_tips.html:26 msgid "*italic* or __italic__" msgstr "" -#: templates/answer_edit_tips.html:30 templates/question_edit_tips.html:28 +#: templates/answer_edit_tips.html:32 templates/question_edit_tips.html:29 msgid "**bold** or __bold__" msgstr "" -#: templates/answer_edit_tips.html:33 templates/question_edit_tips.html:31 +#: templates/answer_edit_tips.html:35 templates/question_edit_tips.html:32 msgid "link" msgstr "" -#: templates/answer_edit_tips.html:33 templates/answer_edit_tips.html.py:37 -#: templates/question_edit_tips.html:31 templates/question_edit_tips.html:36 +#: templates/answer_edit_tips.html:35 templates/answer_edit_tips.html.py:39 +#: templates/question_edit_tips.html:32 templates/question_edit_tips.html:37 msgid "text" msgstr "" -#: templates/answer_edit_tips.html:37 templates/question_edit_tips.html:36 +#: templates/answer_edit_tips.html:39 templates/question_edit_tips.html:37 msgid "image" msgstr "" -#: templates/answer_edit_tips.html:41 templates/question_edit_tips.html:40 +#: templates/answer_edit_tips.html:43 templates/question_edit_tips.html:41 msgid "numbered list:" msgstr "" -#: templates/answer_edit_tips.html:46 templates/question_edit_tips.html:45 +#: templates/answer_edit_tips.html:48 templates/question_edit_tips.html:46 msgid "basic HTML tags are also supported" msgstr "" -#: templates/answer_edit_tips.html:49 templates/question_edit_tips.html:48 +#: templates/answer_edit_tips.html:52 templates/question_edit_tips.html:50 msgid "learn more about Markdown" msgstr "" -#: templates/ask.html:4 templates/ask.html.py:60 +#: templates/ask.html:5 templates/ask.html.py:61 msgid "Ask a question" msgstr "" -#: templates/ask.html:67 +#: templates/ask.html:68 msgid "login to post question info" msgstr "" "You are welcome to start submitting your question " -"anonymously - you are currently not logged in. When you post your " -"question, you will be redirected to the login/signup page. Your question " -"will be saved meanwhile and will be posted when you log in. Login/signup " -"process is very simple. Login takes about 30 seconds, initial signup takes a " -"minute or less." +"anonymously. When you submit the post, you will be redirected to the " +"login/signup page. Your question will be saved in the current session and " +"will be published after you log in. Login/signup process is very simple. " +"Login takes about 30 seconds, initial signup takes a minute or less." -#: templates/ask.html:73 +#: templates/ask.html:74 #, python-format -msgid "must have valid %(email)s to post" +msgid "" +"must have valid %(email)s to post, \n" +" see %(email_validation_faq_url)s\n" +" " msgstr "" "Looks like your email address, %(email)s has not " "yet been validated. To post messages you must verify your email, " -"please see more details here.
You can submit " -"your question now and validate email after that. Your question will saved as " -"pending meanwhile. " +"please see more details here." +"
You can submit your question now and validate email after that. Your " +"question will saved as pending meanwhile. " -#: templates/ask.html:107 +#: templates/ask.html:112 msgid "(required)" msgstr "" -#: templates/ask.html:114 +#: templates/ask.html:119 msgid "Login/signup to post your question" msgstr "Login/Signup to Post" -#: templates/ask.html:116 +#: templates/ask.html:121 msgid "Ask your question" msgstr "Ask Your Question" @@ -966,7 +1198,7 @@ msgstr "" msgid "Badges summary" msgstr "" -#: templates/badges.html:17 templates/user_stats.html:73 +#: templates/badges.html:17 msgid "Badges" msgstr "" @@ -977,38 +1209,41 @@ msgstr "" "Q&A community will be recognized with the variety of badges." #: templates/badges.html:22 +#, python-format msgid "" -"Below is the list of available badges and number of times each type of badge " -"has been awarded." +"Below is the list of available badges and number \n" +" of times each type of badge has been awarded. Give us feedback at %" +"(feedback_faq_url)s.\n" +" " msgstr "" "Currently badges differ only by their level: gold, " "silver and bronze (their meanings are " "described on the right). In the future there will be many types of badges at " -"each level. Please give us your feedback - what kinds of badges would you like to see and suggest the " -"activity for which those badges might be awarded." +"each level. Please give us your feedback - what kinds of badges would you like to see and " +"suggest the activity for which those badges might be awarded." -#: templates/badges.html:48 +#: templates/badges.html:50 msgid "Community badges" msgstr "Badge levels" -#: templates/badges.html:54 +#: templates/badges.html:56 msgid "gold badge description" msgstr "" "Gold badge is the highest award in this community. To obtain it have to show " "profound knowledge and ability in addition to your active participation." -#: templates/badges.html:62 +#: templates/badges.html:64 msgid "silver badge description" msgstr "" "Obtaining silver badge requires significant patience. If you have received " "one, that means you have greatly contributed to this community." -#: templates/badges.html:65 +#: templates/badges.html:67 msgid "bronze badge: often given as a special honor" msgstr "" -#: templates/badges.html:69 +#: templates/badges.html:71 msgid "bronze badge description" msgstr "" "If you are an active participant in this community, you will be recognized " @@ -1080,8 +1315,9 @@ msgstr "" msgid "number of times" msgstr "" -#: templates/book.html:105 templates/index.html:48 templates/questions.html:46 -#: templates/unanswered.html:37 templates/users_questions.html:32 +#: templates/book.html:105 templates/index.html:47 +#: templates/question_summary_list_roll.html:14 templates/questions.html:47 +#: templates/unanswered.html:39 templates/users_questions.html:32 msgid "votes" msgstr "" @@ -1089,15 +1325,17 @@ msgstr "" msgid "the answer has been accepted to be correct" msgstr "" -#: templates/book.html:115 templates/index.html:49 templates/questions.html:47 -#: templates/unanswered.html:38 templates/users_questions.html:42 +#: templates/book.html:115 templates/index.html:48 +#: templates/question_summary_list_roll.html:15 templates/questions.html:48 +#: templates/unanswered.html:40 templates/users_questions.html:40 msgid "views" msgstr "" -#: templates/book.html:125 templates/index.html:69 templates/question.html:499 -#: templates/questions.html:84 templates/questions.html.py:156 -#: templates/tags.html:49 templates/unanswered.html:75 -#: templates/unanswered.html.py:106 templates/users_questions.html:54 +#: templates/book.html:125 templates/index.html:103 +#: templates/question.html:480 templates/question_summary_list_roll.html:52 +#: templates/questions.html:103 templates/questions.html.py:171 +#: templates/tags.html:49 templates/unanswered.html:95 +#: templates/unanswered.html.py:122 templates/users_questions.html:52 msgid "using tags" msgstr "" @@ -1105,7 +1343,7 @@ msgstr "" msgid "subscribe to book RSS feed" msgstr "" -#: templates/book.html:147 templates/index.html:118 +#: templates/book.html:147 templates/index.html:152 msgid "subscribe to the questions feed" msgstr "" @@ -1189,16 +1427,18 @@ msgid "" "The reputation system allows users earn the authorization to perform a " "variety of moderation tasks." msgstr "" +"Karma system allows users to earn rights to perform a variety of moderation " +"tasks" #: templates/faq.html:40 msgid "How does reputation system work?" -msgstr "" +msgstr "How does karma system work?" #: templates/faq.html:41 msgid "Rep system summary" msgstr "" "When a question or answer is upvoted, the user who posted them will gain " -"some points, which are called \"reputation points\". These points serve as a " +"some points, which are called \"karma points\". These points serve as a " "rough measure of the community trust to him/her. Various moderation tasks " "are gradually assigned to the users based on those points." @@ -1237,51 +1477,53 @@ msgstr "" msgid "retag questions" msgstr "" -#: templates/faq.html:77 +#: templates/faq.html:78 msgid "edit community wiki questions" msgstr "" -#: templates/faq.html:81 +#: templates/faq.html:83 msgid "edit any answer" msgstr "" -#: templates/faq.html:85 +#: templates/faq.html:87 msgid "open any closed question" msgstr "" -#: templates/faq.html:89 +#: templates/faq.html:91 msgid "delete any comment" msgstr "" -#: templates/faq.html:93 +#: templates/faq.html:95 msgid "delete any questions and answers and perform other moderation tasks" msgstr "" -#: templates/faq.html:100 +#: templates/faq.html:102 msgid "how to validate email title" msgstr "How to validate email and why?" -#: templates/faq.html:102 -msgid "how to validate email info" -msgstr "" -"

How? If you have just set or changed your email " -"address - check your email and click the included link." -"
The link contains a key generated specifically for you. You can also " -" and check your email again.

Why? Email validation is required to make sure that " +"only you can post messages on your behalf and to " +"minimize spam posts.
With email you can " +"subscribe for updates on the most interesting questions. " +"Also, when you sign up for the first time - create a unique gravatar personal image.

" + +#: templates/faq.html:108 msgid "what is gravatar" msgstr "What is gravatar?" -#: templates/faq.html:107 +#: templates/faq.html:109 msgid "gravatar faq info" msgstr "" "Gravatar means globally r at gravatar.com" -#: templates/faq.html:110 +#: templates/faq.html:112 msgid "To register, do I need to create new password?" msgstr "" -#: templates/faq.html:111 +#: templates/faq.html:113 msgid "" "No, you don't have to. You can login through any service that supports " "OpenID, e.g. Google, Yahoo, AOL, etc." msgstr "" -#: templates/faq.html:112 +#: templates/faq.html:114 msgid "Login now!" msgstr "" -#: templates/faq.html:117 +#: templates/faq.html:119 msgid "Why other people can edit my questions/answers?" msgstr "" -#: templates/faq.html:118 +#: templates/faq.html:120 msgid "Goal of this site is..." msgstr "" -#: templates/faq.html:118 +#: templates/faq.html:120 msgid "" "So questions and answers can be edited like wiki pages by experienced users " "of this site and this improves the overall quality of the knowledge base " "content." msgstr "" -#: templates/faq.html:119 +#: templates/faq.html:121 msgid "If this approach is not for you, we respect your choice." msgstr "" -#: templates/faq.html:123 +#: templates/faq.html:125 msgid "Still have questions?" msgstr "" -#: templates/faq.html:124 -msgid "Please ask your question, help make our community better!" +#: templates/faq.html:126 +#, python-format +msgid "" +"Please ask your question at %(ask_question_url)s, help make our community " +"better!" msgstr "" -"Please ask your question, help make our " +"Please ask your question, help make our " "community better!" -#: templates/faq.html:126 templates/header.html:29 templates/header.html.py:63 +#: templates/faq.html:128 templates/header.html:28 templates/header.html.py:62 msgid "questions" msgstr "" -#: templates/faq.html:126 templates/index.html:123 +#: templates/faq.html:128 templates/index.html:157 msgid "." msgstr "" -#: templates/footer.html:8 templates/header.html:14 templates/index.html:83 +#: templates/feedback.html:6 +msgid "Feedback" +msgstr "" + +#: templates/feedback.html:11 +msgid "Give us your feedback!" +msgstr "" + +#: templates/feedback.html:17 +#, python-format +msgid "" +"\n" +" Dear %(user_name)s, we look " +"forward to hearing your feedback. \n" +" Please type and send us your message below.\n" +" " +msgstr "" + +#: templates/feedback.html:24 +msgid "" +"\n" +" Dear visitor, we look forward to " +"hearing your feedback.\n" +" Please type and send us your message below.\n" +" " +msgstr "" + +#: templates/feedback.html:41 +msgid "(this field is required)" +msgstr "" + +#: templates/feedback.html:49 +msgid "Send Feedback" +msgstr "" + +#: templates/feedback_email.txt:3 +#, python-format +msgid "" +"\n" +"Hello, this is a %(site_title)s forum feedback message\n" +msgstr "" + +#: templates/feedback_email.txt:9 +msgid "Sender is" +msgstr "" + +#: templates/feedback_email.txt:11 templates/feedback_email.txt.py:14 +msgid "email" +msgstr "" + +#: templates/feedback_email.txt:13 +msgid "anonymous" +msgstr "" + +#: templates/feedback_email.txt:19 +msgid "Message body:" +msgstr "" + +#: templates/footer.html:8 templates/header.html:14 templates/index.html:117 msgid "about" msgstr "" -#: templates/footer.html:9 templates/header.html:15 templates/index.html:84 -#: templates/question_edit_tips.html:16 +#: templates/footer.html:9 templates/header.html:15 templates/index.html:118 +#: templates/question_edit_tips.html:17 msgid "faq" msgstr "" @@ -1368,162 +1671,176 @@ msgstr "" msgid "privacy policy" msgstr "" -#: templates/footer.html:14 +#: templates/footer.html:22 msgid "give feedback" msgstr "" -#: templates/header.html:10 +#: templates/header.html:9 msgid "logout" msgstr "" -#: templates/header.html:12 templates/authopenid/signup.html:41 +#: templates/header.html:11 msgid "login" msgstr "" -#: templates/header.html:23 +#: templates/header.html:22 msgid "back to home page" msgstr "" -#: templates/header.html:31 templates/header.html.py:65 +#: templates/header.html:30 templates/header.html.py:64 msgid "users" msgstr "" -#: templates/header.html:33 +#: templates/header.html:32 msgid "books" msgstr "" -#: templates/header.html:36 +#: templates/header.html:35 msgid "unanswered questions" msgstr "unanswered" -#: templates/header.html:40 +#: templates/header.html:39 msgid "my profile" msgstr "" -#: templates/header.html:44 +#: templates/header.html:43 msgid "ask a question" msgstr "" -#: templates/header.html:59 +#: templates/header.html:58 msgid "search" msgstr "" -#: templates/index.html:7 +#: templates/index.html:8 msgid "Home" msgstr "" -#: templates/index.html:22 templates/questions.html:7 +#: templates/index.html:23 templates/questions.html:8 msgid "Questions" msgstr "" -#: templates/index.html:24 +#: templates/index.html:25 msgid "last updated questions" msgstr "" -#: templates/index.html:24 templates/questions.html:25 -#: templates/unanswered.html:20 +#: templates/index.html:25 templates/questions.html:26 +#: templates/unanswered.html:21 msgid "newest" msgstr "" -#: templates/index.html:25 templates/questions.html:27 +#: templates/index.html:26 templates/questions.html:28 msgid "hottest questions" msgstr "" -#: templates/index.html:25 templates/questions.html:27 +#: templates/index.html:26 templates/questions.html:28 msgid "hottest" msgstr "" -#: templates/index.html:26 templates/questions.html:28 +#: templates/index.html:27 templates/questions.html:29 msgid "most voted questions" msgstr "" -#: templates/index.html:26 templates/questions.html:28 +#: templates/index.html:27 templates/questions.html:29 msgid "most voted" msgstr "" -#: templates/index.html:27 +#: templates/index.html:28 msgid "all questions" msgstr "" -#: templates/index.html:47 templates/questions.html:45 -#: templates/unanswered.html:36 templates/users_questions.html:35 +#: templates/index.html:46 templates/question_summary_list_roll.html:13 +#: templates/questions.html:46 templates/unanswered.html:38 +#: templates/users_questions.html:36 msgid "answers" msgstr "" -#: templates/index.html:69 templates/question.html:499 -#: templates/questions.html:84 templates/questions.html.py:156 -#: templates/tags.html:49 templates/unanswered.html:75 -#: templates/unanswered.html.py:106 templates/users_questions.html:52 +#: templates/index.html:78 templates/index.html.py:92 +#: templates/questions.html:78 templates/questions.html.py:92 +#: templates/unanswered.html:70 templates/unanswered.html.py:84 +msgid "Posted:" +msgstr "" + +#: templates/index.html:81 templates/index.html.py:86 +#: templates/questions.html:81 templates/questions.html.py:86 +#: templates/unanswered.html:73 templates/unanswered.html.py:78 +msgid "Updated:" +msgstr "" + +#: templates/index.html:103 templates/question.html:480 +#: templates/question_summary_list_roll.html:52 templates/questions.html:103 +#: templates/questions.html.py:171 templates/tags.html:49 +#: templates/unanswered.html:95 templates/unanswered.html.py:122 +#: templates/users_questions.html:52 msgid "see questions tagged" msgstr "" -#: templates/index.html:80 +#: templates/index.html:114 msgid "welcome to website" -msgstr "" +msgstr "Welcome to Q&A forum" -#: templates/index.html:89 +#: templates/index.html:123 msgid "Recent tags" msgstr "" -#: templates/index.html:94 templates/question.html:125 +#: templates/index.html:128 templates/question.html:135 #, python-format msgid "see questions tagged '%(tagname)s'" msgstr "" -#: templates/index.html:97 templates/index.html.py:123 +#: templates/index.html:131 templates/index.html.py:157 msgid "popular tags" msgstr "tags" -#: templates/index.html:102 +#: templates/index.html:136 msgid "Recent awards" msgstr "Recent badges" -#: templates/index.html:108 +#: templates/index.html:142 msgid "given to" msgstr "" -#: templates/index.html:113 +#: templates/index.html:147 msgid "all awards" msgstr "all badges" -#: templates/index.html:118 +#: templates/index.html:152 msgid "subscribe to last 30 questions by RSS" msgstr "" -#: templates/index.html:123 +#: templates/index.html:157 msgid "Still looking for more? See" msgstr "" -#: templates/index.html:123 +#: templates/index.html:157 msgid "complete list of questions" msgstr "list of all questions" -#: templates/index.html:123 +#: templates/index.html:157 templates/authopenid/signup.html:18 msgid "or" msgstr "" -#: templates/index.html:123 +#: templates/index.html:157 msgid "Please help us answer" msgstr "" -#: templates/index.html:123 +#: templates/index.html:157 msgid "list of unanswered questions" msgstr "unanswered questions" -#: templates/logout.html:6 templates/logout.html.py:17 +#: templates/logout.html:6 templates/logout.html.py:16 msgid "Logout" msgstr "" -#: templates/logout.html:20 +#: templates/logout.html:19 msgid "" "As a registered user you can login with your OpenID, log out of the site or " "permanently remove your account." msgstr "" -"Clicking Logout will log you out from the Q&A forum.

If you wish to sign off completely - please make sure to log out from " -"your OpenID provider." +"Clicking Logout will log you out from the forum" +"but will not sign you off from your OpenID provider.

If you wish to sign off " +"completely - please make sure to log out from your OpenID provider as well." -#: templates/logout.html:21 +#: templates/logout.html:20 msgid "Logout now" msgstr "Logout Now" @@ -1551,6 +1868,33 @@ msgstr "" msgid "next page" msgstr "" +#: templates/post_contributor_info.html:9 +#, python-format +msgid "" +"\n" +" one revision\n" +" " +msgid_plural "" +"\n" +" %(rev_count)s revisions\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/post_contributor_info.html:19 templates/revisions_answer.html:66 +#: templates/revisions_question.html:66 +msgid "asked" +msgstr "" + +#: templates/post_contributor_info.html:21 +msgid "answered" +msgstr "" + +#: templates/post_contributor_info.html:37 templates/revisions_answer.html:68 +#: templates/revisions_question.html:68 +msgid "updated" +msgstr "" + #: templates/privacy.html:6 templates/privacy.html.py:11 msgid "Privacy policy" msgstr "" @@ -1558,6 +1902,10 @@ msgstr "" #: templates/privacy.html:15 msgid "general message about privacy" msgstr "" +"Respecting users privacy is an important core principle " +"of this Q&A forum. " +"Information on this page details how this forum protects your privacy, " +"and what type of information is collected." #: templates/privacy.html:18 msgid "Site Visitors" @@ -1566,6 +1914,9 @@ msgstr "" #: templates/privacy.html:20 msgid "what technical information is collected about visitors" msgstr "" +"Information on question views, revisions of questions and answers - both times " +"and content are recorded for each user in order to correctly count number of views, " +"maintain data integrity and report relevant updates." #: templates/privacy.html:23 msgid "Personal Information" @@ -1574,6 +1925,9 @@ msgstr "" #: templates/privacy.html:25 msgid "details on personal information policies" msgstr "" +"Members of this community may choose to display personally identifiable information " +"in their profiles. Forum will never display such information " +"without a request from the user." #: templates/privacy.html:28 msgid "Other Services" @@ -1582,10 +1936,15 @@ msgstr "" #: templates/privacy.html:30 msgid "details on sharing data with third parties" msgstr "" +"None of the data that is not openly shown on the forum by the choice of the user " +"is shared with any third party." #: templates/privacy.html:35 msgid "cookie policy details" msgstr "" +"Forum software relies on the internet cookie technology to " +"keep track of user sessions. Cookies must be enabled " +"in your browser so that forum can work for you." #: templates/privacy.html:37 msgid "Policy Changes" @@ -1594,194 +1953,305 @@ msgstr "" #: templates/privacy.html:38 msgid "how privacy policies can be changed" msgstr "" +"These policies may be adjusted to improve protection of " +"user's privacy. Whenever such changes occur, users will be " +"notified via the internal messaging system. " -#: templates/question.html:72 templates/question.html.py:73 -#: templates/question.html:85 templates/question.html.py:87 +#: templates/question.html:77 templates/question.html.py:78 +#: templates/question.html:94 templates/question.html.py:96 msgid "i like this post (click again to cancel)" msgstr "" -#: templates/question.html:75 templates/question.html.py:89 -#: templates/question.html:289 +#: templates/question.html:80 templates/question.html.py:98 +#: templates/question.html:257 msgid "current number of votes" msgstr "" -#: templates/question.html:80 templates/question.html.py:81 -#: templates/question.html:94 templates/question.html.py:95 +#: templates/question.html:89 templates/question.html.py:90 +#: templates/question.html:103 templates/question.html.py:104 msgid "i dont like this post (click again to cancel)" msgstr "" -#: templates/question.html:100 templates/question.html.py:101 +#: templates/question.html:109 templates/question.html.py:110 msgid "mark this question as favorite (click again to cancel)" msgstr "" -#: templates/question.html:107 templates/question.html.py:108 +#: templates/question.html:116 templates/question.html.py:117 msgid "remove favorite mark from this question (click again to restore mark)" msgstr "" -#: templates/question.html:134 templates/question.html.py:322 -#: templates/revisions_answer.html:53 templates/revisions_question.html:53 +#: templates/question.html:140 templates/question.html.py:294 +#: templates/revisions_answer.html:56 templates/revisions_question.html:56 msgid "edit" msgstr "" -#: templates/question.html:138 templates/question.html.py:332 +#: templates/question.html:144 templates/question.html.py:301 msgid "delete" msgstr "" -#: templates/question.html:143 +#: templates/question.html:149 msgid "reopen" msgstr "" -#: templates/question.html:148 +#: templates/question.html:153 msgid "close" msgstr "" -#: templates/question.html:154 templates/question.html.py:345 +#: templates/question.html:159 templates/question.html.py:308 msgid "" "report as offensive (i.e containing spam, advertising, malicious text, etc.)" msgstr "" -#: templates/question.html:155 templates/question.html.py:346 +#: templates/question.html:160 templates/question.html.py:309 msgid "flag offensive" msgstr "" -#: templates/question.html:167 templates/question.html.py:355 -#: templates/revisions_answer.html:65 templates/revisions_question.html:65 -msgid "updated" +#: templates/question.html:182 templates/question.html.py:331 +msgid "delete this comment" msgstr "" -#: templates/question.html:216 templates/question.html.py:402 -#: templates/revisions_answer.html:63 templates/revisions_question.html:63 -msgid "asked" -msgstr "" +#: templates/question.html:193 templates/question.html.py:342 +msgid "add comment" +msgstr "post a comment" -#: templates/question.html:246 templates/question.html.py:429 -msgid "comments" -msgstr "" +#: templates/question.html:197 +#, python-format +msgid "" +"\n" +" see one more \n" +" " +msgid_plural "" +"\n" +" see %(counter)s " +"more\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/question.html:247 templates/question.html.py:430 -msgid "add comment" -msgstr "" +#: templates/question.html:203 +#, python-format +msgid "" +"\n" +" see one more " +"comment\n" +" " +msgid_plural "" +"\n" +" see %(counter)s " +"more comments\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/question.html:260 +#: templates/question.html:219 #, python-format msgid "" -"The question has been closed for the following reason \"%(question." -"get_close_reason_display)s\" by" +"The question has been closed for the following reason \"%(close_reason)s\" by" msgstr "" -#: templates/question.html:262 +#: templates/question.html:221 #, python-format -msgid "close date %(question.closed_at)s" +msgid "close date %(closed_at)s" msgstr "" -#: templates/question.html:269 templates/user_stats.html:13 -msgid "Answers" -msgstr " Answers" +#: templates/question.html:229 +#, python-format +msgid "" +"\n" +" One Answer:\n" +" " +msgid_plural "" +"\n" +" %(counter)s Answers:\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/question.html:271 +#: templates/question.html:237 msgid "oldest answers will be shown first" msgstr "" -#: templates/question.html:271 +#: templates/question.html:237 msgid "oldest answers" msgstr "oldest" -#: templates/question.html:272 +#: templates/question.html:239 msgid "newest answers will be shown first" msgstr "" -#: templates/question.html:272 +#: templates/question.html:239 msgid "newest answers" msgstr "newest" -#: templates/question.html:273 +#: templates/question.html:241 msgid "most voted answers will be shown first" msgstr "" -#: templates/question.html:273 +#: templates/question.html:241 msgid "popular answers" msgstr "most voted" -#: templates/question.html:287 templates/question.html.py:288 +#: templates/question.html:255 templates/question.html.py:256 msgid "i like this answer (click again to cancel)" msgstr "" -#: templates/question.html:294 templates/question.html.py:295 +#: templates/question.html:262 templates/question.html.py:263 msgid "i dont like this answer (click again to cancel)" msgstr "" -#: templates/question.html:300 templates/question.html.py:301 +#: templates/question.html:268 templates/question.html.py:269 msgid "mark this answer as favorite (click again to undo)" msgstr "" -#: templates/question.html:306 templates/question.html.py:307 +#: templates/question.html:274 templates/question.html.py:275 msgid "the author of the question has selected this answer as correct" msgstr "" -#: templates/question.html:329 +#: templates/question.html:288 +msgid "answer permanent link" +msgstr "" + +#: templates/question.html:289 +msgid "permanent link" +msgstr "link" + +#: templates/question.html:301 msgid "undelete" msgstr "" -#: templates/question.html:339 -msgid "answer permanent link" +#: templates/question.html:346 +#, python-format +msgid "" +"\n" +" see one more \n" +" " +msgid_plural "" +"\n" +" see %" +"(counter)s more\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/question.html:352 +#, python-format +msgid "" +"\n" +" see one more comment\n" +" " +msgid_plural "" +"\n" +" see %" +"(counter)s more comments\n" +" " +msgstr[0] "" +msgstr[1] "" + +#: templates/question.html:378 templates/question.html.py:381 +msgid "Notify me once a day when there are any new answers" msgstr "" +"Notify me once a day by email when there are any new " +"answers or updates" -#: templates/question.html:340 -msgid "permanent link" +#: templates/question.html:384 +msgid "Notify me weekly when there are any new answers" +msgstr "" +"Notify me weekly when there are any new answers or updates" + +#: templates/question.html:389 +#, python-format +msgid "" +"\n" +" You can always adjust frequency of email updates from your %" +"(profile_url)s\n" +" " +msgstr "" +"\n" +"(note: you can always adjust frequency of email updates)" + +#: templates/question.html:396 +msgid "once you sign in you will be able to subscribe for any updates here" msgstr "" +"Here (once you log in) you will be able to sign " +"up for the periodic email updates about this question." -#: templates/question.html:453 +#: templates/question.html:407 msgid "Your answer" msgstr "" -#: templates/question.html:456 +#: templates/question.html:409 +msgid "Be the first one to answer this question!" +msgstr "" + +#: templates/question.html:415 msgid "you can answer anonymously and then login" msgstr "" -"You are now not logged in but you can answer " -"first and then login" +"Please start posting your answer anonymously " +"- your answer will be saved within the current session and published after " +"you log in or create a new account. Please try to give a substantial " +"answer, for discussions, please use comments and " +"please do remember to vote (after you log in)!" -#: templates/question.html:479 -msgid "Answer the question" +#: templates/question.html:419 +msgid "answer your own question only to give an answer" msgstr "" +"You are welcome to answer your own question, " +"but please make sure to give an answer. Remember that you " +"can always revise your original question. Please " +"use comments for discussions and please don't " +"forget to vote :) for the answers that you liked (or perhaps did " +"not like)! " -#: templates/question.html:481 -msgid "Notify me daily if there are any new answers." +#: templates/question.html:421 +msgid "please only give an answer, no discussions" msgstr "" +"Please try to give a substantial answer. If " +"you wanted to comment on the question or answer, just use the " +"commenting tool. Please remember that you can always revise " +"your answers - no need to answer the same question twice. Also, " +"please don't forget to vote - it really helps to select the " +"best questions and answers!" -#: templates/question.html:483 -msgid "once you sign in you will be able to subscribe for any updates here" -msgstr "Here logged in users can sign up for the question updates." +#: templates/question.html:457 +msgid "Login/Signup to Post Your Answer" +msgstr "" + +#: templates/question.html:460 +msgid "Answer Your Own Question" +msgstr "" + +#: templates/question.html:462 +msgid "Answer the question" +msgstr "Post Your Answer" -#: templates/question.html:494 +#: templates/question.html:475 msgid "Question tags" msgstr "Tags" -#: templates/question.html:504 +#: templates/question.html:485 msgid "question asked" msgstr "Asked" -#: templates/question.html:504 templates/question.html.py:510 -#: templates/user_info.html:51 -msgid "ago" -msgstr "" - -#: templates/question.html:507 +#: templates/question.html:488 msgid "question was seen" msgstr "Seen" -#: templates/question.html:507 +#: templates/question.html:488 msgid "times" msgstr "" -#: templates/question.html:510 +#: templates/question.html:491 msgid "last updated" msgstr "Last updated" -#: templates/question.html:515 +#: templates/question.html:496 msgid "Related questions" msgstr "" -#: templates/question_edit.html:4 templates/question_edit.html.py:65 +#: templates/question_edit.html:5 templates/question_edit.html.py:66 msgid "Edit question" msgstr "" @@ -1791,57 +2261,57 @@ msgstr "Tips" #: templates/question_edit_tips.html:7 msgid "please ask a relevant question" -msgstr "" +msgstr "ask a question relevant to the CNPROG community" #: templates/question_edit_tips.html:10 msgid "please try provide enough details" msgstr "provide enough details" -#: templates/question_retag.html:3 templates/question_retag.html.py:52 +#: templates/question_retag.html:4 templates/question_retag.html.py:53 msgid "Change tags" msgstr "" -#: templates/question_retag.html:39 +#: templates/question_retag.html:40 msgid "up to 5 tags, less than 20 characters each" msgstr "" -#: templates/question_retag.html:82 +#: templates/question_retag.html:83 msgid "Why use and modify tags?" msgstr "" -#: templates/question_retag.html:85 +#: templates/question_retag.html:86 msgid "tags help us keep Questions organized" msgstr "" -#: templates/question_retag.html:91 +#: templates/question_retag.html:94 msgid "tag editors receive special awards from the community" msgstr "" -#: templates/questions.html:23 +#: templates/questions.html:24 msgid "Found by tags" msgstr "Tagged questions" -#: templates/questions.html:23 +#: templates/questions.html:24 msgid "Found by title" msgstr "" -#: templates/questions.html:23 +#: templates/questions.html:24 msgid "All questions" msgstr "" -#: templates/questions.html:25 templates/unanswered.html:20 +#: templates/questions.html:26 templates/unanswered.html:21 msgid "most recently asked questions" msgstr "" -#: templates/questions.html:26 +#: templates/questions.html:27 msgid "most recently updated questions" msgstr "" -#: templates/questions.html:26 +#: templates/questions.html:27 msgid "active" msgstr "" -#: templates/questions.html:109 +#: templates/questions.html:126 #, python-format msgid "" "\n" @@ -1860,7 +2330,7 @@ msgstr[1] "" "

%(q_num)s

questions tagged

%(tagname)s
" -#: templates/questions.html:116 +#: templates/questions.html:133 #, python-format msgid "" "\n" @@ -1881,7 +2351,7 @@ msgstr[1] "" "containing %(searchtitle)s" -#: templates/questions.html:122 +#: templates/questions.html:139 #, python-format msgid "" "\n" @@ -1898,35 +2368,35 @@ msgstr[1] "" "\n" "
%(q_num)s

questions

" -#: templates/questions.html:131 +#: templates/questions.html:148 msgid "latest questions info" msgstr "Newest questions are shown first." -#: templates/questions.html:135 +#: templates/questions.html:152 msgid "Questions are sorted by the time of last update." msgstr "" -#: templates/questions.html:136 +#: templates/questions.html:153 msgid "Most recently answered ones are shown first." msgstr "Most recently answered questions are shown first." -#: templates/questions.html:140 +#: templates/questions.html:157 msgid "Questions sorted by number of responses." msgstr "Questions sorted by the number of answers." -#: templates/questions.html:141 +#: templates/questions.html:158 msgid "Most answered questions are shown first." msgstr " " -#: templates/questions.html:145 +#: templates/questions.html:162 msgid "Questions are sorted by the number of votes." msgstr "" -#: templates/questions.html:146 +#: templates/questions.html:163 msgid "Most voted questions are shown first." msgstr "" -#: templates/questions.html:153 templates/unanswered.html:102 +#: templates/questions.html:168 templates/unanswered.html:118 msgid "Related tags" msgstr "Tags" @@ -1963,6 +2433,10 @@ msgstr "" msgid "Revision history" msgstr "" +#: templates/revisions_answer.html:48 templates/revisions_question.html:48 +msgid "click to hide/show revision" +msgstr "" + #: templates/tags.html:6 templates/tags.html.py:30 msgid "Tag list" msgstr "" @@ -1995,16 +2469,16 @@ msgstr "" msgid "Nothing found" msgstr "" -#: templates/unanswered.html:7 templates/unanswered.html.py:18 +#: templates/unanswered.html:8 templates/unanswered.html.py:19 msgid "Unanswered questions" msgstr "" -#: templates/unanswered.html:97 +#: templates/unanswered.html:114 #, python-format msgid "have %(num_q)s unanswered questions" msgstr "" -"
%(num_q)s
unanswered " -"questions" +"
%(num_q)s
questions without " +"accepted answers" #: templates/user_edit.html:6 msgid "Edit user profile" @@ -2019,139 +2493,202 @@ msgid "image associated with your email address" msgstr "" #: templates/user_edit.html:31 -msgid "avatar" -msgstr "gravatar" +#, python-format +msgid "avatar, see %(gravatar_faq_url)s" +msgstr "gravatar" -#: templates/user_edit.html:36 templates/user_info.html:31 +#: templates/user_edit.html:36 templates/user_info.html:56 msgid "Registered user" msgstr "" -#: templates/user_edit.html:82 +#: templates/user_edit.html:82 templates/user_email_subscriptions.html:17 msgid "Update" msgstr "" -#: templates/user_info.html:34 +#: templates/user_email_subscriptions.html:8 +msgid "Email subscription settings" +msgstr "" + +#: templates/user_email_subscriptions.html:9 +msgid "email subscription settings info" +msgstr "" +"Adjust frequency of email updates. Receive " +"updates on interesting questions by email,
help the community by answering questions of your colleagues. If you do not wish to " +"receive emails - select 'no email' on all items below.
Updates are only " +"sent when there is any new activity on selected items." + +#: templates/user_email_subscriptions.html:18 +msgid "Stop sending email" +msgstr "Stop Email" + +#: templates/user_info.html:32 +msgid "Moderate this user" +msgstr "" + +#: templates/user_info.html:45 msgid "update profile" msgstr "" -#: templates/user_info.html:40 +#: templates/user_info.html:60 msgid "real name" msgstr "" -#: templates/user_info.html:45 +#: templates/user_info.html:65 msgid "member for" -msgstr "" +msgstr "member since" -#: templates/user_info.html:50 +#: templates/user_info.html:70 msgid "last seen" msgstr "" -#: templates/user_info.html:56 +#: templates/user_info.html:76 msgid "user website" msgstr "" -#: templates/user_info.html:62 +#: templates/user_info.html:82 msgid "location" msgstr "" -#: templates/user_info.html:69 +#: templates/user_info.html:89 msgid "age" msgstr "" -#: templates/user_info.html:70 +#: templates/user_info.html:90 msgid "age unit" msgstr "years old" -#: templates/user_info.html:76 +#: templates/user_info.html:96 msgid "todays unused votes" msgstr "" -#: templates/user_info.html:77 +#: templates/user_info.html:97 msgid "votes left" msgstr "" -#: templates/user_preferences.html:10 -msgid "Connect with Twitter" -msgstr "" - -#: templates/user_preferences.html:13 -msgid "Twitter account name:" -msgstr "" - -#: templates/user_preferences.html:15 -msgid "Twitter password:" -msgstr "" - -#: templates/user_preferences.html:17 -msgid "Send my Questions to Twitter" -msgstr "" - -#: templates/user_preferences.html:18 -msgid "Send my Answers to Twitter" -msgstr "" - -#: templates/user_preferences.html:19 -msgid "Save" -msgstr "" +#: templates/user_stats.html:12 +#, python-format +msgid "" +"\n" +" 1 Question\n" +" " +msgid_plural "" +"\n" +" %(counter)s Questions\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:10 -msgid "User questions" -msgstr "" +#: templates/user_stats.html:23 +#, python-format +msgid "" +"\n" +" 1 Answer\n" +" " +msgid_plural "" +"\n" +" %(counter)s Answers\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:20 +#: templates/user_stats.html:36 #, python-format msgid "the answer has been voted for %(vote_count)s times" msgstr "" -#: templates/user_stats.html:20 +#: templates/user_stats.html:36 msgid "this answer has been selected as correct" msgstr "" -#: templates/user_stats.html:28 +#: templates/user_stats.html:46 #, python-format -msgid "the answer has been commented %(comment_count)s times" -msgstr "" +msgid "" +"\n" +" (one comment)\n" +" " +msgid_plural "" +"\n" +" the answer has been commented %(comment_count)s times\n" +" " +msgstr[0] "" +"\n" +"(one comment)" +msgstr[1] "" +"\n" +"(%(comment_count)s comments)" -#: templates/user_stats.html:36 -msgid "Votes" -msgstr "" +#: templates/user_stats.html:61 +#, python-format +msgid "" +"\n" +" 1 Vote\n" +" " +msgid_plural "" +"\n" +" %(cnt)s Votes\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:41 +#: templates/user_stats.html:72 msgid "thumb up" msgstr "" -#: templates/user_stats.html:42 +#: templates/user_stats.html:73 msgid "user has voted up this many times" msgstr "" -#: templates/user_stats.html:46 +#: templates/user_stats.html:77 msgid "thumb down" msgstr "" -#: templates/user_stats.html:47 +#: templates/user_stats.html:78 msgid "user voted down this many times" msgstr "" -#: templates/user_stats.html:54 -msgid "Tags" -msgstr "" +#: templates/user_stats.html:87 +#, python-format +msgid "" +"\n" +" 1 Tag\n" +" " +msgid_plural "" +"\n" +" %(counter)s Tags\n" +" " +msgstr[0] "" +msgstr[1] "" -#: templates/user_stats.html:61 +#: templates/user_stats.html:100 #, python-format msgid "see other questions tagged '%(tag)s' " msgstr "" +#: templates/user_stats.html:114 +#, python-format +msgid "" +"\n" +" 1 Badge\n" +" " +msgid_plural "" +"\n" +" %(counter)s Badges\n" +" " +msgstr[0] "" +msgstr[1] "" + #: templates/user_tabs.html:7 msgid "User profile" msgstr "" #: templates/user_tabs.html:16 msgid "graph of user reputation" -msgstr "" +msgstr "Graph of user karma" #: templates/user_tabs.html:17 msgid "reputation history" -msgstr "" +msgstr "karma history" #: templates/user_tabs.html:23 msgid "questions that user selected as his/her favorite" @@ -2161,10 +2698,6 @@ msgstr "" msgid "favorites" msgstr "" -#: templates/user_tabs.html:28 -msgid "settings" -msgstr "" - #: templates/users.html:6 templates/users.html.py:24 msgid "Users" msgstr "" @@ -2202,16 +2735,21 @@ msgstr "" msgid "thumb-up off" msgstr "" -#: templates/users_questions.html:35 +#: templates/users_questions.html:34 msgid "this answer has been accepted to be correct" msgstr "" -#: templates/authopenid/changeemail.html:7 -#: templates/authopenid/changeemail.html:33 +#: templates/authopenid/changeemail.html:3 +#: templates/authopenid/changeemail.html:9 +#: templates/authopenid/changeemail.html:38 msgid "Change email" msgstr "Change Email" -#: templates/authopenid/changeemail.html:10 +#: templates/authopenid/changeemail.html:11 +msgid "Save your email address" +msgstr "" + +#: templates/authopenid/changeemail.html:16 #, python-format msgid "change %(email)s info" msgstr "" @@ -2219,57 +2757,66 @@ msgstr "" "you'd like to use another email for update subscriptions." "
Currently you are using %(email)s" -#: templates/authopenid/changeemail.html:13 -#: templates/authopenid/changeopenid.html:14 -#: templates/authopenid/changepw.html:19 templates/authopenid/delete.html:15 -#: templates/authopenid/delete.html:25 -msgid "Please correct errors below:" +#: templates/authopenid/changeemail.html:18 +#, python-format +msgid "here is why email is required, see %(gravatar_faq_url)s" msgstr "" +"Please enter your email address in the box below. Valid email address is required on this Q&" +"A forum. If you like, you can receive updates on " +"interesting questions or entire forum via email. Also, your email is used to " +"create a unique gravatar " +"image for your account. Email addresses are never shown or otherwise shared " +"with anybody else." -#: templates/authopenid/changeemail.html:30 +#: templates/authopenid/changeemail.html:31 msgid "Your new Email" msgstr "" "Your new Email: (will not be shown to " "anyone, must be valid)" #: templates/authopenid/changeemail.html:31 -#: templates/authopenid/signin.html:154 -msgid "Password" +msgid "Your Email" msgstr "" +"Your Email (must be valid, never shown to others)" -#: templates/authopenid/changeemail.html:42 +#: templates/authopenid/changeemail.html:38 +msgid "Save Email" +msgstr "" + +#: templates/authopenid/changeemail.html:49 msgid "Validate email" msgstr "" -#: templates/authopenid/changeemail.html:45 +#: templates/authopenid/changeemail.html:52 #, python-format -msgid "validate %(email)s info" +msgid "validate %(email)s info or go to %(change_email_url)s" msgstr "" "An email with a validation link has been sent to %" "(email)s. Please follow the emailed link with your " "web browser. Email validation is necessary to help insure the proper use of " -"email on Q&A forum. If you would like " -"to use another email, please change it again." +"email on Q&A. If you would like " +"to use another email, please change it again." -#: templates/authopenid/changeemail.html:50 +#: templates/authopenid/changeemail.html:57 msgid "Email not changed" msgstr "" -#: templates/authopenid/changeemail.html:53 +#: templates/authopenid/changeemail.html:60 #, python-format -msgid "old %(email)s kept" +msgid "old %(email)s kept, if you like go to %(change_email_url)s" msgstr "" "Your email address %(email)s has not been changed." " If you decide to change it later - you can always do it by editing " -"it in your user profile or by using the previous form again." +"it in your user profile or by using the previous form again." -#: templates/authopenid/changeemail.html:58 +#: templates/authopenid/changeemail.html:65 msgid "Email changed" msgstr "" -#: templates/authopenid/changeemail.html:61 +#: templates/authopenid/changeemail.html:68 #, python-format msgid "your current %(email)s can be used for this" msgstr "" @@ -2278,11 +2825,11 @@ msgstr "" "Email notifications are sent once a day or less frequently - only when there " "are any news." -#: templates/authopenid/changeemail.html:66 +#: templates/authopenid/changeemail.html:73 msgid "Email verified" msgstr "" -#: templates/authopenid/changeemail.html:69 +#: templates/authopenid/changeemail.html:76 msgid "thanks for verifying email" msgstr "" "Thank you for verifying your email! Now " @@ -2291,11 +2838,11 @@ msgstr "" "updates
- then will be notified about changes once a day or less frequently." -#: templates/authopenid/changeemail.html:74 +#: templates/authopenid/changeemail.html:81 msgid "email key not sent" msgstr "Validation email not sent" -#: templates/authopenid/changeemail.html:77 +#: templates/authopenid/changeemail.html:84 #, python-format msgid "email key not sent %(email)s change email here %(change_link)s" msgstr "" @@ -2303,6 +2850,12 @@ msgstr "" "validated before so the new key was not sent. You can change email used for update subscriptions if necessary." +#: templates/authopenid/changeopenid.html:4 +#: templates/authopenid/changeopenid.html:30 +#: templates/authopenid/settings.html:34 +msgid "Change OpenID" +msgstr "" + #: templates/authopenid/changeopenid.html:8 msgid "Account: change OpenID URL" msgstr "" @@ -2312,97 +2865,129 @@ msgid "" "This is where you can change your OpenID URL. Make sure you remember it!" msgstr "" +#: templates/authopenid/changeopenid.html:14 +#: templates/authopenid/delete.html:14 templates/authopenid/delete.html:24 +msgid "Please correct errors below:" +msgstr "" + #: templates/authopenid/changeopenid.html:29 msgid "OpenID URL:" msgstr "" -#: templates/authopenid/changeopenid.html:30 -msgid "Change OpenID" +#: templates/authopenid/changepw.html:5 templates/authopenid/changepw.html:14 +#: templates/authopenid/settings.html:29 +msgid "Change password" msgstr "" -#: templates/authopenid/changepw.html:14 +#: templates/authopenid/changepw.html:7 msgid "Account: change password" -msgstr "" +msgstr "Change your password" -#: templates/authopenid/changepw.html:17 +#: templates/authopenid/changepw.html:8 msgid "This is where you can change your password. Make sure you remember it!" msgstr "" +"To change your password please fill out and " +"submit this form" -#: templates/authopenid/changepw.html:27 -msgid "Current password" -msgstr "" - -#: templates/authopenid/changepw.html:28 -msgid "New password" -msgstr "" - -#: templates/authopenid/changepw.html:29 -msgid "New password again" -msgstr "" - -#: templates/authopenid/changepw.html:30 templates/authopenid/settings.html:29 -msgid "Change password" -msgstr "" - -#: templates/authopenid/complete.html:5 +#: templates/authopenid/complete.html:19 msgid "Connect your OpenID with this site" msgstr "New user signup" -#: templates/authopenid/complete.html:8 +#: templates/authopenid/complete.html:22 msgid "Connect your OpenID with your account on this site" msgstr "New user signup" -#: templates/authopenid/complete.html:12 +#: templates/authopenid/complete.html:27 #, python-format -msgid "register new %(provider)s account info" +msgid "register new %(provider)s account info, see %(gravatar_faq_url)s" msgstr "" "

You are here for the first time with your %" "(provider)s login. Please create your screen name " "and save your email address. Saved email address will let " "you subscribe for the updates on the most interesting " "questions and will be used to create and retrieve your unique avatar image - " -"gravatar." +"gravatar.

" -#: templates/authopenid/complete.html:14 +#: templates/authopenid/complete.html:31 +#, python-format +msgid "" +"%(username)s already exists, choose another name for \n" +" %(provider)s. Email is required too, see %" +"(gravatar_faq_url)s\n" +" " +msgstr "" +"

Oops... looks like screen name %(username)s is " +"already used in another account.

Please choose another screen name " +"to use with your %(provider)s login. Also, a valid email address is " +"required on the Q&A forum. Your " +"email is used to create a unique gravatar image for your account. If you like, " +"you can receive updates on the interesting questions or " +"entire forum by email. Email addresses are never shown or otherwise shared " +"with anybody else.

" + +#: templates/authopenid/complete.html:35 +#, python-format +msgid "" +"register new external %(provider)s account info, see %(gravatar_faq_url)s" +msgstr "" +"

You are here for the first time with your %" +"(provider)s login.

You can either keep your screen " +"name the same as your %(provider)s login name or choose some other " +"nickname.

Also, please save a valid email address. " +"With the email you can subscribe for the updates on the " +"most interesting questions. Email address is also used to create and " +"retrieve your unique avatar image - gravatar.

" + +#: templates/authopenid/complete.html:40 msgid "This account already exists, please use another." msgstr "" -#: templates/authopenid/complete.html:19 templates/authopenid/complete.html:32 -#: templates/authopenid/signin.html:138 +#: templates/authopenid/complete.html:55 msgid "Sorry, looks like we have some errors:" msgstr "" -#: templates/authopenid/complete.html:47 +#: templates/authopenid/complete.html:76 msgid "Screen name label" msgstr "Screen Name (will be shown to others)" -#: templates/authopenid/complete.html:48 +#: templates/authopenid/complete.html:83 msgid "Email address label" msgstr "" "Email Address (will not be shared with " "anyone, must be valid)" -#: templates/authopenid/complete.html:49 +#: templates/authopenid/complete.html:89 templates/authopenid/signup.html:15 +msgid "receive updates motivational blurb" +msgstr "" +"Receive forum updates by email - this will help our " +"community grow and become more useful.
By default " +"Q&A forum sends up to one " +"email digest per week - only when there is anything new.
If " +"you like, please adjust this now or any time later from your user account." + +#: templates/authopenid/complete.html:91 msgid "create account" msgstr "Signup" -#: templates/authopenid/complete.html:56 +#: templates/authopenid/complete.html:100 msgid "Existing account" msgstr "" -#: templates/authopenid/complete.html:57 +#: templates/authopenid/complete.html:101 msgid "user name" msgstr "" -#: templates/authopenid/complete.html:58 templates/authopenid/signin.html:128 +#: templates/authopenid/complete.html:102 msgid "password" msgstr "" -#: templates/authopenid/complete.html:61 +#: templates/authopenid/complete.html:107 msgid "Register" msgstr "" -#: templates/authopenid/complete.html:62 templates/authopenid/signin.html:156 +#: templates/authopenid/complete.html:108 templates/authopenid/signin.html:140 msgid "Forgot your password?" msgstr "" @@ -2415,12 +3000,11 @@ msgid "Your account details are:" msgstr "" #: templates/authopenid/confirm_email.txt:6 -#: templates/authopenid/sendpw_email.txt:7 msgid "Username:" msgstr "" #: templates/authopenid/confirm_email.txt:7 -#: templates/authopenid/delete.html:20 +#: templates/authopenid/delete.html:19 msgid "Password:" msgstr "" @@ -2430,45 +3014,51 @@ msgstr "" #: templates/authopenid/confirm_email.txt:12 #: templates/authopenid/email_validation.txt:14 -#: templates/authopenid/sendpw_email.txt:13 +#: templates/authopenid/sendpw_email.txt:8 msgid "" "Sincerely,\n" "Forum Administrator" msgstr "" +"Sincerely,\n" +"Q&A Forum Administrator" + +#: templates/authopenid/delete.html:4 templates/authopenid/settings.html:38 +msgid "Delete account" +msgstr "" -#: templates/authopenid/delete.html:9 +#: templates/authopenid/delete.html:8 msgid "Account: delete account" msgstr "" -#: templates/authopenid/delete.html:13 +#: templates/authopenid/delete.html:12 msgid "" "Note: After deleting your account, anyone will be able to register this " "username." msgstr "" -#: templates/authopenid/delete.html:17 +#: templates/authopenid/delete.html:16 msgid "Check confirm box, if you want delete your account." msgstr "" -#: templates/authopenid/delete.html:32 +#: templates/authopenid/delete.html:31 msgid "I am sure I want to delete my account." msgstr "" -#: templates/authopenid/delete.html:33 +#: templates/authopenid/delete.html:32 msgid "Password/OpenID URL" msgstr "" -#: templates/authopenid/delete.html:33 +#: templates/authopenid/delete.html:32 msgid "(required for your security)" msgstr "" -#: templates/authopenid/delete.html:35 +#: templates/authopenid/delete.html:34 msgid "Delete account permanently" msgstr "" #: templates/authopenid/email_validation.txt:2 msgid "Greetings from the Q&A forum" -msgstr "Greetings from the Q&A forum" +msgstr "" #: templates/authopenid/email_validation.txt:4 msgid "To make use of the Forum, please follow the link below:" @@ -2485,38 +3075,33 @@ msgid "" "for any inconvenience" msgstr "" -#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:8 -msgid "Send new password" -msgstr "" - -#: templates/authopenid/sendpw.html:12 -msgid "Lost your password? No problem - here you can reset it." +#: templates/authopenid/external_legacy_login_info.html:4 +#: templates/authopenid/external_legacy_login_info.html:7 +msgid "Traditional login information" msgstr "" -#: templates/authopenid/sendpw.html:13 -msgid "" -"Please enter your username below and new password will be sent to your " -"registered e-mail" -msgstr "" +#: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:7 +msgid "Send new password" +msgstr "Recover password" -#: templates/authopenid/sendpw.html:28 -msgid "User name" +#: templates/authopenid/sendpw.html:10 +msgid "password recovery information" msgstr "" +"Forgot you password? No problems - just get a new " +"one!
Please follow the following steps:
• submit your " +"user name below and check your email
follow the " +"activation link for the new password - sent to you by email and " +"login with the suggested password
• at this you might want to " +"change your password to something you can remember better" -#: templates/authopenid/sendpw.html:30 +#: templates/authopenid/sendpw.html:21 msgid "Reset password" -msgstr "" +msgstr "Send me a new password" -#: templates/authopenid/sendpw.html:30 +#: templates/authopenid/sendpw.html:22 msgid "return to login" msgstr "" -#: templates/authopenid/sendpw.html:33 -msgid "" -"Note: your new password will be activated only after you click the " -"activation link in the email message" -msgstr "" - #: templates/authopenid/sendpw_email.txt:2 #, python-format msgid "" @@ -2525,15 +3110,18 @@ msgid "" msgstr "" #: templates/authopenid/sendpw_email.txt:5 -msgid "Your new account details are:" -msgstr "" - -#: templates/authopenid/sendpw_email.txt:8 -msgid "New password:" +#, python-format +msgid "" +"email explanation how to use new %(password)s for %(username)s\n" +"with the %(key_link)s" msgstr "" +"To change your password, please follow these steps:\n" +"* visit this link: %(key_link)s\n" +"* login with user name %(username)s and password %(password)s\n" +"* go to your user profile and set the password to something you can remember" -#: templates/authopenid/sendpw_email.txt:10 -msgid "To confirm that you wanted to reset your password please visit:" +#: templates/authopenid/settings.html:4 +msgid "Account functions" msgstr "" #: templates/authopenid/settings.html:30 @@ -2552,15 +3140,11 @@ msgstr "" msgid "Change openid associated to your account" msgstr "" -#: templates/authopenid/settings.html:38 -msgid "Delete account" -msgstr "" - #: templates/authopenid/settings.html:39 msgid "Erase your username and all your data from website" msgstr "" -#: templates/authopenid/signin.html:4 templates/authopenid/signin.html:21 +#: templates/authopenid/signin.html:5 templates/authopenid/signin.html:21 msgid "User login" msgstr "User login" @@ -2588,148 +3172,105 @@ msgstr "" "strong> %(summary)s...\" is saved and will be " "posted once you log in." -#: templates/authopenid/signin.html:40 +#: templates/authopenid/signin.html:42 msgid "Click to sign in through any of these services." msgstr "" -"

Please select your favorite login method below." -"

External login services use OpenID technology that increases " -"security of your online identity and makes login process simpler. First option " -"requires login name and password.

" +"

Please select your favorite login method below." +"

External login services use OpenID technology, where your password " +"always stays confidential between you and your login provider and you don't " +"have to remember another one. CNPROG option requires your login name and " +"password entered here.

" -#: templates/authopenid/signin.html:113 +#: templates/authopenid/signin.html:117 msgid "Enter your Provider user name" msgstr "" -"Enter your Provider " -"user name
(or select another login method above)" +"Enter your Provider user name
(or " +"select another login method above)" -#: templates/authopenid/signin.html:120 +#: templates/authopenid/signin.html:124 msgid "" "Enter your OpenID " "web address" msgstr "" "Enter your OpenID web address
(or choose another login " -"method above)" +"openid.net\">OpenID web address
(or choose " +"another login method above)" -#: templates/authopenid/signin.html:122 templates/authopenid/signin.html:130 -#: templates/authopenid/signin.html:155 +#: templates/authopenid/signin.html:126 templates/authopenid/signin.html:138 msgid "Login" msgstr "" -#: templates/authopenid/signin.html:125 +#: templates/authopenid/signin.html:129 msgid "Enter your login name and password" msgstr "" -"Enter your forum login and password
" -"(or select your OpenID provider above)" - -#: templates/authopenid/signin.html:126 -msgid "login name" -msgstr "" - -#: templates/authopenid/signin.html:134 -msgid "we support two login modes" -msgstr "" -"You can log in either through one of these services or traditionally - using " -"local username/password." +"Enter your CNPROG login and password
(or select your OpenID provider above)" -#: templates/authopenid/signin.html:152 -msgid "Use login name and password" +#: templates/authopenid/signin.html:133 +msgid "Login name" msgstr "" -#: templates/authopenid/signin.html:153 -msgid "Login name" +#: templates/authopenid/signin.html:135 +msgid "Password" msgstr "" -#: templates/authopenid/signin.html:157 -msgid "Create new account" +#: templates/authopenid/signin.html:139 +msgid "Create account" msgstr "" -#: templates/authopenid/signin.html:166 +#: templates/authopenid/signin.html:149 msgid "Why use OpenID?" msgstr "" -#: templates/authopenid/signin.html:169 +#: templates/authopenid/signin.html:152 msgid "with openid it is easier" msgstr "With the OpenID you don't need to create new username and password." -#: templates/authopenid/signin.html:172 +#: templates/authopenid/signin.html:155 msgid "reuse openid" msgstr "You can safely re-use the same login for all OpenID-enabled websites." -#: templates/authopenid/signin.html:175 +#: templates/authopenid/signin.html:158 msgid "openid is widely adopted" msgstr "" "There are > 160,000,000 OpenID account in use. Over 10,000 sites are OpenID-" "enabled." -#: templates/authopenid/signin.html:178 +#: templates/authopenid/signin.html:161 msgid "openid is supported open standard" msgstr "OpenID is based on an open standard, supported by many organizations." -#: templates/authopenid/signin.html:183 +#: templates/authopenid/signin.html:166 msgid "Find out more" msgstr "" -#: templates/authopenid/signin.html:184 +#: templates/authopenid/signin.html:167 msgid "Get OpenID" msgstr "" -#: templates/authopenid/signup.html:4 templates/authopenid/signup.html.py:8 +#: templates/authopenid/signup.html:4 msgid "Signup" msgstr "" -#: templates/authopenid/signup.html:12 -msgid "" -"We support two types of user registration: conventional username/password, " -"and" +#: templates/authopenid/signup.html:8 +msgid "Create login name and password" msgstr "" -#: templates/authopenid/signup.html:12 -msgid "the OpenID method" +#: templates/authopenid/signup.html:10 +msgid "Traditional signup info" msgstr "" +"If you prefer, create your forum login name and " +"password here. However, please keep in mind that we also support " +"OpenID login method. With OpenID you can " +"simply reuse your external login (e.g. Gmail or AOL) without ever sharing " +"your login details with anyone and having to remember yet another password." #: templates/authopenid/signup.html:17 -msgid "Sorry, looks like we have some errors" -msgstr "" - -#: templates/authopenid/signup.html:35 -msgid "Conventional registration" -msgstr "" - -#: templates/authopenid/signup.html:36 -msgid "choose a user name" -msgstr "" - -#: templates/authopenid/signup.html:42 -msgid "back to login" +msgid "Create Account" msgstr "" -#: templates/authopenid/signup.html:46 -msgid "Register with your OpenID" +#: templates/authopenid/signup.html:19 +msgid "return to OpenID login" msgstr "" - -#: templates/authopenid/signup.html:49 -msgid "Login with your OpenID" -msgstr "" - -# -#~ msgid "editing tips" -#~ msgstr "Tips" - -#~ msgid "Newest questions shown first." -#~ msgstr "" -#~ "Questions are sorted by entry date.Newest questions " -#~ "shown first." - -#~ msgid "" -#~ "please use space to separate tags (this enables autocomplete feature)" -#~ msgstr "please use space to separate tags (uses autocomplete utility)" - -#~ msgid "select openid provider" -#~ msgstr "1) Please select your id service provider." - -#~ msgid "verify openid link and login" -#~ msgstr "" -#~ "2) Please verify the OpenID URL (type your login name over {username}, if " -#~ "present) and then log in." diff --git a/locale/es/LC_MESSAGES/django.mo b/locale/es/LC_MESSAGES/django.mo index 91639ba1..fc7ebe14 100644 Binary files a/locale/es/LC_MESSAGES/django.mo and b/locale/es/LC_MESSAGES/django.mo differ diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po index d7655dd7..b528fcf2 100644 --- a/locale/es/LC_MESSAGES/django.po +++ b/locale/es/LC_MESSAGES/django.po @@ -2,21 +2,22 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-17 16:44+0000\n" -"PO-Revision-Date: 2009-08-17 10:04-0600\n" +"POT-Creation-Date: 2009-08-12 15:53+0000\n" +"PO-Revision-Date: \n" "Last-Translator: Bruno Sarlo \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: settings.py:12 urls.py:25 forum/views.py:310 forum/views.py:705 +#: settings.py:12 urls.py:25 forum/views.py:304 forum/views.py:698 msgid "account/" msgstr "cuenta/" #: settings.py:12 urls.py:26 django_authopenid/urls.py:9 #: django_authopenid/urls.py:10 django_authopenid/urls.py:11 -#: django_authopenid/urls.py:13 forum/views.py:310 forum/views.py:706 +#: django_authopenid/urls.py:13 forum/views.py:304 forum/views.py:699 +#: templates/authopenid/confirm_email.txt:10 msgid "signin/" msgstr "ingresar/" @@ -24,132 +25,127 @@ msgstr "ingresar/" msgid "upfiles/" msgstr "archivossubidos/" -#: urls.py:27 django_authopenid/urls.py:16 -msgid "signup/" -msgstr "registrarse/" - -#: urls.py:28 urls.py:29 urls.py:30 django_authopenid/urls.py:26 +#: urls.py:27 urls.py:28 urls.py:29 django_authopenid/urls.py:26 #: django_authopenid/urls.py:27 msgid "email/" msgstr "email/" -#: urls.py:28 +#: urls.py:27 msgid "change/" msgstr "cambiar/" -#: urls.py:29 +#: urls.py:28 msgid "sendkey/" msgstr "enviarclave/" -#: urls.py:30 +#: urls.py:29 msgid "verify/" msgstr "verificar/" -#: urls.py:31 +#: urls.py:30 msgid "about/" msgstr "acercadenosotros/" -#: urls.py:32 +#: urls.py:31 msgid "faq/" msgstr "preguntasfrecuentes/" -#: urls.py:33 +#: urls.py:32 msgid "privacy/" msgstr "códigodeprivacidad/" -#: urls.py:34 +#: urls.py:33 msgid "logout/" msgstr "cerrarsesion/" -#: urls.py:35 urls.py:36 urls.py:37 urls.py:49 forum/models.py:425 +#: urls.py:34 urls.py:35 urls.py:36 urls.py:48 forum/models.py:418 msgid "answers/" msgstr "respuestas/" -#: urls.py:35 urls.py:47 +#: urls.py:34 urls.py:46 msgid "comments/" msgstr "comentarios/" -#: urls.py:36 urls.py:41 urls.py:55 templates/user_info.html:34 +#: urls.py:35 urls.py:40 urls.py:54 templates/user_info.html:34 msgid "edit/" msgstr "editar/" -#: urls.py:37 urls.py:46 +#: urls.py:36 urls.py:45 msgid "revisions/" msgstr "revisiones/" -#: urls.py:38 urls.py:39 urls.py:40 urls.py:41 urls.py:42 urls.py:43 -#: urls.py:44 urls.py:45 urls.py:46 urls.py:47 urls.py:48 forum/feed.py:19 -#: forum/models.py:313 forum/views.py:1232 forum/views.py:1234 -#: forum/views.py:1474 +#: urls.py:37 urls.py:38 urls.py:39 urls.py:40 urls.py:41 urls.py:42 +#: urls.py:43 urls.py:44 urls.py:45 urls.py:46 urls.py:47 forum/feed.py:19 +#: forum/models.py:306 forum/views.py:1416 msgid "questions/" msgstr "preguntas/" -#: urls.py:39 urls.py:65 +#: urls.py:38 urls.py:64 msgid "ask/" msgstr "preguntar/" -#: urls.py:40 +#: urls.py:39 msgid "unanswered/" msgstr "sinrespuesta/" -#: urls.py:42 +#: urls.py:41 msgid "close/" msgstr "cerrar/" -#: urls.py:43 +#: urls.py:42 msgid "reopen/" msgstr "reabrir/" -#: urls.py:44 +#: urls.py:43 msgid "answer/" msgstr "respuesta/" -#: urls.py:45 +#: urls.py:44 msgid "vote/" msgstr "votar/" -#: urls.py:48 urls.py:49 django_authopenid/urls.py:29 +#: urls.py:47 urls.py:48 django_authopenid/urls.py:29 msgid "delete/" msgstr "borrar/" -#: urls.py:51 +#: urls.py:50 msgid "question/" msgstr "pregunta/" -#: urls.py:52 urls.py:53 forum/views.py:747 forum/views.py:2071 +#: urls.py:51 urls.py:52 forum/views.py:740 forum/views.py:2013 msgid "tags/" msgstr "etiquetas/" -#: urls.py:54 urls.py:55 urls.py:56 forum/views.py:1034 forum/views.py:1038 -#: forum/views.py:1476 forum/views.py:1809 forum/views.py:2073 +#: urls.py:53 urls.py:54 urls.py:55 forum/views.py:993 forum/views.py:997 +#: forum/views.py:1418 forum/views.py:1751 forum/views.py:2015 msgid "users/" msgstr "usuarios/" -#: urls.py:57 urls.py:58 +#: urls.py:56 urls.py:57 msgid "badges/" msgstr "distinciones/" -#: urls.py:59 +#: urls.py:58 msgid "messages/" msgstr "mensajes/" -#: urls.py:59 +#: urls.py:58 msgid "markread/" msgstr "marcarleido/" -#: urls.py:61 +#: urls.py:60 msgid "nimda/" msgstr "administrador/" -#: urls.py:63 +#: urls.py:62 msgid "upload/" msgstr "subir/" -#: urls.py:64 urls.py:65 urls.py:66 +#: urls.py:63 urls.py:64 urls.py:65 msgid "books/" msgstr "libros/" -#: urls.py:67 +#: urls.py:66 msgid "search/" msgstr "buscar/" @@ -244,7 +240,7 @@ msgstr "la nueva contraseña no coincide" msgid "Incorrect username." msgstr "Nombre de usuario incorrecto" -#: django_authopenid/urls.py:10 forum/views.py:310 forum/views.py:706 +#: django_authopenid/urls.py:10 forum/views.py:304 forum/views.py:699 msgid "newquestion/" msgstr "nuevapregunta/" @@ -264,19 +260,14 @@ msgstr "completado/" msgid "register/" msgstr "registrarse/" -#: django_authopenid/urls.py:19 +#: django_authopenid/urls.py:16 +msgid "signup/" +msgstr "registrarse/" + +#: django_authopenid/urls.py:18 msgid "sendpw/" msgstr "enviarcontrasena/" -#: django_authopenid/urls.py:20 -#, fuzzy -msgid "password/" -msgstr "contraseña" - -#: django_authopenid/urls.py:20 -msgid "confirm/" -msgstr "" - #: django_authopenid/urls.py:27 msgid "validate/" msgstr "" @@ -532,8 +523,8 @@ msgstr "" "por favor use solo los siguientes caracteres en los nombres de etiquetas: " "letras 'a-z', números y caracteres '.-_#'" -#: forum/forms.py:75 templates/index.html:57 templates/question.html:210 -#: templates/question.html.py:396 templates/questions.html:58 +#: forum/forms.py:75 templates/index.html:57 templates/question.html:209 +#: templates/question.html.py:395 templates/questions.html:58 #: templates/questions.html.py:70 templates/unanswered.html:48 #: templates/unanswered.html.py:60 msgid "community wiki" @@ -559,84 +550,75 @@ msgstr "" "ingresa un breve resumen de tu revisión (ej. error ortográfico, gramática, " "mejoras de estilo. Este campo es opcional." -#: forum/forms.py:98 forum/forms.py:159 -msgid "please choice a category" -msgstr "por favor escoja una categoría" - -#: forum/forms.py:99 forum/forms.py:160 -msgid "Category" -msgstr "Categoría" - -#: forum/forms.py:180 +#: forum/forms.py:175 msgid "this email does not have to be linked to gravatar" msgstr "este email no tiene porque estar asociado a un Gravatar" -#: forum/forms.py:181 +#: forum/forms.py:176 msgid "Real name" msgstr "Nombre real" -#: forum/forms.py:182 +#: forum/forms.py:177 msgid "Website" msgstr "Sitio Web" -#: forum/forms.py:183 +#: forum/forms.py:178 msgid "Location" msgstr "Ubicación" -#: forum/forms.py:184 +#: forum/forms.py:179 msgid "Date of birth" msgstr "Fecha de nacimiento" -#: forum/forms.py:184 +#: forum/forms.py:179 msgid "will not be shown, used to calculate age, format: YYYY-MM-DD" msgstr "no será mostrado, usado para calcular la edad. Formato: YYY-MM-DD" -#: forum/forms.py:185 templates/authopenid/settings.html:21 +#: forum/forms.py:180 templates/authopenid/settings.html:21 msgid "Profile" msgstr "Perfil" -#: forum/forms.py:212 forum/forms.py:213 +#: forum/forms.py:207 forum/forms.py:208 msgid "this email has already been registered, please use another one" msgstr "este email ya ha sido registrado, por favor use otro" -#: forum/models.py:253 -#, python-format +#: forum/models.py:246 msgid "%(author)s modified the question" msgstr "%(author)s modificó la pregunta" -#: forum/models.py:257 +#: forum/models.py:250 #, python-format msgid "%(people)s posted %(new_answer_count)s new answers" msgstr "%(people)s publicaron %(new_answer_count)s nuevas respuestas" -#: forum/models.py:262 +#: forum/models.py:255 #, python-format msgid "%(people)s commented the question" msgstr "%(people)s comentarion la pregunta" -#: forum/models.py:267 +#: forum/models.py:260 #, python-format msgid "%(people)s commented answers" msgstr "%(people)s comentaron la respuesta" -#: forum/models.py:269 +#: forum/models.py:262 #, python-format msgid "%(people)s commented an answer" msgstr "%(people)s comentaron la respuesta" -#: forum/models.py:313 forum/models.py:425 +#: forum/models.py:306 forum/models.py:418 msgid "revisions" msgstr "revisiones/" -#: forum/models.py:448 templates/badges.html:51 +#: forum/models.py:441 templates/badges.html:51 msgid "gold" msgstr "oro" -#: forum/models.py:449 templates/badges.html:59 +#: forum/models.py:442 templates/badges.html:59 msgid "silver" msgstr "plata" -#: forum/models.py:450 templates/badges.html:66 +#: forum/models.py:443 templates/badges.html:66 msgid "bronze" msgstr "bronce" @@ -724,27 +706,27 @@ msgstr "preferencias del usuario" msgid "profile - user preferences" msgstr "perfil - preferencia de " -#: forum/views.py:988 +#: forum/views.py:947 #, python-format msgid "subscription saved, %(email)s needs validation" msgstr "subscripción guardada, %(email)s necesita validación" -#: forum/views.py:1918 +#: forum/views.py:1860 msgid "uploading images is limited to users with >60 reputation points" msgstr "para subir imagenes debes tener más de 60 puntos de reputación" -#: forum/views.py:1920 +#: forum/views.py:1862 msgid "allowed file types are 'jpg', 'jpeg', 'gif', 'bmp', 'png', 'tiff'" msgstr "" "los tipos de archivos permitidos son 'jpg', 'jpeg', 'gif', 'bmp', 'png', " "'tiff'" -#: forum/views.py:1922 +#: forum/views.py:1864 #, python-format msgid "maximum upload file size is %sK" msgstr "tamaño máximo permitido es archivo %sK" -#: forum/views.py:1924 +#: forum/views.py:1866 #, python-format msgid "" "Error uploading file. Please contact the site administrator. Thank you. %s" @@ -868,22 +850,22 @@ msgid "select revision" msgstr "seleccionar revisión" #: templates/answer_edit.html:62 templates/ask.html:94 -#: templates/question.html:468 templates/question_edit.html:91 +#: templates/question.html:467 templates/question_edit.html:91 msgid "Toggle the real time Markdown editor preview" msgstr "Activar la visualización en tiempo real de Markdown" #: templates/answer_edit.html:62 templates/ask.html:94 -#: templates/question.html:468 templates/question_edit.html:91 +#: templates/question.html:467 templates/question_edit.html:91 msgid "toggle preview" msgstr "Activar previsualización" -#: templates/answer_edit.html:71 templates/question_edit.html:121 +#: templates/answer_edit.html:71 templates/question_edit.html:115 #: templates/question_retag.html:73 msgid "Save edit" msgstr "Guardar la edición" #: templates/answer_edit.html:72 templates/close.html:29 -#: templates/question_edit.html:122 templates/question_retag.html:74 +#: templates/question_edit.html:116 templates/question_retag.html:74 #: templates/reopen.html:30 templates/user_edit.html:83 #: templates/authopenid/changeemail.html:34 msgid "Cancel" @@ -974,16 +956,15 @@ msgstr "" "a>.
Puedes enviar tu pregunta ahora y validar tu email luego. Tu " "pregunta será guardada mientras tanto y publicada cuando valides tu email." -#: templates/ask.html:107 templates/ask.html.py:114 -#: templates/question_edit.html:117 +#: templates/ask.html:107 msgid "(required)" msgstr "(requerido)" -#: templates/ask.html:121 +#: templates/ask.html:114 msgid "Login/signup to post your question" msgstr "Iniciar sesión/registrarse para publicar su pregunta" -#: templates/ask.html:123 +#: templates/ask.html:116 msgid "Ask your question" msgstr "Haz tu pregunta" @@ -999,7 +980,7 @@ msgstr "Usuarios han sido galardonados con distinciones:" msgid "Badges summary" msgstr "Resumen de distinciones" -#: templates/badges.html:17 templates/user_stats.html:73 +#: templates/badges.html:17 templates/user_stats.html:115 msgid "Badges" msgstr "Distinciones" @@ -1100,17 +1081,17 @@ msgid "ask the author" msgstr "preguntar al autor" #: templates/book.html:88 templates/book.html.py:93 -#: templates/users_questions.html:18 +#: templates/users_questions.html:17 msgid "this question was selected as favorite" msgstr "esta pregunta ha sido seleccionada como favorita" #: templates/book.html:88 templates/book.html.py:93 -#: templates/users_questions.html:11 templates/users_questions.html.py:18 +#: templates/users_questions.html:11 templates/users_questions.html.py:17 msgid "number of times" msgstr "numero de veces" #: templates/book.html:105 templates/index.html:48 templates/questions.html:46 -#: templates/unanswered.html:37 templates/users_questions.html:32 +#: templates/unanswered.html:37 templates/users_questions.html:30 msgid "votes" msgstr "votos" @@ -1119,14 +1100,14 @@ msgid "the answer has been accepted to be correct" msgstr "la respuesta ha sido aceptada como correcta" #: templates/book.html:115 templates/index.html:49 templates/questions.html:47 -#: templates/unanswered.html:38 templates/users_questions.html:42 +#: templates/unanswered.html:38 templates/users_questions.html:40 msgid "views" msgstr "vistas" -#: templates/book.html:125 templates/index.html:69 templates/question.html:500 -#: templates/questions.html:84 templates/questions.html.py:157 +#: templates/book.html:125 templates/index.html:69 templates/question.html:499 +#: templates/questions.html:84 templates/questions.html.py:156 #: templates/tags.html:49 templates/unanswered.html:75 -#: templates/unanswered.html.py:106 templates/users_questions.html:54 +#: templates/unanswered.html.py:106 templates/users_questions.html:52 msgid "using tags" msgstr "usando etiquetas" @@ -1138,23 +1119,6 @@ msgstr "suscribirse al RSS del libro" msgid "subscribe to the questions feed" msgstr "suscribirse al agregado de noticias" -#: templates/categories.html:6 templates/categories.html.py:29 -msgid "Category list" -msgstr "Lista de Categorías" - -#: templates/categories.html:34 templates/tags.html:42 -msgid "Nothing found" -msgstr "Nada encontrado" - -#: templates/categories.html:40 -#, fuzzy -msgid "see questions that matches" -msgstr "ver preguntas etiquetadas" - -#: templates/categories.html:40 -msgid "category " -msgstr "categoría" - #: templates/close.html:6 templates/close.html.py:16 msgid "Close question" msgstr "Cerrar pregunta" @@ -1514,14 +1478,14 @@ msgid "all questions" msgstr "todas las preguntas" #: templates/index.html:47 templates/questions.html:45 -#: templates/unanswered.html:36 templates/users_questions.html:37 +#: templates/unanswered.html:36 templates/users_questions.html:35 msgid "answers" msgstr "respuestas" -#: templates/index.html:69 templates/question.html:500 -#: templates/questions.html:84 templates/questions.html.py:157 +#: templates/index.html:69 templates/question.html:499 +#: templates/questions.html:84 templates/questions.html.py:156 #: templates/tags.html:49 templates/unanswered.html:75 -#: templates/unanswered.html.py:106 templates/users_questions.html:54 +#: templates/unanswered.html.py:106 templates/users_questions.html:52 msgid "see questions tagged" msgstr "ver preguntas etiquetadas" @@ -1668,7 +1632,7 @@ msgid "i like this post (click again to cancel)" msgstr "Me gusta esta entrada (clickear devuelta para cancelar)" #: templates/question.html:75 templates/question.html.py:89 -#: templates/question.html:290 +#: templates/question.html:289 msgid "current number of votes" msgstr "número actual de votos" @@ -1687,171 +1651,169 @@ msgstr "" "remover marca de favorito a esta pregunta (clickear devuelta para volver a " "marcar)" -#: templates/question.html:128 templates/questions.html:87 -msgid "Category: " -msgstr "Categoría: " - -#: templates/question.html:135 templates/question.html.py:323 +#: templates/question.html:134 templates/question.html.py:322 #: templates/revisions_answer.html:53 templates/revisions_question.html:53 msgid "edit" msgstr "editar" -#: templates/question.html:139 templates/question.html.py:333 +#: templates/question.html:138 templates/question.html.py:332 msgid "delete" msgstr "borrar" -#: templates/question.html:144 +#: templates/question.html:143 msgid "reopen" msgstr "re-abrir" -#: templates/question.html:149 +#: templates/question.html:148 msgid "close" msgstr "cerrar" -#: templates/question.html:155 templates/question.html.py:346 +#: templates/question.html:154 templates/question.html.py:345 msgid "" "report as offensive (i.e containing spam, advertising, malicious text, etc.)" msgstr "" "reportar como ofensivo (ej. contiene spam, publicidad, texto malicioso, etc.)" -#: templates/question.html:156 templates/question.html.py:347 +#: templates/question.html:155 templates/question.html.py:346 msgid "flag offensive" msgstr "marcar como ofensivo" -#: templates/question.html:168 templates/question.html.py:356 +#: templates/question.html:167 templates/question.html.py:355 #: templates/revisions_answer.html:65 templates/revisions_question.html:65 msgid "updated" msgstr "actualizado" -#: templates/question.html:217 templates/question.html.py:403 +#: templates/question.html:216 templates/question.html.py:402 #: templates/revisions_answer.html:63 templates/revisions_question.html:63 msgid "asked" msgstr "preguntado" -#: templates/question.html:247 templates/question.html.py:430 +#: templates/question.html:246 templates/question.html.py:429 msgid "comments" msgstr "comentarios" -#: templates/question.html:248 templates/question.html.py:431 +#: templates/question.html:247 templates/question.html.py:430 msgid "add comment" msgstr "agregar comentario" -#: templates/question.html:261 -msgid "The question has been closed for the following reason" -msgstr "La pregunta fue cerrada por el siguiente motivo " - -#: templates/question.html:261 -msgid "by" -msgstr "por" +#: templates/question.html:260 +#, python-format +msgid "" +"The question has been closed for the following reason \"%(question." +"get_close_reason_display)s\" by" +msgstr "" +"La pregunta ha sido cerrada por el siguiente motivo \"%(question." +"get_close_reason_display)s\" por" -#: templates/question.html:263 -msgid "close date " -msgstr "fecha de cierre" +#: templates/question.html:262 +#, python-format +msgid "close date %(question.closed_at)s" +msgstr "fecha de cerrada %(question.closed_at)s" -#: templates/question.html:270 templates/user_stats.html:13 +#: templates/question.html:269 templates/user_stats.html:28 msgid "Answers" msgstr "Respuestas" -#: templates/question.html:272 +#: templates/question.html:271 msgid "oldest answers will be shown first" msgstr "la respuesta mas vieja será mostrada primero" -#: templates/question.html:272 +#: templates/question.html:271 msgid "oldest answers" msgstr "pregunta más vieja" -#: templates/question.html:273 +#: templates/question.html:272 msgid "newest answers will be shown first" msgstr "preguntas más nuevas serán mostradas primero" -#: templates/question.html:273 +#: templates/question.html:272 msgid "newest answers" msgstr "más nuevas" -#: templates/question.html:274 +#: templates/question.html:273 msgid "most voted answers will be shown first" msgstr "las preguntas más votadas serán mostradas primero" -#: templates/question.html:274 +#: templates/question.html:273 msgid "popular answers" msgstr "respuestas populares" -#: templates/question.html:288 templates/question.html.py:289 +#: templates/question.html:287 templates/question.html.py:288 msgid "i like this answer (click again to cancel)" msgstr "me gusta esta respuesta (clickear devuelta para cancelar)" -#: templates/question.html:295 templates/question.html.py:296 +#: templates/question.html:294 templates/question.html.py:295 msgid "i dont like this answer (click again to cancel)" msgstr "no me gusta esta respuesta (clickear devuelta para cancelar)" -#: templates/question.html:301 templates/question.html.py:302 +#: templates/question.html:300 templates/question.html.py:301 msgid "mark this answer as favorite (click again to undo)" msgstr "marcar esta respuesta como favorita (clickear devuelta para deshacer)" -#: templates/question.html:307 templates/question.html.py:308 +#: templates/question.html:306 templates/question.html.py:307 msgid "the author of the question has selected this answer as correct" msgstr "el autor de esta pregunta ha seleccionado esta respuesta como correcta" -#: templates/question.html:330 +#: templates/question.html:329 msgid "undelete" msgstr "deshacer eliminar" -#: templates/question.html:340 +#: templates/question.html:339 msgid "answer permanent link" msgstr "enlace permanente a respuesta" -#: templates/question.html:341 +#: templates/question.html:340 msgid "permanent link" msgstr "enlace permanente" -#: templates/question.html:454 +#: templates/question.html:453 msgid "Your answer" msgstr "Tu respuesta" -#: templates/question.html:457 +#: templates/question.html:456 msgid "you can answer anonymously and then login" msgstr "puedes responder de forma anónima y luego ingresar" -#: templates/question.html:480 +#: templates/question.html:479 msgid "Answer the question" msgstr "Responde la pregunta" -#: templates/question.html:482 +#: templates/question.html:481 msgid "Notify me daily if there are any new answers." msgstr "Notificarme diariamente si hay nuevas respuestas." -#: templates/question.html:484 +#: templates/question.html:483 msgid "once you sign in you will be able to subscribe for any updates here" msgstr "" "una vez que hayas ingresado podrás suscribirte a cualquiera de las " "actualizaciones aquí." -#: templates/question.html:495 +#: templates/question.html:494 msgid "Question tags" msgstr "Tags de la pregunta" -#: templates/question.html:505 +#: templates/question.html:504 msgid "question asked" msgstr "pregunta preguntada" -#: templates/question.html:505 templates/question.html.py:511 +#: templates/question.html:504 templates/question.html.py:510 #: templates/user_info.html:51 msgid "ago" msgstr " atras" -#: templates/question.html:508 +#: templates/question.html:507 msgid "question was seen" msgstr "la pregunta fue vista" -#: templates/question.html:508 +#: templates/question.html:507 msgid "times" msgstr "veces" -#: templates/question.html:511 +#: templates/question.html:510 msgid "last updated" msgstr "última vez actualizada" -#: templates/question.html:516 +#: templates/question.html:515 msgid "Related questions" msgstr "Preguntas relacionadas" @@ -1916,7 +1878,7 @@ msgstr "preguntas actualizadas más recientemente" msgid "active" msgstr "actividad" -#: templates/questions.html:110 +#: templates/questions.html:109 #, python-format msgid "" "\n" @@ -1935,7 +1897,7 @@ msgstr[1] "" "\t\t\ttiene un total de %(q_num)s preguntas etiquetadas con %(tagname)s\n" "\t\t\t" -#: templates/questions.html:117 +#: templates/questions.html:116 #, python-format msgid "" "\n" @@ -1954,7 +1916,7 @@ msgstr[1] "" "\t\t\thay un total de %(q_num)s pregunta que contiene %(searchtitle)s\n" "\t\t\t" -#: templates/questions.html:123 +#: templates/questions.html:122 #, fuzzy, python-format msgid "" "\n" @@ -1967,36 +1929,36 @@ msgid_plural "" msgstr[0] "ver preguntas etiquetadas '%(tagname)s'" msgstr[1] "ver pregunta etiquetada '%(tagname)s'" -#: templates/questions.html:132 +#: templates/questions.html:131 msgid "latest questions info" msgstr "Más recientes preguntas son mostradas primero." -#: templates/questions.html:136 +#: templates/questions.html:135 msgid "Questions are sorted by the time of last update." msgstr "" "Las preguntas estan ordenadas por fecha de último update." -#: templates/questions.html:137 +#: templates/questions.html:136 msgid "Most recently answered ones are shown first." msgstr "Las más recientemente respondidas son mostradas primero." -#: templates/questions.html:141 +#: templates/questions.html:140 msgid "Questions sorted by number of responses." msgstr "Preguntas ordenadas por número de respuestas." -#: templates/questions.html:142 +#: templates/questions.html:141 msgid "Most answered questions are shown first." msgstr "Preguntas más respondidas aparecen primero." -#: templates/questions.html:146 +#: templates/questions.html:145 msgid "Questions are sorted by the number of votes." msgstr "Las preguntas son ordenadas por el número de votos." -#: templates/questions.html:147 +#: templates/questions.html:146 msgid "Most voted questions are shown first." msgstr "Las preguntas más votadas son mostradas primero." -#: templates/questions.html:154 templates/unanswered.html:102 +#: templates/questions.html:153 templates/unanswered.html:102 msgid "Related tags" msgstr "Etiquetas relacionadas" @@ -2061,6 +2023,10 @@ msgstr "Todas las etiquetas que coincidan con la busqueda" msgid "all tags - make this empty in english" msgstr "todas las tags" +#: templates/tags.html:42 +msgid "Nothing found" +msgstr "Nada encontrado" + #: templates/unanswered.html:7 templates/unanswered.html.py:18 msgid "Unanswered questions" msgstr "Preguntas sin respuesta" @@ -2160,50 +2126,41 @@ msgstr "Enviar mis respuestas a Twitter" msgid "Save" msgstr "Guardar" -#: templates/user_stats.html:10 +#: templates/user_stats.html:16 msgid "User questions" msgstr "Preguntas del usuario" -#: templates/user_stats.html:20 +#: templates/user_stats.html:38 #, python-format msgid "the answer has been voted for %(vote_count)s times" msgstr "la respuesta ha sido votada %(vote_count)s veces" -#: templates/user_stats.html:20 +#: templates/user_stats.html:38 msgid "this answer has been selected as correct" msgstr "esta respuesta ha sido seleccionada como correcta" -#: templates/user_stats.html:28 +#: templates/user_stats.html:46 #, python-format msgid "the answer has been commented %(comment_count)s times" msgstr "la respuesta ha sido comentada %(comment_count)s veces" -#: templates/user_stats.html:36 -#, fuzzy -msgid "Votes" -msgstr "votos" - -#: templates/user_stats.html:41 -msgid "thumb up" -msgstr "" +#: templates/user_stats.html:60 +msgid "votes total" +msgstr "votos totales" -#: templates/user_stats.html:42 +#: templates/user_stats.html:69 msgid "user has voted up this many times" msgstr "el usuario ha votado positivo esta cantidad de veces" -#: templates/user_stats.html:46 -msgid "thumb down" -msgstr "" - -#: templates/user_stats.html:47 +#: templates/user_stats.html:74 msgid "user voted down this many times" msgstr "el usuario voto negativo esta cantidad de veces" -#: templates/user_stats.html:54 +#: templates/user_stats.html:87 msgid "Tags" msgstr "Etiquetas" -#: templates/user_stats.html:61 +#: templates/user_stats.html:97 #, python-format msgid "see other questions tagged '%(tag)s' " msgstr "ver otras preguntas etiqueteadas '%(tag)s'" @@ -2220,11 +2177,6 @@ msgstr "gráfica de la reputación del usuario" msgid "reputation history" msgstr "historial de reputación" -#: templates/user_tabs.html:23 -#, fuzzy -msgid "questions that user selected as his/her favorite" -msgstr "esta pregunta ha sido seleccionada como favorita" - #: templates/user_tabs.html:24 msgid "favorites" msgstr "favoritos" @@ -2262,15 +2214,7 @@ msgstr "Nada encontrado." msgid "this questions was selected as favorite" msgstr "esta pregunta ha sido seleccionada como favorita" -#: templates/users_questions.html:12 -msgid "thumb-up on" -msgstr "" - -#: templates/users_questions.html:19 -msgid "thumb-up off" -msgstr "" - -#: templates/users_questions.html:35 +#: templates/users_questions.html:33 msgid "this answer has been accepted to be correct" msgstr "esta respuesta ha sido aceptada como correcta" @@ -2296,7 +2240,7 @@ msgid "Your new Email" msgstr "Tu nuevo Email" #: templates/authopenid/changeemail.html:31 -#: templates/authopenid/signin.html:137 +#: templates/authopenid/signin.html:136 msgid "Password" msgstr "Contraseña" @@ -2404,7 +2348,7 @@ msgid "This account already exists, please use another." msgstr "Esta cuenta ya existe, por favor usar otra." #: templates/authopenid/complete.html:19 templates/authopenid/complete.html:32 -#: templates/authopenid/sendpw.html:16 templates/authopenid/signin.html:121 +#: templates/authopenid/signin.html:120 msgid "Sorry, looks like we have some errors:" msgstr "Ups, parece que hay errores:" @@ -2436,10 +2380,40 @@ msgstr "contraseña" msgid "Register" msgstr "Registrarse" -#: templates/authopenid/complete.html:62 templates/authopenid/signin.html:139 +#: templates/authopenid/complete.html:62 templates/authopenid/signin.html:138 msgid "Forgot your password?" msgstr "¿Olvidaste tu contraseña?" +#: templates/authopenid/confirm_email.txt:2 +msgid "Thank you for registering at our Q&A forum!" +msgstr "" + +#: templates/authopenid/confirm_email.txt:4 +msgid "Your account details are:" +msgstr "" + +#: templates/authopenid/confirm_email.txt:6 +#: templates/authopenid/sendpw_email.txt:7 +msgid "Username:" +msgstr "Nombre de usuario:" + +#: templates/authopenid/confirm_email.txt:7 +#: templates/authopenid/delete.html:20 +msgid "Password:" +msgstr "Contraseña" + +#: templates/authopenid/confirm_email.txt:9 +msgid "Please sign in here:" +msgstr "" + +#: templates/authopenid/confirm_email.txt:12 +#: templates/authopenid/email_validation.txt:14 +#: templates/authopenid/sendpw_email.txt:13 +msgid "" +"Sincerely,\n" +"Forum Administrator" +msgstr "" + #: templates/authopenid/delete.html:9 msgid "Account: delete account" msgstr "Cuenta: borrar cuenta" @@ -2456,10 +2430,6 @@ msgstr "" msgid "Check confirm box, if you want delete your account." msgstr "Marca caja de confirmación, si deseas borrar tu cuenta." -#: templates/authopenid/delete.html:20 -msgid "Password:" -msgstr "Contraseña" - #: templates/authopenid/delete.html:32 msgid "I am sure I want to delete my account." msgstr "Estoy seguro que quiero borrar mi cuenta." @@ -2476,6 +2446,25 @@ msgstr "(requerido por tu seguridad)" msgid "Delete account permanently" msgstr "Borrar la cuenta de forma permanente" +#: templates/authopenid/email_validation.txt:2 +msgid "Greetings from the Q&A forum" +msgstr "" + +#: templates/authopenid/email_validation.txt:4 +msgid "To make use of the Forum, please follow the link below:" +msgstr "" + +#: templates/authopenid/email_validation.txt:8 +msgid "Following the link above will help us verify your email address." +msgstr "" + +#: templates/authopenid/email_validation.txt:10 +msgid "" +"If you beleive that this message was sent in mistake - \n" +"no further action is needed. Just ingore this email, we apologize\n" +"for any inconvenience" +msgstr "" + #: templates/authopenid/sendpw.html:4 templates/authopenid/sendpw.html.py:8 msgid "Send new password" msgstr "Enviar nueva contraseña" @@ -2512,6 +2501,26 @@ msgstr "" "Nota: tu nueva contraseña solo será activada luego de que hagas click en el " "link de activación en el email enviado." +#: templates/authopenid/sendpw_email.txt:2 +#, python-format +msgid "" +"Someone has requested to reset your password on %(site_url)s.\n" +"If it were not you, it is safe to ignore this email." +msgstr "" + +#: templates/authopenid/sendpw_email.txt:5 +msgid "Your new account details are:" +msgstr "" + +#: templates/authopenid/sendpw_email.txt:8 +#, fuzzy +msgid "New password:" +msgstr "Nueva contraseña:" + +#: templates/authopenid/sendpw_email.txt:10 +msgid "To confirm that you wanted to reset your password please visit:" +msgstr "" + #: templates/authopenid/settings.html:30 msgid "Give your account a new password." msgstr "Crea una nueva contraseña para tu cuenta." @@ -2580,7 +2589,7 @@ msgstr "" "Ingresa tu dirección (URL) de OpenID" -#: templates/authopenid/signin.html:112 templates/authopenid/signin.html:138 +#: templates/authopenid/signin.html:112 templates/authopenid/signin.html:137 msgid "Login" msgstr "Ingresar" @@ -2588,48 +2597,48 @@ msgstr "Ingresar" msgid "we support two login modes" msgstr "soportamos dos tipos de ingreso" -#: templates/authopenid/signin.html:135 +#: templates/authopenid/signin.html:134 msgid "Use login name and password" msgstr "Nombre de usuario y contraseña" -#: templates/authopenid/signin.html:136 +#: templates/authopenid/signin.html:135 msgid "Login name" msgstr "Nombre de usuario" -#: templates/authopenid/signin.html:140 +#: templates/authopenid/signin.html:139 msgid "Create new account" msgstr "Crear cuenta nueva" -#: templates/authopenid/signin.html:149 +#: templates/authopenid/signin.html:148 msgid "Why use OpenID?" msgstr "¿Porqué usar OpenID?" -#: templates/authopenid/signin.html:152 +#: templates/authopenid/signin.html:151 msgid "with openid it is easier" msgstr "Con OpenID no necesitas crear un nuevo nombre de usuario y contraseña." -#: templates/authopenid/signin.html:155 +#: templates/authopenid/signin.html:154 msgid "reuse openid" msgstr "" "Puedes de forma segura re-usar el mismo nombre de usuario para todos los " "sitios que acepten OpenID." -#: templates/authopenid/signin.html:158 +#: templates/authopenid/signin.html:157 msgid "openid is widely adopted" msgstr "" "OpenID es extensamente usado. Hay más de 160,000,000 cuentas de OpenID en " "uso en el mundo. Mas de 10,000 sitios aceptan OpenID." -#: templates/authopenid/signin.html:161 +#: templates/authopenid/signin.html:160 msgid "openid is supported open standard" msgstr "" "OpenID es basado en un standard abierto, apoyado por muchas organizaciones." -#: templates/authopenid/signin.html:166 +#: templates/authopenid/signin.html:165 msgid "Find out more" msgstr "Averigua más" -#: templates/authopenid/signin.html:167 +#: templates/authopenid/signin.html:166 msgid "Get OpenID" msgstr "Adquiere una OpenID" @@ -2673,20 +2682,6 @@ msgstr "Registrate con tu OpenID" msgid "Login with your OpenID" msgstr "Ingresar con tu OpenID" -#, fuzzy -#~ msgid "complete list of quesionts" -#~ msgstr "lista completa de preguntas" - -#~ msgid "votes total" -#~ msgstr "votos totales" - -#~ msgid "Username:" -#~ msgstr "Nombre de usuario:" - -#, fuzzy -#~ msgid "New password:" -#~ msgstr "Nueva contraseña:" - #~ msgid "site title" #~ msgstr "Preguntalo.com.uy" diff --git a/middleware/anon_user.py b/middleware/anon_user.py new file mode 100644 index 00000000..c7ff05bc --- /dev/null +++ b/middleware/anon_user.py @@ -0,0 +1,26 @@ +from django.http import HttpResponseRedirect +from django_authopenid.util import get_next_url +from user_messages import create_message, get_and_delete_messages +import logging + +class AnonymousMessageManager(object): + def __init__(self,request): + self.request = request + def create(self,message=''): + create_message(self.request,message) + def get_and_delete(self): + messages = get_and_delete_messages(self.request) + return messages + +def dummy_deepcopy(*arg): + """this is necessary to prevent deepcopy() on anonymous user object + that now contains reference to request, which cannot be deepcopied + """ + return None + +class ConnectToSessionMessagesMiddleware(object): + def process_request(self, request): + if not request.user.is_authenticated(): + request.user.__deepcopy__ = dummy_deepcopy #plug on deepcopy which may be called by django db "driver" + request.user.message_set = AnonymousMessageManager(request) #here request is linked to anon user + request.user.get_and_delete_messages = request.user.message_set.get_and_delete diff --git a/middleware/cancel.py b/middleware/cancel.py new file mode 100644 index 00000000..f03ff35e --- /dev/null +++ b/middleware/cancel.py @@ -0,0 +1,15 @@ +from django.http import HttpResponseRedirect +from django_authopenid.util import get_next_url +import logging +class CancelActionMiddleware(object): + def process_view(self, request, view_func, view_args, view_kwargs): + if 'cancel' in request.REQUEST: + #todo use session messages for the anonymous users + try: + msg = getattr(view_func,'CANCEL_MESSAGE') + except AttributeError: + msg = 'action canceled' + request.user.message_set.create(message=msg) + return HttpResponseRedirect(get_next_url(request)) + else: + return None diff --git a/middleware/pagesize.py b/middleware/pagesize.py index bb6c7aa3..f6e6fcfd 100644 --- a/middleware/pagesize.py +++ b/middleware/pagesize.py @@ -26,4 +26,8 @@ class QuestionsPageSizeMiddleware(object): user.questions_per_page = pagesize user.save() # put pagesize into session - request.session["pagesize"] = pagesize \ No newline at end of file + request.session["pagesize"] = pagesize + + def process_exception(self,request,exception): + import logging + logging.debug('have exception %s' % str(exception)) diff --git a/migration b/migration new file mode 100644 index 00000000..eb5dffa1 --- /dev/null +++ b/migration @@ -0,0 +1,7 @@ +cp cnprog-current/templates/content/style/style.css test/templates/content/style/ +cp cnprog-current/templates/footer.html test/templates/ +cp cnprog-current/templates/content/images/logo.png test/templates/content/images +cp cnprog-current/locale/en/LC_MESSAGES/django.po test/locale/en/LC_MESSAGES/ +python manage.py makemessages -l en -e html,py,txt +#fix fuzzy messages +python manage.py compilemessages diff --git a/session_messages/.svn/all-wcprops b/session_messages/.svn/all-wcprops new file mode 100644 index 00000000..2a15b353 --- /dev/null +++ b/session_messages/.svn/all-wcprops @@ -0,0 +1,23 @@ +K 25 +svn:wc:ra_dav:version-url +V 38 +/svn/!svn/ver/5/trunk/session_messages +END +__init__.py +K 25 +svn:wc:ra_dav:version-url +V 50 +/svn/!svn/ver/5/trunk/session_messages/__init__.py +END +models.py +K 25 +svn:wc:ra_dav:version-url +V 48 +/svn/!svn/ver/2/trunk/session_messages/models.py +END +context_processors.py +K 25 +svn:wc:ra_dav:version-url +V 60 +/svn/!svn/ver/2/trunk/session_messages/context_processors.py +END diff --git a/session_messages/.svn/dir-prop-base b/session_messages/.svn/dir-prop-base new file mode 100644 index 00000000..4cc643b7 --- /dev/null +++ b/session_messages/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +*.pyc + +END diff --git a/session_messages/.svn/entries b/session_messages/.svn/entries new file mode 100644 index 00000000..67c0db8a --- /dev/null +++ b/session_messages/.svn/entries @@ -0,0 +1,64 @@ +8 + +dir +5 +http://django-session-messages.googlecode.com/svn/trunk/session_messages +http://django-session-messages.googlecode.com/svn + + + +2009-03-10T23:30:03.043791Z +5 +carl.j.meyer +has-props + +svn:special svn:externals svn:needs-lock + + + + + + + + + + + +b8288d2d-7354-0410-af5b-714f73743f4b + +__init__.py +file + + + + +2009-10-25T23:36:02.000000Z +89aa0f71c9973e4889e5fad0b4771a34 +2009-03-10T23:30:03.043791Z +5 +carl.j.meyer + +models.py +file + + + + +2009-10-25T23:36:02.000000Z +c5b4f274dbb06bc66a14f0c18c9115cd +2008-08-14T23:13:23.180432Z +2 +carl.j.meyer + +context_processors.py +file + + + + +2009-10-25T23:36:02.000000Z +24779c7e504d3f7f1918fdf3fe8096bc +2008-08-14T23:13:23.180432Z +2 +carl.j.meyer + diff --git a/session_messages/.svn/format b/session_messages/.svn/format new file mode 100644 index 00000000..45a4fb75 --- /dev/null +++ b/session_messages/.svn/format @@ -0,0 +1 @@ +8 diff --git a/session_messages/.svn/text-base/__init__.py.svn-base b/session_messages/.svn/text-base/__init__.py.svn-base new file mode 100644 index 00000000..0136c888 --- /dev/null +++ b/session_messages/.svn/text-base/__init__.py.svn-base @@ -0,0 +1,36 @@ +""" +Lightweight session-based messaging system. + +Time-stamp: <2009-03-10 19:22:29 carljm __init__.py> + +""" +VERSION = (0, 1, 'pre') + +def create_message (request, message): + """ + Create a message in the current session. + + """ + assert hasattr(request, 'session'), "django-session-messages requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." + + try: + request.session['messages'].append(message) + except KeyError: + request.session['messages'] = [message] + +def get_and_delete_messages (request, include_auth=False): + """ + Get and delete all messages for current session. + + Optionally also fetches user messages from django.contrib.auth. + + """ + assert hasattr(request, 'session'), "django-session-messages requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." + + messages = request.session.pop('messages', []) + + if include_auth and request.user.is_authenticated(): + messages.extend(request.user.get_and_delete_messages()) + + return messages + diff --git a/session_messages/.svn/text-base/context_processors.py.svn-base b/session_messages/.svn/text-base/context_processors.py.svn-base new file mode 100644 index 00000000..df9840fd --- /dev/null +++ b/session_messages/.svn/text-base/context_processors.py.svn-base @@ -0,0 +1,48 @@ +""" +Context processor for lightweight session messages. + +Time-stamp: <2008-07-19 23:16:19 carljm context_processors.py> + +""" +from django.utils.encoding import StrAndUnicode + +from session_messages import get_and_delete_messages + +def session_messages (request): + """ + Returns session messages for the current session. + + """ + return { 'session_messages': LazyMessages(request) } + +class LazyMessages (StrAndUnicode): + """ + Lazy message container, so messages aren't actually retrieved from + session and deleted until the template asks for them. + + """ + def __init__(self, request): + self.request = request + + def __iter__(self): + return iter(self.messages) + + def __len__(self): + return len(self.messages) + + def __nonzero__(self): + return bool(self.messages) + + def __unicode__(self): + return unicode(self.messages) + + def __getitem__(self, *args, **kwargs): + return self.messages.__getitem__(*args, **kwargs) + + def _get_messages(self): + if hasattr(self, '_messages'): + return self._messages + self._messages = get_and_delete_messages(self.request) + return self._messages + messages = property(_get_messages) + diff --git a/session_messages/.svn/text-base/models.py.svn-base b/session_messages/.svn/text-base/models.py.svn-base new file mode 100644 index 00000000..b67ead6d --- /dev/null +++ b/session_messages/.svn/text-base/models.py.svn-base @@ -0,0 +1,3 @@ +""" +blank models.py +""" diff --git a/session_messages/__init__.py b/session_messages/__init__.py new file mode 100644 index 00000000..4dd10a6b --- /dev/null +++ b/session_messages/__init__.py @@ -0,0 +1,37 @@ +""" +Lightweight session-based messaging system. + +Time-stamp: <2009-03-10 19:22:29 carljm __init__.py> + +""" +VERSION = (0, 1, 'pre') + +def create_message (request, message): + """ + Create a message in the current session. + + """ + assert hasattr(request, 'session'), "django-session-messages requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." + + try: + request.session['messages'].append(message) + except KeyError: + request.session['messages'] = [message] + +def get_and_delete_messages (request, include_auth=False): + """ + Get and delete all messages for current session. + + Optionally also fetches user messages from django.contrib.auth. + + """ + assert hasattr(request, 'session'), "django-session-messages requires session middleware to be installed. Edit your MIDDLEWARE_CLASSES setting to insert 'django.contrib.sessions.middleware.SessionMiddleware'." + + messages = request.session.pop('messages', []) + import logging + + if include_auth and request.user.is_authenticated(): + messages.extend(request.user.get_and_delete_messages()) + + return messages + diff --git a/session_messages/context_processors.py b/session_messages/context_processors.py new file mode 100644 index 00000000..df9840fd --- /dev/null +++ b/session_messages/context_processors.py @@ -0,0 +1,48 @@ +""" +Context processor for lightweight session messages. + +Time-stamp: <2008-07-19 23:16:19 carljm context_processors.py> + +""" +from django.utils.encoding import StrAndUnicode + +from session_messages import get_and_delete_messages + +def session_messages (request): + """ + Returns session messages for the current session. + + """ + return { 'session_messages': LazyMessages(request) } + +class LazyMessages (StrAndUnicode): + """ + Lazy message container, so messages aren't actually retrieved from + session and deleted until the template asks for them. + + """ + def __init__(self, request): + self.request = request + + def __iter__(self): + return iter(self.messages) + + def __len__(self): + return len(self.messages) + + def __nonzero__(self): + return bool(self.messages) + + def __unicode__(self): + return unicode(self.messages) + + def __getitem__(self, *args, **kwargs): + return self.messages.__getitem__(*args, **kwargs) + + def _get_messages(self): + if hasattr(self, '_messages'): + return self._messages + self._messages = get_and_delete_messages(self.request) + return self._messages + messages = property(_get_messages) + diff --git a/session_messages/models.py b/session_messages/models.py new file mode 100644 index 00000000..b67ead6d --- /dev/null +++ b/session_messages/models.py @@ -0,0 +1,3 @@ +""" +blank models.py +""" diff --git a/settings.py b/settings.py index fcfdb548..daada933 100644 --- a/settings.py +++ b/settings.py @@ -1,25 +1,23 @@ # encoding:utf-8 # Django settings for lanai project. import os.path +import sys +sys.path.insert(0,'/home/fadeev/incoming/Django-1.1.1') #DEBUG SETTINGS -DEBUG = True +DEBUG = False TEMPLATE_DEBUG = DEBUG -INTERNAL_IPS = ('127.0.0.1',) - -#for OpenID auth -ugettext = lambda s: s -LOGIN_URL = '/%s%s' % (ugettext('account/'), ugettext('signin/')) +INTERNAL_IPS = ('127.0.0.1','128.200.203.33') #EMAIL AND ADMINS ADMINS = ( - ('CNProg team', 'team@cnprog.com'), + ('Evgeny Fadeev', 'evgeny.fadeev@gmail.com'), ) MANAGERS = ADMINS SITE_ID = 1 -ADMIN_MEDIA_PREFIX = '/admin/media/' +ADMIN_MEDIA_PREFIX = '/forum/admin/media/' SECRET_KEY = '$oo^&_m&qwbib=(_4m_n*zn-d=g#s0he5fx9xonnym#8p6yigm' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( @@ -36,16 +34,18 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.transaction.TransactionMiddleware', #'django.middleware.sqlprint.SqlPrintingMiddleware', - #'middleware.pagesize.QuestionsPageSizeMiddleware', + 'middleware.anon_user.ConnectToSessionMessagesMiddleware', + 'middleware.pagesize.QuestionsPageSizeMiddleware', + 'middleware.cancel.CancelActionMiddleware', #'debug_toolbar.middleware.DebugToolbarMiddleware', ) TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.request', - 'context.auth_processor', 'context.application_settings', #'django.core.context_processors.i18n', - 'django.core.context_processors.auth' #this is required for admin + 'user_messages.context_processors.user_messages',#must be before auth + 'django.core.context_processors.auth', #this is required for admin ) ROOT_URLCONF = 'urls' @@ -73,7 +73,8 @@ INSTALLED_APPS = ( 'django.contrib.humanize', 'forum', 'django_authopenid', - 'debug_toolbar' , + #'debug_toolbar' , + 'user_messages', ) # User settings diff --git a/settings_local.py.dist b/settings_local.py.dist index 48d7b567..e43522ef 100644 --- a/settings_local.py.dist +++ b/settings_local.py.dist @@ -1,5 +1,6 @@ # encoding:utf-8 import os.path +from django.utils.translation import ugettext as _ SITE_SRC_ROOT = os.path.dirname(__file__) LOG_FILENAME = 'django.lanai.log' @@ -8,40 +9,60 @@ LOG_FILENAME = 'django.lanai.log' import logging logging.basicConfig(filename=os.path.join(SITE_SRC_ROOT, 'log', LOG_FILENAME), level=logging.DEBUG,) -DATABASE_NAME = '' # Or path to database file if using sqlite3. +DATABASE_NAME = 'cnprog' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. -DATABASE_ENGINE = '' #mysql, etc +DATABASE_ENGINE = 'mysql' #mysql, etc #Moved from settings.py for better organization. (please check it up to clean up settings.py) #email server settings SERVER_EMAIL = '' -DEFAULT_FROM_EMAIL = 'team@cnprog.com' +DEFAULT_FROM_EMAIL = '' EMAIL_HOST_USER = '' EMAIL_HOST_PASSWORD = '' -EMAIL_SUBJECT_PREFIX = '[cnprog.com]' -EMAIL_HOST='smtp.gmail.com' -EMAIL_PORT='587' -EMAIL_USE_TLS=True +EMAIL_SUBJECT_PREFIX = '[CNPROG] ' +EMAIL_HOST='cnprog.com' +EMAIL_PORT='25' +EMAIL_USE_TLS=False #LOCALIZATIONS -TIME_ZONE = 'Asia/Chongqing Asia/Chungking' -# LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'America/Tijuana' #OTHER SETTINGS -APP_TITLE = u'CNProg.com 程序员问答社区' -APP_KEYWORDS = u'技术问答社区,中国程序员,编程技术社区,程序员社区,程序员论坛,程序员wiki,程序员博客' -APP_DESCRIPTION = u'中国程序员的编程技术问答社区。我们做专业的、可协作编辑的技术问答社区。' -APP_INTRO = u'

CNProg是一个面向程序员的可协作编辑的开放源代码问答社区

您可以在这里提问各类程序技术问题 - 问题不分语言和平台。 同时也希望您对力所能及的问题,给予您的宝贵答案。

' -APP_COPYRIGHT = 'Copyright CNPROG.COM 2009' +APP_TITLE = u'CNPROG Q&A Forum' +APP_KEYWORDS = u'CNPROG,forum,community' +APP_DESCRIPTION = u'Ask and answer questions.' +APP_INTRO = u'

Ask and answer questions, make the world better!

' +APP_COPYRIGHT = 'Copyright CNPROG, 2009. Some rights reserved under creative commons license.' + +########################### +# +# this will allow running your forum with url like http://site.com/forum +# +# FORUM_SCRIPT_ALIAS = 'forum/' +# +# also make sure to set '/':'/forum' in file templates/content/js/com.cnprog.i18n.js +# this is necessary to make client scripts work in this configuration +# +FORUM_SCRIPT_ALIAS = '' #no leading slash, default = '' empty string USE_I18N = True LANGUAGE_CODE = 'en' -EMAIL_VALIDATION = 'off' +EMAIL_VALIDATION = 'off' #string - on|off MIN_USERNAME_LENGTH = 1 EMAIL_UNIQUE = False -APP_URL = 'http://server.com' #used by email notif system and RSS +APP_URL = 'http://cnprog.com' #used by email notif system and RSS GOOGLE_SITEMAP_CODE = '55uGNnQVJW8p1bbXeF/Xbh9I7nZBM/wLhRz6N/I1kkA=' GOOGLE_ANALYTICS_KEY = '' -BOOKS_ON = True +BOOKS_ON = False +WIKI_ON = True +USE_EXTERNAL_LEGACY_LOGIN = True +EXTERNAL_LEGACY_LOGIN_HOST = 'login.cnprog.com' +EXTERNAL_LEGACY_LOGIN_PORT = 80 +EXTERNAL_LEGACY_LOGIN_PROVIDER_NAME = 'CNPROG' +FEEDBACK_SITE_URL = None #None or url +LOGIN_URL = '/%s%s%s' % (FORUM_SCRIPT_ALIAS,'account/','signin/') + +DJANGO_VERSION = 1.1 +RESOURCE_REVISION=4 diff --git a/sql_scripts/091111_upgrade_evgeny.sql b/sql_scripts/091111_upgrade_evgeny.sql new file mode 100644 index 00000000..cb76ec3c --- /dev/null +++ b/sql_scripts/091111_upgrade_evgeny.sql @@ -0,0 +1 @@ +ALTER TABLE `auth_user` add column is_approved tinyint(1) not NULL; diff --git a/tables.sql b/tables.sql new file mode 100644 index 00000000..6034c08c --- /dev/null +++ b/tables.sql @@ -0,0 +1,440 @@ +BEGIN; +CREATE TABLE `forum_emailfeedsetting` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `subscriber_id` integer NOT NULL, + `feed_type` varchar(16) NOT NULL, + `frequency` varchar(8) NOT NULL, + `added_at` datetime NOT NULL, + `reported_at` datetime NULL +) +; +ALTER TABLE `forum_emailfeedsetting` ADD CONSTRAINT subscriber_id_refs_id_6fee6730cc813af8 FOREIGN KEY (`subscriber_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `tag` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `name` varchar(255) NOT NULL UNIQUE, + `created_by_id` integer NOT NULL, + `deleted` bool NOT NULL, + `deleted_at` datetime NULL, + `deleted_by_id` integer NULL, + `used_count` integer UNSIGNED NOT NULL +) +; +ALTER TABLE `tag` ADD CONSTRAINT created_by_id_refs_id_6ae4d97547205d6d FOREIGN KEY (`created_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `tag` ADD CONSTRAINT deleted_by_id_refs_id_6ae4d97547205d6d FOREIGN KEY (`deleted_by_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `comment` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `content_type_id` integer NOT NULL, + `object_id` integer UNSIGNED NOT NULL, + `user_id` integer NOT NULL, + `comment` varchar(300) NOT NULL, + `added_at` datetime NOT NULL +) +; +ALTER TABLE `comment` ADD CONSTRAINT content_type_id_refs_id_89a4b13ec5a7994 FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +ALTER TABLE `comment` ADD CONSTRAINT user_id_refs_id_5ba842626be725e8 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `vote` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `content_type_id` integer NOT NULL, + `object_id` integer UNSIGNED NOT NULL, + `user_id` integer NOT NULL, + `vote` smallint NOT NULL, + `voted_at` datetime NOT NULL, + UNIQUE (`content_type_id`, `object_id`, `user_id`) +) +; +ALTER TABLE `vote` ADD CONSTRAINT content_type_id_refs_id_77dc6ffafedbbec FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +ALTER TABLE `vote` ADD CONSTRAINT user_id_refs_id_3ce5b20589f5b210 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `flagged_item` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `content_type_id` integer NOT NULL, + `object_id` integer UNSIGNED NOT NULL, + `user_id` integer NOT NULL, + `flagged_at` datetime NOT NULL, + UNIQUE (`content_type_id`, `object_id`, `user_id`) +) +; +ALTER TABLE `flagged_item` ADD CONSTRAINT content_type_id_refs_id_261d26c8891bb28c FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +ALTER TABLE `flagged_item` ADD CONSTRAINT user_id_refs_id_92ae9d35e3c608 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `question` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `title` varchar(300) NOT NULL, + `author_id` integer NOT NULL, + `added_at` datetime NOT NULL, + `wiki` bool NOT NULL, + `wikified_at` datetime NULL, + `answer_accepted` bool NOT NULL, + `closed` bool NOT NULL, + `closed_by_id` integer NULL, + `closed_at` datetime NULL, + `close_reason` smallint NULL, + `deleted` bool NOT NULL, + `deleted_at` datetime NULL, + `deleted_by_id` integer NULL, + `locked` bool NOT NULL, + `locked_by_id` integer NULL, + `locked_at` datetime NULL, + `score` integer NOT NULL, + `vote_up_count` integer NOT NULL, + `vote_down_count` integer NOT NULL, + `answer_count` integer UNSIGNED NOT NULL, + `comment_count` integer UNSIGNED NOT NULL, + `view_count` integer UNSIGNED NOT NULL, + `offensive_flag_count` smallint NOT NULL, + `favourite_count` integer UNSIGNED NOT NULL, + `last_edited_at` datetime NULL, + `last_edited_by_id` integer NULL, + `last_activity_at` datetime NOT NULL, + `last_activity_by_id` integer NOT NULL, + `tagnames` varchar(125) NOT NULL, + `summary` varchar(180) NOT NULL, + `html` longtext NOT NULL +) +; +ALTER TABLE `question` ADD CONSTRAINT author_id_refs_id_5159d9f3a9162ff4 FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `question` ADD CONSTRAINT closed_by_id_refs_id_5159d9f3a9162ff4 FOREIGN KEY (`closed_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `question` ADD CONSTRAINT deleted_by_id_refs_id_5159d9f3a9162ff4 FOREIGN KEY (`deleted_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `question` ADD CONSTRAINT locked_by_id_refs_id_5159d9f3a9162ff4 FOREIGN KEY (`locked_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `question` ADD CONSTRAINT last_edited_by_id_refs_id_5159d9f3a9162ff4 FOREIGN KEY (`last_edited_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `question` ADD CONSTRAINT last_activity_by_id_refs_id_5159d9f3a9162ff4 FOREIGN KEY (`last_activity_by_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `forum_questionview` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `who_id` integer NOT NULL, + `when` datetime NOT NULL +) +; +ALTER TABLE `forum_questionview` ADD CONSTRAINT question_id_refs_id_fe63ebce6b3cbac FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `forum_questionview` ADD CONSTRAINT who_id_refs_id_293b67239e957c53 FOREIGN KEY (`who_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `favorite_question` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `user_id` integer NOT NULL, + `added_at` datetime NOT NULL +) +; +ALTER TABLE `favorite_question` ADD CONSTRAINT question_id_refs_id_2cafd2f21ebe1cc3 FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `favorite_question` ADD CONSTRAINT user_id_refs_id_1632ce11ad7ac7de FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `question_revision` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `revision` integer UNSIGNED NOT NULL, + `title` varchar(300) NOT NULL, + `author_id` integer NOT NULL, + `revised_at` datetime NOT NULL, + `tagnames` varchar(125) NOT NULL, + `summary` varchar(300) NOT NULL, + `text` longtext NOT NULL +) +; +ALTER TABLE `question_revision` ADD CONSTRAINT question_id_refs_id_61316ec87bef5296 FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `question_revision` ADD CONSTRAINT author_id_refs_id_79de7cc0b077fdb1 FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `forum_anonymousanswer` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `session_key` varchar(40) NOT NULL, + `wiki` bool NOT NULL, + `added_at` datetime NOT NULL, + `ip_addr` char(15) NOT NULL, + `author_id` integer NULL, + `text` longtext NOT NULL, + `summary` varchar(180) NOT NULL +) +; +ALTER TABLE `forum_anonymousanswer` ADD CONSTRAINT question_id_refs_id_17dd6b2f4cc171c7 FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `forum_anonymousanswer` ADD CONSTRAINT author_id_refs_id_3ac41be013fb542e FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `forum_anonymousquestion` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `title` varchar(300) NOT NULL, + `session_key` varchar(40) NOT NULL, + `text` longtext NOT NULL, + `summary` varchar(180) NOT NULL, + `tagnames` varchar(125) NOT NULL, + `wiki` bool NOT NULL, + `added_at` datetime NOT NULL, + `ip_addr` char(15) NOT NULL, + `author_id` integer NULL +) +; +ALTER TABLE `forum_anonymousquestion` ADD CONSTRAINT author_id_refs_id_2a673297511a98a FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `answer` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `author_id` integer NOT NULL, + `added_at` datetime NOT NULL, + `wiki` bool NOT NULL, + `wikified_at` datetime NULL, + `accepted` bool NOT NULL, + `accepted_at` datetime NULL, + `deleted` bool NOT NULL, + `deleted_by_id` integer NULL, + `locked` bool NOT NULL, + `locked_by_id` integer NULL, + `locked_at` datetime NULL, + `score` integer NOT NULL, + `vote_up_count` integer NOT NULL, + `vote_down_count` integer NOT NULL, + `comment_count` integer UNSIGNED NOT NULL, + `offensive_flag_count` smallint NOT NULL, + `last_edited_at` datetime NULL, + `last_edited_by_id` integer NULL, + `html` longtext NOT NULL +) +; +ALTER TABLE `answer` ADD CONSTRAINT question_id_refs_id_2300e0297d6550c9 FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `answer` ADD CONSTRAINT author_id_refs_id_6573e62f192b0170 FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `answer` ADD CONSTRAINT deleted_by_id_refs_id_6573e62f192b0170 FOREIGN KEY (`deleted_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `answer` ADD CONSTRAINT locked_by_id_refs_id_6573e62f192b0170 FOREIGN KEY (`locked_by_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `answer` ADD CONSTRAINT last_edited_by_id_refs_id_6573e62f192b0170 FOREIGN KEY (`last_edited_by_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `answer_revision` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `answer_id` integer NOT NULL, + `revision` integer UNSIGNED NOT NULL, + `author_id` integer NOT NULL, + `revised_at` datetime NOT NULL, + `summary` varchar(300) NOT NULL, + `text` longtext NOT NULL +) +; +ALTER TABLE `answer_revision` ADD CONSTRAINT answer_id_refs_id_47145eaebe77d8fe FOREIGN KEY (`answer_id`) REFERENCES `answer` (`id`); +ALTER TABLE `answer_revision` ADD CONSTRAINT author_id_refs_id_2c17693c3ccc055f FOREIGN KEY (`author_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `badge` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `name` varchar(50) NOT NULL, + `type` smallint NOT NULL, + `slug` varchar(50) NOT NULL, + `description` varchar(300) NOT NULL, + `multiple` bool NOT NULL, + `awarded_count` integer UNSIGNED NOT NULL, + UNIQUE (`name`, `type`) +) +; +CREATE TABLE `award` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `badge_id` integer NOT NULL, + `content_type_id` integer NOT NULL, + `object_id` integer UNSIGNED NOT NULL, + `awarded_at` datetime NOT NULL, + `notified` bool NOT NULL +) +; +ALTER TABLE `award` ADD CONSTRAINT user_id_refs_id_5d197ea32d83e9b6 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `award` ADD CONSTRAINT badge_id_refs_id_4237a025651af0e1 FOREIGN KEY (`badge_id`) REFERENCES `badge` (`id`); +ALTER TABLE `award` ADD CONSTRAINT content_type_id_refs_id_72f17e2d83bbde26 FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +CREATE TABLE `repute` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `positive` smallint NOT NULL, + `negative` smallint NOT NULL, + `question_id` integer NOT NULL, + `reputed_at` datetime NOT NULL, + `reputation_type` smallint NOT NULL, + `reputation` integer NOT NULL +) +; +ALTER TABLE `repute` ADD CONSTRAINT user_id_refs_id_fcf719405a426cd FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `repute` ADD CONSTRAINT question_id_refs_id_4749166abeb39c4e FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +CREATE TABLE `activity` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `activity_type` smallint NOT NULL, + `active_at` datetime NOT NULL, + `content_type_id` integer NOT NULL, + `object_id` integer UNSIGNED NOT NULL, + `is_auditted` bool NOT NULL +) +; +ALTER TABLE `activity` ADD CONSTRAINT user_id_refs_id_6015206347c8583f FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `activity` ADD CONSTRAINT content_type_id_refs_id_78877d15efa8edfd FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +CREATE TABLE `book` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `title` varchar(255) NOT NULL, + `short_name` varchar(255) NOT NULL, + `author` varchar(255) NOT NULL, + `price` numeric(6, 2) NOT NULL, + `pages` smallint NOT NULL, + `published_at` datetime NOT NULL, + `publication` varchar(255) NOT NULL, + `cover_img` varchar(255) NOT NULL, + `tagnames` varchar(125) NOT NULL, + `added_at` datetime NOT NULL, + `last_edited_at` datetime NOT NULL +) +; +ALTER TABLE `book` ADD CONSTRAINT user_id_refs_id_607b4cfdf0283c8d FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `book_author_info` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `book_id` integer NOT NULL, + `blog_url` varchar(255) NOT NULL, + `added_at` datetime NOT NULL, + `last_edited_at` datetime NOT NULL +) +; +ALTER TABLE `book_author_info` ADD CONSTRAINT user_id_refs_id_3781e2a5fbe1cfda FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `book_author_info` ADD CONSTRAINT book_id_refs_id_688c8f047c49bbf8 FOREIGN KEY (`book_id`) REFERENCES `book` (`id`); +CREATE TABLE `book_author_rss` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `book_id` integer NOT NULL, + `title` varchar(255) NOT NULL, + `url` varchar(255) NOT NULL, + `rss_created_at` datetime NOT NULL, + `added_at` datetime NOT NULL +) +; +ALTER TABLE `book_author_rss` ADD CONSTRAINT user_id_refs_id_1fd25dcf3596f741 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `book_author_rss` ADD CONSTRAINT book_id_refs_id_f64066171717121 FOREIGN KEY (`book_id`) REFERENCES `book` (`id`); +CREATE TABLE `forum_anonymousemail` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `key` varchar(32) NOT NULL, + `email` varchar(75) NOT NULL UNIQUE, + `isvalid` bool NOT NULL +) +; +CREATE TABLE `question_tags` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `tag_id` integer NOT NULL, + UNIQUE (`question_id`, `tag_id`) +) +; +ALTER TABLE `question_tags` ADD CONSTRAINT question_id_refs_id_35d758e3d99eb83a FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `question_tags` ADD CONSTRAINT tag_id_refs_id_3b0ddddfbc0346ad FOREIGN KEY (`tag_id`) REFERENCES `tag` (`id`); +CREATE TABLE `question_followed_by` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `question_id` integer NOT NULL, + `user_id` integer NOT NULL, + UNIQUE (`question_id`, `user_id`) +) +; +ALTER TABLE `question_followed_by` ADD CONSTRAINT question_id_refs_id_6ea9c52125c22aae FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +ALTER TABLE `question_followed_by` ADD CONSTRAINT user_id_refs_id_49cca2976d30712d FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `book_question` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `book_id` integer NOT NULL, + `question_id` integer NOT NULL, + UNIQUE (`book_id`, `question_id`) +) +; +ALTER TABLE `book_question` ADD CONSTRAINT book_id_refs_id_535ac8946a43c4d1 FOREIGN KEY (`book_id`) REFERENCES `book` (`id`); +ALTER TABLE `book_question` ADD CONSTRAINT question_id_refs_id_372b7e81c7aff6d8 FOREIGN KEY (`question_id`) REFERENCES `question` (`id`); +CREATE TABLE `django_authopenid_nonce` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `server_url` varchar(255) NOT NULL, + `timestamp` integer NOT NULL, + `salt` varchar(40) NOT NULL +) +; +CREATE TABLE `django_authopenid_association` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `server_url` longtext NOT NULL, + `handle` varchar(255) NOT NULL, + `secret` longtext NOT NULL, + `issued` integer NOT NULL, + `lifetime` integer NOT NULL, + `assoc_type` longtext NOT NULL +) +; +CREATE TABLE `django_authopenid_userassociation` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `openid_url` varchar(255) NOT NULL, + `user_id` integer NOT NULL UNIQUE +) +; +ALTER TABLE `django_authopenid_userassociation` ADD CONSTRAINT user_id_refs_id_f63a9e7163d208d FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `django_authopenid_userpasswordqueue` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL UNIQUE, + `new_password` varchar(30) NOT NULL, + `confirm_key` varchar(40) NOT NULL +) +; +ALTER TABLE `django_authopenid_userpasswordqueue` ADD CONSTRAINT user_id_refs_id_7f488ca76bcaaa4 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `django_authopenid_externallogindata` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `external_username` varchar(40) NOT NULL UNIQUE, + `external_session_data` longtext NOT NULL, + `user_id` integer NULL +) +; +ALTER TABLE `django_authopenid_externallogindata` ADD CONSTRAINT user_id_refs_id_462c0ee2c3e5e139 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `auth_permission` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `name` varchar(50) NOT NULL, + `content_type_id` integer NOT NULL, + `codename` varchar(100) NOT NULL, + UNIQUE (`content_type_id`, `codename`) +) +; +ALTER TABLE `auth_permission` ADD CONSTRAINT content_type_id_refs_id_6bc81a32728de91f FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`); +CREATE TABLE `auth_group` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `name` varchar(80) NOT NULL UNIQUE +) +; +CREATE TABLE `auth_user` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `username` varchar(30) NOT NULL UNIQUE, + `first_name` varchar(30) NOT NULL, + `last_name` varchar(30) NOT NULL, + `email` varchar(75) NOT NULL, + `password` varchar(128) NOT NULL, + `is_staff` bool NOT NULL, + `is_active` bool NOT NULL, + `is_superuser` bool NOT NULL, + `last_login` datetime NOT NULL, + `date_joined` datetime NOT NULL, + `is_approved` bool NOT NULL, + `email_isvalid` bool NOT NULL, + `email_key` varchar(32) NULL, + `reputation` integer UNSIGNED NOT NULL, + `gravatar` varchar(32) NOT NULL, + `gold` smallint NOT NULL, + `silver` smallint NOT NULL, + `bronze` smallint NOT NULL, + `questions_per_page` smallint NOT NULL, + `last_seen` datetime NOT NULL, + `real_name` varchar(100) NOT NULL, + `website` varchar(200) NOT NULL, + `location` varchar(100) NOT NULL, + `date_of_birth` date NULL, + `about` longtext NOT NULL +) +; +CREATE TABLE `auth_message` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `message` longtext NOT NULL +) +; +ALTER TABLE `auth_message` ADD CONSTRAINT user_id_refs_id_7837edc69af0b65a FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +CREATE TABLE `auth_group_permissions` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `group_id` integer NOT NULL, + `permission_id` integer NOT NULL, + UNIQUE (`group_id`, `permission_id`) +) +; +ALTER TABLE `auth_group_permissions` ADD CONSTRAINT group_id_refs_id_2ccea4c93cea63fe FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`); +ALTER TABLE `auth_group_permissions` ADD CONSTRAINT permission_id_refs_id_4de83ca7792de1 FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`); +CREATE TABLE `auth_user_groups` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `group_id` integer NOT NULL, + UNIQUE (`user_id`, `group_id`) +) +; +ALTER TABLE `auth_user_groups` ADD CONSTRAINT user_id_refs_id_1993cb70831107f1 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `auth_user_groups` ADD CONSTRAINT group_id_refs_id_321a8efef0ee9890 FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`); +CREATE TABLE `auth_user_user_permissions` ( + `id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, + `user_id` integer NOT NULL, + `permission_id` integer NOT NULL, + UNIQUE (`user_id`, `permission_id`) +) +; +ALTER TABLE `auth_user_user_permissions` ADD CONSTRAINT user_id_refs_id_166738bf2045483 FOREIGN KEY (`user_id`) REFERENCES `auth_user` (`id`); +ALTER TABLE `auth_user_user_permissions` ADD CONSTRAINT permission_id_refs_id_6d7fb3c2067e79cb FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`); +COMMIT; diff --git a/templates/404.html b/templates/404.html index 2fa38f99..227de3ae 100644 --- a/templates/404.html +++ b/templates/404.html @@ -33,7 +33,7 @@ diff --git a/templates/about.html b/templates/about.html index eaf0d591..6d5f3060 100644 --- a/templates/about.html +++ b/templates/about.html @@ -12,10 +12,35 @@
+ +

{% blocktrans %}CNPROG Q&A is a collaboratively edited question + and answer site created for the CNPROG community. + {% endblocktrans %} +

+

{% blocktrans %}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.{% endblocktrans %} +

+ +

{% blocktrans %}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.{% endblocktrans %} +

+

{% blocktrans %}No points are necessary to ask or answer the questions - so please - + join us!{% endblocktrans %} +

+

+ {% blocktrans %} + If you would like to find out more about this site - please see frequently asked questions. + {% endblocktrans %} +

{% endblock %} diff --git a/templates/answer_edit.html b/templates/answer_edit.html index 8baa7c1e..cd247a3c 100644 --- a/templates/answer_edit.html +++ b/templates/answer_edit.html @@ -1,14 +1,15 @@ {% extends "base.html" %} {% load i18n %} +{% load extra_tags %} {% block title %}{% spaceless %}{% trans "Edit answer" %}{% endspaceless %}{% endblock %} {% block forejs %} - - - - - - + + + + + + - - - - - + + + + + + + - - + +
  • - icon + icon http://{your-openid-url}
  • - icon + icon http://username.myopenid.com/
  • - icon + icon http://flickr.com/username/
  • - icon + icon http://technorati.com/people/technorati/username/
  • - icon + icon http://username.wordpress.com
  • - icon + icon http://username.blogspot.com/
  • - icon + icon http://username.livejournal.com
  • - icon + icon http://claimid.com/username
  • - icon + icon http://username.myvidoop.com/
  • - icon + icon http://username.pip.verisignlabs.com/
  • @@ -125,27 +127,17 @@

    {% trans 'Enter your login name and password' %}

    - {% if form1.errors %} -

    - {% trans "Sorry, looks like we have some errors:" %}
    -

      - {% if form1.username.errors %} -
    • {{ form1.username.errors|join:", " }}
    • - {% endif %} - {% if form1.password.errors %} -
    • {{ form1.password.errors|join:", " }}
    • - {% endif %} -
    -

    - {% endif %} -
    - {{form1.username}}
    - - {{form1.password}}
    + {% if form1.errors %} + {{form1.non_field_errors.as_ul}} + {% endif %} +
    +

    {% trans "Create account" %}
    - {% trans "I forgot my password" %} + {% trans "Forgot your password?" %}

    @@ -170,9 +162,9 @@ -

    - {% trans "Find out more" %} »
    - {% trans "Get OpenID" %} » +

    {% endblock%} diff --git a/templates/authopenid/signup.html b/templates/authopenid/signup.html index 5e405d3f..45dfb51b 100644 --- a/templates/authopenid/signup.html +++ b/templates/authopenid/signup.html @@ -1,53 +1,22 @@ -{% extends "base.html" %} +{% extends "base_content.html" %} {% load i18n %} {% block title %}{% spaceless %}{% trans "Signup" %}{% endspaceless %}{% endblock %} {% block content %} -
    -

    {% trans "Signup" %}

    - -
    -
    -

    {% trans "We support two types of user registration: conventional username/password, and" %} {% trans "the OpenID method" %}.

    - - {% if form.errors %} - -

    - {% trans "Sorry, looks like we have some errors" %}
    -

      - {% if form.username.errors %} -
    • {{ form.username.errors|join:", " }}
    • - {% endif %} - {% if form.email.errors %} -
    • {{ form.email.errors|join:", " }}
    • - {% endif %} - {% if form.password2.errors %} -
    • {{ form.password2.errors|join:", " }} -
    • - {% endif %} -
    -

    - {% endif %} +
    + {% trans "Create login name and password" %}
    -
    -
    - {% trans "Conventional registration" %} -

    {{ form.username }}
    - -

    {{ form.email }}
    -

    {{ form.password1 }}
    -

    {{ form.password2 }}
    - -
    -
    -
    - -
    -
    {{ form2.openid_url }}
    -
    -
    -
    +

    {% trans "Traditional signup info" %}

    +
    +
      + {{form.as_ul}} +
    +

    {% trans "receive updates motivational blurb" %}

    + {% include "edit_user_email_feeds_form.html" %} + +
    {% endblock %} diff --git a/templates/badges.html b/templates/badges.html index 4a1ba091..1902a3b0 100644 --- a/templates/badges.html +++ b/templates/badges.html @@ -19,7 +19,9 @@

    {% trans "Community gives you awards for your questions, answers and votes." %}
    - {% trans "Below is the list of available badges and number of times each type of badge has been awarded." %} + {% blocktrans %}Below is the list of available badges and number + of times each type of badge has been awarded. Give us feedback at {{feedback_faq_url}}. + {% endblocktrans %}

    {% for badge in badges %} @@ -32,7 +34,7 @@ {% endfor %}
    -  {{ badge.name }} ✕ {{ badge.awarded_count|intcomma }} +  {{ badge.name }} × {{ badge.awarded_count|intcomma }}

    {{ badge.description }} diff --git a/templates/base.html b/templates/base.html index 0f568f73..2b933c4a 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,6 +1,7 @@ {% load extra_filters %} +{% load extra_tags %} {% load i18n %} @@ -12,16 +13,16 @@ {% if settings.GOOGLE_SITEMAP_CODE %} {% endif %} - - + + - - - + + + - {% if messages %} + {% if user_messages %} @@ -57,8 +58,8 @@